From 6ae8b0070fef37cab517b3eb268b3443cab5b5f4 Mon Sep 17 00:00:00 2001 From: Nico Loesch Date: Mon, 27 Jul 2026 06:36:11 +0000 Subject: [PATCH 01/20] First pass implementation of anyllm wrapper --- README.md | 27 +- docs/index.md | 30 +- docs/providers.md | 53 + docs/reference.md | 8 +- docs/usage/installation.md | 10 +- mkdocs.yml | 1 + pyproject.toml | 13 +- src/omop_llm/__init__.py | 24 +- src/omop_llm/backend.py | 506 ++++++ src/omop_llm/capabilities.py | 41 + src/omop_llm/errors.py | 15 + src/omop_llm/interface/__init__.py | 3 - src/omop_llm/interface/client.py | 276 ---- src/omop_llm/interface/instructor_client.py | 153 -- src/omop_llm/providers/__init__.py | 15 + src/omop_llm/providers/base.py | 111 ++ src/omop_llm/providers/registry.py | 146 ++ src/omop_llm/providers/supported.py | 234 +++ src/omop_llm/structured.py | 179 +++ tests/__init__.py | 0 tests/conftest.py | 113 ++ tests/test_backend.py | 213 +++ tests/test_dummy.py | 3 - tests/test_oa_configurator_integration.py | 41 + tests/test_providers_ollama.py | 82 + tests/test_registry.py | 74 + tests/test_structured.py | 53 + uv.lock | 1538 ++++++------------- 28 files changed, 2440 insertions(+), 1522 deletions(-) create mode 100644 docs/providers.md create mode 100644 src/omop_llm/backend.py create mode 100644 src/omop_llm/capabilities.py create mode 100644 src/omop_llm/errors.py delete mode 100644 src/omop_llm/interface/__init__.py delete mode 100644 src/omop_llm/interface/client.py delete mode 100644 src/omop_llm/interface/instructor_client.py create mode 100644 src/omop_llm/providers/__init__.py create mode 100644 src/omop_llm/providers/base.py create mode 100644 src/omop_llm/providers/registry.py create mode 100644 src/omop_llm/providers/supported.py create mode 100644 src/omop_llm/structured.py create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/test_backend.py delete mode 100644 tests/test_dummy.py create mode 100644 tests/test_oa_configurator_integration.py create mode 100644 tests/test_providers_ollama.py create mode 100644 tests/test_registry.py create mode 100644 tests/test_structured.py diff --git a/README.md b/README.md index 30cd3cb..1c206e2 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,27 @@ # omop-llm -LLM interface for embedding and instructor tasks. Extended documentation can be found [here](https://AustralianCancerDataNetwork.github.io/omop-llm). + +Shared chat/embedding backend contract for the OMOP stack, built on [any-llm](https://github.com/mozilla-ai/any-llm). One typed `ModelBackend` interface (sync and async methods, both real), a closed set of supported providers (local: `ollama`, `llamacpp`, `vllm`; cloud: `openai`, `anthropic`, `gemini`), and explicit capability declarations instead of provider-name guessing. Extended documentation can be found [here](https://AustralianCancerDataNetwork.github.io/omop-llm). + +```python +from omop_llm import build_backend + +backend = build_backend(provider="ollama", model="llama3.2:8b", base_url="http://localhost:11434") + +# async +response = await backend.async_complete([{"role": "user", "content": "Hello"}]) + +# sync +response = backend.complete([{"role": "user", "content": "Hello"}]) +``` + +Or resolved from an `oa-configurator` stack config: + +```python +from oa_configurator import Resolver, load_stack_config +from omop_llm import build_backend_from_resolved + +resolved = Resolver(load_stack_config()).resolve_model("embed-default") +backend = build_backend_from_resolved(resolved) +``` + +See [docs/index.md](docs/index.md) for the full design (provider registry, capability model, structured extraction). diff --git a/docs/index.md b/docs/index.md index b369160..07d0477 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,19 +1,29 @@ # OMOP LLM Interface -`omop-llm` is designed as a simple LLM interface in the context of the OMOP CDM. Particularly, the wrapper currently exposes two interfaces: +`omop-llm` is the shared chat/embedding backend contract for the OMOP stack: a generic interface for calling a chat or embedding model, so packages that need one compose it in rather than each writing their own adapter, capability model, and provider vocabulary from scratch. -!!! warning +## What it provides - The backend is realised as an OpenAI client that would support a wide variety of models but we currently only support Ollama. Extension for this is planned in future releases. +- **`ModelBackend`** ([reference](reference.md#omop_llm.backend)): the one calling contract every consumer uses, built by `build_backend(provider, model, ...)`. It wraps a single [any-llm](https://github.com/mozilla-ai/any-llm) provider instance and exposes chat completion, embeddings, and structured extraction as methods on one object, each with a synchronous form and an `async_`-prefixed asynchronous form (`complete`/`async_complete`, `embed_texts`/`async_embed_texts`, and so on), so both async and fully synchronous consumers get a real, non-hand-rolled path. +- **A closed provider registry** ([reference](reference.md#omop_llm.providers)): `omop-llm` supports selected providers, not any-llm's full set shown in the [Providers overview](providers.md). Every supported provider is a real subclass of any-llm's own provider class, which is both the allow-list (nothing outside this set is reachable through `omop-llm`) and the seam for provider-specific behavior, such as Ollama's canonical model naming and embedding-dimension fast path (see `omop_llm.providers.supported`). +- **An explicit capability model** ([`ModelCapabilities`](reference.md#omop_llm.capabilities)): `streaming`/`embeddings`/`extended_thinking` come straight from any-llm's own provider metadata. `tool_use`/`structured_output` do not exist as any-llm capability flags at all (confirmed by reading its `ProviderMetadata` type directly), so `omop-llm` declares those two itself, meaning a caller requiring a capability the resolved backend does not have fails at construction time, not mid-run. +- **Structured single-object extraction** (`ModelBackend.extract`/`async_extract`): pulling one validated Pydantic object out of one LLM call. This is *not* the same problem as multi-turn agentic tool use (a model calling several real tools across several turns), which stays on `ModelBackend.complete(messages, tools=...)` directly. See [`omop_llm.structured`](reference.md#omop_llm.structured)'s own docstring for why the primary strategy is any-llm's native `response_format=` translation, and why `instructor`-based extraction (the optional fallback) is only offered for `openai`/`llamacpp`/`vllm`, not `ollama`/`anthropic`/`gemini`. -- **`LLMClient`**: Base client with the capacity to: - - obtain metadata information - - calculate embeddings on demand - - calculate semantic similarity -- **`InstructorClient`**: Child client of `LLMClient` to: - - provide an interface for the [`instructor`](https://python.useinstructor.com/) library with easy instantiation. - - chat completions using chat messages. +`omop-llm` depends on `oa-configurator` for config resolution. Two entry points: +1. `build_backend(provider, model, ...)` takes plain keyword arguments directly, and +2. `build_backend_from_resolved(resolved)` takes an `oa_configurator.ResolvedModel` (from `Resolver(stack).resolve_model(name)`) and does the field mapping for you. +`omop-llm` has no `PackageConfigBase` subclass of its own: it has no inherent specific model it needs. Each real consumer declares its own plain string field (e.g. `embedding_model: str = "embed-default"`) naming a `[models.*]` entry, and resolves it itself. + +## What it deliberately does not do + +- Install, launch, or manage any inference server (`ollama`, `llama-server`, `vllm`); that is Docker Compose / TRE deployment's job. +- Guess capabilities from a model name or provider string; see the capability model above. +- Reimplement `instructor`'s validate-and-retry loop, or any-llm's own per-provider wire translation; both are used directly, not duplicated. +- Own configuration parsing, TOML tables, or secrets; that is `oa-configurator`'s job. ## Documentation overview + - [Installation](usage/installation.md) +- [Providers](providers.md) +- [API Reference](reference.md) diff --git a/docs/providers.md b/docs/providers.md new file mode 100644 index 0000000..34c671a --- /dev/null +++ b/docs/providers.md @@ -0,0 +1,53 @@ +# Providers + +`omop-llm` intentionally supports a select collection of `any-llm`'s own providers in two categories: +- local (`ollama`, `llamacpp`, `vllm`), and +- cloud (`openai`, `anthropic`, `gemini`). + +See any-llm's own [provider reference](https://docs.mozilla.ai/any-llm/providers/) for capabilities of these providers. +Given the interface we have devised, future providers will be extended in the future to support other use-cases. + +## `base_url` and `api_key` + +Every one of these fields is optional on `build_backend(provider, model, base_url=None, api_key=None, ...)`. What "not set" resolves to differs per provider. +Resolution order for both, always: **explicit argument → the provider's own environment variable → a class-level default (if any)**. + +| Provider | Default `base_url` when not set | `api_key` required? | +|---|---|---| +| `ollama` | any-llm sets none; falls through to the official `ollama` SDK's own default (`http://localhost:11434`) | No | +| `llamacpp` | `http://127.0.0.1:8080/v1` (any-llm's own default, matches `llama-server`'s conventional port) | No | +| `vllm` | `http://localhost:8000/v1` (any-llm's own default, matches vLLM's conventional port) | No (any-llm's `VllmProvider` explicitly overrides key verification, since self-hosted vLLM commonly runs without auth) | +| `openai` | `https://api.openai.com/v1` (any-llm's own explicit default) | Yes | +| `anthropic` | any-llm sets none; falls through to the `anthropic` SDK's own default (the real Anthropic API) | Yes | +| `gemini` | any-llm sets none; falls through to the `google-genai` SDK's own default (the real Gemini API) | Yes | + +! note "The pattern" +Local providers either have no sensible universal default or a conventional local-dev default. You'll almost always want to set `base_url` explicitly once you're pointed at anything other than a single local instance on the default port. Cloud providers need no `base_url` at all for the normal case: leaving it unset resolves to the real vendor API, exactly as if you were calling that vendor's own SDK directly with no `base_url` override. You only set `base_url` for a cloud provider to point at something *other* than the vendor's real endpoint (an Azure OpenAI-style proxy, for instance). + +| Provider | Env var for `base_url` | Env var for `api_key` | +|---|---|---| +| `ollama` | `OLLAMA_HOST` | none (not required) | +| `llamacpp` | `LLAMACPP_API_BASE` | none (not required) | +| `vllm` | `VLLM_API_BASE` | `VLLM_API_KEY` (optional) | +| `openai` | `OPENAI_BASE_URL` | `OPENAI_API_KEY` | +| `anthropic` | `ANTHROPIC_BASE_URL` | `ANTHROPIC_API_KEY` | +| `gemini` | `GOOGLE_GEMINI_BASE_URL` | `GEMINI_API_KEY` or `GOOGLE_API_KEY` | + +## Capabilities per provider + +| Provider | `streaming` | `embeddings` | `extended_thinking` | `tool_use` | `structured_output` | Notes | +|---|---|---|---|---|---|---| +| `ollama` | ✅ | ✅ | ✅ | ✅ | ✅ | Canonical model names require an explicit tag (`llama3:8b`, not `llama3` or `llama3:latest`); see `omop_llm.providers.supported.OllamaProvider.canonical_model_name`. | +| `llamacpp` | ✅ | ✅ | ✅ | ✅ | ✅ | Covers both a local `llama-server` and a CUDA/TRE fallback profile; only `base_url` changes. | +| `vllm` | ✅ | ✅ | ✅ | ✅ | ✅ | Preferred TRE/NVIDIA backend. | +| `openai` | ✅ | ✅ | ❌ | ✅ | ✅ | | +| `anthropic` | ✅ | ❌ | ✅ | ✅ | ✅ | No embeddings API; `ModelBackend.embed_texts`/`async_embed_texts` refuse this provider. | +| `gemini` | ✅ | ✅ | ✅ | ✅ | ✅ | | + +`streaming`/`embeddings`/`extended_thinking` come from any-llm's own `get_provider_metadata()`, verified directly against the installed package for these six providers. `tool_use`/`structured_output` are declared by `omop-llm` itself, since any-llm tracks neither (see [`ModelCapabilities`](reference.md#omop_llm.capabilities)). + +## Adding a provider + +- Add a class to `omop_llm.providers.supported`, subclassing both `ProviderMixin` and any-llm's own provider class for it (imported under an `AnyLLM`-prefixed alias to avoid a name collision with the new class). +- `canonical_model_name` is a required override, not a default passthrough, on purpose: it forces a conscious decision about that provider's naming rules rather than silently inheriting "no transformation needed." +- `PROVIDER_REGISTRY` (`omop_llm.providers.registry`) picks the new class up automatically, no separate list to update. diff --git a/docs/reference.md b/docs/reference.md index 6d0e9a1..05aaed9 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -5,6 +5,10 @@ This reference is automatically generated from the source code. ::: omop_llm options: members: - - interface + - backend + - capabilities + - errors + - providers + - structured show_submodules: true - show_root_heading: true \ No newline at end of file + show_root_heading: true diff --git a/docs/usage/installation.md b/docs/usage/installation.md index 636673f..88d19e7 100644 --- a/docs/usage/installation.md +++ b/docs/usage/installation.md @@ -1,7 +1,11 @@ # Installation instructions -The package is currently only available from GitHub. +```bash +pip install omop-llm +``` + +Structured extraction via `instructor` (see [docs/index.md](../index.md)) is an optional extra: ```bash -pip install git+https://github.com/AustralianCancerDataNetwork/omop-llm.git -``` \ No newline at end of file +pip install "omop-llm[instructor]" +``` diff --git a/mkdocs.yml b/mkdocs.yml index f0e15cc..be8a61a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -30,6 +30,7 @@ theme: nav: - Home: index.md - Installation: usage/installation.md + - Providers: providers.md - "API Reference": reference.md plugins: diff --git a/pyproject.toml b/pyproject.toml index 20f7c38..6ad08db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,19 +9,23 @@ license = "Apache-2.0" readme = "README.md" requires-python = ">=3.12" dependencies = [ - "instructor>=1.13.0", - "prompt-spec>=0.1.4", - "openai", - "numpy", + "any-llm-sdk[ollama,gemini]>=1.22.0", + "httpx", + "oa-configurator>=0.2.0,<1.0.0", "pydantic", ] [project.optional-dependencies] +instructor = [ + "instructor>=1.13.0", +] dev = [ + "omop-llm[instructor]", "mypy>=1.19.1", "ty>=0.0.59", "ruff", "pytest>=9.0.2", + "pytest-asyncio>=1.0.0", "pytest-cov>=7.0.0", "types-pyyaml>=6.0.12.20250915", "mkdocs<2.0", @@ -50,6 +54,7 @@ python_files = ["test_*.py"] python_classes = ["Test*"] python_functions = ["test_*"] addopts = "-v --tb=short" +asyncio_mode = "auto" markers = [ "unit: Unit tests that do not require external API calls", "integration: Tests that hit live LLM endpoints (requires API keys)", diff --git a/src/omop_llm/__init__.py b/src/omop_llm/__init__.py index 6ec496a..91ccaf3 100644 --- a/src/omop_llm/__init__.py +++ b/src/omop_llm/__init__.py @@ -1,3 +1,23 @@ -from .interface import CHAT_MESSAGE_DICT, InstructorClient, LLMClient +from omop_llm.backend import ModelBackend as ModelBackend +from omop_llm.backend import build_backend as build_backend +from omop_llm.backend import build_backend_from_resolved as build_backend_from_resolved +from omop_llm.capabilities import ModelCapabilities as ModelCapabilities +from omop_llm.errors import OmopLlmError as OmopLlmError +from omop_llm.errors import UnsupportedCapabilityError as UnsupportedCapabilityError +from omop_llm.errors import UnsupportedProviderError as UnsupportedProviderError +from omop_llm.providers import canonical_model_name as canonical_model_name +from omop_llm.providers import capabilities_for as capabilities_for +from omop_llm.providers import supported_providers as supported_providers -__all__ = ["CHAT_MESSAGE_DICT", "InstructorClient", "LLMClient"] \ No newline at end of file +__all__ = [ + "ModelBackend", + "ModelCapabilities", + "OmopLlmError", + "UnsupportedCapabilityError", + "UnsupportedProviderError", + "build_backend", + "build_backend_from_resolved", + "canonical_model_name", + "capabilities_for", + "supported_providers", +] diff --git a/src/omop_llm/backend.py b/src/omop_llm/backend.py new file mode 100644 index 0000000..1de950b --- /dev/null +++ b/src/omop_llm/backend.py @@ -0,0 +1,506 @@ +"""``ModelBackend``: the one calling contract every consumer uses. + +A thin wrapper around a single, already-constructed any-llm provider +instance (an entry of :data:`omop_llm.providers.registry.PROVIDER_REGISTRY`). +Chat completion, embeddings, and structured extraction are all methods on +one object, gated by :class:`~omop_llm.capabilities.ModelCapabilities`, +rather than split across separate classes per modality. + +Every method has a synchronous form and an ``async_``-prefixed +asynchronous form (``complete``/``async_complete``, +``embed_texts``/``async_embed_texts``, and so on). This was a deliberate +choice, not an oversight: ``omop-emb``'s current codebase has no +``async``/``await`` anywhere (confirmed by inspecting it directly), so a +consumer that has to synchronously wait on a result needs a real sync +path, not one hand-rolled per call site. any-llm already supplies the sync +bridging for chat (``AnyLLM.completion()`` wraps ``acompletion()`` +internally) and for embeddings (``AnyLLM._embedding()``'s own default +implementation wraps ``aembedding()`` the same way, confirmed by reading +its source, and it is exactly what any-llm's own module-level +``embedding()`` function calls). Neither sync method is reimplemented +here; both are called directly. + +Consumers only ever see this class, never a raw any-llm provider instance. +If any-llm needed replacing, only this module's method bodies, and the +``providers/`` subclasses, would change; the public methods below would +not. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any + +from any_llm.any_llm import AnyLLM +from any_llm.types.completion import ChatCompletion, ReasoningEffort +from oa_configurator import ResolvedModel +from pydantic import BaseModel + +from omop_llm.capabilities import ModelCapabilities +from omop_llm.errors import UnsupportedCapabilityError +from omop_llm.providers.base import ProviderMixin +from omop_llm.providers.registry import ( + canonical_model_name, + capabilities_for, + provider_class_for, +) + + +@dataclass +class ModelBackend: + """One resolved, ready-to-call model. + + Built by :func:`build_backend`. Wraps a single constructed any-llm + provider instance and binds ``model``/``configuration`` to it, so + callers do not repeat them on every call. + + Parameters + ---------- + _client : AnyLLM + The constructed any-llm provider instance backing this backend. + model : str + The canonical model name or identifier passed to the underlying + provider. + capabilities : ModelCapabilities + What this resolved backend can actually do. + configuration : dict, optional + Default keyword arguments merged into every call, overridden by + any argument the caller passes explicitly. + _api_base : str, optional + The base URL this backend was constructed with, if any. Threaded + through to provider-specific fast paths such as + :meth:`~omop_llm.providers.supported.OllamaProvider.embedding_dimension_hint`. + """ + + _client: AnyLLM + model: str + capabilities: ModelCapabilities + configuration: dict[str, Any] = field(default_factory=dict) + _api_base: str | None = None + + @property + def provider(self) -> str: + """The provider key this backend was resolved to, e.g. ``"ollama"``. + + Read directly off ``_client``'s own any-llm ``PROVIDER_NAME`` class + attribute rather than stored separately at construction time, so + there is exactly one place this string is ever defined (see + :data:`omop_llm.providers.registry.PROVIDER_REGISTRY`, whose keys + are derived from the same attribute). + """ + return self._client.PROVIDER_NAME + + def _build_call_kwargs( + self, + *, + tools: list[dict[str, Any]] | None, + response_format: dict[str, Any] | type | None, + max_tokens: int | None, + temperature: float | None, + reasoning_effort: ReasoningEffort | None, + extra: dict[str, Any], + ) -> dict[str, Any]: + if extra.get("stream"): + raise NotImplementedError( + "ModelBackend does not support stream=True yet; " + "complete()/async_complete() are typed to always return a " + "ChatCompletion, not a chunk iterator" + ) + call_kwargs: dict[str, Any] = {**self.configuration, **extra} + if tools is not None: + call_kwargs["tools"] = tools + if response_format is not None: + call_kwargs["response_format"] = response_format + if max_tokens is not None: + call_kwargs["max_tokens"] = max_tokens + if temperature is not None: + call_kwargs["temperature"] = temperature + if reasoning_effort is not None: + call_kwargs["reasoning_effort"] = reasoning_effort + return call_kwargs + + def complete( + self, + messages: list[dict[str, Any]], + *, + tools: list[dict[str, Any]] | None = None, + response_format: dict[str, Any] | type | None = None, + max_tokens: int | None = None, + temperature: float | None = None, + reasoning_effort: ReasoningEffort | None = None, + **kwargs: Any, + ) -> ChatCompletion: + """Run one chat completion synchronously. See :meth:`async_complete` for parameters.""" + call_kwargs = self._build_call_kwargs( + tools=tools, response_format=response_format, max_tokens=max_tokens, + temperature=temperature, reasoning_effort=reasoning_effort, extra=kwargs, + ) + # call_kwargs is built dynamically, so its exact keys are not + # visible to the type checker at this call site, so it cannot pick + # a specific overload. stream is rejected above, so this is always + # the non-streaming ChatCompletion branch. + return self._client.completion( # ty: ignore[no-matching-overload] + model=self.model, messages=messages, **call_kwargs + ) + + async def async_complete( + self, + messages: list[dict[str, Any]], + *, + tools: list[dict[str, Any]] | None = None, + response_format: dict[str, Any] | type | None = None, + max_tokens: int | None = None, + temperature: float | None = None, + reasoning_effort: ReasoningEffort | None = None, + **kwargs: Any, + ) -> ChatCompletion: + """Run one chat completion. + + Parameters + ---------- + messages : list of dict + Chat history in OpenAI message format. + tools : list of dict, optional + Raw OpenAI-style tool schema list. any-llm normalizes tool-call + parsing per provider, so callers doing multi-turn agentic tool + use pass the same schema regardless of which provider is + resolved. Requires ``self.capabilities.tool_use``. + response_format : dict or type, optional + A raw JSON-schema dict, or a Pydantic model class. any-llm + translates a Pydantic class into each provider's own native + structured-output mechanism. See :meth:`extract`/:meth:`async_extract` + for a convenience method that validates and unwraps the + result. + max_tokens : int, optional + Maximum number of tokens to generate. + temperature : float, optional + Sampling temperature. + reasoning_effort : ReasoningEffort, optional + Requested extended-thinking effort, any-llm's own normalized + parameter across providers. Only meaningful when + ``self.capabilities.extended_thinking`` is ``True``; a provider + without reasoning support ignores it. + **kwargs : Any + Additional provider-specific arguments, passed through + unchanged. ``stream`` is rejected: this method always returns + a ``ChatCompletion``, never a chunk iterator, and streaming is + not designed or supported here yet. + + Returns + ------- + ChatCompletion + The completion response. + """ + call_kwargs = self._build_call_kwargs( + tools=tools, response_format=response_format, max_tokens=max_tokens, + temperature=temperature, reasoning_effort=reasoning_effort, extra=kwargs, + ) + return await self._client.acompletion( # ty: ignore[no-matching-overload] + model=self.model, messages=messages, **call_kwargs + ) + + def embed_texts(self, texts: list[str]) -> list[list[float]]: + """Embed a batch of texts synchronously. See :meth:`async_embed_texts` for parameters.""" + self._require_embeddings() + response = self._client._embedding( + model=self.model, inputs=texts, **self.configuration + ) + return [item.embedding for item in response.data] + + async def async_embed_texts(self, texts: list[str]) -> list[list[float]]: + """Embed a batch of texts. + + Parameters + ---------- + texts : list of str + Texts to embed. + + Returns + ------- + list of list of float + One embedding vector per input text, in the same order. + + Raises + ------ + UnsupportedCapabilityError + If ``self.capabilities.embeddings`` is ``False`` (e.g. for an + ``anthropic`` backend, which has no embeddings API). + """ + self._require_embeddings() + response = await self._client.aembedding( + model=self.model, inputs=texts, **self.configuration + ) + return [item.embedding for item in response.data] + + def _require_embeddings(self) -> None: + if not self.capabilities.embeddings: + raise UnsupportedCapabilityError( + f"backend for model {self.model!r} does not support embeddings" + ) + + def dimensions(self) -> int: + """Discover this model's embedding dimensionality synchronously. + + See :meth:`async_dimensions` for the three-tier lookup order. + + Returns + ------- + int + The embedding vector length. + """ + configured = self.configuration.get("embedding_dim") + if configured is not None: + return int(configured) + assert isinstance(self._client, ProviderMixin) + hint = self._client.embedding_dimension_hint(self.model, api_base=self._api_base) + if hint is not None: + return hint + [vector] = self.embed_texts(["dimension probe"]) + return len(vector) + + async def async_dimensions(self) -> int: + """Discover this model's embedding dimensionality. + + Three tiers: a configured override (``configuration["embedding_dim"]``), + then a provider-specific fast path (e.g. Ollama's + ``POST /api/show``), then a live probe (embed one short string and + measure the vector). + + Returns + ------- + int + The embedding vector length. + """ + configured = self.configuration.get("embedding_dim") + if configured is not None: + return int(configured) + assert isinstance(self._client, ProviderMixin) + hint = await self._client.async_embedding_dimension_hint(self.model, api_base=self._api_base) + if hint is not None: + return hint + [vector] = await self.async_embed_texts(["dimension probe"]) + return len(vector) + + def extract[T: BaseModel]( + self, + messages: list[dict[str, Any]], + response_model: type[T], + **kwargs: Any, + ) -> T: + """Extract one validated ``response_model`` instance synchronously. + + See :meth:`async_extract` for parameters. + """ + self._require_structured_output() + completion = self.complete(messages, response_format=response_model, **kwargs) + return self._unwrap_parsed(completion, response_model) + + async def async_extract[T: BaseModel]( + self, + messages: list[dict[str, Any]], + response_model: type[T], + **kwargs: Any, + ) -> T: + """Extract one validated ``response_model`` instance from a chat call. + + A thin convenience method built on :meth:`async_complete` with + ``response_format=response_model``: checks that a parsed instance + actually came back, and unwraps it. See :mod:`omop_llm.structured` + for ``extract_with_retry``, a separate, optional fallback for + callers that want validate-and-retry resilience instead of relying + on native structured decoding. + + Parameters + ---------- + messages : list of dict + Chat history in OpenAI message format. + response_model : type of BaseModel + The Pydantic model to constrain and validate the response + against. + **kwargs : Any + Additional arguments forwarded to :meth:`async_complete`. + + Returns + ------- + BaseModel + A validated instance of ``response_model``. + + Raises + ------ + UnsupportedCapabilityError + If ``self.capabilities.structured_output`` is ``False``, or if + the provider accepted ``response_format`` but returned no + parsed instance. + """ + self._require_structured_output() + completion = await self.async_complete(messages, response_format=response_model, **kwargs) + return self._unwrap_parsed(completion, response_model) + + def _require_structured_output(self) -> None: + if not self.capabilities.structured_output: + raise UnsupportedCapabilityError( + f"backend for model {self.model!r} does not declare structured_output support" + ) + + @staticmethod + def _unwrap_parsed[T: BaseModel](completion: ChatCompletion, response_model: type[T]) -> T: + message = completion.choices[0].message + parsed = getattr(message, "parsed", None) + if parsed is None: + raise UnsupportedCapabilityError( + f"provider returned no parsed {response_model.__name__} instance " + "(response_format was accepted but not honored)" + ) + return parsed # type: ignore[no-any-return] + + def is_available(self, **kwargs: Any) -> bool: + """Check whether this backend can actually be reached, synchronously. + + Probes ``list_models`` against the resolved provider. Swallows any + error and reports ``False`` rather than raising, since the point of + a health check is to answer "can I use this," not to propagate the + specific failure. + + Parameters + ---------- + **kwargs : Any + Forwarded to the underlying ``list_models`` call, e.g. + ``timeout=2.0``. + + Returns + ------- + bool + Whether listing models against this backend succeeded. + """ + try: + self._client.list_models(**kwargs) + except Exception: # noqa: BLE001 (deliberately broad: any failure means "unavailable") + return False + return True + + async def async_is_available(self, **kwargs: Any) -> bool: + """Check whether this backend can actually be reached. + + See :meth:`is_available` for details. + + Parameters + ---------- + **kwargs : Any + Forwarded to the underlying ``alist_models`` call, e.g. + ``timeout=2.0``. + + Returns + ------- + bool + Whether listing models against this backend succeeded. + """ + try: + await self._client.alist_models(**kwargs) + except Exception: # noqa: BLE001 (deliberately broad: any failure means "unavailable") + return False + return True + + +def build_backend( + provider: str, + model: str, + *, + base_url: str | None = None, + api_key: str | None = None, + configuration: dict[str, Any] | None = None, +) -> ModelBackend: + """Resolve a provider and model into a ready-to-call backend. + + Plain keyword arguments in, a :class:`ModelBackend` out, mirroring the + shape ``oa-configurator``'s own database resolution already uses + (``Resolver(stack).resolve_resource(name).create_engine(**kwargs)`` + returns a plain ``sqlalchemy.Engine``, no intermediate config object). + See :func:`build_backend_from_resolved` for the ``oa-configurator`` + integration built on top of this function. + + Canonicalizes ``model`` for the resolved provider (see + :func:`omop_llm.providers.registry.canonical_model_name`), so a + :class:`ModelBackend`'s ``model`` attribute is always canonical. + + Parameters + ---------- + provider : str + A key in :data:`omop_llm.providers.registry.PROVIDER_REGISTRY`. + model : str + Raw model name or identifier; canonicalized before use. + base_url : str, optional + The base URL for this specific deployment of the provider. + api_key : str, optional + The API key for this specific deployment, if one is required. + configuration : dict, optional + Default keyword arguments merged into every call this backend + makes (e.g. ``max_tokens``, ``temperature``, ``embedding_dim``). + + Returns + ------- + ModelBackend + A backend ready to call, for example, :meth:`ModelBackend.complete` + or :meth:`ModelBackend.async_complete`. + + Raises + ------ + ValueError + If ``model`` cannot be made canonical for the resolved provider + (e.g. an Ollama name with no explicit tag). + """ + provider_class = provider_class_for(provider) + capabilities = capabilities_for(provider) + canonical_model = canonical_model_name(provider, model) + client = provider_class(api_key=api_key, api_base=base_url) + return ModelBackend( + _client=client, + model=canonical_model, + capabilities=capabilities, + configuration=dict(configuration) if configuration else {}, + _api_base=base_url, + ) + + +def build_backend_from_resolved(resolved: ResolvedModel) -> ModelBackend: + """Build a backend from an ``oa-configurator`` ``ResolvedModel``. + + The ``oa-configurator`` integration point: ``oa-configurator`` itself + knows nothing about ``omop-llm`` (its ``ResolvedModel`` is plain data, + the same way ``ResolvedResource`` is), so this glue lives here instead, + mirroring ``omop_alchemy.config.create_cdm_engine(resolved: ResolvedResource) -> sa.Engine``: + a consumer of ``oa-configurator`` takes its plain resolved output and + does its own construction from it. + + A typical caller (e.g. a package's own config module) does:: + + from oa_configurator import Resolver, load_stack_config + from omop_llm import build_backend_from_resolved + + stack = load_stack_config() + resolved = Resolver(stack).resolve_model(config.embedding_model) + backend = build_backend_from_resolved(resolved) + + Parameters + ---------- + resolved : ResolvedModel + A model resolved via ``oa_configurator.Resolver.resolve_model()``. + + Returns + ------- + ModelBackend + A backend ready to call, for example, :meth:`ModelBackend.complete` + or :meth:`ModelBackend.async_complete`. + + Raises + ------ + ValueError + If ``resolved.model`` cannot be made canonical for the resolved + provider (e.g. an Ollama name with no explicit tag). + """ + return build_backend( + provider=resolved.provider.provider, + model=resolved.model, + base_url=resolved.provider.base_url, + api_key=resolved.provider.api_key, + configuration=resolved.configuration, + ) diff --git a/src/omop_llm/capabilities.py b/src/omop_llm/capabilities.py new file mode 100644 index 0000000..a8e7d96 --- /dev/null +++ b/src/omop_llm/capabilities.py @@ -0,0 +1,41 @@ +"""What a resolved backend can actually do.""" + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True, slots=True) +class ModelCapabilities: + """Capability declaration for one provider. + + ``streaming``, ``embeddings``, and ``extended_thinking`` are read + directly from any-llm's own ``ProviderMetadata``. That data is accurate + per provider and not worth re-declaring by hand. + + ``tool_use`` and ``structured_output`` have no equivalent in any-llm: + it exposes no capability flag for either, for any provider (confirmed + by inspecting ``any_llm.types.provider.ProviderMetadata``). These two + are declared by omop_llm itself in ``providers.registry`` and must not + be inferred from any-llm's own introspection. + + Parameters + ---------- + streaming : bool + Whether the provider supports streaming completions. + embeddings : bool + Whether the provider supports the embeddings endpoint. + extended_thinking : bool + Whether the provider supports reasoning/extended-thinking output. + tool_use : bool + Whether the provider supports tool/function calling. + structured_output : bool + Whether the provider supports structured (schema-constrained) + output. + """ + + streaming: bool + embeddings: bool + extended_thinking: bool + tool_use: bool + structured_output: bool diff --git a/src/omop_llm/errors.py b/src/omop_llm/errors.py new file mode 100644 index 0000000..748bc66 --- /dev/null +++ b/src/omop_llm/errors.py @@ -0,0 +1,15 @@ +"""Exceptions raised by omop_llm.""" + +from __future__ import annotations + + +class OmopLlmError(RuntimeError): + """Base class for all omop_llm errors.""" + + +class UnsupportedProviderError(OmopLlmError): + """Raised when a provider key is not in omop_llm's supported registry.""" + + +class UnsupportedCapabilityError(OmopLlmError): + """Raised when a requested capability is not available on the resolved backend.""" diff --git a/src/omop_llm/interface/__init__.py b/src/omop_llm/interface/__init__.py deleted file mode 100644 index f738afb..0000000 --- a/src/omop_llm/interface/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .client import CHAT_MESSAGE_DICT as CHAT_MESSAGE_DICT -from .client import LLMClient as LLMClient -from .instructor_client import InstructorClient as InstructorClient diff --git a/src/omop_llm/interface/client.py b/src/omop_llm/interface/client.py deleted file mode 100644 index fee518a..0000000 --- a/src/omop_llm/interface/client.py +++ /dev/null @@ -1,276 +0,0 @@ -import logging -from dataclasses import dataclass, field -from typing import Any - -import numpy as np -import requests -from openai import OpenAI - -logger = logging.getLogger(__name__) - - -type CHAT_MESSAGE_DICT = dict[str, str] - - -class LLMClientError(RuntimeError): - """ - Custom exception for LLM Client runtime errors. - """ - - -@dataclass -class LLMClient: - """ - Base class for LLM clients. - - This class replicates the API of the OntoGPT LLMClient but serves as a - base for other implementations (e.g., InstructorClient). Relies on the - OpenAI client for core functionality. - - Parameters - ---------- - model : str - The name of the model to use (e.g., 'gpt-4', 'llama3'). - api_base : str - The base URL for the API endpoint. - api_key : str - The API key for authentication. - temperature : float, optional - The temperature parameter for generation. Default is 1.0. - system_message : str, optional - The default system message to prepend to chats. Default is "". - - Attributes - ---------- - _base_client : OpenAI - The initialized OpenAI client instance. - _embedding_dim : int or None - Cached embedding dimension size. - """ - - model: str - api_base: str - api_key: str = "ollama" # required by OpenAI client, ignored by Ollama - temperature: float = 1.0 - system_message: str = "" - embedding_batch_size: int = 32 - _base_client: OpenAI = field(init=False, repr=False) - _embedding_dim: int | None = field(init=False, default=None) - - def __post_init__(self) -> None: - logger.info(f"Initialising {self.__class__.__name__} for model={self.model}") - if self.api_key is None: - self.api_key = "ollama" # Default to "ollama" for compatibility, but can be overridden - - self._base_client = OpenAI( - base_url=self.api_base, - api_key=self.api_key, - ) - - @property - def embedding_dim(self) -> int: - """ - Retrieve the embedding dimension for the current model. - - If the dimension is not cached, it attempts to fetch it from the API. - Currently supports Ollama endpoints. - - Returns - ------- - int - The size of the embedding vector. - - Raises - ------ - ValueError - If model information cannot be found in the Ollama response. - NotImplementedError - If the API base is not supported for automatic dimension retrieval. - """ - if self._embedding_dim is not None: - return self._embedding_dim - - if ( - "ollama" in self.api_base or - ( - ( - "localhost" in self.api_base or - "127.0.0.1" in self.api_base - ) and self.api_key == "ollama" - ) - ): - # Strip /v1 to access base Ollama API - ollama_url_without_v1 = self.api_base.replace("/v1", "") - requests_url = f"{ollama_url_without_v1}/api/show" - - response = requests.post(requests_url, json={"name": self.model}).json() - model_info = response.get("model_info", {}) - - if model_info: - # Find keys resembling 'embedding_length' - embedding_key = [key for key in model_info if "embedding_length" in key] - if len(embedding_key) == 1: - self._embedding_dim = int(model_info[embedding_key[0]]) - return self._embedding_dim - - raise ValueError(f"Model information not found in Ollama response: {response}") - else: - raise NotImplementedError("Embedding dimension retrieval not implemented for this API base") - - @property - def base_client(self) -> OpenAI: - return self._base_client - - def embeddings(self, text: str | list[str] | tuple[str, ...], batch_size: int | None = None) -> np.ndarray: - """ - Retrieve embeddings for the given text. - - Parameters - ---------- - text : str or List[str] - The input text or list of texts to embed. - batch_size : int, optional - The number of texts to process in a single API call. Default is 32. - - Returns - ------- - np.ndarray - A 2D numpy array containing the embeddings. - - Raises - ------ - AssertionError - If the base client has not been initialized. - """ - assert self.base_client is not None, "Base client should be initialized" - if batch_size is None: - batch_size = self.embedding_batch_size - - if isinstance(text, str): - text = (text, ) - elif isinstance(text, list): - text = tuple(text) - - batch_buffer = [] - - for batch_chunk_idx in range(0, len(text), batch_size): - logger.debug(f"Processing batch chunk from index {batch_chunk_idx} to {batch_chunk_idx + batch_size}") - batch_chunk = text[batch_chunk_idx:batch_chunk_idx + batch_size] - response = self.base_client.embeddings.create( - model=self.model, - input=batch_chunk, - ) - batch_buffer.extend([emb.embedding for emb in response.data]) - - return np.array(batch_buffer) - - def similarity( - self, - terms: str | list[str] | np.ndarray, - terms_to_match: str | list[str] | np.ndarray, - **kwargs: Any - ) -> np.ndarray: - """ - Calculate the cosine similarity between two sets of terms. - - This method handles inputs as strings, lists of strings, or pre-computed - numpy arrays of embeddings. - - Parameters - ---------- - terms : str, List[str], or np.ndarray - The source terms or embeddings. - terms_to_match : str, List[str], or np.ndarray - The target terms or embeddings to match against. - **kwargs : Any - Additional arguments passed to the embedding function if embedding is required. - - Returns - ------- - np.ndarray - A similarity matrix. - - Raises - ------ - ValueError - If inputs are not strings, lists, or numpy arrays. - """ - if isinstance(terms, str): - terms = [terms] - if isinstance(terms_to_match, str): - terms_to_match = [terms_to_match] - - # Process source terms - if isinstance(terms, np.ndarray): - terms_embeddings = terms - elif isinstance(terms, list): - terms_embeddings = self.embeddings(text=terms, **kwargs) - else: - raise TypeError("terms must be either a string, list of strings, or numpy array") - - # Process target terms - if isinstance(terms_to_match, np.ndarray): - terms_to_match_embeddings = terms_to_match - elif isinstance(terms_to_match, list): - terms_to_match_embeddings = self.embeddings(text=terms_to_match, **kwargs) - else: - raise TypeError("terms_to_match must be either a string, list of strings, or numpy array") - - return self.cosine_similarity(terms_embeddings, terms_to_match_embeddings) - - @staticmethod - def cosine_similarity(vecs_a: np.ndarray, vecs_b: np.ndarray) -> np.ndarray: - """ - Compute the cosine similarity between two matrices of vectors. - - Parameters - ---------- - vecs_a : np.ndarray - A 2D array of vectors (Shape: M x D). - vecs_b : np.ndarray - A 2D array of vectors (Shape: N x D). - - Returns - ------- - np.ndarray - The dot product of the normalized vectors (Shape: M x N). - - Notes - ----- - A small epsilon (1e-10) is added to the norms to prevent division by zero. - """ - assert vecs_a.ndim == 2 and vecs_b.ndim == 2, "Input vectors must be 2D arrays" - - norm_a = np.linalg.norm(vecs_a, axis=1, keepdims=True) - norm_b = np.linalg.norm(vecs_b, axis=1, keepdims=True) - - # Prevent division by zero - norm_a[norm_a == 0] = 1e-10 - norm_b[norm_b == 0] = 1e-10 - - vecs_a_norm = vecs_a / norm_a - vecs_b_norm = vecs_b / norm_b - - return np.dot(vecs_a_norm, vecs_b_norm.T) - - def euclidean_distance(self, text1: str, text2: str, **kwargs: Any) -> float: - """ - Calculate the Euclidean distance between embeddings of two texts. - - Parameters - ---------- - text1 : str - The first text string. - text2 : str - The second text string. - **kwargs : Any - Additional arguments passed to the embedding function. - - Returns - ------- - float - The Euclidean distance (L2 norm) between the two embedding vectors. - """ - a1 = self.embeddings(text1, **kwargs) - a2 = self.embeddings(text2, **kwargs) - return float(np.linalg.norm(np.array(a1) - np.array(a2))) \ No newline at end of file diff --git a/src/omop_llm/interface/instructor_client.py b/src/omop_llm/interface/instructor_client.py deleted file mode 100644 index 6524913..0000000 --- a/src/omop_llm/interface/instructor_client.py +++ /dev/null @@ -1,153 +0,0 @@ -import logging -from dataclasses import dataclass, field -from typing import Any - -import instructor -from prompt_spec import PromptTemplate -from pydantic import BaseModel - -from .client import LLMClient, LLMClientError - -logger = logging.getLogger(__name__) - - -type CHAT_MESSAGE_DICT = dict[str, str] - - -@dataclass -class InstructorClient(LLMClient): - """ - LLMClient implementation backed by pydantic-instructor. - - This client extends the base LLMClient to support structured outputs - via the `instructor` library. - - Parameters - ---------- - instructor_mode : instructor.Mode - The mode for the instructor client (e.g., JSON, TOOLS). Default is JSON. - - Attributes - ---------- - _client : Any - The initialized instructor client wrapper. - """ - - instructor_mode: instructor.Mode = instructor.Mode.JSON - _client: Any = field(init=False, repr=False) - - def __post_init__(self) -> None: - """ - Initialize the Instructor wrapper around the OpenAI client. - """ - super().__post_init__() - self._client = instructor.from_openai( - self.base_client, - mode=self.instructor_mode, - ) - - def complete[T: BaseModel]( - self, - messages: list[CHAT_MESSAGE_DICT], - response_model: type[T] | None = None, - show_prompt: bool = False, - **kwargs: Any, - ) -> str | T: - """ - Run a chat completion. - - If `response_model` is provided, structured output is returned based - on the Pydantic model. Otherwise, plain text is returned. - - Parameters - ---------- - messages : list of dict - The list of chat messages (e.g., `[{'role': 'user', 'content': '...'}]`). - response_model : type[T], optional - A Pydantic model class (T) to structure the response. - Must be a subclass of BaseModel. - show_prompt : bool, optional - If True, logs the rendered prompt before sending. Default is False. - **kwargs : Any - Additional arguments passed to `chat.completions.create`. - - Returns - ------- - Union[str, T] - The response string (if no model provided) or an instance of T (the Pydantic model). - - Raises - ------ - LLMClientError - If the completion request fails. - """ - if show_prompt: - # Note: render_prompt_messages only uses the 'messages' list. - rendered_text = self.render_prompt_messages(messages=messages) - logger.info(f"SENDING PROMPT:\n{rendered_text}") - - try: - result = self._client.chat.completions.create( - model=self.model, - messages=messages, - temperature=self.temperature, - response_model=response_model, - **kwargs, - ) - except Exception as e: - logger.error(f"Instructor completion failed: {e}") - raise LLMClientError("Instructor completion failed") from e - - if response_model is not None: - # result is already an instance of T here - return result - - return result.choices[0].message.content - - # TODO: The next two messages should be in prompt_spec! - def messages_from_prompt_template( - self, prompt_template: PromptTemplate | None, text: str - ) -> list[dict[str, str]]: - """ - Generate a list of messages from a PromptTemplate. - - Raises - ------ - NotImplementedError - This method is currently dropped for Template in LinkML. - """ - raise NotImplementedError("Method dropped for Template in LinkML") - - # Unreachable code preserved for reference/future implementation - # messages = [{"role": "system", "content": prompt_template.system}] if self.system_message else [] - # for example in prompt_template.examples: - # messages.append({"role": "user", "content": example.input}) - # messages.append({"role": "assistant", "content": example.output.model_dump_json(indent=2)}) - # messages.append({"role": "user", "content": text}) - # return messages - - def render_prompt_messages(self, messages: list[CHAT_MESSAGE_DICT]) -> str: - """ - Render a list of chat messages into a single string for logging or display. - - Parameters - ---------- - messages : list of dict - The chat history. - - Returns - ------- - str - A formatted string representation of the conversation. - """ - lines = [] - for msg in messages: - role_label = "System:" - if msg['role'] == "user": - role_label = "Input:" - elif msg['role'] == "assistant": - role_label = "Output:" - - lines.append(f"{role_label} {msg['content']}") - - return "\n".join(lines) diff --git a/src/omop_llm/providers/__init__.py b/src/omop_llm/providers/__init__.py new file mode 100644 index 0000000..2f9cc62 --- /dev/null +++ b/src/omop_llm/providers/__init__.py @@ -0,0 +1,15 @@ +from omop_llm.providers.registry import ( + PROVIDER_REGISTRY as PROVIDER_REGISTRY, +) +from omop_llm.providers.registry import ( + canonical_model_name as canonical_model_name, +) +from omop_llm.providers.registry import ( + capabilities_for as capabilities_for, +) +from omop_llm.providers.registry import ( + provider_class_for as provider_class_for, +) +from omop_llm.providers.registry import ( + supported_providers as supported_providers, +) diff --git a/src/omop_llm/providers/base.py b/src/omop_llm/providers/base.py new file mode 100644 index 0000000..9254254 --- /dev/null +++ b/src/omop_llm/providers/base.py @@ -0,0 +1,111 @@ +"""Shared base for omop_llm's own provider subclasses.""" + +from __future__ import annotations + +from abc import ABC, abstractmethod + + +class ProviderMixin(ABC): + """Marks a class as one of omop_llm's own provider subclasses. + + Declares the two capabilities any-llm does not track itself + (``TOOL_USE``, ``STRUCTURED_OUTPUT``) as class attributes, alongside + any-llm's own ``SUPPORTS_*`` flags on the sibling base class. Also + declares the two provider-specific hooks a resolved backend needs: + :meth:`canonical_model_name` and :meth:`embedding_dimension_hint`. + + Every provider omop_llm supports gets a real subclass built on this + mixin. Any-llm's own base class, ``AnyLLM``, is already an ``abc.ABC`` + with real abstract methods, so this mixin composes with it safely. + ``canonical_model_name`` is a required override, not a default + passthrough, so adding a new provider forces a deliberate decision + about its naming rules rather than silently inheriting "no + transformation needed." + + Attributes + ---------- + TOOL_USE : bool + Whether this provider supports tool/function calling. + STRUCTURED_OUTPUT : bool + Whether this provider supports structured (schema-constrained) + output. + """ + + TOOL_USE: bool + STRUCTURED_OUTPUT: bool + + @classmethod + @abstractmethod + def canonical_model_name(cls, name: str) -> str: + """Return the canonical form of a model name for this provider. + + The canonical form is the identifier used as a stable key + wherever a consumer persists model identity (e.g. ``omop-emb``'s + embedding registry), and the ``model`` value + :func:`~omop_llm.backend.build_backend` resolves to. Implementations + must be idempotent: calling this on an already-canonical name + returns the same string unchanged. + + Parameters + ---------- + name : str + Raw model name as supplied by the caller, e.g. ``"llama3"`` or + ``"text-embedding-3-small"``. + + Returns + ------- + str + The canonical model name for this provider. + + Raises + ------ + ValueError + If the name cannot be made canonical (e.g. an Ollama name with + no explicit tag). + """ + ... + + async def async_embedding_dimension_hint(self, model: str, *, api_base: str | None) -> int | None: + """Look up this model's embedding dimension via a provider-specific fast path. + + Default: no fast path available. Override where a provider + exposes model metadata directly (e.g. Ollama's ``POST /api/show``). + Used as the middle tier of :meth:`omop_llm.backend.ModelBackend.async_dimensions`, + between a configured override and a live embedding probe. + + Parameters + ---------- + model : str + The canonical model name. + api_base : str, optional + The resolved base URL this backend was constructed with. May + be ``None`` if it was not explicitly configured; providers + that need it to build a fast-path request should return + ``None`` in that case rather than guessing a default. + + Returns + ------- + int or None + The embedding dimension, or ``None`` if this provider has no + fast path for it. + """ + return None + + def embedding_dimension_hint(self, model: str, *, api_base: str | None) -> int | None: + """Synchronous counterpart to :meth:`async_embedding_dimension_hint`. + + Parameters + ---------- + model : str + The canonical model name. + api_base : str, optional + The resolved base URL this backend was constructed with. See + :meth:`async_embedding_dimension_hint`. + + Returns + ------- + int or None + The embedding dimension, or ``None`` if this provider has no + fast path for it. + """ + return None diff --git a/src/omop_llm/providers/registry.py b/src/omop_llm/providers/registry.py new file mode 100644 index 0000000..844e0b7 --- /dev/null +++ b/src/omop_llm/providers/registry.py @@ -0,0 +1,146 @@ +"""The closed set of providers omop_llm exposes. + +any-llm itself supports around fifty providers; omop_llm intentionally +supports six, matched to what this stack actually runs: local (``ollama``, +``llama-server`` via ``llamacpp``, ``vllm``) and cloud (``openai``, +``anthropic``, ``gemini``). See :mod:`omop_llm.providers.supported` for +the six classes themselves. A provider not defined there is structurally +unreachable through omop_llm's public API, regardless of what any-llm +itself supports. + +``PROVIDER_REGISTRY`` is built by discovering +:class:`~omop_llm.providers.base.ProviderMixin`'s own subclasses, +not by a second, separately-maintained list of classes: the set of +supported providers is defined exactly once, in +:mod:`omop_llm.providers.supported`, and this module can't drift out of +sync with it because it has nothing of its own to drift. One caveat that +comes with discovery over a class registry: any other direct subclass of +``ProviderMixin`` loaded into the process (e.g. a test fixture) +would also appear here. Nothing in this package does that; if a test ever +needs a fake provider, it should not subclass the mixin directly. +""" + +from __future__ import annotations + +from typing import Final + +from any_llm.any_llm import AnyLLM + +from omop_llm.capabilities import ModelCapabilities +from omop_llm.errors import UnsupportedProviderError +from omop_llm.providers import supported as _supported # noqa: F401 (required for PROVIDER_REGISTRY to be populated) +from omop_llm.providers.base import ProviderMixin + +PROVIDER_REGISTRY: Final[dict[str, type[AnyLLM]]] = { + cls.PROVIDER_NAME: cls + for cls in ProviderMixin.__subclasses__() + if issubclass(cls, AnyLLM) +} + + +def supported_providers() -> tuple[str, ...]: + """List the provider keys omop_llm will construct a backend for. + + Returns + ------- + tuple of str + The registered provider keys, sorted alphabetically. + """ + return tuple(sorted(PROVIDER_REGISTRY)) + + +def provider_class_for(provider_key: str) -> type[AnyLLM]: + """Look up a registered provider class. + + Parameters + ---------- + provider_key : str + A key expected to be in :data:`PROVIDER_REGISTRY`. + + Returns + ------- + type of AnyLLM + The provider class registered for ``provider_key``. + + Raises + ------ + UnsupportedProviderError + If ``provider_key`` is not registered. + """ + try: + return PROVIDER_REGISTRY[provider_key] + except KeyError: + raise UnsupportedProviderError( + f"{provider_key!r} is not a supported provider. " + f"Supported: {', '.join(supported_providers())}" + ) from None + + +def capabilities_for(provider_key: str) -> ModelCapabilities: + """Build the capability declaration for one registered provider. + + ``streaming``, ``embeddings``, and ``extended_thinking`` come straight + from any-llm's own ``get_provider_metadata()``. ``tool_use`` and + ``structured_output`` come from the class attributes each provider + subclass declares itself, since any-llm tracks neither. + + Parameters + ---------- + provider_key : str + A key expected to be in :data:`PROVIDER_REGISTRY`. + + Returns + ------- + ModelCapabilities + The capability declaration for this provider. + + Raises + ------ + UnsupportedProviderError + If ``provider_key`` is not registered. + """ + provider_class = provider_class_for(provider_key) + meta = provider_class.get_provider_metadata() + assert issubclass(provider_class, ProviderMixin) + return ModelCapabilities( + streaming=meta.streaming, + embeddings=meta.embedding, + extended_thinking=meta.reasoning, + tool_use=provider_class.TOOL_USE, + structured_output=provider_class.STRUCTURED_OUTPUT, + ) + + +def canonical_model_name(provider_key: str, name: str) -> str: + """Canonicalize a model name for one registered provider. + + Useful for deciding what to persist as a model's stable identity (e.g. + in a database) independently of building a full + :class:`~omop_llm.backend.ModelBackend`. :func:`~omop_llm.backend.build_backend` + also calls this internally, so a backend's ``model`` attribute is + always canonical without callers needing to remember to do it + themselves. + + Parameters + ---------- + provider_key : str + A key expected to be in :data:`PROVIDER_REGISTRY`. + name : str + Raw model name to canonicalize. + + Returns + ------- + str + The canonical model name for this provider. + + Raises + ------ + UnsupportedProviderError + If ``provider_key`` is not registered. + ValueError + If ``name`` cannot be made canonical for this provider (e.g. an + Ollama name with no explicit tag). + """ + provider_class = provider_class_for(provider_key) + assert issubclass(provider_class, ProviderMixin) + return provider_class.canonical_model_name(name) diff --git a/src/omop_llm/providers/supported.py b/src/omop_llm/providers/supported.py new file mode 100644 index 0000000..44c540d --- /dev/null +++ b/src/omop_llm/providers/supported.py @@ -0,0 +1,234 @@ +"""The six providers omop_llm supports, as explicit classes. + +any-llm itself supports around fifty providers (see its own reference: +https://docs.mozilla.ai/any-llm/providers/); omop_llm intentionally +supports six, matched to what this stack actually runs: local (``ollama``, +``llama-server`` via ``llamacpp``, ``vllm``) and cloud (``openai``, +``anthropic``, ``gemini``). See :mod:`omop_llm.providers.registry` for how +this closed set becomes the allow-list. + +Each class here subclasses both :class:`~omop_llm.providers.base.ProviderMixin` +(our contract: ``TOOL_USE``/``STRUCTURED_OUTPUT``, and the required +``canonical_model_name`` override) and any-llm's own provider class for +that provider. any-llm's own classes are imported under an ``AnyLLM``- +prefixed alias specifically so ours can keep the same short name +any-llm uses (``OllamaProvider``, not ``OmopLlmOllamaProvider``) without +colliding: the two are distinguished by which module they live in +(``omop_llm.providers`` vs. ``any_llm.providers.ollama.ollama``), not by a +repeated prefix on every reference to our own class. + +Written out explicitly rather than generated from a loop or factory, on +purpose: ``canonical_model_name`` is a required override specifically so +adding a provider forces a conscious decision about its naming rules, +which a generated class would silently default around. +""" + +from __future__ import annotations + +import httpx +from any_llm.providers.anthropic.anthropic import AnthropicProvider as AnyLLMAnthropicProvider +from any_llm.providers.gemini.gemini import GeminiProvider as AnyLLMGeminiProvider +from any_llm.providers.llamacpp.llamacpp import LlamacppProvider as AnyLLMLlamacppProvider +from any_llm.providers.ollama.ollama import OllamaProvider as AnyLLMOllamaProvider +from any_llm.providers.openai.openai import OpenaiProvider as AnyLLMOpenaiProvider +from any_llm.providers.vllm.vllm import VllmProvider as AnyLLMVllmProvider + +from omop_llm.providers.base import ProviderMixin + + +class OllamaProvider(ProviderMixin, AnyLLMOllamaProvider): + """Ollama. Native ``/api/chat`` via the official ``ollama`` SDK, not the OpenAI-compat shim. + + Verified by reading ``any_llm.providers.ollama.ollama`` directly: + ``_init_client`` constructs ``ollama.AsyncClient``, and + ``_convert_response_format`` maps an OpenAI-style ``response_format`` + onto Ollama's native ``format`` field. See :mod:`omop_llm.structured` + for why ``instructor``'s own Ollama support is deliberately not wired + in as an alternative. + + The one provider here with real behavior beyond any-llm's own, + ported from ``omop-emb/src/omop_emb/embeddings/embedding_providers.py``'s + ``OllamaProvider``: canonical model naming (rejects untagged names and + the mutable ``:latest`` tag) and a fast embedding-dimension lookup via + Ollama's native ``POST /api/show``, over ``httpx`` (already a + transitive dependency of ``any-llm-sdk``) since any-llm has no + equivalent call. + + No default ``base_url``: falls through to the official ``ollama`` + SDK's own default (``http://localhost:11434``). No ``api_key`` + required. + """ + + TOOL_USE = True + STRUCTURED_OUTPUT = True + + @classmethod + def canonical_model_name(cls, name: str) -> str: + """Require an explicit, immutable Ollama model tag. + + Rejects both untagged names and the mutable ``:latest`` tag, for + the same reason ``omop-emb`` already enforces this: ``:latest`` + can silently repoint after an ``ollama pull``, breaking + consistency between stored embeddings and new query embeddings. + + Parameters + ---------- + name : str + Model name with an explicit tag, e.g. ``"llama3:8b"`` or + ``"nomic-embed-text:v1.5"``. + + Returns + ------- + str + The input name, validated and stripped of whitespace. + + Raises + ------ + ValueError + If the name has no tag, or if the tag is ``:latest``. + """ + name = name.strip() + if ":" not in name: + raise ValueError( + f"Ollama model name {name!r} must include an explicit tag. " + f"Use a specific version (e.g. '{name}:8b') instead of relying on " + "the mutable ':latest' pointer. Running 'ollama pull " + f"{name}' can silently change which model version ':latest' " + "refers to, breaking consistency between stored embeddings " + "and new query embeddings." + ) + + _model_part, tag = name.rsplit(":", 1) + if tag == "latest": + raise ValueError( + f"Ollama model name {name!r} uses the mutable ':latest' tag. " + "':latest' can change between 'ollama pull' runs, breaking " + "consistency between stored embeddings and new query " + "embeddings. Use an explicit, immutable tag (e.g. " + "':8b')." + ) + return name + + def embedding_dimension_hint(self, model: str, *, api_base: str | None) -> int | None: + """See :meth:`omop_llm.providers.base.ProviderMixin.embedding_dimension_hint`.""" + if api_base is None: + return None + response = httpx.post(f"{api_base.rstrip('/')}/api/show", json={"name": model}).json() + return _extract_embedding_length(response) + + async def async_embedding_dimension_hint(self, model: str, *, api_base: str | None) -> int | None: + """See :meth:`omop_llm.providers.base.ProviderMixin.async_embedding_dimension_hint`.""" + if api_base is None: + return None + async with httpx.AsyncClient() as client: + response = await client.post(f"{api_base.rstrip('/')}/api/show", json={"name": model}) + return _extract_embedding_length(response.json()) + + +def _extract_embedding_length(response: dict) -> int | None: + model_info = response.get("model_info", {}) + if not model_info: + return None + embedding_keys = [key for key in model_info if "embedding_length" in key] + if len(embedding_keys) != 1: + return None + return int(model_info[embedding_keys[0]]) + + +class LlamacppProvider(ProviderMixin, AnyLLMLlamacppProvider): + """llama.cpp's ``llama-server``. Covers local dev and a CUDA/TRE fallback profile. + + The wire contract is the same either way; only ``base_url`` changes. + Defaults to ``http://127.0.0.1:8080/v1`` (any-llm's own default, + matching ``llama-server``'s conventional port) when ``base_url`` is + not given. No ``api_key`` required. + """ + + TOOL_USE = True + STRUCTURED_OUTPUT = True + + @classmethod + def canonical_model_name(cls, name: str) -> str: + """No transformation: ``llama-server`` model names have no mutable-tag concern.""" + return name + + +class VllmProvider(ProviderMixin, AnyLLMVllmProvider): + """vLLM, the preferred TRE/NVIDIA backend. + + Defaults to ``http://localhost:8000/v1`` (any-llm's own default, + matching vLLM's conventional port) when ``base_url`` is not given. + ``api_key`` is optional (confirmed: any-llm's ``VllmProvider`` + overrides key verification to make it so, since self-hosted vLLM + commonly runs without auth). + """ + + TOOL_USE = True + STRUCTURED_OUTPUT = True + + @classmethod + def canonical_model_name(cls, name: str) -> str: + """No transformation: vLLM model names have no mutable-tag concern.""" + return name + + +class OpenaiProvider(ProviderMixin, AnyLLMOpenaiProvider): + """OpenAI, e.g. ``gpt-4o``. + + Defaults to ``https://api.openai.com/v1`` (any-llm's own explicit + default) when ``base_url`` is not given, the real OpenAI API, same as + leaving ``base_url`` unset in the ``openai`` SDK directly. Requires + ``api_key`` (explicit, or the ``OPENAI_API_KEY`` environment + variable); raises if neither is set. + """ + + TOOL_USE = True + STRUCTURED_OUTPUT = True + + @classmethod + def canonical_model_name(cls, name: str) -> str: + """No transformation: OpenAI model names have no mutable-tag concern.""" + return name + + +class AnthropicProvider(ProviderMixin, AnyLLMAnthropicProvider): + """Anthropic (Claude). + + any-llm sets no explicit default ``base_url`` for this provider; it + falls through to the ``anthropic`` SDK's own default (the real + Anthropic API) when not given. Requires ``api_key`` (explicit, or the + ``ANTHROPIC_API_KEY`` environment variable). + + Note: any-llm's ``get_provider_metadata()`` reports ``embedding=False`` + for Anthropic (it has no embeddings API), so + :meth:`omop_llm.backend.ModelBackend.embed_texts` refuses this + provider. That is unrelated to ``TOOL_USE``/``STRUCTURED_OUTPUT`` + below: Anthropic's Messages API supports tool calling and tool-based + structured output regardless of the missing embeddings surface. + """ + + TOOL_USE = True + STRUCTURED_OUTPUT = True + + @classmethod + def canonical_model_name(cls, name: str) -> str: + """No transformation: Anthropic model names have no mutable-tag concern.""" + return name + + +class GeminiProvider(ProviderMixin, AnyLLMGeminiProvider): + """Gemini, e.g. ``gemini-2.5-pro``. + + any-llm sets no explicit default ``base_url`` for this provider; it + falls through to the ``google-genai`` SDK's own default (the real + Gemini API) when not given. Requires ``api_key`` (explicit, or the + ``GEMINI_API_KEY``/``GOOGLE_API_KEY`` environment variables). + """ + + TOOL_USE = True + STRUCTURED_OUTPUT = True + + @classmethod + def canonical_model_name(cls, name: str) -> str: + """No transformation: Gemini model names have no mutable-tag concern.""" + return name diff --git a/src/omop_llm/structured.py b/src/omop_llm/structured.py new file mode 100644 index 0000000..2b14347 --- /dev/null +++ b/src/omop_llm/structured.py @@ -0,0 +1,179 @@ +"""Optional fallback for structured extraction: ``instructor``'s validate-and-retry loop. + +The primary structured-extraction path lives on +:meth:`omop_llm.backend.ModelBackend.extract`/:meth:`~omop_llm.backend.ModelBackend.async_extract`, +built directly on any-llm's own ``response_format=`` +passthrough. This module is a separate, explicitly scoped alternative for +callers that specifically want resilience against a model returning +almost-valid JSON, kept out of ``backend.py`` so importing ``omop_llm`` +never requires the optional ``instructor`` dependency. + +It is *not* wired in as a silent alternative for every provider. This was +checked directly against ``instructor``'s own source +(``instructor.v2.auto_client._PROVIDER_BUILDERS``): + +- ``ollama`` is not safe to route through it: instructor's own Ollama + builder constructs a plain ``openai.AsyncOpenAI(base_url=".../v1")`` + client, the OpenAI-compat shim, not native ``/api/chat``, and picks + TOOLS-vs-JSON mode from a hardcoded model-name-substring list (the exact + "guess capability from the model name" anti-pattern this whole package + exists to retire). Using it for ``ollama`` would silently regress the + native-transport fidelity ``cava-nlp-shard`` depends on today. +- ``llamacpp``/``vllm`` have no dedicated builder in ``instructor`` at all + (its provider list tops out at roughly 23 hosted vendors). They are + reachable only by routing through instructor's ``openai`` builder with + an explicit ``base_url`` override, which is what + :func:`extract_with_retry`/:func:`async_extract_with_retry` do. +- ``anthropic``/``gemini`` are not offered here either: this module only + vouches for providers whose any-llm integration is already + OpenAI-compat-native, so there is no native-transport distinction to + lose. Requesting anything outside ``{"openai", "llamacpp", "vllm"}`` + raises :class:`~omop_llm.errors.UnsupportedCapabilityError` rather than + silently downgrading transport. +""" + +from __future__ import annotations + +from typing import Any + +from pydantic import BaseModel + +from omop_llm.errors import UnsupportedCapabilityError +from omop_llm.providers.supported import LlamacppProvider, OpenaiProvider, VllmProvider + +_INSTRUCTOR_SAFE_PROVIDERS = frozenset({ + OpenaiProvider.PROVIDER_NAME, + LlamacppProvider.PROVIDER_NAME, + VllmProvider.PROVIDER_NAME +}) + + +def _check_provider_and_base_url(provider: str, base_url: str | None) -> None: + if provider not in _INSTRUCTOR_SAFE_PROVIDERS: + raise UnsupportedCapabilityError( + f"instructor-based extraction is not offered for provider {provider!r}: " + f"only {sorted(_INSTRUCTOR_SAFE_PROVIDERS)} are confirmed to share any-llm's " + "transport for this provider, see the omop_llm.structured module docstring" + ) + if provider != "openai" and base_url is None: + raise ValueError( + f"base_url is required for provider={provider!r} " + "(without it, instructor's 'openai' builder would silently target " + "the real OpenAI API instead of your local/TRE server)" + ) + + +def _require_instructor() -> Any: + try: + import instructor + except ImportError as exc: + raise UnsupportedCapabilityError( + "instructor-based extraction requires the 'instructor' optional extra: " + "pip install 'omop-llm[instructor]'" + ) from exc + return instructor + + +def extract_with_retry[T: BaseModel]( + provider: str, + model: str, + messages: list[dict[str, Any]], + response_model: type[T], + *, + base_url: str | None = None, + api_key: str | None = None, + max_retries: int = 2, + **kwargs: Any, +) -> T: + """Extract via ``instructor``'s validate-and-retry loop, synchronously. + + See :func:`async_extract_with_retry` for parameters. + """ + _check_provider_and_base_url(provider, base_url) + instructor = _require_instructor() + + client_kwargs: dict[str, Any] = {"async_client": False} + if base_url is not None: + client_kwargs["base_url"] = base_url + if api_key is not None: + client_kwargs["api_key"] = api_key + + client = instructor.from_provider(f"openai/{model}", **client_kwargs) + return client.chat.completions.create( + messages=messages, + response_model=response_model, + max_retries=max_retries, + **kwargs, + ) + + +async def async_extract_with_retry[T: BaseModel]( + provider: str, + model: str, + messages: list[dict[str, Any]], + response_model: type[T], + *, + base_url: str | None = None, + api_key: str | None = None, + max_retries: int = 2, + **kwargs: Any, +) -> T: + """Extract via ``instructor``'s validate-and-retry loop. + + Requires the ``instructor`` optional extra + (``pip install 'omop-llm[instructor]'``). + + Parameters + ---------- + provider : str + One of ``{"openai", "llamacpp", "vllm"}`` (see module docstring). + ``llamacpp``/``vllm`` are routed through ``instructor``'s + ``openai`` builder with an explicit ``base_url``, which is + therefore required for those two, to avoid silently falling back + to instructor's real-OpenAI default endpoint. + model : str + The model name or identifier. + messages : list of dict + Chat history in OpenAI message format. + response_model : type of BaseModel + The Pydantic model to constrain and validate the response against. + base_url : str, optional + The provider's base URL. Required when ``provider`` is not + ``"openai"``. + api_key : str, optional + The API key for this provider, if one is required. + max_retries : int, optional + Number of validate-and-retry attempts. Default is 2. + **kwargs : Any + Additional arguments forwarded to instructor's + ``chat.completions.create``. + + Returns + ------- + BaseModel + A validated instance of ``response_model``. + + Raises + ------ + UnsupportedCapabilityError + If ``provider`` is not in ``{"openai", "llamacpp", "vllm"}``, or if + the ``instructor`` optional extra is not installed. + ValueError + If ``provider`` is not ``"openai"`` and ``base_url`` is not given. + """ + _check_provider_and_base_url(provider, base_url) + instructor = _require_instructor() + + client_kwargs: dict[str, Any] = {"async_client": True} + if base_url is not None: + client_kwargs["base_url"] = base_url + if api_key is not None: + client_kwargs["api_key"] = api_key + + client = instructor.from_provider(f"openai/{model}", **client_kwargs) + return await client.chat.completions.create( + messages=messages, + response_model=response_model, + max_retries=max_retries, + **kwargs, + ) 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..c749db1 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,113 @@ +"""Shared test fixtures. + +Contract tests here exercise omop_llm's own wrapper logic (kwargs merging, +response unpacking, capability gating, provider registration) against +either a fake ``AnyLLM`` client or the real any-llm provider classes +constructed offline (no network call happens at construction time, only +``.completion()``/``.acompletion()``/``._embedding()``/``.aembedding()`` +touch the network, and no test here calls those on a real provider). +Nothing in this suite requires a running Ollama/llama-server/vLLM instance +or a live API key. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any + +import pytest + +from omop_llm.providers.base import ProviderMixin + + +@dataclass +class FakeChatCompletionMessage: + content: str | None = None + parsed: Any = None + + +@dataclass +class FakeChoice: + message: FakeChatCompletionMessage + + +@dataclass +class FakeChatCompletion: + choices: list[FakeChoice] + + +@dataclass +class FakeEmbeddingItem: + embedding: list[float] + + +@dataclass +class FakeEmbeddingResponse: + data: list[FakeEmbeddingItem] + + +@dataclass +class FakeAnyLLMClient(ProviderMixin): + """Stands in for a constructed any-llm provider instance. + + Records every call it receives so tests can assert on exactly what + :class:`omop_llm.backend.ModelBackend` passed through, without needing + a real provider or network access. Method names match the real + ``AnyLLM`` surface :class:`~omop_llm.backend.ModelBackend` calls: + ``completion``/``acompletion`` for chat, ``_embedding``/``aembedding`` + for embeddings (matching any-llm's own asymmetric naming, confirmed by + reading ``any_llm/api.py`` and ``any_llm/any_llm.py`` directly), and + ``embedding_dimension_hint``/``async_embedding_dimension_hint`` for the + provider-specific dimension fast path. + + Subclasses :class:`~omop_llm.providers.base.ProviderMixin`, not just + ``AnyLLM``'s duck-typed surface: every real ``_client`` a + :class:`~omop_llm.backend.ModelBackend` is ever built with also is one, + since :data:`~omop_llm.providers.registry.PROVIDER_REGISTRY` only + contains classes that are both. Not doing so here would make this fake + a less accurate stand-in than the objects it replaces. + """ + + TOOL_USE = True + STRUCTURED_OUTPUT = True + + completion_response: FakeChatCompletion | None = None + embedding_response: FakeEmbeddingResponse | None = None + dimension_hint: int | None = None + completion_calls: list[dict[str, Any]] = field(default_factory=list) + embedding_calls: list[dict[str, Any]] = field(default_factory=list) + + @classmethod + def canonical_model_name(cls, name: str) -> str: + return name + + def completion(self, **kwargs: Any) -> FakeChatCompletion: + self.completion_calls.append(kwargs) + assert self.completion_response is not None + return self.completion_response + + async def acompletion(self, **kwargs: Any) -> FakeChatCompletion: + self.completion_calls.append(kwargs) + assert self.completion_response is not None + return self.completion_response + + def _embedding(self, **kwargs: Any) -> FakeEmbeddingResponse: + self.embedding_calls.append(kwargs) + assert self.embedding_response is not None + return self.embedding_response + + async def aembedding(self, **kwargs: Any) -> FakeEmbeddingResponse: + self.embedding_calls.append(kwargs) + assert self.embedding_response is not None + return self.embedding_response + + def embedding_dimension_hint(self, model: str, *, api_base: str | None) -> int | None: + return self.dimension_hint + + async def async_embedding_dimension_hint(self, model: str, *, api_base: str | None) -> int | None: + return self.dimension_hint + + +@pytest.fixture +def fake_client() -> FakeAnyLLMClient: + return FakeAnyLLMClient() diff --git a/tests/test_backend.py b/tests/test_backend.py new file mode 100644 index 0000000..c3f734b --- /dev/null +++ b/tests/test_backend.py @@ -0,0 +1,213 @@ +"""ModelBackend: kwargs merging, response unpacking, capability gating, sync/async parity. + +Uses ``FakeAnyLLMClient`` (see conftest.py) to test omop_llm's own wrapper +logic in isolation, plus real (offline-constructed, never called over the +network) provider instances to test ``build_backend``'s construction, +canonicalization, and capability-gate behavior. +""" + +from __future__ import annotations + +import pytest +from pydantic import BaseModel + +from omop_llm.backend import ModelBackend, build_backend +from omop_llm.capabilities import ModelCapabilities +from omop_llm.errors import UnsupportedCapabilityError +from tests.conftest import ( + FakeAnyLLMClient, + FakeChatCompletion, + FakeChatCompletionMessage, + FakeChoice, + FakeEmbeddingItem, + FakeEmbeddingResponse, +) + +_CAPS = ModelCapabilities( + streaming=True, embeddings=True, extended_thinking=True, tool_use=True, structured_output=True +) + + +class Answer(BaseModel): + value: str + + +def _backend(fake_client: FakeAnyLLMClient, **kwargs) -> ModelBackend: + return ModelBackend(_client=fake_client, model="m", capabilities=_CAPS, **kwargs) # type: ignore[arg-type] + + +@pytest.mark.parametrize("sync", [True, False]) +async def test_complete_merges_configuration_and_call_kwargs(fake_client: FakeAnyLLMClient, sync: bool) -> None: + fake_client.completion_response = FakeChatCompletion( + choices=[FakeChoice(message=FakeChatCompletionMessage(content="hi"))] + ) + backend = _backend(fake_client, configuration={"temperature": 0.0, "max_tokens": 8000}) + tools = [{"name": "lookup_item", "input_schema": {"type": "object"}}] + + if sync: + backend.complete([{"role": "user", "content": "hi"}], tools=tools, max_tokens=2048) + else: + await backend.async_complete([{"role": "user", "content": "hi"}], tools=tools, max_tokens=2048) + + [call] = fake_client.completion_calls + assert call["model"] == "m" + assert call["tools"] == tools + # explicit call-time max_tokens overrides the configured default + assert call["max_tokens"] == 2048 + # configured temperature carries through untouched + assert call["temperature"] == 0.0 + + +@pytest.mark.parametrize("sync", [True, False]) +async def test_complete_passes_response_format_through_untouched(fake_client: FakeAnyLLMClient, sync: bool) -> None: + fake_client.completion_response = FakeChatCompletion( + choices=[FakeChoice(message=FakeChatCompletionMessage(parsed={"ok": True}))] + ) + backend = _backend(fake_client) + + class Dummy: + pass + + if sync: + backend.complete([{"role": "user", "content": "hi"}], response_format=Dummy) + else: + await backend.async_complete([{"role": "user", "content": "hi"}], response_format=Dummy) + [call] = fake_client.completion_calls + assert call["response_format"] is Dummy + + +@pytest.mark.parametrize("sync", [True, False]) +async def test_embed_texts_unpacks_embedding_vectors(fake_client: FakeAnyLLMClient, sync: bool) -> None: + fake_client.embedding_response = FakeEmbeddingResponse( + data=[FakeEmbeddingItem(embedding=[0.1, 0.2]), FakeEmbeddingItem(embedding=[0.3, 0.4])] + ) + backend = _backend(fake_client) + backend.model = "embed-default" + + vectors = backend.embed_texts(["a", "b"]) if sync else await backend.async_embed_texts(["a", "b"]) + assert vectors == [[0.1, 0.2], [0.3, 0.4]] + [call] = fake_client.embedding_calls + assert call["model"] == "embed-default" + assert call["inputs"] == ["a", "b"] + + +@pytest.mark.parametrize("sync", [True, False]) +async def test_embed_texts_rejects_backend_without_embeddings(fake_client: FakeAnyLLMClient, sync: bool) -> None: + no_embed_caps = ModelCapabilities( + streaming=True, embeddings=False, extended_thinking=True, tool_use=True, structured_output=True + ) + backend = ModelBackend(_client=fake_client, model="m", capabilities=no_embed_caps) # type: ignore[arg-type] + with pytest.raises(UnsupportedCapabilityError): + if sync: + backend.embed_texts(["a"]) + else: + await backend.async_embed_texts(["a"]) + + +@pytest.mark.parametrize("sync", [True, False]) +async def test_dimensions_prefers_configured_override(fake_client: FakeAnyLLMClient, sync: bool) -> None: + backend = _backend(fake_client, configuration={"embedding_dim": 768}) + result = backend.dimensions() if sync else await backend.async_dimensions() + assert result == 768 + assert fake_client.embedding_calls == [] # no live probe needed + + +@pytest.mark.parametrize("sync", [True, False]) +async def test_dimensions_uses_provider_hint_before_live_probe(fake_client: FakeAnyLLMClient, sync: bool) -> None: + fake_client.dimension_hint = 1024 + backend = _backend(fake_client) + result = backend.dimensions() if sync else await backend.async_dimensions() + assert result == 1024 + assert fake_client.embedding_calls == [] # hint short-circuits the live probe + + +@pytest.mark.parametrize("sync", [True, False]) +async def test_dimensions_falls_back_to_live_probe(fake_client: FakeAnyLLMClient, sync: bool) -> None: + fake_client.embedding_response = FakeEmbeddingResponse(data=[FakeEmbeddingItem(embedding=[0.0] * 384)]) + backend = _backend(fake_client) + result = backend.dimensions() if sync else await backend.async_dimensions() + assert result == 384 + + +@pytest.mark.parametrize("sync", [True, False]) +async def test_extract_rejects_backend_without_structured_output(fake_client: FakeAnyLLMClient, sync: bool) -> None: + no_structured_caps = ModelCapabilities( + streaming=True, embeddings=True, extended_thinking=True, tool_use=True, structured_output=False + ) + backend = ModelBackend(_client=fake_client, model="m", capabilities=no_structured_caps) # type: ignore[arg-type] + fake_client.completion_response = FakeChatCompletion(choices=[FakeChoice(message=FakeChatCompletionMessage())]) + with pytest.raises(UnsupportedCapabilityError): + if sync: + backend.extract([{"role": "user", "content": "hi"}], Answer) + else: + await backend.async_extract([{"role": "user", "content": "hi"}], Answer) + + +@pytest.mark.parametrize("sync", [True, False]) +async def test_extract_unwraps_parsed_instance(fake_client: FakeAnyLLMClient, sync: bool) -> None: + fake_client.completion_response = FakeChatCompletion( + choices=[FakeChoice(message=FakeChatCompletionMessage(parsed=Answer(value="42")))] + ) + backend = _backend(fake_client) + result = ( + backend.extract([{"role": "user", "content": "hi"}], Answer) + if sync + else await backend.async_extract([{"role": "user", "content": "hi"}], Answer) + ) + assert result == Answer(value="42") + [call] = fake_client.completion_calls + assert call["response_format"] is Answer + + +@pytest.mark.parametrize("sync", [True, False]) +async def test_extract_raises_when_provider_did_not_honor_schema(fake_client: FakeAnyLLMClient, sync: bool) -> None: + fake_client.completion_response = FakeChatCompletion( + choices=[FakeChoice(message=FakeChatCompletionMessage(parsed=None))] + ) + backend = _backend(fake_client) + with pytest.raises(UnsupportedCapabilityError): + if sync: + backend.extract([{"role": "user", "content": "hi"}], Answer) + else: + await backend.async_extract([{"role": "user", "content": "hi"}], Answer) + + +def test_build_backend_constructs_offline_for_local_provider() -> None: + backend = build_backend( + provider="llamacpp", model="local-chat", base_url="http://localhost:8080/v1" + ) + assert backend.model == "local-chat" + assert backend.capabilities.tool_use is True + + +def test_provider_property_reads_from_the_constructed_client_not_a_stored_field() -> None: + backend = build_backend(provider="llamacpp", model="local-chat", base_url="http://localhost:8080/v1") + assert backend.provider == "llamacpp" + + +def test_build_backend_passes_configuration_through() -> None: + backend = build_backend( + provider="llamacpp", + model="local-chat", + base_url="http://localhost:8080/v1", + configuration={"temperature": 0.0}, + ) + assert backend.configuration == {"temperature": 0.0} + + +def test_build_backend_canonicalizes_the_model_name() -> None: + backend = build_backend(provider="ollama", model="llama3:8b", base_url="http://localhost:11434") + assert backend.model == "llama3:8b" + + +def test_build_backend_rejects_non_canonical_ollama_name() -> None: + with pytest.raises(ValueError, match="explicit tag"): + build_backend(provider="ollama", model="llama3", base_url="http://localhost:11434") + + +def test_build_backend_constructs_offline_for_embedding_capable_provider() -> None: + backend = build_backend( + provider="ollama", model="qwen3-embedding:0.6b", base_url="http://localhost:11434" + ) + assert backend.model == "qwen3-embedding:0.6b" + assert backend.capabilities.embeddings is True diff --git a/tests/test_dummy.py b/tests/test_dummy.py deleted file mode 100644 index 4981ce0..0000000 --- a/tests/test_dummy.py +++ /dev/null @@ -1,3 +0,0 @@ -def test_import(): - """Dummy test to ensure the package can be imported.""" - assert True \ No newline at end of file diff --git a/tests/test_oa_configurator_integration.py b/tests/test_oa_configurator_integration.py new file mode 100644 index 0000000..19e8fc5 --- /dev/null +++ b/tests/test_oa_configurator_integration.py @@ -0,0 +1,41 @@ +"""``build_backend_from_resolved``: the oa-configurator integration point. + +Constructs ``oa_configurator.ResolvedModel``/``ResolvedProvider`` directly +(no TOML file, no stack config needed) to test the field mapping in +isolation. Provider construction is offline throughout, no network access. +""" + +from __future__ import annotations + +from oa_configurator.resolver import ResolvedModel, ResolvedProvider + +from omop_llm.backend import build_backend_from_resolved + + +def test_maps_resolved_fields_onto_build_backend() -> None: + resolved = ResolvedModel( + name="local-chat", + provider=ResolvedProvider( + name="local-llamacpp", + provider="llamacpp", + base_url="http://localhost:8080/v1", + api_key=None, + ), + model="local-chat", + configuration={"max_tokens": 8000, "temperature": 0.0}, + ) + backend = build_backend_from_resolved(resolved) + assert backend.model == "local-chat" + assert backend.configuration == {"max_tokens": 8000, "temperature": 0.0} + assert backend.capabilities.tool_use is True + + +def test_canonicalizes_the_model_name() -> None: + resolved = ResolvedModel( + name="m", + provider=ResolvedProvider(name="p", provider="ollama", base_url="http://localhost:11434", api_key=None), + model="llama3:8b", + configuration={}, + ) + backend = build_backend_from_resolved(resolved) + assert backend.model == "llama3:8b" diff --git a/tests/test_providers_ollama.py b/tests/test_providers_ollama.py new file mode 100644 index 0000000..cd5fee8 --- /dev/null +++ b/tests/test_providers_ollama.py @@ -0,0 +1,82 @@ +"""OllamaProvider's real, provider-specific behavior. + +Ported from ``omop-emb/src/omop_emb/embeddings/embedding_providers.py``'s +``OllamaProvider``. ``httpx`` calls are monkeypatched, no test here touches +the network. +""" + +from __future__ import annotations + +from typing import Any + +import httpx +import pytest + +from omop_llm.providers.supported import OllamaProvider + + +@pytest.mark.parametrize("name", ["llama3:8b", "nomic-embed-text:v1.5"]) +def test_canonical_model_name_accepts_explicit_tags(name: str) -> None: + assert OllamaProvider.canonical_model_name(name) == name + + +def test_canonical_model_name_rejects_untagged_names() -> None: + with pytest.raises(ValueError, match="explicit tag"): + OllamaProvider.canonical_model_name("llama3") + + +def test_canonical_model_name_rejects_mutable_latest_tag() -> None: + with pytest.raises(ValueError, match="latest"): + OllamaProvider.canonical_model_name("llama3:latest") + + +def test_canonical_model_name_is_idempotent() -> None: + once = OllamaProvider.canonical_model_name("llama3:8b") + twice = OllamaProvider.canonical_model_name(once) + assert once == twice + + +def test_embedding_dimension_hint_returns_none_without_api_base() -> None: + provider = OllamaProvider(api_key=None, api_base="http://localhost:11434") + assert provider.embedding_dimension_hint("nomic-embed-text:v1.5", api_base=None) is None + + +def test_embedding_dimension_hint_parses_api_show_response(monkeypatch: pytest.MonkeyPatch) -> None: + def fake_post(url: str, json: dict[str, Any]) -> httpx.Response: + assert url == "http://localhost:11434/api/show" + assert json == {"name": "nomic-embed-text:v1.5"} + return httpx.Response(200, json={"model_info": {"nomic-embed-text.embedding_length": 768}}) + + monkeypatch.setattr(httpx, "post", fake_post) + provider = OllamaProvider(api_key=None, api_base="http://localhost:11434") + result = provider.embedding_dimension_hint("nomic-embed-text:v1.5", api_base="http://localhost:11434") + assert result == 768 + + +def test_embedding_dimension_hint_returns_none_when_metadata_is_missing(monkeypatch: pytest.MonkeyPatch) -> None: + def fake_post(url: str, json: dict[str, Any]) -> httpx.Response: + return httpx.Response(200, json={"model_info": {}}) + + monkeypatch.setattr(httpx, "post", fake_post) + provider = OllamaProvider(api_key=None, api_base="http://localhost:11434") + result = provider.embedding_dimension_hint("some-model:8b", api_base="http://localhost:11434") + assert result is None + + +async def test_async_embedding_dimension_hint_returns_none_without_api_base() -> None: + provider = OllamaProvider(api_key=None, api_base="http://localhost:11434") + result = await provider.async_embedding_dimension_hint("nomic-embed-text:v1.5", api_base=None) + assert result is None + + +async def test_async_embedding_dimension_hint_parses_api_show_response(monkeypatch: pytest.MonkeyPatch) -> None: + async def fake_post(self: httpx.AsyncClient, url: str, json: dict[str, Any]) -> httpx.Response: + assert url == "http://localhost:11434/api/show" + return httpx.Response(200, json={"model_info": {"nomic-embed-text.embedding_length": 768}}) + + monkeypatch.setattr(httpx.AsyncClient, "post", fake_post) + provider = OllamaProvider(api_key=None, api_base="http://localhost:11434") + result = await provider.async_embedding_dimension_hint( + "nomic-embed-text:v1.5", api_base="http://localhost:11434" + ) + assert result == 768 diff --git a/tests/test_registry.py b/tests/test_registry.py new file mode 100644 index 0000000..e566171 --- /dev/null +++ b/tests/test_registry.py @@ -0,0 +1,74 @@ +"""Provider registry: the allow-list, capability declarations, and canonicalization dispatch.""" + +from __future__ import annotations + +import pytest + +from omop_llm.errors import UnsupportedProviderError +from omop_llm.providers import ( + canonical_model_name, + capabilities_for, + provider_class_for, + supported_providers, +) + + +def test_supported_providers_is_the_closed_six() -> None: + assert supported_providers() == ( + "anthropic", + "gemini", + "llamacpp", + "ollama", + "openai", + "vllm", + ) + + +def test_unregistered_provider_rejected() -> None: + with pytest.raises(UnsupportedProviderError): + provider_class_for("azure") + + +@pytest.mark.parametrize( + ("provider", "expect_embeddings"), + [ + ("ollama", True), + ("llamacpp", True), + ("vllm", True), + ("openai", True), + ("anthropic", False), # Anthropic has no embeddings API + ("gemini", True), + ], +) +def test_capabilities_embeddings_match_any_llm_metadata( + provider: str, expect_embeddings: bool +) -> None: + caps = capabilities_for(provider) + assert caps.embeddings is expect_embeddings + + +def test_capabilities_tool_use_and_structured_output_are_declared_not_inferred() -> None: + for provider in supported_providers(): + caps = capabilities_for(provider) + # These two are never read off any-llm's own metadata; it has no + # such fields at all. Every registered provider currently declares + # both True; this just pins that it comes from our own registry. + assert caps.tool_use is True + assert caps.structured_output is True + + +def test_unregistered_provider_capabilities_rejected() -> None: + with pytest.raises(UnsupportedProviderError): + capabilities_for("bedrock") + + +def test_canonical_model_name_dispatches_to_the_right_provider() -> None: + # ollama has real transformation rules; every other registered provider + # is currently a no-op passthrough. + assert canonical_model_name("openai", "gpt-4o") == "gpt-4o" + assert canonical_model_name("ollama", "llama3:8b") == "llama3:8b" + + +def test_canonical_model_name_unregistered_provider_rejected() -> None: + with pytest.raises(UnsupportedProviderError): + canonical_model_name("bedrock", "some-model") diff --git a/tests/test_structured.py b/tests/test_structured.py new file mode 100644 index 0000000..a5d8769 --- /dev/null +++ b/tests/test_structured.py @@ -0,0 +1,53 @@ +"""Structured extraction fallback: the instructor allow-list guard rails. + +No test here calls a real provider or instructor client over the network, +these test the guard rails (which providers are refused, and why) +:mod:`omop_llm.structured`'s module docstring explains. +""" + +from __future__ import annotations + +import pytest +from pydantic import BaseModel + +from omop_llm.errors import UnsupportedCapabilityError +from omop_llm.structured import ( + _INSTRUCTOR_SAFE_PROVIDERS, + async_extract_with_retry, + extract_with_retry, +) + + +class Answer(BaseModel): + value: str + + +def test_instructor_safe_providers_is_the_openai_compat_native_set() -> None: + # Deliberately excludes ollama (instructor's own Ollama builder uses the + # OpenAI-compat shim, not native /api/chat, see structured.py's module + # docstring) and anthropic/gemini (not vouched for here at all). + assert _INSTRUCTOR_SAFE_PROVIDERS == frozenset({"openai", "llamacpp", "vllm"}) + + +@pytest.mark.parametrize("provider", ["ollama", "anthropic", "gemini", "azure"]) +def test_extract_with_retry_rejects_unsafe_providers(provider: str) -> None: + with pytest.raises(UnsupportedCapabilityError): + extract_with_retry(provider, "some-model", [{"role": "user", "content": "hi"}], Answer, base_url="http://x") + + +@pytest.mark.parametrize("provider", ["ollama", "anthropic", "gemini", "azure"]) +async def test_async_extract_with_retry_rejects_unsafe_providers(provider: str) -> None: + with pytest.raises(UnsupportedCapabilityError): + await async_extract_with_retry( + provider, "some-model", [{"role": "user", "content": "hi"}], Answer, base_url="http://x" + ) + + +def test_extract_with_retry_requires_base_url_for_self_hosted_providers() -> None: + with pytest.raises(ValueError, match="base_url"): + extract_with_retry("llamacpp", "local-chat", [{"role": "user", "content": "hi"}], Answer) + + +async def test_async_extract_with_retry_requires_base_url_for_self_hosted_providers() -> None: + with pytest.raises(ValueError, match="base_url"): + await async_extract_with_retry("llamacpp", "local-chat", [{"role": "user", "content": "hi"}], Answer) diff --git a/uv.lock b/uv.lock index f809420..b04d3fc 100644 --- a/uv.lock +++ b/uv.lock @@ -3,7 +3,9 @@ revision = 3 requires-python = ">=3.12" resolution-markers = [ "python_full_version >= '3.15'", - "python_full_version < '3.15'", + "python_full_version == '3.14.*'", + "python_full_version == '3.13.*'", + "python_full_version < '3.13'", ] [[package]] @@ -128,15 +130,6 @@ 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 = "alabaster" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, -] - [[package]] name = "annotated-doc" version = "0.0.4" @@ -156,35 +149,62 @@ wheels = [ ] [[package]] -name = "antlr4-python3-runtime" -version = "4.9.3" +name = "anthropic" +version = "0.120.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3e/38/7859ff46355f76f8d19459005ca000b6e7012f2f1ca597746cbcd1fbfe5e/antlr4-python3-runtime-4.9.3.tar.gz", hash = "sha256:f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b", size = 117034, upload-time = "2021-11-06T17:52:23.524Z" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "docstring-parser" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/5c/3331da4fc009d448008a50c78d86cc929e8c937cd1442245ce3f80561c4e/anthropic-0.120.0.tar.gz", hash = "sha256:6ba6007dc9b00365b20f6101a6618f5196ac1ceef81512e4b5cc0e7436d4975d", size = 1008042, upload-time = "2026-07-24T16:32:52.384Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/8a/8522bdf809e1f95f0d9c936540987a3f6afba01d2921a2bf488dedf836a8/anthropic-0.120.0-py3-none-any.whl", hash = "sha256:591bd531563ec7b63a1e138f5c11f14cb94edda99623b349c2ce2ece8e08b8a5", size = 1022602, upload-time = "2026-07-24T16:32:50.506Z" }, +] [[package]] -name = "anyio" -version = "4.14.2" +name = "any-llm-sdk" +version = "1.22.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "idna" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "anthropic" }, + { name = "httpx" }, + { name = "openai" }, + { name = "openresponses-types" }, + { name = "pydantic" }, + { name = "rich" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/de/a29738e1c8702a79a514f9521430fe77686fb9f9122980f4899a7baf58b5/any_llm_sdk-1.22.1.tar.gz", hash = "sha256:b07c6fcef6d7fc13e0f0419bc1464f9316b17e663c34382fb7722a906241f635", size = 164931, upload-time = "2026-07-22T15:43:29.12Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, + { url = "https://files.pythonhosted.org/packages/03/5a/85c558cc9fb6fd8a7fd3e7523c9c20545316190d22372026ddbf53cdc0c9/any_llm_sdk-1.22.1-py3-none-any.whl", hash = "sha256:950339b68ca1d99fb123c523d246140e1c947b0ff26e3fb9bc37194a49f3b509", size = 217209, upload-time = "2026-07-22T15:43:27.664Z" }, +] + +[package.optional-dependencies] +gemini = [ + { name = "google-cloud-storage" }, + { name = "google-genai" }, +] +ollama = [ + { name = "ollama" }, ] [[package]] -name = "arrow" -version = "1.4.0" +name = "anyio" +version = "4.14.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "python-dateutil" }, - { name = "tzdata" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b9/33/032cdc44182491aa708d06a68b62434140d8c50820a087fac7af37703357/arrow-1.4.0.tar.gz", hash = "sha256:ed0cc050e98001b8779e84d461b0098c4ac597e88704a655582b21d116e526d7", size = 152931, upload-time = "2025-10-18T17:46:46.761Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-py3-none-any.whl", hash = "sha256:749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205", size = 68797, upload-time = "2025-10-18T17:46:45.663Z" }, + { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, ] [[package]] @@ -246,53 +266,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, ] -[[package]] -name = "backports-zstd" -version = "1.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/b5/5a873da082bd08acd6a497f7aae224e94a7c27fa8f24488089cc50a16c84/backports_zstd-1.6.0.tar.gz", hash = "sha256:80a7859ffe70bf239d7a2ce15293bdeb5b4280ff7dc326ffab312b0e254dbb24", size = 1000009, upload-time = "2026-06-14T10:50:58.555Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/bb/009af3a9532d4cc66d5385391c512210fae32ab2442605f26aca1d8d2957/backports_zstd-1.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0466b14723f3b7697669c00ee66fe16e30e25636b286b0a923fa86fa3d8a753c", size = 437407, upload-time = "2026-06-14T10:49:50.155Z" }, - { url = "https://files.pythonhosted.org/packages/0c/76/f7c02efde81ebb9993586f9e435d2fd1191a6f806f640e4eeb8d004493ed/backports_zstd-1.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1d146926e997d2d3de8212bdcbf4985344a2622ca3bec458d8908000a84fd883", size = 363519, upload-time = "2026-06-14T10:49:51.383Z" }, - { url = "https://files.pythonhosted.org/packages/2e/5e/0cf66f12472fe3e082cc4134395a7e0b8746cfb30aabd74251ce8fafa9a7/backports_zstd-1.6.0-cp312-cp312-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:460fd6b3f338c659507ae36cfd6b58ac9942a2ff233c5cf574416dfec0451a84", size = 507756, upload-time = "2026-06-14T10:49:52.497Z" }, - { url = "https://files.pythonhosted.org/packages/03/95/7ed25c90369360f96f8bfa961540845e063377c32a43b775201af66a588c/backports_zstd-1.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c2b1f4a640c51130caa92cef5bf72bd3c3dbbcfbf814c37403aa0601b1811b0", size = 477578, upload-time = "2026-06-14T10:49:53.887Z" }, - { url = "https://files.pythonhosted.org/packages/e3/75/f16b1d3e33ca396525847c81d96e3de7bc74d2c6f9ca2ddee76b0c450697/backports_zstd-1.6.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:beb43e9885202c8d4f3762319ed4d5e98e197622afbff8439fbbdd81d08938b9", size = 583029, upload-time = "2026-06-14T10:49:55.132Z" }, - { url = "https://files.pythonhosted.org/packages/e1/2b/a17b111b631e1c79a0e570881c1a266c661b936585afa395435a458b1991/backports_zstd-1.6.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fbb746522ebfc11155f1cd688e2c48ef3d74125e38b63eabdaab068a055c3e88", size = 641741, upload-time = "2026-06-14T10:49:56.42Z" }, - { url = "https://files.pythonhosted.org/packages/6b/b2/d17b2722c636d64b4e77ddc68d8d0625719d39f94021be8719a218af4c0a/backports_zstd-1.6.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a99710fbb225d459d66def4dc2bb2cd4a9a0bdc8b799fc0621cfdd863be9c93", size = 495554, upload-time = "2026-06-14T10:49:57.652Z" }, - { url = "https://files.pythonhosted.org/packages/63/12/2853e8b6c03f03795b6548ea61f82cc104d4f7ff2523a04bc69f46984663/backports_zstd-1.6.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f69365ee2b836939137de024a302395a1cb8654fb6dc5ffef6381105259c8f87", size = 570027, upload-time = "2026-06-14T10:49:59.003Z" }, - { url = "https://files.pythonhosted.org/packages/18/aa/83f37b81f3b8c6ea035bf260ec374648bd59372894c02323dc9de3cbdf77/backports_zstd-1.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:66cf8038893c7708ec345ffb3ac63c775d10f430f323ac2f0334fdb6a397c57c", size = 483594, upload-time = "2026-06-14T10:50:00.49Z" }, - { url = "https://files.pythonhosted.org/packages/f5/6a/d77f8cd2ff642d3b3652c1ccab5b6583114dbf10f8cb0143531357c83998/backports_zstd-1.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e514c71ca72f3b56bd8fbda1a6a5b7d1100a2764b42a3c74a38841f25f9b00ab", size = 511206, upload-time = "2026-06-14T10:50:01.86Z" }, - { url = "https://files.pythonhosted.org/packages/56/b2/99a60fe4d1aac8053769d2463271d5df37a7c11c387072fdbb0b16aed7f7/backports_zstd-1.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7741e44f7938ec94f9a52678c8d19b7bc548522ffdc39c9e4481af8db545fa9a", size = 587416, upload-time = "2026-06-14T10:50:03.236Z" }, - { url = "https://files.pythonhosted.org/packages/ec/1e/a9c003fe4d14bd4bf671598d4c7dcc1cef51e3513d9d7111ba1d07b6f07b/backports_zstd-1.6.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:97e8a9674652496c7612b528085dd5a296c052a2edc466ca1bfb7b0b27820413", size = 567615, upload-time = "2026-06-14T10:50:04.524Z" }, - { url = "https://files.pythonhosted.org/packages/ec/b9/955bd604f692c550c7cb66d00bd7691ead5c86df8ebd23d7254eeaa90789/backports_zstd-1.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:23a793f2fed4dbf0517319759a2cded0b0dd8e8d3797fe30badd5693e320c175", size = 632269, upload-time = "2026-06-14T10:50:05.86Z" }, - { url = "https://files.pythonhosted.org/packages/18/d7/9f61f612f8a4193484c78a1f26db82a50141234189885113ef0085a8a961/backports_zstd-1.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b951113113ed4b8d173418a4f155c14b739dace626b3fa3f82be1831958d39e4", size = 500066, upload-time = "2026-06-14T10:50:07.446Z" }, - { url = "https://files.pythonhosted.org/packages/81/a3/19fb8c48d94139481c5ccaf2fb54c31b543fa635fd7bd7399aadd15752ac/backports_zstd-1.6.0-cp312-cp312-win32.whl", hash = "sha256:6430b34a2ae6fcc604672f4f913102563473d9a015bdca1ce8c95041cc1f2677", size = 291825, upload-time = "2026-06-14T10:50:08.762Z" }, - { url = "https://files.pythonhosted.org/packages/58/38/40ba081c6c71f0f22c64d3d54b912ad75a4e6812caa1397cbb15b5693b12/backports_zstd-1.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:08793876172551a930ce4d65c712cd516184d1a97070d4a1193e05bf0cf7040d", size = 329201, upload-time = "2026-06-14T10:50:09.979Z" }, - { url = "https://files.pythonhosted.org/packages/2b/6c/f7116dd2edc6f960545f0d8616939eae3a20031b3b6669697d4f9fd83b2e/backports_zstd-1.6.0-cp312-cp312-win_arm64.whl", hash = "sha256:03b7c59c71f7a597e2bcb3f8368371e9a660a1bdf1c37afc1f1ad1496a013c19", size = 291901, upload-time = "2026-06-14T10:50:11.198Z" }, - { url = "https://files.pythonhosted.org/packages/38/06/c430537d59c55d49bcd15ecf4b1aa965453219caad810a4f2b484816f4be/backports_zstd-1.6.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:2ace939e4d620e119423606f2d3d7115f8707733bf57f279ad9a9383f875986f", size = 400327, upload-time = "2026-06-14T10:50:12.446Z" }, - { url = "https://files.pythonhosted.org/packages/36/48/2f8323bb0e3ebba88b54877a2979afeb83983fb2ca572f09ad61aae2d3a0/backports_zstd-1.6.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:4c68a9ed2df0cca51d774c521e68a34d2e3d9ebfc687ef8096adfd4f345b551d", size = 454276, upload-time = "2026-06-14T10:50:13.667Z" }, - { url = "https://files.pythonhosted.org/packages/7c/39/87a665244a65f5b87a06b848c29a8cce07e91d59c5988ee2a32c0293a21c/backports_zstd-1.6.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:30576f49b82328ec8af16c11100efe52ca88526f71bbe100ef6b4e707dc13bf2", size = 357457, upload-time = "2026-06-14T10:50:14.906Z" }, - { url = "https://files.pythonhosted.org/packages/7f/8b/854d4a47bb8b7a48bfb2ed381c7b03a70efb4fc49f0e4a1509b38a2e1727/backports_zstd-1.6.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b4bddfcfb6679215d6f4dc5f79a1f9301af339480d70527a14b57a1f2e6b6cbf", size = 366139, upload-time = "2026-06-14T10:50:16.399Z" }, - { url = "https://files.pythonhosted.org/packages/8f/de/c3af43eb8df6f2581e157e18a3e0121eadb826055b2fde3f91ec188689cb/backports_zstd-1.6.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:65048ed08c5124f05ff9f355ab9703014bb2dbe7f8d9948ce193685b1775f442", size = 446683, upload-time = "2026-06-14T10:50:17.633Z" }, - { url = "https://files.pythonhosted.org/packages/5c/39/87cf3d883d386c10ac52f5322604fb9afdd204229f4c47d4a820a839b8ff/backports_zstd-1.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5918fc6b31437208721276964323933cd86077b8d5b469c59c1b3fd2c8220a05", size = 436869, upload-time = "2026-06-14T10:50:19.113Z" }, - { url = "https://files.pythonhosted.org/packages/5e/b6/9479e6f0f18824ad38e8d7dd85161ab0842a198be669421232925bb30960/backports_zstd-1.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4b6c8b02ab0ccb2431bb7bc238be91d158b308915e7b07937388e540466fe7e7", size = 363090, upload-time = "2026-06-14T10:50:20.302Z" }, - { url = "https://files.pythonhosted.org/packages/d9/74/a5e98fe108e17c91d9bc590a19e77f5d47d579e34d3f5bc098a949d6c27c/backports_zstd-1.6.0-cp313-cp313-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:711e6b98f8924e8b4a61ff97ab6321f33de024e1ed6a32f5123763aeda8459be", size = 507070, upload-time = "2026-06-14T10:50:21.536Z" }, - { url = "https://files.pythonhosted.org/packages/69/f5/392bb7dce7363b77bc5403060f418fad438b9cfdd3edd10d65cee7d8fd11/backports_zstd-1.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2ba9ac10fc393e5123a08802e0e895a107cb4a66b9973d2844dbd8a343111e59", size = 477200, upload-time = "2026-06-14T10:50:22.91Z" }, - { url = "https://files.pythonhosted.org/packages/e4/4d/dfb665806ba4f74bc48071d32006843b53568c4a17ff627a3061de5eaa09/backports_zstd-1.6.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2f723219335387d7546412d8141e0303590600949b4184a1391a0c6a3c756058", size = 582724, upload-time = "2026-06-14T10:50:24.28Z" }, - { url = "https://files.pythonhosted.org/packages/57/b2/beeca7393a8310debd82ee2f0ce5c1801e8d7cb673f7f226f4a0866ca238/backports_zstd-1.6.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:64b94d7a836568926a3309ff510c7f8261b881b341fd4992cabf4f0998878f8a", size = 643493, upload-time = "2026-06-14T10:50:25.736Z" }, - { url = "https://files.pythonhosted.org/packages/38/26/ce90e9eed6f25aaa4a4fa305a2aaf2d2ad81fd69de8eb248ddd91c80d1e0/backports_zstd-1.6.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e39258a09b1c7ca70b5e94a5c5ccfe4700b4250b8077cfeab31d0f79565d4c9b", size = 492190, upload-time = "2026-06-14T10:50:27.205Z" }, - { url = "https://files.pythonhosted.org/packages/17/9b/37b9b146df1f5452419a96071a7017cbac212ec9b137d7a88ca46dc2aa9e/backports_zstd-1.6.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:15b1aae0f64cd742df4bba1d989d0a09a6ec619202543fdba684640454541fd3", size = 567432, upload-time = "2026-06-14T10:50:28.386Z" }, - { url = "https://files.pythonhosted.org/packages/06/66/81b30991be83237529f36335ac3682bce26409064b906ac6122874575196/backports_zstd-1.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:25b5ddc789480072551af571a746e9500356b2aff0499861cf2ca07ea7431e68", size = 483021, upload-time = "2026-06-14T10:50:29.654Z" }, - { url = "https://files.pythonhosted.org/packages/49/2a/792c65dcc1e45eb0c1bdc012ee94b84867186bfe27a860d0813bd216f03b/backports_zstd-1.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a13cfa3410a75e4cb87abdb669aaf79da861cb79299159054ff8f77b9671bc40", size = 510596, upload-time = "2026-06-14T10:50:31.657Z" }, - { url = "https://files.pythonhosted.org/packages/1d/22/01b92a600505620e4cb5f20429e181f30458b7207ca8b52ca5ca6068c35f/backports_zstd-1.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:2ddab55a5f54dec8acfad68ef70f1c704fd21919990ddc238afbd6f496e61c6a", size = 587143, upload-time = "2026-06-14T10:50:32.868Z" }, - { url = "https://files.pythonhosted.org/packages/d8/60/4672f5110b9eb01388cc6225a739e3a5fcd749a63a9c4c1450a04fa27113/backports_zstd-1.6.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fa305a84087e10d7a85e8a8a3dcba8cdbda4868f2180173b264b7b488fd37c55", size = 565238, upload-time = "2026-06-14T10:50:34.173Z" }, - { url = "https://files.pythonhosted.org/packages/5c/3b/19928d60ea7d25820bf12ef88de74534ca85b56ff7cf13c1b0e74e3a3d7c/backports_zstd-1.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:df27b57d214a3124fbe4e933ef5a903d4567f154260d9aece8c797a987f2a205", size = 633970, upload-time = "2026-06-14T10:50:35.506Z" }, - { url = "https://files.pythonhosted.org/packages/df/97/c4cecb3e0ff53563ef9819f0395d919ceaae9c5147392ac23bac7afdb20f/backports_zstd-1.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:28fecd73459d74910ae1987ab84b7bef690d3dd860948430dd5555108b006daf", size = 496539, upload-time = "2026-06-14T10:50:37.015Z" }, - { url = "https://files.pythonhosted.org/packages/eb/f4/46b2f29d2938a80e56e61a19f11ab093f531a9f8cd0ec8eeaac1246bcd99/backports_zstd-1.6.0-cp313-cp313-win32.whl", hash = "sha256:3e689af303df287142770abe3a48bbefd24dab4a09da5807d0e1fa8c75bab026", size = 291451, upload-time = "2026-06-14T10:50:38.518Z" }, - { url = "https://files.pythonhosted.org/packages/d1/ad/b529f92166da61f496621345f95d2dc583c8ca5ac553c084a4ef6c12cd71/backports_zstd-1.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:b067b1ef9c8e41fb0882c828aa37829938b5c0dab067eca72b23fc24c563b9da", size = 329023, upload-time = "2026-06-14T10:50:39.742Z" }, - { url = "https://files.pythonhosted.org/packages/30/d8/6be904d20345fbebec583ca83676e01f30c76118b283eb666d8ec8291ca1/backports_zstd-1.6.0-cp313-cp313-win_arm64.whl", hash = "sha256:a838296f5b84c920172fb579cac894d255c1fc25457c7234613ddcfa385e49b7", size = 291636, upload-time = "2026-06-14T10:50:41.004Z" }, -] - [[package]] name = "backrefs" version = "8.0" @@ -329,43 +302,88 @@ wheels = [ ] [[package]] -name = "cfgraph" -version = "0.2.1" +name = "cffi" +version = "2.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "rdflib" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cb/51/3e7e021920cfe2f7d18b672642e13f7dc4f53545d530b52ee6533b6681ca/CFGraph-0.2.1.tar.gz", hash = "sha256:b57fe7044a10b8ff65aa3a8a8ddc7d4cd77bf511b42e57289cd52cbc29f8fe74", size = 2630, upload-time = "2018-11-20T15:27:28.69Z" } - -[[package]] -name = "chardet" -version = "7.4.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/19/b6/9df434a8eeba2e6628c465a1dfa31034228ef79b26f76f46278f4ef7e49d/chardet-7.4.3.tar.gz", hash = "sha256:cc1d4eb92a4ec1c2df3b490836ffa46922e599d34ce0bb75cf41fd2bf6303d56", size = 784800, upload-time = "2026-04-13T21:33:39.803Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/61/33/29de185079e6675c3f375546e30a559b7ddc75ce972f18d6e566cd9ea4eb/chardet-7.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:75d3c65cc16bddf40b8da1fd25ba84fca5f8070f2b14e86083653c1c85aee971", size = 874870, upload-time = "2026-04-13T21:33:05.977Z" }, - { url = "https://files.pythonhosted.org/packages/9c/2f/4c5af01fd1a7506a1d5375403d68925eac70289229492db5aa68b58103d8/chardet-7.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:29af5999f654e8729d251f1724a62b538b1262d9292cccaefddf8a02aae1ef6a", size = 854859, upload-time = "2026-04-13T21:33:07.381Z" }, - { url = "https://files.pythonhosted.org/packages/36/21/edb36ad5dfa48d7f8eed97ab43931ecdaa8c15166c21b1d614967e49d681/chardet-7.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:626f00299ad62dfe937058a09572beed442ccc7b58f87aa667949b20fd3db235", size = 875032, upload-time = "2026-04-13T21:33:08.741Z" }, - { url = "https://files.pythonhosted.org/packages/e5/59/a32a241d861cf180853a11c8e5a67641cb1b2af13c3a5ccce83ec07e2c9f/chardet-7.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9a4904dd5f071b7a7d7f50b4a67a86db3c902d243bf31708f1d5cde2f68239cb", size = 888283, upload-time = "2026-04-13T21:33:10.213Z" }, - { url = "https://files.pythonhosted.org/packages/87/2e/e1ee6a77abf3782c00e05b89c4d4328c8353bf9500661c4348df1dd68614/chardet-7.4.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5d2879598bc220689e8ce509fe9c3f37ad2fca53a36be9c9bd91abdd91dd364f", size = 879974, upload-time = "2026-04-13T21:33:11.448Z" }, - { url = "https://files.pythonhosted.org/packages/32/60/fca69c534602a7ced04280c952a246ad1edde2a6ca3a164f65d32ac41fe7/chardet-7.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:4b2799bd58e7245cfa8d4ab2e8ad1d76a5c3a5b1f32318eb6acca4c69a3e7101", size = 943973, upload-time = "2026-04-13T21:33:12.756Z" }, - { url = "https://files.pythonhosted.org/packages/7c/43/79ac9b4db5bc87020c9dbc419125371d80882d1d197e9c4765ba8682b605/chardet-7.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a9e4486df251b8962e86ea9f139ca235aa6e0542a00f7844c9a04160afb99aa9", size = 873769, upload-time = "2026-04-13T21:33:14.002Z" }, - { url = "https://files.pythonhosted.org/packages/55/5f/25bdec773905bff0ff6cf35ca73b17bd05593b4f87bd8c5fa43705f7167d/chardet-7.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4fbff1907925b0c5a1064cffb5e040cd5e338585c9c552625f30de6bc2f3107a", size = 853991, upload-time = "2026-04-13T21:33:15.564Z" }, - { url = "https://files.pythonhosted.org/packages/b4/07/a29380ee0b215d23d77733b5ad60c5c0c7969650e080c667acdf9462040d/chardet-7.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:365135eaf37ba65a828f8e668eb0a8c38c479dcbec724dc25f4dfd781049c357", size = 874024, upload-time = "2026-04-13T21:33:16.915Z" }, - { url = "https://files.pythonhosted.org/packages/a8/b1/3338e121cbd4c8a126b8ccb1061170c2ce51a53f678c502793ea49c6fd6d/chardet-7.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfc134b70c846c21ead8e43ada3ae1a805fff732f6922f8abcf2ff27b8f6493d", size = 887410, upload-time = "2026-04-13T21:33:18.368Z" }, - { url = "https://files.pythonhosted.org/packages/63/1c/44a9a9e0c59c185a5d307ceaeee8768afa1558f0a24f7a4b5fa11b67586b/chardet-7.4.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9acd9988a93e09390f3cd231201ea7166c415eb8da1b735928990ffc05cb9fbb", size = 879269, upload-time = "2026-04-13T21:33:20.377Z" }, - { url = "https://files.pythonhosted.org/packages/1b/b3/5d0e77ea774bd3224321c248880ea0c0379000ac5c2bb6d77609549de247/chardet-7.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:e1b98790c284ff813f18f7cf7de5f05ea2435a080030c7f1a8318f3a4f80b131", size = 944155, upload-time = "2026-04-13T21:33:21.694Z" }, - { url = "https://files.pythonhosted.org/packages/70/a8/bf0811d859e13801279a2ae64f37a408027b282f2047bc0001c75dd356ad/chardet-7.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d892d3dcd652fdef53e3d6327d39b17c0df40a899dfc919abaeb64c974497531", size = 872887, upload-time = "2026-04-13T21:33:23.328Z" }, - { url = "https://files.pythonhosted.org/packages/51/ac/b9d68ebddfe1b02c77af5bf81120e12b036b4432dc6af7a303d90e2bc38b/chardet-7.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:acc46d1b8b7d5783216afe15db56d1c179b9a40e5a1558bc13164c4fd20674c4", size = 853964, upload-time = "2026-04-13T21:33:24.724Z" }, - { url = "https://files.pythonhosted.org/packages/2a/81/17fa103ea9caf5d325a5e4051ab2ba65996fd66baa60b81ee41af1f54e10/chardet-7.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ac3bf11c645734a1701a3804e43eabd98851838192267d08c353a834ab79fea", size = 876006, upload-time = "2026-04-13T21:33:26.098Z" }, - { url = "https://files.pythonhosted.org/packages/c2/20/193faab46a68ea550587331a698c3dca8099f8901d10937c4443135c7ed9/chardet-7.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e3bd9f936e04bae89c254262af08d9e5b98f805175ba1e29d454e6cba3107b7", size = 887680, upload-time = "2026-04-13T21:33:27.49Z" }, - { url = "https://files.pythonhosted.org/packages/40/c6/94a3c673327392652ee8bdea9a45bc8a5f5365197a7387d68f0eed007115/chardet-7.4.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:27cc23da03630cdecc9aa81a895aa86629c211f995cd57651f0fbc280717bf93", size = 879865, upload-time = "2026-04-13T21:33:29.052Z" }, - { url = "https://files.pythonhosted.org/packages/b1/2c/cad8b5e3623a987f3c930b68e2bdd06cfc388cd91cd42ed05f1227701b73/chardet-7.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:b95c934b9ad59e2ba8abb9be49df70d3ad1b0d95d864b9fdb7588d4fa8bd921c", size = 939594, upload-time = "2026-04-13T21:33:31.391Z" }, - { url = "https://files.pythonhosted.org/packages/33/e0/d06e42fd6f02a58e5e227e5106587751cb38adcff0aaf949add744b78b6e/chardet-7.4.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c77867f0c1cb8bd819502249fcdc500364aedb07881e11b743726fa2148e7b6e", size = 889714, upload-time = "2026-04-13T21:33:32.772Z" }, - { url = "https://files.pythonhosted.org/packages/d4/ed/40d091954d48abea037baae6be8fb79905e5f78d34d12ea955132c7d8011/chardet-7.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cf1efeaf65a6ef2f5b9cc3a1df6f08ba2831b369ccaa4c7018eaf90aa757bb11", size = 872319, upload-time = "2026-04-13T21:33:34.427Z" }, - { url = "https://files.pythonhosted.org/packages/bb/77/82a46821dbfbdfe062710d2bf2ede13426304e3567a23c57d919c0c31630/chardet-7.4.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f3504c139a2ad544077dd2d9e412cd08b01786843d76997cd43bb6de311723c", size = 892021, upload-time = "2026-04-13T21:33:35.766Z" }, - { url = "https://files.pythonhosted.org/packages/49/57/42d30c562bda5b4a839766c1aad8d5856b798ad2a1c3247b72a679afec94/chardet-7.4.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457f619882ba66327d4d8d14c6c342269bdb1e4e1c38e8117df941d14d351b04", size = 902509, upload-time = "2026-04-13T21:33:37.096Z" }, - { url = "https://files.pythonhosted.org/packages/8c/6c/0a40afdb50a0fe041ab95553b835a8160b6cf0e81edf2ae2fe9f5224cbf9/chardet-7.4.3-py3-none-any.whl", hash = "sha256:1173b74051570cf08099d7429d92e4882d375ad4217f92a6e5240ccfb26f231e", size = 626562, upload-time = "2026-04-13T21:33:38.559Z" }, + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/5f/ff100cae70ebe9d8df1c01a00e510e45d9adb5c1fdda84791b199141de97/cffi-2.1.0.tar.gz", hash = "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9", size = 531036, upload-time = "2026-07-06T21:34:30.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/85/990925db5df586ec90beb97529c853497e7f85ba0234830447faf41c3057/cffi-2.1.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f", size = 184829, upload-time = "2026-07-06T21:32:44.324Z" }, + { url = "https://files.pythonhosted.org/packages/4b/92/e7bb136ad6b5352603732cf907ef862ca103f20f2031c1735a46300c20c9/cffi-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde", size = 184728, upload-time = "2026-07-06T21:32:45.683Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c0/d1ec30ffb370f748f2fb54425972bfef9871e0132e82fb589c46b6676049/cffi-2.1.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d", size = 214815, upload-time = "2026-07-06T21:32:48.557Z" }, + { url = "https://files.pythonhosted.org/packages/1b/dc/5620cf930688be01f2d673804291de757a934c90b946dbdc3d84130c2ea4/cffi-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7", size = 222429, upload-time = "2026-07-06T21:32:49.848Z" }, + { url = "https://files.pythonhosted.org/packages/4b/a4/77b53abbf7a1e0beb9637edbef2a94d15f9c822f591e85d439ffd91519a6/cffi-2.1.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b", size = 210315, upload-time = "2026-07-06T21:32:51.221Z" }, + { url = "https://files.pythonhosted.org/packages/58/0c/f528df19cc94b675087324d4760d9e6d5bfae97d6217aa4fac43de4f5fcc/cffi-2.1.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7", size = 208859, upload-time = "2026-07-06T21:32:52.512Z" }, + { url = "https://files.pythonhosted.org/packages/62/f2/c9522a81c32132799a1972c39f5c5f8b4c8b9f00488a23feaa6c06f07741/cffi-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66", size = 221844, upload-time = "2026-07-06T21:32:53.704Z" }, + { url = "https://files.pythonhosted.org/packages/6e/28/bd53988b9833e8f8ad539d26f4c07a6b3f6bcb1e9e02e7ca038250b3428d/cffi-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe", size = 225287, upload-time = "2026-07-06T21:32:54.907Z" }, + { url = "https://files.pythonhosted.org/packages/79/99/0d0fd37f055224085f42bbb2c022d002e17dde4a97972822327b07d84101/cffi-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b", size = 223681, upload-time = "2026-07-06T21:32:56.329Z" }, + { url = "https://files.pythonhosted.org/packages/b0/80/c138990aa2a70b1a269f6e06348729836d733d6f970867943f61d367f8cc/cffi-2.1.0-cp312-cp312-win32.whl", hash = "sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a", size = 175269, upload-time = "2026-07-06T21:32:57.777Z" }, + { url = "https://files.pythonhosted.org/packages/a8/eb/f636456ff21a83fc13c032b58cc5dde061691546ac79efa284b2989b7982/cffi-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384", size = 185881, upload-time = "2026-07-06T21:32:59.253Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/400ea43e721727dca8a65c4521390e9196757caba4a45643acb2b63271b8/cffi-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6", size = 180088, upload-time = "2026-07-06T21:33:02.278Z" }, + { url = "https://files.pythonhosted.org/packages/96/88/a996879e2eeccb815f6e3a5967b12a308257412acec882039d386bd2aa7b/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda", size = 194331, upload-time = "2026-07-06T21:33:03.697Z" }, + { url = "https://files.pythonhosted.org/packages/58/85/7ae00d5c8dd6266f4e944c3db630f3c5c9a98b61d469c714d848b1d8138a/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b", size = 196966, upload-time = "2026-07-06T21:33:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e9/45c3a76ad8d43ad9261f4c95436da61128d3ca545d72b9612c0ab5be0b1c/cffi-2.1.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a", size = 184795, upload-time = "2026-07-06T21:33:06.699Z" }, + { url = "https://files.pythonhosted.org/packages/84/4c/82f132cb4418ee6d953d982b19191e87e2a6372c8a4ce36e50b69d6ade4a/cffi-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea", size = 184746, upload-time = "2026-07-06T21:33:08.071Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1c/4ed5a0e5bdca6cbc275556de3328dd1b76fd0c11cc13c88fe66d1d8715f2/cffi-2.1.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db", size = 214747, upload-time = "2026-07-06T21:33:09.671Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a6/e879bb68cc23a2bc9ba8f4b7d8019f0c2694bad2ab6c4a3701d429439f58/cffi-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f", size = 222392, upload-time = "2026-07-06T21:33:10.896Z" }, + { url = "https://files.pythonhosted.org/packages/88/f6/01890cfd63c08f8eb96a8319b0443690197d240a8bd6346048cf7bde9190/cffi-2.1.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d", size = 210285, upload-time = "2026-07-06T21:33:12.251Z" }, + { url = "https://files.pythonhosted.org/packages/a6/cf/2b684132056f438567b61e19d690dd31cd0921ace051e0a458be6074369e/cffi-2.1.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0", size = 208801, upload-time = "2026-07-06T21:33:13.617Z" }, + { url = "https://files.pythonhosted.org/packages/6f/08/f2e7d62c460faae0926f2d6e423694aa409ced3bc1fe2927a0a6e5f05416/cffi-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224", size = 221808, upload-time = "2026-07-06T21:33:15.466Z" }, + { url = "https://files.pythonhosted.org/packages/38/37/04f54b8e63a02f3d908332c9effbf8c366167c6f733ed8a3d4f79b7e2a1e/cffi-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c", size = 225241, upload-time = "2026-07-06T21:33:16.869Z" }, + { url = "https://files.pythonhosted.org/packages/a9/d6/c72eecca433cd3e681c65ed313ab4835d9d4a379704d0f628a6a05f51c2e/cffi-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a", size = 223588, upload-time = "2026-07-06T21:33:18.239Z" }, + { url = "https://files.pythonhosted.org/packages/c6/4b/e706f67279140f92939da3475ad610df18bfd52d50f14953a8e5fede71d5/cffi-2.1.0-cp313-cp313-win32.whl", hash = "sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2", size = 175248, upload-time = "2026-07-06T21:33:19.799Z" }, + { url = "https://files.pythonhosted.org/packages/5a/47/59eb7975cb0e4ef0afa764ea945b29a5bb4537a9f771cb7d6c8a5dd74c95/cffi-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512", size = 185717, upload-time = "2026-07-06T21:33:21.47Z" }, + { url = "https://files.pythonhosted.org/packages/5a/af/34fee85c48f8d94efc8597bc09470c9dd274c145f1c12e0fbc6ab6d38d74/cffi-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f", size = 180114, upload-time = "2026-07-06T21:33:22.515Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f0/81478e482afa03f6d18dc8f2afb5edc45b3080853b634b5ed91961be0998/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a", size = 194142, upload-time = "2026-07-06T21:33:23.657Z" }, + { url = "https://files.pythonhosted.org/packages/7d/95/8de304305cd9204974b0ca051b86d307cafca13aa575a0ef1b44d92c0d8c/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3", size = 196819, upload-time = "2026-07-06T21:33:25.007Z" }, + { url = "https://files.pythonhosted.org/packages/20/71/7c8372d30e42415602ed9f268f7cfd66f1b855fed881ecd168bcb45dbc0b/cffi-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d", size = 184965, upload-time = "2026-07-06T21:33:26.605Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5c/584e626835f0375c928176c04137c96927165cb8733cdb3150ec04e5ee5e/cffi-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac", size = 184952, upload-time = "2026-07-06T21:33:27.823Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d2/065fcae1c73979fac8e054462478d0ff8a29c40cdc2ed7ea5676a061df53/cffi-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6", size = 222353, upload-time = "2026-07-06T21:33:29.178Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a5/e8bbb1ce5b3ac2f53ad6a10bde44318a5a8d99d4f4a000d44a6e39aeb3e4/cffi-2.1.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913", size = 210051, upload-time = "2026-07-06T21:33:30.534Z" }, + { url = "https://files.pythonhosted.org/packages/28/ed/c127d3ac36e899c965e3361357c3befacd6578c03f40125183e41c3b219e/cffi-2.1.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d", size = 208630, upload-time = "2026-07-06T21:33:31.753Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d7/97d3136f81db489ec8d1d67748c110d6c994268fd7528014aa9f2b085e4e/cffi-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5", size = 221593, upload-time = "2026-07-06T21:33:33.044Z" }, + { url = "https://files.pythonhosted.org/packages/d3/27/93195977168ee63aed233a1a0993a2178798654d1f4bddcdd321d6fd3b21/cffi-2.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce", size = 225146, upload-time = "2026-07-06T21:33:34.224Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c1/6dbd291ee2ae5a50a034aa057207081f545923bbf15dad4511e985aafff5/cffi-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326", size = 223240, upload-time = "2026-07-06T21:33:35.57Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6f/ade5ce9863a57992a6ea3d0d10d7e29b8749fc127204b3d493d667b2815f/cffi-2.1.0-cp314-cp314-win32.whl", hash = "sha256:1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd", size = 177723, upload-time = "2026-07-06T21:33:51.626Z" }, + { url = "https://files.pythonhosted.org/packages/41/de/92b9eeed4ae4a21d6fd9b2a2c8505cbed573299902ea73981cc13f7ff62c/cffi-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb", size = 187937, upload-time = "2026-07-06T21:33:53.403Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1a/cc6ae6c2913a03aab8898eee57963cf1035b8df5872ed8b9115fcc7e2be8/cffi-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804", size = 183001, upload-time = "2026-07-06T21:33:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/14/f0/134c00ce0779ec86dea2aa1aac69339c2741a8045072676763512363a2ea/cffi-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714", size = 188538, upload-time = "2026-07-06T21:33:36.792Z" }, + { url = "https://files.pythonhosted.org/packages/50/d8/3b86aba791cb610d24e8a3e1b2cd529e71fa15096b04e4d4e360049d4a4c/cffi-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376", size = 188230, upload-time = "2026-07-06T21:33:38.011Z" }, + { url = "https://files.pythonhosted.org/packages/14/d0/117dcd9209255ad8571fbc8c92ef32593a1d294dcec91ddc4e4db50606f2/cffi-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98", size = 223899, upload-time = "2026-07-06T21:33:39.514Z" }, + { url = "https://files.pythonhosted.org/packages/b6/3d/f20f8b886b254e3ad10e15cd4186d3aed49f3e6a35ab37aab9f8f25f7c03/cffi-2.1.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13", size = 211652, upload-time = "2026-07-06T21:33:40.851Z" }, + { url = "https://files.pythonhosted.org/packages/28/3b/fad54de07260b93ddeef4b96d0131d57ea900675df1d410ae1deee52d7a6/cffi-2.1.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d", size = 210755, upload-time = "2026-07-06T21:33:42.183Z" }, + { url = "https://files.pythonhosted.org/packages/cc/82/3d5c705acb7abbba9bbd7d79b8e62e0f25b6120eb7ae6ac49f1b721722fe/cffi-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056", size = 223933, upload-time = "2026-07-06T21:33:43.603Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d0/47e338384ab6b1004241002fa616301020cea4fc95f283506565d252f276/cffi-2.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4", size = 226749, upload-time = "2026-07-06T21:33:45.046Z" }, + { url = "https://files.pythonhosted.org/packages/70/25/65bd5b58ea4bfdfc15cde02cb5365f89ef8ab8b2adfb8fe5c4bd4233382f/cffi-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94", size = 225703, upload-time = "2026-07-06T21:33:46.374Z" }, + { url = "https://files.pythonhosted.org/packages/dc/78/aa01ac599a8a4322533d45a1f9bc93b338276d2d59dabbe7c6d92a775c81/cffi-2.1.0-cp314-cp314t-win32.whl", hash = "sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76", size = 182857, upload-time = "2026-07-06T21:33:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/b9/26/d00496b22de4d4228f32dde94ad996f350c8aad676d63bcca0743c8dea4d/cffi-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5", size = 194065, upload-time = "2026-07-06T21:33:48.953Z" }, + { url = "https://files.pythonhosted.org/packages/d5/dd/0c7dbf815a579ff005008a2d815a55d6bb047c349eef536d9dc53d3f0a8d/cffi-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8", size = 186404, upload-time = "2026-07-06T21:33:50.309Z" }, + { url = "https://files.pythonhosted.org/packages/55/c7/8c8c50cb11c6750051daf12164098a9a6f027ac4356967fd4d800a07f242/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c", size = 194121, upload-time = "2026-07-06T21:33:56.109Z" }, + { url = "https://files.pythonhosted.org/packages/99/e2/67680bf19a6b60d2bb7ff83baefa2a4c3d2d7dc0f3277034b802e1fc504c/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001", size = 196820, upload-time = "2026-07-06T21:33:57.288Z" }, + { url = "https://files.pythonhosted.org/packages/ed/da/4bbe583a3b3a5c8c60892124fe17f3fa3656523faf0d3484eae90f091853/cffi-2.1.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3", size = 184936, upload-time = "2026-07-06T21:33:58.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/4b/1f4c36ab273980d7aa75bb126ea4f8971f24a96108acad3a0a084028c57b/cffi-2.1.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc", size = 185045, upload-time = "2026-07-06T21:34:00.085Z" }, + { url = "https://files.pythonhosted.org/packages/ef/c3/ad299dc38f3583f8d916b299f028af418a9ec98bc695fcbebeae7420691c/cffi-2.1.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699", size = 222342, upload-time = "2026-07-06T21:34:01.814Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d8/df4543cc087245044ed02ef3ad8e0a26619d0075ac7a77a12dc81177851b/cffi-2.1.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022", size = 210073, upload-time = "2026-07-06T21:34:03.255Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/fac738d73728c6cea2a88a2883dca54892496cbba88a1dc1f2909cb8a6f5/cffi-2.1.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0", size = 208551, upload-time = "2026-07-06T21:34:04.433Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3f/0b04a700dd64f465c93020253a793a82c9b4dff9961f48facd0df945d9b8/cffi-2.1.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1", size = 221649, upload-time = "2026-07-06T21:34:06.157Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/b7379a5704c79eda57ce075869ba70a0368d1c850f803b3c0d078d39dcaf/cffi-2.1.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28", size = 225203, upload-time = "2026-07-06T21:34:07.489Z" }, + { url = "https://files.pythonhosted.org/packages/5a/02/d5e6c43ea85c41bda2a184a3418f195fe7cf602967a8d2b94e085b83deef/cffi-2.1.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629", size = 223263, upload-time = "2026-07-06T21:34:08.712Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d8/772b8259bf75749adffb1c546828978381fb516f60cf701f6c83daf60c85/cffi-2.1.0-cp315-cp315-win32.whl", hash = "sha256:0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6", size = 177696, upload-time = "2026-07-06T21:34:26.355Z" }, + { url = "https://files.pythonhosted.org/packages/2f/dd/afa2191fc6d57fedd26e5844a2fe2fcc0bbfa00961bbaa5a41e4921e7cca/cffi-2.1.0-cp315-cp315-win_amd64.whl", hash = "sha256:bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853", size = 187914, upload-time = "2026-07-06T21:34:27.58Z" }, + { url = "https://files.pythonhosted.org/packages/05/ef/6cd4f8c671517162379dc79cfae5aea9106bc38abb89628d5c16adf6a838/cffi-2.1.0-cp315-cp315-win_arm64.whl", hash = "sha256:8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda", size = 183004, upload-time = "2026-07-06T21:34:28.905Z" }, + { url = "https://files.pythonhosted.org/packages/11/b6/12fc55092817a5faa26fb8c40c7f9d662e11a46ee248c137aafc42517d92/cffi-2.1.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc", size = 188378, upload-time = "2026-07-06T21:34:09.926Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2e/cdac88979f295fde5daa69622c7d2111e56e7ceb94f211357fbe452339e4/cffi-2.1.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca", size = 188319, upload-time = "2026-07-06T21:34:11.101Z" }, + { url = "https://files.pythonhosted.org/packages/e0/27/1d0b408497e41a74795af122d7b603c418c5fed0171450f899afd04e594f/cffi-2.1.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d", size = 223904, upload-time = "2026-07-06T21:34:12.606Z" }, + { url = "https://files.pythonhosted.org/packages/8b/31/e115c985105dd7ffb32444505f18ceb874bb42d992af05d5dced7ecf1980/cffi-2.1.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8", size = 211554, upload-time = "2026-07-06T21:34:13.987Z" }, + { url = "https://files.pythonhosted.org/packages/5a/67/9e6e09409336d9e515c58367e7cfcf4f89df06ad25252675595a58eb59d5/cffi-2.1.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd", size = 210795, upload-time = "2026-07-06T21:34:15.972Z" }, + { url = "https://files.pythonhosted.org/packages/19/e5/d3cc82a4a0be7902af279c04181ad038449c096734464a5ae1de3e1401bd/cffi-2.1.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f", size = 223843, upload-time = "2026-07-06T21:34:17.509Z" }, + { url = "https://files.pythonhosted.org/packages/b9/65/b434abc97ce7cecc2c640fde160507c0ecc7e21544b483ba3325d2e2ea17/cffi-2.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc", size = 226773, upload-time = "2026-07-06T21:34:19.05Z" }, + { url = "https://files.pythonhosted.org/packages/b5/9f/d4dc66ca651eb1145a133314cda721abf13cfac3d28c4a0402263ae6ad75/cffi-2.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9", size = 225719, upload-time = "2026-07-06T21:34:20.576Z" }, + { url = "https://files.pythonhosted.org/packages/68/5a/e536c528bc8057496c360c0978559a2dc45653f89dd6151078aa7d8fca1a/cffi-2.1.0-cp315-cp315t-win32.whl", hash = "sha256:cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b", size = 182760, upload-time = "2026-07-06T21:34:22.059Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0b/0ffe8b82d3875bced5fa1e7986a7a46b748262a40ab7f60b475eb9fb1bb3/cffi-2.1.0-cp315-cp315t-win_amd64.whl", hash = "sha256:f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5", size = 193769, upload-time = "2026-07-06T21:34:23.589Z" }, + { url = "https://files.pythonhosted.org/packages/a0/17/1073b53b68c9b5ca6914adf5f8bf55aacc2d3be102418c90700160ea8605/cffi-2.1.0-cp315-cp315t-win_arm64.whl", hash = "sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210", size = 186405, upload-time = "2026-07-06T21:34:24.857Z" }, ] [[package]] @@ -520,29 +538,53 @@ wheels = [ ] [[package]] -name = "curies" -version = "0.14.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "pystow" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/51/df/e1e3c2f5130e13aa0443a68f44c86efb901f993ba45b85ed7eabbcdd20b7/curies-0.14.1.tar.gz", hash = "sha256:543942892d1da01ba59e63720d223d504d97908d5c2c83458f33af54b87c92b3", size = 72008, upload-time = "2026-07-03T12:07:09.601Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/ca/d0ac8050ed500f9cf839d07832ab2ea9f390a65916f420b5d7cf4b829e64/curies-0.14.1-py3-none-any.whl", hash = "sha256:04de52080536881108b52ca864e8d4096140a7c3f5f556d8c2836c8ff99adc8f", size = 81631, upload-time = "2026-07-03T12:07:10.582Z" }, -] - -[[package]] -name = "deprecated" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, +name = "cryptography" +version = "49.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", size = 4032100, upload-time = "2026-06-12T20:02:32.143Z" }, + { url = "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978, upload-time = "2026-06-12T20:01:21.305Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422, upload-time = "2026-06-12T20:01:48.566Z" }, + { url = "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503, upload-time = "2026-06-12T20:02:47.091Z" }, + { url = "https://files.pythonhosted.org/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", size = 5309779, upload-time = "2026-06-12T20:02:08.987Z" }, + { url = "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683, upload-time = "2026-06-12T20:02:03.335Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874, upload-time = "2026-06-12T20:02:54.323Z" }, + { url = "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283, upload-time = "2026-06-12T20:01:34.822Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", size = 5265844, upload-time = "2026-06-12T20:01:24.09Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290, upload-time = "2026-06-12T20:01:30.848Z" }, + { url = "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612, upload-time = "2026-06-12T20:01:29.246Z" }, + { url = "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804, upload-time = "2026-06-12T20:01:42.853Z" }, + { url = "https://files.pythonhosted.org/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", size = 3810026, upload-time = "2026-06-12T20:02:39.262Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9e/db72b3ae7fc9cfad53e630e56c6ae83b9b6ff0bf3718ffb8012d20b3aabf/cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7", size = 4013892, upload-time = "2026-06-12T20:02:10.735Z" }, + { url = "https://files.pythonhosted.org/packages/86/12/c48a424f38db03027be9f7ed5c7dc5de9933dbee992865f98b13727a009d/cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", size = 4678835, upload-time = "2026-06-12T20:02:48.743Z" }, + { url = "https://files.pythonhosted.org/packages/68/28/8a3ad4653662c93fc44dc4e5d8fd374c25c42e07b34bbfbadf49cf57a5a8/cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", size = 4697239, upload-time = "2026-06-12T20:02:56.03Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b2/2193fc74f81aee4f9b62733133b73b5176718932ed8f2e4b03fa040480a6/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", size = 4685593, upload-time = "2026-06-12T20:02:50.666Z" }, + { url = "https://files.pythonhosted.org/packages/47/f1/1d3eaa243bfc5de4a187b22aa8c048b3e4980bfbe830ac46e6bac2e66947/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d", size = 5289961, upload-time = "2026-06-12T20:01:46.468Z" }, + { url = "https://files.pythonhosted.org/packages/58/39/2d51306721330c486495853eda1c567880ff036de15a14c4b74f399934af/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", size = 4731145, upload-time = "2026-06-12T20:02:16.832Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/983e838c7fd0d87fd8c969bcdd328edaf5f756e38df5281637424c155873/cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", size = 4321719, upload-time = "2026-06-12T20:02:52.611Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f5/8f571d7e27c55bce9f76f026143bcb1e040a4233149ecca0bea5fa5dd5f7/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", size = 4685209, upload-time = "2026-06-12T20:02:07.282Z" }, + { url = "https://files.pythonhosted.org/packages/e7/84/0e27016a6fc5a0886f797018b26aa42f40c09a82332bff77822a451deaaa/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866", size = 5246285, upload-time = "2026-06-12T20:01:32.439Z" }, + { url = "https://files.pythonhosted.org/packages/11/2d/5e1fb307cb5931881516b464c98774b3f2c36b5d4bb9a2830253cf553cad/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", size = 4730441, upload-time = "2026-06-12T20:02:01.469Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c0/bff5a02ee731d207d6a1ed51732549d8c53d2bc8da1d10ec6f2844201d68/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", size = 4815869, upload-time = "2026-06-12T20:01:36.574Z" }, + { url = "https://files.pythonhosted.org/packages/b9/26/814681d14248d95d73d5c3eea0c39a94eb8302df966f670a2c60de90974b/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", size = 4960948, upload-time = "2026-06-12T20:02:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/93ecac273d3738939d023612ad12cca9a3740a5345d69fda04134c43fd96/cryptography-49.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61", size = 3799153, upload-time = "2026-06-12T20:01:39.059Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", size = 4025947, upload-time = "2026-06-12T20:01:25.745Z" }, + { url = "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429, upload-time = "2026-06-12T20:01:53.628Z" }, + { url = "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968, upload-time = "2026-06-12T20:02:45.383Z" }, + { url = "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758, upload-time = "2026-06-12T20:01:41.13Z" }, + { url = "https://files.pythonhosted.org/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", size = 5298863, upload-time = "2026-06-12T20:02:24.579Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983, upload-time = "2026-06-12T20:01:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173, upload-time = "2026-06-12T20:01:44.743Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298, upload-time = "2026-06-12T20:02:20.918Z" }, + { url = "https://files.pythonhosted.org/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", size = 5254338, upload-time = "2026-06-12T20:02:22.737Z" }, + { url = "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650, upload-time = "2026-06-12T20:02:41.389Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820, upload-time = "2026-06-12T20:01:51.847Z" }, + { url = "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968, upload-time = "2026-06-12T20:02:12.524Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" }, ] [[package]] @@ -563,15 +605,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" }, ] -[[package]] -name = "docutils" -version = "0.22.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, -] - [[package]] name = "editorconfig" version = "0.17.1" @@ -581,24 +614,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/96/fd/a40c621ff207f3ce8e484aa0fc8ba4eb6e3ecf52e15b42ba764b457a9550/editorconfig-0.17.1-py3-none-any.whl", hash = "sha256:1eda9c2c0db8c16dbd50111b710572a5e6de934e39772de1959d41f64fc17c82", size = 16360, upload-time = "2025-06-09T08:21:35.654Z" }, ] -[[package]] -name = "et-xmlfile" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234, upload-time = "2024-10-25T17:25:40.039Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" }, -] - -[[package]] -name = "fqdn" -version = "1.5.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/30/3e/a80a8c077fd798951169626cde3e239adeba7dab75deb3555716415bd9b0/fqdn-1.5.1.tar.gz", hash = "sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f", size = 6015, upload-time = "2021-03-11T07:16:29.08Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014", size = 9121, upload-time = "2021-03-11T07:16:28.351Z" }, -] - [[package]] name = "frozenlist" version = "1.8.0" @@ -701,67 +716,135 @@ wheels = [ ] [[package]] -name = "graphviz" -version = "0.21" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f8/b3/3ac91e9be6b761a4b30d66ff165e54439dcd48b83f4e20d644867215f6ca/graphviz-0.21.tar.gz", hash = "sha256:20743e7183be82aaaa8ad6c93f8893c923bd6658a04c32ee115edb3c8a835f78", size = 200434, upload-time = "2025-06-15T09:35:05.824Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl", hash = "sha256:54f33de9f4f911d7e84e4191749cac8cc5653f815b06738c54db9a15ab8b1e42", size = 47300, upload-time = "2025-06-15T09:35:04.433Z" }, -] - -[[package]] -name = "greenlet" -version = "3.5.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a3/74/b13368064b09053253555d3f2839cc2684d22d5aed0d2ccffbf7a6736558/greenlet-3.5.4.tar.gz", hash = "sha256:0232ae1de90a8e07867bb127d7a6ba2301e859145489f25cda8a6096dabe1d20", size = 206538, upload-time = "2026-07-22T12:47:14.468Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/04/81bd731d6d1e3a469d9a4c36f5eb069bcf0cbb2d5d342c9fec22245b91fc/greenlet-3.5.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3d66250e8b09f182ede05490998c818b5961f7a3640332d44c4927caec7bbfe4", size = 295909, upload-time = "2026-07-22T11:38:09.261Z" }, - { url = "https://files.pythonhosted.org/packages/cc/dd/f5f22903a6ae70f5ea328ed0beaec92ad903f0e3b7d2845133b354abc4b8/greenlet-3.5.4-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c90e930c9c192e5b3ee9fb8bcd920ea3926155e2e3ded39fc697323addecee17", size = 612011, upload-time = "2026-07-22T12:26:40.69Z" }, - { url = "https://files.pythonhosted.org/packages/8e/10/92a4a88d12b915d74ea5b6d288e4afefda4771647caa34442c156f7a454f/greenlet-3.5.4-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:791fdfeeb9c6e0c7b10fa151bf110d2a6974866f13dcb5b1c7efae698245893a", size = 624299, upload-time = "2026-07-22T12:29:02.089Z" }, - { url = "https://files.pythonhosted.org/packages/50/6d/0b14bb9db2989f32cd9fe7f76afedea01ee8bee3f87c07e69f24adfe7e63/greenlet-3.5.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f88193799d43dbf8c8a806d6405c9c52fe2af40bf75072a606357b33cc336c7f", size = 621541, upload-time = "2026-07-22T11:51:09.464Z" }, - { url = "https://files.pythonhosted.org/packages/48/3d/25e9a2d9eb6b2e8b7ca4e80a3a26cb887cce6c8e0a87c921164f11bc5574/greenlet-3.5.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b7a5f095767c4493afcd06067f2bb3b8716e3f3f9e92b99c88e7e99f885b3d4d", size = 1581444, upload-time = "2026-07-22T12:25:03.818Z" }, - { url = "https://files.pythonhosted.org/packages/b9/96/4c9bf2e2c408dcc0556edce69efa9f802e82223573c53240136a086821f1/greenlet-3.5.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:42afdc1ab5f66da8c586c32af9224a74a706b4f0ea0dc3a4188a0860a09c65c9", size = 1645842, upload-time = "2026-07-22T11:51:12.295Z" }, - { url = "https://files.pythonhosted.org/packages/b5/41/303ecb26a3a56122c0f4d4073ee078881847bd6b6f463ae0ec57ec20223b/greenlet-3.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:60149df8f462d1b230038e6590c23c3b4768bb5d6c022b3b6e82532b34b0b8a3", size = 247169, upload-time = "2026-07-22T11:38:19.893Z" }, - { url = "https://files.pythonhosted.org/packages/a4/e3/ef56864b4c35fcb3eb3b41b869f6cc46f4cd3f5e2c68e74acde8ac433951/greenlet-3.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:77d6ce04fed0d9aeed42e0f37923cc43eba9b027bdd9c34546bb4ccd143d0fe0", size = 245565, upload-time = "2026-07-22T11:38:27.061Z" }, - { url = "https://files.pythonhosted.org/packages/c0/9a/e51225dcd58713f16ccbdcc501a8da21098ea14515b7870f1f94459e5ff5/greenlet-3.5.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:24e61b88cb7e1b1d794b32a10cc346ac779681d6d74ff137a3e0a444d2bf1f02", size = 294831, upload-time = "2026-07-22T11:38:53.389Z" }, - { url = "https://files.pythonhosted.org/packages/9f/ea/de50a50fadf979713ab18b46f22ad5ff5f2dcfc637a3ebdecf669801e1a5/greenlet-3.5.4-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:870d730fec833f5a06906a32596cc099b9161594642a92a520b7a88911c95356", size = 614619, upload-time = "2026-07-22T12:26:42.282Z" }, - { url = "https://files.pythonhosted.org/packages/db/c7/2aae27fea41205b8650294c301f042a2a4bb6155eea48c995b890a92f2c1/greenlet-3.5.4-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec5ff0d1878df6af3bf9b638a5a92a7d5693291de77c91bff10fa48519c604ef", size = 627021, upload-time = "2026-07-22T12:29:03.445Z" }, - { url = "https://files.pythonhosted.org/packages/eb/56/79fd826f9ccaae0b84e1b4ef68dabba5e105bb044ffcd448a0b782fcba9a/greenlet-3.5.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d84d993f6e575c950d91a23c1345d18fe1a4310d447bf630849d7809196b52f0", size = 624002, upload-time = "2026-07-22T11:51:11.391Z" }, - { url = "https://files.pythonhosted.org/packages/0a/1a/27319f97e731298513dcba1a2e91b63e9d8811d9de22130f960b129b1bf1/greenlet-3.5.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:58023945f421093de5e6fa108c0985a8659d43f49e0216da25099369a121bcbd", size = 1581533, upload-time = "2026-07-22T12:25:05.322Z" }, - { url = "https://files.pythonhosted.org/packages/b1/6d/24240bf562e9786dd2799ee0a4a4dadb4ded22510f41b20245099159ac8c/greenlet-3.5.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bae2728e1897aa8df8cb1af38cd48b3a743aefe29372de7b8b7a9f532501e69f", size = 1645781, upload-time = "2026-07-22T11:51:14.805Z" }, - { url = "https://files.pythonhosted.org/packages/c1/5a/442ab1a9ef7ca6bf7210e5397a95972206a91a31033a03c8900866a10039/greenlet-3.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:ca5726c0b08ca35ae873557266a78b2c3f3b2b7d7401aa5ff886c2045dd0111c", size = 247133, upload-time = "2026-07-22T11:39:20.661Z" }, - { url = "https://files.pythonhosted.org/packages/3e/e6/9160210222386b1a378ff94db846b9508ca24a121cf684991561fdb69280/greenlet-3.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:7c1303791d603080cac6fc3b34df51c3b75b723739c282c8029e48a0d241672f", size = 245500, upload-time = "2026-07-22T11:40:22.185Z" }, - { url = "https://files.pythonhosted.org/packages/a5/a7/6ab1d4f9cd548d15ab90da29947f2076100130bb179b0bde59f795a459e3/greenlet-3.5.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:7e8afa5eac028f8140ceafe5ceec66e6aa127ddcb21452d2a564dcd2900b5f22", size = 295410, upload-time = "2026-07-22T11:40:35.747Z" }, - { url = "https://files.pythonhosted.org/packages/cd/7a/422f63b4715cbc0b24385305407adf38b48f6bb68b3e6b04090e994d0f5a/greenlet-3.5.4-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73b37afe369021423ea53dd3123e04bffa7e93ac64429b9f50835b2e4fcae7cf", size = 661286, upload-time = "2026-07-22T12:26:43.8Z" }, - { url = "https://files.pythonhosted.org/packages/d0/31/5a1cac663bf5582190c5a714ef81364f03cde232227f39748f8ae4c11da5/greenlet-3.5.4-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3ef964f56dfcb6f9bbef2a190d9126795eac408716aeae47b5e7c73c32aafca9", size = 673517, upload-time = "2026-07-22T12:29:04.815Z" }, - { url = "https://files.pythonhosted.org/packages/15/4a/2a82a1e3f8aaca020853ac8d12211280ca2b231aa08ea39f636f1060c319/greenlet-3.5.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c53ff01a5c53a40f2c16820ebc56d7c61a77f5fbe009dadd96292d5682f80f8", size = 670917, upload-time = "2026-07-22T11:51:13.589Z" }, - { url = "https://files.pythonhosted.org/packages/68/b0/e379a152b17bfdfa95795af4049e37c0fd1b4d81f020d426db104ed07c77/greenlet-3.5.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecca4d80d55a01ad6b23b33262662956149fbb7b2c6be2910f1705921958cbf3", size = 1628478, upload-time = "2026-07-22T12:25:06.678Z" }, - { url = "https://files.pythonhosted.org/packages/5e/43/bffdfa64f7317f954c5c1230b5dd5922676ce198689a68c1ac1ed4b1b1a5/greenlet-3.5.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2ffbc533e0eaf8e80d8471411646ab88fe58f641d508c0b02b24494479f4d9ec", size = 1692021, upload-time = "2026-07-22T11:51:17.008Z" }, - { url = "https://files.pythonhosted.org/packages/d0/11/f799f9637e2c6e9b0b716015e339040598b058cf7654dfc0d67468b177ed/greenlet-3.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:305f69e6c4523d7f6979ed001cff4e5853c063e5da04880296603aa0227e544c", size = 248031, upload-time = "2026-07-22T11:40:11.007Z" }, - { url = "https://files.pythonhosted.org/packages/05/75/625bcdd74d5e6b2dca1ecba3c3ac77bcf8a026c21a649a46cef23e421f97/greenlet-3.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:f260930bbbbcf9caee661211235a5111c86dfe5832fdf6ae4570da1e0995320f", size = 246892, upload-time = "2026-07-22T11:40:27.357Z" }, - { url = "https://files.pythonhosted.org/packages/ec/69/35c62ed49c320cb4d98e14698ccca5467d3bfe683984172be9cb564d9ce3/greenlet-3.5.4-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:41ddab54e4b238f4a6c323f39b4e59e176affd5a94d461a9fb7583dac74240a3", size = 305571, upload-time = "2026-07-22T11:40:31.659Z" }, - { url = "https://files.pythonhosted.org/packages/5b/6c/64d60216b3640dcb0b62d913dd9e0d80030c09115bb2e4ba70c95d10ca45/greenlet-3.5.4-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3dabe3e2809013052c68bdf0b7fa5f5f2859c43a80803131ad61af9cabd7867", size = 672568, upload-time = "2026-07-22T12:26:45.298Z" }, - { url = "https://files.pythonhosted.org/packages/5c/de/ba3ab0a96292e53039530333b0d2ae18d9e508f3a325cd7bf15f8172944c/greenlet-3.5.4-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:27d3f00718634d4520a3a150154ac5da36f257869d41321953375b90bfbbc72c", size = 680076, upload-time = "2026-07-22T12:29:06.125Z" }, - { url = "https://files.pythonhosted.org/packages/a5/be/aeada79083c6f1c15f45d77a332f9c441af263ee298e3eb17522cd337d22/greenlet-3.5.4-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cbd60b5763c6543c1827e48faaf14ea9bfbad245f52b1a4d76a2a2d8884c6c66", size = 676733, upload-time = "2026-07-22T11:51:16.027Z" }, - { url = "https://files.pythonhosted.org/packages/e9/10/2392fc3a98948652ef5fd1e7275c04f861dd13f74b78a2b4309f4ee4d090/greenlet-3.5.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f00f910f0e7b35416c63b23ad78b769aeccfc1775f712b43c4ee525624a2eef7", size = 1637327, upload-time = "2026-07-22T12:25:07.879Z" }, - { url = "https://files.pythonhosted.org/packages/55/c6/e7237a3dfa1f205ed0d9ea1e46d70bd2811b32d516266399fb59d28ab90a/greenlet-3.5.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:91c26423753b92caf41ab3f98fd547d7374d4d9fc2d85be041886c1579d9255e", size = 1697493, upload-time = "2026-07-22T11:51:19.214Z" }, - { url = "https://files.pythonhosted.org/packages/55/e3/4ba8154ba2a3d43729e499f72471b4b5c993f3826d3e24da81d5f06d6572/greenlet-3.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:ee032b91fd8ec29ec6c4cea2b8c561b178435134bd0752c7334b94e9c736c132", size = 251637, upload-time = "2026-07-22T11:40:37.44Z" }, - { url = "https://files.pythonhosted.org/packages/90/03/e3f96dfc100261a29545ddc8270cafe58f9195b6651466910e820910de77/greenlet-3.5.4-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:178111881dd7a6c946471fda85485ec796e1043c2b939f694b096e2ecf986809", size = 296076, upload-time = "2026-07-22T11:39:38.364Z" }, - { url = "https://files.pythonhosted.org/packages/a4/3d/da52d208e5c977bce8667e784729e584e38b5785f4c1ec0f4c836e9a1c42/greenlet-3.5.4-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d92df08dd65fede97fc37aad36c2e9dcda3b31c467f8e0c2c096456cb818e927", size = 666870, upload-time = "2026-07-22T12:26:46.691Z" }, - { url = "https://files.pythonhosted.org/packages/2d/8a/7e6dee25cb8a8cf9b362c8e597cc269593378bd916f16c736c059a52e85a/greenlet-3.5.4-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:99e8f8c4ebc4fd80aa26c1280ae9ad43a0976e786349703a181cf0bae60413e5", size = 677678, upload-time = "2026-07-22T12:29:07.508Z" }, - { url = "https://files.pythonhosted.org/packages/6c/21/5a38699fa45de749e3857d93b8f07e4c20489e77c2d35d915a2e1c456606/greenlet-3.5.4-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:394de08dad5ffcb1f50c2159d93e398d9d2da3ed437645eaa54771fa720db9f0", size = 676067, upload-time = "2026-07-22T11:51:18.163Z" }, - { url = "https://files.pythonhosted.org/packages/5e/ba/863116ab8ff1ca7a729e327800268939d182db47aa433db70e216e7d9194/greenlet-3.5.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:c883d61f2282d72c767a14936641b3efcbde9d82f1080712aaea0b1d3126cb88", size = 1633489, upload-time = "2026-07-22T12:25:09.605Z" }, - { url = "https://files.pythonhosted.org/packages/fa/06/7466ced82818d6132462d7f26b3f83c66ea15d2b193a6c0088d558ed7d95/greenlet-3.5.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2a924f15d17957e252a810acefcb5942f5ca712298e8b6fcaed9a307d357522c", size = 1696584, upload-time = "2026-07-22T11:51:21.304Z" }, - { url = "https://files.pythonhosted.org/packages/f9/4d/55b638489260065de9ffce606c8b5d04507bef705de4b212a0c3d6a1a0df/greenlet-3.5.4-cp315-cp315-win_amd64.whl", hash = "sha256:ed17e5f3420360d5b459de8462efb52060399a5326a613d4cde31cef63ef95da", size = 248297, upload-time = "2026-07-22T11:42:04.055Z" }, - { url = "https://files.pythonhosted.org/packages/bb/08/9dd4ae635da93d41dc268bc34bd62a9d711ed8b8825c5d22ac910c7d6e6d/greenlet-3.5.4-cp315-cp315-win_arm64.whl", hash = "sha256:f908898d6fa484ce4b6f447ce70ea99b52c503fee419e53cf74d60a16bc9e667", size = 247423, upload-time = "2026-07-22T11:44:00.764Z" }, - { url = "https://files.pythonhosted.org/packages/19/66/7c87ed9cdbf1d49c2c6cd1c7b9dd4d16c33b24235ca03972293a1876b30c/greenlet-3.5.4-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:1833637f17d5e7472548a48575c394fe39f1b1890d676d162d86593610f44d8c", size = 306487, upload-time = "2026-07-22T11:41:25.118Z" }, - { url = "https://files.pythonhosted.org/packages/5b/05/0a4201e7c0054866eefc05da234f236dd4c950d0fbf9ca0517141f01b269/greenlet-3.5.4-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:12cda9122e03341f1cb6b8207a19d7a9d375e52f1b4e9243918375f40fd7b4b9", size = 676479, upload-time = "2026-07-22T12:26:48.129Z" }, - { url = "https://files.pythonhosted.org/packages/3d/c0/4b6b8c5a3aec70f0649cd89662d120fdd6421e2bdc8e3b15c3ab5ec568d8/greenlet-3.5.4-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d83ae0e32d14957ab7170785a20f582635c8474deab1bfbb552b17e769a6ce25", size = 684321, upload-time = "2026-07-22T12:29:08.925Z" }, - { url = "https://files.pythonhosted.org/packages/24/c9/b49c31c9a972eee91e260445770e922244a7efc542697f51a012ec046d0f/greenlet-3.5.4-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f1467de1bb767f75db0aa34c195e3a496d8d1278c796e70c24ce205d3e99cde", size = 681293, upload-time = "2026-07-22T11:51:20.43Z" }, - { url = "https://files.pythonhosted.org/packages/95/6f/7f2d4653770500eee667866016d42d7a68e3d3462f80df6b8e3fcd48a0eb/greenlet-3.5.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:0fa53040b78b578120eecdc0265e3f1051487cc425d11a2b7c761daadf4feaa8", size = 1642474, upload-time = "2026-07-22T12:25:10.819Z" }, - { url = "https://files.pythonhosted.org/packages/5a/d8/8cba31036a4caae448087ba5d150660ab03b4a0f54d9150f6495a3be7262/greenlet-3.5.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:60e0bc961d367df506660e9ac0177a76bc6d81305300704b0977d1634f76efe2", size = 1701012, upload-time = "2026-07-22T11:51:23.17Z" }, - { url = "https://files.pythonhosted.org/packages/e3/cd/3f77a4cce3bae631b08eb52f53a82a976669600337e21dfdba811cb50267/greenlet-3.5.4-cp315-cp315t-win_amd64.whl", hash = "sha256:f680e549edb3eaf21eea4e7fe101e15ec180c74b7879ab46adc080f22d4015d2", size = 251977, upload-time = "2026-07-22T11:41:38.125Z" }, - { url = "https://files.pythonhosted.org/packages/93/e8/65e8707d00fe2a49bf12f609a9b2b39ba6dd23c2810eacad877c4fc94bfe/greenlet-3.5.4-cp315-cp315t-win_arm64.whl", hash = "sha256:08fc36de8442d5c3e95b044550dbea9bf144d31ec0cc58e36fb241cb6ef6a994", size = 250538, upload-time = "2026-07-22T11:40:17.985Z" }, +name = "google-api-core" +version = "2.33.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "googleapis-common-protos" }, + { name = "proto-plus" }, + { name = "protobuf" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/62/8fb1fb647d2788c950d69d6a769cd9d55c918ac1fc57be2f90b7e4029787/google_api_core-2.33.0.tar.gz", hash = "sha256:3a36bcc3e319783f4c97da41f6f45ea6ffcaa55848e341de16e09cb70243c2bb", size = 181607, upload-time = "2026-07-22T16:28:28.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/31/5056a347bb934ea04583c8b27916ef1501729c72638629545bce26ff4223/google_api_core-2.33.0-py3-none-any.whl", hash = "sha256:a2e22a0c1d0f03eafff1858b38cf46f832d5902b0c052235bf0ab8402929fbdc", size = 176462, upload-time = "2026-07-22T16:28:22.447Z" }, +] + +[[package]] +name = "google-auth" +version = "2.56.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyasn1-modules" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/33/dbc946a407401b975f0719658f18e664ece2109f79ffd1ff3bf226c205f4/google_auth-2.56.2.tar.gz", hash = "sha256:e28f103ca8091fb7012b99c44243d7366c29863713b8e34a220c3322b7a07051", size = 365820, upload-time = "2026-07-21T21:53:28.188Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/63/50636aae68c9bf17c891c7eb18b49baa9bd6b31d2a97b8de4813a9fc8d1c/google_auth-2.56.2-py3-none-any.whl", hash = "sha256:c8270ea95b2697b74e3d8438ae9c5b898e38b623b915c7b5c5635921e7de68a6", size = 258588, upload-time = "2026-07-21T21:53:26.399Z" }, +] + +[package.optional-dependencies] +requests = [ + { name = "requests" }, +] + +[[package]] +name = "google-cloud-core" +version = "2.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core" }, + { name = "google-auth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/dd/1eef226e470369b26824a505c34482c0b493bc35fe8e0c6b003b5feca21a/google_cloud_core-2.6.0.tar.gz", hash = "sha256:e76149739f90fac1fc6757c09f47eaccb3145b54adbd7759b0f7c4b235f46c83", size = 36001, upload-time = "2026-05-07T08:04:04.124Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/4a/98da8930ab109c73d9a5d13782a9ebb81ea8c111f6d534a567b71d23e52b/google_cloud_core-2.6.0-py3-none-any.whl", hash = "sha256:6d63ac8e5eca6d9e4319d0a1e2265fadcd7f1049904378caecfa01cf52dd869e", size = 29390, upload-time = "2026-05-07T08:02:34.672Z" }, +] + +[[package]] +name = "google-cloud-storage" +version = "3.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core" }, + { name = "google-auth" }, + { name = "google-cloud-core" }, + { name = "google-crc32c" }, + { name = "google-resumable-media" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e3/25/355ed97c1723c787dfaa888808d55db18371f82c38ff862357b1e902cd19/google_cloud_storage-3.13.0.tar.gz", hash = "sha256:d11d8706ea1520fba0f21043bcb7897caf7015d76ce1ad9a4f60237e4d7a9f6c", size = 17340960, upload-time = "2026-07-13T19:10:07.524Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/e8/b3678a0931ee7d4b3fdaf0813e6206d66e0922b2c26d912f308728b5b95a/google_cloud_storage-3.13.0-py3-none-any.whl", hash = "sha256:648af3ef8a6acc674e1359d3c920c67eb89a7a5ab66b336bd3ac43fed6b5ab84", size = 341428, upload-time = "2026-07-13T19:09:52.39Z" }, +] + +[[package]] +name = "google-crc32c" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/41/4b9c02f99e4c5fb477122cd5437403b552873f014616ac1d19ac8221a58d/google_crc32c-1.8.0.tar.gz", hash = "sha256:a428e25fb7691024de47fecfbff7ff957214da51eddded0da0ae0e0f03a2cf79", size = 14192, upload-time = "2025-12-16T00:35:25.142Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/5f/7307325b1198b59324c0fa9807cafb551afb65e831699f2ce211ad5c8240/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:4b8286b659c1335172e39563ab0a768b8015e88e08329fa5321f774275fc3113", size = 31300, upload-time = "2025-12-16T00:21:56.723Z" }, + { url = "https://files.pythonhosted.org/packages/21/8e/58c0d5d86e2220e6a37befe7e6a94dd2f6006044b1a33edf1ff6d9f7e319/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:2a3dc3318507de089c5384cc74d54318401410f82aa65b2d9cdde9d297aca7cb", size = 30867, upload-time = "2025-12-16T00:38:31.302Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a9/a780cc66f86335a6019f557a8aaca8fbb970728f0efd2430d15ff1beae0e/google_crc32c-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14f87e04d613dfa218d6135e81b78272c3b904e2a7053b841481b38a7d901411", size = 33364, upload-time = "2025-12-16T00:40:22.96Z" }, + { url = "https://files.pythonhosted.org/packages/21/3f/3457ea803db0198c9aaca2dd373750972ce28a26f00544b6b85088811939/google_crc32c-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb5c869c2923d56cb0c8e6bcdd73c009c36ae39b652dbe46a05eb4ef0ad01454", size = 33740, upload-time = "2025-12-16T00:40:23.96Z" }, + { url = "https://files.pythonhosted.org/packages/df/c0/87c2073e0c72515bb8733d4eef7b21548e8d189f094b5dad20b0ecaf64f6/google_crc32c-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc0c8912038065eafa603b238abf252e204accab2a704c63b9e14837a854962", size = 34437, upload-time = "2025-12-16T00:35:21.395Z" }, + { url = "https://files.pythonhosted.org/packages/d1/db/000f15b41724589b0e7bc24bc7a8967898d8d3bc8caf64c513d91ef1f6c0/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:3ebb04528e83b2634857f43f9bb8ef5b2bbe7f10f140daeb01b58f972d04736b", size = 31297, upload-time = "2025-12-16T00:23:20.709Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0d/8ebed0c39c53a7e838e2a486da8abb0e52de135f1b376ae2f0b160eb4c1a/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:450dc98429d3e33ed2926fc99ee81001928d63460f8538f21a5d6060912a8e27", size = 30867, upload-time = "2025-12-16T00:43:14.628Z" }, + { url = "https://files.pythonhosted.org/packages/ce/42/b468aec74a0354b34c8cbf748db20d6e350a68a2b0912e128cabee49806c/google_crc32c-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3b9776774b24ba76831609ffbabce8cdf6fa2bd5e9df37b594221c7e333a81fa", size = 33344, upload-time = "2025-12-16T00:40:24.742Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e8/b33784d6fc77fb5062a8a7854e43e1e618b87d5ddf610a88025e4de6226e/google_crc32c-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:89c17d53d75562edfff86679244830599ee0a48efc216200691de8b02ab6b2b8", size = 33694, upload-time = "2025-12-16T00:40:25.505Z" }, + { url = "https://files.pythonhosted.org/packages/92/b1/d3cbd4d988afb3d8e4db94ca953df429ed6db7282ed0e700d25e6c7bfc8d/google_crc32c-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:57a50a9035b75643996fbf224d6661e386c7162d1dfdab9bc4ca790947d1007f", size = 34435, upload-time = "2025-12-16T00:35:22.107Z" }, + { url = "https://files.pythonhosted.org/packages/21/88/8ecf3c2b864a490b9e7010c84fd203ec8cf3b280651106a3a74dd1b0ca72/google_crc32c-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:e6584b12cb06796d285d09e33f63309a09368b9d806a551d8036a4207ea43697", size = 31301, upload-time = "2025-12-16T00:24:48.527Z" }, + { url = "https://files.pythonhosted.org/packages/36/c6/f7ff6c11f5ca215d9f43d3629163727a272eabc356e5c9b2853df2bfe965/google_crc32c-1.8.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:f4b51844ef67d6cf2e9425983274da75f18b1597bb2c998e1c0a0e8d46f8f651", size = 30868, upload-time = "2025-12-16T00:48:12.163Z" }, + { url = "https://files.pythonhosted.org/packages/56/15/c25671c7aad70f8179d858c55a6ae8404902abe0cdcf32a29d581792b491/google_crc32c-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b0d1a7afc6e8e4635564ba8aa5c0548e3173e41b6384d7711a9123165f582de2", size = 33381, upload-time = "2025-12-16T00:40:26.268Z" }, + { url = "https://files.pythonhosted.org/packages/42/fa/f50f51260d7b0ef5d4898af122d8a7ec5a84e2984f676f746445f783705f/google_crc32c-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8b3f68782f3cbd1bce027e48768293072813469af6a61a86f6bb4977a4380f21", size = 33734, upload-time = "2025-12-16T00:40:27.028Z" }, + { url = "https://files.pythonhosted.org/packages/08/a5/7b059810934a09fb3ccb657e0843813c1fee1183d3bc2c8041800374aa2c/google_crc32c-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:d511b3153e7011a27ab6ee6bb3a5404a55b994dc1a7322c0b87b29606d9790e2", size = 34878, upload-time = "2025-12-16T00:35:23.142Z" }, +] + +[[package]] +name = "google-genai" +version = "2.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "google-auth", extra = ["requests"] }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "sniffio" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/df/4f820054c99f29f2fe3de4a8a7c9534dd795302e4a07483a0cb07c3a29b6/google_genai-2.14.0.tar.gz", hash = "sha256:a9d1f4f362d76280f1be1340fcb3c86e63dbca128f6a4ae09d86ab47ff7148e8", size = 641055, upload-time = "2026-07-22T21:35:44.717Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/86/5ac5fb53e44cca4a6607fb917eb331fa237c65a103b9ec2e8e8acc8a42db/google_genai-2.14.0-py3-none-any.whl", hash = "sha256:ae7172cdd35695189b516b33a878e4132e5daa2dbc03a5b44cddfa8a82fad664", size = 1030738, upload-time = "2026-07-22T21:35:42.785Z" }, +] + +[[package]] +name = "google-resumable-media" +version = "2.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-crc32c" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/f8/1ca5781d6be9cb9f73f7d40f4958c4bd1226a60598e3e39e1d6aaf838c4b/google_resumable_media-2.10.0.tar.gz", hash = "sha256:e324bc9d0fdae4c52a08ae90456edc4e71ece858399e1217ac0eb3a51d6bc6ee", size = 2164570, upload-time = "2026-06-03T16:14:26.103Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/d8/00c6854ac1512bb9eaf13bd3f8f28222f7674947fc510a4ff7616f2efc80/google_resumable_media-2.10.0-py3-none-any.whl", hash = "sha256:88152884bee37b2bf36a0ab81ad8c7fd12212c9803dd981d77c1b35b02d34e7c", size = 81533, upload-time = "2026-06-03T16:13:12.51Z" }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.75.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/c8/f439cffde755cffa462bfbb156278fa6f9d09119719af9814b858fd4f81f/googleapis_common_protos-1.75.0.tar.gz", hash = "sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd", size = 151035, upload-time = "2026-05-07T08:04:49.423Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed", size = 300631, upload-time = "2026-05-07T08:03:30.345Z" }, ] [[package]] @@ -782,15 +865,6 @@ 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 = "hbreader" -version = "0.9.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/66/3a649ce125e03d1d43727a8b833cd211f0b9fe54a7e5be326f50d6f1d951/hbreader-0.9.1.tar.gz", hash = "sha256:d2c132f8ba6276d794c66224c3297cec25c8079d0a4cf019c061611e0a3b94fa", size = 19016, upload-time = "2021-02-25T19:22:32.799Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/24/61844afbf38acf419e01ca2639f7bd079584523d34471acbc4152ee991c5/hbreader-0.9.1-py3-none-any.whl", hash = "sha256:9a6e76c9d1afc1b977374a5dc430a1ebb0ea0488205546d4678d6e31cc5f6801", size = 7595, upload-time = "2021-02-25T19:22:31.944Z" }, -] - [[package]] name = "httpcore" version = "1.0.9" @@ -828,15 +902,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, ] -[[package]] -name = "imagesize" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/e6/7bf14eeb8f8b7251141944835abd42eb20a658d89084b7e1f3e5fe394090/imagesize-2.0.0.tar.gz", hash = "sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3", size = 1773045, upload-time = "2026-03-03T14:18:29.941Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96", size = 9441, upload-time = "2026-03-03T14:18:27.892Z" }, -] - [[package]] name = "iniconfig" version = "2.3.0" @@ -868,27 +933,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/80/8d/f668a30fff4d25b36533355e23aeb0b5724df4628eb974124ed64b7bcf8d/instructor-1.15.4-py3-none-any.whl", hash = "sha256:00e0ecda80fd9746fb6d082d3f9641e193adb1d8849f0775f91519a82aeff968", size = 252522, upload-time = "2026-06-28T07:36:36.863Z" }, ] -[[package]] -name = "isodate" -version = "0.7.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, -] - -[[package]] -name = "isoduration" -version = "20.11.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "arrow" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7c/1a/3c8edc664e06e6bd06cce40c6b22da5f1429aa4224d0c590f3be21c91ead/isoduration-20.11.0.tar.gz", hash = "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9", size = 11649, upload-time = "2020-11-01T11:00:00.312Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042", size = 11321, upload-time = "2020-11-01T10:59:58.02Z" }, -] - [[package]] name = "jinja2" version = "3.1.6" @@ -985,88 +1029,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/51/0b/607e06204b207c29a86759c763624aabbdd16613844564536847d71ad461/jsbeautifier-2.0.3-py3-none-any.whl", hash = "sha256:f0190e279a2cdb827556ada63f41c9c63c11f8116ee06e264b24aa50311cbee3", size = 93877, upload-time = "2026-06-30T15:41:11.731Z" }, ] -[[package]] -name = "json-flattener" -version = "0.1.9" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6d/77/b00e46d904818826275661a690532d3a3a43a4ded0264b2d7fcdb5c0feea/json_flattener-0.1.9.tar.gz", hash = "sha256:84cf8523045ffb124301a602602201665fcb003a171ece87e6f46ed02f7f0c15", size = 11479, upload-time = "2022-02-26T01:36:04.545Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/00/cc/7fbd75d3362e939eb98bcf9bd22f3f7df8c237a85148899ed3d38e5614e5/json_flattener-0.1.9-py3-none-any.whl", hash = "sha256:6b027746f08bf37a75270f30c6690c7149d5f704d8af1740c346a3a1236bc941", size = 10799, upload-time = "2022-02-26T01:36:03.06Z" }, -] - -[[package]] -name = "jsonasobj" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e5/ba/13523c1408a23bac4e08ef2312732733c0129c4ff085d351eafaf45fd080/jsonasobj-1.3.1.tar.gz", hash = "sha256:d52e0544a54a08f6ea3f77fa3387271e3648655e0eace2f21e825c26370e44a2", size = 4315, upload-time = "2021-02-08T22:03:20.336Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/71/57/38c47753c67ad67f76ba04ea673c9b77431a19e7b2601937e6872a99e841/jsonasobj-1.3.1-py3-none-any.whl", hash = "sha256:b9e329dc1ceaae7cf5d5b214684a0b100e0dad0be6d5bbabac281ec35ddeca65", size = 4388, upload-time = "2021-02-08T22:03:19.17Z" }, -] - -[[package]] -name = "jsonasobj2" -version = "1.0.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "hbreader" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/67/3a/feb245b755f7a47a0df4f30be645e8485d15ff13d0c95e018e4505a8811f/jsonasobj2-1.0.4.tar.gz", hash = "sha256:f50b1668ef478004aa487b2d2d094c304e5cb6b79337809f4a1f2975cc7fbb4e", size = 95522, upload-time = "2021-06-02T17:43:28.39Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/90/0d93963711f811efe528e3cead2f2bfb78c196df74d8a24fe8d655288e50/jsonasobj2-1.0.4-py3-none-any.whl", hash = "sha256:12e86f86324d54fcf60632db94ea74488d5314e3da554c994fe1e2c6f29acb79", size = 6324, upload-time = "2021-06-02T17:43:27.126Z" }, -] - -[[package]] -name = "jsonpointer" -version = "3.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/c7/af399a2e7a67fd18d63c40c5e62d3af4e67b836a2107468b6a5ea24c4304/jsonpointer-3.1.1.tar.gz", hash = "sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900", size = 9068, upload-time = "2026-03-23T22:32:32.458Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl", hash = "sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca", size = 7659, upload-time = "2026-03-23T22:32:31.568Z" }, -] - -[[package]] -name = "jsonschema" -version = "4.26.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "jsonschema-specifications" }, - { name = "referencing" }, - { name = "rpds-py" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, -] - -[package.optional-dependencies] -format = [ - { name = "fqdn" }, - { name = "idna" }, - { name = "isoduration" }, - { name = "jsonpointer" }, - { name = "rfc3339-validator" }, - { name = "rfc3987" }, - { name = "uri-template" }, - { name = "webcolors" }, -] - -[[package]] -name = "jsonschema-specifications" -version = "2025.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "referencing" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, -] - [[package]] name = "librt" version = "0.13.0" @@ -1129,65 +1091,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5f/5d/3dcec2884ba1b0806d1408612555c38dd5d68e90156b59f75f6e36435c3a/librt-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2f281549a4c52ac7bb97997f14353f8bd0e53a34ca0dad1c905cfd0b4a58ae99", size = 110771, upload-time = "2026-07-08T12:26:12.303Z" }, ] -[[package]] -name = "linkml" -version = "1.11.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "antlr4-python3-runtime" }, - { name = "click" }, - { name = "graphviz" }, - { name = "hbreader" }, - { name = "isodate" }, - { name = "jinja2" }, - { name = "jsonasobj2" }, - { name = "jsonschema", extra = ["format"] }, - { name = "linkml-runtime" }, - { name = "openpyxl" }, - { name = "parse" }, - { name = "prefixcommons" }, - { name = "prefixmaps" }, - { name = "pydantic" }, - { name = "pyjsg" }, - { name = "pyshex" }, - { name = "pyshexc" }, - { name = "python-dateutil" }, - { name = "pyyaml" }, - { name = "rdflib" }, - { name = "requests" }, - { name = "sphinx-click" }, - { name = "sqlalchemy" }, - { name = "watchdog" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b4/26/38e7340959cd4a87bfe5403cfcf5311d9fe2ff4382fa00e96008a1342760/linkml-1.11.1.tar.gz", hash = "sha256:2f6774e13628270cadaeecda3313db0437ecc15cd44ee35c6c2655dbe31c8524", size = 374853, upload-time = "2026-05-20T17:05:42.359Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/fb/3068f649cc436be915f51b2f5ac0656c83dc9bcc6d4f8940633e295042c0/linkml-1.11.1-py3-none-any.whl", hash = "sha256:d1bbb97a8b1ea4a99b145007875733a5e5e89b3acfe3e9d1e369fa4a582990ed", size = 483751, upload-time = "2026-05-20T17:05:38.663Z" }, -] - -[[package]] -name = "linkml-runtime" -version = "1.11.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "curies" }, - { name = "deprecated" }, - { name = "hbreader" }, - { name = "json-flattener" }, - { name = "jsonasobj2" }, - { name = "jsonschema" }, - { name = "prefixcommons" }, - { name = "prefixmaps" }, - { name = "pydantic" }, - { name = "pyyaml" }, - { name = "rdflib" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d0/7c/36332b49226f37d05d0dbfa4fb1c8017963d62ae722102c9c11c1f530696/linkml_runtime-1.11.1.tar.gz", hash = "sha256:e71300b596c4f35aeccd9dca096806678402213dbdb2c5e8e68f507e21320754", size = 556549, upload-time = "2026-05-20T17:05:43.633Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/63/1d/600b0dd24aa61f03d35293a2e9a4695add1e94c03d8701436fb52d5daf4f/linkml_runtime-1.11.1-py3-none-any.whl", hash = "sha256:b22c77d8fd920d0f4f43a6ece31393dc0b28bb47790f3e1c114210318c36b3da", size = 654566, upload-time = "2026-05-20T17:05:40.526Z" }, -] - [[package]] name = "markdown" version = "3.10.2" @@ -1580,100 +1483,66 @@ wheels = [ ] [[package]] -name = "numpy" -version = "2.5.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/22/fd/89965aa4ac08c74998539fcbf24fa3540f3e15237fbeb6bcf9c908f4aade/numpy-2.5.1.tar.gz", hash = "sha256:a48a113e6afea91f5608793bafa7ef2ad481fefbda87ec5069f483de61cb9fa3", size = 20755553, upload-time = "2026-07-04T17:08:00.933Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/7b/14687aa674250e5e546f616f486b0d56d3631cd5b2415739141ce40bdcea/numpy-2.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2c889b56fe48b1018f764b0eec8df59ab654e9148aa91faa12596043500de277", size = 16801574, upload-time = "2026-07-04T17:06:12.423Z" }, - { url = "https://files.pythonhosted.org/packages/e1/19/cc5bb2a3f2913d27d6dbb2c78d25921fabaedc6741d4a5a615a11f3c5bf3/numpy-2.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab451b59c5643c570974c43aef780703ef1d3b4965d2be07afd530615a9358d1", size = 11772250, upload-time = "2026-07-04T17:06:15.726Z" }, - { url = "https://files.pythonhosted.org/packages/42/77/fdf34a71dd30f54979b18603bee915e0aaf825b07afe79acd60b04b691e2/numpy-2.5.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:78798bd5b9ad744056af8efa90e3b9ddaa53272a0848a483084a1cc0a13b2dc0", size = 5331516, upload-time = "2026-07-04T17:06:17.913Z" }, - { url = "https://files.pythonhosted.org/packages/ce/e2/eb7efa015b4cce41e2517bf182a7fce0d7d5b9d9ed76a29bfa0f4fe4505c/numpy-2.5.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:2ae0ca40bcb22d6ba59c1dfd5446f49940b0f2d821fde133f10dda11f816b84e", size = 6664863, upload-time = "2026-07-04T17:06:20.02Z" }, - { url = "https://files.pythonhosted.org/packages/a9/4b/a2b32dd94ee9ffbeecb28152240042a3949db33b1c834d44090b80e1b3b8/numpy-2.5.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61ac47e772e6b8ea489e1d2f441a34c5c3ac17327e7ce294cbdf535795ad4e75", size = 15167977, upload-time = "2026-07-04T17:06:21.621Z" }, - { url = "https://files.pythonhosted.org/packages/b8/a9/6e73d68500f80773f65f0654ea932019d6694329a0eb0ed0533de38df376/numpy-2.5.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:59fda5e192b570217ec2580c96f00e9a7e12ef6866a900eb089b62c1a32545ca", size = 16672469, upload-time = "2026-07-04T17:06:24.064Z" }, - { url = "https://files.pythonhosted.org/packages/24/7d/ad3e59015135f5261c95fd4cafeff159c955febd83a99a1d9250c4233815/numpy-2.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f7119ebff1a9829e9f431a4f9d28e703023bb6b9fe7c8f724467dbfc27c94ab3", size = 16527531, upload-time = "2026-07-04T17:06:26.69Z" }, - { url = "https://files.pythonhosted.org/packages/83/d0/a39b2fbcde9cb17a1dac678f254b33a6336298af9df338824c685425d5e8/numpy-2.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e824c2acf8862052246be5a44c15da1777940c60d010dd2aab897824d9c430f9", size = 18431940, upload-time = "2026-07-04T17:06:29.521Z" }, - { url = "https://files.pythonhosted.org/packages/04/12/cff070947791c1ed425ff76413189adbdc2fbe215eba7ce7fa454a03c7f8/numpy-2.5.1-cp312-cp312-win32.whl", hash = "sha256:08d60c810432eb83360958dea0999ac4cfb94531ea8efcbf0b7f277c2068aeb2", size = 6066764, upload-time = "2026-07-04T17:06:32.571Z" }, - { url = "https://files.pythonhosted.org/packages/65/66/53f31807a48a750f9d748da273bc3fcedd12b27ff1f3e373bfec55ef2dc0/numpy-2.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:f7d60026c0bdb1380e83bfa7a0419c4577ee4b9a08880afcb6dadeb74c649fa2", size = 12430966, upload-time = "2026-07-04T17:06:34.926Z" }, - { url = "https://files.pythonhosted.org/packages/2b/2a/d1a88066b1c14186f5d3c0d18c94f17b064511982bab0578d49ee9d43c29/numpy-2.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:17a25e09640602e10bc8de0e6fa2b3fd68eedd84ba6d7842dc8f32f9ab87bd0b", size = 10350488, upload-time = "2026-07-04T17:06:37.785Z" }, - { url = "https://files.pythonhosted.org/packages/eb/07/ec2a3f0c91761581d4b7104a740791800025983f9a4dc4e73f91a99aeac4/numpy-2.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0bfebd8695f9863592fe744be833a258120b14a9f39da255e8aa8fade2c0ddd1", size = 16796419, upload-time = "2026-07-04T17:06:40.37Z" }, - { url = "https://files.pythonhosted.org/packages/ab/ab/ddb499fc4f8780354395face5b65c7fd107bcd6e1d667a5f07d046956f6f/numpy-2.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:30b44a6b53a7ae63c54c089a8726e5563ed302716c5b7ccc85afade40b0e7ff6", size = 11765832, upload-time = "2026-07-04T17:06:42.768Z" }, - { url = "https://files.pythonhosted.org/packages/88/b3/3c28c558a09fc72100c646dac6d2fce8e834c471b0edca01a29996706117/numpy-2.5.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:6165343f81b56ef8f514f396989e529b61d9dc709b99421b07e9f3e698e2287d", size = 5325143, upload-time = "2026-07-04T17:06:45.466Z" }, - { url = "https://files.pythonhosted.org/packages/5e/0e/ce19b985bb15c596f4f05954e76cccc77c845083b3b8f938a6c68e523128/numpy-2.5.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4939237038ada79308dda3204ac6462df056b5672b2e25db1149cf873668b3e1", size = 6659749, upload-time = "2026-07-04T17:06:47.288Z" }, - { url = "https://files.pythonhosted.org/packages/2e/20/1ee6614d64332a1bba6411f38e68cb79eec1b2459e20a623777c5c5492a2/numpy-2.5.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c6759f538fb912fc46de0a6b1758ccf7b57bc7c7ebebc23974fdac3de8db0cd", size = 15164716, upload-time = "2026-07-04T17:06:49.494Z" }, - { url = "https://files.pythonhosted.org/packages/ed/a7/2bcd3fdbb87804755c35b729bf8709d62025c5f4cfd7d5b2415997097515/numpy-2.5.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9726558e8db4a5bf7929a70ae50f63abda4daf0efe810e3bfbab95976f75fc1a", size = 16661440, upload-time = "2026-07-04T17:06:52.061Z" }, - { url = "https://files.pythonhosted.org/packages/fc/d7/a41e3310c886fe457d36e670bbf24fae411aca8a7b6ad92a32afd924077c/numpy-2.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3935f3b419b244a02732676fa5317a9193cc596a4c0646db07e5b421229ac9f7", size = 16526305, upload-time = "2026-07-04T17:06:54.605Z" }, - { url = "https://files.pythonhosted.org/packages/53/75/4333a9a707c1edd3a4e1a0c58eca52c0f31e55089fa80db02b5565b24df7/numpy-2.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc932a65ded7ce9013d120845a2514dcccb1a67bfc8deb8d37633762951904a6", size = 18423008, upload-time = "2026-07-04T17:06:57.54Z" }, - { url = "https://files.pythonhosted.org/packages/ee/90/e314a32b1c11a2ffe818ddad3a57b50b4b6e1b6c487192eb50cdef0415d0/numpy-2.5.1-cp313-cp313-win32.whl", hash = "sha256:4b4ff1608417eb7a59da7b967bbb798cacfe071d2caf526a24281cd562072ed9", size = 6063885, upload-time = "2026-07-04T17:07:00.14Z" }, - { url = "https://files.pythonhosted.org/packages/10/70/800b3fca480af32df9e8ea9f3d4a0c8feb4b32d7f195d174eabbda4829ad/numpy-2.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:6c3fe51bc6a16453d452997053454f309e8e0ed7b42d6b361ce4ac8c32913d74", size = 12425674, upload-time = "2026-07-04T17:07:02.387Z" }, - { url = "https://files.pythonhosted.org/packages/8b/0b/196350c122f50f6ca56846f2d71efd5e0d24b7b2e07355e019b2e2c7a11e/numpy-2.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:f7feb014281029e628ba2d5a007407443b06e418b6fe451d1e2adcbc8eba0107", size = 10350256, upload-time = "2026-07-04T17:07:04.878Z" }, - { url = "https://files.pythonhosted.org/packages/db/f4/731b6085a83faf6ca843394cbd5e217280c214399f7e8b21b9f552af0ae2/numpy-2.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7c786fe9a5bbe360022e584c5a34cf6b54265c71bd7ec8ac3d8fec38968071f8", size = 16795063, upload-time = "2026-07-04T17:07:07.374Z" }, - { url = "https://files.pythonhosted.org/packages/bf/64/0e215f2048dd11a55bb989ed41b3585ef57452404e638d703a211a3e4157/numpy-2.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32985c896d897419ef8da6917872d80b78ad0ea26d85b23245c7366ffde76d75", size = 11776652, upload-time = "2026-07-04T17:07:09.907Z" }, - { url = "https://files.pythonhosted.org/packages/b5/59/2b844c7a6e9deff69b404a66221e1542937734f65d5e6e39411876053862/numpy-2.5.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:efd736408cc97c79b9e6917338dfc8f06013b2274f992e96b1d9a81a71e2a2c2", size = 5335944, upload-time = "2026-07-04T17:07:12.227Z" }, - { url = "https://files.pythonhosted.org/packages/86/51/9bf7cb2cabcebc9e017e4ec7e6322b378317a542c08b4cb68479c1efc716/numpy-2.5.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:ab84dc6b074fa881cae55bea94cc4f68e285181ba7f32497bf7dee6b1496165b", size = 6656266, upload-time = "2026-07-04T17:07:14.368Z" }, - { url = "https://files.pythonhosted.org/packages/83/3e/fb7615b211b82a32f44d5180a6d421b61f84d4fadd578b48ba4ac34e189f/numpy-2.5.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caf3e317d33d60c37986b452613f4ab51246d0691350c03d0cb4a898627f4a95", size = 15179720, upload-time = "2026-07-04T17:07:16.272Z" }, - { url = "https://files.pythonhosted.org/packages/41/5f/0f992cb24560673496c5d68de61913b57166ce530ffda07c1f280e0cc464/numpy-2.5.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54ad769f17bc2d833b620851989f62054fb9ab93c969d9e1dc3c8e3d56beea21", size = 16664835, upload-time = "2026-07-04T17:07:19.021Z" }, - { url = "https://files.pythonhosted.org/packages/a2/2f/97d6475ee91afe2587797d09446f9d3e475ad4cb681662d824809327b75a/numpy-2.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c12afb53450fa976d4c681c50a7423729a4c51c0465ed9f32b8a9cabbc472373", size = 16539135, upload-time = "2026-07-04T17:07:22.015Z" }, - { url = "https://files.pythonhosted.org/packages/c4/5b/4db81e4ba0be7e2776b1de68c82aa862c7f8ec27e1b4927d4ae075e20678/numpy-2.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8c11c405efc5ff6816d5983c96cdfa215bab3428961243af3ff59b228490438", size = 18426684, upload-time = "2026-07-04T17:07:24.941Z" }, - { url = "https://files.pythonhosted.org/packages/1f/64/c0ba2d90724d450279a7df8f32057241070250a26a7e2b5337d77347f481/numpy-2.5.1-cp314-cp314-win32.whl", hash = "sha256:f2479a47f8d5932d1718168a681ad6e536a9df484c83cfcf9de365e164537ace", size = 6116103, upload-time = "2026-07-04T17:07:27.622Z" }, - { url = "https://files.pythonhosted.org/packages/c1/1a/837f9ed7405adcd7a40538792eb169eddd8fa5630c16a1ef49dae71a30f4/numpy-2.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:24d0eb82c0541d3415a33425db64ae439dffccd7b4dbcb30e7c35120205c506a", size = 12562177, upload-time = "2026-07-04T17:07:29.887Z" }, - { url = "https://files.pythonhosted.org/packages/22/ed/49707938b6dd0a78a9178dd93227dc89e4c11af47f5c798d70366e8d0483/numpy-2.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:5a4c988b38d261deeeaad9954e3deb091ad905c94e8bb6708654ef1d97f286b0", size = 10627739, upload-time = "2026-07-04T17:07:32.568Z" }, - { url = "https://files.pythonhosted.org/packages/a6/c7/bb4b882cfe7f299cbc8b66e42e7dd78cf9d14e40f9469fc5e3db7e15b3bd/numpy-2.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a33276be12fa045805f477f22482088b66bb758ffbe89a9d21457de863a32e22", size = 11894709, upload-time = "2026-07-04T17:07:34.941Z" }, - { url = "https://files.pythonhosted.org/packages/40/3f/5af7f4a7f6224aef48017aa82bb6174c7a659d724be0c75017b7e64a55b4/numpy-2.5.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:f089d7b00756190aacf1f5d34bdf38c3c430ac82b4f868f8cede73380460fce7", size = 5453810, upload-time = "2026-07-04T17:07:37.495Z" }, - { url = "https://files.pythonhosted.org/packages/20/c9/3474309bc94d634d3f9c3eddf03250ecb8c22cd948ef16fef69a77cc5d7b/numpy-2.5.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:09e9bfd8d2cf479c7d174804fb3811c53a8e9f20a37444008606b57d6b7a826d", size = 6761189, upload-time = "2026-07-04T17:07:39.563Z" }, - { url = "https://files.pythonhosted.org/packages/90/8a/558ae39fdd55d7e7f7fef9a84a6e964ac6b23edbd2a07e52bb084500507d/numpy-2.5.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e68d8dd1e7eba712948f2053a29ec86917bc70ba1358df869d9f06649ef9cf09", size = 15225039, upload-time = "2026-07-04T17:07:41.682Z" }, - { url = "https://files.pythonhosted.org/packages/63/27/ca7392b2d030277bdf0273e7d23255b3ee57d57a7c170a6f4fb3981e1e5d/numpy-2.5.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99d5095fa265a0c4152e7bb12759e14381ef5496152f1ce58f44bdf55c44beb4", size = 16701306, upload-time = "2026-07-04T17:07:44.611Z" }, - { url = "https://files.pythonhosted.org/packages/02/42/03d53ae7996c44d4374a8262e9dc41671fd56cbb98f7d47ef85cf5da4c6b/numpy-2.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ab87a91b3cc3382b8956095bd8f95e00cf679bb81554339be1a2ba404a1473c1", size = 16589955, upload-time = "2026-07-04T17:07:47.694Z" }, - { url = "https://files.pythonhosted.org/packages/7b/15/6c1784ae469640e65db111e9a34b3d0f14d91e8a38b9ce34810ced370dbb/numpy-2.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:224ca51130ef7da85bea2191625181cb4f337f9cb64b471f10c1a12aa8b60077", size = 18464252, upload-time = "2026-07-04T17:07:50.684Z" }, - { url = "https://files.pythonhosted.org/packages/94/a8/f98e50356cf167df656c526c2dfeec2d7dde182f2a3da4b458a5938e2776/numpy-2.5.1-cp314-cp314t-win32.whl", hash = "sha256:6eab239876581b2b3c5a242281b6007bbdbcd1c7085d7709bb57c5929b11e6bf", size = 6263298, upload-time = "2026-07-04T17:07:53.445Z" }, - { url = "https://files.pythonhosted.org/packages/72/ac/96ae880cdecad0b3275d9359fcec72667b49a4863c9f12942e43679dda02/numpy-2.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:83ce9c80d5b521b0d77ddcbe5447c218d247929b6cc056ca5351342accfff0af", size = 12748623, upload-time = "2026-07-04T17:07:55.384Z" }, - { url = "https://files.pythonhosted.org/packages/a1/5a/4d2b1601df3602dba7a14f3348ba9bfe94a18adb428e693df6154c293831/numpy-2.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:5a6db61f9aaa57e369905c67d852045d3c4f7126405b29d09b19dec118e9c9cb", size = 10697674, upload-time = "2026-07-04T17:07:58.506Z" }, +name = "ollama" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/72/5f12423b6b39ca8430fbe56f77fcf4ef60f63067c7c4a2e30e200ed9ec16/ollama-0.6.2.tar.gz", hash = "sha256:936d55daa684f474364c098611c933626f8d6c7d67065c5b7ae0c477b508b07f", size = 53145, upload-time = "2026-04-29T21:21:15.018Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/ab/d6722beeb2d10f7a3b9ff49375708904fde18f82b5609a0bc4aeb5996a4d/ollama-0.6.2-py3-none-any.whl", hash = "sha256:3ad7daab28e5a973445c36a73882a3ef698c2ebb00e21e308652741577509f7d", size = 15115, upload-time = "2026-04-29T21:21:13.794Z" }, ] [[package]] name = "omop-llm" source = { editable = "." } dependencies = [ - { name = "instructor" }, - { name = "numpy" }, - { name = "openai" }, - { name = "prompt-spec" }, + { name = "any-llm-sdk", extra = ["gemini", "ollama"] }, + { name = "httpx" }, { name = "pydantic" }, ] [package.optional-dependencies] dev = [ + { name = "instructor" }, { name = "mkdocs" }, { name = "mkdocs-material" }, { name = "mkdocs-mermaid2-plugin" }, { name = "mkdocstrings", extra = ["python"] }, { name = "mypy" }, { name = "pytest" }, + { name = "pytest-asyncio" }, { name = "pytest-cov" }, { name = "ruff" }, { name = "ty" }, { name = "types-pyyaml" }, ] +instructor = [ + { name = "instructor" }, +] [package.metadata] requires-dist = [ - { name = "instructor", specifier = ">=1.13.0" }, + { name = "any-llm-sdk", extras = ["gemini", "ollama"], specifier = ">=1.22.0" }, + { name = "httpx" }, + { name = "instructor", marker = "extra == 'dev'", specifier = ">=1.13.0" }, + { name = "instructor", marker = "extra == 'instructor'", specifier = ">=1.13.0" }, { name = "mkdocs", marker = "extra == 'dev'", specifier = "<2.0" }, { name = "mkdocs-material", marker = "extra == 'dev'" }, { name = "mkdocs-mermaid2-plugin", marker = "extra == 'dev'" }, { name = "mkdocstrings", extras = ["python"], marker = "extra == 'dev'" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.19.1" }, - { name = "numpy" }, - { name = "openai" }, - { name = "prompt-spec", specifier = ">=0.1.4" }, { name = "pydantic" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0.2" }, + { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=1.0.0" }, { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=7.0.0" }, { name = "ruff", marker = "extra == 'dev'" }, { name = "ty", marker = "extra == 'dev'", specifier = ">=0.0.59" }, { name = "types-pyyaml", marker = "extra == 'dev'", specifier = ">=6.0.12.20250915" }, ] -provides-extras = ["dev"] +provides-extras = ["dev", "instructor"] [[package]] name = "openai" @@ -1695,15 +1564,15 @@ wheels = [ ] [[package]] -name = "openpyxl" -version = "3.1.5" +name = "openresponses-types" +version = "2.3.0.post1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "et-xmlfile" }, + { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464, upload-time = "2024-06-28T14:03:44.161Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/26/b612c3215f5599714fa94d63eb5ee59b4eb66dbdeeaf86bb4d848359484d/openresponses_types-2.3.0.post1.tar.gz", hash = "sha256:11b8896d3621d2ac2439f6ff106f34ddcb1bbd517c317a6c852a9df2e98a0753", size = 19254, upload-time = "2026-01-22T20:02:03.933Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" }, + { url = "https://files.pythonhosted.org/packages/b2/5f/e16dad89ed24f586da5b01b9b206d3adbf21fe1af8e4dc55d5b93158fde6/openresponses_types-2.3.0.post1-py3-none-any.whl", hash = "sha256:88f6abcef9cad839203abff420dd080978bf6eb33cc06ddc5d78da4ccdba7613", size = 13847, upload-time = "2026-01-22T20:02:02.582Z" }, ] [[package]] @@ -1724,15 +1593,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, ] -[[package]] -name = "parse" -version = "1.22.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a4/f2/0b504486c2a5564798607d3860e48ed19c6443d5e9cc3ec61cc6b8b4ef58/parse-1.22.1.tar.gz", hash = "sha256:d3a4740ec3da338e2b258b2d69741b731eadfddca59e24a14bc4ee5fce38c911", size = 36970, upload-time = "2026-05-26T03:44:52.624Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/c5/7c16e99869e1f422629092cfd23e3b58e461988c3f9c36fd3624bb4142e6/parse-1.22.1-py2.py3-none-any.whl", hash = "sha256:20f0925a46f06602485ac90d751764d0697fd8455aaa97489ba8953a4b66de32", size = 20925, upload-time = "2026-05-26T03:44:51.156Z" }, -] - [[package]] name = "pathspec" version = "1.1.1" @@ -1760,52 +1620,6 @@ 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 = "prefixcommons" -version = "0.1.12" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "pytest-logging" }, - { name = "pyyaml" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7d/b5/c5b63a4bf5dedb36567181fdb98dbcc7aaa025faebabaaffa2f5eb4b8feb/prefixcommons-0.1.12.tar.gz", hash = "sha256:22c4e2d37b63487b3ab48f0495b70f14564cb346a15220f23919eb0c1851f69f", size = 24063, upload-time = "2022-07-19T00:06:12.478Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/31/e8/715b09df3dab02b07809d812042dc47a46236b5603d9d3a2572dbd1d8a97/prefixcommons-0.1.12-py3-none-any.whl", hash = "sha256:16dbc0a1f775e003c724f19a694fcfa3174608f5c8b0e893d494cf8098ac7f8b", size = 29482, upload-time = "2022-07-19T00:06:08.709Z" }, -] - -[[package]] -name = "prefixmaps" -version = "0.2.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "curies" }, - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/4d/cf/f588bcdfd2c841839b9d59ce219a46695da56aa2805faff937bbafb9ee2b/prefixmaps-0.2.6.tar.gz", hash = "sha256:7421e1244eea610217fa1ba96c9aebd64e8162a930dc0626207cd8bf62ecf4b9", size = 709899, upload-time = "2024-10-17T16:30:57.738Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/89/b2/2b2153173f2819e3d7d1949918612981bc6bd895b75ffa392d63d115f327/prefixmaps-0.2.6-py3-none-any.whl", hash = "sha256:f6cef28a7320fc6337cf411be212948ce570333a0ce958940ef684c7fb192a62", size = 754732, upload-time = "2024-10-17T16:30:55.731Z" }, -] - -[[package]] -name = "prompt-spec" -version = "0.1.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "linkml" }, - { name = "linkml-runtime" }, - { name = "openai" }, - { name = "pydantic" }, - { name = "python-dotenv" }, - { name = "ruamel-yaml" }, - { name = "typer" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/24/2e/904ea01123027bfb648ddbd9b651beb5d2836836f7f12b33e4f938c282de/prompt_spec-0.1.4.tar.gz", hash = "sha256:8414e5a6942d213f6405d66487d0cf182bf7657181b24cd7b6fe7e8f2446caf2", size = 12807, upload-time = "2026-01-09T12:22:23.5Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/20/96/c175c088a1a40611a49ad36ffcce021a170dbee546b292d3564d883df32a/prompt_spec-0.1.4-py3-none-any.whl", hash = "sha256:e12e0cac5a93367974023c37fdace258cc729efd732a20b49a683e477f26f70e", size = 14315, upload-time = "2026-01-09T12:22:22.104Z" }, -] - [[package]] name = "propcache" version = "0.5.2" @@ -1900,6 +1714,63 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, ] +[[package]] +name = "proto-plus" +version = "1.28.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/3e/29e0d6a2c5adde6ab5772253fd16ab346324026b89a66e354689c86d0584/proto_plus-1.28.2.tar.gz", hash = "sha256:26d843eb99c1e32fdf1d20ff0faae56607f7748fe774acf9ecd5cfe6c6472501", size = 58063, upload-time = "2026-07-22T16:28:29.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/84/4e9a53a062d4073c74897a6bd20fff74d55307341b3e85c081002462b3ef/proto_plus-1.28.2-py3-none-any.whl", hash = "sha256:b874236fcac2358f601e4330bcb76cb8b89c851303ccf4078408b3d4774d1c52", size = 50693, upload-time = "2026-07-22T16:28:24.059Z" }, +] + +[[package]] +name = "protobuf" +version = "7.35.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/01/9ef0afd7999eb9badb3a768b4aedd78c86d4c65cfaf1958ab276199e76b4/protobuf-7.35.1.tar.gz", hash = "sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a", size = 458717, upload-time = "2026-06-11T21:55:40.257Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/03/8aeeb7458d22546bf64b5250ca1daeb5ff757d900e8e4a7476c6f0db843e/protobuf-7.35.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6", size = 433226, upload-time = "2026-06-11T21:55:31.719Z" }, + { url = "https://files.pythonhosted.org/packages/37/4b/dfb89eb0e652a1ff073c39a59fb5e3a83cfe9b57a2c83fa6d78270101767/protobuf-7.35.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799", size = 328847, upload-time = "2026-06-11T21:55:34.035Z" }, + { url = "https://files.pythonhosted.org/packages/0f/58/dc12f2cd484951524af6e3382c785869b9b3fb5e52ee95ae23add53ee8f9/protobuf-7.35.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4", size = 344030, upload-time = "2026-06-11T21:55:34.941Z" }, + { url = "https://files.pythonhosted.org/packages/e4/be/5b3cfe508bfab6761414ff944e3366eb13be4fd71efcd69450f89ba39f43/protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4", size = 327130, upload-time = "2026-06-11T21:55:35.921Z" }, + { url = "https://files.pythonhosted.org/packages/d8/bc/6d6c7ba8709c85f8f2c390b2b118d6fb08a783676a572271851bf45a7d22/protobuf-7.35.1-cp310-abi3-win32.whl", hash = "sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30", size = 428945, upload-time = "2026-06-11T21:55:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/0a/19/8d0cb6f20a1ef7b18f1c8986ad5783f22f84cce39c6ce9a6e645ea55192e/protobuf-7.35.1-cp310-abi3-win_amd64.whl", hash = "sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87", size = 439996, upload-time = "2026-06-11T21:55:38.123Z" }, + { url = "https://files.pythonhosted.org/packages/19/c7/5f7c636ec43e0c545e28d1f1db71990108306f7bdcb89f069ba97e428e7f/protobuf-7.35.1-py3-none-any.whl", hash = "sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9", size = 171659, upload-time = "2026-06-11T21:55:39.155Z" }, +] + +[[package]] +name = "pyasn1" +version = "0.6.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/9a/23310166d960def5897e91fe20e5b724601b02a22e84ba1f94232c0b7f67/pyasn1-0.6.4.tar.gz", hash = "sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81", size = 151262, upload-time = "2026-07-09T01:12:33.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl", hash = "sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b", size = 84410, upload-time = "2026-07-09T01:12:32.92Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + [[package]] name = "pydantic" version = "2.13.4" @@ -1999,20 +1870,6 @@ 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 = "pyjsg" -version = "0.12.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "antlr4-python3-runtime" }, - { name = "jsonasobj" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c4/4e/192169ba1066454f016927fc46c7e595a6c701fd1173bd249efcf6de40b3/pyjsg-0.12.4.tar.gz", hash = "sha256:bb1c0ff1f50846d2b5185b182e28b0b6978eae51a2078ce3eb1e0f28dea7b9ab", size = 149852, upload-time = "2026-05-01T14:43:44.665Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/09/05/d129d016f5124adb882816bdaef44bb877e313ceb0a109abcf553f1ac90c/pyjsg-0.12.4-py3-none-any.whl", hash = "sha256:a57ae58bfd7192b32654a0024bc6462fb459d54e837f0b2b5cff0726aad2e557", size = 81728, upload-time = "2026-05-01T14:43:43.394Z" }, -] - [[package]] name = "pymdown-extensions" version = "11.0.1" @@ -2026,66 +1883,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d6/54/da572c98c0b77626a91b5d3b89f0231d8bff5125c225420908632f8b342d/pymdown_extensions-11.0.1-py3-none-any.whl", hash = "sha256:db3943a62bab7e03af1364f0c4083e64b91fb097675a4b6cceccfbe9a77e5eb2", size = 269455, upload-time = "2026-07-02T17:59:21.271Z" }, ] -[[package]] -name = "pyparsing" -version = "3.3.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, -] - -[[package]] -name = "pyshex" -version = "0.9.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cfgraph" }, - { name = "chardet" }, - { name = "pyshexc" }, - { name = "rdflib-shim" }, - { name = "requests" }, - { name = "shexjsg" }, - { name = "sparqlslurper" }, - { name = "sparqlwrapper" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/85/ca/f0e6ecd16e65318f69fe5937982955c340a9e5828dcb391371100577c174/pyshex-0.9.0.tar.gz", hash = "sha256:87288b5e5613f734f55f0085334558218ff618fb1061aabdcee19841092b3eca", size = 508959, upload-time = "2026-05-07T12:55:05.188Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/40/ea/66c21d1f5fec82e6218a70b5672870f76878f41bf3b9570235b4e7223118/pyshex-0.9.0-py3-none-any.whl", hash = "sha256:d81344deed686b7c169f23156221ae281225e2ba02b14fe9810335afdefffa9d", size = 54742, upload-time = "2026-05-07T12:55:03.803Z" }, -] - -[[package]] -name = "pyshexc" -version = "0.10.3.post1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "antlr4-python3-runtime" }, - { name = "chardet" }, - { name = "jsonasobj" }, - { name = "pyjsg" }, - { name = "rdflib-shim" }, - { name = "shexjsg" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/36/c5/81196cd2ab23953c4a8d39c7627437984bf8566eae896c5745356a7de7c8/pyshexc-0.10.3.post1.tar.gz", hash = "sha256:80d9d067c80af9a796e3c1c47d2207edf2e9a9fc39d3ca0ce5dd2019334ea915", size = 130019, upload-time = "2026-05-01T11:34:19.23Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/1d/d8d5be9e72e518b42f544e196de9c07161b0933143c9d0e4e2e33de60d79/pyshexc-0.10.3.post1-py3-none-any.whl", hash = "sha256:5d247f2822ef9864152545935d93a07dce66640608ea9414c96f69da7fe7a168", size = 71730, upload-time = "2026-05-01T11:34:17.836Z" }, -] - -[[package]] -name = "pystow" -version = "0.8.21" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "backports-zstd", marker = "python_full_version < '3.14'" }, - { name = "tqdm" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5a/08/e572a7b66ba91b5335bca61afa2250d6419dc391ebf3cb5ac8795748dee0/pystow-0.8.21.tar.gz", hash = "sha256:460c299093d3e6f45433141ba0c5bc5d99c7fe98b042a6f578040d5103db7aab", size = 54881, upload-time = "2026-07-04T10:25:21.439Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/ac/991733a89b74f62d05d3ad6da773e2d36e26a3a71b6ee6b9ef82b450b486/pystow-0.8.21-py3-none-any.whl", hash = "sha256:7b14f77f0395b93a3a94d85526972c98d526d86a8efa5036363b9da3e2d34003", size = 62430, upload-time = "2026-07-04T10:25:20.274Z" }, -] - [[package]] name = "pytest" version = "9.1.1" @@ -2103,27 +1900,31 @@ wheels = [ ] [[package]] -name = "pytest-cov" -version = "7.1.0" +name = "pytest-asyncio" +version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "coverage" }, - { name = "pluggy" }, { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -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" } +sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/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" }, + { url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" }, ] [[package]] -name = "pytest-logging" -version = "2015.11.4" +name = "pytest-cov" +version = "7.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/dc/1e/fb11174c9eaebcec27d36e9e994b90ffa168bc3226925900b9dbbf16c9da/pytest-logging-2015.11.4.tar.gz", hash = "sha256:cec5c85ecf18aab7b2ead5498a31b9f758680ef5a902b9054ab3f2bdbb77c896", size = 3916, upload-time = "2015-11-04T12:15:54.122Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] [[package]] name = "python-dateutil" @@ -2137,15 +1938,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] -[[package]] -name = "python-dotenv" -version = "1.2.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, -] - [[package]] name = "pyyaml" version = "6.0.3" @@ -2204,57 +1996,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, ] -[[package]] -name = "rdflib" -version = "7.6.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyparsing" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/98/f5/18bb77b7af9526add0c727a3b2048959847dc5fb030913e2918bf384fec3/rdflib-7.6.0.tar.gz", hash = "sha256:6c831288d5e4a5a7ece85d0ccde9877d512a3d0f02d7c06455d00d6d0ea379df", size = 4943826, upload-time = "2026-02-13T07:15:55.938Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/10/c2/6604a71269e0c1bd75656d5a001432d16f2cc5b8c057140ec797155c295e/rdflib-7.6.0-py3-none-any.whl", hash = "sha256:30c0a3ebf4c0e09215f066be7246794b6492e054e782d7ac2a34c9f70a15e0dd", size = 615416, upload-time = "2026-02-13T07:15:46.487Z" }, -] - -[[package]] -name = "rdflib-jsonld" -version = "0.6.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "rdflib" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5a/48/9eaecac5f5ba6b31dd932fbbe67206afcbd24a7a696c03c6c920ac7ddc39/rdflib-jsonld-0.6.1.tar.gz", hash = "sha256:eda5a42a2e09f80d4da78e32b5c684bccdf275368f1541e6b7bcddfb1382a0e0", size = 130465, upload-time = "2021-09-14T12:22:20.082Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/d2/760527679057a7dad67f4e41f3e0c463b247f0bdbffc594e0add7c9077d6/rdflib_jsonld-0.6.1-py2.py3-none-any.whl", hash = "sha256:bcf84317e947a661bae0a3f2aee1eced697075fc4ac4db6065a3340ea0f10fc2", size = 16381, upload-time = "2021-09-14T12:22:17.805Z" }, -] - -[[package]] -name = "rdflib-shim" -version = "1.0.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "rdflib" }, - { name = "rdflib-jsonld" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1b/c8/1014ec6b5f4428c630deffba1f9851043ae378eb1d6ef52a03bd492cea99/rdflib_shim-1.0.3.tar.gz", hash = "sha256:d955d11e2986aab42b6830ca56ac6bc9c893abd1d049a161c6de2f1b99d4fc0d", size = 7783, upload-time = "2021-12-21T16:31:06.945Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/97/d8a785d2c7131c731c90cb0e65af9400081af4380bea4ec04868dc21aa92/rdflib_shim-1.0.3-py3-none-any.whl", hash = "sha256:7a853e7750ef1e9bf4e35dea27d54e02d4ed087de5a9e0c329c4a6d82d647081", size = 5190, upload-time = "2021-12-21T16:31:05.719Z" }, -] - -[[package]] -name = "referencing" -version = "0.37.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "rpds-py" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, -] - [[package]] name = "requests" version = "2.34.2" @@ -2270,27 +2011,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, ] -[[package]] -name = "rfc3339-validator" -version = "0.1.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513, upload-time = "2021-05-12T16:37:54.178Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490, upload-time = "2021-05-12T16:37:52.536Z" }, -] - -[[package]] -name = "rfc3987" -version = "1.3.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/14/bb/f1395c4b62f251a1cb503ff884500ebd248eed593f41b469f89caa3547bd/rfc3987-1.3.8.tar.gz", hash = "sha256:d3c4d257a560d544e9826b38bc81db676890c79ab9d7ac92b39c7a253d5ca733", size = 20700, upload-time = "2018-07-29T17:23:47.954Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/65/d4/f7407c3d15d5ac779c3dd34fbbc6ea2090f77bd7dd12f207ccf881551208/rfc3987-1.3.8-py2.py3-none-any.whl", hash = "sha256:10702b1e51e5658843460b189b185c0366d2cf4cff716f13111b0ea9fd2dce53", size = 13377, upload-time = "2018-07-29T17:23:45.313Z" }, -] - [[package]] name = "rich" version = "14.3.4" @@ -2304,120 +2024,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/76/6d163cfac87b632216f71879e6b2cf17163f773ff59c00b5ff4900a80fa3/rich-14.3.4-py3-none-any.whl", hash = "sha256:07e7adb4690f68864777b1450859253bed81a99a31ac321ac1817b2313558952", size = 310480, upload-time = "2026-04-11T02:57:47.484Z" }, ] -[[package]] -name = "roman-numerals" -version = "4.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2", size = 9077, upload-time = "2025-12-17T18:25:34.381Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, -] - -[[package]] -name = "rpds-py" -version = "2026.6.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051, upload-time = "2026-06-30T07:17:53.009Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/be/2e8974163072e7bab7df1a5acd54c4498e75e35d6d18b864d3a9d5dadc92/rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0", size = 343691, upload-time = "2026-06-30T07:15:14.96Z" }, - { url = "https://files.pythonhosted.org/packages/a4/73/319dfa745dd668efe89309141ded489126461fcecd2b8f3a3cda185129b6/rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf", size = 338542, upload-time = "2026-06-30T07:15:16.267Z" }, - { url = "https://files.pythonhosted.org/packages/21/63/4239893be1c4d09b709b1a8f6be4188f0870084ff547f46606b8a75f1b03/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24", size = 368180, upload-time = "2026-06-30T07:15:17.62Z" }, - { url = "https://files.pythonhosted.org/packages/1c/ca/9c5de382225234ceb37b1844ebdb140db12b2a278bb9efe2fcd19f6c82ce/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e", size = 375067, upload-time = "2026-06-30T07:15:18.952Z" }, - { url = "https://files.pythonhosted.org/packages/87/dc/863f69d1bf04ade34b7fe0d59b9fdf6f0135fe2d7cbca74f1d665589559d/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975", size = 490509, upload-time = "2026-06-30T07:15:20.434Z" }, - { url = "https://files.pythonhosted.org/packages/ce/ef/eac16a12048b45ec7c7fa94f2be3438a5f26bf9cc8580b18a1cfd609b7f6/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680", size = 382754, upload-time = "2026-06-30T07:15:21.831Z" }, - { url = "https://files.pythonhosted.org/packages/04/8f/d2f3f532616be4d06c316ef119683e832bd3d41e112bf3a88f4151c95b17/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6", size = 366189, upload-time = "2026-06-30T07:15:23.371Z" }, - { url = "https://files.pythonhosted.org/packages/e3/29/41a7b0e98a4b44cd676ab7598419623373eb43b20be68c084935c1a8cf88/rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a", size = 377750, upload-time = "2026-06-30T07:15:24.659Z" }, - { url = "https://files.pythonhosted.org/packages/2e/05/ecda0bec46f9a1565090bcdc941d023f6a25aff85fda28f89f8d19878152/rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4", size = 395576, upload-time = "2026-06-30T07:15:25.987Z" }, - { url = "https://files.pythonhosted.org/packages/68/a8/6ed52f03ee6cb854ce78785cc9a9a672eb880e83fd7224d471f667d151f1/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa", size = 543807, upload-time = "2026-06-30T07:15:27.356Z" }, - { url = "https://files.pythonhosted.org/packages/8f/d6/156c0d3eea27ba09b92562ba2364ba124c0a061b199e17eac637cd25a5e2/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc", size = 611187, upload-time = "2026-06-30T07:15:28.931Z" }, - { url = "https://files.pythonhosted.org/packages/f1/31/774212ed989c62f7f310220089f9b0a3fb8f40f5443d1727abd5d9f52bc9/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822", size = 573030, upload-time = "2026-06-30T07:15:30.553Z" }, - { url = "https://files.pythonhosted.org/packages/c9/50/22f73127a41f1ce4f87fe39aadfb9a126345801c274aa93ae88456249327/rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed", size = 202185, upload-time = "2026-06-30T07:15:32.027Z" }, - { url = "https://files.pythonhosted.org/packages/04/3a/f0ee4d4dde9d3b69dedf1b5f74e7a40017046d55052d173e418c6a94f960/rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f", size = 220394, upload-time = "2026-06-30T07:15:33.359Z" }, - { url = "https://files.pythonhosted.org/packages/f3/83/3382fe37f809b59f02aac04dbc4e765b480b46ee0227ed516e3bdc4d3dfc/rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96", size = 215753, upload-time = "2026-06-30T07:15:34.778Z" }, - { url = "https://files.pythonhosted.org/packages/a4/9e/b818ee580026ec578138e961027a68820c40afeb1ec8f6819b54fb99e196/rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223", size = 343012, upload-time = "2026-06-30T07:15:36.005Z" }, - { url = "https://files.pythonhosted.org/packages/f3/6b/686d9dc4359a8f163cfbbf89ee0b4e586431de22fe8248edb63a8cf50d49/rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f", size = 338203, upload-time = "2026-06-30T07:15:37.462Z" }, - { url = "https://files.pythonhosted.org/packages/9e/9b/069aa329940f8207615e091f5eedbbd40e1e15eac68a0790fd05ccdf796c/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f", size = 367984, upload-time = "2026-06-30T07:15:39.008Z" }, - { url = "https://files.pythonhosted.org/packages/14/db/34c203e4becff3703e4d3bc121842c00b8689197f398161203a880052f4e/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7", size = 374815, upload-time = "2026-06-30T07:15:40.253Z" }, - { url = "https://files.pythonhosted.org/packages/ee/7d/8071067d2cc453d916ad836e828c943f575e8a44612537759002a1e07381/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6", size = 490545, upload-time = "2026-06-30T07:15:41.729Z" }, - { url = "https://files.pythonhosted.org/packages/a3/42/da06c5aa8f0484ff07f270787434204d9f4535e2f8c3b51ed402267e63c3/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af", size = 382828, upload-time = "2026-06-30T07:15:43.327Z" }, - { url = "https://files.pythonhosted.org/packages/57/d7/fe978efc2ae50abe48eb7464668ea99f53c010c60aeebb7b35ad27f23661/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf", size = 365678, upload-time = "2026-06-30T07:15:44.992Z" }, - { url = "https://files.pythonhosted.org/packages/69/9d/1d8922e1990b2a6eb532b6ff53d3e73d2b3bbffc84116c75826bee73dfc6/rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885", size = 377811, upload-time = "2026-06-30T07:15:46.523Z" }, - { url = "https://files.pythonhosted.org/packages/b1/3d/198dceafb4fb034a6a47347e1b0735d34e0bd4a50be4e898d408ee66cb14/rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4", size = 395382, upload-time = "2026-06-30T07:15:47.955Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f1/13968e49655d40b6b19d8b9140296bbc6f1d86b3f0f6c346cf9f1adddf4b/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7", size = 543832, upload-time = "2026-06-30T07:15:49.33Z" }, - { url = "https://files.pythonhosted.org/packages/ac/ab/289bcb1b90bd3e40a2900c561fa0e2087345ecbb094f0b870f2345142b7c/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d", size = 611011, upload-time = "2026-06-30T07:15:50.847Z" }, - { url = "https://files.pythonhosted.org/packages/1e/16/5043105e679436ccfbc8e5e0dd2d663ed18a8b8113515fd06a5e5d77c83e/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97", size = 572431, upload-time = "2026-06-30T07:15:52.394Z" }, - { url = "https://files.pythonhosted.org/packages/85/ed/adab103321c0a6565d5ae1c2998349bc3ee175b82ccc5ae8fc04cc413075/rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0", size = 201710, upload-time = "2026-06-30T07:15:53.894Z" }, - { url = "https://files.pythonhosted.org/packages/7b/ed/a03b09668e74e5dabbf2e211f6468e1820c0552f7b0500082da31841bf7b/rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80", size = 219454, upload-time = "2026-06-30T07:15:55.25Z" }, - { url = "https://files.pythonhosted.org/packages/27/17/b8642c12930b71bc2b25831f6708ccf0f75abcd11883932ec9ce54ba3a78/rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb", size = 215063, upload-time = "2026-06-30T07:15:56.573Z" }, - { url = "https://files.pythonhosted.org/packages/b6/36/7fbe9dcdaf857fb3f63c2a2284b62492d95f5e8334e947e5fb6e7f68c9be/rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e", size = 344510, upload-time = "2026-06-30T07:15:57.921Z" }, - { url = "https://files.pythonhosted.org/packages/ba/54/f785cc3d3f60839ca57a5af4927a9f347b07b2799c373fc20f7949f87c7e/rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd", size = 339495, upload-time = "2026-06-30T07:15:59.238Z" }, - { url = "https://files.pythonhosted.org/packages/63/ef/d4cdaf309e6b095b43597103cf8c0b951d6cca2acce68c474f75ec12e0c7/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d", size = 369454, upload-time = "2026-06-30T07:16:01.021Z" }, - { url = "https://files.pythonhosted.org/packages/96/4a/9559a68b7ee15db09d7981212e8c2e219d2a1d6d4faa0391d813c3496a36/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda", size = 374583, upload-time = "2026-06-30T07:16:02.287Z" }, - { url = "https://files.pythonhosted.org/packages/ef/75/8964aa7d2c6e8ac43eba8eb6e6b0fdda1f46d39f2fc3e6aa9f2cb17f485d/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8", size = 492919, upload-time = "2026-06-30T07:16:03.723Z" }, - { url = "https://files.pythonhosted.org/packages/8f/97/6908094ac804115e65aedfd90f1b5fee4eebebd3f6c4cfc5419939267565/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53", size = 383725, upload-time = "2026-06-30T07:16:05.305Z" }, - { url = "https://files.pythonhosted.org/packages/d1/9c/0d1fdc2e7aba23e290d603bc494e97bd205bae262ce33c6b32a69768ed5e/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504", size = 367255, upload-time = "2026-06-30T07:16:07.086Z" }, - { url = "https://files.pythonhosted.org/packages/c4/fe/f0209ca4a9ed074bc8acb44dfd0e81c3122e94c9689f5645b7973a866719/rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc", size = 379060, upload-time = "2026-06-30T07:16:08.525Z" }, - { url = "https://files.pythonhosted.org/packages/c6/8d/f1cc54c616b9d8897de8738aac148d20afca93f68187475fe194d09a71b9/rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77", size = 395960, upload-time = "2026-06-30T07:16:09.989Z" }, - { url = "https://files.pythonhosted.org/packages/fb/04/aafff00f73aeca2945f734f1d483c64ab8f472d0864ab02377fd8e89c3b2/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698", size = 545356, upload-time = "2026-06-30T07:16:11.816Z" }, - { url = "https://files.pythonhosted.org/packages/fd/cc/e229663b9e4ddac5a4acbe9085dd80a71af2a5d356b8b39d6bff233f24b0/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd", size = 612319, upload-time = "2026-06-30T07:16:13.586Z" }, - { url = "https://files.pythonhosted.org/packages/e3/7a/8a0e6d3e6cd066af108b71b43122c3fe158dd9eb86acac626593a2582eb1/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d", size = 573508, upload-time = "2026-06-30T07:16:15.23Z" }, - { url = "https://files.pythonhosted.org/packages/87/03/2a69ab618a789cf6cf85c86bb844c62d090e700ab1a2aa676b3741b6c516/rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8", size = 202504, upload-time = "2026-06-30T07:16:16.893Z" }, - { url = "https://files.pythonhosted.org/packages/85/62/a3892ba945f4e24c78f352e5de3c7620d8479f73f211406a97263d13c7d2/rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5", size = 220380, upload-time = "2026-06-30T07:16:18.108Z" }, - { url = "https://files.pythonhosted.org/packages/3d/e7/c2bd44dc831931815ad11ebb5f430b5a0a4d3caa9de837107876c30c3432/rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703", size = 215976, upload-time = "2026-06-30T07:16:19.654Z" }, - { url = "https://files.pythonhosted.org/packages/79/9c/fff7b74bce9a091ec9a012a03f9ff5f69364eaf9451060dfc4486da2ffdd/rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90", size = 346840, upload-time = "2026-06-30T07:16:21.268Z" }, - { url = "https://files.pythonhosted.org/packages/e9/44/77bcb1168b33704908295533d27f10eb811e9e3e193e8993dc99572211d3/rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4", size = 340282, upload-time = "2026-06-30T07:16:22.875Z" }, - { url = "https://files.pythonhosted.org/packages/87/3c/7a9081c7c9e645b39efe19e4ffbeccd80add246327cd9b888aecffd72317/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9", size = 370403, upload-time = "2026-06-30T07:16:24.415Z" }, - { url = "https://files.pythonhosted.org/packages/f7/69/af47021eb7dad6ff3396cb001c08f0f3c4d06c20253f75be6421a59fe6b7/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f", size = 376055, upload-time = "2026-06-30T07:16:26.111Z" }, - { url = "https://files.pythonhosted.org/packages/81/fc/a3bcf517084396a6dd258c592567a3c011ba4557f2fde23dceaf26e74f2e/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41", size = 494419, upload-time = "2026-06-30T07:16:27.596Z" }, - { url = "https://files.pythonhosted.org/packages/c9/eb/13d529d1788135425c7bf207f8463458ca5d92e43f3f701365b83e9dffc1/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945", size = 384848, upload-time = "2026-06-30T07:16:29.183Z" }, - { url = "https://files.pythonhosted.org/packages/8e/f4/b7ac49f30013aba8f7b9566b1dd07e81de95e708c1374b7bacc5b9bc5c9c/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f", size = 371369, upload-time = "2026-06-30T07:16:30.912Z" }, - { url = "https://files.pythonhosted.org/packages/31/86/6260bafa622f788b07ddec0e52d810305c8b9b0b8c27f58a2ab04bf62b4f/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1", size = 379673, upload-time = "2026-06-30T07:16:32.486Z" }, - { url = "https://files.pythonhosted.org/packages/19/c3/03f1ee79a047b48daeca157c89a18509cde22b6b951d642b9b0af1be660a/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e", size = 397500, upload-time = "2026-06-30T07:16:34.471Z" }, - { url = "https://files.pythonhosted.org/packages/f0/95/8ed0cd8c377dca12aea498f119fe639fc474d1461545c39d2b5872eb1c0f/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538", size = 545978, upload-time = "2026-06-30T07:16:36.45Z" }, - { url = "https://files.pythonhosted.org/packages/d3/f2/0eb57f0eaa83f8fc152a7e03de968ab77e1f00732bebc892b190c6eebde7/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db", size = 613350, upload-time = "2026-06-30T07:16:38.213Z" }, - { url = "https://files.pythonhosted.org/packages/5b/de/e0674bdbc3ef7634989b3f854c3f34bc1f587d36e5bfdc5c378d57034619/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2", size = 576486, upload-time = "2026-06-30T07:16:39.797Z" }, - { url = "https://files.pythonhosted.org/packages/f2/f6/21101359743cd136ada781e8210a85769578422ba460672eea0e29739200/rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e", size = 201068, upload-time = "2026-06-30T07:16:41.316Z" }, - { url = "https://files.pythonhosted.org/packages/a6/b2/9574d4d44f7760c2aa32d92a0a4f41698e33f5b204a0bf5c9758f52c79d5/rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2", size = 220600, upload-time = "2026-06-30T07:16:43.091Z" }, - { url = "https://files.pythonhosted.org/packages/08/ae/f23a2697e6ee6340a578b0f136be6483657bef0c6f9497b752bb5c0964bb/rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13", size = 344726, upload-time = "2026-06-30T07:16:44.5Z" }, - { url = "https://files.pythonhosted.org/packages/c3/63/e7b3a1a5358dd32c930a1062d8e15b67fd6e8922e81df9e91706d66ee5c8/rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05", size = 339587, upload-time = "2026-06-30T07:16:46.255Z" }, - { url = "https://files.pythonhosted.org/packages/ec/64/10a85681916ca55fffb91b0a211f84e34297c109243484dd6394660a8a7c/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba", size = 369585, upload-time = "2026-06-30T07:16:48.101Z" }, - { url = "https://files.pythonhosted.org/packages/76/c2/baf95c7c38823e12ba34407c5f5767a89e5cf2233895e56f608167ae9493/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617", size = 375479, upload-time = "2026-06-30T07:16:49.93Z" }, - { url = "https://files.pythonhosted.org/packages/6a/94/0aad06c72d65101e11d33528d438cda99a39ce0da99466e156158f2541d3/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9", size = 492418, upload-time = "2026-06-30T07:16:51.641Z" }, - { url = "https://files.pythonhosted.org/packages/b5/17/de3f5a479a1f056535d7489819639d8cd591ea6281d700390b43b1abd745/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb", size = 384123, upload-time = "2026-06-30T07:16:53.622Z" }, - { url = "https://files.pythonhosted.org/packages/46/7d/bf09bd1b145bb2671c03e1e6d1ab8651858d90d8c7dfeadd85a37a934fd8/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885", size = 367351, upload-time = "2026-06-30T07:16:55.241Z" }, - { url = "https://files.pythonhosted.org/packages/a3/ea/1bb734f314b8be319149ddee80b18bd41372bdcfbdf88d28131c0cd37719/rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a", size = 378827, upload-time = "2026-06-30T07:16:56.841Z" }, - { url = "https://files.pythonhosted.org/packages/4b/93/d9611e5b25e26df9a3649813ed66193ace9347a7c7fc4ab7cf70e94851c0/rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868", size = 395966, upload-time = "2026-06-30T07:16:58.557Z" }, - { url = "https://files.pythonhosted.org/packages/c3/cb/99d77e16e5534ae1d90629bbe419ba6ee170833a6a85e3aa1cc41726fbbc/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187", size = 545680, upload-time = "2026-06-30T07:17:00.164Z" }, - { url = "https://files.pythonhosted.org/packages/59/15/11a29755f790cef7a2f755e8e14f4f0c33f39489e1893a632a2eee59672b/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107", size = 611853, upload-time = "2026-06-30T07:17:01.962Z" }, - { url = "https://files.pythonhosted.org/packages/68/86/0c27547e21644da938fb530f7e1a8148dd24d02db07e7a5f2567a17ce710/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba", size = 573715, upload-time = "2026-06-30T07:17:03.693Z" }, - { url = "https://files.pythonhosted.org/packages/29/71/4d8fcf700931815594bce892255bbd973b94efaf0fc1932b0590df18d886/rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369", size = 202864, upload-time = "2026-06-30T07:17:05.746Z" }, - { url = "https://files.pythonhosted.org/packages/eb/62/b577562de0edbb55b2be85ce5fd09c33e386b9b13eee09833af4240fd5c4/rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146", size = 220430, upload-time = "2026-06-30T07:17:07.471Z" }, - { url = "https://files.pythonhosted.org/packages/c8/95/d6d0b2509825141eef60669a5739eec88dbc6a48053d6c92993a5704defe/rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e", size = 215877, upload-time = "2026-06-30T07:17:09.008Z" }, - { url = "https://files.pythonhosted.org/packages/b7/bf/f3ea278f0afd615c1d0f19cb69043a41526e2bb600c2b536eb192218eb27/rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b", size = 346933, upload-time = "2026-06-30T07:17:10.762Z" }, - { url = "https://files.pythonhosted.org/packages/9d/29/9907bdf1c5346763cf10b7f6852aad86652168c259def904cbe0082c5864/rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690", size = 340274, upload-time = "2026-06-30T07:17:12.266Z" }, - { url = "https://files.pythonhosted.org/packages/6f/2c/8e03767b5778ef25cebf74a7a91a2c3806f8eced4c92cb7406bbe060756d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342", size = 370763, upload-time = "2026-06-30T07:17:14.107Z" }, - { url = "https://files.pythonhosted.org/packages/2e/e1/df2a7e1ba2efd796af26194250b8d42c821b46592311595162af9ef0528d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6", size = 376467, upload-time = "2026-06-30T07:17:15.76Z" }, - { url = "https://files.pythonhosted.org/packages/6b/de/8a0814d1946af29cb068fb259aa8622f856df1d0bab58429448726b537f5/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140", size = 496689, upload-time = "2026-06-30T07:17:17.308Z" }, - { url = "https://files.pythonhosted.org/packages/df/f3/f19e0c852ba13694f5a79f3b719331051573cb5693feacf8a88ffffc3a71/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442", size = 385340, upload-time = "2026-06-30T07:17:18.928Z" }, - { url = "https://files.pythonhosted.org/packages/e2/ae/7ec3a9d2d4351f99e37bcb06b6b6f954512646bfdbf9742e1de727865daf/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12", size = 372179, upload-time = "2026-06-30T07:17:20.539Z" }, - { url = "https://files.pythonhosted.org/packages/d3/ac/9cee911dff2aaa9a5a8354f6610bf2e6a616de9197c5fff4f54f82585f1e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5", size = 379993, upload-time = "2026-06-30T07:17:22.212Z" }, - { url = "https://files.pythonhosted.org/packages/83/6b/7c2a07ba88d1e9a936612f7a5d067467ed03d971d5a06f7d309dff044a7e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf", size = 398909, upload-time = "2026-06-30T07:17:23.66Z" }, - { url = "https://files.pythonhosted.org/packages/97/0b/776ffcb66783637b0031f6d58d6fb55913c8b5abf00aeecd46bf933fb477/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00", size = 546584, upload-time = "2026-06-30T07:17:25.264Z" }, - { url = "https://files.pythonhosted.org/packages/55/33/ba3bc04d7092bd553c9b2b195624992d2cc4f3de1f380b7b93cbee67bd79/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef", size = 614357, upload-time = "2026-06-30T07:17:26.888Z" }, - { url = "https://files.pythonhosted.org/packages/8b/71/14edf065f04630b1a8472f7653cad03f6c478bcf95ea0e6aed55451e33ea/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a", size = 576533, upload-time = "2026-06-30T07:17:28.546Z" }, - { url = "https://files.pythonhosted.org/packages/ba/76/65002b08596c389105720a8c0d22298b8dc25a4baf89b2ce431343c8b1de/rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577", size = 201204, upload-time = "2026-06-30T07:17:30.193Z" }, - { url = "https://files.pythonhosted.org/packages/8c/97/d855d6b3c322d1f27e26f5241c42016b56cf01377ea8ed348285f54652f0/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324", size = 220719, upload-time = "2026-06-30T07:17:31.788Z" }, -] - -[[package]] -name = "ruamel-yaml" -version = "0.19.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/3b/ebda527b56beb90cb7652cb1c7e4f91f48649fbcd8d2eb2fb6e77cd3329b/ruamel_yaml-0.19.1.tar.gz", hash = "sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993", size = 142709, upload-time = "2026-01-02T16:50:31.84Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93", size = 118102, upload-time = "2026-01-02T16:50:29.201Z" }, -] - [[package]] name = "ruff" version = "0.16.0" @@ -2461,18 +2067,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, ] -[[package]] -name = "shexjsg" -version = "0.9.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyjsg" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d4/d6/6e948660a888a37a5100e7bf7109e89b249ed526df430b674da12950de17/shexjsg-0.9.0.tar.gz", hash = "sha256:750016fabdb5487b27e2e714145f3602cd3ac4eb0dd9b7d7751d0cde62c0d1d8", size = 65164, upload-time = "2026-05-04T13:34:38.537Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/86/72/b03ca1560615933f079ba7d291d3532ed95c2a3205911fe71d192654acaa/shexjsg-0.9.0-py3-none-any.whl", hash = "sha256:abf18db2d9895bc46740f68ae699b2ccfe08c783f6e0c038e6077293ad01c0a5", size = 15344, upload-time = "2026-05-04T13:34:36.955Z" }, -] - [[package]] name = "six" version = "1.17.0" @@ -2491,15 +2085,6 @@ 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 = "snowballstemmer" -version = "3.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/f8/0a71edf031f03c40db17503cb8ca78a69a171254e568e7db241b0ab57ea1/snowballstemmer-3.1.1.tar.gz", hash = "sha256:e07bbc54a0d798fe6010a12398422e62a8bfbba95c394fd0956ef58cb4d3e260", size = 123314, upload-time = "2026-06-03T00:56:40.194Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl", hash = "sha256:7e207fa178741da09cdee59d3ecec3827ad5f92b1fc5c9ff3755b639f71f5752", size = 104164, upload-time = "2026-06-03T00:56:38.614Z" }, -] - [[package]] name = "soupsieve" version = "2.9.1" @@ -2509,169 +2094,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0f/2c/437fe806897c2d6cfdc3ee43a18da8bf8e568530a4ae9bac781541ca9896/soupsieve-2.9.1-py3-none-any.whl", hash = "sha256:4f4477399246b7a0c720a88ca2454b11cd6bb9ae4c9d170140786e916776c14c", size = 37404, upload-time = "2026-07-21T16:57:16.421Z" }, ] -[[package]] -name = "sparqlslurper" -version = "0.5.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "rdflib" }, - { name = "rdflib-shim" }, - { name = "sparqlwrapper" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/4f/22/6c375a48851f96b334e147db62ebee615283b87f30398ba94b3551d60984/sparqlslurper-0.5.1.tar.gz", hash = "sha256:9282ebb064fc6152a58269d194cb1e7b275b0f095425a578d75b96dcc851f546", size = 640336, upload-time = "2021-12-21T21:28:04.095Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/77/48ce09fce2836856588beb84f434c1f8812d1428326efd993b619d49d949/sparqlslurper-0.5.1-py3-none-any.whl", hash = "sha256:ae49b2d8ce3dd38df7a40465b228ad5d33fb7e11b3f248d195f9cadfc9cfff87", size = 6555, upload-time = "2021-12-21T21:28:01.95Z" }, -] - -[[package]] -name = "sparqlwrapper" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "rdflib" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/4e/cc/453752fffa759ef41a3ceadb3f167e13dae1a74c1db057d9f6a7affa9240/SPARQLWrapper-2.0.0.tar.gz", hash = "sha256:3fed3ebcc77617a4a74d2644b86fd88e0f32e7f7003ac7b2b334c026201731f1", size = 98429, upload-time = "2022-03-13T23:14:00.671Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/31/89/176e3db96e31e795d7dfd91dd67749d3d1f0316bb30c6931a6140e1a0477/SPARQLWrapper-2.0.0-py3-none-any.whl", hash = "sha256:c99a7204fff676ee28e6acef327dc1ff8451c6f7217dcd8d49e8872f324a8a20", size = 28620, upload-time = "2022-03-13T23:13:58.969Z" }, -] - -[[package]] -name = "sphinx" -version = "9.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "alabaster" }, - { name = "babel" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "docutils" }, - { name = "imagesize" }, - { name = "jinja2" }, - { name = "packaging" }, - { name = "pygments" }, - { name = "requests" }, - { name = "roman-numerals" }, - { name = "snowballstemmer" }, - { name = "sphinxcontrib-applehelp" }, - { name = "sphinxcontrib-devhelp" }, - { name = "sphinxcontrib-htmlhelp" }, - { name = "sphinxcontrib-jsmath" }, - { name = "sphinxcontrib-qthelp" }, - { name = "sphinxcontrib-serializinghtml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl", hash = "sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978", size = 3921742, upload-time = "2025-12-31T15:09:25.561Z" }, -] - -[[package]] -name = "sphinx-click" -version = "6.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "docutils" }, - { name = "sphinx" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9a/ed/a9767cd1b8b7fbdf260a89d5c8c86e20e3536b9878579e5ab7965a291e55/sphinx_click-6.2.0.tar.gz", hash = "sha256:fc78b4154a4e5159462e36de55b8643747da6cda86b3b52a8bb62289e603776c", size = 27035, upload-time = "2025-12-04T19:33:05.437Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/44/bd/cb244695f67f77b0a36200ce1670fc42a6fe2770847e870daab99cc2b177/sphinx_click-6.2.0-py3-none-any.whl", hash = "sha256:1fb1851cb4f2c286d43cbcd57f55db6ef5a8d208bfc3370f19adde232e5803d7", size = 8939, upload-time = "2025-12-04T19:33:04.037Z" }, -] - -[[package]] -name = "sphinxcontrib-applehelp" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, -] - -[[package]] -name = "sphinxcontrib-devhelp" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, -] - -[[package]] -name = "sphinxcontrib-htmlhelp" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, -] - -[[package]] -name = "sphinxcontrib-jsmath" -version = "1.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, -] - -[[package]] -name = "sphinxcontrib-qthelp" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, -] - -[[package]] -name = "sphinxcontrib-serializinghtml" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, -] - -[[package]] -name = "sqlalchemy" -version = "2.0.51" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/02/f1/a7a892f18d4d224e6b26f706531eafccc41e37594d37d304786969ee13cb/sqlalchemy-2.0.51.tar.gz", hash = "sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9", size = 9912201, upload-time = "2026-06-15T15:41:20.012Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/70/e868bc5412acd101a8280f25c95f10eeae0771c4eb806b02491142810ee8/sqlalchemy-2.0.51-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d78702b26ba1c18b2d0fb2ea940ba7f17a9581b42e8361ff93920ebbee1235a", size = 2160291, upload-time = "2026-06-15T16:08:48.918Z" }, - { url = "https://files.pythonhosted.org/packages/e5/1c/71ee0f8a6b9d7316a1ccd30430b4c62b6c2e36adc96017a4e3a72dce49d6/sqlalchemy-2.0.51-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581921d849d6e6f994d560389192955e80e2950e18fcdfe2ccea863e01158e6e", size = 3343835, upload-time = "2026-06-15T16:19:42.613Z" }, - { url = "https://files.pythonhosted.org/packages/2b/7c/7ab9f9aadc5944fdd06612484ed7918fe376ad871a5f50404dc1536e0194/sqlalchemy-2.0.51-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d21ce524ab86c23046e992a5b81cb54c21079c6df6e78b8fc77d77cac70a6b9", size = 3358470, upload-time = "2026-06-15T16:26:38.011Z" }, - { url = "https://files.pythonhosted.org/packages/d0/7d/ff77169fee6186de145a7f2b87006c39638391130abbab2b1f63ac6ea583/sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c5d98a2709840027f5a347c3af0a7c3d5f6c1ff93af2ca1c54494e23cba8f389", size = 3289874, upload-time = "2026-06-15T16:19:45.212Z" }, - { url = "https://files.pythonhosted.org/packages/6f/3b/6c505903710d781b55bc3141ee34a062bf9745a6b5bc7333305b9ed63b33/sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1181256e0f16479691b5616d36375dc2620ad8332b25978763c3d206ad3f3f1d", size = 3321692, upload-time = "2026-06-15T16:26:39.747Z" }, - { url = "https://files.pythonhosted.org/packages/3c/b7/c5ffe50aa2f4d947c9250e1519d939260329a07fe6272edfccd784b3d007/sqlalchemy-2.0.51-cp312-cp312-win32.whl", hash = "sha256:9f380393be5abeb6815f68fd39271b95127173511b6706b0a630a9995d53f8f5", size = 2119674, upload-time = "2026-06-15T16:23:09.543Z" }, - { url = "https://files.pythonhosted.org/packages/25/dc/46a65916af68a06ef6b972c6050ba4c8f97070fe3fb33097d34229d9bef6/sqlalchemy-2.0.51-cp312-cp312-win_amd64.whl", hash = "sha256:2cf39aabdf48e87c1c2c2ed6d20d33ffa0733b3071ce9c5f66357947dd009080", size = 2146670, upload-time = "2026-06-15T16:23:11.048Z" }, - { url = "https://files.pythonhosted.org/packages/54/fe/a210d52fd1a90ecfae8a78e9d8b27e18d733d60818a8bf250ff690b75120/sqlalchemy-2.0.51-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c2056838b6685b72fdb36c99996cf862753461a62f2e84f4196371d3b2d6a07", size = 2157184, upload-time = "2026-06-15T16:08:50.374Z" }, - { url = "https://files.pythonhosted.org/packages/17/6b/2dce8369b199cb855110e056032f94a9f66dacc2237d3d39c115a86eac56/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:483b11bd46bf35fc14c52faf338b04300c9e6ce554bce9b11be85bfec3bc3195", size = 3284735, upload-time = "2026-06-15T16:19:46.934Z" }, - { url = "https://files.pythonhosted.org/packages/53/ff/dbc495b8a14da840faffb353857a72d4190113cac33727906fb997047f0f/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1bed1ee8b01da6088210aa9412023326fb98a599ba502e6118308601dcbef77f", size = 3302756, upload-time = "2026-06-15T16:26:41.336Z" }, - { url = "https://files.pythonhosted.org/packages/cf/d5/fde8f4dddcf518ee15ab35a7c6a28acc32c8ba548d1d2aa451f96e6dbb0b/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:72ca54c952107ba5cd58854b67a5a6268631289d21651a1235396f3b98b47400", size = 3232055, upload-time = "2026-06-15T16:19:49.286Z" }, - { url = "https://files.pythonhosted.org/packages/67/d1/43d3a0ac955a58601c24fa23038b1c55ee3a1ec02c0f96ebb1eae2bcf614/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b3e693d15533a45cd5906f0589f9c35090bef6ef45bf1e8195c424aa0ae06a8d", size = 3269850, upload-time = "2026-06-15T16:26:43.017Z" }, - { url = "https://files.pythonhosted.org/packages/94/df/de669c7054cd47c4439ac34b1b2ee8b804a794791fbb10720e997a2c87c7/sqlalchemy-2.0.51-cp313-cp313-win32.whl", hash = "sha256:b93ab07b5292dbe7e6b8da89475275e7042744283921344b56105f3eeb0f828b", size = 2117721, upload-time = "2026-06-15T16:23:12.36Z" }, - { url = "https://files.pythonhosted.org/packages/d0/8a/403c51d064196bae20a0bc2476577f83a3f8dd299719a97417086b7f2ec5/sqlalchemy-2.0.51-cp313-cp313-win_amd64.whl", hash = "sha256:0f053118c30e53161857a953e4de667d90e274980dccbe5dd3829bbbeece72a5", size = 2143615, upload-time = "2026-06-15T16:23:13.906Z" }, - { url = "https://files.pythonhosted.org/packages/b1/49/a739be2e1d02a96a658eb71ab45d921c874249252358ad24a5bffdd02525/sqlalchemy-2.0.51-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491", size = 2158999, upload-time = "2026-06-15T16:08:51.759Z" }, - { url = "https://files.pythonhosted.org/packages/23/6b/2e0e38cf75c8780eca78d9b2e78164f8bcfd70125e5caa588ff5cbb9c9f4/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d", size = 3282539, upload-time = "2026-06-15T16:19:51.065Z" }, - { url = "https://files.pythonhosted.org/packages/dd/a1/e77854cb5336fd37dc3c6ae3b71de242c98caac5725120be0b526b31cbd0/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54", size = 3287545, upload-time = "2026-06-15T16:26:44.735Z" }, - { url = "https://files.pythonhosted.org/packages/f6/ab/9e17272fd4dac8df3b83c4fbe52b998a1c9d89a843c8c35ff29b74ff7364/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e", size = 3230929, upload-time = "2026-06-15T16:19:52.625Z" }, - { url = "https://files.pythonhosted.org/packages/02/3c/52f408ea701781caee975606beccc48845f2aee8711ac29843d612c0306c/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d", size = 3252888, upload-time = "2026-06-15T16:26:46.454Z" }, - { url = "https://files.pythonhosted.org/packages/24/16/3efd2ee6bc4ca4693a30a1dd17a91b606cae15d517d2a4746611d9b73ce8/sqlalchemy-2.0.51-cp314-cp314-win32.whl", hash = "sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8", size = 2120551, upload-time = "2026-06-15T16:23:15.629Z" }, - { url = "https://files.pythonhosted.org/packages/7b/78/55b12e70f45bccc40d9e483925c065027b3b98ea4cbbdf6f8c2546feaf6c/sqlalchemy-2.0.51-cp314-cp314-win_amd64.whl", hash = "sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499", size = 2146318, upload-time = "2026-06-15T16:23:17.108Z" }, - { url = "https://files.pythonhosted.org/packages/21/db/a9574ed40fed418924b1b1a3e54f47ee3963053b3d3d325a0d36b41f2c08/sqlalchemy-2.0.51-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de", size = 2178920, upload-time = "2026-06-15T15:59:56.285Z" }, - { url = "https://files.pythonhosted.org/packages/bf/90/a1bb5c7cbba76b7bc1fbd586d0a5479a7bc9c27b4a8298f22ec9423b2bb3/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7", size = 3566534, upload-time = "2026-06-15T15:58:35.024Z" }, - { url = "https://files.pythonhosted.org/packages/15/4b/481f1fed30e0e9e8dd24aecbb49f29eb57fe7657ece5cf06ee9b84bb97d8/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72", size = 3535844, upload-time = "2026-06-15T16:02:43.973Z" }, - { url = "https://files.pythonhosted.org/packages/02/71/0aa64aeda645510af0a43f7d9ee70932f0d1dc4263aed34c50ee891d9df3/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23", size = 3475355, upload-time = "2026-06-15T15:58:36.592Z" }, - { url = "https://files.pythonhosted.org/packages/05/db/6061db32316446135a3abae5f308d144ab988a34234726042da3e58b1c63/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522", size = 3486591, upload-time = "2026-06-15T16:02:45.346Z" }, - { url = "https://files.pythonhosted.org/packages/0d/c9/f14fdf71bb8957e0c7e39db69bbdf12b5c80f4ef775fdfa127bf4e0d6760/sqlalchemy-2.0.51-cp314-cp314t-win32.whl", hash = "sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7", size = 2151313, upload-time = "2026-06-15T16:03:39.127Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c6/673e618e6f4f297e126d9b56ea2f6478708f6c1af4e3223835c22e2c3697/sqlalchemy-2.0.51-cp314-cp314t-win_amd64.whl", hash = "sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2", size = 2186280, upload-time = "2026-06-15T16:03:40.569Z" }, - { url = "https://files.pythonhosted.org/packages/e2/22/dbf013a12ec759e54a34a119e9e217435b3f71b2dd5c61a7ade0a25dae87/sqlalchemy-2.0.51-py3-none-any.whl", hash = "sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5", size = 1944334, upload-time = "2026-06-15T16:09:22.418Z" }, -] - [[package]] name = "tenacity" version = "9.1.4" @@ -2763,24 +2185,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] -[[package]] -name = "tzdata" -version = "2026.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415", size = 198674, upload-time = "2026-07-10T08:50:37.887Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168, upload-time = "2026-07-10T08:50:36.46Z" }, -] - -[[package]] -name = "uri-template" -version = "1.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/31/c7/0336f2bd0bcbada6ccef7aaa25e443c118a704f828a0620c6fa0207c1b64/uri-template-1.3.0.tar.gz", hash = "sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7", size = 21678, upload-time = "2023-06-21T01:49:05.374Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl", hash = "sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363", size = 11140, upload-time = "2023-06-21T01:49:03.467Z" }, -] - [[package]] name = "urllib3" version = "2.7.0" @@ -2815,76 +2219,80 @@ wheels = [ ] [[package]] -name = "webcolors" -version = "25.10.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/7a/eb316761ec35664ea5174709a68bbd3389de60d4a1ebab8808bfc264ed67/webcolors-25.10.0.tar.gz", hash = "sha256:62abae86504f66d0f6364c2a8520de4a0c47b80c03fc3a5f1815fedbef7c19bf", size = 53491, upload-time = "2025-10-31T07:51:03.977Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl", hash = "sha256:032c727334856fc0b968f63daa252a1ac93d33db2f5267756623c210e57a4f1d", size = 14905, upload-time = "2025-10-31T07:51:01.778Z" }, -] - -[[package]] -name = "wrapt" -version = "2.2.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/a4/282c8e64300a59fc834518a54bf0afabb4ff9218b5fa76958b450459a844/wrapt-2.2.2.tar.gz", hash = "sha256:0788e321027c999bf221b667bd4a54aaefd1a36283749a860ac3eb77daed0302", size = 129068, upload-time = "2026-06-20T23:49:44.49Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/85/180b40628b23772692a0c76e8030114e1c0ae068470ed531919f0a5f2a4a/wrapt-2.2.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8417fd3c674d3c8023d080292d29301531a12daf8bd938dd419710dd2f464f2b", size = 81484, upload-time = "2026-06-20T23:47:59.924Z" }, - { url = "https://files.pythonhosted.org/packages/94/f2/21c90f2a16689702e2aaff45795b11018dff2c9b1242bac10d225483f676/wrapt-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e7070c7472582e31af3dfc2622b2381a0df7435110a9388ed8db5ffbce67efb", size = 82151, upload-time = "2026-06-20T23:48:01.303Z" }, - { url = "https://files.pythonhosted.org/packages/5f/b3/7e6e9fcf4fe7e1b69a49fe6cc5a44e8224bab6283c5233c97e132f14908e/wrapt-2.2.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2e096c9d39a59b35b63c9aacfbbbec2088ff51ff1fc31051acc60a07f42f273a", size = 169828, upload-time = "2026-06-20T23:48:02.719Z" }, - { url = "https://files.pythonhosted.org/packages/0b/43/894f132d857ed5a9904d937baf368badcbe5ea9e436e2f1930fe21c9f1f0/wrapt-2.2.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d1a6050405bf334be33bf66296f113563622972a34900ae6fa60fd283a1a900", size = 171544, upload-time = "2026-06-20T23:48:04.266Z" }, - { url = "https://files.pythonhosted.org/packages/29/de/3c833e03725b477e9ea34028224dd21a48781830101e4e036f77e8b6b102/wrapt-2.2.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10adb01371408c6de504a6658b9886480f1a4919a83752748a387a504a21df79", size = 160663, upload-time = "2026-06-20T23:48:05.708Z" }, - { url = "https://files.pythonhosted.org/packages/33/be/27edce350b24e3054d9d047f65f16d4c4d4c1f3f31c4278a1f8a95c723c8/wrapt-2.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3442eee2a5798f9b451f1b2cd7518ce8b7e28a2a364696c414460a0e295c012a", size = 169387, upload-time = "2026-06-20T23:48:07.243Z" }, - { url = "https://files.pythonhosted.org/packages/e2/c4/9fd9679af8bf38e146652c7f47b6b352c3e5795b4ad1c0b7f94e15ac2aa7/wrapt-2.2.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6c99012a22f735a85eed7c4b86a3e99c30fdd57d9e115b2b45f796264b58d0bf", size = 158849, upload-time = "2026-06-20T23:48:08.91Z" }, - { url = "https://files.pythonhosted.org/packages/bc/c2/aa6c0c2206803068c6859dabe01f8c84c43744da93d4c67b8946d21655ee/wrapt-2.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3b686cfc008776a3952d6213cb296ed7f45d782a8453936406faa89eac0835ab", size = 168147, upload-time = "2026-06-20T23:48:10.374Z" }, - { url = "https://files.pythonhosted.org/packages/42/63/3eb25da41049d20ae18fcab2dd8b056e02387c4bfa626cbdfb7c3b872e4f/wrapt-2.2.2-cp312-cp312-win32.whl", hash = "sha256:ef2cce266b5b0b07e19fa82e59673b81142b7a3607c8ed1254113d048ed668da", size = 77734, upload-time = "2026-06-20T23:48:11.769Z" }, - { url = "https://files.pythonhosted.org/packages/da/09/0390e008a305360948fa9ce69507d041ac12cb2ee5d28e34467e2ee79391/wrapt-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:abf8c20a2d72ee69e16328b3c91342c446e723bfe48bfcc4dded3b9722ac027f", size = 80585, upload-time = "2026-06-20T23:48:13.117Z" }, - { url = "https://files.pythonhosted.org/packages/d3/b3/84c445c66969f2d3457276b183a48c91097d59bbef9af6c075366b0f8c36/wrapt-2.2.2-cp312-cp312-win_arm64.whl", hash = "sha256:c6c64c5d02578bc4c4bca4f0aef1504de933c1d5b4ac2710b9131111459506c8", size = 79553, upload-time = "2026-06-20T23:48:14.5Z" }, - { url = "https://files.pythonhosted.org/packages/43/fc/f32f4b22c6511173c11d9e541ab4e7d8467a0f1b3455acaf784115d31ff8/wrapt-2.2.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9e8b648270c613720a202d9a45ebabc33261b22c3a839b115ac5bce8c0bb0d69", size = 81296, upload-time = "2026-06-20T23:48:15.881Z" }, - { url = "https://files.pythonhosted.org/packages/72/06/4d117d5d77a9344776c0248b24dae3d3dd2f58e5f765fa08cf887072e719/wrapt-2.2.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6fb7e94e8fe3e4c3067bb1653a91cce7c5e83acc119fdd41501b1bf74654617", size = 81841, upload-time = "2026-06-20T23:48:17.262Z" }, - { url = "https://files.pythonhosted.org/packages/15/ff/63ad96f98eb58a742b1a20d80f21da88924405910149950b912368150468/wrapt-2.2.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb18fc51e813df0d9c98049e3bf2298a5495a648602040e21fa3c7329371159e", size = 167882, upload-time = "2026-06-20T23:48:18.764Z" }, - { url = "https://files.pythonhosted.org/packages/20/1f/8bb62d8933df7acf3247194e6e9fc68edf9d2fa203252c89c94b319dd472/wrapt-2.2.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94b00b00f806eb3ef2abe9049ed45994a81ee9284884d96e6b8314927c6cea3d", size = 167411, upload-time = "2026-06-20T23:48:20.315Z" }, - { url = "https://files.pythonhosted.org/packages/17/09/8789dcb09ee1de715727db7521aabbb68ffa68dfade3a49468440cfced49/wrapt-2.2.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:62415fd095bc590b842b6d092f2b5d9ccbaeb7e0b28535c03dcea2718b48636b", size = 158607, upload-time = "2026-06-20T23:48:21.728Z" }, - { url = "https://files.pythonhosted.org/packages/9c/20/66e02562d53ee67d841f175e38e3c993c2d78a3e104c576cad61c028b43c/wrapt-2.2.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a41e758d80dc0ab8c210f641ac892009d356cf1f955d97db544c8dd317b4d14c", size = 166367, upload-time = "2026-06-20T23:48:23.177Z" }, - { url = "https://files.pythonhosted.org/packages/bd/a3/832ac4e41222fb263b3042d42c2f08d305db7d0f0c9b1d3a271a9eede8f6/wrapt-2.2.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b84cd4058001c9727b0e9980b7a9e66325b5ca748b1b578e822cade1bc6b304f", size = 157176, upload-time = "2026-06-20T23:48:24.711Z" }, - { url = "https://files.pythonhosted.org/packages/b7/01/1bd5e4d2df9c0178989ac8da9186543465388588ee2ef153e2591accebef/wrapt-2.2.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:26fc73a1b15e0946d2942b9a4426d162b51676338327dc067ccd8d2d76385f94", size = 167025, upload-time = "2026-06-20T23:48:26.118Z" }, - { url = "https://files.pythonhosted.org/packages/1c/69/583ed25291ab53e1ec117135fb1c33425e2f46d2bc8f29c17f7a94cf4274/wrapt-2.2.2-cp313-cp313-win32.whl", hash = "sha256:3c4095803491f6ef72128914c28ec05bbad9758433bb35f6715a3e9c8e46fb2d", size = 77605, upload-time = "2026-06-20T23:48:27.643Z" }, - { url = "https://files.pythonhosted.org/packages/29/68/e69fc6d06e1523c68e0d00f95c9aed1158ce9908ee41603f7f2eae3d5db6/wrapt-2.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:2cb07f414fab25dbe6b5c7398e1491423a5c81a6209533639969a6c928d474a4", size = 80508, upload-time = "2026-06-20T23:48:29.013Z" }, - { url = "https://files.pythonhosted.org/packages/55/21/fe7a393d9e5dc0923bed8f5d857e9dcff210f1fa0888c02cc8f3ffaa55aa/wrapt-2.2.2-cp313-cp313-win_arm64.whl", hash = "sha256:1fc7691f070220215cccb2a20836b9adbaecb8ff22ad47abe63de5f110994fac", size = 79565, upload-time = "2026-06-20T23:48:30.429Z" }, - { url = "https://files.pythonhosted.org/packages/b6/e5/c120d13bf5091164f68c3c1657e84f16f57e71d978421b626393ac5bd7eb/wrapt-2.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ec8f83949028366531383603139403cac7a826e4011955813cdd640017845ce5", size = 83264, upload-time = "2026-06-20T23:48:31.807Z" }, - { url = "https://files.pythonhosted.org/packages/d3/b0/d4a1eb97e0e286625bdf21bc7f702637f9607787ffbbdb5ec14d50c79dbf/wrapt-2.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4b481fb0c40d9fd90a5809911208da700987d373a20a4709dc9e3944af7a6bec", size = 83791, upload-time = "2026-06-20T23:48:33.482Z" }, - { url = "https://files.pythonhosted.org/packages/18/1e/f060df47755e87b57684cee7bfc1362b204df55fac96ffebc0631b697b79/wrapt-2.2.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0065a3b657cec06813b4241d2462ccec287f6863103d7445b725fb3a889736f9", size = 203399, upload-time = "2026-06-20T23:48:34.97Z" }, - { url = "https://files.pythonhosted.org/packages/c4/de/2316a757a1abb6453700b79d83e532146dcef2611348282d4d8889792161/wrapt-2.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30f7424af5c5c345b7f26490e097f74a2ef45b3d08b664dc33571aee3bd3b56c", size = 210461, upload-time = "2026-06-20T23:48:36.569Z" }, - { url = "https://files.pythonhosted.org/packages/ed/29/d1160785ae18ca2495a6d82a21154103d74f656c9fd457fb35f6b11b965a/wrapt-2.2.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:07fdcb012821859168641acf68afad61ef9783cf37100af85f152550e9677194", size = 195313, upload-time = "2026-06-20T23:48:38.175Z" }, - { url = "https://files.pythonhosted.org/packages/f5/2d/7caa9598ae61a9cf0989cc501739cbeeb7d650ab3193cca1407b9af0c6ab/wrapt-2.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f90038ab58fafb584801ca62d72384d7d5225d93c76f7b773c22fae545bd8066", size = 206116, upload-time = "2026-06-20T23:48:39.804Z" }, - { url = "https://files.pythonhosted.org/packages/ac/02/281ea1088b8650d865f311b35cf86fd21df89128e2909714f1161e01c9d0/wrapt-2.2.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:c5d7825491bfa2d08b97e9557768987952c7b9ae687d06c3320b40a37ccb7f20", size = 192668, upload-time = "2026-06-20T23:48:41.346Z" }, - { url = "https://files.pythonhosted.org/packages/be/7d/976e2d5b4b5c5babda40974edd54d0a5585cb60132ed86b46f4b80239b16/wrapt-2.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ad520e6daa9bbf136f14de735474dbec7dcc0891f718e1d274ce8dc92e645af", size = 198891, upload-time = "2026-06-20T23:48:43.056Z" }, - { url = "https://files.pythonhosted.org/packages/59/b7/e47651797c097f75a37e2ce86dcf04048ff576f3a674f7c558df7b5e9622/wrapt-2.2.2-cp313-cp313t-win32.whl", hash = "sha256:25904acb9475f46c24fe0423dbc8fda8cc5fbc282ab3dc6e72e919748c53f4e9", size = 78537, upload-time = "2026-06-20T23:48:44.509Z" }, - { url = "https://files.pythonhosted.org/packages/d1/6f/9fa5d59fb06d890defb5a8f727ce6a14d2932c8760153f96956628559fee/wrapt-2.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:305d4c247d61c4115794a169141823c62f719525ddb90b23aa332741c77d2c28", size = 82005, upload-time = "2026-06-20T23:48:46.391Z" }, - { url = "https://files.pythonhosted.org/packages/15/80/4c7bd9873d1f9f7d138d93556b500469dbe24f42710b877519c2b9eb380d/wrapt-2.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c20279cd1a29800815d7b2d6338b60a6c6e78263f9d6e62e0eda251ba9cae2d0", size = 80762, upload-time = "2026-06-20T23:48:47.964Z" }, - { url = "https://files.pythonhosted.org/packages/24/05/7fd9c3f83b2c74cbfc572a0b88aa37431e04bd8aed70d2c0efd3464206de/wrapt-2.2.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0e64826f920c42d9d9f87e8cc09ffae66c51ede12d59061a5a426deb9aa71745", size = 81341, upload-time = "2026-06-20T23:48:49.39Z" }, - { url = "https://files.pythonhosted.org/packages/4b/68/1bfa43100dd90d4ef74a05897b86275cf57e1313ca14aae2545bc9f872c9/wrapt-2.2.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dcaa5e1451bd8751d7bd1568dfa3321c78092a52a7ecb5d1a0f18a5791e1fd00", size = 81921, upload-time = "2026-06-20T23:48:50.986Z" }, - { url = "https://files.pythonhosted.org/packages/74/eb/df7b7f0b631dbbc750f39be27d8b55f65777d8ac86da80e12be41a644c4b/wrapt-2.2.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0abfd648dac9ac9c5b3aa9b523d27f1789046640b58dcd5652a720ddb325e1fc", size = 167713, upload-time = "2026-06-20T23:48:52.598Z" }, - { url = "https://files.pythonhosted.org/packages/4d/9a/d1bd36f6d088c8e652a9383cabbd49af30b8c576302a7eccddbab6963e3f/wrapt-2.2.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4bfd8d1eb438153eff8b8cfe87f032ba65731e1ce06138b5090f745a33f6f95", size = 166779, upload-time = "2026-06-20T23:48:54.33Z" }, - { url = "https://files.pythonhosted.org/packages/4c/ae/24ffacd4187fac2740a1972093929e836dea092d42c87d728cd98fee11a6/wrapt-2.2.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c427c9d06d859848a69f0d928fe28b5c33a941b2265d10a0e1f15cd244f1ee33", size = 158407, upload-time = "2026-06-20T23:48:55.944Z" }, - { url = "https://files.pythonhosted.org/packages/a3/ed/974427668249a356051e8d67d47fa54ef6c777f0fcf3bae9d292c047d4b6/wrapt-2.2.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4250b43d1a129d947e083c4dc6baf333c9bb34edd26f912d5b0457841fc858ab", size = 166594, upload-time = "2026-06-20T23:48:57.617Z" }, - { url = "https://files.pythonhosted.org/packages/fb/5f/e1d7c6e4523f78db2fbd7826babd0348da1d5e0834c4f918b9ab5757dfae/wrapt-2.2.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:173e5bb5ca350a6e0abab60b7ec7cdd7992a814cb14b4de670a28f067f105663", size = 157068, upload-time = "2026-06-20T23:48:59.171Z" }, - { url = "https://files.pythonhosted.org/packages/1e/c1/7ebd1027f00700c0b0233b20aceef2b4784294ed64971424c4a78e069e34/wrapt-2.2.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:aa14b01804bce36c6d63d7b6a4f55df390f29f8648cc13a1f40b166f4d54680d", size = 166470, upload-time = "2026-06-20T23:49:00.737Z" }, - { url = "https://files.pythonhosted.org/packages/99/eb/974e471a6a978b8180186b8a9dc5ae3361ce269a967190b709b8ce17abfb/wrapt-2.2.2-cp314-cp314-win32.whl", hash = "sha256:58f9f8d637c9a6e245c6ef5b109b67ec187d2faed23d1405656b51d96e0a5b56", size = 78062, upload-time = "2026-06-20T23:49:02.327Z" }, - { url = "https://files.pythonhosted.org/packages/49/ec/e1281156cdc7a66693838ad7a0865ad641c74abd337a957d668b575aaffb/wrapt-2.2.2-cp314-cp314-win_amd64.whl", hash = "sha256:385cb1866f20479e83299af585375bfa0a4b0c6c9907a981483ea782ea8ae406", size = 80832, upload-time = "2026-06-20T23:49:03.837Z" }, - { url = "https://files.pythonhosted.org/packages/45/7d/1b6b5ddd94005a2dac97a4490c9838f3154977850d633abcb65b30089437/wrapt-2.2.2-cp314-cp314-win_arm64.whl", hash = "sha256:8ffbeaea6771a6eba6e6eeb09767864995726bc8240bb54baf88a9bb1db34d5c", size = 80029, upload-time = "2026-06-20T23:49:05.237Z" }, - { url = "https://files.pythonhosted.org/packages/b0/33/9ebcf8aafe91c601127cbd93708c16aa8f688f34a10bf004046803ecdc4f/wrapt-2.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:09f811d43f6f33ec7515f0be76b159569f4057ab54d3e079c3204dddb90afa2a", size = 83357, upload-time = "2026-06-20T23:49:06.632Z" }, - { url = "https://files.pythonhosted.org/packages/39/38/ec45b635153327b52e52732a0ea980e5f00b7efba65f9e018828f1e69daa/wrapt-2.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a795d3c06e5fbf9ea2f13196180b77aeab1b4685917256ee0d014cc163d90063", size = 83794, upload-time = "2026-06-20T23:49:08.098Z" }, - { url = "https://files.pythonhosted.org/packages/4e/ea/1a89e6d3b7a83c3affe5c09cde77792c947e63e4bc85ad84cd5bb9abb0d8/wrapt-2.2.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:45c2f2768e790c9f8db90f239ef23a2af8e7570f25a35619ef902df4a738447f", size = 203362, upload-time = "2026-06-20T23:49:09.811Z" }, - { url = "https://files.pythonhosted.org/packages/19/d8/3b58763d9863b5a73771c0d97110f9595d248db454009e07e1535ee905a4/wrapt-2.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbf00ee0cb55ec24e2b0995a71942b85b21a066db8f3f46e1dbfdb9433ffba81", size = 210449, upload-time = "2026-06-20T23:49:11.521Z" }, - { url = "https://files.pythonhosted.org/packages/2d/6f/17fd9e053103d8be148d20d5d7505facc72d5fe1f9127973904ceaed79cf/wrapt-2.2.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2252f77663651b89255895f58cc6ac08fcb206d4371813e5af61bb62d4f7689c", size = 195349, upload-time = "2026-06-20T23:49:13.346Z" }, - { url = "https://files.pythonhosted.org/packages/ef/04/d0d1ccaaa12cb7dccf28a23f0279a608ba498f71e81d949d5ed54bcfd5c1/wrapt-2.2.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2cd7181ab1c31192ff5219269830744b5a62020b3a6d433588c4f1c95b8f8bff", size = 206099, upload-time = "2026-06-20T23:49:15.051Z" }, - { url = "https://files.pythonhosted.org/packages/44/b3/e8aa07b619890a2aa6cde1931b1887abb08820721b564a5f80b7ca3f3aa0/wrapt-2.2.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:6fe35fd51b74867d8b80174c277bd6bbf6a73e443f908129dc531c4b688a20d5", size = 192728, upload-time = "2026-06-20T23:49:16.854Z" }, - { url = "https://files.pythonhosted.org/packages/b7/f0/1819fb50f0d3c9bd758d8a83b56f1b470dee8b5b8eac8702b7c137cea9d4/wrapt-2.2.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:11d95fc2fbad3163596c39d440e6f21ca9fccece74b56e30a37ac2fca786a07c", size = 198842, upload-time = "2026-06-20T23:49:18.504Z" }, - { url = "https://files.pythonhosted.org/packages/67/7c/e88313f16a99930b899ef970d91c281544a470749a359decad994483bbda/wrapt-2.2.2-cp314-cp314t-win32.whl", hash = "sha256:d8a15813215f33fa83667bfc978b300e35669ea8bb424e970a1426bcb7bc6cca", size = 79059, upload-time = "2026-06-20T23:49:20.107Z" }, - { url = "https://files.pythonhosted.org/packages/a0/4f/ac12fda57a55068a094ec42851fb0a40e8489d8941863d517452de62e507/wrapt-2.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d09db0f7e8357060d3c38fc22a018aba683a796bf184360fd1a58f6fc180dc77", size = 82462, upload-time = "2026-06-20T23:49:21.631Z" }, - { url = "https://files.pythonhosted.org/packages/48/a7/df732dac86d9b2027c56bd163dbc883e037b16c3469614752e148d219c61/wrapt-2.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:f32fe639c39561ccc187bcae17e9271be0eb45f1c2952510d2f29b33ab577347", size = 81182, upload-time = "2026-06-20T23:49:23.199Z" }, - { url = "https://files.pythonhosted.org/packages/6e/d2/6317eb6d4554855bbf12d61857774af34747bf88a42c19bf306de67e2fa3/wrapt-2.2.2-py3-none-any.whl", hash = "sha256:5bad217350f19ce99ca5b5e71d406765ea86fe541628426772b657375ee1c048", size = 61460, upload-time = "2026-06-20T23:49:42.966Z" }, +name = "websockets" +version = "16.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/f7/bc3a25c5ec26ce62ce487690becc2f3710bbc7b33338f005ad390db0b986/websockets-16.1.1.tar.gz", hash = "sha256:db234eda965dcce15df96bb9709f587cd87d4d52aaf0e80e2f34ec04c7670c57", size = 182204, upload-time = "2026-07-17T22:51:05.858Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/9d/681cda21c9eee743203a6cb79b9d3d05adad9aa60ec660c6c9bf4dd619ca/websockets-16.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cc97814dfb786a83b6e2dc2e79351e1b83e6d715647d6887fcabd83026417a00", size = 179600, upload-time = "2026-07-17T22:49:13.92Z" }, + { url = "https://files.pythonhosted.org/packages/fb/8d/6195a88b45e8d2a8f745fc2046e36f885a3c9763e6767d2c46229bf9510c/websockets-16.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e047dc87ef7ca50f4d309bf775ad4a71711c58556d75d7bd0604b2317f43e94b", size = 177272, upload-time = "2026-07-17T22:49:15.453Z" }, + { url = "https://files.pythonhosted.org/packages/73/e3/fe2d498c64dea0095c9a9f9a351af4cd6eef31b618395582bc1f38ba45ff/websockets-16.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:01fbdcbac298efe19360b94bc0039c8f746f0220ba570f327577bfee81059175", size = 177542, upload-time = "2026-07-17T22:49:16.875Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ed/f1831681fce0e3242346e5458486003c5f124ed69e5e0b847fd029db4973/websockets-16.1.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0f62863e8a00a6d33c3d6566ec0b89f23787b747ffe0c3bc71ec0e76b82c94b1", size = 187137, upload-time = "2026-07-17T22:49:18.323Z" }, + { url = "https://files.pythonhosted.org/packages/6f/79/4ff9dcc1bb46f6b4c536936dde1fd60f9b564f3304307274db97f4c9496d/websockets-16.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8087e82f842609734c9b5a1330464f8e94e346ba0e18c832c08bafa4b0d63c15", size = 188374, upload-time = "2026-07-17T22:49:19.65Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/5c49b6efb36cab733d23773f6de575e1dba65736ead17d5d2b2a1daef779/websockets-16.1.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2bb5d041a8307d2e18782e7ce777f6fdb1e8c2f5d09291484b18c294b789d9aa", size = 191155, upload-time = "2026-07-17T22:49:21.331Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f6/56ccceda3a4838d18f1d40821480da4775397e8b1eecf4031e20c50e2e90/websockets-16.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1db4de4a0e95673f7545d393c49eeb0c2f18ac1ef93073218c79d5cdb2ee75ab", size = 189011, upload-time = "2026-07-17T22:49:22.889Z" }, + { url = "https://files.pythonhosted.org/packages/86/d6/ad5286241a2bce1107e2798d3bfbd62cf79aee167bdb654f8cb1e9dbf949/websockets-16.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f17dbe07eb3ea7f99e4df9b7e0efefe80fbf30d37a8cc4d561a0aed310bc8847", size = 187766, upload-time = "2026-07-17T22:49:24.339Z" }, + { url = "https://files.pythonhosted.org/packages/bc/67/d65c970b7e347fdca69479beb7811c2060529956730a7a4e3ae7c66b0e31/websockets-16.1.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4b57693728576d84ede0a77987ab16881b783d2cd9f1dc180a8fbbc3f79c4428", size = 185173, upload-time = "2026-07-17T22:49:25.743Z" }, + { url = "https://files.pythonhosted.org/packages/1d/5b/14af3cd4ee69d8ea9baca58f3dc3cfb1ba78332a347fd478cb096549d60e/websockets-16.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2a636ff1e7a5c4edf71ef0e79adae7f25dba93b4fcbe3dc958733477ffeb0eaf", size = 187809, upload-time = "2026-07-17T22:49:27.147Z" }, + { url = "https://files.pythonhosted.org/packages/7b/11/be301710d70de97e3e7b3586e6d492c9c06d6a61bf1c2202c36cf0c75607/websockets-16.1.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d6bec75c290fe484a8ba4cacdf838501e17c06ecfbbf31eede81a9e431bd7751", size = 186412, upload-time = "2026-07-17T22:49:28.611Z" }, + { url = "https://files.pythonhosted.org/packages/db/07/fe1435bf6fe738a3d3b54dbe0c18dabf12cba4d909ac8b58b539ce27c1f4/websockets-16.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:54509b8e92fee4453e152b7558ddef37ce9705a044922f2095a6105e3f80c96f", size = 188290, upload-time = "2026-07-17T22:49:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0a/81f394aff8efcbb01208c1ced77df0a3c7fcce584a88c7273663697946c2/websockets-16.1.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:f0aa4aad3b1b69ad3fd85a0fd0952ec64331c762bd77ec51cc814170873890b2", size = 185844, upload-time = "2026-07-17T22:49:31.447Z" }, + { url = "https://files.pythonhosted.org/packages/39/5c/dd485b995473f415510251fe9bd708f2d24458f439fce958daf8d66dc7c6/websockets-16.1.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:42290eb6db4ccaca7012656738214f8514082fb6fa40cdeb61bb9a471b52e383", size = 186823, upload-time = "2026-07-17T22:49:33.104Z" }, + { url = "https://files.pythonhosted.org/packages/9d/0b/f78de76ff446f1e66af12b43c48a35f31744de93cfdec2f4ea67d5d7bbf1/websockets-16.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:53260c8930da5771cec89439bff99c20c8cb03ddb9588b980697355a83cd4bd3", size = 187102, upload-time = "2026-07-17T22:49:34.616Z" }, + { url = "https://files.pythonhosted.org/packages/37/a1/4cf892007778eaf84ad162bfc98046e0ed89b63ac55949e3236626b2a23f/websockets-16.1.1-cp312-cp312-win32.whl", hash = "sha256:1d27fa8462ad6a1cb36206a3d0640b2333340def181fae11ed7f9adeaa5c0747", size = 179943, upload-time = "2026-07-17T22:49:36.213Z" }, + { url = "https://files.pythonhosted.org/packages/d9/de/6abe251d28c3a3f217096575400b27750b18e0b1d2fff3a2a239960fea07/websockets-16.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:b436f6ec4fc3a6b4237c84d3f83170ed2b40bb584222f0ac47a0c8a5921980c7", size = 180243, upload-time = "2026-07-17T22:49:37.626Z" }, + { url = "https://files.pythonhosted.org/packages/ce/fd/6ec6c6d2850aea25b1b2aa9901a016980bb87d01e89b3eb00470b1b5d471/websockets-16.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ab59169ace05dcb49a1d4118f0bde139557adf45091bd85747e36bf5de984dd1", size = 179587, upload-time = "2026-07-17T22:49:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d8/1d299d2dd34087db39831a34cc645ef8a6f89d78efada6983093513cd81c/websockets-16.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5e3b7d601f6f84156b08cc4a5e541c2b50ad7b36cfc302b657a12477c904a5df", size = 177272, upload-time = "2026-07-17T22:49:40.293Z" }, + { url = "https://files.pythonhosted.org/packages/3d/86/0a70d3ae2f0f2256bb41302d9804dbca65d4360281e7feb3e1f94102ac46/websockets-16.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cd2ca96a082a36964aca83e992f72abeb61b7306c1a6cba4c7d06a7b93750cac", size = 177530, upload-time = "2026-07-17T22:49:41.786Z" }, + { url = "https://files.pythonhosted.org/packages/b5/c2/c676c69444d9db448b3f0a55a98dcc534affce0bce961d9d2f0b8499b10a/websockets-16.1.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f5d497865f05bb222cab7016c6034542e84e5f29f49c6fd3f4939cda7197b5b8", size = 187197, upload-time = "2026-07-17T22:49:43.658Z" }, + { url = "https://files.pythonhosted.org/packages/0b/13/88137fbaf726ebe29d62c1117fa11fa2bbb6209dc79d4ad738efbe36a2aa/websockets-16.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bae954c382e013d5ea5b190d2830526bfa45ad121c326da0049b8c769f185db6", size = 188433, upload-time = "2026-07-17T22:49:45.147Z" }, + { url = "https://files.pythonhosted.org/packages/01/6d/46c2f2ce6751cb26f39293e1ecbf8544cb01321397cd476c2756b98c216d/websockets-16.1.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e09f753a169951eb4f28c2c774f71069304f66e7277e0f5a2892423599cfa854", size = 189868, upload-time = "2026-07-17T22:49:46.581Z" }, + { url = "https://files.pythonhosted.org/packages/29/2b/170a9e8097636cfde4dc3c592b6e00b18a44a2f5407606d96ca542dd5838/websockets-16.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:024193f8551a2b0eafbdd160911012c4e6c228c28430c84433253299a9e42d6a", size = 189059, upload-time = "2026-07-17T22:49:47.972Z" }, + { url = "https://files.pythonhosted.org/packages/a7/48/f0d4ebc9ab4b473b8861b9e20fdb663d515d42f7befdf62cdb60fee7a1ec/websockets-16.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:aabe464bfd13bd25f4821faf111da6fefdc389f870265a53105580e45b0a2e49", size = 187814, upload-time = "2026-07-17T22:49:49.344Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ba/39a41d3ae8e72696a9492581900611c5a91e2b07563b0bcd2523adea9854/websockets-16.1.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a28fcbc9b6baf54a2e23f8655f308e4ccc6afdd7266f8fe7954f320dcda0f785", size = 185229, upload-time = "2026-07-17T22:49:50.787Z" }, + { url = "https://files.pythonhosted.org/packages/3c/36/ac15b604f850d1907f0a85ed721cefe47cd45034b3620069b829746cccbe/websockets-16.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79eace538c6a97e96d0d03d4f9d314f9677f5ed85a8a984992ffd90b13cb8a56", size = 187874, upload-time = "2026-07-17T22:49:52.228Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f3/3fbd5d71d59299c3770faa5884d4f45070236ca5a35ab3a61830812c409a/websockets-16.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:496af849a472b531f758dbd4d61338f5000538cb1a7b3d20d9d32a264517f509", size = 186469, upload-time = "2026-07-17T22:49:53.776Z" }, + { url = "https://files.pythonhosted.org/packages/b4/fc/dd90349bba58af2a53ef2ddd9c32716c81eb6d59a0687939fff561860878/websockets-16.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5283810d2646741a0d8da2aa733d6aefa0545809afccb2a5d105a26bc45125f1", size = 188347, upload-time = "2026-07-17T22:49:55.202Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f3/f73ba86427682da59b78c11d77ba56d5b801c32e84afe79b274bbd6a9bb2/websockets-16.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4e3b680b1e0a27457e727a0d572fd81dffa87b6dbf8b228ab57da64f7d85aead", size = 185903, upload-time = "2026-07-17T22:49:56.75Z" }, + { url = "https://files.pythonhosted.org/packages/34/7c/f95eb20e80104173b3a0a092291f89ea4047ef6e608e0a57ca06eb14eecb/websockets-16.1.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:69159730a823dde3ea8d08783e8d47ef135a6d7e8d44eb127e32b321c9db8e3e", size = 186855, upload-time = "2026-07-17T22:49:58.467Z" }, + { url = "https://files.pythonhosted.org/packages/b0/35/dd875b3e050ff232d60fa377707f890e369f74d134f1be32e8f68879747c/websockets-16.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ed5bb271084b46530ee2ddc0410537a9961152c5ccba2fc98c5276d992ccba87", size = 187140, upload-time = "2026-07-17T22:50:00.016Z" }, + { url = "https://files.pythonhosted.org/packages/e8/dc/5cbfcb41824502f6af93b8f3943a4d06c67c23c7d2e31eb18748c4a5b2a7/websockets-16.1.1-cp313-cp313-win32.whl", hash = "sha256:cfb70b4eb56cac4da0a83588f3ad50d46beb0690391082f3d4e2d488c70b68ea", size = 179928, upload-time = "2026-07-17T22:50:01.685Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c1/71e5deb5b7f8f226997ab64908c184ac3105c0155ce2d486f318e5dd08a8/websockets-16.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:d9531d9cbeac99af6f038fb1bc351403531f7d634a2c2e10e2f7c854c6ed5b68", size = 180242, upload-time = "2026-07-17T22:50:03.117Z" }, + { url = "https://files.pythonhosted.org/packages/73/a2/ba78a164eeea4620df4a4df4bd2ed6017438c4655cc0f36f2c0bc0432355/websockets-16.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:443aefe96b7fdb132e2a70806cca1f2af49bb3f28e47abcd7c2e9dcf4d8fa1b8", size = 179635, upload-time = "2026-07-17T22:50:05.001Z" }, + { url = "https://files.pythonhosted.org/packages/b9/08/d26d7a7628cd4ac34cbbdb63ac80914ca842ed8e42938c40a53567806df3/websockets-16.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6456ff333092d509127d75a638cb411afae8ff17f092635015d1902efec8a293", size = 177320, upload-time = "2026-07-17T22:50:06.427Z" }, + { url = "https://files.pythonhosted.org/packages/0f/45/ebec83e6269536aa5932533c67b0af5c781f3e73fdbcd68672dcf43f4f44/websockets-16.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fce6c48559c86d1ac3632ccb1bebc7d5442fbe79bd9bb0e40379ee54be2a4051", size = 177544, upload-time = "2026-07-17T22:50:07.834Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d5/abc614d2297f6c1c3e01e61260364457a47c25cc1cf6a879038902bc6aa8/websockets-16.1.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:92b820d345f7a3fc7b8163949ee92df910f290c3fc517b3d5301c78065adafe1", size = 187270, upload-time = "2026-07-17T22:50:09.275Z" }, + { url = "https://files.pythonhosted.org/packages/52/71/4c99af3b87dff1b2927981f6876607d4acb45338c665242168d3982f7758/websockets-16.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a606d9c24035242a3e256e9d5b77ed9cd6bccfcb7cf993e5ca3c0f6f68fb6a7", size = 188509, upload-time = "2026-07-17T22:50:10.722Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b4/5c8ca14b0df7eb84ed0524165c5359150210140817a3312aee57bf62a1cf/websockets-16.1.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:414e596c75f74e0994084694189d7dc9229fb278e33064d6784b73ffbba3ca31", size = 189882, upload-time = "2026-07-17T22:50:12.293Z" }, + { url = "https://files.pythonhosted.org/packages/25/c1/bedfba9e70557129cb8083748d167bdcc01483dedf0f0df143676df05cbe/websockets-16.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:536676848fc5961aca9d20389951f59169508f765637a172403dc5434d722fa0", size = 189114, upload-time = "2026-07-17T22:50:13.789Z" }, + { url = "https://files.pythonhosted.org/packages/df/09/aa835b2787835aebd839114be5de51b797cb480b63ba42b26d34dfe147cb/websockets-16.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:97fd3a0e8b53efa41970ac1dff3d8cf0d2884cadeb4caaf95db7ad1526926ee3", size = 187861, upload-time = "2026-07-17T22:50:15.179Z" }, + { url = "https://files.pythonhosted.org/packages/20/26/f6408330694dbc9830857d9d23bc14ac4f6875127a480cfdda8d5ca21198/websockets-16.1.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7b1b19636af86a3c7995d4d028dbe376f39b4bf31541146f9c123582a6c94562", size = 185286, upload-time = "2026-07-17T22:50:16.741Z" }, + { url = "https://files.pythonhosted.org/packages/17/9a/e0675e70dd8a80762cf35bb18799d3f290a4890ffe6439bc51d222796083/websockets-16.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:41c8e77f17294c0ac18008a7309b99b34ee72247ef10b6dff4c3f8b5ac29896b", size = 187935, upload-time = "2026-07-17T22:50:18.213Z" }, + { url = "https://files.pythonhosted.org/packages/33/c1/3234cfb86afde01b81e9bddcc6e534c440975d60a13991259e833069ab3e/websockets-16.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:9f63bcef7f4b02b06b35fc01c93b96c43b5e88e1e8868676caacf493d5a31f3a", size = 186444, upload-time = "2026-07-17T22:50:19.67Z" }, + { url = "https://files.pythonhosted.org/packages/89/87/9c15206e1d778923d8daa9657de07aa62ea815e13448319c98458c37b281/websockets-16.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dab9eb87869da2d6ed3af3f3adf28414baae6ec9d4df355ffc18889132f3436c", size = 188409, upload-time = "2026-07-17T22:50:21.28Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/cf5de5c67676de2d3eef8b2a518f168f6796595447a5b7161ba0d012915c/websockets-16.1.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:43e3a9fdd7cbf7ba6040c31fae0faf84ca1474fef777c4e37912f1540f854499", size = 185958, upload-time = "2026-07-17T22:50:22.719Z" }, + { url = "https://files.pythonhosted.org/packages/62/c0/731b6ddede2e4136912ec4cff2cffbda35af73546be4762c3d7bd3bd79af/websockets-16.1.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:056ae37939ed7e9974f364f5864e76e49182622d8f9751ac1903c0d09b013985", size = 186911, upload-time = "2026-07-17T22:50:24.108Z" }, + { url = "https://files.pythonhosted.org/packages/8c/7f/39c634472c4469a24a7c09cecddffb08fac6d0e74f73881a94ee8a40a196/websockets-16.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a0eadbbf2c30f01efa58e1f110eb6fa293261f6b0b1aa38f7f48707107690af9", size = 187204, upload-time = "2026-07-17T22:50:25.548Z" }, + { url = "https://files.pythonhosted.org/packages/26/89/9667c256c256dafcc62d21328ce7a40067da857969b68ee9af375b0aaf72/websockets-16.1.1-cp314-cp314-win32.whl", hash = "sha256:195c978b065fa40910582464f99d6b15c8b314c68e0546549a55ed83f4735328", size = 179603, upload-time = "2026-07-17T22:50:27.086Z" }, + { url = "https://files.pythonhosted.org/packages/bd/dd/1c099d6c0fc5deb6b46ccdbb6981fdb4b12c917869cb3952408409dc18db/websockets-16.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:4e8d01cc3bcae7bbf8167f944aeafefed590fae5693552bba9794a9df68371cc", size = 179948, upload-time = "2026-07-17T22:50:28.521Z" }, + { url = "https://files.pythonhosted.org/packages/35/25/9956b2d5e0529d5d23924f21bba1440d4c5c88a562e4f08550871ffa97a7/websockets-16.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0ffd3031ea8bda8d61762e84220186105ba3b748b3c8da2ae4f7816fac03e573", size = 179963, upload-time = "2026-07-17T22:50:29.982Z" }, + { url = "https://files.pythonhosted.org/packages/17/06/55ffc976c488b6aee9ea05761ff7c4e88e7c1fd82818c8ca7b556ad2f90c/websockets-16.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:84a2cef8deffbd9ab8ee0ea546a2a6a7030c28f44e6cdd4547dbfeb489eb8999", size = 177497, upload-time = "2026-07-17T22:50:31.396Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/f7dac2e980bacc92bdc26cebae4ae4d50cae5380732c50980598fc0bbae4/websockets-16.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3df13f73af9b3b38ab1195eb299ecb67a4330c911c97ae04043ff74085728abe", size = 177698, upload-time = "2026-07-17T22:50:32.829Z" }, + { url = "https://files.pythonhosted.org/packages/b2/39/26762f734113e22da2b942c3aca85798e0c0405d64c256549540ff31e5a1/websockets-16.1.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:23253dd5bcae3f9aaee0a1d30967a8dbd52e5d3cff93a2e5b84df57b77d4750d", size = 187561, upload-time = "2026-07-17T22:50:34.24Z" }, + { url = "https://files.pythonhosted.org/packages/11/94/c3f330851806b9b02138b774d593478323e73c99238681b4b93efe64e02d/websockets-16.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1c5705e314449e3308872fe084b8571ce078ee4fc55a98a769bdefe5917392", size = 188732, upload-time = "2026-07-17T22:50:36.088Z" }, + { url = "https://files.pythonhosted.org/packages/d1/f2/eb2c450f052de334ae33cf200ece6e87b0e14d186807074e4eb1cd2cdea2/websockets-16.1.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:69e52d175a0a7d1e13b4b67ad41c560b7d98e8c6f6126eb0bda496c784faf8c7", size = 190872, upload-time = "2026-07-17T22:50:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/70/31/2ac8cecf3a74f7fed9132129fc3d90b3998a1554570c11a69b2a8c20332d/websockets-16.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1f79c89b5eb034d1722938a891916582f8f7f503f58ca22518a63c3f2cd18499", size = 189305, upload-time = "2026-07-17T22:50:39.53Z" }, + { url = "https://files.pythonhosted.org/packages/6a/cf/8ab19650d3c0d4562c92e70ab47c257c4aa5c6a713ed87fe63766b31fefc/websockets-16.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:39f2a024af5c345ffe8fcf1ee18c049c024c94df393bb09b044a6917c77bde43", size = 188033, upload-time = "2026-07-17T22:50:40.912Z" }, + { url = "https://files.pythonhosted.org/packages/66/d7/a49a38a6127a4acb134fb1912b215d900cc657605cff32445bf519f3acc4/websockets-16.1.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:952303a7318d4cbe1011400839bb2051c9f84fa0a35923267f5daba34b15d458", size = 185748, upload-time = "2026-07-17T22:50:42.559Z" }, + { url = "https://files.pythonhosted.org/packages/95/3e/ad1fa40388c7f2e0bb2c7930d0090b6c5498594bd1cdaec18864df3d9e97/websockets-16.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:249116b4a76063d930a46391ad56e135c286e4562a18309029fc2c73f4ed4c62", size = 188285, upload-time = "2026-07-17T22:50:43.974Z" }, + { url = "https://files.pythonhosted.org/packages/35/b8/d5db28ca264b9104f82196f92dc8843e35fd391f763d42e4ad358f5bc97e/websockets-16.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:61922544a0587a13fd3f53e4c0e5e606510c7b0d9d22c8444e5fae22a06b38cb", size = 186777, upload-time = "2026-07-17T22:50:45.474Z" }, + { url = "https://files.pythonhosted.org/packages/42/9c/726cb39d0cc43ae848dce4aa2acb04eecc6738b1264ec6d700bf6bcfb9f8/websockets-16.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:46dcaa042cd1de6c59e7d9269fa63ff7572b6df40510600b678f0826b3c7af51", size = 188682, upload-time = "2026-07-17T22:50:46.973Z" }, + { url = "https://files.pythonhosted.org/packages/be/c7/1168704de8c2dd483edabe4a22cbe4465dd8be8dd95561d214f9fe092871/websockets-16.1.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:38565aca3e01ea8734e578fb2118dade0ecb0250533f29e22b8d1a7a196cf4d0", size = 186377, upload-time = "2026-07-17T22:50:48.413Z" }, + { url = "https://files.pythonhosted.org/packages/ca/40/f9ff2d630ffce4e7dfea0b2288e1caf9ebbf9ff8a9ec9396136ce8b94935/websockets-16.1.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:42f599f4d48c7e1a3338fdaac3acd075be3b3cf02d4b274f3bf2767aedd3d217", size = 187148, upload-time = "2026-07-17T22:50:49.845Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/e177c8299f78d7cbe2d14df228643c10c70c0e86e108e092056bbcc16e46/websockets-16.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dcc04fedf83effaeb9cce98abc9469bb1b42ef85f03e01c8c1f4438ef7555737", size = 187578, upload-time = "2026-07-17T22:50:51.619Z" }, + { url = "https://files.pythonhosted.org/packages/49/b2/b6987faf330f5af5c787a2610124c2e8403d51724f9001ec4fff6311fe7a/websockets-16.1.1-cp314-cp314t-win32.whl", hash = "sha256:8483c2096363120eea8b07c06ae7304d520f686665fffd4811fad423930a65d7", size = 179729, upload-time = "2026-07-17T22:50:53.269Z" }, + { url = "https://files.pythonhosted.org/packages/a2/6e/fbac6ed878dd362fbad7d415fa4f84d38e3e33fed8cde45c64e783acf826/websockets-16.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:bcce07e23e5769375158f5efdcdafa8d5cd014b93c6683865b840ed65b96f231", size = 180072, upload-time = "2026-07-17T22:50:54.969Z" }, + { url = "https://files.pythonhosted.org/packages/be/4d/2d0d67834092e354d2b0498f014a41249a89556bc406cf86f3e1557bb463/websockets-16.1.1-py3-none-any.whl", hash = "sha256:6abbd3e82c731c8e531714466acd5d87b5e88ac3243465337ba71d68e23ae7e3", size = 173814, upload-time = "2026-07-17T22:51:04.184Z" }, ] [[package]] From ba727669036149ac6a46b260c8266492315ca403 Mon Sep 17 00:00:00 2001 From: Nico Loesch Date: Tue, 28 Jul 2026 00:02:41 +0000 Subject: [PATCH 02/20] Cleanup of docstrings, methods and tests --- .gitignore | 2 +- src/omop_llm/backend.py | 164 +++++++++++++----- src/omop_llm/capabilities.py | 9 +- src/omop_llm/errors.py | 4 + src/omop_llm/providers/__init__.py | 27 +-- src/omop_llm/providers/supported.py | 49 ++---- src/omop_llm/structured.py | 10 +- tests/conftest.py | 18 +- tests/providers/__init__.py | 0 .../test_ollama.py} | 0 tests/test_backend.py | 10 +- tests/test_registry.py | 3 +- tests/test_structured.py | 8 +- 13 files changed, 189 insertions(+), 115 deletions(-) create mode 100644 tests/providers/__init__.py rename tests/{test_providers_ollama.py => providers/test_ollama.py} (100%) diff --git a/.gitignore b/.gitignore index b7faf40..d50942f 100644 --- a/.gitignore +++ b/.gitignore @@ -186,7 +186,7 @@ cython_debug/ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore # and can be added to the global gitignore or merged into this file. However, if you prefer, # you could uncomment the following to ignore the entire vscode folder -# .vscode/ +.vscode/ # Ruff stuff: .ruff_cache/ diff --git a/src/omop_llm/backend.py b/src/omop_llm/backend.py index 1de950b..35d0022 100644 --- a/src/omop_llm/backend.py +++ b/src/omop_llm/backend.py @@ -9,21 +9,11 @@ Every method has a synchronous form and an ``async_``-prefixed asynchronous form (``complete``/``async_complete``, ``embed_texts``/``async_embed_texts``, and so on). This was a deliberate -choice, not an oversight: ``omop-emb``'s current codebase has no -``async``/``await`` anywhere (confirmed by inspecting it directly), so a -consumer that has to synchronously wait on a result needs a real sync -path, not one hand-rolled per call site. any-llm already supplies the sync -bridging for chat (``AnyLLM.completion()`` wraps ``acompletion()`` -internally) and for embeddings (``AnyLLM._embedding()``'s own default -implementation wraps ``aembedding()`` the same way, confirmed by reading -its source, and it is exactly what any-llm's own module-level -``embedding()`` function calls). Neither sync method is reimplemented -here; both are called directly. +choice, not an oversight. Consumers only ever see this class, never a raw any-llm provider instance. If any-llm needed replacing, only this module's method bodies, and the -``providers/`` subclasses, would change; the public methods below would -not. +``providers/`` subclasses, would need to change. """ from __future__ import annotations @@ -37,7 +27,7 @@ from pydantic import BaseModel from omop_llm.capabilities import ModelCapabilities -from omop_llm.errors import UnsupportedCapabilityError +from omop_llm.errors import NoParsedOutputError, UnsupportedCapabilityError from omop_llm.providers.base import ProviderMixin from omop_llm.providers.registry import ( canonical_model_name, @@ -135,10 +125,8 @@ def complete( tools=tools, response_format=response_format, max_tokens=max_tokens, temperature=temperature, reasoning_effort=reasoning_effort, extra=kwargs, ) - # call_kwargs is built dynamically, so its exact keys are not - # visible to the type checker at this call site, so it cannot pick - # a specific overload. stream is rejected above, so this is always - # the non-streaming ChatCompletion branch. + # always non-streaming and typed to return a ChatCompletion + # but not captured by any single any-llm overload return self._client.completion( # ty: ignore[no-matching-overload] model=self.model, messages=messages, **call_kwargs ) @@ -195,12 +183,30 @@ async def async_complete( tools=tools, response_format=response_format, max_tokens=max_tokens, temperature=temperature, reasoning_effort=reasoning_effort, extra=kwargs, ) + # See the matching comment in complete(): our response_format + # union doesn't match any single any-llm acompletion() overload. return await self._client.acompletion( # ty: ignore[no-matching-overload] model=self.model, messages=messages, **call_kwargs ) def embed_texts(self, texts: list[str]) -> list[list[float]]: - """Embed a batch of texts synchronously. See :meth:`async_embed_texts` for parameters.""" + """Embed a batch of texts. + + Parameters + ---------- + texts : list of str + Texts to embed. + + Returns + ------- + list of list of float + One embedding vector per input text, in the same order. + + Raises + ------ + UnsupportedCapabilityError + If ``self.capabilities.embeddings`` is ``False``. + """ self._require_embeddings() response = self._client._embedding( model=self.model, inputs=texts, **self.configuration @@ -208,7 +214,7 @@ def embed_texts(self, texts: list[str]) -> list[list[float]]: return [item.embedding for item in response.data] async def async_embed_texts(self, texts: list[str]) -> list[list[float]]: - """Embed a batch of texts. + """Embed a batch of texts asynchronously. Parameters ---------- @@ -223,8 +229,7 @@ async def async_embed_texts(self, texts: list[str]) -> list[list[float]]: Raises ------ UnsupportedCapabilityError - If ``self.capabilities.embeddings`` is ``False`` (e.g. for an - ``anthropic`` backend, which has no embeddings API). + If ``self.capabilities.embeddings`` is ``False``. """ self._require_embeddings() response = await self._client.aembedding( @@ -240,8 +245,10 @@ def _require_embeddings(self) -> None: def dimensions(self) -> int: """Discover this model's embedding dimensionality synchronously. - - See :meth:`async_dimensions` for the three-tier lookup order. + Three tiers: + 1. a configured override (``configuration["embedding_dim"]``), + 2. a provider-specific fast path (e.g. Ollama's ``POST /api/show``), and + 3. a live probe (embed one short string and measure the vector). Returns ------- @@ -260,11 +267,10 @@ def dimensions(self) -> int: async def async_dimensions(self) -> int: """Discover this model's embedding dimensionality. - - Three tiers: a configured override (``configuration["embedding_dim"]``), - then a provider-specific fast path (e.g. Ollama's - ``POST /api/show``), then a live probe (embed one short string and - measure the vector). + Three tiers: + 1. a configured override (``configuration["embedding_dim"]``), + 2. a provider-specific fast path (e.g. Ollama's ``POST /api/show``), and + 3. a live probe (embed one short string and measure the vector). Returns ------- @@ -287,9 +293,46 @@ def extract[T: BaseModel]( response_model: type[T], **kwargs: Any, ) -> T: - """Extract one validated ``response_model`` instance synchronously. + """Extract one validated ``response_model`` instance from a chat call synchronously. + + A thin convenience method built on :meth:`complete` with + ``response_format=response_model``. Checks that a parsed instance + actually came back, and unwraps it. + + Notes + ----- + See :mod:`omop_llm.structured` for ``extract_with_retry``, a separate, optional fallback + for callers that want validate-and-retry resilience instead of relying + on native structured decoding. + + Parameters + ---------- + messages : list of dict + Chat history in OpenAI message format. + response_model : type of BaseModel + The Pydantic model to constrain and validate the response + against. + **kwargs : Any + Additional arguments forwarded to :meth:`complete`. + + Returns + ------- + BaseModel + A validated instance of ``response_model``. - See :meth:`async_extract` for parameters. + Raises + ------ + UnsupportedCapabilityError + If ``self.capabilities.structured_output`` is ``False``. + NoParsedOutputError + If the provider returned no parsed instance (refusal or empty + content). + any_llm.exceptions.LengthFinishReasonError + If the response was truncated before completing. + any_llm.exceptions.ContentFilterFinishReasonError + If a content filter blocked the response. + pydantic.ValidationError + If the model's output does not match ``response_model``'s schema. """ self._require_structured_output() completion = self.complete(messages, response_format=response_model, **kwargs) @@ -304,10 +347,13 @@ async def async_extract[T: BaseModel]( """Extract one validated ``response_model`` instance from a chat call. A thin convenience method built on :meth:`async_complete` with - ``response_format=response_model``: checks that a parsed instance - actually came back, and unwraps it. See :mod:`omop_llm.structured` - for ``extract_with_retry``, a separate, optional fallback for - callers that want validate-and-retry resilience instead of relying + ``response_format=response_model``. Checks that a parsed instance + actually came back, and unwraps it. + + Notes + ----- + See :mod:`omop_llm.structured` for ``extract_with_retry``, a separate, optional fallback + for callers that want validate-and-retry resilience instead of relying on native structured decoding. Parameters @@ -328,9 +374,16 @@ async def async_extract[T: BaseModel]( Raises ------ UnsupportedCapabilityError - If ``self.capabilities.structured_output`` is ``False``, or if - the provider accepted ``response_format`` but returned no - parsed instance. + If ``self.capabilities.structured_output`` is ``False``. + NoParsedOutputError + If the provider returned no parsed instance (refusal or empty + content). + any_llm.exceptions.LengthFinishReasonError + If the response was truncated before completing. + any_llm.exceptions.ContentFilterFinishReasonError + If a content filter blocked the response. + pydantic.ValidationError + If the model's output does not match ``response_model``'s schema. """ self._require_structured_output() completion = await self.async_complete(messages, response_format=response_model, **kwargs) @@ -344,12 +397,34 @@ def _require_structured_output(self) -> None: @staticmethod def _unwrap_parsed[T: BaseModel](completion: ChatCompletion, response_model: type[T]) -> T: + """Unwrap a completion's parsed instance, or raise if there is none to unwrap. + + Parameters + ---------- + completion : ChatCompletion + The completion returned from a call made with + ``response_format=response_model``. + response_model : type of BaseModel + The Pydantic model the caller expected back. + + Returns + ------- + BaseModel + The validated ``response_model`` instance any-llm attached to + the completion. + + Raises + ------ + NoParsedOutputError + If no parsed instance is present. + """ message = completion.choices[0].message parsed = getattr(message, "parsed", None) if parsed is None: - raise UnsupportedCapabilityError( + raise NoParsedOutputError( f"provider returned no parsed {response_model.__name__} instance " - "(response_format was accepted but not honored)" + "(no validation or length/content-filter error was raised, so the " + "model likely refused to answer or returned empty content)" ) return parsed # type: ignore[no-any-return] @@ -374,14 +449,17 @@ def is_available(self, **kwargs: Any) -> bool: """ try: self._client.list_models(**kwargs) - except Exception: # noqa: BLE001 (deliberately broad: any failure means "unavailable") + except Exception: # noqa: BLE001 (any exception means "unavailable") return False return True async def async_is_available(self, **kwargs: Any) -> bool: - """Check whether this backend can actually be reached. + """Check whether this backend can actually be reached, asynchronously. - See :meth:`is_available` for details. + Probes ``list_models`` against the resolved provider. Swallows any + error and reports ``False`` rather than raising, since the point of + a health check is to answer "can I use this," not to propagate the + specific failure. Parameters ---------- @@ -396,7 +474,7 @@ async def async_is_available(self, **kwargs: Any) -> bool: """ try: await self._client.alist_models(**kwargs) - except Exception: # noqa: BLE001 (deliberately broad: any failure means "unavailable") + except Exception: # noqa: BLE001 (any exception means "unavailable") return False return True diff --git a/src/omop_llm/capabilities.py b/src/omop_llm/capabilities.py index a8e7d96..8db2c1a 100644 --- a/src/omop_llm/capabilities.py +++ b/src/omop_llm/capabilities.py @@ -10,13 +10,10 @@ class ModelCapabilities: """Capability declaration for one provider. ``streaming``, ``embeddings``, and ``extended_thinking`` are read - directly from any-llm's own ``ProviderMetadata``. That data is accurate - per provider and not worth re-declaring by hand. + directly from any-llm's own ``ProviderMetadata``. - ``tool_use`` and ``structured_output`` have no equivalent in any-llm: - it exposes no capability flag for either, for any provider (confirmed - by inspecting ``any_llm.types.provider.ProviderMetadata``). These two - are declared by omop_llm itself in ``providers.registry`` and must not + ``tool_use`` and ``structured_output`` have no equivalent in any-llm. + These two are declared by omop_llm itself in ``providers.registry`` and must not be inferred from any-llm's own introspection. Parameters diff --git a/src/omop_llm/errors.py b/src/omop_llm/errors.py index 748bc66..c0933d8 100644 --- a/src/omop_llm/errors.py +++ b/src/omop_llm/errors.py @@ -13,3 +13,7 @@ class UnsupportedProviderError(OmopLlmError): class UnsupportedCapabilityError(OmopLlmError): """Raised when a requested capability is not available on the resolved backend.""" + + +class NoParsedOutputError(OmopLlmError): + """Raised when a structured-output call produced no parsed instance to unwrap.""" diff --git a/src/omop_llm/providers/__init__.py b/src/omop_llm/providers/__init__.py index 2f9cc62..e8ef5c7 100644 --- a/src/omop_llm/providers/__init__.py +++ b/src/omop_llm/providers/__init__.py @@ -1,15 +1,16 @@ from omop_llm.providers.registry import ( - PROVIDER_REGISTRY as PROVIDER_REGISTRY, -) -from omop_llm.providers.registry import ( - canonical_model_name as canonical_model_name, -) -from omop_llm.providers.registry import ( - capabilities_for as capabilities_for, -) -from omop_llm.providers.registry import ( - provider_class_for as provider_class_for, -) -from omop_llm.providers.registry import ( - supported_providers as supported_providers, + PROVIDER_REGISTRY, + canonical_model_name, + capabilities_for, + provider_class_for, + supported_providers, ) + + +__all__ = [ + "PROVIDER_REGISTRY", + "canonical_model_name", + "capabilities_for", + "provider_class_for", + "supported_providers", +] diff --git a/src/omop_llm/providers/supported.py b/src/omop_llm/providers/supported.py index 44c540d..332df06 100644 --- a/src/omop_llm/providers/supported.py +++ b/src/omop_llm/providers/supported.py @@ -37,22 +37,11 @@ class OllamaProvider(ProviderMixin, AnyLLMOllamaProvider): - """Ollama. Native ``/api/chat`` via the official ``ollama`` SDK, not the OpenAI-compat shim. - - Verified by reading ``any_llm.providers.ollama.ollama`` directly: - ``_init_client`` constructs ``ollama.AsyncClient``, and - ``_convert_response_format`` maps an OpenAI-style ``response_format`` - onto Ollama's native ``format`` field. See :mod:`omop_llm.structured` - for why ``instructor``'s own Ollama support is deliberately not wired - in as an alternative. - - The one provider here with real behavior beyond any-llm's own, - ported from ``omop-emb/src/omop_emb/embeddings/embedding_providers.py``'s - ``OllamaProvider``: canonical model naming (rejects untagged names and - the mutable ``:latest`` tag) and a fast embedding-dimension lookup via - Ollama's native ``POST /api/show``, over ``httpx`` (already a - transitive dependency of ``any-llm-sdk``) since any-llm has no - equivalent call. + """Wrapped Ollama provider, for local dev and TRE fallback. + Extends any-llm's own ``OllamaProvider`` with canonical model naming and + embedding-dimension lookup via Ollama's native ``POST /api/show``. + + Supports structured output natively using ``response_format``. No default ``base_url``: falls through to the official ``ollama`` SDK's own default (``http://localhost:11434``). No ``api_key`` @@ -66,9 +55,8 @@ class OllamaProvider(ProviderMixin, AnyLLMOllamaProvider): def canonical_model_name(cls, name: str) -> str: """Require an explicit, immutable Ollama model tag. - Rejects both untagged names and the mutable ``:latest`` tag, for - the same reason ``omop-emb`` already enforces this: ``:latest`` - can silently repoint after an ``ollama pull``, breaking + Rejects both untagged names and the mutable ``:latest`` tag: + ``:latest`` can silently repoint after an ``ollama pull``, breaking consistency between stored embeddings and new query embeddings. Parameters @@ -114,7 +102,7 @@ def embedding_dimension_hint(self, model: str, *, api_base: str | None) -> int | if api_base is None: return None response = httpx.post(f"{api_base.rstrip('/')}/api/show", json={"name": model}).json() - return _extract_embedding_length(response) + return self._extract_embedding_length(response) async def async_embedding_dimension_hint(self, model: str, *, api_base: str | None) -> int | None: """See :meth:`omop_llm.providers.base.ProviderMixin.async_embedding_dimension_hint`.""" @@ -122,17 +110,18 @@ async def async_embedding_dimension_hint(self, model: str, *, api_base: str | No return None async with httpx.AsyncClient() as client: response = await client.post(f"{api_base.rstrip('/')}/api/show", json={"name": model}) - return _extract_embedding_length(response.json()) - + return self._extract_embedding_length(response.json()) -def _extract_embedding_length(response: dict) -> int | None: - model_info = response.get("model_info", {}) - if not model_info: - return None - embedding_keys = [key for key in model_info if "embedding_length" in key] - if len(embedding_keys) != 1: - return None - return int(model_info[embedding_keys[0]]) + @staticmethod + def _extract_embedding_length(response: dict) -> int | None: + """Extract the embedding length from an Ollama ``/api/show`` response.""" + model_info = response.get("model_info", {}) + if not model_info: + return None + embedding_keys = [key for key in model_info if "embedding_length" in key] + if len(embedding_keys) != 1: + return None + return int(model_info[embedding_keys[0]]) class LlamacppProvider(ProviderMixin, AnyLLMLlamacppProvider): diff --git a/src/omop_llm/structured.py b/src/omop_llm/structured.py index 2b14347..01e17fe 100644 --- a/src/omop_llm/structured.py +++ b/src/omop_llm/structured.py @@ -41,6 +41,7 @@ from omop_llm.errors import UnsupportedCapabilityError from omop_llm.providers.supported import LlamacppProvider, OpenaiProvider, VllmProvider +# Compatible with instructor's generic OpenAI-API client builder. _INSTRUCTOR_SAFE_PROVIDERS = frozenset({ OpenaiProvider.PROVIDER_NAME, LlamacppProvider.PROVIDER_NAME, @@ -87,7 +88,10 @@ def extract_with_retry[T: BaseModel]( ) -> T: """Extract via ``instructor``'s validate-and-retry loop, synchronously. - See :func:`async_extract_with_retry` for parameters. + Always builds instructor's ``openai`` client, a generic + OpenAI-API-compatible constructor, to pass through all providers + ``_INSTRUCTOR_SAFE_PROVIDERS`` allows. See :func:`async_extract_with_retry` + for parameters. """ _check_provider_and_base_url(provider, base_url) instructor = _require_instructor() @@ -120,6 +124,10 @@ async def async_extract_with_retry[T: BaseModel]( ) -> T: """Extract via ``instructor``'s validate-and-retry loop. + Always builds instructor's ``openai`` client, a generic + OpenAI-API-compatible constructor, to pass through all providers + ``_INSTRUCTOR_SAFE_PROVIDERS`` allows. + Requires the ``instructor`` optional extra (``pip install 'omop-llm[instructor]'``). diff --git a/tests/conftest.py b/tests/conftest.py index c749db1..6be86a1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -53,19 +53,11 @@ class FakeAnyLLMClient(ProviderMixin): Records every call it receives so tests can assert on exactly what :class:`omop_llm.backend.ModelBackend` passed through, without needing a real provider or network access. Method names match the real - ``AnyLLM`` surface :class:`~omop_llm.backend.ModelBackend` calls: - ``completion``/``acompletion`` for chat, ``_embedding``/``aembedding`` - for embeddings (matching any-llm's own asymmetric naming, confirmed by - reading ``any_llm/api.py`` and ``any_llm/any_llm.py`` directly), and - ``embedding_dimension_hint``/``async_embedding_dimension_hint`` for the - provider-specific dimension fast path. - - Subclasses :class:`~omop_llm.providers.base.ProviderMixin`, not just - ``AnyLLM``'s duck-typed surface: every real ``_client`` a - :class:`~omop_llm.backend.ModelBackend` is ever built with also is one, - since :data:`~omop_llm.providers.registry.PROVIDER_REGISTRY` only - contains classes that are both. Not doing so here would make this fake - a less accurate stand-in than the objects it replaces. + ``AnyLLM`` surface :class:`~omop_llm.backend.ModelBackend` calls. + + Only subclasses ``ProviderMixin``, for ``ModelBackend``'s + ``isinstance`` check; the rest of ``AnyLLM``'s surface is duck-typed + below, not inherited. """ TOOL_USE = True diff --git a/tests/providers/__init__.py b/tests/providers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_providers_ollama.py b/tests/providers/test_ollama.py similarity index 100% rename from tests/test_providers_ollama.py rename to tests/providers/test_ollama.py diff --git a/tests/test_backend.py b/tests/test_backend.py index c3f734b..b9b5ba8 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -13,7 +13,7 @@ from omop_llm.backend import ModelBackend, build_backend from omop_llm.capabilities import ModelCapabilities -from omop_llm.errors import UnsupportedCapabilityError +from omop_llm.errors import NoParsedOutputError, UnsupportedCapabilityError from tests.conftest import ( FakeAnyLLMClient, FakeChatCompletion, @@ -33,7 +33,7 @@ class Answer(BaseModel): def _backend(fake_client: FakeAnyLLMClient, **kwargs) -> ModelBackend: - return ModelBackend(_client=fake_client, model="m", capabilities=_CAPS, **kwargs) # type: ignore[arg-type] + return ModelBackend(_client=fake_client, model="m", capabilities=_CAPS, **kwargs) # ty: ignore[invalid-argument-type] @pytest.mark.parametrize("sync", [True, False]) @@ -96,7 +96,7 @@ async def test_embed_texts_rejects_backend_without_embeddings(fake_client: FakeA no_embed_caps = ModelCapabilities( streaming=True, embeddings=False, extended_thinking=True, tool_use=True, structured_output=True ) - backend = ModelBackend(_client=fake_client, model="m", capabilities=no_embed_caps) # type: ignore[arg-type] + backend = ModelBackend(_client=fake_client, model="m", capabilities=no_embed_caps) # ty: ignore[invalid-argument-type] with pytest.raises(UnsupportedCapabilityError): if sync: backend.embed_texts(["a"]) @@ -134,7 +134,7 @@ async def test_extract_rejects_backend_without_structured_output(fake_client: Fa no_structured_caps = ModelCapabilities( streaming=True, embeddings=True, extended_thinking=True, tool_use=True, structured_output=False ) - backend = ModelBackend(_client=fake_client, model="m", capabilities=no_structured_caps) # type: ignore[arg-type] + backend = ModelBackend(_client=fake_client, model="m", capabilities=no_structured_caps) # ty: ignore[invalid-argument-type] fake_client.completion_response = FakeChatCompletion(choices=[FakeChoice(message=FakeChatCompletionMessage())]) with pytest.raises(UnsupportedCapabilityError): if sync: @@ -165,7 +165,7 @@ async def test_extract_raises_when_provider_did_not_honor_schema(fake_client: Fa choices=[FakeChoice(message=FakeChatCompletionMessage(parsed=None))] ) backend = _backend(fake_client) - with pytest.raises(UnsupportedCapabilityError): + with pytest.raises(NoParsedOutputError): if sync: backend.extract([{"role": "user", "content": "hi"}], Answer) else: diff --git a/tests/test_registry.py b/tests/test_registry.py index e566171..9619d50 100644 --- a/tests/test_registry.py +++ b/tests/test_registry.py @@ -13,7 +13,8 @@ ) -def test_supported_providers_is_the_closed_six() -> None: +def test_registry_has_no_unexpected_providers() -> None: + """Pins exact registry membership, so a stray ProviderMixin subclass (e.g. a test fixture) fails loudly instead of silently entering PROVIDER_REGISTRY.""" assert supported_providers() == ( "anthropic", "gemini", diff --git a/tests/test_structured.py b/tests/test_structured.py index a5d8769..ad26a8e 100644 --- a/tests/test_structured.py +++ b/tests/test_structured.py @@ -11,6 +11,7 @@ from pydantic import BaseModel from omop_llm.errors import UnsupportedCapabilityError +from omop_llm.providers import supported_providers from omop_llm.structured import ( _INSTRUCTOR_SAFE_PROVIDERS, async_extract_with_retry, @@ -22,6 +23,9 @@ class Answer(BaseModel): value: str + +_UNSAFE_PROVIDERS = sorted(set(supported_providers()) - _INSTRUCTOR_SAFE_PROVIDERS) + ["azure"] + def test_instructor_safe_providers_is_the_openai_compat_native_set() -> None: # Deliberately excludes ollama (instructor's own Ollama builder uses the # OpenAI-compat shim, not native /api/chat, see structured.py's module @@ -29,13 +33,13 @@ def test_instructor_safe_providers_is_the_openai_compat_native_set() -> None: assert _INSTRUCTOR_SAFE_PROVIDERS == frozenset({"openai", "llamacpp", "vllm"}) -@pytest.mark.parametrize("provider", ["ollama", "anthropic", "gemini", "azure"]) +@pytest.mark.parametrize("provider", _UNSAFE_PROVIDERS) def test_extract_with_retry_rejects_unsafe_providers(provider: str) -> None: with pytest.raises(UnsupportedCapabilityError): extract_with_retry(provider, "some-model", [{"role": "user", "content": "hi"}], Answer, base_url="http://x") -@pytest.mark.parametrize("provider", ["ollama", "anthropic", "gemini", "azure"]) +@pytest.mark.parametrize("provider", _UNSAFE_PROVIDERS) async def test_async_extract_with_retry_rejects_unsafe_providers(provider: str) -> None: with pytest.raises(UnsupportedCapabilityError): await async_extract_with_retry( From 3a461a2b5d4ba2f27fcaf137bc88cabeeffeaf73 Mon Sep 17 00:00:00 2001 From: Nico Loesch Date: Tue, 28 Jul 2026 00:34:35 +0000 Subject: [PATCH 03/20] Allow batched embedding processing --- src/omop_llm/backend.py | 70 ++++++++++++++++++++++++++++++++++------- 1 file changed, 59 insertions(+), 11 deletions(-) diff --git a/src/omop_llm/backend.py b/src/omop_llm/backend.py index 35d0022..622b1dd 100644 --- a/src/omop_llm/backend.py +++ b/src/omop_llm/backend.py @@ -18,6 +18,7 @@ from __future__ import annotations +from collections.abc import Iterator from dataclasses import dataclass, field from typing import Any @@ -36,6 +37,27 @@ ) +def _chunked[T](items: list[T], size: int) -> Iterator[list[T]]: + """Yield successive sub-lists of ``items``, each at most ``size`` long. + + Parameters + ---------- + items : list + The items to chunk. + size : int + Maximum length of each yielded chunk. Must be positive. + + Raises + ------ + ValueError + If ``size`` is not a positive integer. + """ + if size <= 0: + raise ValueError(f"batch_size must be a positive integer, got {size!r}") + for start in range(0, len(items), size): + yield items[start : start + size] + + @dataclass class ModelBackend: """One resolved, ready-to-call model. @@ -189,13 +211,19 @@ async def async_complete( model=self.model, messages=messages, **call_kwargs ) - def embed_texts(self, texts: list[str]) -> list[list[float]]: + def embed_texts(self, texts: list[str], *, batch_size: int | None = None) -> list[list[float]]: """Embed a batch of texts. Parameters ---------- texts : list of str Texts to embed. + batch_size : int, optional + If given, ``texts`` is chunked into sub-batches of at most this + size, each sent as its own call, rather than one call with the + entire list. Useful for bulk callers embedding more texts than + a single provider request should carry. Default is ``None`` + (one call for the whole list). Returns ------- @@ -206,20 +234,34 @@ def embed_texts(self, texts: list[str]) -> list[list[float]]: ------ UnsupportedCapabilityError If ``self.capabilities.embeddings`` is ``False``. + ValueError + If ``batch_size`` is not a positive integer. """ self._require_embeddings() - response = self._client._embedding( - model=self.model, inputs=texts, **self.configuration - ) - return [item.embedding for item in response.data] - - async def async_embed_texts(self, texts: list[str]) -> list[list[float]]: + if batch_size is None: + response = self._client._embedding(model=self.model, inputs=texts, **self.configuration) + return [item.embedding for item in response.data] + vectors: list[list[float]] = [] + for chunk in _chunked(texts, batch_size): + response = self._client._embedding(model=self.model, inputs=chunk, **self.configuration) + vectors.extend(item.embedding for item in response.data) + return vectors + + async def async_embed_texts( + self, texts: list[str], *, batch_size: int | None = None + ) -> list[list[float]]: """Embed a batch of texts asynchronously. Parameters ---------- texts : list of str Texts to embed. + batch_size : int, optional + If given, ``texts`` is chunked into sub-batches of at most this + size, each sent as its own call, rather than one call with the + entire list. Useful for bulk callers embedding more texts than + a single provider request should carry. Default is ``None`` + (one call for the whole list). Returns ------- @@ -230,12 +272,18 @@ async def async_embed_texts(self, texts: list[str]) -> list[list[float]]: ------ UnsupportedCapabilityError If ``self.capabilities.embeddings`` is ``False``. + ValueError + If ``batch_size`` is not a positive integer. """ self._require_embeddings() - response = await self._client.aembedding( - model=self.model, inputs=texts, **self.configuration - ) - return [item.embedding for item in response.data] + if batch_size is None: + response = await self._client.aembedding(model=self.model, inputs=texts, **self.configuration) + return [item.embedding for item in response.data] + vectors: list[list[float]] = [] + for chunk in _chunked(texts, batch_size): + response = await self._client.aembedding(model=self.model, inputs=chunk, **self.configuration) + vectors.extend(item.embedding for item in response.data) + return vectors def _require_embeddings(self) -> None: if not self.capabilities.embeddings: From f278ab00fa00621da5756b178fb8f3b11ad49d39 Mon Sep 17 00:00:00 2001 From: Nico Loesch Date: Tue, 28 Jul 2026 01:03:48 +0000 Subject: [PATCH 04/20] Allow native retry of extraction --- src/omop_llm/backend.py | 75 ++++++++++++++++++++++++++++++----------- 1 file changed, 56 insertions(+), 19 deletions(-) diff --git a/src/omop_llm/backend.py b/src/omop_llm/backend.py index 622b1dd..8f211ae 100644 --- a/src/omop_llm/backend.py +++ b/src/omop_llm/backend.py @@ -25,7 +25,7 @@ from any_llm.any_llm import AnyLLM from any_llm.types.completion import ChatCompletion, ReasoningEffort from oa_configurator import ResolvedModel -from pydantic import BaseModel +from pydantic import BaseModel, ValidationError from omop_llm.capabilities import ModelCapabilities from omop_llm.errors import NoParsedOutputError, UnsupportedCapabilityError @@ -339,19 +339,20 @@ def extract[T: BaseModel]( self, messages: list[dict[str, Any]], response_model: type[T], + *, + max_retries: int = 0, **kwargs: Any, ) -> T: """Extract one validated ``response_model`` instance from a chat call synchronously. A thin convenience method built on :meth:`complete` with ``response_format=response_model``. Checks that a parsed instance - actually came back, and unwraps it. - + actually came back, and unwraps it. + Notes ----- - See :mod:`omop_llm.structured` for ``extract_with_retry``, a separate, optional fallback - for callers that want validate-and-retry resilience instead of relying - on native structured decoding. + ``max_retries`` is native (not `instructor`-based), so it works for + all providers, unlike :func:`omop_llm.structured.extract_with_retry`. Parameters ---------- @@ -360,6 +361,9 @@ def extract[T: BaseModel]( response_model : type of BaseModel The Pydantic model to constrain and validate the response against. + max_retries : int, optional + Number of additional attempts after a validation failure. + Default is 0, i.e. fail immediately. **kwargs : Any Additional arguments forwarded to :meth:`complete`. @@ -374,35 +378,45 @@ def extract[T: BaseModel]( If ``self.capabilities.structured_output`` is ``False``. NoParsedOutputError If the provider returned no parsed instance (refusal or empty - content). + content), after exhausting ``max_retries``. any_llm.exceptions.LengthFinishReasonError If the response was truncated before completing. any_llm.exceptions.ContentFilterFinishReasonError If a content filter blocked the response. pydantic.ValidationError - If the model's output does not match ``response_model``'s schema. + If the model's output does not match ``response_model``'s + schema, after exhausting ``max_retries``. """ self._require_structured_output() - completion = self.complete(messages, response_format=response_model, **kwargs) - return self._unwrap_parsed(completion, response_model) + conversation = list(messages) + for attempt in range(max_retries + 1): + try: + completion = self.complete(conversation, response_format=response_model, **kwargs) + return self._unwrap_parsed(completion, response_model) + except (NoParsedOutputError, ValidationError) as exc: + if attempt >= max_retries: + raise + conversation = [*conversation, self._retry_extract_message(exc, response_model)] + raise AssertionError("unreachable") async def async_extract[T: BaseModel]( self, messages: list[dict[str, Any]], response_model: type[T], + *, + max_retries: int = 0, **kwargs: Any, ) -> T: """Extract one validated ``response_model`` instance from a chat call. A thin convenience method built on :meth:`async_complete` with ``response_format=response_model``. Checks that a parsed instance - actually came back, and unwraps it. - + actually came back, and unwraps it. + Notes ----- - See :mod:`omop_llm.structured` for ``extract_with_retry``, a separate, optional fallback - for callers that want validate-and-retry resilience instead of relying - on native structured decoding. + ``max_retries`` is native (not `instructor`-based), so it works for + all providers, unlike :func:`omop_llm.structured.extract_with_retry`. Parameters ---------- @@ -411,6 +425,9 @@ async def async_extract[T: BaseModel]( response_model : type of BaseModel The Pydantic model to constrain and validate the response against. + max_retries : int, optional + Number of additional attempts after a validation failure. + Default is 0, i.e. fail immediately. **kwargs : Any Additional arguments forwarded to :meth:`async_complete`. @@ -425,17 +442,26 @@ async def async_extract[T: BaseModel]( If ``self.capabilities.structured_output`` is ``False``. NoParsedOutputError If the provider returned no parsed instance (refusal or empty - content). + content), after exhausting ``max_retries``. any_llm.exceptions.LengthFinishReasonError If the response was truncated before completing. any_llm.exceptions.ContentFilterFinishReasonError If a content filter blocked the response. pydantic.ValidationError - If the model's output does not match ``response_model``'s schema. + If the model's output does not match ``response_model``'s + schema, after exhausting ``max_retries``. """ self._require_structured_output() - completion = await self.async_complete(messages, response_format=response_model, **kwargs) - return self._unwrap_parsed(completion, response_model) + conversation = list(messages) + for attempt in range(max_retries + 1): + try: + completion = await self.async_complete(conversation, response_format=response_model, **kwargs) + return self._unwrap_parsed(completion, response_model) + except (NoParsedOutputError, ValidationError) as exc: + if attempt >= max_retries: + raise + conversation = [*conversation, self._retry_extract_message(exc, response_model)] + raise AssertionError("unreachable") def _require_structured_output(self) -> None: if not self.capabilities.structured_output: @@ -443,6 +469,17 @@ def _require_structured_output(self) -> None: f"backend for model {self.model!r} does not declare structured_output support" ) + @staticmethod + def _retry_extract_message[T: BaseModel](exc: Exception, response_model: type[T]) -> dict[str, Any]: + """Build a follow-up user message asking the model to correct a failed extraction.""" + return { + "role": "user", + "content": ( + f"Your previous response was invalid: {exc}. " + f"Respond again with valid JSON matching {response_model.__name__}'s schema." + ), + } + @staticmethod def _unwrap_parsed[T: BaseModel](completion: ChatCompletion, response_model: type[T]) -> T: """Unwrap a completion's parsed instance, or raise if there is none to unwrap. From f996a0bd745f980bc4dcf03ce242bbd9e5ee4c41 Mon Sep 17 00:00:00 2001 From: Nico Loesch Date: Tue, 28 Jul 2026 01:12:11 +0000 Subject: [PATCH 05/20] Check for correct ollama args --- pyproject.toml | 1 + src/omop_llm/providers/supported.py | 33 +++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 6ad08db..131e08a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ dependencies = [ "any-llm-sdk[ollama,gemini]>=1.22.0", "httpx", "oa-configurator>=0.2.0,<1.0.0", + "ollama", # already imported by any-llm-sdk "pydantic", ] diff --git a/src/omop_llm/providers/supported.py b/src/omop_llm/providers/supported.py index 332df06..d9d6efc 100644 --- a/src/omop_llm/providers/supported.py +++ b/src/omop_llm/providers/supported.py @@ -25,6 +25,8 @@ from __future__ import annotations +from typing import Any + import httpx from any_llm.providers.anthropic.anthropic import AnthropicProvider as AnyLLMAnthropicProvider from any_llm.providers.gemini.gemini import GeminiProvider as AnyLLMGeminiProvider @@ -32,9 +34,14 @@ from any_llm.providers.ollama.ollama import OllamaProvider as AnyLLMOllamaProvider from any_llm.providers.openai.openai import OpenaiProvider as AnyLLMOpenaiProvider from any_llm.providers.vllm.vllm import VllmProvider as AnyLLMVllmProvider +from any_llm.types.completion import CompletionParams +from oa_configurator import get_logger +from ollama import Options from omop_llm.providers.base import ProviderMixin +_logger = get_logger(__name__) + class OllamaProvider(ProviderMixin, AnyLLMOllamaProvider): """Wrapped Ollama provider, for local dev and TRE fallback. @@ -123,6 +130,32 @@ def _extract_embedding_length(response: dict) -> int | None: return None return int(model_info[embedding_keys[0]]) + @staticmethod + def _convert_completion_params(params: CompletionParams, **kwargs: Any) -> dict[str, Any]: + """Override any-llm's param conversion to fix/flag what Ollama's ``Options`` would silently drop. + + - ``max_tokens`` -> ``num_predict`` (Ollama's native name; ``num_predict`` + wins if both are present). + - Any other key not in ``Options.model_fields`` or popped elsewhere + is logged as a warning instead of silently vanishing. + + Notes + ----- + - poppped_before_options: Popped in any_llm/providers/ollama/ollama.py:L.202-203 + """ + popped_before_options = frozenset({"tools", "think"}) + converted = AnyLLMOllamaProvider._convert_completion_params(params, **kwargs) + if "max_tokens" in converted: + max_tokens = converted.pop("max_tokens") + converted.setdefault("num_predict", max_tokens) + unrecognized = converted.keys() - Options.model_fields.keys() - popped_before_options + if unrecognized: + _logger.warning( + "Ollama will silently ignore unrecognized completion kwargs: %s", + sorted(unrecognized), + ) + return converted + class LlamacppProvider(ProviderMixin, AnyLLMLlamacppProvider): """llama.cpp's ``llama-server``. Covers local dev and a CUDA/TRE fallback profile. From 399f9f4bf0b3ea095a4cf59348cad0f69cc98909 Mon Sep 17 00:00:00 2001 From: Nico Loesch Date: Tue, 28 Jul 2026 01:14:06 +0000 Subject: [PATCH 06/20] Add tests for new features --- tests/providers/test_ollama.py | 36 ++++++++++ tests/test_backend.py | 120 +++++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+) diff --git a/tests/providers/test_ollama.py b/tests/providers/test_ollama.py index cd5fee8..6cf2f23 100644 --- a/tests/providers/test_ollama.py +++ b/tests/providers/test_ollama.py @@ -11,6 +11,7 @@ import httpx import pytest +from any_llm.types.completion import CompletionParams from omop_llm.providers.supported import OllamaProvider @@ -80,3 +81,38 @@ async def fake_post(self: httpx.AsyncClient, url: str, json: dict[str, Any]) -> "nomic-embed-text:v1.5", api_base="http://localhost:11434" ) assert result == 768 + + +def test_convert_completion_params_translates_max_tokens_to_num_predict() -> None: + params = CompletionParams(model_id="llama3:8b", messages=[{"role": "user", "content": "hi"}], max_tokens=512) + converted = OllamaProvider._convert_completion_params(params) + assert "max_tokens" not in converted + assert converted["num_predict"] == 512 + + +def test_convert_completion_params_prefers_explicit_num_predict_over_max_tokens() -> None: + params = CompletionParams(model_id="llama3:8b", messages=[{"role": "user", "content": "hi"}], max_tokens=512) + converted = OllamaProvider._convert_completion_params(params, num_predict=128) + assert converted["num_predict"] == 128 + assert "max_tokens" not in converted + + +def test_convert_completion_params_omits_num_predict_when_max_tokens_not_set() -> None: + params = CompletionParams(model_id="llama3:8b", messages=[{"role": "user", "content": "hi"}]) + converted = OllamaProvider._convert_completion_params(params) + assert "num_predict" not in converted + + +def test_convert_completion_params_warns_on_unrecognized_kwargs(caplog: pytest.LogCaptureFixture) -> None: + params = CompletionParams(model_id="llama3:8b", messages=[{"role": "user", "content": "hi"}]) + with caplog.at_level("WARNING", logger="omop_llm.providers.supported"): + converted = OllamaProvider._convert_completion_params(params, bogus_kwarg="x") + assert converted["bogus_kwarg"] == "x" + assert "bogus_kwarg" in caplog.text + + +def test_convert_completion_params_does_not_warn_on_tools_or_think(caplog: pytest.LogCaptureFixture) -> None: + params = CompletionParams(model_id="llama3:8b", messages=[{"role": "user", "content": "hi"}]) + with caplog.at_level("WARNING", logger="omop_llm.providers.supported"): + OllamaProvider._convert_completion_params(params, tools=[{"type": "function"}], think=True) + assert caplog.text == "" diff --git a/tests/test_backend.py b/tests/test_backend.py index b9b5ba8..b12d45d 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -91,6 +91,42 @@ async def test_embed_texts_unpacks_embedding_vectors(fake_client: FakeAnyLLMClie assert call["inputs"] == ["a", "b"] +@pytest.mark.parametrize("sync", [True, False]) +async def test_embed_texts_batch_size_chunks_and_preserves_order(fake_client: FakeAnyLLMClient, sync: bool) -> None: + texts = ["a", "bb", "ccc", "dddd", "e"] + + def fake_embedding(**kwargs) -> FakeEmbeddingResponse: + fake_client.embedding_calls.append(kwargs) + return FakeEmbeddingResponse(data=[FakeEmbeddingItem(embedding=[float(len(t))]) for t in kwargs["inputs"]]) + + async def fake_aembedding(**kwargs) -> FakeEmbeddingResponse: + return fake_embedding(**kwargs) + + # Instance attribute assignment doesn't need a `self` parameter, unlike + # the class-declared bound method ty checks this against. + fake_client._embedding = fake_embedding # ty: ignore[invalid-assignment] + fake_client.aembedding = fake_aembedding # ty: ignore[invalid-assignment] + backend = _backend(fake_client) + + vectors = ( + backend.embed_texts(texts, batch_size=2) + if sync + else await backend.async_embed_texts(texts, batch_size=2) + ) + assert vectors == [[1.0], [2.0], [3.0], [4.0], [1.0]] + assert [len(call["inputs"]) for call in fake_client.embedding_calls] == [2, 2, 1] + + +@pytest.mark.parametrize("sync", [True, False]) +async def test_embed_texts_rejects_non_positive_batch_size(fake_client: FakeAnyLLMClient, sync: bool) -> None: + backend = _backend(fake_client) + with pytest.raises(ValueError, match="positive"): + if sync: + backend.embed_texts(["a"], batch_size=0) + else: + await backend.async_embed_texts(["a"], batch_size=0) + + @pytest.mark.parametrize("sync", [True, False]) async def test_embed_texts_rejects_backend_without_embeddings(fake_client: FakeAnyLLMClient, sync: bool) -> None: no_embed_caps = ModelCapabilities( @@ -172,6 +208,90 @@ async def test_extract_raises_when_provider_did_not_honor_schema(fake_client: Fa await backend.async_extract([{"role": "user", "content": "hi"}], Answer) +@pytest.mark.parametrize("sync", [True, False]) +async def test_extract_raises_after_exhausting_max_retries(fake_client: FakeAnyLLMClient, sync: bool) -> None: + fake_client.completion_response = FakeChatCompletion( + choices=[FakeChoice(message=FakeChatCompletionMessage(parsed=None))] + ) + backend = _backend(fake_client) + with pytest.raises(NoParsedOutputError): + if sync: + backend.extract([{"role": "user", "content": "hi"}], Answer, max_retries=2) + else: + await backend.async_extract([{"role": "user", "content": "hi"}], Answer, max_retries=2) + assert len(fake_client.completion_calls) == 3 # initial attempt + 2 retries + + +@pytest.mark.parametrize("sync", [True, False]) +async def test_extract_retries_after_no_parsed_output_and_succeeds( + fake_client: FakeAnyLLMClient, sync: bool +) -> None: + responses = [ + FakeChatCompletion(choices=[FakeChoice(message=FakeChatCompletionMessage(parsed=None))]), + FakeChatCompletion(choices=[FakeChoice(message=FakeChatCompletionMessage(parsed=Answer(value="42")))]), + ] + calls: list[dict] = [] + + def fake_completion(**kwargs) -> FakeChatCompletion: + calls.append(kwargs) + return responses[len(calls) - 1] + + async def fake_acompletion(**kwargs) -> FakeChatCompletion: + return fake_completion(**kwargs) + + # Instance attribute assignment doesn't need a `self` parameter, unlike + # the class-declared bound method ty checks this against. + fake_client.completion = fake_completion # ty: ignore[invalid-assignment] + fake_client.acompletion = fake_acompletion # ty: ignore[invalid-assignment] + backend = _backend(fake_client) + + result = ( + backend.extract([{"role": "user", "content": "hi"}], Answer, max_retries=1) + if sync + else await backend.async_extract([{"role": "user", "content": "hi"}], Answer, max_retries=1) + ) + assert result == Answer(value="42") + assert len(calls) == 2 + # the retried call carries the original message plus a corrective follow-up + assert calls[1]["messages"][0] == {"role": "user", "content": "hi"} + assert len(calls[1]["messages"]) == 2 + assert calls[1]["messages"][1]["role"] == "user" + + +@pytest.mark.parametrize("sync", [True, False]) +async def test_extract_retries_after_validation_error_and_succeeds( + fake_client: FakeAnyLLMClient, sync: bool +) -> None: + fake_client.completion_response = FakeChatCompletion( + choices=[FakeChoice(message=FakeChatCompletionMessage(parsed=Answer(value="42")))] + ) + calls: list[dict] = [] + + def fake_completion(**kwargs) -> FakeChatCompletion: + calls.append(kwargs) + if len(calls) == 1: + Answer.model_validate({}) # raises pydantic.ValidationError: 'value' is required + assert fake_client.completion_response is not None + return fake_client.completion_response + + async def fake_acompletion(**kwargs) -> FakeChatCompletion: + return fake_completion(**kwargs) + + # Instance attribute assignment doesn't need a `self` parameter, unlike + # the class-declared bound method ty checks this against. + fake_client.completion = fake_completion # ty: ignore[invalid-assignment] + fake_client.acompletion = fake_acompletion # ty: ignore[invalid-assignment] + backend = _backend(fake_client) + + result = ( + backend.extract([{"role": "user", "content": "hi"}], Answer, max_retries=1) + if sync + else await backend.async_extract([{"role": "user", "content": "hi"}], Answer, max_retries=1) + ) + assert result == Answer(value="42") + assert len(calls) == 2 + + def test_build_backend_constructs_offline_for_local_provider() -> None: backend = build_backend( provider="llamacpp", model="local-chat", base_url="http://localhost:8080/v1" From 5238bbfe90de05b6824c36738905a77f2a22123b Mon Sep 17 00:00:00 2001 From: Nico Loesch Date: Tue, 28 Jul 2026 01:23:27 +0000 Subject: [PATCH 07/20] Include bug reference --- src/omop_llm/providers/supported.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/omop_llm/providers/supported.py b/src/omop_llm/providers/supported.py index d9d6efc..a114ec5 100644 --- a/src/omop_llm/providers/supported.py +++ b/src/omop_llm/providers/supported.py @@ -142,6 +142,7 @@ def _convert_completion_params(params: CompletionParams, **kwargs: Any) -> dict[ Notes ----- - poppped_before_options: Popped in any_llm/providers/ollama/ollama.py:L.202-203 + - Tracked in: https://github.com/mozilla-ai/any-llm/issues/1206 """ popped_before_options = frozenset({"tools", "think"}) converted = AnyLLMOllamaProvider._convert_completion_params(params, **kwargs) From 2b08076cb1a09cb83fd43bb6572213a1b4b07377 Mon Sep 17 00:00:00 2001 From: Nico Loesch Date: Tue, 28 Jul 2026 01:32:19 +0000 Subject: [PATCH 08/20] Further docstring cleanup --- src/omop_llm/providers/base.py | 3 +- src/omop_llm/providers/supported.py | 62 ++++++++++------------------- src/omop_llm/structured.py | 15 +++---- 3 files changed, 26 insertions(+), 54 deletions(-) diff --git a/src/omop_llm/providers/base.py b/src/omop_llm/providers/base.py index 9254254..c44c634 100644 --- a/src/omop_llm/providers/base.py +++ b/src/omop_llm/providers/base.py @@ -40,8 +40,7 @@ def canonical_model_name(cls, name: str) -> str: """Return the canonical form of a model name for this provider. The canonical form is the identifier used as a stable key - wherever a consumer persists model identity (e.g. ``omop-emb``'s - embedding registry), and the ``model`` value + wherever a consumer persists model identity, and the ``model`` value :func:`~omop_llm.backend.build_backend` resolves to. Implementations must be idempotent: calling this on an already-canonical name returns the same string unchanged. diff --git a/src/omop_llm/providers/supported.py b/src/omop_llm/providers/supported.py index a114ec5..769e896 100644 --- a/src/omop_llm/providers/supported.py +++ b/src/omop_llm/providers/supported.py @@ -1,26 +1,15 @@ -"""The six providers omop_llm supports, as explicit classes. +"""The providers omop_llm supports, as explicit classes. any-llm itself supports around fifty providers (see its own reference: -https://docs.mozilla.ai/any-llm/providers/); omop_llm intentionally -supports six, matched to what this stack actually runs: local (``ollama``, -``llama-server`` via ``llamacpp``, ``vllm``) and cloud (``openai``, -``anthropic``, ``gemini``). See :mod:`omop_llm.providers.registry` for how -this closed set becomes the allow-list. +https://docs.mozilla.ai/any-llm/providers/). +omop_llm currently supports the following models: +- local (``ollama``, ``llama-server`` via ``llamacpp``, ``vllm``), and +- cloud (``openai``, ``anthropic``, ``gemini``). Each class here subclasses both :class:`~omop_llm.providers.base.ProviderMixin` (our contract: ``TOOL_USE``/``STRUCTURED_OUTPUT``, and the required ``canonical_model_name`` override) and any-llm's own provider class for -that provider. any-llm's own classes are imported under an ``AnyLLM``- -prefixed alias specifically so ours can keep the same short name -any-llm uses (``OllamaProvider``, not ``OmopLlmOllamaProvider``) without -colliding: the two are distinguished by which module they live in -(``omop_llm.providers`` vs. ``any_llm.providers.ollama.ollama``), not by a -repeated prefix on every reference to our own class. - -Written out explicitly rather than generated from a loop or factory, on -purpose: ``canonical_model_name`` is a required override specifically so -adding a provider forces a conscious decision about its naming rules, -which a generated class would silently default around. +that provider. """ from __future__ import annotations @@ -47,12 +36,10 @@ class OllamaProvider(ProviderMixin, AnyLLMOllamaProvider): """Wrapped Ollama provider, for local dev and TRE fallback. Extends any-llm's own ``OllamaProvider`` with canonical model naming and embedding-dimension lookup via Ollama's native ``POST /api/show``. - Supports structured output natively using ``response_format``. - No default ``base_url``: falls through to the official ``ollama`` - SDK's own default (``http://localhost:11434``). No ``api_key`` - required. + ``base_url`` defaults to ``http://localhost:11434`` if not given. + ``api_key`` is not required. """ TOOL_USE = True @@ -161,10 +148,8 @@ def _convert_completion_params(params: CompletionParams, **kwargs: Any) -> dict[ class LlamacppProvider(ProviderMixin, AnyLLMLlamacppProvider): """llama.cpp's ``llama-server``. Covers local dev and a CUDA/TRE fallback profile. - The wire contract is the same either way; only ``base_url`` changes. - Defaults to ``http://127.0.0.1:8080/v1`` (any-llm's own default, - matching ``llama-server``'s conventional port) when ``base_url`` is - not given. No ``api_key`` required. + ``base_url`` defaults to ``http://127.0.0.1:8080/v1`` if not given. + ``api_key`` is not required. """ TOOL_USE = True @@ -179,11 +164,8 @@ def canonical_model_name(cls, name: str) -> str: class VllmProvider(ProviderMixin, AnyLLMVllmProvider): """vLLM, the preferred TRE/NVIDIA backend. - Defaults to ``http://localhost:8000/v1`` (any-llm's own default, - matching vLLM's conventional port) when ``base_url`` is not given. - ``api_key`` is optional (confirmed: any-llm's ``VllmProvider`` - overrides key verification to make it so, since self-hosted vLLM - commonly runs without auth). + ``base_url`` defaults to ``http://localhost:8000/v1`` if not given. + ``api_key`` is optional since self-hosted vLLM commonly runs without auth. """ TOOL_USE = True @@ -217,17 +199,15 @@ def canonical_model_name(cls, name: str) -> str: class AnthropicProvider(ProviderMixin, AnyLLMAnthropicProvider): """Anthropic (Claude). - any-llm sets no explicit default ``base_url`` for this provider; it - falls through to the ``anthropic`` SDK's own default (the real - Anthropic API) when not given. Requires ``api_key`` (explicit, or the - ``ANTHROPIC_API_KEY`` environment variable). + ``base_url`` defaults to ``anthropic`` SDK default when not given. + ``api_key`` is required (explicit, or the ``ANTHROPIC_API_KEY`` env var). - Note: any-llm's ``get_provider_metadata()`` reports ``embedding=False`` + Notes + ----- + any-llm's ``get_provider_metadata()`` reports ``embedding=False`` for Anthropic (it has no embeddings API), so :meth:`omop_llm.backend.ModelBackend.embed_texts` refuses this - provider. That is unrelated to ``TOOL_USE``/``STRUCTURED_OUTPUT`` - below: Anthropic's Messages API supports tool calling and tool-based - structured output regardless of the missing embeddings surface. + provider. """ TOOL_USE = True @@ -242,10 +222,8 @@ def canonical_model_name(cls, name: str) -> str: class GeminiProvider(ProviderMixin, AnyLLMGeminiProvider): """Gemini, e.g. ``gemini-2.5-pro``. - any-llm sets no explicit default ``base_url`` for this provider; it - falls through to the ``google-genai`` SDK's own default (the real - Gemini API) when not given. Requires ``api_key`` (explicit, or the - ``GEMINI_API_KEY``/``GOOGLE_API_KEY`` environment variables). + ``base_url`` defaults to ``gemini`` SDK default when not given. + ``api_key`` is required (explicit, or the ``GEMINI_API_KEY``/``GOOGLE_API_KEY`` env var). """ TOOL_USE = True diff --git a/src/omop_llm/structured.py b/src/omop_llm/structured.py index 01e17fe..4f7a49f 100644 --- a/src/omop_llm/structured.py +++ b/src/omop_llm/structured.py @@ -15,21 +15,15 @@ - ``ollama`` is not safe to route through it: instructor's own Ollama builder constructs a plain ``openai.AsyncOpenAI(base_url=".../v1")`` client, the OpenAI-compat shim, not native ``/api/chat``, and picks - TOOLS-vs-JSON mode from a hardcoded model-name-substring list (the exact - "guess capability from the model name" anti-pattern this whole package - exists to retire). Using it for ``ollama`` would silently regress the - native-transport fidelity ``cava-nlp-shard`` depends on today. -- ``llamacpp``/``vllm`` have no dedicated builder in ``instructor`` at all - (its provider list tops out at roughly 23 hosted vendors). They are - reachable only by routing through instructor's ``openai`` builder with + TOOLS-vs-JSON mode from a hardcoded model-name-substring list +- ``llamacpp``/``vllm`` have no dedicated builder in ``instructor`` at all. + They are reachable only by routing through instructor's ``openai`` builder with an explicit ``base_url`` override, which is what :func:`extract_with_retry`/:func:`async_extract_with_retry` do. - ``anthropic``/``gemini`` are not offered here either: this module only vouches for providers whose any-llm integration is already OpenAI-compat-native, so there is no native-transport distinction to - lose. Requesting anything outside ``{"openai", "llamacpp", "vllm"}`` - raises :class:`~omop_llm.errors.UnsupportedCapabilityError` rather than - silently downgrading transport. + lose. """ from __future__ import annotations @@ -42,6 +36,7 @@ from omop_llm.providers.supported import LlamacppProvider, OpenaiProvider, VllmProvider # Compatible with instructor's generic OpenAI-API client builder. +# See docstring above for details _INSTRUCTOR_SAFE_PROVIDERS = frozenset({ OpenaiProvider.PROVIDER_NAME, LlamacppProvider.PROVIDER_NAME, From 5c5819de4521fe8f71e8bc00a4c0abd8315c8987 Mon Sep 17 00:00:00 2001 From: Nico Loesch Date: Wed, 29 Jul 2026 00:53:14 +0000 Subject: [PATCH 09/20] Rename build_backend to build_model_backend for better disambiguation --- README.md | 8 ++++---- docs/index.md | 6 +++--- docs/providers.md | 2 +- src/omop_llm/__init__.py | 8 ++++---- src/omop_llm/backend.py | 14 +++++++------- src/omop_llm/providers/base.py | 2 +- src/omop_llm/providers/registry.py | 2 +- tests/test_backend.py | 16 ++++++++-------- tests/test_oa_configurator_integration.py | 8 ++++---- 9 files changed, 33 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 1c206e2..e1ec4c6 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ Shared chat/embedding backend contract for the OMOP stack, built on [any-llm](https://github.com/mozilla-ai/any-llm). One typed `ModelBackend` interface (sync and async methods, both real), a closed set of supported providers (local: `ollama`, `llamacpp`, `vllm`; cloud: `openai`, `anthropic`, `gemini`), and explicit capability declarations instead of provider-name guessing. Extended documentation can be found [here](https://AustralianCancerDataNetwork.github.io/omop-llm). ```python -from omop_llm import build_backend +from omop_llm import build_model_backend -backend = build_backend(provider="ollama", model="llama3.2:8b", base_url="http://localhost:11434") +backend = build_model_backend(provider="ollama", model="llama3.2:8b", base_url="http://localhost:11434") # async response = await backend.async_complete([{"role": "user", "content": "Hello"}]) @@ -18,10 +18,10 @@ Or resolved from an `oa-configurator` stack config: ```python from oa_configurator import Resolver, load_stack_config -from omop_llm import build_backend_from_resolved +from omop_llm import build_model_backend_from_resolved resolved = Resolver(load_stack_config()).resolve_model("embed-default") -backend = build_backend_from_resolved(resolved) +backend = build_model_backend_from_resolved(resolved) ``` See [docs/index.md](docs/index.md) for the full design (provider registry, capability model, structured extraction). diff --git a/docs/index.md b/docs/index.md index 07d0477..e9e9bd8 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,14 +4,14 @@ ## What it provides -- **`ModelBackend`** ([reference](reference.md#omop_llm.backend)): the one calling contract every consumer uses, built by `build_backend(provider, model, ...)`. It wraps a single [any-llm](https://github.com/mozilla-ai/any-llm) provider instance and exposes chat completion, embeddings, and structured extraction as methods on one object, each with a synchronous form and an `async_`-prefixed asynchronous form (`complete`/`async_complete`, `embed_texts`/`async_embed_texts`, and so on), so both async and fully synchronous consumers get a real, non-hand-rolled path. +- **`ModelBackend`** ([reference](reference.md#omop_llm.backend)): the one calling contract every consumer uses, built by `build_model_backend(provider, model, ...)`. It wraps a single [any-llm](https://github.com/mozilla-ai/any-llm) provider instance and exposes chat completion, embeddings, and structured extraction as methods on one object, each with a synchronous form and an `async_`-prefixed asynchronous form (`complete`/`async_complete`, `embed_texts`/`async_embed_texts`, and so on), so both async and fully synchronous consumers get a real, non-hand-rolled path. - **A closed provider registry** ([reference](reference.md#omop_llm.providers)): `omop-llm` supports selected providers, not any-llm's full set shown in the [Providers overview](providers.md). Every supported provider is a real subclass of any-llm's own provider class, which is both the allow-list (nothing outside this set is reachable through `omop-llm`) and the seam for provider-specific behavior, such as Ollama's canonical model naming and embedding-dimension fast path (see `omop_llm.providers.supported`). - **An explicit capability model** ([`ModelCapabilities`](reference.md#omop_llm.capabilities)): `streaming`/`embeddings`/`extended_thinking` come straight from any-llm's own provider metadata. `tool_use`/`structured_output` do not exist as any-llm capability flags at all (confirmed by reading its `ProviderMetadata` type directly), so `omop-llm` declares those two itself, meaning a caller requiring a capability the resolved backend does not have fails at construction time, not mid-run. - **Structured single-object extraction** (`ModelBackend.extract`/`async_extract`): pulling one validated Pydantic object out of one LLM call. This is *not* the same problem as multi-turn agentic tool use (a model calling several real tools across several turns), which stays on `ModelBackend.complete(messages, tools=...)` directly. See [`omop_llm.structured`](reference.md#omop_llm.structured)'s own docstring for why the primary strategy is any-llm's native `response_format=` translation, and why `instructor`-based extraction (the optional fallback) is only offered for `openai`/`llamacpp`/`vllm`, not `ollama`/`anthropic`/`gemini`. `omop-llm` depends on `oa-configurator` for config resolution. Two entry points: -1. `build_backend(provider, model, ...)` takes plain keyword arguments directly, and -2. `build_backend_from_resolved(resolved)` takes an `oa_configurator.ResolvedModel` (from `Resolver(stack).resolve_model(name)`) and does the field mapping for you. +1. `build_model_backend(provider, model, ...)` takes plain keyword arguments directly, and +2. `build_model_backend_from_resolved(resolved)` takes an `oa_configurator.ResolvedModel` (from `Resolver(stack).resolve_model(name)`) and does the field mapping for you. `omop-llm` has no `PackageConfigBase` subclass of its own: it has no inherent specific model it needs. Each real consumer declares its own plain string field (e.g. `embedding_model: str = "embed-default"`) naming a `[models.*]` entry, and resolves it itself. diff --git a/docs/providers.md b/docs/providers.md index 34c671a..5e6dbe0 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -9,7 +9,7 @@ Given the interface we have devised, future providers will be extended in the fu ## `base_url` and `api_key` -Every one of these fields is optional on `build_backend(provider, model, base_url=None, api_key=None, ...)`. What "not set" resolves to differs per provider. +Every one of these fields is optional on `build_model_backend(provider, model, base_url=None, api_key=None, ...)`. What "not set" resolves to differs per provider. Resolution order for both, always: **explicit argument → the provider's own environment variable → a class-level default (if any)**. | Provider | Default `base_url` when not set | `api_key` required? | diff --git a/src/omop_llm/__init__.py b/src/omop_llm/__init__.py index 91ccaf3..71d9f9b 100644 --- a/src/omop_llm/__init__.py +++ b/src/omop_llm/__init__.py @@ -1,6 +1,6 @@ from omop_llm.backend import ModelBackend as ModelBackend -from omop_llm.backend import build_backend as build_backend -from omop_llm.backend import build_backend_from_resolved as build_backend_from_resolved +from omop_llm.backend import build_model_backend as build_model_backend +from omop_llm.backend import build_model_backend_from_resolved as build_model_backend_from_resolved from omop_llm.capabilities import ModelCapabilities as ModelCapabilities from omop_llm.errors import OmopLlmError as OmopLlmError from omop_llm.errors import UnsupportedCapabilityError as UnsupportedCapabilityError @@ -15,8 +15,8 @@ "OmopLlmError", "UnsupportedCapabilityError", "UnsupportedProviderError", - "build_backend", - "build_backend_from_resolved", + "build_model_backend", + "build_model_backend_from_resolved", "canonical_model_name", "capabilities_for", "supported_providers", diff --git a/src/omop_llm/backend.py b/src/omop_llm/backend.py index 8f211ae..64677d5 100644 --- a/src/omop_llm/backend.py +++ b/src/omop_llm/backend.py @@ -62,7 +62,7 @@ def _chunked[T](items: list[T], size: int) -> Iterator[list[T]]: class ModelBackend: """One resolved, ready-to-call model. - Built by :func:`build_backend`. Wraps a single constructed any-llm + Built by :func:`build_model_backend`. Wraps a single constructed any-llm provider instance and binds ``model``/``configuration`` to it, so callers do not repeat them on every call. @@ -564,7 +564,7 @@ async def async_is_available(self, **kwargs: Any) -> bool: return True -def build_backend( +def build_model_backend( provider: str, model: str, *, @@ -578,7 +578,7 @@ def build_backend( shape ``oa-configurator``'s own database resolution already uses (``Resolver(stack).resolve_resource(name).create_engine(**kwargs)`` returns a plain ``sqlalchemy.Engine``, no intermediate config object). - See :func:`build_backend_from_resolved` for the ``oa-configurator`` + See :func:`build_model_backend_from_resolved` for the ``oa-configurator`` integration built on top of this function. Canonicalizes ``model`` for the resolved provider (see @@ -624,7 +624,7 @@ def build_backend( ) -def build_backend_from_resolved(resolved: ResolvedModel) -> ModelBackend: +def build_model_backend_from_resolved(resolved: ResolvedModel) -> ModelBackend: """Build a backend from an ``oa-configurator`` ``ResolvedModel``. The ``oa-configurator`` integration point: ``oa-configurator`` itself @@ -637,11 +637,11 @@ def build_backend_from_resolved(resolved: ResolvedModel) -> ModelBackend: A typical caller (e.g. a package's own config module) does:: from oa_configurator import Resolver, load_stack_config - from omop_llm import build_backend_from_resolved + from omop_llm import build_model_backend_from_resolved stack = load_stack_config() resolved = Resolver(stack).resolve_model(config.embedding_model) - backend = build_backend_from_resolved(resolved) + backend = build_model_backend_from_resolved(resolved) Parameters ---------- @@ -660,7 +660,7 @@ def build_backend_from_resolved(resolved: ResolvedModel) -> ModelBackend: If ``resolved.model`` cannot be made canonical for the resolved provider (e.g. an Ollama name with no explicit tag). """ - return build_backend( + return build_model_backend( provider=resolved.provider.provider, model=resolved.model, base_url=resolved.provider.base_url, diff --git a/src/omop_llm/providers/base.py b/src/omop_llm/providers/base.py index c44c634..a1d0fd0 100644 --- a/src/omop_llm/providers/base.py +++ b/src/omop_llm/providers/base.py @@ -41,7 +41,7 @@ def canonical_model_name(cls, name: str) -> str: The canonical form is the identifier used as a stable key wherever a consumer persists model identity, and the ``model`` value - :func:`~omop_llm.backend.build_backend` resolves to. Implementations + :func:`~omop_llm.backend.build_model_backend` resolves to. Implementations must be idempotent: calling this on an already-canonical name returns the same string unchanged. diff --git a/src/omop_llm/providers/registry.py b/src/omop_llm/providers/registry.py index 844e0b7..db36778 100644 --- a/src/omop_llm/providers/registry.py +++ b/src/omop_llm/providers/registry.py @@ -116,7 +116,7 @@ def canonical_model_name(provider_key: str, name: str) -> str: Useful for deciding what to persist as a model's stable identity (e.g. in a database) independently of building a full - :class:`~omop_llm.backend.ModelBackend`. :func:`~omop_llm.backend.build_backend` + :class:`~omop_llm.backend.ModelBackend`. :func:`~omop_llm.backend.build_model_backend` also calls this internally, so a backend's ``model`` attribute is always canonical without callers needing to remember to do it themselves. diff --git a/tests/test_backend.py b/tests/test_backend.py index b12d45d..95314f4 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -2,7 +2,7 @@ Uses ``FakeAnyLLMClient`` (see conftest.py) to test omop_llm's own wrapper logic in isolation, plus real (offline-constructed, never called over the -network) provider instances to test ``build_backend``'s construction, +network) provider instances to test ``build_model_backend``'s construction, canonicalization, and capability-gate behavior. """ @@ -11,7 +11,7 @@ import pytest from pydantic import BaseModel -from omop_llm.backend import ModelBackend, build_backend +from omop_llm.backend import ModelBackend, build_model_backend from omop_llm.capabilities import ModelCapabilities from omop_llm.errors import NoParsedOutputError, UnsupportedCapabilityError from tests.conftest import ( @@ -293,7 +293,7 @@ async def fake_acompletion(**kwargs) -> FakeChatCompletion: def test_build_backend_constructs_offline_for_local_provider() -> None: - backend = build_backend( + backend = build_model_backend( provider="llamacpp", model="local-chat", base_url="http://localhost:8080/v1" ) assert backend.model == "local-chat" @@ -301,12 +301,12 @@ def test_build_backend_constructs_offline_for_local_provider() -> None: def test_provider_property_reads_from_the_constructed_client_not_a_stored_field() -> None: - backend = build_backend(provider="llamacpp", model="local-chat", base_url="http://localhost:8080/v1") + backend = build_model_backend(provider="llamacpp", model="local-chat", base_url="http://localhost:8080/v1") assert backend.provider == "llamacpp" def test_build_backend_passes_configuration_through() -> None: - backend = build_backend( + backend = build_model_backend( provider="llamacpp", model="local-chat", base_url="http://localhost:8080/v1", @@ -316,17 +316,17 @@ def test_build_backend_passes_configuration_through() -> None: def test_build_backend_canonicalizes_the_model_name() -> None: - backend = build_backend(provider="ollama", model="llama3:8b", base_url="http://localhost:11434") + backend = build_model_backend(provider="ollama", model="llama3:8b", base_url="http://localhost:11434") assert backend.model == "llama3:8b" def test_build_backend_rejects_non_canonical_ollama_name() -> None: with pytest.raises(ValueError, match="explicit tag"): - build_backend(provider="ollama", model="llama3", base_url="http://localhost:11434") + build_model_backend(provider="ollama", model="llama3", base_url="http://localhost:11434") def test_build_backend_constructs_offline_for_embedding_capable_provider() -> None: - backend = build_backend( + backend = build_model_backend( provider="ollama", model="qwen3-embedding:0.6b", base_url="http://localhost:11434" ) assert backend.model == "qwen3-embedding:0.6b" diff --git a/tests/test_oa_configurator_integration.py b/tests/test_oa_configurator_integration.py index 19e8fc5..40a80cd 100644 --- a/tests/test_oa_configurator_integration.py +++ b/tests/test_oa_configurator_integration.py @@ -1,4 +1,4 @@ -"""``build_backend_from_resolved``: the oa-configurator integration point. +"""``build_model_backend_from_resolved``: the oa-configurator integration point. Constructs ``oa_configurator.ResolvedModel``/``ResolvedProvider`` directly (no TOML file, no stack config needed) to test the field mapping in @@ -9,7 +9,7 @@ from oa_configurator.resolver import ResolvedModel, ResolvedProvider -from omop_llm.backend import build_backend_from_resolved +from omop_llm.backend import build_model_backend_from_resolved def test_maps_resolved_fields_onto_build_backend() -> None: @@ -24,7 +24,7 @@ def test_maps_resolved_fields_onto_build_backend() -> None: model="local-chat", configuration={"max_tokens": 8000, "temperature": 0.0}, ) - backend = build_backend_from_resolved(resolved) + backend = build_model_backend_from_resolved(resolved) assert backend.model == "local-chat" assert backend.configuration == {"max_tokens": 8000, "temperature": 0.0} assert backend.capabilities.tool_use is True @@ -37,5 +37,5 @@ def test_canonicalizes_the_model_name() -> None: model="llama3:8b", configuration={}, ) - backend = build_backend_from_resolved(resolved) + backend = build_model_backend_from_resolved(resolved) assert backend.model == "llama3:8b" From 2e1b8620e63b41277a6693ffb45619e4e299189b Mon Sep 17 00:00:00 2001 From: Nico Loesch Date: Wed, 29 Jul 2026 03:32:30 +0000 Subject: [PATCH 10/20] Ingest prefixing for asymmetric models --- docs/index.md | 1 + docs/reference.md | 1 + src/omop_llm/__init__.py | 33 ++++++--- src/omop_llm/backend.py | 53 ++++++++++++-- src/omop_llm/embeddings.py | 79 +++++++++++++++++++++ tests/test_backend.py | 84 +++++++++++++++++++++++ tests/test_embeddings.py | 81 ++++++++++++++++++++++ tests/test_oa_configurator_integration.py | 40 +++++++++++ 8 files changed, 358 insertions(+), 14 deletions(-) create mode 100644 src/omop_llm/embeddings.py create mode 100644 tests/test_embeddings.py diff --git a/docs/index.md b/docs/index.md index e9e9bd8..833d9d6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -5,6 +5,7 @@ ## What it provides - **`ModelBackend`** ([reference](reference.md#omop_llm.backend)): the one calling contract every consumer uses, built by `build_model_backend(provider, model, ...)`. It wraps a single [any-llm](https://github.com/mozilla-ai/any-llm) provider instance and exposes chat completion, embeddings, and structured extraction as methods on one object, each with a synchronous form and an `async_`-prefixed asynchronous form (`complete`/`async_complete`, `embed_texts`/`async_embed_texts`, and so on), so both async and fully synchronous consumers get a real, non-hand-rolled path. +- **Asymmetric embedding prefixes** ([`EmbeddingRole`](reference.md#omop_llm.embeddings)): asymmetric embedding models (nomic-embed-text, the E5 family, BGE, and others) need a different prefix prepended depending on whether the text is being indexed or used to search. `embed_texts(texts, role=EmbeddingRole.DOCUMENT)`/`role=EmbeddingRole.QUERY` applies whichever of `configuration["document_prefix"]`/`["query_prefix"]` matches. `build_model_backend` warns once, at construction, if an embedding-capable backend has no prefixes configured or a configured prefix doesn't match a commonly recognized convention (`omop_llm.embeddings.KNOWN_EMBEDDING_PREFIXES`); neither warning blocks construction, since a missing or unusual prefix isn't necessarily wrong. `build_model_backend_from_resolved` sources these (and `embedding_dim`) from `oa-configurator`'s own typed `ModelConfig.document_prefix`/`query_prefix`/`embedding_dim` fields when set, so they live in one place (the model's own `[models.*]` entry) rather than being duplicated per consuming package. - **A closed provider registry** ([reference](reference.md#omop_llm.providers)): `omop-llm` supports selected providers, not any-llm's full set shown in the [Providers overview](providers.md). Every supported provider is a real subclass of any-llm's own provider class, which is both the allow-list (nothing outside this set is reachable through `omop-llm`) and the seam for provider-specific behavior, such as Ollama's canonical model naming and embedding-dimension fast path (see `omop_llm.providers.supported`). - **An explicit capability model** ([`ModelCapabilities`](reference.md#omop_llm.capabilities)): `streaming`/`embeddings`/`extended_thinking` come straight from any-llm's own provider metadata. `tool_use`/`structured_output` do not exist as any-llm capability flags at all (confirmed by reading its `ProviderMetadata` type directly), so `omop-llm` declares those two itself, meaning a caller requiring a capability the resolved backend does not have fails at construction time, not mid-run. - **Structured single-object extraction** (`ModelBackend.extract`/`async_extract`): pulling one validated Pydantic object out of one LLM call. This is *not* the same problem as multi-turn agentic tool use (a model calling several real tools across several turns), which stays on `ModelBackend.complete(messages, tools=...)` directly. See [`omop_llm.structured`](reference.md#omop_llm.structured)'s own docstring for why the primary strategy is any-llm's native `response_format=` translation, and why `instructor`-based extraction (the optional fallback) is only offered for `openai`/`llamacpp`/`vllm`, not `ollama`/`anthropic`/`gemini`. diff --git a/docs/reference.md b/docs/reference.md index 05aaed9..052a0d9 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -7,6 +7,7 @@ This reference is automatically generated from the source code. members: - backend - capabilities + - embeddings - errors - providers - structured diff --git a/src/omop_llm/__init__.py b/src/omop_llm/__init__.py index 71d9f9b..4f973c1 100644 --- a/src/omop_llm/__init__.py +++ b/src/omop_llm/__init__.py @@ -1,15 +1,28 @@ -from omop_llm.backend import ModelBackend as ModelBackend -from omop_llm.backend import build_model_backend as build_model_backend -from omop_llm.backend import build_model_backend_from_resolved as build_model_backend_from_resolved -from omop_llm.capabilities import ModelCapabilities as ModelCapabilities -from omop_llm.errors import OmopLlmError as OmopLlmError -from omop_llm.errors import UnsupportedCapabilityError as UnsupportedCapabilityError -from omop_llm.errors import UnsupportedProviderError as UnsupportedProviderError -from omop_llm.providers import canonical_model_name as canonical_model_name -from omop_llm.providers import capabilities_for as capabilities_for -from omop_llm.providers import supported_providers as supported_providers +from omop_llm.backend import ( + ModelBackend, + build_model_backend, + build_model_backend_from_resolved, +) + +from omop_llm.capabilities import ModelCapabilities +from omop_llm.embeddings import ( + EmbeddingRole, + KNOWN_EMBEDDING_PREFIXES +) +from omop_llm.errors import ( + OmopLlmError, + UnsupportedCapabilityError, + UnsupportedProviderError +) +from omop_llm.providers import ( + canonical_model_name, + capabilities_for, + supported_providers +) __all__ = [ + "EmbeddingRole", + "KNOWN_EMBEDDING_PREFIXES", "ModelBackend", "ModelCapabilities", "OmopLlmError", diff --git a/src/omop_llm/backend.py b/src/omop_llm/backend.py index 64677d5..d9e0e11 100644 --- a/src/omop_llm/backend.py +++ b/src/omop_llm/backend.py @@ -28,6 +28,7 @@ from pydantic import BaseModel, ValidationError from omop_llm.capabilities import ModelCapabilities +from omop_llm.embeddings import EmbeddingRole, apply_embedding_prefix, warn_if_prefixes_look_wrong from omop_llm.errors import NoParsedOutputError, UnsupportedCapabilityError from omop_llm.providers.base import ProviderMixin from omop_llm.providers.registry import ( @@ -211,13 +212,26 @@ async def async_complete( model=self.model, messages=messages, **call_kwargs ) - def embed_texts(self, texts: list[str], *, batch_size: int | None = None) -> list[list[float]]: + def embed_texts( + self, + texts: list[str], + *, + role: EmbeddingRole | None = None, + batch_size: int | None = None, + ) -> list[list[float]]: """Embed a batch of texts. Parameters ---------- texts : list of str Texts to embed. + role : EmbeddingRole, optional + Whether ``texts`` are being indexed (``DOCUMENT``) or used to + search (``QUERY``). When given, prepends whichever of + ``configuration["document_prefix"]``/``["query_prefix"]`` + matches, needed for asymmetric embedding models (e.g. + nomic-embed-text, E5, BGE). Omit for symmetric models, or when + texts are already prefixed. batch_size : int, optional If given, ``texts`` is chunked into sub-batches of at most this size, each sent as its own call, rather than one call with the @@ -238,6 +252,8 @@ def embed_texts(self, texts: list[str], *, batch_size: int | None = None) -> lis If ``batch_size`` is not a positive integer. """ self._require_embeddings() + if role is not None: + texts = apply_embedding_prefix(texts, role, self.configuration) if batch_size is None: response = self._client._embedding(model=self.model, inputs=texts, **self.configuration) return [item.embedding for item in response.data] @@ -248,7 +264,11 @@ def embed_texts(self, texts: list[str], *, batch_size: int | None = None) -> lis return vectors async def async_embed_texts( - self, texts: list[str], *, batch_size: int | None = None + self, + texts: list[str], + *, + role: EmbeddingRole | None = None, + batch_size: int | None = None, ) -> list[list[float]]: """Embed a batch of texts asynchronously. @@ -256,6 +276,13 @@ async def async_embed_texts( ---------- texts : list of str Texts to embed. + role : EmbeddingRole, optional + Whether ``texts`` are being indexed (``DOCUMENT``) or used to + search (``QUERY``). When given, prepends whichever of + ``configuration["document_prefix"]``/``["query_prefix"]`` + matches, needed for asymmetric embedding models (e.g. + nomic-embed-text, E5, BGE). Omit for symmetric models, or when + texts are already prefixed. batch_size : int, optional If given, ``texts`` is chunked into sub-batches of at most this size, each sent as its own call, rather than one call with the @@ -276,6 +303,8 @@ async def async_embed_texts( If ``batch_size`` is not a positive integer. """ self._require_embeddings() + if role is not None: + texts = apply_embedding_prefix(texts, role, self.configuration) if batch_size is None: response = await self._client.aembedding(model=self.model, inputs=texts, **self.configuration) return [item.embedding for item in response.data] @@ -614,12 +643,15 @@ def build_model_backend( provider_class = provider_class_for(provider) capabilities = capabilities_for(provider) canonical_model = canonical_model_name(provider, model) + resolved_configuration = dict(configuration) if configuration else {} + if capabilities.embeddings: + warn_if_prefixes_look_wrong(model=canonical_model, configuration=resolved_configuration) client = provider_class(api_key=api_key, api_base=base_url) return ModelBackend( _client=client, model=canonical_model, capabilities=capabilities, - configuration=dict(configuration) if configuration else {}, + configuration=resolved_configuration, _api_base=base_url, ) @@ -643,6 +675,12 @@ def build_model_backend_from_resolved(resolved: ResolvedModel) -> ModelBackend: resolved = Resolver(stack).resolve_model(config.embedding_model) backend = build_model_backend_from_resolved(resolved) + ``resolved.embedding_dim``/``document_prefix``/``query_prefix`` (typed + ``ModelConfig`` fields) are folded into the ``configuration`` dict under + their matching keys before construction, taking precedence over the same + keys if also present in ``resolved.configuration`` (the free-form + fallback for knobs with no dedicated field). + Parameters ---------- resolved : ResolvedModel @@ -660,10 +698,17 @@ def build_model_backend_from_resolved(resolved: ResolvedModel) -> ModelBackend: If ``resolved.model`` cannot be made canonical for the resolved provider (e.g. an Ollama name with no explicit tag). """ + configuration = dict(resolved.configuration) + if resolved.embedding_dim is not None: + configuration["embedding_dim"] = resolved.embedding_dim + if resolved.document_prefix is not None: + configuration["document_prefix"] = resolved.document_prefix + if resolved.query_prefix is not None: + configuration["query_prefix"] = resolved.query_prefix return build_model_backend( provider=resolved.provider.provider, model=resolved.model, base_url=resolved.provider.base_url, api_key=resolved.provider.api_key, - configuration=resolved.configuration, + configuration=configuration, ) diff --git a/src/omop_llm/embeddings.py b/src/omop_llm/embeddings.py new file mode 100644 index 0000000..e4f97af --- /dev/null +++ b/src/omop_llm/embeddings.py @@ -0,0 +1,79 @@ +"""Embedding role prefixing: ``EmbeddingRole`` and a best-effort sanity check. + +Asymmetric embedding models (nomic-embed-text, the E5 family, BGE, and +others) are trained with distinct prefixes for the text being indexed +versus the text used to search it. Sending text without the correct +prefix produces a valid-looking embedding that just retrieves badly, +with no error to notice. +""" + +from __future__ import annotations + +import logging +from enum import StrEnum +from typing import Any + +logger = logging.getLogger(__name__) + + +class EmbeddingRole(StrEnum): + """Role of text being embedded, for models with asymmetric prefixes.""" + + DOCUMENT = "document" + QUERY = "query" + + +CONFIGURATION_KEY_BY_ROLE: dict[EmbeddingRole, str] = { + EmbeddingRole.DOCUMENT: "document_prefix", + EmbeddingRole.QUERY: "query_prefix", +} + +# Prefix conventions used by common asymmetric embedding models. Not +# exhaustive, and not meant to be: used only to flag a configured prefix +# that doesn't match anything recognized, never to reject or "correct" +# one. New, legitimate conventions turn up regularly; extend this set +# as they're encountered rather than trying to gate on it. +KNOWN_EMBEDDING_PREFIXES: frozenset[str] = frozenset( + { + "search_document: ", + "search_query: ", + "passage: ", + "query: ", + "Represent this sentence for searching relevant passages: ", + } +) + + +def apply_embedding_prefix( + texts: list[str], role: EmbeddingRole, configuration: dict[str, Any] +) -> list[str]: + """Prepend *role*'s configured prefix to each of *texts*, if one is set.""" + prefix = configuration.get(CONFIGURATION_KEY_BY_ROLE[role], "") + if not prefix: + return texts + return [f"{prefix}{text}" for text in texts] + + +def warn_if_prefixes_look_wrong(*, model: str, configuration: dict[str, Any]) -> None: + """Log a warning for a missing or unrecognized configured prefix. + + Called once, at :func:`~omop_llm.backend.build_model_backend` time, for + any backend that declares embeddings support. Never raises: a prefix + outside :data:`KNOWN_EMBEDDING_PREFIXES` is not necessarily wrong, this + is a heads-up, not validation. + """ + for role, key in CONFIGURATION_KEY_BY_ROLE.items(): + prefix = configuration.get(key) + if not prefix: + logger.warning( + "%s: no %s configured for model %r. Fine for symmetric models; " + "asymmetric models (e.g. nomic-embed-text, E5, BGE) need one " + "to retrieve correctly.", + role.value.capitalize(), key, model, + ) + elif prefix not in KNOWN_EMBEDDING_PREFIXES: + logger.warning( + "%s prefix %r for model %r doesn't match a commonly recognized " + "convention. Not necessarily wrong, just worth double-checking.", + role.value.capitalize(), prefix, model, + ) diff --git a/tests/test_backend.py b/tests/test_backend.py index 95314f4..054e03c 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -13,6 +13,7 @@ from omop_llm.backend import ModelBackend, build_model_backend from omop_llm.capabilities import ModelCapabilities +from omop_llm.embeddings import EmbeddingRole from omop_llm.errors import NoParsedOutputError, UnsupportedCapabilityError from tests.conftest import ( FakeAnyLLMClient, @@ -127,6 +128,64 @@ async def test_embed_texts_rejects_non_positive_batch_size(fake_client: FakeAnyL await backend.async_embed_texts(["a"], batch_size=0) +@pytest.mark.parametrize("sync", [True, False]) +async def test_embed_texts_applies_role_prefix(fake_client: FakeAnyLLMClient, sync: bool) -> None: + fake_client.embedding_response = FakeEmbeddingResponse(data=[FakeEmbeddingItem(embedding=[0.1])]) + backend = _backend( + fake_client, configuration={"document_prefix": "passage: ", "query_prefix": "query: "} + ) + + if sync: + backend.embed_texts(["diabetes"], role=EmbeddingRole.DOCUMENT) + else: + await backend.async_embed_texts(["diabetes"], role=EmbeddingRole.DOCUMENT) + [call] = fake_client.embedding_calls + assert call["inputs"] == ["passage: diabetes"] + + +@pytest.mark.parametrize("sync", [True, False]) +async def test_embed_texts_query_role_uses_query_prefix(fake_client: FakeAnyLLMClient, sync: bool) -> None: + fake_client.embedding_response = FakeEmbeddingResponse(data=[FakeEmbeddingItem(embedding=[0.1])]) + backend = _backend( + fake_client, configuration={"document_prefix": "passage: ", "query_prefix": "query: "} + ) + + if sync: + backend.embed_texts(["hypertension"], role=EmbeddingRole.QUERY) + else: + await backend.async_embed_texts(["hypertension"], role=EmbeddingRole.QUERY) + [call] = fake_client.embedding_calls + assert call["inputs"] == ["query: hypertension"] + + +@pytest.mark.parametrize("sync", [True, False]) +async def test_embed_texts_no_role_leaves_text_untouched(fake_client: FakeAnyLLMClient, sync: bool) -> None: + fake_client.embedding_response = FakeEmbeddingResponse(data=[FakeEmbeddingItem(embedding=[0.1])]) + backend = _backend(fake_client, configuration={"document_prefix": "passage: "}) + + if sync: + backend.embed_texts(["diabetes"]) + else: + await backend.async_embed_texts(["diabetes"]) + [call] = fake_client.embedding_calls + assert call["inputs"] == ["diabetes"] + + +@pytest.mark.parametrize("sync", [True, False]) +async def test_embed_texts_role_with_no_configured_prefix_is_a_noop( + fake_client: FakeAnyLLMClient, sync: bool +) -> None: + fake_client.embedding_response = FakeEmbeddingResponse(data=[FakeEmbeddingItem(embedding=[0.1])]) + backend = _backend(fake_client) + + if sync: + backend.embed_texts(["diabetes"], role=EmbeddingRole.DOCUMENT) + else: + await backend.async_embed_texts(["diabetes"], role=EmbeddingRole.DOCUMENT) + [call] = fake_client.embedding_calls + assert call["inputs"] == ["diabetes"] + + @pytest.mark.parametrize("sync", [True, False]) async def test_embed_texts_rejects_backend_without_embeddings(fake_client: FakeAnyLLMClient, sync: bool) -> None: no_embed_caps = ModelCapabilities( @@ -331,3 +390,28 @@ def test_build_backend_constructs_offline_for_embedding_capable_provider() -> No ) assert backend.model == "qwen3-embedding:0.6b" assert backend.capabilities.embeddings is True + + +def test_build_backend_warns_on_missing_prefixes_for_embedding_model(caplog: pytest.LogCaptureFixture) -> None: + with caplog.at_level("WARNING", logger="omop_llm.embeddings"): + build_model_backend(provider="ollama", model="qwen3-embedding:0.6b", base_url="http://localhost:11434") + assert "document_prefix" in caplog.text + assert "query_prefix" in caplog.text + + +def test_build_backend_no_warning_when_prefixes_configured(caplog: pytest.LogCaptureFixture) -> None: + with caplog.at_level("WARNING", logger="omop_llm.embeddings"): + build_model_backend( + provider="ollama", + model="qwen3-embedding:0.6b", + base_url="http://localhost:11434", + configuration={"document_prefix": "search_document: ", "query_prefix": "search_query: "}, + ) + assert caplog.text == "" + + +def test_build_backend_no_prefix_warning_for_non_embedding_provider(caplog: pytest.LogCaptureFixture) -> None: + # anthropic is the one provider in the registry with embeddings=False. + with caplog.at_level("WARNING", logger="omop_llm.embeddings"): + build_model_backend(provider="anthropic", model="claude-haiku-4-5", api_key="sk-test") + assert caplog.text == "" diff --git a/tests/test_embeddings.py b/tests/test_embeddings.py new file mode 100644 index 0000000..5249a6c --- /dev/null +++ b/tests/test_embeddings.py @@ -0,0 +1,81 @@ +"""EmbeddingRole prefixing and the known-prefix sanity check.""" + +from __future__ import annotations + +import pytest + +from omop_llm.embeddings import ( + KNOWN_EMBEDDING_PREFIXES, + EmbeddingRole, + apply_embedding_prefix, + warn_if_prefixes_look_wrong, +) + + +class TestApplyEmbeddingPrefix: + def test_document_prefix_applied(self) -> None: + result = apply_embedding_prefix( + ["diabetes"], EmbeddingRole.DOCUMENT, {"document_prefix": "passage: "} + ) + assert result == ["passage: diabetes"] + + def test_query_prefix_applied(self) -> None: + result = apply_embedding_prefix( + ["hypertension"], EmbeddingRole.QUERY, {"query_prefix": "query: "} + ) + assert result == ["query: hypertension"] + + def test_no_configured_prefix_is_a_noop(self) -> None: + result = apply_embedding_prefix(["diabetes"], EmbeddingRole.DOCUMENT, {}) + assert result == ["diabetes"] + + def test_wrong_role_key_is_ignored(self) -> None: + result = apply_embedding_prefix( + ["diabetes"], EmbeddingRole.DOCUMENT, {"query_prefix": "query: "} + ) + assert result == ["diabetes"] + + def test_applies_to_every_text(self) -> None: + result = apply_embedding_prefix( + ["a", "b", "c"], EmbeddingRole.DOCUMENT, {"document_prefix": "p: "} + ) + assert result == ["p: a", "p: b", "p: c"] + + +class TestWarnIfPrefixesLookWrong: + def test_warns_when_both_missing(self, caplog: pytest.LogCaptureFixture) -> None: + with caplog.at_level("WARNING", logger="omop_llm.embeddings"): + warn_if_prefixes_look_wrong(model="nomic-embed-text:v1.5", configuration={}) + assert "document_prefix" in caplog.text + assert "query_prefix" in caplog.text + + def test_no_warning_when_both_known(self, caplog: pytest.LogCaptureFixture) -> None: + with caplog.at_level("WARNING", logger="omop_llm.embeddings"): + warn_if_prefixes_look_wrong( + model="nomic-embed-text:v1.5", + configuration={ + "document_prefix": "search_document: ", + "query_prefix": "search_query: ", + }, + ) + assert caplog.text == "" + + def test_warns_on_unrecognized_prefix(self, caplog: pytest.LogCaptureFixture) -> None: + with caplog.at_level("WARNING", logger="omop_llm.embeddings"): + warn_if_prefixes_look_wrong( + model="some-new-model", + configuration={"document_prefix": "totally_made_up: ", "query_prefix": "query: "}, + ) + assert "totally_made_up: " in caplog.text + assert "doesn't match a commonly recognized" in caplog.text + + def test_does_not_raise_for_unrecognized_prefix(self) -> None: + # A prefix outside KNOWN_EMBEDDING_PREFIXES is a heads-up, not an error. + warn_if_prefixes_look_wrong( + model="some-new-model", configuration={"document_prefix": "custom: ", "query_prefix": "custom: "} + ) + + def test_every_known_prefix_is_a_non_empty_string(self) -> None: + for prefix in KNOWN_EMBEDDING_PREFIXES: + assert isinstance(prefix, str) + assert prefix diff --git a/tests/test_oa_configurator_integration.py b/tests/test_oa_configurator_integration.py index 40a80cd..d64bb37 100644 --- a/tests/test_oa_configurator_integration.py +++ b/tests/test_oa_configurator_integration.py @@ -22,6 +22,9 @@ def test_maps_resolved_fields_onto_build_backend() -> None: api_key=None, ), model="local-chat", + embedding_dim=None, + document_prefix=None, + query_prefix=None, configuration={"max_tokens": 8000, "temperature": 0.0}, ) backend = build_model_backend_from_resolved(resolved) @@ -35,7 +38,44 @@ def test_canonicalizes_the_model_name() -> None: name="m", provider=ResolvedProvider(name="p", provider="ollama", base_url="http://localhost:11434", api_key=None), model="llama3:8b", + embedding_dim=None, + document_prefix=None, + query_prefix=None, configuration={}, ) backend = build_model_backend_from_resolved(resolved) assert backend.model == "llama3:8b" + + +def test_folds_embedding_dim_and_prefixes_into_configuration() -> None: + resolved = ResolvedModel( + name="nomic-embed", + provider=ResolvedProvider(name="p", provider="ollama", base_url="http://localhost:11434", api_key=None), + model="nomic-embed-text:v1.5", + embedding_dim=768, + document_prefix="search_document: ", + query_prefix="search_query: ", + configuration={"max_tokens": 8000}, + ) + backend = build_model_backend_from_resolved(resolved) + assert backend.configuration == { + "max_tokens": 8000, + "embedding_dim": 768, + "document_prefix": "search_document: ", + "query_prefix": "search_query: ", + } + + +def test_dedicated_fields_take_precedence_over_configuration_dict() -> None: + resolved = ResolvedModel( + name="nomic-embed", + provider=ResolvedProvider(name="p", provider="ollama", base_url="http://localhost:11434", api_key=None), + model="nomic-embed-text:v1.5", + embedding_dim=768, + document_prefix="search_document: ", + query_prefix=None, + configuration={"document_prefix": "stale: ", "query_prefix": "query: "}, + ) + backend = build_model_backend_from_resolved(resolved) + assert backend.configuration["document_prefix"] == "search_document: " + assert backend.configuration["query_prefix"] == "query: " From 83851969083e35b007a0b5616456dff9c67b8bb2 Mon Sep 17 00:00:00 2001 From: Nico Loesch Date: Wed, 29 Jul 2026 03:38:43 +0000 Subject: [PATCH 11/20] Include asymmetric embedding docs --- docs/index.md | 3 +- docs/usage/asymmetric-embeddings.md | 65 +++++++++++++++++++++++++++++ mkdocs.yml | 1 + 3 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 docs/usage/asymmetric-embeddings.md diff --git a/docs/index.md b/docs/index.md index 833d9d6..85eb8b3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -5,7 +5,7 @@ ## What it provides - **`ModelBackend`** ([reference](reference.md#omop_llm.backend)): the one calling contract every consumer uses, built by `build_model_backend(provider, model, ...)`. It wraps a single [any-llm](https://github.com/mozilla-ai/any-llm) provider instance and exposes chat completion, embeddings, and structured extraction as methods on one object, each with a synchronous form and an `async_`-prefixed asynchronous form (`complete`/`async_complete`, `embed_texts`/`async_embed_texts`, and so on), so both async and fully synchronous consumers get a real, non-hand-rolled path. -- **Asymmetric embedding prefixes** ([`EmbeddingRole`](reference.md#omop_llm.embeddings)): asymmetric embedding models (nomic-embed-text, the E5 family, BGE, and others) need a different prefix prepended depending on whether the text is being indexed or used to search. `embed_texts(texts, role=EmbeddingRole.DOCUMENT)`/`role=EmbeddingRole.QUERY` applies whichever of `configuration["document_prefix"]`/`["query_prefix"]` matches. `build_model_backend` warns once, at construction, if an embedding-capable backend has no prefixes configured or a configured prefix doesn't match a commonly recognized convention (`omop_llm.embeddings.KNOWN_EMBEDDING_PREFIXES`); neither warning blocks construction, since a missing or unusual prefix isn't necessarily wrong. `build_model_backend_from_resolved` sources these (and `embedding_dim`) from `oa-configurator`'s own typed `ModelConfig.document_prefix`/`query_prefix`/`embedding_dim` fields when set, so they live in one place (the model's own `[models.*]` entry) rather than being duplicated per consuming package. +- **Asymmetric embedding prefixes** ([`EmbeddingRole`](reference.md#omop_llm.embeddings), [full guide](usage/asymmetric-embeddings.md)): asymmetric embedding models (nomic-embed-text, the E5 family, BGE, and others) need a different prefix prepended depending on whether the text is being indexed or used to search. `embed_texts(texts, role=EmbeddingRole.DOCUMENT)`/`role=EmbeddingRole.QUERY` applies it automatically, sourced from `configuration["document_prefix"]`/`["query_prefix"]` (a plain dict, or `oa-configurator`'s typed `ModelConfig.document_prefix`/`query_prefix` fields via `build_model_backend_from_resolved`) so the values live in one place, not duplicated per consuming package. - **A closed provider registry** ([reference](reference.md#omop_llm.providers)): `omop-llm` supports selected providers, not any-llm's full set shown in the [Providers overview](providers.md). Every supported provider is a real subclass of any-llm's own provider class, which is both the allow-list (nothing outside this set is reachable through `omop-llm`) and the seam for provider-specific behavior, such as Ollama's canonical model naming and embedding-dimension fast path (see `omop_llm.providers.supported`). - **An explicit capability model** ([`ModelCapabilities`](reference.md#omop_llm.capabilities)): `streaming`/`embeddings`/`extended_thinking` come straight from any-llm's own provider metadata. `tool_use`/`structured_output` do not exist as any-llm capability flags at all (confirmed by reading its `ProviderMetadata` type directly), so `omop-llm` declares those two itself, meaning a caller requiring a capability the resolved backend does not have fails at construction time, not mid-run. - **Structured single-object extraction** (`ModelBackend.extract`/`async_extract`): pulling one validated Pydantic object out of one LLM call. This is *not* the same problem as multi-turn agentic tool use (a model calling several real tools across several turns), which stays on `ModelBackend.complete(messages, tools=...)` directly. See [`omop_llm.structured`](reference.md#omop_llm.structured)'s own docstring for why the primary strategy is any-llm's native `response_format=` translation, and why `instructor`-based extraction (the optional fallback) is only offered for `openai`/`llamacpp`/`vllm`, not `ollama`/`anthropic`/`gemini`. @@ -26,5 +26,6 @@ ## Documentation overview - [Installation](usage/installation.md) +- [Asymmetric Embeddings](usage/asymmetric-embeddings.md) - [Providers](providers.md) - [API Reference](reference.md) diff --git a/docs/usage/asymmetric-embeddings.md b/docs/usage/asymmetric-embeddings.md new file mode 100644 index 0000000..65f5a22 --- /dev/null +++ b/docs/usage/asymmetric-embeddings.md @@ -0,0 +1,65 @@ +# Asymmetric Embeddings { data-toc-label="Asymmetric Embeddings" } + +## What are asymmetric embedding models? + +Most general-purpose embedding models (e.g. `text-embedding-3-small`) produce vectors in a symmetric space: the same transformation is applied whether you are indexing a document or submitting a search query. + +**Asymmetric models**, such as [nomic-embed-text](https://huggingface.co/nomic-ai/nomic-embed-text-v1.5), [E5](https://huggingface.co/intfloat/e5-large-v2), and [BGE](https://huggingface.co/BAAI/bge-large-en-v1.5), are trained with *task-specific prefixes* prepended to the input. The model's training objective explicitly separates the representation space for documents being indexed from the space for queries being searched. Sending text without the correct prefix does not raise an error, but similarity scores degrade substantially and silently — this is a correctness footgun, not a performance one. + +!!! tip "Prefix examples by model family" + | Model | Document prefix | Query prefix | + |---|---|---| + | `nomic-embed-text:v1.5` | `search_document: ` | `search_query: ` | + | `e5-large-v2` | `passage: ` | `query: ` | + | `bge-large-en-v1.5` | *(none)* | `Represent this sentence for searching relevant passages: ` | + + Always check the model card: task prefixes are model-specific and can change between versions. + +## `EmbeddingRole`: the two roles a text can play + +`omop_llm.EmbeddingRole` is a `StrEnum` with two members: `DOCUMENT` (text being indexed) and `QUERY` (text used to search). It exists because the prefix convention is a property of the *model*, not of whatever domain is calling it — every consumer of a given asymmetric model shares the same two roles and the same prefix strings, so the concept lives here rather than being reinvented per consumer package. + +```python +from omop_llm import EmbeddingRole, ModelBackend + +# Indexing +doc_vectors = model_backend.embed_texts(["Hypertension", "Diabetes"], role=EmbeddingRole.DOCUMENT) + +# Searching +query_vectors = model_backend.embed_texts(["high blood pressure"], role=EmbeddingRole.QUERY) +``` + +Passing `role=` is optional. Omit it (or leave it `None`) for a symmetric model, or when you don't want prefix application for some other reason — `embed_texts`/`async_embed_texts` pass the text through unchanged when `role` is `None`. + +## Where the prefix values come from + +Prefix strings are configured once per model, not per caller. Two ways to supply them: + +1. **Direct `configuration` dict**, when calling `build_model_backend` with plain keyword arguments: + + ```python + from omop_llm import build_model_backend + + backend = build_model_backend( + "ollama", + "nomic-embed-text:v1.5", + base_url="http://localhost:11434", + configuration={ + "document_prefix": "search_document: ", + "query_prefix": "search_query: ", + }, + ) + ``` + +2. **`oa-configurator`'s `[models.*]` entry**, when calling `build_model_backend_from_resolved(resolved)`. `ModelConfig` has typed `document_prefix`/`query_prefix` fields (alongside `embedding_dim`) specifically for this — see `oa-configurator`'s [config reference](https://AustralianCancerDataNetwork.github.io/OA_Configurator/config-reference/#modelsname) for the TOML shape and `omop-config models add`/`list` for managing it via CLI. `build_model_backend_from_resolved` folds these typed fields into the `configuration` dict before constructing the backend, so the mechanism is identical either way — only the source of the values differs. + +Either way, the resolved `configuration` dict is what `apply_embedding_prefix`/`embed_texts(role=...)` actually reads at call time; `ModelBackend` itself doesn't care whether the values came from a literal dict or a resolved config entry. + +## Construction-time sanity check + +`build_model_backend`/`build_model_backend_from_resolved` call `warn_if_prefixes_look_wrong` once, at construction, whenever the resolved backend is embedding-capable. It logs (never raises) two independent things: + +- **Missing**: no `document_prefix`/`query_prefix` configured at all. Fine for a symmetric model; worth double-checking for an asymmetric one. +- **Unrecognized**: a prefix is configured, but doesn't match anything in `omop_llm.embeddings.KNOWN_EMBEDDING_PREFIXES` — a small `frozenset` of common conventions (`"search_document: "`, `"passage: "`, etc.), deliberately *not* an exhaustive per-model lookup table (the space of models is unbounded and grows constantly). An unrecognized prefix isn't necessarily wrong — it's a nudge to double-check against the model card, not a rejection. + +Neither warning blocks construction. There is no validation that a prefix is *correct* for a given model — that isn't knowable from the string alone. diff --git a/mkdocs.yml b/mkdocs.yml index be8a61a..b1aaec5 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -30,6 +30,7 @@ theme: nav: - Home: index.md - Installation: usage/installation.md + - Asymmetric Embeddings: usage/asymmetric-embeddings.md - Providers: providers.md - "API Reference": reference.md From 089848b4ce254b379525c8ea055d5f0385ce762f Mon Sep 17 00:00:00 2001 From: Nico Loesch Date: Wed, 29 Jul 2026 03:50:25 +0000 Subject: [PATCH 12/20] Ignore MacOS files --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index d50942f..3d5baea 100644 --- a/.gitignore +++ b/.gitignore @@ -205,3 +205,6 @@ cython_debug/ marimo/_static/ marimo/_lsp/ __marimo__/ + +# MacOS +.DS_Store From a51dbbf2536ebf8cb2a5821b8ec19a1138305d8d Mon Sep 17 00:00:00 2001 From: Nico Loesch Date: Tue, 4 Aug 2026 04:23:48 +0000 Subject: [PATCH 13/20] Marker for oa-configurator --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 131e08a..2bfec49 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ requires-python = ">=3.12" dependencies = [ "any-llm-sdk[ollama,gemini]>=1.22.0", "httpx", - "oa-configurator>=0.2.0,<1.0.0", + "oa-configurator>=0.2.0,<1.0.0", # TODO: raise upper bound once oa-configurator 1.0 is published (source already compatible) "ollama", # already imported by any-llm-sdk "pydantic", ] From c8bf8d5c9fddbbcf7e6e9a4636ea5231a265320a Mon Sep 17 00:00:00 2001 From: Nico Loesch Date: Thu, 6 Aug 2026 03:08:04 +0000 Subject: [PATCH 14/20] Bump versions --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2bfec49..d6b2381 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ requires-python = ">=3.12" dependencies = [ "any-llm-sdk[ollama,gemini]>=1.22.0", "httpx", - "oa-configurator>=0.2.0,<1.0.0", # TODO: raise upper bound once oa-configurator 1.0 is published (source already compatible) + "oa-configurator>=0.2.0,<1.0.0", # TODO: raise to >=0.2.0,<2.0.0 "ollama", # already imported by any-llm-sdk "pydantic", ] From 0eb74124fdb8a170fdfde1cd1d3654c2c481e631 Mon Sep 17 00:00:00 2001 From: Nico Loesch Date: Thu, 6 Aug 2026 05:12:54 +0000 Subject: [PATCH 15/20] Small docs adaptation --- docs/providers.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/providers.md b/docs/providers.md index 5e6dbe0..1c3315b 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -21,8 +21,8 @@ Resolution order for both, always: **explicit argument → the provider's own en | `anthropic` | any-llm sets none; falls through to the `anthropic` SDK's own default (the real Anthropic API) | Yes | | `gemini` | any-llm sets none; falls through to the `google-genai` SDK's own default (the real Gemini API) | Yes | -! note "The pattern" -Local providers either have no sensible universal default or a conventional local-dev default. You'll almost always want to set `base_url` explicitly once you're pointed at anything other than a single local instance on the default port. Cloud providers need no `base_url` at all for the normal case: leaving it unset resolves to the real vendor API, exactly as if you were calling that vendor's own SDK directly with no `base_url` override. You only set `base_url` for a cloud provider to point at something *other* than the vendor's real endpoint (an Azure OpenAI-style proxy, for instance). +!!! note "The pattern" + Local providers either have no sensible universal default or a conventional local-dev default. You'll almost always want to set `base_url` explicitly once you're pointed at anything other than a single local instance on the default port. Cloud providers need no `base_url` at all for the normal case: leaving it unset resolves to the real vendor API, exactly as if you were calling that vendor's own SDK directly with no `base_url` override. You only set `base_url` for a cloud provider to point at something *other* than the vendor's real endpoint (an Azure OpenAI-style proxy, for instance). | Provider | Env var for `base_url` | Env var for `api_key` | |---|---|---| From 5bcbc5f35199a85e1bb559c0834559ab1c69c5f3 Mon Sep 17 00:00:00 2001 From: Nico Loesch Date: Mon, 10 Aug 2026 04:44:57 +0000 Subject: [PATCH 16/20] Have singular capabilities that are shared between Model and Provider and configured independently and then accurately resolved --- README.md | 7 +- docs/index.md | 4 +- docs/providers.md | 2 +- docs/usage/asymmetric-embeddings.md | 17 ++- src/omop_llm/__init__.py | 8 +- src/omop_llm/backend.py | 147 ++++++++++++++------- src/omop_llm/capabilities.py | 47 ++++--- src/omop_llm/embeddings.py | 26 ++-- src/omop_llm/providers/__init__.py | 4 +- src/omop_llm/providers/registry.py | 13 +- tests/test_backend.py | 151 +++++++++++++++++++--- tests/test_embeddings.py | 27 ++-- tests/test_oa_configurator_integration.py | 61 +++++++-- tests/test_registry.py | 8 +- 14 files changed, 368 insertions(+), 154 deletions(-) diff --git a/README.md b/README.md index e1ec4c6..07dbdfe 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,12 @@ Shared chat/embedding backend contract for the OMOP stack, built on [any-llm](https://github.com/mozilla-ai/any-llm). One typed `ModelBackend` interface (sync and async methods, both real), a closed set of supported providers (local: `ollama`, `llamacpp`, `vllm`; cloud: `openai`, `anthropic`, `gemini`), and explicit capability declarations instead of provider-name guessing. Extended documentation can be found [here](https://AustralianCancerDataNetwork.github.io/omop-llm). ```python -from omop_llm import build_model_backend +from omop_llm import build_model_backend, Capabilities -backend = build_model_backend(provider="ollama", model="llama3.2:8b", base_url="http://localhost:11434") +backend = build_model_backend( + provider="ollama", model="llama3:8b", base_url="http://localhost:11434", + model_capabilities=Capabilities(), # plain chat only -- nothing else needed here +) # async response = await backend.async_complete([{"role": "user", "content": "Hello"}]) diff --git a/docs/index.md b/docs/index.md index 85eb8b3..d2f55e0 100644 --- a/docs/index.md +++ b/docs/index.md @@ -5,9 +5,9 @@ ## What it provides - **`ModelBackend`** ([reference](reference.md#omop_llm.backend)): the one calling contract every consumer uses, built by `build_model_backend(provider, model, ...)`. It wraps a single [any-llm](https://github.com/mozilla-ai/any-llm) provider instance and exposes chat completion, embeddings, and structured extraction as methods on one object, each with a synchronous form and an `async_`-prefixed asynchronous form (`complete`/`async_complete`, `embed_texts`/`async_embed_texts`, and so on), so both async and fully synchronous consumers get a real, non-hand-rolled path. -- **Asymmetric embedding prefixes** ([`EmbeddingRole`](reference.md#omop_llm.embeddings), [full guide](usage/asymmetric-embeddings.md)): asymmetric embedding models (nomic-embed-text, the E5 family, BGE, and others) need a different prefix prepended depending on whether the text is being indexed or used to search. `embed_texts(texts, role=EmbeddingRole.DOCUMENT)`/`role=EmbeddingRole.QUERY` applies it automatically, sourced from `configuration["document_prefix"]`/`["query_prefix"]` (a plain dict, or `oa-configurator`'s typed `ModelConfig.document_prefix`/`query_prefix` fields via `build_model_backend_from_resolved`) so the values live in one place, not duplicated per consuming package. +- **Asymmetric embedding prefixes** ([`EmbeddingRole`](reference.md#omop_llm.embeddings), [full guide](usage/asymmetric-embeddings.md)): asymmetric embedding models (nomic-embed-text, the E5 family, BGE, and others) need a different prefix prepended depending on whether the text is being indexed or used to search. `embed_texts(texts, role=EmbeddingRole.DOCUMENT)`/`role=EmbeddingRole.QUERY` applies it automatically, sourced from `build_model_backend`'s own `document_prefix`/`query_prefix` arguments (or `oa-configurator`'s typed `ModelConfig.document_prefix`/`query_prefix` fields via `build_model_backend_from_resolved`) so the values live in one place, not duplicated per consuming package. - **A closed provider registry** ([reference](reference.md#omop_llm.providers)): `omop-llm` supports selected providers, not any-llm's full set shown in the [Providers overview](providers.md). Every supported provider is a real subclass of any-llm's own provider class, which is both the allow-list (nothing outside this set is reachable through `omop-llm`) and the seam for provider-specific behavior, such as Ollama's canonical model naming and embedding-dimension fast path (see `omop_llm.providers.supported`). -- **An explicit capability model** ([`ModelCapabilities`](reference.md#omop_llm.capabilities)): `streaming`/`embeddings`/`extended_thinking` come straight from any-llm's own provider metadata. `tool_use`/`structured_output` do not exist as any-llm capability flags at all (confirmed by reading its `ProviderMetadata` type directly), so `omop-llm` declares those two itself, meaning a caller requiring a capability the resolved backend does not have fails at construction time, not mid-run. +- **An explicit, per-model capability model** ([`Capabilities`](reference.md#omop_llm.capabilities)): a resolved backend's effective capabilities are the provider's own ceiling (`provider_capabilities_for`, sourced from any-llm's metadata plus `omop-llm`'s own `tool_use`/`structured_output` declarations, since any-llm tracks neither) AND'd with what the *specific model* is declared to support (e.g. `oa-configurator`'s `ModelConfig.embeddings`/`tool_use`/`structured_output`/`extended_thinking`, since neither any-llm nor `omop-llm` can introspect this per model) — a capability is only effective if both agree. `streaming` is always `False`, since `ModelBackend` doesn't implement it regardless of what the provider/model support. A caller requiring a capability the resolved backend doesn't have fails at construction time, not mid-run. - **Structured single-object extraction** (`ModelBackend.extract`/`async_extract`): pulling one validated Pydantic object out of one LLM call. This is *not* the same problem as multi-turn agentic tool use (a model calling several real tools across several turns), which stays on `ModelBackend.complete(messages, tools=...)` directly. See [`omop_llm.structured`](reference.md#omop_llm.structured)'s own docstring for why the primary strategy is any-llm's native `response_format=` translation, and why `instructor`-based extraction (the optional fallback) is only offered for `openai`/`llamacpp`/`vllm`, not `ollama`/`anthropic`/`gemini`. `omop-llm` depends on `oa-configurator` for config resolution. Two entry points: diff --git a/docs/providers.md b/docs/providers.md index 1c3315b..1e24a2a 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -44,7 +44,7 @@ Resolution order for both, always: **explicit argument → the provider's own en | `anthropic` | ✅ | ❌ | ✅ | ✅ | ✅ | No embeddings API; `ModelBackend.embed_texts`/`async_embed_texts` refuse this provider. | | `gemini` | ✅ | ✅ | ✅ | ✅ | ✅ | | -`streaming`/`embeddings`/`extended_thinking` come from any-llm's own `get_provider_metadata()`, verified directly against the installed package for these six providers. `tool_use`/`structured_output` are declared by `omop-llm` itself, since any-llm tracks neither (see [`ModelCapabilities`](reference.md#omop_llm.capabilities)). +`streaming`/`embeddings`/`extended_thinking` come from any-llm's own `get_provider_metadata()`, verified directly against the installed package for these six providers. `tool_use`/`structured_output` are declared by `omop-llm` itself, since any-llm tracks neither (see [`Capabilities`](reference.md#omop_llm.capabilities)). ## Adding a provider diff --git a/docs/usage/asymmetric-embeddings.md b/docs/usage/asymmetric-embeddings.md index 65f5a22..c87783e 100644 --- a/docs/usage/asymmetric-embeddings.md +++ b/docs/usage/asymmetric-embeddings.md @@ -33,27 +33,26 @@ Passing `role=` is optional. Omit it (or leave it `None`) for a symmetric model, ## Where the prefix values come from -Prefix strings are configured once per model, not per caller. Two ways to supply them: +Prefix strings are configured once per model via dedicated `document_prefix`/`query_prefix` parameters. Two ways to supply them: -1. **Direct `configuration` dict**, when calling `build_model_backend` with plain keyword arguments: +1. **Direct keyword arguments**, when calling `build_model_backend`: ```python - from omop_llm import build_model_backend + from omop_llm import build_model_backend, Capabilities backend = build_model_backend( "ollama", "nomic-embed-text:v1.5", base_url="http://localhost:11434", - configuration={ - "document_prefix": "search_document: ", - "query_prefix": "search_query: ", - }, + model_capabilities=Capabilities(embeddings=True), + document_prefix="search_document: ", + query_prefix="search_query: ", ) ``` -2. **`oa-configurator`'s `[models.*]` entry**, when calling `build_model_backend_from_resolved(resolved)`. `ModelConfig` has typed `document_prefix`/`query_prefix` fields (alongside `embedding_dim`) specifically for this — see `oa-configurator`'s [config reference](https://AustralianCancerDataNetwork.github.io/OA_Configurator/config-reference/#modelsname) for the TOML shape and `omop-config models add`/`list` for managing it via CLI. `build_model_backend_from_resolved` folds these typed fields into the `configuration` dict before constructing the backend, so the mechanism is identical either way — only the source of the values differs. +2. **`oa-configurator`'s `[models.*]` entry**, when calling `build_model_backend_from_resolved(resolved)`. `ModelConfig` has typed `document_prefix`/`query_prefix` fields (alongside `embedding_dim`) specifically for this — see `oa-configurator`'s [config reference](https://AustralianCancerDataNetwork.github.io/OA_Configurator/config-reference/#modelsname) for the TOML shape and `omop-config models add`/`list` for managing it via CLI. `build_model_backend_from_resolved` forwards these typed fields straight through as the same keyword arguments. -Either way, the resolved `configuration` dict is what `apply_embedding_prefix`/`embed_texts(role=...)` actually reads at call time; `ModelBackend` itself doesn't care whether the values came from a literal dict or a resolved config entry. +Either way, the resolved backend stores them on `ModelBackend.document_prefix`/`.query_prefix`, which is what `apply_embedding_prefix`/`embed_texts(role=...)` actually reads at call time. ## Construction-time sanity check diff --git a/src/omop_llm/__init__.py b/src/omop_llm/__init__.py index 4f973c1..1fe1417 100644 --- a/src/omop_llm/__init__.py +++ b/src/omop_llm/__init__.py @@ -4,7 +4,7 @@ build_model_backend_from_resolved, ) -from omop_llm.capabilities import ModelCapabilities +from omop_llm.capabilities import Capabilities from omop_llm.embeddings import ( EmbeddingRole, KNOWN_EMBEDDING_PREFIXES @@ -16,7 +16,7 @@ ) from omop_llm.providers import ( canonical_model_name, - capabilities_for, + provider_capabilities_for, supported_providers ) @@ -24,13 +24,13 @@ "EmbeddingRole", "KNOWN_EMBEDDING_PREFIXES", "ModelBackend", - "ModelCapabilities", + "Capabilities", "OmopLlmError", "UnsupportedCapabilityError", "UnsupportedProviderError", "build_model_backend", "build_model_backend_from_resolved", "canonical_model_name", - "capabilities_for", + "provider_capabilities_for", "supported_providers", ] diff --git a/src/omop_llm/backend.py b/src/omop_llm/backend.py index d9e0e11..4fdf5bd 100644 --- a/src/omop_llm/backend.py +++ b/src/omop_llm/backend.py @@ -3,7 +3,7 @@ A thin wrapper around a single, already-constructed any-llm provider instance (an entry of :data:`omop_llm.providers.registry.PROVIDER_REGISTRY`). Chat completion, embeddings, and structured extraction are all methods on -one object, gated by :class:`~omop_llm.capabilities.ModelCapabilities`, +one object, gated by :class:`~omop_llm.capabilities.Capabilities`, rather than split across separate classes per modality. Every method has a synchronous form and an ``async_``-prefixed @@ -18,6 +18,7 @@ from __future__ import annotations +import dataclasses from collections.abc import Iterator from dataclasses import dataclass, field from typing import Any @@ -27,13 +28,13 @@ from oa_configurator import ResolvedModel from pydantic import BaseModel, ValidationError -from omop_llm.capabilities import ModelCapabilities +from omop_llm.capabilities import Capabilities from omop_llm.embeddings import EmbeddingRole, apply_embedding_prefix, warn_if_prefixes_look_wrong from omop_llm.errors import NoParsedOutputError, UnsupportedCapabilityError from omop_llm.providers.base import ProviderMixin from omop_llm.providers.registry import ( canonical_model_name, - capabilities_for, + provider_capabilities_for, provider_class_for, ) @@ -74,11 +75,18 @@ class ModelBackend: model : str The canonical model name or identifier passed to the underlying provider. - capabilities : ModelCapabilities + capabilities : Capabilities What this resolved backend can actually do. configuration : dict, optional Default keyword arguments merged into every call, overridden by - any argument the caller passes explicitly. + any argument the caller passes explicitly. Pure provider passthrough + only, and doesn't include any of the other fields below. + embedding_dim : int, optional + Configured embedding dimension override, read by :meth:`dimensions`. + document_prefix : str, optional + Prefix prepended to document/passage text before embedding. + query_prefix : str, optional + Prefix prepended to query text before embedding. _api_base : str, optional The base URL this backend was constructed with, if any. Threaded through to provider-specific fast paths such as @@ -87,8 +95,11 @@ class ModelBackend: _client: AnyLLM model: str - capabilities: ModelCapabilities + capabilities: Capabilities configuration: dict[str, Any] = field(default_factory=dict) + embedding_dim: int | None = None + document_prefix: str | None = None + query_prefix: str | None = None _api_base: str | None = None @property @@ -228,10 +239,10 @@ def embed_texts( role : EmbeddingRole, optional Whether ``texts`` are being indexed (``DOCUMENT``) or used to search (``QUERY``). When given, prepends whichever of - ``configuration["document_prefix"]``/``["query_prefix"]`` - matches, needed for asymmetric embedding models (e.g. - nomic-embed-text, E5, BGE). Omit for symmetric models, or when - texts are already prefixed. + ``self.document_prefix``/``self.query_prefix`` matches, needed + for asymmetric embedding models (e.g. nomic-embed-text, E5, + BGE). Omit for symmetric models, or when texts are already + prefixed. batch_size : int, optional If given, ``texts`` is chunked into sub-batches of at most this size, each sent as its own call, rather than one call with the @@ -253,7 +264,9 @@ def embed_texts( """ self._require_embeddings() if role is not None: - texts = apply_embedding_prefix(texts, role, self.configuration) + texts = apply_embedding_prefix( + texts, role, document_prefix=self.document_prefix, query_prefix=self.query_prefix + ) if batch_size is None: response = self._client._embedding(model=self.model, inputs=texts, **self.configuration) return [item.embedding for item in response.data] @@ -279,10 +292,10 @@ async def async_embed_texts( role : EmbeddingRole, optional Whether ``texts`` are being indexed (``DOCUMENT``) or used to search (``QUERY``). When given, prepends whichever of - ``configuration["document_prefix"]``/``["query_prefix"]`` - matches, needed for asymmetric embedding models (e.g. - nomic-embed-text, E5, BGE). Omit for symmetric models, or when - texts are already prefixed. + ``self.document_prefix``/``self.query_prefix`` matches, needed + for asymmetric embedding models (e.g. nomic-embed-text, E5, + BGE). Omit for symmetric models, or when texts are already + prefixed. batch_size : int, optional If given, ``texts`` is chunked into sub-batches of at most this size, each sent as its own call, rather than one call with the @@ -304,7 +317,9 @@ async def async_embed_texts( """ self._require_embeddings() if role is not None: - texts = apply_embedding_prefix(texts, role, self.configuration) + texts = apply_embedding_prefix( + texts, role, document_prefix=self.document_prefix, query_prefix=self.query_prefix + ) if batch_size is None: response = await self._client.aembedding(model=self.model, inputs=texts, **self.configuration) return [item.embedding for item in response.data] @@ -322,8 +337,8 @@ def _require_embeddings(self) -> None: def dimensions(self) -> int: """Discover this model's embedding dimensionality synchronously. - Three tiers: - 1. a configured override (``configuration["embedding_dim"]``), + Three tiers: + 1. a configured override (``self.embedding_dim``), 2. a provider-specific fast path (e.g. Ollama's ``POST /api/show``), and 3. a live probe (embed one short string and measure the vector). @@ -331,10 +346,15 @@ def dimensions(self) -> int: ------- int The embedding vector length. + + Raises + ------ + UnsupportedCapabilityError + If ``self.capabilities.embeddings`` is ``False``. """ - configured = self.configuration.get("embedding_dim") - if configured is not None: - return int(configured) + self._require_embeddings() + if self.embedding_dim is not None: + return self.embedding_dim assert isinstance(self._client, ProviderMixin) hint = self._client.embedding_dimension_hint(self.model, api_base=self._api_base) if hint is not None: @@ -344,8 +364,8 @@ def dimensions(self) -> int: async def async_dimensions(self) -> int: """Discover this model's embedding dimensionality. - Three tiers: - 1. a configured override (``configuration["embedding_dim"]``), + Three tiers: + 1. a configured override (``self.embedding_dim``), 2. a provider-specific fast path (e.g. Ollama's ``POST /api/show``), and 3. a live probe (embed one short string and measure the vector). @@ -353,10 +373,15 @@ async def async_dimensions(self) -> int: ------- int The embedding vector length. + + Raises + ------ + UnsupportedCapabilityError + If ``self.capabilities.embeddings`` is ``False``. """ - configured = self.configuration.get("embedding_dim") - if configured is not None: - return int(configured) + self._require_embeddings() + if self.embedding_dim is not None: + return self.embedding_dim assert isinstance(self._client, ProviderMixin) hint = await self._client.async_embedding_dimension_hint(self.model, api_base=self._api_base) if hint is not None: @@ -597,9 +622,13 @@ def build_model_backend( provider: str, model: str, *, + model_capabilities: Capabilities, base_url: str | None = None, api_key: str | None = None, configuration: dict[str, Any] | None = None, + embedding_dim: int | None = None, + document_prefix: str | None = None, + query_prefix: str | None = None, ) -> ModelBackend: """Resolve a provider and model into a ready-to-call backend. @@ -620,13 +649,26 @@ def build_model_backend( A key in :data:`omop_llm.providers.registry.PROVIDER_REGISTRY`. model : str Raw model name or identifier; canonicalized before use. + model_capabilities : Capabilities + What this specific model is declared to support. Required, not + optional: neither any-llm nor omop-llm can introspect this per + model, so the caller has to say. Pass ``Capabilities()`` to + declare none of them, explicitly rather than by omission. base_url : str, optional The base URL for this specific deployment of the provider. api_key : str, optional The API key for this specific deployment, if one is required. configuration : dict, optional Default keyword arguments merged into every call this backend - makes (e.g. ``max_tokens``, ``temperature``, ``embedding_dim``). + makes (e.g. ``max_tokens``, ``temperature``). Pure provider + passthrough -- use ``embedding_dim``/``document_prefix``/``query_prefix`` + below for those, never this dict. + embedding_dim : int, optional + Configured embedding dimension override. + document_prefix : str, optional + Prefix prepended to document/passage text before embedding. + query_prefix : str, optional + Prefix prepended to query text before embedding. Returns ------- @@ -638,20 +680,33 @@ def build_model_backend( ------ ValueError If ``model`` cannot be made canonical for the resolved provider - (e.g. an Ollama name with no explicit tag). + (e.g. an Ollama name with no explicit tag), or if ``embedding_dim`` + is given but the effective capabilities don't include embeddings. """ provider_class = provider_class_for(provider) - capabilities = capabilities_for(provider) + provider_caps = provider_capabilities_for(provider) + effective_caps = provider_caps & model_capabilities + effective_caps = dataclasses.replace(effective_caps, streaming=False) # ModelBackend doesn't implement streaming as of now (see complete()/async_complete() docstring) canonical_model = canonical_model_name(provider, model) + if embedding_dim is not None and not effective_caps.embeddings: + raise ValueError( + f"embedding_dim={embedding_dim!r} given for model {canonical_model!r}, " + "but its effective capabilities don't include embeddings." + ) resolved_configuration = dict(configuration) if configuration else {} - if capabilities.embeddings: - warn_if_prefixes_look_wrong(model=canonical_model, configuration=resolved_configuration) + if effective_caps.embeddings: + warn_if_prefixes_look_wrong( + model=canonical_model, document_prefix=document_prefix, query_prefix=query_prefix + ) client = provider_class(api_key=api_key, api_base=base_url) return ModelBackend( _client=client, model=canonical_model, - capabilities=capabilities, + capabilities=effective_caps, configuration=resolved_configuration, + embedding_dim=embedding_dim, + document_prefix=document_prefix, + query_prefix=query_prefix, _api_base=base_url, ) @@ -675,11 +730,11 @@ def build_model_backend_from_resolved(resolved: ResolvedModel) -> ModelBackend: resolved = Resolver(stack).resolve_model(config.embedding_model) backend = build_model_backend_from_resolved(resolved) - ``resolved.embedding_dim``/``document_prefix``/``query_prefix`` (typed - ``ModelConfig`` fields) are folded into the ``configuration`` dict under - their matching keys before construction, taking precedence over the same - keys if also present in ``resolved.configuration`` (the free-form - fallback for knobs with no dedicated field). + A thin translator: ``resolved.embedding_dim``/``document_prefix``/``query_prefix`` + are forwarded as-is, and ``resolved.embeddings``/``tool_use``/``structured_output``/``extended_thinking`` + are collected into one :class:`~omop_llm.capabilities.Capabilities`. + ``resolved.configuration`` is passed through untouched -- nothing + gets folded into it. Parameters ---------- @@ -698,17 +753,19 @@ def build_model_backend_from_resolved(resolved: ResolvedModel) -> ModelBackend: If ``resolved.model`` cannot be made canonical for the resolved provider (e.g. an Ollama name with no explicit tag). """ - configuration = dict(resolved.configuration) - if resolved.embedding_dim is not None: - configuration["embedding_dim"] = resolved.embedding_dim - if resolved.document_prefix is not None: - configuration["document_prefix"] = resolved.document_prefix - if resolved.query_prefix is not None: - configuration["query_prefix"] = resolved.query_prefix return build_model_backend( provider=resolved.provider.provider, model=resolved.model, base_url=resolved.provider.base_url, api_key=resolved.provider.api_key, - configuration=configuration, + configuration=dict(resolved.configuration), + embedding_dim=resolved.embedding_dim, + document_prefix=resolved.document_prefix, + query_prefix=resolved.query_prefix, + model_capabilities=Capabilities( + embeddings=resolved.embeddings, + tool_use=resolved.tool_use, + structured_output=resolved.structured_output, + extended_thinking=resolved.extended_thinking, + ), ) diff --git a/src/omop_llm/capabilities.py b/src/omop_llm/capabilities.py index 8db2c1a..b2ea03c 100644 --- a/src/omop_llm/capabilities.py +++ b/src/omop_llm/capabilities.py @@ -1,38 +1,45 @@ -"""What a resolved backend can actually do.""" +"""What something -- a provider, a model, or a resolved backend -- can do.""" from __future__ import annotations +import dataclasses from dataclasses import dataclass @dataclass(frozen=True, slots=True) -class ModelCapabilities: - """Capability declaration for one provider. +class Capabilities: + """A capability declaration: what something can do. + Used for providers, models, and resolved backends (the combination of the two). - ``streaming``, ``embeddings``, and ``extended_thinking`` are read - directly from any-llm's own ``ProviderMetadata``. - - ``tool_use`` and ``structured_output`` have no equivalent in any-llm. - These two are declared by omop_llm itself in ``providers.registry`` and must not - be inferred from any-llm's own introspection. + Opt-in: every field defaults to ``False``. Neither any-llm nor + omop_llm can introspect a model's real capabilities, so nothing is + assumed. Parameters ---------- streaming : bool - Whether the provider supports streaming completions. + Whether streaming completions are supported. embeddings : bool - Whether the provider supports the embeddings endpoint. + Whether the embeddings endpoint is supported. extended_thinking : bool - Whether the provider supports reasoning/extended-thinking output. + Whether reasoning/extended-thinking output is supported. tool_use : bool - Whether the provider supports tool/function calling. + Whether tool/function calling is supported. structured_output : bool - Whether the provider supports structured (schema-constrained) - output. + Whether structured (schema-constrained) output is supported. """ - streaming: bool - embeddings: bool - extended_thinking: bool - tool_use: bool - structured_output: bool + streaming: bool = False + embeddings: bool = False + extended_thinking: bool = False + tool_use: bool = False + structured_output: bool = False + + def __and__(self, other: Capabilities) -> Capabilities: + """Element-wise AND: a capability is only available if both sides have it.""" + return Capabilities( + **{ + f.name: getattr(self, f.name) and getattr(other, f.name) + for f in dataclasses.fields(self) + } + ) diff --git a/src/omop_llm/embeddings.py b/src/omop_llm/embeddings.py index e4f97af..45baee7 100644 --- a/src/omop_llm/embeddings.py +++ b/src/omop_llm/embeddings.py @@ -11,7 +11,6 @@ import logging from enum import StrEnum -from typing import Any logger = logging.getLogger(__name__) @@ -23,11 +22,6 @@ class EmbeddingRole(StrEnum): QUERY = "query" -CONFIGURATION_KEY_BY_ROLE: dict[EmbeddingRole, str] = { - EmbeddingRole.DOCUMENT: "document_prefix", - EmbeddingRole.QUERY: "query_prefix", -} - # Prefix conventions used by common asymmetric embedding models. Not # exhaustive, and not meant to be: used only to flag a configured prefix # that doesn't match anything recognized, never to reject or "correct" @@ -45,31 +39,33 @@ class EmbeddingRole(StrEnum): def apply_embedding_prefix( - texts: list[str], role: EmbeddingRole, configuration: dict[str, Any] + texts: list[str], role: EmbeddingRole, *, document_prefix: str | None, query_prefix: str | None ) -> list[str]: - """Prepend *role*'s configured prefix to each of *texts*, if one is set.""" - prefix = configuration.get(CONFIGURATION_KEY_BY_ROLE[role], "") + """Prepend *role*'s prefix to each of *texts*, if one is given.""" + prefix = document_prefix if role is EmbeddingRole.DOCUMENT else query_prefix if not prefix: return texts return [f"{prefix}{text}" for text in texts] -def warn_if_prefixes_look_wrong(*, model: str, configuration: dict[str, Any]) -> None: - """Log a warning for a missing or unrecognized configured prefix. +def warn_if_prefixes_look_wrong(*, model: str, document_prefix: str | None, query_prefix: str | None) -> None: + """Log a warning for a missing or unrecognized prefix. Called once, at :func:`~omop_llm.backend.build_model_backend` time, for any backend that declares embeddings support. Never raises: a prefix outside :data:`KNOWN_EMBEDDING_PREFIXES` is not necessarily wrong, this is a heads-up, not validation. """ - for role, key in CONFIGURATION_KEY_BY_ROLE.items(): - prefix = configuration.get(key) + for role, prefix in ( + (EmbeddingRole.DOCUMENT, document_prefix), + (EmbeddingRole.QUERY, query_prefix), + ): if not prefix: logger.warning( - "%s: no %s configured for model %r. Fine for symmetric models; " + "%s: no %s_prefix configured for model %r. Fine for symmetric models; " "asymmetric models (e.g. nomic-embed-text, E5, BGE) need one " "to retrieve correctly.", - role.value.capitalize(), key, model, + role.value.capitalize(), role.value, model, ) elif prefix not in KNOWN_EMBEDDING_PREFIXES: logger.warning( diff --git a/src/omop_llm/providers/__init__.py b/src/omop_llm/providers/__init__.py index e8ef5c7..ec9673b 100644 --- a/src/omop_llm/providers/__init__.py +++ b/src/omop_llm/providers/__init__.py @@ -1,7 +1,7 @@ from omop_llm.providers.registry import ( PROVIDER_REGISTRY, canonical_model_name, - capabilities_for, + provider_capabilities_for, provider_class_for, supported_providers, ) @@ -10,7 +10,7 @@ __all__ = [ "PROVIDER_REGISTRY", "canonical_model_name", - "capabilities_for", + "provider_capabilities_for", "provider_class_for", "supported_providers", ] diff --git a/src/omop_llm/providers/registry.py b/src/omop_llm/providers/registry.py index db36778..ee15dfe 100644 --- a/src/omop_llm/providers/registry.py +++ b/src/omop_llm/providers/registry.py @@ -26,7 +26,7 @@ from any_llm.any_llm import AnyLLM -from omop_llm.capabilities import ModelCapabilities +from omop_llm.capabilities import Capabilities from omop_llm.errors import UnsupportedProviderError from omop_llm.providers import supported as _supported # noqa: F401 (required for PROVIDER_REGISTRY to be populated) from omop_llm.providers.base import ProviderMixin @@ -76,8 +76,11 @@ def provider_class_for(provider_key: str) -> type[AnyLLM]: ) from None -def capabilities_for(provider_key: str) -> ModelCapabilities: - """Build the capability declaration for one registered provider. +def provider_capabilities_for(provider_key: str) -> Capabilities: + """Build the provider-wide capability ceiling for one registered provider. + + This is the *provider's* transport-level ceiling, not a specific + model's effective capabilities. ``streaming``, ``embeddings``, and ``extended_thinking`` come straight from any-llm's own ``get_provider_metadata()``. ``tool_use`` and @@ -91,7 +94,7 @@ def capabilities_for(provider_key: str) -> ModelCapabilities: Returns ------- - ModelCapabilities + Capabilities The capability declaration for this provider. Raises @@ -102,7 +105,7 @@ def capabilities_for(provider_key: str) -> ModelCapabilities: provider_class = provider_class_for(provider_key) meta = provider_class.get_provider_metadata() assert issubclass(provider_class, ProviderMixin) - return ModelCapabilities( + return Capabilities( streaming=meta.streaming, embeddings=meta.embedding, extended_thinking=meta.reasoning, diff --git a/tests/test_backend.py b/tests/test_backend.py index 054e03c..c56dfd7 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -12,7 +12,7 @@ from pydantic import BaseModel from omop_llm.backend import ModelBackend, build_model_backend -from omop_llm.capabilities import ModelCapabilities +from omop_llm.capabilities import Capabilities from omop_llm.embeddings import EmbeddingRole from omop_llm.errors import NoParsedOutputError, UnsupportedCapabilityError from tests.conftest import ( @@ -24,7 +24,7 @@ FakeEmbeddingResponse, ) -_CAPS = ModelCapabilities( +_CAPS = Capabilities( streaming=True, embeddings=True, extended_thinking=True, tool_use=True, structured_output=True ) @@ -131,9 +131,7 @@ async def test_embed_texts_rejects_non_positive_batch_size(fake_client: FakeAnyL @pytest.mark.parametrize("sync", [True, False]) async def test_embed_texts_applies_role_prefix(fake_client: FakeAnyLLMClient, sync: bool) -> None: fake_client.embedding_response = FakeEmbeddingResponse(data=[FakeEmbeddingItem(embedding=[0.1])]) - backend = _backend( - fake_client, configuration={"document_prefix": "passage: ", "query_prefix": "query: "} - ) + backend = _backend(fake_client, document_prefix="passage: ", query_prefix="query: ") if sync: backend.embed_texts(["diabetes"], role=EmbeddingRole.DOCUMENT) @@ -146,9 +144,7 @@ async def test_embed_texts_applies_role_prefix(fake_client: FakeAnyLLMClient, sy @pytest.mark.parametrize("sync", [True, False]) async def test_embed_texts_query_role_uses_query_prefix(fake_client: FakeAnyLLMClient, sync: bool) -> None: fake_client.embedding_response = FakeEmbeddingResponse(data=[FakeEmbeddingItem(embedding=[0.1])]) - backend = _backend( - fake_client, configuration={"document_prefix": "passage: ", "query_prefix": "query: "} - ) + backend = _backend(fake_client, document_prefix="passage: ", query_prefix="query: ") if sync: backend.embed_texts(["hypertension"], role=EmbeddingRole.QUERY) @@ -161,7 +157,7 @@ async def test_embed_texts_query_role_uses_query_prefix(fake_client: FakeAnyLLMC @pytest.mark.parametrize("sync", [True, False]) async def test_embed_texts_no_role_leaves_text_untouched(fake_client: FakeAnyLLMClient, sync: bool) -> None: fake_client.embedding_response = FakeEmbeddingResponse(data=[FakeEmbeddingItem(embedding=[0.1])]) - backend = _backend(fake_client, configuration={"document_prefix": "passage: "}) + backend = _backend(fake_client, document_prefix="passage: ") if sync: backend.embed_texts(["diabetes"]) @@ -188,7 +184,7 @@ async def test_embed_texts_role_with_no_configured_prefix_is_a_noop( @pytest.mark.parametrize("sync", [True, False]) async def test_embed_texts_rejects_backend_without_embeddings(fake_client: FakeAnyLLMClient, sync: bool) -> None: - no_embed_caps = ModelCapabilities( + no_embed_caps = Capabilities( streaming=True, embeddings=False, extended_thinking=True, tool_use=True, structured_output=True ) backend = ModelBackend(_client=fake_client, model="m", capabilities=no_embed_caps) # ty: ignore[invalid-argument-type] @@ -199,9 +195,55 @@ async def test_embed_texts_rejects_backend_without_embeddings(fake_client: FakeA await backend.async_embed_texts(["a"]) +@pytest.mark.parametrize("sync", [True, False]) +async def test_dimensions_rejects_backend_without_embeddings(fake_client: FakeAnyLLMClient, sync: bool) -> None: + """dimensions()/async_dimensions() previously skipped this gate + entirely, so a configured embedding_dim override looked usable even on + a backend whose capabilities.embeddings is False.""" + no_embed_caps = Capabilities( + streaming=True, embeddings=False, extended_thinking=True, tool_use=True, structured_output=True + ) + backend = ModelBackend( + _client=fake_client, model="m", capabilities=no_embed_caps, embedding_dim=42 + ) # ty: ignore[invalid-argument-type] + with pytest.raises(UnsupportedCapabilityError): + if sync: + backend.dimensions() + else: + await backend.async_dimensions() + + +@pytest.mark.parametrize("sync", [True, False]) +async def test_embed_texts_never_forwards_bookkeeping_fields_to_the_provider( + fake_client: FakeAnyLLMClient, sync: bool +) -> None: + """embedding_dim/document_prefix/query_prefix are ModelBackend's own + bookkeeping (dimensions()/apply_embedding_prefix() read them directly), + never provider call kwargs. Previously folded into `configuration` and + forwarded unfiltered, breaking the real call with an unexpected kwarg.""" + fake_client.embedding_response = FakeEmbeddingResponse(data=[FakeEmbeddingItem(embedding=[0.1])]) + backend = _backend( + fake_client, + configuration={"encoding_format": "float"}, + embedding_dim=768, + document_prefix="passage: ", + query_prefix="query: ", + ) + + if sync: + backend.embed_texts(["diabetes"], role=EmbeddingRole.DOCUMENT) + else: + await backend.async_embed_texts(["diabetes"], role=EmbeddingRole.DOCUMENT) + [call] = fake_client.embedding_calls + assert call["encoding_format"] == "float" # genuine passthrough kwargs still forwarded + assert "embedding_dim" not in call + assert "document_prefix" not in call + assert "query_prefix" not in call + + @pytest.mark.parametrize("sync", [True, False]) async def test_dimensions_prefers_configured_override(fake_client: FakeAnyLLMClient, sync: bool) -> None: - backend = _backend(fake_client, configuration={"embedding_dim": 768}) + backend = _backend(fake_client, embedding_dim=768) result = backend.dimensions() if sync else await backend.async_dimensions() assert result == 768 assert fake_client.embedding_calls == [] # no live probe needed @@ -226,7 +268,7 @@ async def test_dimensions_falls_back_to_live_probe(fake_client: FakeAnyLLMClient @pytest.mark.parametrize("sync", [True, False]) async def test_extract_rejects_backend_without_structured_output(fake_client: FakeAnyLLMClient, sync: bool) -> None: - no_structured_caps = ModelCapabilities( + no_structured_caps = Capabilities( streaming=True, embeddings=True, extended_thinking=True, tool_use=True, structured_output=False ) backend = ModelBackend(_client=fake_client, model="m", capabilities=no_structured_caps) # ty: ignore[invalid-argument-type] @@ -353,14 +395,18 @@ async def fake_acompletion(**kwargs) -> FakeChatCompletion: def test_build_backend_constructs_offline_for_local_provider() -> None: backend = build_model_backend( - provider="llamacpp", model="local-chat", base_url="http://localhost:8080/v1" + provider="llamacpp", model="local-chat", base_url="http://localhost:8080/v1", + model_capabilities=Capabilities(tool_use=True), ) assert backend.model == "local-chat" assert backend.capabilities.tool_use is True def test_provider_property_reads_from_the_constructed_client_not_a_stored_field() -> None: - backend = build_model_backend(provider="llamacpp", model="local-chat", base_url="http://localhost:8080/v1") + backend = build_model_backend( + provider="llamacpp", model="local-chat", base_url="http://localhost:8080/v1", + model_capabilities=Capabilities(), + ) assert backend.provider == "llamacpp" @@ -370,31 +416,91 @@ def test_build_backend_passes_configuration_through() -> None: model="local-chat", base_url="http://localhost:8080/v1", configuration={"temperature": 0.0}, + model_capabilities=Capabilities(), ) assert backend.configuration == {"temperature": 0.0} def test_build_backend_canonicalizes_the_model_name() -> None: - backend = build_model_backend(provider="ollama", model="llama3:8b", base_url="http://localhost:11434") + backend = build_model_backend( + provider="ollama", model="llama3:8b", base_url="http://localhost:11434", + model_capabilities=Capabilities(), + ) assert backend.model == "llama3:8b" def test_build_backend_rejects_non_canonical_ollama_name() -> None: with pytest.raises(ValueError, match="explicit tag"): - build_model_backend(provider="ollama", model="llama3", base_url="http://localhost:11434") + build_model_backend( + provider="ollama", model="llama3", base_url="http://localhost:11434", + model_capabilities=Capabilities(), + ) + + +def test_build_backend_requires_model_capabilities() -> None: + """model_capabilities is required, not opt-in-by-default: omitting it + entirely must fail loudly rather than silently resolve to 'nothing + granted.'""" + with pytest.raises(TypeError, match="model_capabilities"): + build_model_backend(provider="ollama", model="llama3:8b", base_url="http://localhost:11434") # ty: ignore[missing-argument] def test_build_backend_constructs_offline_for_embedding_capable_provider() -> None: backend = build_model_backend( - provider="ollama", model="qwen3-embedding:0.6b", base_url="http://localhost:11434" + provider="ollama", model="qwen3-embedding:0.6b", base_url="http://localhost:11434", + model_capabilities=Capabilities(embeddings=True), ) assert backend.model == "qwen3-embedding:0.6b" assert backend.capabilities.embeddings is True +def test_build_backend_streaming_is_always_false() -> None: + """ModelBackend doesn't implement streaming, regardless of what the + provider supports (ollama genuinely does) or what the caller asks for.""" + backend = build_model_backend( + provider="ollama", model="llama3:8b", base_url="http://localhost:11434", + model_capabilities=Capabilities(streaming=True), + ) + assert backend.capabilities.streaming is False + + +def test_build_backend_empty_model_capabilities_grants_nothing() -> None: + """ollama's own provider metadata says embeddings=True; declaring + Capabilities() (nothing) for this specific model must not let that + leak through -- proving this is a real AND, not just the provider + ceiling passed through.""" + backend = build_model_backend( + provider="ollama", model="llama3:8b", base_url="http://localhost:11434", + model_capabilities=Capabilities(), + ) + assert backend.capabilities.embeddings is False + + +def test_build_backend_model_declaration_cannot_widen_provider_capability() -> None: + """anthropic's own provider metadata says embeddings=False; declaring + it on model_capabilities can't override that -- the provider is a + ceiling, not just one vote.""" + backend = build_model_backend( + provider="anthropic", model="claude-test", api_key="sk-test", + model_capabilities=Capabilities(embeddings=True), + ) + assert backend.capabilities.embeddings is False + + +def test_build_backend_rejects_embedding_dim_without_embeddings_capability() -> None: + with pytest.raises(ValueError, match="embeddings"): + build_model_backend( + provider="anthropic", model="claude-test", api_key="sk-test", embedding_dim=42, + model_capabilities=Capabilities(), + ) + + def test_build_backend_warns_on_missing_prefixes_for_embedding_model(caplog: pytest.LogCaptureFixture) -> None: with caplog.at_level("WARNING", logger="omop_llm.embeddings"): - build_model_backend(provider="ollama", model="qwen3-embedding:0.6b", base_url="http://localhost:11434") + build_model_backend( + provider="ollama", model="qwen3-embedding:0.6b", base_url="http://localhost:11434", + model_capabilities=Capabilities(embeddings=True), + ) assert "document_prefix" in caplog.text assert "query_prefix" in caplog.text @@ -405,7 +511,9 @@ def test_build_backend_no_warning_when_prefixes_configured(caplog: pytest.LogCap provider="ollama", model="qwen3-embedding:0.6b", base_url="http://localhost:11434", - configuration={"document_prefix": "search_document: ", "query_prefix": "search_query: "}, + model_capabilities=Capabilities(embeddings=True), + document_prefix="search_document: ", + query_prefix="search_query: ", ) assert caplog.text == "" @@ -413,5 +521,8 @@ def test_build_backend_no_warning_when_prefixes_configured(caplog: pytest.LogCap def test_build_backend_no_prefix_warning_for_non_embedding_provider(caplog: pytest.LogCaptureFixture) -> None: # anthropic is the one provider in the registry with embeddings=False. with caplog.at_level("WARNING", logger="omop_llm.embeddings"): - build_model_backend(provider="anthropic", model="claude-haiku-4-5", api_key="sk-test") + build_model_backend( + provider="anthropic", model="claude-haiku-4-5", api_key="sk-test", + model_capabilities=Capabilities(), + ) assert caplog.text == "" diff --git a/tests/test_embeddings.py b/tests/test_embeddings.py index 5249a6c..cbd65ac 100644 --- a/tests/test_embeddings.py +++ b/tests/test_embeddings.py @@ -15,29 +15,31 @@ class TestApplyEmbeddingPrefix: def test_document_prefix_applied(self) -> None: result = apply_embedding_prefix( - ["diabetes"], EmbeddingRole.DOCUMENT, {"document_prefix": "passage: "} + ["diabetes"], EmbeddingRole.DOCUMENT, document_prefix="passage: ", query_prefix=None ) assert result == ["passage: diabetes"] def test_query_prefix_applied(self) -> None: result = apply_embedding_prefix( - ["hypertension"], EmbeddingRole.QUERY, {"query_prefix": "query: "} + ["hypertension"], EmbeddingRole.QUERY, document_prefix=None, query_prefix="query: " ) assert result == ["query: hypertension"] def test_no_configured_prefix_is_a_noop(self) -> None: - result = apply_embedding_prefix(["diabetes"], EmbeddingRole.DOCUMENT, {}) + result = apply_embedding_prefix( + ["diabetes"], EmbeddingRole.DOCUMENT, document_prefix=None, query_prefix=None + ) assert result == ["diabetes"] def test_wrong_role_key_is_ignored(self) -> None: result = apply_embedding_prefix( - ["diabetes"], EmbeddingRole.DOCUMENT, {"query_prefix": "query: "} + ["diabetes"], EmbeddingRole.DOCUMENT, document_prefix=None, query_prefix="query: " ) assert result == ["diabetes"] def test_applies_to_every_text(self) -> None: result = apply_embedding_prefix( - ["a", "b", "c"], EmbeddingRole.DOCUMENT, {"document_prefix": "p: "} + ["a", "b", "c"], EmbeddingRole.DOCUMENT, document_prefix="p: ", query_prefix=None ) assert result == ["p: a", "p: b", "p: c"] @@ -45,7 +47,7 @@ def test_applies_to_every_text(self) -> None: class TestWarnIfPrefixesLookWrong: def test_warns_when_both_missing(self, caplog: pytest.LogCaptureFixture) -> None: with caplog.at_level("WARNING", logger="omop_llm.embeddings"): - warn_if_prefixes_look_wrong(model="nomic-embed-text:v1.5", configuration={}) + warn_if_prefixes_look_wrong(model="nomic-embed-text:v1.5", document_prefix=None, query_prefix=None) assert "document_prefix" in caplog.text assert "query_prefix" in caplog.text @@ -53,27 +55,22 @@ def test_no_warning_when_both_known(self, caplog: pytest.LogCaptureFixture) -> N with caplog.at_level("WARNING", logger="omop_llm.embeddings"): warn_if_prefixes_look_wrong( model="nomic-embed-text:v1.5", - configuration={ - "document_prefix": "search_document: ", - "query_prefix": "search_query: ", - }, + document_prefix="search_document: ", + query_prefix="search_query: ", ) assert caplog.text == "" def test_warns_on_unrecognized_prefix(self, caplog: pytest.LogCaptureFixture) -> None: with caplog.at_level("WARNING", logger="omop_llm.embeddings"): warn_if_prefixes_look_wrong( - model="some-new-model", - configuration={"document_prefix": "totally_made_up: ", "query_prefix": "query: "}, + model="some-new-model", document_prefix="totally_made_up: ", query_prefix="query: " ) assert "totally_made_up: " in caplog.text assert "doesn't match a commonly recognized" in caplog.text def test_does_not_raise_for_unrecognized_prefix(self) -> None: # A prefix outside KNOWN_EMBEDDING_PREFIXES is a heads-up, not an error. - warn_if_prefixes_look_wrong( - model="some-new-model", configuration={"document_prefix": "custom: ", "query_prefix": "custom: "} - ) + warn_if_prefixes_look_wrong(model="some-new-model", document_prefix="custom: ", query_prefix="custom: ") def test_every_known_prefix_is_a_non_empty_string(self) -> None: for prefix in KNOWN_EMBEDDING_PREFIXES: diff --git a/tests/test_oa_configurator_integration.py b/tests/test_oa_configurator_integration.py index d64bb37..0e5680c 100644 --- a/tests/test_oa_configurator_integration.py +++ b/tests/test_oa_configurator_integration.py @@ -25,6 +25,10 @@ def test_maps_resolved_fields_onto_build_backend() -> None: embedding_dim=None, document_prefix=None, query_prefix=None, + embeddings=True, + tool_use=True, + structured_output=True, + extended_thinking=True, configuration={"max_tokens": 8000, "temperature": 0.0}, ) backend = build_model_backend_from_resolved(resolved) @@ -41,13 +45,20 @@ def test_canonicalizes_the_model_name() -> None: embedding_dim=None, document_prefix=None, query_prefix=None, + embeddings=True, + tool_use=True, + structured_output=True, + extended_thinking=True, configuration={}, ) backend = build_model_backend_from_resolved(resolved) assert backend.model == "llama3:8b" -def test_folds_embedding_dim_and_prefixes_into_configuration() -> None: +def test_embedding_dim_and_prefixes_land_on_dedicated_fields_not_configuration() -> None: + """The old behaviour folded these into `configuration`, which is exactly + the bug that let them leak into the real provider call. They're now + dedicated ModelBackend fields, and configuration stays untouched.""" resolved = ResolvedModel( name="nomic-embed", provider=ResolvedProvider(name="p", provider="ollama", base_url="http://localhost:11434", api_key=None), @@ -55,18 +66,23 @@ def test_folds_embedding_dim_and_prefixes_into_configuration() -> None: embedding_dim=768, document_prefix="search_document: ", query_prefix="search_query: ", + embeddings=True, + tool_use=True, + structured_output=True, + extended_thinking=True, configuration={"max_tokens": 8000}, ) backend = build_model_backend_from_resolved(resolved) - assert backend.configuration == { - "max_tokens": 8000, - "embedding_dim": 768, - "document_prefix": "search_document: ", - "query_prefix": "search_query: ", - } + assert backend.embedding_dim == 768 + assert backend.document_prefix == "search_document: " + assert backend.query_prefix == "search_query: " + assert backend.configuration == {"max_tokens": 8000} -def test_dedicated_fields_take_precedence_over_configuration_dict() -> None: +def test_configuration_dict_is_passed_through_untouched() -> None: + """A same-named key already in resolved.configuration is just data now + -- it's a coincidence, not a collision, since nothing merges into or + reads out of `configuration` for these anymore.""" resolved = ResolvedModel( name="nomic-embed", provider=ResolvedProvider(name="p", provider="ollama", base_url="http://localhost:11434", api_key=None), @@ -74,8 +90,33 @@ def test_dedicated_fields_take_precedence_over_configuration_dict() -> None: embedding_dim=768, document_prefix="search_document: ", query_prefix=None, + embeddings=True, + tool_use=True, + structured_output=True, + extended_thinking=True, configuration={"document_prefix": "stale: ", "query_prefix": "query: "}, ) backend = build_model_backend_from_resolved(resolved) - assert backend.configuration["document_prefix"] == "search_document: " - assert backend.configuration["query_prefix"] == "query: " + assert backend.document_prefix == "search_document: " + assert backend.configuration == {"document_prefix": "stale: ", "query_prefix": "query: "} + + +def test_capability_fields_narrow_the_provider_ceiling() -> None: + """ollama's own provider metadata says embeddings=True; a model-level + declaration of embeddings=False must still narrow the effective result, + proving this is a real AND and not just reading the provider ceiling.""" + resolved = ResolvedModel( + name="local-chat", + provider=ResolvedProvider(name="p", provider="ollama", base_url="http://localhost:11434", api_key=None), + model="local-chat:8b", + embedding_dim=None, + document_prefix=None, + query_prefix=None, + embeddings=False, + tool_use=True, + structured_output=True, + extended_thinking=True, + configuration={}, + ) + backend = build_model_backend_from_resolved(resolved) + assert backend.capabilities.embeddings is False diff --git a/tests/test_registry.py b/tests/test_registry.py index 9619d50..82b6583 100644 --- a/tests/test_registry.py +++ b/tests/test_registry.py @@ -7,7 +7,7 @@ from omop_llm.errors import UnsupportedProviderError from omop_llm.providers import ( canonical_model_name, - capabilities_for, + provider_capabilities_for, provider_class_for, supported_providers, ) @@ -44,13 +44,13 @@ def test_unregistered_provider_rejected() -> None: def test_capabilities_embeddings_match_any_llm_metadata( provider: str, expect_embeddings: bool ) -> None: - caps = capabilities_for(provider) + caps = provider_capabilities_for(provider) assert caps.embeddings is expect_embeddings def test_capabilities_tool_use_and_structured_output_are_declared_not_inferred() -> None: for provider in supported_providers(): - caps = capabilities_for(provider) + caps = provider_capabilities_for(provider) # These two are never read off any-llm's own metadata; it has no # such fields at all. Every registered provider currently declares # both True; this just pins that it comes from our own registry. @@ -60,7 +60,7 @@ def test_capabilities_tool_use_and_structured_output_are_declared_not_inferred() def test_unregistered_provider_capabilities_rejected() -> None: with pytest.raises(UnsupportedProviderError): - capabilities_for("bedrock") + provider_capabilities_for("bedrock") def test_canonical_model_name_dispatches_to_the_right_provider() -> None: From 9e31f29f67e7abf03c90f7c4dda56c3907a44dde Mon Sep 17 00:00:00 2001 From: Nico Loesch Date: Mon, 10 Aug 2026 04:52:06 +0000 Subject: [PATCH 17/20] Placeholder API Key for vllm and llamacpp --- src/omop_llm/structured.py | 41 +++++++++++------ tests/test_structured.py | 92 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 13 deletions(-) diff --git a/src/omop_llm/structured.py b/src/omop_llm/structured.py index 4f7a49f..9dc8292 100644 --- a/src/omop_llm/structured.py +++ b/src/omop_llm/structured.py @@ -51,7 +51,7 @@ def _check_provider_and_base_url(provider: str, base_url: str | None) -> None: f"only {sorted(_INSTRUCTOR_SAFE_PROVIDERS)} are confirmed to share any-llm's " "transport for this provider, see the omop_llm.structured module docstring" ) - if provider != "openai" and base_url is None: + if provider != OpenaiProvider.PROVIDER_NAME and base_url is None: raise ValueError( f"base_url is required for provider={provider!r} " "(without it, instructor's 'openai' builder would silently target " @@ -70,6 +70,31 @@ def _require_instructor() -> Any: return instructor +# llamacpp/vllm don't require real credentials, but the openai SDK client +# instructor builds underneath still requires a non-empty api_key string +# to construct at all, regardless of whether the target server checks it. +_LOCAL_PLACEHOLDER_API_KEY = "not-needed" + + +def _instructor_client_kwargs( + provider: str, *, base_url: str | None, api_key: str | None, async_client: bool +) -> dict[str, Any]: + """Build the kwargs for instructor's 'openai' client builder. + + Substitutes ``_LOCAL_PLACEHOLDER_API_KEY`` when the caller gave no key + and the provider isn't real OpenAI; ``openai`` itself still falls + through to ``OPENAI_API_KEY`` as before. + """ + client_kwargs: dict[str, Any] = {"async_client": async_client} + if base_url is not None: + client_kwargs["base_url"] = base_url + if api_key is not None: + client_kwargs["api_key"] = api_key + elif provider != OpenaiProvider.PROVIDER_NAME: + client_kwargs["api_key"] = _LOCAL_PLACEHOLDER_API_KEY + return client_kwargs + + def extract_with_retry[T: BaseModel]( provider: str, model: str, @@ -90,12 +115,7 @@ def extract_with_retry[T: BaseModel]( """ _check_provider_and_base_url(provider, base_url) instructor = _require_instructor() - - client_kwargs: dict[str, Any] = {"async_client": False} - if base_url is not None: - client_kwargs["base_url"] = base_url - if api_key is not None: - client_kwargs["api_key"] = api_key + client_kwargs = _instructor_client_kwargs(provider, base_url=base_url, api_key=api_key, async_client=False) client = instructor.from_provider(f"openai/{model}", **client_kwargs) return client.chat.completions.create( @@ -166,12 +186,7 @@ async def async_extract_with_retry[T: BaseModel]( """ _check_provider_and_base_url(provider, base_url) instructor = _require_instructor() - - client_kwargs: dict[str, Any] = {"async_client": True} - if base_url is not None: - client_kwargs["base_url"] = base_url - if api_key is not None: - client_kwargs["api_key"] = api_key + client_kwargs = _instructor_client_kwargs(provider, base_url=base_url, api_key=api_key, async_client=True) client = instructor.from_provider(f"openai/{model}", **client_kwargs) return await client.chat.completions.create( diff --git a/tests/test_structured.py b/tests/test_structured.py index ad26a8e..038b72b 100644 --- a/tests/test_structured.py +++ b/tests/test_structured.py @@ -7,6 +7,8 @@ from __future__ import annotations +from unittest.mock import AsyncMock, MagicMock + import pytest from pydantic import BaseModel @@ -14,6 +16,8 @@ from omop_llm.providers import supported_providers from omop_llm.structured import ( _INSTRUCTOR_SAFE_PROVIDERS, + _LOCAL_PLACEHOLDER_API_KEY, + _instructor_client_kwargs, async_extract_with_retry, extract_with_retry, ) @@ -55,3 +59,91 @@ def test_extract_with_retry_requires_base_url_for_self_hosted_providers() -> Non async def test_async_extract_with_retry_requires_base_url_for_self_hosted_providers() -> None: with pytest.raises(ValueError, match="base_url"): await async_extract_with_retry("llamacpp", "local-chat", [{"role": "user", "content": "hi"}], Answer) + + +class TestInstructorClientKwargs: + """llamacpp/vllm don't require real credentials, but the openai SDK + client instructor builds underneath still requires a non-empty + api_key string to construct at all -- this is what previously made + extract_with_retry("llamacpp", ...) fail with no api_key given.""" + + @pytest.mark.parametrize("provider", ["llamacpp", "vllm"]) + def test_local_provider_without_api_key_gets_placeholder(self, provider: str) -> None: + kwargs = _instructor_client_kwargs(provider, base_url="http://x", api_key=None, async_client=False) + assert kwargs["api_key"] == _LOCAL_PLACEHOLDER_API_KEY + + def test_openai_without_api_key_gets_no_placeholder(self) -> None: + # Real OpenAI must keep falling through to OPENAI_API_KEY, not a fake key. + kwargs = _instructor_client_kwargs("openai", base_url=None, api_key=None, async_client=False) + assert "api_key" not in kwargs + + def test_explicit_api_key_is_never_overridden(self) -> None: + kwargs = _instructor_client_kwargs("llamacpp", base_url="http://x", api_key="real-key", async_client=False) + assert kwargs["api_key"] == "real-key" + + def test_async_client_flag_is_forwarded(self) -> None: + kwargs = _instructor_client_kwargs("openai", base_url=None, api_key="k", async_client=True) + assert kwargs["async_client"] is True + + +class TestInstructorClientConstruction: + """End-to-end: instructor.from_provider is mocked, but extract_with_retry's + own kwargs-building and call plumbing run for real.""" + + def test_extract_with_retry_passes_placeholder_key_for_local_provider(self, monkeypatch: pytest.MonkeyPatch) -> None: + import instructor + + fake_client = MagicMock() + fake_client.chat.completions.create.return_value = Answer(value="ok") + captured: dict = {} + + def fake_from_provider(model_string: str, **kwargs): + captured.update(kwargs) + return fake_client + + monkeypatch.setattr(instructor, "from_provider", fake_from_provider) + + result = extract_with_retry( + "llamacpp", "local-chat", [{"role": "user", "content": "hi"}], Answer, base_url="http://x" + ) + assert result == Answer(value="ok") + assert captured["api_key"] == _LOCAL_PLACEHOLDER_API_KEY + assert captured["async_client"] is False + + async def test_async_extract_with_retry_passes_placeholder_key_for_local_provider( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + import instructor + + fake_client = MagicMock() + fake_client.chat.completions.create = AsyncMock(return_value=Answer(value="ok")) + captured: dict = {} + + def fake_from_provider(model_string: str, **kwargs): + captured.update(kwargs) + return fake_client + + monkeypatch.setattr(instructor, "from_provider", fake_from_provider) + + result = await async_extract_with_retry( + "vllm", "local-chat", [{"role": "user", "content": "hi"}], Answer, base_url="http://x" + ) + assert result == Answer(value="ok") + assert captured["api_key"] == _LOCAL_PLACEHOLDER_API_KEY + assert captured["async_client"] is True + + def test_extract_with_retry_never_fakes_a_key_for_real_openai(self, monkeypatch: pytest.MonkeyPatch) -> None: + import instructor + + fake_client = MagicMock() + fake_client.chat.completions.create.return_value = Answer(value="ok") + captured: dict = {} + + def fake_from_provider(model_string: str, **kwargs): + captured.update(kwargs) + return fake_client + + monkeypatch.setattr(instructor, "from_provider", fake_from_provider) + + extract_with_retry("openai", "gpt-4o", [{"role": "user", "content": "hi"}], Answer, api_key="sk-real") + assert captured["api_key"] == "sk-real" From 7171f7ea589568d044b7b7a65a01aa1a83356520 Mon Sep 17 00:00:00 2001 From: Nico Loesch Date: Mon, 10 Aug 2026 04:57:33 +0000 Subject: [PATCH 18/20] Strip whitespace for canonical_model_name --- src/omop_llm/providers/registry.py | 11 +++++++++-- src/omop_llm/providers/supported.py | 3 +-- tests/test_registry.py | 14 ++++++++++++++ 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/omop_llm/providers/registry.py b/src/omop_llm/providers/registry.py index ee15dfe..1b0a6f9 100644 --- a/src/omop_llm/providers/registry.py +++ b/src/omop_llm/providers/registry.py @@ -124,6 +124,9 @@ def canonical_model_name(provider_key: str, name: str) -> str: always canonical without callers needing to remember to do it themselves. + Strips surrounding whitespace and rejects an empty result *before* + dispatching to the provider's own ``canonical_model_name``. + Parameters ---------- provider_key : str @@ -141,9 +144,13 @@ def canonical_model_name(provider_key: str, name: str) -> str: UnsupportedProviderError If ``provider_key`` is not registered. ValueError - If ``name`` cannot be made canonical for this provider (e.g. an - Ollama name with no explicit tag). + If ``name`` is empty or whitespace-only, or cannot otherwise be + made canonical for this provider (e.g. an Ollama name with no + explicit tag). """ provider_class = provider_class_for(provider_key) assert issubclass(provider_class, ProviderMixin) + name = name.strip() + if not name: + raise ValueError(f"Model name cannot be empty or whitespace-only for provider {provider_key!r}.") return provider_class.canonical_model_name(name) diff --git a/src/omop_llm/providers/supported.py b/src/omop_llm/providers/supported.py index 769e896..f265bc1 100644 --- a/src/omop_llm/providers/supported.py +++ b/src/omop_llm/providers/supported.py @@ -62,14 +62,13 @@ def canonical_model_name(cls, name: str) -> str: Returns ------- str - The input name, validated and stripped of whitespace. + The input name, validated. Raises ------ ValueError If the name has no tag, or if the tag is ``:latest``. """ - name = name.strip() if ":" not in name: raise ValueError( f"Ollama model name {name!r} must include an explicit tag. " diff --git a/tests/test_registry.py b/tests/test_registry.py index 82b6583..de8bc4e 100644 --- a/tests/test_registry.py +++ b/tests/test_registry.py @@ -73,3 +73,17 @@ def test_canonical_model_name_dispatches_to_the_right_provider() -> None: def test_canonical_model_name_unregistered_provider_rejected() -> None: with pytest.raises(UnsupportedProviderError): canonical_model_name("bedrock", "some-model") + + +@pytest.mark.parametrize("provider", ["openai", "anthropic", "gemini", "llamacpp", "vllm"]) +def test_canonical_model_name_strips_whitespace(provider: str) -> None: + # Previously only ollama stripped whitespace; the other 5 providers + # returned the name unchanged, preserving stray whitespace in what's + # meant to be a stable storage key. + assert canonical_model_name(provider, " gpt-4o ") == "gpt-4o" + + +@pytest.mark.parametrize("name", ["", " ", "\t\n"]) +def test_canonical_model_name_rejects_empty_after_strip(name: str) -> None: + with pytest.raises(ValueError, match="empty or whitespace"): + canonical_model_name("openai", name) From 03ee78d0336be879728bbc92e40c3d21de51e2cf Mon Sep 17 00:00:00 2001 From: Nico Loesch Date: Mon, 10 Aug 2026 05:06:48 +0000 Subject: [PATCH 19/20] Add typing bits as per PR --- pyproject.toml | 3 +-- src/omop_llm/__init__.py | 2 ++ src/omop_llm/py.typed | 0 3 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 src/omop_llm/py.typed diff --git a/pyproject.toml b/pyproject.toml index d6b2381..71d20cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,11 +5,10 @@ description = "LLM interfaces for OMOP" authors = [ {name = "Nico Loesch", email = "n.loesch@unsw.edu.au"} ] -license = "Apache-2.0" readme = "README.md" requires-python = ">=3.12" dependencies = [ - "any-llm-sdk[ollama,gemini]>=1.22.0", + "any-llm-sdk[ollama,gemini]>=1.22.0,<2.0.0", "httpx", "oa-configurator>=0.2.0,<1.0.0", # TODO: raise to >=0.2.0,<2.0.0 "ollama", # already imported by any-llm-sdk diff --git a/src/omop_llm/__init__.py b/src/omop_llm/__init__.py index 1fe1417..436c0a1 100644 --- a/src/omop_llm/__init__.py +++ b/src/omop_llm/__init__.py @@ -10,6 +10,7 @@ KNOWN_EMBEDDING_PREFIXES ) from omop_llm.errors import ( + NoParsedOutputError, OmopLlmError, UnsupportedCapabilityError, UnsupportedProviderError @@ -25,6 +26,7 @@ "KNOWN_EMBEDDING_PREFIXES", "ModelBackend", "Capabilities", + "NoParsedOutputError", "OmopLlmError", "UnsupportedCapabilityError", "UnsupportedProviderError", diff --git a/src/omop_llm/py.typed b/src/omop_llm/py.typed new file mode 100644 index 0000000..e69de29 From d368d8b8f89d4c518f471e20e0122c5aa52021c8 Mon Sep 17 00:00:00 2001 From: gkennos Date: Wed, 12 Aug 2026 21:36:14 +1000 Subject: [PATCH 20/20] license and linting --- LICENSE | 202 ++++++++++++++++++++++++++ pyproject.toml | 2 +- src/omop_llm/__init__.py | 14 +- src/omop_llm/backend.py | 6 +- src/omop_llm/providers/__init__.py | 1 - src/omop_llm/providers/registry.py | 4 +- src/omop_llm/providers/supported.py | 8 +- uv.lock | 218 ++++++++++++++++++++++------ 8 files changed, 397 insertions(+), 58 deletions(-) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/pyproject.toml b/pyproject.toml index 71d20cf..41a1ad6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ requires-python = ">=3.12" dependencies = [ "any-llm-sdk[ollama,gemini]>=1.22.0,<2.0.0", "httpx", - "oa-configurator>=0.2.0,<1.0.0", # TODO: raise to >=0.2.0,<2.0.0 + "oa-configurator>=1.0.0,<2.0.0", "ollama", # already imported by any-llm-sdk "pydantic", ] diff --git a/src/omop_llm/__init__.py b/src/omop_llm/__init__.py index 436c0a1..9b0ebc1 100644 --- a/src/omop_llm/__init__.py +++ b/src/omop_llm/__init__.py @@ -3,29 +3,25 @@ build_model_backend, build_model_backend_from_resolved, ) - from omop_llm.capabilities import Capabilities -from omop_llm.embeddings import ( - EmbeddingRole, - KNOWN_EMBEDDING_PREFIXES -) +from omop_llm.embeddings import KNOWN_EMBEDDING_PREFIXES, EmbeddingRole from omop_llm.errors import ( NoParsedOutputError, OmopLlmError, UnsupportedCapabilityError, - UnsupportedProviderError + UnsupportedProviderError, ) from omop_llm.providers import ( canonical_model_name, provider_capabilities_for, - supported_providers + supported_providers, ) __all__ = [ - "EmbeddingRole", "KNOWN_EMBEDDING_PREFIXES", - "ModelBackend", "Capabilities", + "EmbeddingRole", + "ModelBackend", "NoParsedOutputError", "OmopLlmError", "UnsupportedCapabilityError", diff --git a/src/omop_llm/backend.py b/src/omop_llm/backend.py index 4fdf5bd..11af980 100644 --- a/src/omop_llm/backend.py +++ b/src/omop_llm/backend.py @@ -29,7 +29,11 @@ from pydantic import BaseModel, ValidationError from omop_llm.capabilities import Capabilities -from omop_llm.embeddings import EmbeddingRole, apply_embedding_prefix, warn_if_prefixes_look_wrong +from omop_llm.embeddings import ( + EmbeddingRole, + apply_embedding_prefix, + warn_if_prefixes_look_wrong, +) from omop_llm.errors import NoParsedOutputError, UnsupportedCapabilityError from omop_llm.providers.base import ProviderMixin from omop_llm.providers.registry import ( diff --git a/src/omop_llm/providers/__init__.py b/src/omop_llm/providers/__init__.py index ec9673b..378070e 100644 --- a/src/omop_llm/providers/__init__.py +++ b/src/omop_llm/providers/__init__.py @@ -6,7 +6,6 @@ supported_providers, ) - __all__ = [ "PROVIDER_REGISTRY", "canonical_model_name", diff --git a/src/omop_llm/providers/registry.py b/src/omop_llm/providers/registry.py index 1b0a6f9..e35fa01 100644 --- a/src/omop_llm/providers/registry.py +++ b/src/omop_llm/providers/registry.py @@ -28,7 +28,9 @@ from omop_llm.capabilities import Capabilities from omop_llm.errors import UnsupportedProviderError -from omop_llm.providers import supported as _supported # noqa: F401 (required for PROVIDER_REGISTRY to be populated) +from omop_llm.providers import ( + supported as _supported, # noqa: F401 (required for PROVIDER_REGISTRY to be populated) +) from omop_llm.providers.base import ProviderMixin PROVIDER_REGISTRY: Final[dict[str, type[AnyLLM]]] = { diff --git a/src/omop_llm/providers/supported.py b/src/omop_llm/providers/supported.py index f265bc1..3f98b1b 100644 --- a/src/omop_llm/providers/supported.py +++ b/src/omop_llm/providers/supported.py @@ -17,9 +17,13 @@ from typing import Any import httpx -from any_llm.providers.anthropic.anthropic import AnthropicProvider as AnyLLMAnthropicProvider +from any_llm.providers.anthropic.anthropic import ( + AnthropicProvider as AnyLLMAnthropicProvider, +) from any_llm.providers.gemini.gemini import GeminiProvider as AnyLLMGeminiProvider -from any_llm.providers.llamacpp.llamacpp import LlamacppProvider as AnyLLMLlamacppProvider +from any_llm.providers.llamacpp.llamacpp import ( + LlamacppProvider as AnyLLMLlamacppProvider, +) from any_llm.providers.ollama.ollama import OllamaProvider as AnyLLMOllamaProvider from any_llm.providers.openai.openai import OpenaiProvider as AnyLLMOpenaiProvider from any_llm.providers.vllm.vllm import VllmProvider as AnyLLMVllmProvider diff --git a/uv.lock b/uv.lock index b04d3fc..5a81fe4 100644 --- a/uv.lock +++ b/uv.lock @@ -539,52 +539,52 @@ wheels = [ [[package]] name = "cryptography" -version = "49.0.0" +version = "50.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", size = 4032100, upload-time = "2026-06-12T20:02:32.143Z" }, - { url = "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978, upload-time = "2026-06-12T20:01:21.305Z" }, - { url = "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422, upload-time = "2026-06-12T20:01:48.566Z" }, - { url = "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503, upload-time = "2026-06-12T20:02:47.091Z" }, - { url = "https://files.pythonhosted.org/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", size = 5309779, upload-time = "2026-06-12T20:02:08.987Z" }, - { url = "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683, upload-time = "2026-06-12T20:02:03.335Z" }, - { url = "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874, upload-time = "2026-06-12T20:02:54.323Z" }, - { url = "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283, upload-time = "2026-06-12T20:01:34.822Z" }, - { url = "https://files.pythonhosted.org/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", size = 5265844, upload-time = "2026-06-12T20:01:24.09Z" }, - { url = "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290, upload-time = "2026-06-12T20:01:30.848Z" }, - { url = "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612, upload-time = "2026-06-12T20:01:29.246Z" }, - { url = "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804, upload-time = "2026-06-12T20:01:42.853Z" }, - { url = "https://files.pythonhosted.org/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", size = 3810026, upload-time = "2026-06-12T20:02:39.262Z" }, - { url = "https://files.pythonhosted.org/packages/ec/9e/db72b3ae7fc9cfad53e630e56c6ae83b9b6ff0bf3718ffb8012d20b3aabf/cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7", size = 4013892, upload-time = "2026-06-12T20:02:10.735Z" }, - { url = "https://files.pythonhosted.org/packages/86/12/c48a424f38db03027be9f7ed5c7dc5de9933dbee992865f98b13727a009d/cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", size = 4678835, upload-time = "2026-06-12T20:02:48.743Z" }, - { url = "https://files.pythonhosted.org/packages/68/28/8a3ad4653662c93fc44dc4e5d8fd374c25c42e07b34bbfbadf49cf57a5a8/cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", size = 4697239, upload-time = "2026-06-12T20:02:56.03Z" }, - { url = "https://files.pythonhosted.org/packages/a8/b2/2193fc74f81aee4f9b62733133b73b5176718932ed8f2e4b03fa040480a6/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", size = 4685593, upload-time = "2026-06-12T20:02:50.666Z" }, - { url = "https://files.pythonhosted.org/packages/47/f1/1d3eaa243bfc5de4a187b22aa8c048b3e4980bfbe830ac46e6bac2e66947/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d", size = 5289961, upload-time = "2026-06-12T20:01:46.468Z" }, - { url = "https://files.pythonhosted.org/packages/58/39/2d51306721330c486495853eda1c567880ff036de15a14c4b74f399934af/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", size = 4731145, upload-time = "2026-06-12T20:02:16.832Z" }, - { url = "https://files.pythonhosted.org/packages/17/50/983e838c7fd0d87fd8c969bcdd328edaf5f756e38df5281637424c155873/cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", size = 4321719, upload-time = "2026-06-12T20:02:52.611Z" }, - { url = "https://files.pythonhosted.org/packages/a7/f5/8f571d7e27c55bce9f76f026143bcb1e040a4233149ecca0bea5fa5dd5f7/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", size = 4685209, upload-time = "2026-06-12T20:02:07.282Z" }, - { url = "https://files.pythonhosted.org/packages/e7/84/0e27016a6fc5a0886f797018b26aa42f40c09a82332bff77822a451deaaa/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866", size = 5246285, upload-time = "2026-06-12T20:01:32.439Z" }, - { url = "https://files.pythonhosted.org/packages/11/2d/5e1fb307cb5931881516b464c98774b3f2c36b5d4bb9a2830253cf553cad/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", size = 4730441, upload-time = "2026-06-12T20:02:01.469Z" }, - { url = "https://files.pythonhosted.org/packages/e4/c0/bff5a02ee731d207d6a1ed51732549d8c53d2bc8da1d10ec6f2844201d68/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", size = 4815869, upload-time = "2026-06-12T20:01:36.574Z" }, - { url = "https://files.pythonhosted.org/packages/b9/26/814681d14248d95d73d5c3eea0c39a94eb8302df966f670a2c60de90974b/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", size = 4960948, upload-time = "2026-06-12T20:02:18.688Z" }, - { url = "https://files.pythonhosted.org/packages/4c/fe/93ecac273d3738939d023612ad12cca9a3740a5345d69fda04134c43fd96/cryptography-49.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61", size = 3799153, upload-time = "2026-06-12T20:01:39.059Z" }, - { url = "https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", size = 4025947, upload-time = "2026-06-12T20:01:25.745Z" }, - { url = "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429, upload-time = "2026-06-12T20:01:53.628Z" }, - { url = "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968, upload-time = "2026-06-12T20:02:45.383Z" }, - { url = "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758, upload-time = "2026-06-12T20:01:41.13Z" }, - { url = "https://files.pythonhosted.org/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", size = 5298863, upload-time = "2026-06-12T20:02:24.579Z" }, - { url = "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983, upload-time = "2026-06-12T20:01:50.14Z" }, - { url = "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173, upload-time = "2026-06-12T20:01:44.743Z" }, - { url = "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298, upload-time = "2026-06-12T20:02:20.918Z" }, - { url = "https://files.pythonhosted.org/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", size = 5254338, upload-time = "2026-06-12T20:02:22.737Z" }, - { url = "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650, upload-time = "2026-06-12T20:02:41.389Z" }, - { url = "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820, upload-time = "2026-06-12T20:01:51.847Z" }, - { url = "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968, upload-time = "2026-06-12T20:02:12.524Z" }, - { url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/de/41/6cbdcf9142d00fe82836fbb51e503e58088575cf7a0fe1dbff6695bf0840/cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9", size = 880201, upload-time = "2026-07-31T14:25:10.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/5c/59086b4aac5e879d38ddbcf74e4be7ade89cebc3eb199a55da998c3bb46a/cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03", size = 4001252, upload-time = "2026-07-31T14:23:33.331Z" }, + { url = "https://files.pythonhosted.org/packages/57/ef/8f2df13c7216bcad3e1c74e07f6e193d93e998e114f524a53877c9af27ad/cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645", size = 4719554, upload-time = "2026-07-31T14:23:35.611Z" }, + { url = "https://files.pythonhosted.org/packages/d9/41/029086c34d91052fc3b88bcc8056f709a7c915c7a23b235a54eb800b1c97/cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7", size = 4702130, upload-time = "2026-07-31T14:23:37.635Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ff/b6ce0954962e7f7b969f850a883744197bb3910bdfd7b6da162eab7d9f68/cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3", size = 4725244, upload-time = "2026-07-31T14:23:39.471Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/63a1027cb7fec360a182208e1b7767d5aa1fe57be3d6aa856e69a321edc0/cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f", size = 5342265, upload-time = "2026-07-31T14:23:41.286Z" }, + { url = "https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae", size = 4734609, upload-time = "2026-07-31T14:23:43.139Z" }, + { url = "https://files.pythonhosted.org/packages/15/37/36a9c479bbe49acea2636c7fd3360d20f7b7e079c300352011c44850b181/cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a", size = 4356517, upload-time = "2026-07-31T14:23:44.939Z" }, + { url = "https://files.pythonhosted.org/packages/32/98/8a151d64367204cbc63ec65d37502f1d9c53cf4bfc6ec3c532614dbec60d/cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987", size = 4724529, upload-time = "2026-07-31T14:23:46.93Z" }, + { url = "https://files.pythonhosted.org/packages/22/f6/ec13b470172126464a86bf54d2294a46d29837fc51ba3e45d4047946fb5e/cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169", size = 5299852, upload-time = "2026-07-31T14:23:48.851Z" }, + { url = "https://files.pythonhosted.org/packages/da/3a/f05e32c99d440c9bb891ea0e36c9091891e36be5a9a87ab2ee6ea20729f6/cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f", size = 4734462, upload-time = "2026-07-31T14:23:50.861Z" }, + { url = "https://files.pythonhosted.org/packages/ca/dc/bd72b26be8953f80625f63151efd38eee71c76ca6cf591c08ff34615a79e/cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105", size = 4852708, upload-time = "2026-07-31T14:23:52.715Z" }, + { url = "https://files.pythonhosted.org/packages/27/20/c930314a2ab476d15dec966ec87e2e9637bb02b06106b12c0396c57bb603/cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef", size = 5004179, upload-time = "2026-07-31T14:23:54.887Z" }, + { url = "https://files.pythonhosted.org/packages/32/2e/c9db68a0c4bfa28e310707527c0ee3a2bd254104d2e02e68f368e197aa4c/cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30", size = 3840395, upload-time = "2026-07-31T14:23:56.677Z" }, + { url = "https://files.pythonhosted.org/packages/c3/fb/951032a3bf22a5697c83183fb6294a4843772947a70e616c57b3ff5f522e/cryptography-50.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41", size = 3989258, upload-time = "2026-07-31T14:23:58.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/67/91eb047e69c5e845f2f14b8a2e4a1aab0f283cb885531e9e22c8adb176bc/cryptography-50.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc", size = 4700648, upload-time = "2026-07-31T14:24:00.702Z" }, + { url = "https://files.pythonhosted.org/packages/30/82/85f0f7425c856b9f96459411eb12e74ef72df9caf6f8f15bf23a33ff131f/cryptography-50.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f", size = 4682442, upload-time = "2026-07-31T14:24:02.538Z" }, + { url = "https://files.pythonhosted.org/packages/1a/28/b555a365adff1cca2fbe7b9e487d68a40de6bc67ff2cb587473eb43de0e7/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3", size = 4707596, upload-time = "2026-07-31T14:24:04.394Z" }, + { url = "https://files.pythonhosted.org/packages/72/d8/f52538140cc719df62a01cf87d1c7142318d235817109d6f4054d7c352d6/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533", size = 5314552, upload-time = "2026-07-31T14:24:06.31Z" }, + { url = "https://files.pythonhosted.org/packages/38/14/6120e5bd7c5aa022ad15424ba4d5c5269d0d9448ed4d55e492ea91e3c1c4/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037", size = 4717113, upload-time = "2026-07-31T14:24:08.349Z" }, + { url = "https://files.pythonhosted.org/packages/fa/71/190bf38c3ee2e0f8efc9860ae100c9df4169742eef274b91e7aa1cb133b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f", size = 4338580, upload-time = "2026-07-31T14:24:10.227Z" }, + { url = "https://files.pythonhosted.org/packages/3a/63/504ccfbbe61fd8aa983f7f146399cdf034c72c2fc55f5b2dfdcdcdb20c99/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11", size = 4707038, upload-time = "2026-07-31T14:24:12.169Z" }, + { url = "https://files.pythonhosted.org/packages/01/77/2cf79bbfc4d12ca106437a6e170d6aaa01a373e93093118aaaef0e801bd4/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d", size = 5273110, upload-time = "2026-07-31T14:24:14.38Z" }, + { url = "https://files.pythonhosted.org/packages/e5/45/8aae2972c520145377ea3559a605a899bebe227bf070b33cdb445929a9b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c", size = 4716439, upload-time = "2026-07-31T14:24:16.415Z" }, + { url = "https://files.pythonhosted.org/packages/7b/20/4fe50b619a48c2525cc46e2dbc1ac490708d704be5d467bdaac6dc955682/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c", size = 4837383, upload-time = "2026-07-31T14:24:18.553Z" }, + { url = "https://files.pythonhosted.org/packages/92/91/3a31366e183343d3703f8995c095f5734676bd6938118047e50fcf279eb4/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95", size = 4985772, upload-time = "2026-07-31T14:24:20.385Z" }, + { url = "https://files.pythonhosted.org/packages/74/9a/02ffe35b2853d121689871eb5dce862092562b3a1ed5cc98f1aaed441506/cryptography-50.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269", size = 3816291, upload-time = "2026-07-31T14:24:22.125Z" }, + { url = "https://files.pythonhosted.org/packages/03/37/73d005be173aff344af30e9fd2a576575cb2391a7101d9cd3842e1fa8cce/cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07", size = 4036009, upload-time = "2026-07-31T14:24:24.122Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c6/7a6202a534e32103a285b7834a120869557fe198d51d7cfe59754c8bda9c/cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3", size = 4745252, upload-time = "2026-07-31T14:24:26.118Z" }, + { url = "https://files.pythonhosted.org/packages/85/4f/0fa8c2f4428198f15d9ff8d63400e27afbf94ce833f6108da1eb3753f945/cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f", size = 4728939, upload-time = "2026-07-31T14:24:27.994Z" }, + { url = "https://files.pythonhosted.org/packages/d1/63/54dd723490ba2dc09b299682c10b38db38f159728bcaae8c591b8af2f22d/cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5", size = 4748483, upload-time = "2026-07-31T14:24:30.254Z" }, + { url = "https://files.pythonhosted.org/packages/1d/dd/7c77d26285cc7f6991efce64a0f5b4f9383bfa5dd8c5033003eaf7db4cdb/cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f", size = 5367599, upload-time = "2026-07-31T14:24:32.457Z" }, + { url = "https://files.pythonhosted.org/packages/46/c9/f60aed34c013f317f92817b6c171c2d22a78270fa41109bd4b08af26b194/cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025", size = 4762647, upload-time = "2026-07-31T14:24:34.599Z" }, + { url = "https://files.pythonhosted.org/packages/be/f3/f9a0173b139372c3a48ed98154b45cc6b9de17c789d5ab552e621c293609/cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a", size = 4385197, upload-time = "2026-07-31T14:24:36.647Z" }, + { url = "https://files.pythonhosted.org/packages/d8/36/83bb81f6e569bc38e1e4a7bc80f29b46bb9601920bc455fc8e888f5d5742/cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b", size = 4748095, upload-time = "2026-07-31T14:24:39.493Z" }, + { url = "https://files.pythonhosted.org/packages/6b/16/d3008eff98c764979865834c3d386d4fd041b5f52e7f34fc29ac1a5eb515/cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708", size = 5325948, upload-time = "2026-07-31T14:24:41.556Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f8/d97f9603efda3888187bfdb893f26c41be4735c10631d05d284ee6b047c4/cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47", size = 4762400, upload-time = "2026-07-31T14:24:43.636Z" }, + { url = "https://files.pythonhosted.org/packages/64/a2/4615c8f7d81a00b1d6e6afe19f694e1543582349fb5f4076f6cb5dc36485/cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9", size = 4878208, upload-time = "2026-07-31T14:24:45.522Z" }, + { url = "https://files.pythonhosted.org/packages/d2/1a/efcfb02f91407149a0dacffffab791f7e19bf6385f63b3666dc8b5e5c9c8/cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7", size = 5037050, upload-time = "2026-07-31T14:24:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/57/30/4a22984d4f1bdfb8c054f07a92bc176b97a3134cc1d6c4b3bffb1f3688b4/cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba", size = 3874135, upload-time = "2026-07-31T14:24:50.085Z" }, ] [[package]] @@ -847,6 +847,73 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed", size = 300631, upload-time = "2026-05-07T08:03:30.345Z" }, ] +[[package]] +name = "greenlet" +version = "3.5.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/d8/7cc97c142388aef03f622e001c572c4f84e9252a439549d483f555771970/greenlet-3.5.5.tar.gz", hash = "sha256:adb4bae02e91a8e863e48b177e4014bdcac8a6b5e047ea1df687a61534b85e6c", size = 207585, upload-time = "2026-08-10T15:09:36.136Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/7e/9ecd0285e3153532ae07aeb88063c43c72b4221cf0d4d123b02f3682e3ff/greenlet-3.5.5-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:49520f0c95a48b42cf55414b8e8479beb274ea70431afc33e3f79903c71f4380", size = 295809, upload-time = "2026-08-10T13:25:34.023Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/60e4bbcc89252037b18087f2ec16405d5b2d5be42dde191bbf3667e96102/greenlet-3.5.5-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55272212cbc5f43d1d723725ab931f1939969b7e9523882ca58b55061769d053", size = 611910, upload-time = "2026-08-10T14:14:35.18Z" }, + { url = "https://files.pythonhosted.org/packages/a4/17/cd5134be659cd4a443e7a61ae670dabec165a814c51162916d637b6dd38e/greenlet-3.5.5-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655bca754a2ef4efcb0eb48a94d3f4593536d0f3d48f8ed44343c01d16a92f95", size = 624198, upload-time = "2026-08-10T14:27:25.229Z" }, + { url = "https://files.pythonhosted.org/packages/9b/30/87c212b5c684d0e72974f1063b7a9687631e8985902c06e1016542c874e7/greenlet-3.5.5-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ca5d6ae0739e5764f2cfcfaa562ac5a990cbdaedca93251c5e3cf07c362371f", size = 629504, upload-time = "2026-08-10T14:30:07.967Z" }, + { url = "https://files.pythonhosted.org/packages/78/ac/5c5b959999b6f09c3026b5dfe171575bc3121c5236ce74f495096f25b203/greenlet-3.5.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:147b25a42e5ca5be3d42356e8f608b37af715a1c196e9bf9d1627f3341adfe1d", size = 621439, upload-time = "2026-08-10T13:40:49.391Z" }, + { url = "https://files.pythonhosted.org/packages/63/2c/eb487fafc9f50ffff2b1e0b697f70fb34bf150821c08ab225aacf5583a7e/greenlet-3.5.5-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:1b5ed9162c0c098e0bbc2cf88a94f433c1b8926f831745252e099e5d83e17759", size = 432462, upload-time = "2026-08-10T14:30:02.309Z" }, + { url = "https://files.pythonhosted.org/packages/c8/8b/6acf112ed8aee499f25b4d6949820fb02ac950ff9c1f3d793bd5be0599f2/greenlet-3.5.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:27493374cff1d1b7919dc8126547f2aea582737e3046147b434b1e12de56389b", size = 1581342, upload-time = "2026-08-10T14:15:05.653Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d7/734e5f198888876b42d7616ff6644c075baf6b8a2412deadd6b0e1b8b20c/greenlet-3.5.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:12e2ee66c2aba86133f10fd99d6a8856c6d351ffb7be0e4d52ef2cc5fbb705b2", size = 1645744, upload-time = "2026-08-10T13:40:30.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/30/1f42b88dc587b5899ee50616ad56ee40cafaf225df4fb829f10183c62a5c/greenlet-3.5.5-cp312-cp312-win_amd64.whl", hash = "sha256:49ddacd36af37735fab103846f4ee4d18a492dde72730d1699c0c8ebe30d9f18", size = 324171, upload-time = "2026-08-10T13:28:44.472Z" }, + { url = "https://files.pythonhosted.org/packages/76/e5/4dee4d8d2e603fe5fdd7b444e63219f7b9bd852c60c6214511c7157cbe88/greenlet-3.5.5-cp312-cp312-win_arm64.whl", hash = "sha256:5f1b1ff4828cdc1aba4266aff814085d04a1d07959287219af021b838b265d52", size = 308362, upload-time = "2026-08-10T13:26:46.839Z" }, + { url = "https://files.pythonhosted.org/packages/fb/3d/8cef5f724ec0d4add2af8961d504535ec60c3cca9e464f6d03bdba29d85b/greenlet-3.5.5-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:b79fd2a5bc099b5e744f34c4c9a58954a5f4cb7529fb4b6e8446057d61b6edaa", size = 294730, upload-time = "2026-08-10T13:27:51.206Z" }, + { url = "https://files.pythonhosted.org/packages/88/4b/8e7aa3f514273aecff30a16ab1bac09ff54cfc7e6860fdd8058c37ff2499/greenlet-3.5.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:634cf15a233a949136879dd388e25d3296e16f3f1e217d2456797b8579ebc6ed", size = 614536, upload-time = "2026-08-10T14:14:36.589Z" }, + { url = "https://files.pythonhosted.org/packages/85/48/4e95e9dd5a8a397dc6a6345dd7f1935113d0fca4f85e89d3976da9cd988d/greenlet-3.5.5-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:499adea519f748407fc6806d20eedabac2884fd73b9f38d81236e190ba20dfef", size = 626924, upload-time = "2026-08-10T14:27:27.048Z" }, + { url = "https://files.pythonhosted.org/packages/0e/84/eaa476d6bf3816828d0d70e80dcc36bf30a058233bd889e707e693f6e860/greenlet-3.5.5-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7278591501941bb2456af102bb9cd59aab48c6cfd6e2dd68fa1290bb0c49a42", size = 632726, upload-time = "2026-08-10T14:30:09.874Z" }, + { url = "https://files.pythonhosted.org/packages/89/5d/398a1c71fa7a277deeb376c999979de6786f08fc2d5747a0b9d6e11738dd/greenlet-3.5.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2eabb980975cba5b93a95f6f69287d05fc05ac955bfd6a320a7c083eeb52c0b0", size = 623906, upload-time = "2026-08-10T13:40:50.501Z" }, + { url = "https://files.pythonhosted.org/packages/d0/f2/0cc2849ede68579291e9c59b3ab6ec1958f98681cca5b14d8fc75bf674a4/greenlet-3.5.5-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:4dfc7c4470354e7b09184d1a3a985761053a2fd694ddb5b5c80242afc2c8c90b", size = 434966, upload-time = "2026-08-10T14:30:03.729Z" }, + { url = "https://files.pythonhosted.org/packages/04/1b/745450fc5ea9e0cb17d840d248f284db3363de736d362c7d2d883e3eadba/greenlet-3.5.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:03115c2e0a371999bf8ae616aa8d653f96641d4705c457aebaa187276e9f7537", size = 1581430, upload-time = "2026-08-10T14:15:06.853Z" }, + { url = "https://files.pythonhosted.org/packages/d4/29/d51b296e3191bb15d3d81ec375af1909e4466c0f395d744ed475801798a9/greenlet-3.5.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4441153ffba21b90d3ca89fe3d31f5c093ae6c0bf0cfdfc98f54cde22f95b62e", size = 1645684, upload-time = "2026-08-10T13:40:32.133Z" }, + { url = "https://files.pythonhosted.org/packages/12/63/369f1a1625e64e9e31df3963c6044056e3fdfa3fa3fdba3c54ffefa6e987/greenlet-3.5.5-cp313-cp313-win_amd64.whl", hash = "sha256:95c5b1f4b3a193f8a0c2de4bfdcb48d119f7f1063941f1de1f2168051b3e52dd", size = 324075, upload-time = "2026-08-10T13:26:58.974Z" }, + { url = "https://files.pythonhosted.org/packages/45/78/649cb5c09d4d81f6dd1444e75474a7206784743283a21d24171562ac4899/greenlet-3.5.5-cp313-cp313-win_arm64.whl", hash = "sha256:1af90aa4bc129883b340cdd6957a3bc74f60528a4993bbd1f53aaebe1d9981cc", size = 308260, upload-time = "2026-08-10T13:27:50.795Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8c/080e881fa2be95ff1ddbd6994b2bab3b1a78df3b3fcab39306011764fcc7/greenlet-3.5.5-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d4a389a852e392a6366058651a20fa5ba40d979865aa81bea2ccbdc44805070d", size = 295309, upload-time = "2026-08-10T13:26:03.032Z" }, + { url = "https://files.pythonhosted.org/packages/25/cc/0ac614e6586c0e42d4cc281a5819150f4f43685744a4c5ff77139286409d/greenlet-3.5.5-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70b157cd319873e8b544ddc2de158f55bbd0a9b0218c8ce9332039801518e328", size = 661185, upload-time = "2026-08-10T14:14:37.867Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b9/6808725354be8ad305dfe5172377664fc9642d4fc043be246b3314cf4482/greenlet-3.5.5-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8bdfd1424abcf26832961e766570cae79efdb9599d709088c9cb6ef82b194926", size = 673419, upload-time = "2026-08-10T14:27:28.652Z" }, + { url = "https://files.pythonhosted.org/packages/eb/52/f005d579acde46c3d1cc3cab1c9f3d5708c8a3006a4120e8cf5da801afe9/greenlet-3.5.5-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d98ef6f92e67c6dbf299dbfd8facc1b0d2d9cedf91e325e73b3d0373fe4309d8", size = 677863, upload-time = "2026-08-10T14:30:11.663Z" }, + { url = "https://files.pythonhosted.org/packages/42/2e/40c509967da7f254680826a2fa0dd22138ec79946c70b97542d74cde8b43/greenlet-3.5.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:182de51c6b572a705f2fafaab2e783bcf7d2760940229dfe73086cbae037af3e", size = 670822, upload-time = "2026-08-10T13:40:51.833Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8a/a75f8a2bdcef3c358a3147cdc9db3aa83755f0a038f766ab0bedb66f512c/greenlet-3.5.5-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:159df1942d88e8f784cbb38d6f18bdb365cd11319cfbb3e89623de2b97892d53", size = 480554, upload-time = "2026-08-10T14:30:05.171Z" }, + { url = "https://files.pythonhosted.org/packages/2d/22/c3c2eee4a8fe191d6d1d183086c56133d646024e3d70bfd414829f64560b/greenlet-3.5.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8fec3f165dfe332e490c3247c0f6c23b0bfc45f06496ad7f00ddb00e3d35e4dc", size = 1628469, upload-time = "2026-08-10T14:15:08.11Z" }, + { url = "https://files.pythonhosted.org/packages/f7/87/25babd09b94cb1f03e71db815fde463f0262e40cfbd953d58a8d77311351/greenlet-3.5.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c6ce25fee6cabc8bf22cb8b52e642cbb821be5b9aec8094d07ff03378141b8e9", size = 1691952, upload-time = "2026-08-10T13:40:33.502Z" }, + { url = "https://files.pythonhosted.org/packages/2e/3d/5cc9701117ea4dc0eb7bf1f4f9b7888a6e2e5277ddfae095805ace50f2b6/greenlet-3.5.5-cp314-cp314-win_amd64.whl", hash = "sha256:7dffc5c859fe6059974df1e37d7923d654a83e2ae18fdd616994270e001115e1", size = 327458, upload-time = "2026-08-10T13:27:02.868Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6b/594fa2de7fae7629168a404a4305d7d7e31a5742c50a801b1839543cb93d/greenlet-3.5.5-cp314-cp314-win_arm64.whl", hash = "sha256:5e2afcfc4d4305dd715809b03da5cbe437c8984f61d8917751eb5fe4aefa3e07", size = 311146, upload-time = "2026-08-10T13:27:25.046Z" }, + { url = "https://files.pythonhosted.org/packages/24/e0/50cd600b469e5734c72709b6b1838b6bc63f307b573c772c3132d6ecfe92/greenlet-3.5.5-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:0e5a7de979d764aea1f5b6e95cf92b5b37741b9823702041f34b126e7f690277", size = 305471, upload-time = "2026-08-10T13:26:20.568Z" }, + { url = "https://files.pythonhosted.org/packages/75/a3/77acd66dfc6387b5219b2080806c0cabb73c10eb1bb44b413c40a62015ba/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fef01bd457f11fc158b130ca0027a3c365693280e8e231b65bdaf57999f39f5b", size = 672470, upload-time = "2026-08-10T14:14:39.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/71/0d178142dca3ec19f46fb2212ae73d30ad53b9d548dc64804086033a7089/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5173a72310725a74afc82c164f0e52cb8ad0de62f2bb623f24f6c0cc07d80272", size = 679973, upload-time = "2026-08-10T14:27:30.072Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ac/0d7887aa4bbfc9eba075cc428244dfc96f623478454d5ec81180d0d6bd5a/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5e9ec2e7c98e895fcea0c5cc57b2606cf86ece6d0a56578f3eb225e2af4f0387", size = 681587, upload-time = "2026-08-10T14:30:13.519Z" }, + { url = "https://files.pythonhosted.org/packages/6e/31/46eb8567302eaf787abf88d09df014e14ae3baf460af1b8b0efdbd3efcd5/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44f08341873200ba8a60a8bc14ace3d91f1754f7fa7bc66157714a8cd420a476", size = 676634, upload-time = "2026-08-10T13:40:53.004Z" }, + { url = "https://files.pythonhosted.org/packages/4f/18/8d58ba1c429b0383e3219a3d0e0bba241d0444d8ed05b73349953c7d7c7b/greenlet-3.5.5-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:102817506f6090b5176c746a82603341a549b40e5c3d5b72a4c672228a918c41", size = 510175, upload-time = "2026-08-10T14:30:07.047Z" }, + { url = "https://files.pythonhosted.org/packages/a3/e9/b88bbf5b29970cb84172dc2c32aa3e5e579ceb94c808e81c826454138850/greenlet-3.5.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d246c0db9a2513cd45f019ba178ea4d4d4705bd210ee465e2c15d76a1ab13874", size = 1637320, upload-time = "2026-08-10T14:15:09.317Z" }, + { url = "https://files.pythonhosted.org/packages/6d/8c/7631ed29cc6f0392f11830076e172ce4885e70b0bc2c1bce1731176d4b4e/greenlet-3.5.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:72507285b5caa1d17904a3f7c322ca780823a54170a0e04ec3f37bcc60d4db71", size = 1697412, upload-time = "2026-08-10T13:40:34.924Z" }, + { url = "https://files.pythonhosted.org/packages/da/0f/f7dd935f9c4cb1be49098770587f54d8a78518e55c89bce86c4fb4109057/greenlet-3.5.5-cp314-cp314t-win_amd64.whl", hash = "sha256:7805655781fb8f28a55d05fe57ed61f5f10f1892fb587673e3bb5264f28041f0", size = 331514, upload-time = "2026-08-10T13:29:20.611Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e5/681b01f8fbc1b55232822f99e8f8afeb78a55a7c76a7bf9dbdc7ccb03a6d/greenlet-3.5.5-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:c0db80fcd5b8aece93f66c64f78a786bbb6b96c5fe63ef5a5a4581ecf8bab206", size = 295975, upload-time = "2026-08-10T13:28:45.985Z" }, + { url = "https://files.pythonhosted.org/packages/11/f2/69b488cd9e7267bf4b0fe8cdebf25d8d6df680d21bdf41150d23e23d6652/greenlet-3.5.5-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b241c32f912ada659808d68e308c568baf577eebf757d15471472de0c18cfad", size = 666823, upload-time = "2026-08-10T14:14:40.222Z" }, + { url = "https://files.pythonhosted.org/packages/84/d4/d5bc2fdebbdda0c94555925ba79948b8395d75a7f6a36cc85dce5bab9f11/greenlet-3.5.5-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ef6a08349401d8eaf3cb12688ac8557de95788556b8631ef17555a4a173022c0", size = 677613, upload-time = "2026-08-10T14:27:31.543Z" }, + { url = "https://files.pythonhosted.org/packages/65/53/4e13642efc4d7ad6554ecb2242a5be42666b2e1a067323e88dfc0124a04b/greenlet-3.5.5-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37faa97daccb6d9f4c2141ce3118d023c3c5506864a7d8bdf726f665018c1f76", size = 681436, upload-time = "2026-08-10T14:30:14.839Z" }, + { url = "https://files.pythonhosted.org/packages/bd/93/542d8a3a90f3b35c6ad8bf7e56a03010287f2cafa289a5b7985b5207db39/greenlet-3.5.5-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f2e3d061b8e13aec2f0441689b3c71b244a20e5d274a52cb0f7e31bd1d139552", size = 675930, upload-time = "2026-08-10T13:40:54.205Z" }, + { url = "https://files.pythonhosted.org/packages/cd/32/188447c9a468d6977d2989397226b0c6b65ab6f4cf943f931643328512fc/greenlet-3.5.5-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:b18007dc2473a7942fd157366b55f01da6fed7ce85318591005b419e0a439474", size = 487404, upload-time = "2026-08-10T14:30:08.903Z" }, + { url = "https://files.pythonhosted.org/packages/52/b5/89c9f2e8460d71101037d47a1feed11928615a5edd42370be290e0657eeb/greenlet-3.5.5-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:9ab5f5b93655e77fe0d6c2dfd22b5eac751bb1f876d8ec21761b7c1fb9266007", size = 1633878, upload-time = "2026-08-10T14:15:10.693Z" }, + { url = "https://files.pythonhosted.org/packages/b8/60/297de93f3b02ac78a5e04d32bb8bbe3080f4a73d8ed95016561463b70618/greenlet-3.5.5-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f0e5a21bd4452a88cf032fc43c4a5b307ab1380eacb63b5988f9c0317885e773", size = 1696597, upload-time = "2026-08-10T13:40:36.252Z" }, + { url = "https://files.pythonhosted.org/packages/18/25/54c6eaff4f337fb670215e89eb2d00d9499487b658e709d4b477be4a342e/greenlet-3.5.5-cp315-cp315-win_amd64.whl", hash = "sha256:469dbb0a78625642f4a626cfd0c6e8bccc0385b5e49189b6308bbe849ec88a8e", size = 327700, upload-time = "2026-08-10T13:28:06.752Z" }, + { url = "https://files.pythonhosted.org/packages/67/67/857e88a36301caa0e029870132c2478bd55d896630321432afab03a3115f/greenlet-3.5.5-cp315-cp315-win_arm64.whl", hash = "sha256:2d57406c3efd32d7a81e17a674314e8bd00792cdab49ea3228a49aa1bfb2e769", size = 311750, upload-time = "2026-08-10T13:34:08.815Z" }, + { url = "https://files.pythonhosted.org/packages/10/e2/3144c0a116067ac1e30457b0139a94d60d1d36a86e015de68e9ac87cb3bc/greenlet-3.5.5-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:68184dfcf50ccaa8e864770fe0633a7e27250ea9329f8192ef47ee9ecfd78e1c", size = 306387, upload-time = "2026-08-10T13:27:00.897Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a1/cb4223a7e9b9f43b8807e8eb212358bfe2dfaa174a9ea2889eb1714dcba2/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ec0dc0e59dc9c61af5c47348365ccbbd7addfafe0a93b00336ff3da2907bdc6", size = 676472, upload-time = "2026-08-10T14:14:41.417Z" }, + { url = "https://files.pythonhosted.org/packages/9e/cd/a154b4498e5d8f12ada291cfb3b8d596eadde2177f5bf09a9be699d2a446/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e604f58e35833fc46ef20302bcb314dddbfd3fcf33a4f936216d51dd678d63ae", size = 684238, upload-time = "2026-08-10T14:27:32.946Z" }, + { url = "https://files.pythonhosted.org/packages/ce/f4/e450a68a152f819491d8c7df6a8254e761d87e6a78759268961f8c5bd4dd/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2888a3a38bc5ee5bb6c438372197152e815837e4fab7ed7a1f86ef18ffd58ad1", size = 686022, upload-time = "2026-08-10T14:30:15.96Z" }, + { url = "https://files.pythonhosted.org/packages/bf/bb/b0031d260c2968a3c87deebc51d80c64e499377f993aafe06ee3b7488cc2/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:40239b5384f96da3963585cc6d7eaa9b56f8ae67e8d92cc82dd9e202fc847de3", size = 681246, upload-time = "2026-08-10T13:40:55.402Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/17e63d6bf3b9c9b9dbea981b7f643a71f79603bdfb4f1c3a9cf353e22aed/greenlet-3.5.5-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:1e8d9391fe77f15649589a907cef972dbbd6352ef7ff7dc0492f658c0c26495f", size = 516951, upload-time = "2026-08-10T14:30:10.907Z" }, + { url = "https://files.pythonhosted.org/packages/9a/07/da554b71ab88e649da146e1065d86a48a5c5d92e50ab74ef41b504aa7f56/greenlet-3.5.5-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:a1eaccf5c3a1d3e46dead602c72e6836731e8e245c9de6a27764567b6b62d4c0", size = 1642735, upload-time = "2026-08-10T14:15:11.92Z" }, + { url = "https://files.pythonhosted.org/packages/78/76/26a3782a051677668af9d92beaa47cd87ba9dd5072f762961144a03dd4c6/greenlet-3.5.5-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:19e4e026fe20691f333b8eb1a3bc9625eceba8c3f9d62ec5a6f8581afbc6b5a5", size = 1700925, upload-time = "2026-08-10T13:40:37.656Z" }, + { url = "https://files.pythonhosted.org/packages/28/d9/fe7baf4190c2ae71f267efb9de21b3172bb35bc0ed1ef53dd6027d658e33/greenlet-3.5.5-cp315-cp315t-win_amd64.whl", hash = "sha256:712aee154f648bde84634654bb38bb78c69ac640c37a45c9effed800735049d8", size = 331829, upload-time = "2026-08-10T13:26:48.851Z" }, + { url = "https://files.pythonhosted.org/packages/df/af/419a4e383bd600858a9b67e9b280a60fdc383ee3f2fe5b6c0c1ef04e74d1/greenlet-3.5.5-cp315-cp315t-win_arm64.whl", hash = "sha256:7f049911ee81a16a03c33d5450d8d5867d27f596ca5fb201b86f4524e874468b", size = 315093, upload-time = "2026-08-10T13:29:34.949Z" }, +] + [[package]] name = "griffelib" version = "2.1.0" @@ -1482,6 +1549,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] +[[package]] +name = "oa-configurator" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "pydantic" }, + { name = "rich" }, + { name = "sqlalchemy" }, + { name = "tomli-w" }, + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/bf/8934b30a7ffb24b17dd05fe39a85cf30d47123e3cbe093ce964797a33a43/oa_configurator-1.0.0.tar.gz", hash = "sha256:5ab2a0ba7d2b723312e02422a937047a792c3cba1c4ae59fbd4df53ec0955209", size = 156038, upload-time = "2026-08-11T12:58:39.487Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/84/30fd96eb7a83e9e6ef0d911e402002c3ece45541a8d7892be2799152b114/oa_configurator-1.0.0-py3-none-any.whl", hash = "sha256:9e3ae9e904bdf92201c22df851b8bcc47ad66f206a8fafcd79875b7b27670beb", size = 52209, upload-time = "2026-08-11T12:58:37.91Z" }, +] + [[package]] name = "ollama" version = "0.6.2" @@ -1501,6 +1585,8 @@ source = { editable = "." } dependencies = [ { name = "any-llm-sdk", extra = ["gemini", "ollama"] }, { name = "httpx" }, + { name = "oa-configurator" }, + { name = "ollama" }, { name = "pydantic" }, ] @@ -1525,7 +1611,7 @@ instructor = [ [package.metadata] requires-dist = [ - { name = "any-llm-sdk", extras = ["gemini", "ollama"], specifier = ">=1.22.0" }, + { name = "any-llm-sdk", extras = ["gemini", "ollama"], specifier = ">=1.22.0,<2.0.0" }, { name = "httpx" }, { name = "instructor", marker = "extra == 'dev'", specifier = ">=1.13.0" }, { name = "instructor", marker = "extra == 'instructor'", specifier = ">=1.13.0" }, @@ -1534,6 +1620,8 @@ requires-dist = [ { name = "mkdocs-mermaid2-plugin", marker = "extra == 'dev'" }, { name = "mkdocstrings", extras = ["python"], marker = "extra == 'dev'" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.19.1" }, + { name = "oa-configurator", specifier = ">=1.0.0,<2.0.0" }, + { name = "ollama" }, { name = "pydantic" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0.2" }, { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=1.0.0" }, @@ -2094,6 +2182,41 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0f/2c/437fe806897c2d6cfdc3ee43a18da8bf8e568530a4ae9bac781541ca9896/soupsieve-2.9.1-py3-none-any.whl", hash = "sha256:4f4477399246b7a0c720a88ca2454b11cd6bb9ae4c9d170140786e916776c14c", size = 37404, upload-time = "2026-07-21T16:57:16.421Z" }, ] +[[package]] +name = "sqlalchemy" +version = "2.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/21/77b4c147963073040dc3c3a5cb7a8c3001a1893c0209432cb77f9df836aa/sqlalchemy-2.0.52.tar.gz", hash = "sha256:5e2d46356ac2ccb7d268ab6c2319ac6a2b42f1b8d5fd8bd3d46855cd82abee97", size = 9945637, upload-time = "2026-08-11T19:07:09.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/d5/1b77a026d161f98a08f11af1a5f6c47b98ee7c7e2648af525a1004826c78/sqlalchemy-2.0.52-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:be8c49131665dfe2cc74c498aa1240ffb548d0fd901325dd11c2c7a18956f727", size = 2170940, upload-time = "2026-08-11T20:58:11.25Z" }, + { url = "https://files.pythonhosted.org/packages/54/bd/f444444adb37b5d53753fb1730ee7a421628e2e3b756c4da461af7e6394a/sqlalchemy-2.0.52-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b2d9e507a458832adcfbd8af6e2036ddf069b7710b799448542ebccae2dceee", size = 3383415, upload-time = "2026-08-11T21:02:38.534Z" }, + { url = "https://files.pythonhosted.org/packages/be/57/2eadf93a552568c57e8680b7e58bb5e9770d80942a1bdbaf4f2f63f0d7c8/sqlalchemy-2.0.52-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8738008376d22f30f411ea3efecf39b51110b6996d80bb73786f30bcfdd5fd3b", size = 3398577, upload-time = "2026-08-11T21:16:59.092Z" }, + { url = "https://files.pythonhosted.org/packages/15/c3/2887cf9dd111d1fbf05d22165b404c221ef43e029f7a2695e7302f27a7cc/sqlalchemy-2.0.52-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37a4d548327b6cab9c7d8cdb4e0e82feabee0110c4d150059068e2d1cfbd99ee", size = 3328225, upload-time = "2026-08-11T21:02:40.183Z" }, + { url = "https://files.pythonhosted.org/packages/02/0f/466bdf9e1feeeef5587f868c187d8687e21ff8c85b1775e9041130181132/sqlalchemy-2.0.52-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e49f51a5d59857a7a0dcaf9469febf7197d9394bd88f00d69c2c4e848112cdbf", size = 3357374, upload-time = "2026-08-11T21:17:01.076Z" }, + { url = "https://files.pythonhosted.org/packages/22/20/5c2b4583904af4173076dda1c9e53c9e2ffc7a702d2efde0216bbacbf7cb/sqlalchemy-2.0.52-cp312-cp312-win32.whl", hash = "sha256:afda3ec521d0517d0de783fc70030775841900896d832de5bbd066549290470e", size = 2129366, upload-time = "2026-08-11T21:14:50.991Z" }, + { url = "https://files.pythonhosted.org/packages/ed/06/543dab8ef62d4e9fb96fb31a30c2b8b14a8763bccf48d428294d6b3041c0/sqlalchemy-2.0.52-cp312-cp312-win_amd64.whl", hash = "sha256:2d5e53e36e37129fe0be8b9d08b6e4052c10a963ee6cda56c8c10dcc194b99ca", size = 2157344, upload-time = "2026-08-11T21:14:52.453Z" }, + { url = "https://files.pythonhosted.org/packages/7f/18/e30c6fe1eca1bf34a39fbdd6066121cc9974c850faf6f349eac563697a26/sqlalchemy-2.0.52-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2eb3c6a64b1bfe6704777cfd504e7b8ad093a5f3e03ce67663a5e6742f294e43", size = 2167724, upload-time = "2026-08-11T20:58:12.679Z" }, + { url = "https://files.pythonhosted.org/packages/d0/56/2e17d161a4f7ecc1c2ffb93e607b4e1898bb551b451b283235acb8f6ce47/sqlalchemy-2.0.52-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:923bb183c1dc64fdf7b717965e3d59938ec4f8b8710b419a21ce403e5da9a9e1", size = 3321189, upload-time = "2026-08-11T21:02:41.932Z" }, + { url = "https://files.pythonhosted.org/packages/cf/b8/8490916e893f3f8d74dc9cc54c078619364999dee37047a188e73abbc852/sqlalchemy-2.0.52-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:651d6d8782e80679e6151707c7b490834d46ada526328895abf567f25e63d29c", size = 3338185, upload-time = "2026-08-11T21:17:02.597Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f7/752cc8ee453da222829b3f5c4613614bf750d97429363b70414fa10478e4/sqlalchemy-2.0.52-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b08cddb8989775e3c88799d86704bdfc3ee6e9846118201aa5997f16f27e3a15", size = 3271698, upload-time = "2026-08-11T21:02:43.963Z" }, + { url = "https://files.pythonhosted.org/packages/51/e6/074ade0c07b9e4c8e8bca46820320ed94df9702afdb6f2af06623068d2e6/sqlalchemy-2.0.52-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ab66fa9618269390d4dfa222f2f2f88f7bc4bf5da13905131b818217db7e8057", size = 3308936, upload-time = "2026-08-11T21:17:04.172Z" }, + { url = "https://files.pythonhosted.org/packages/66/07/557c0d04716705599227945ac14e0a17ad0338e899f37d8c2ddff4dcc663/sqlalchemy-2.0.52-cp313-cp313-win32.whl", hash = "sha256:c63bda077685c85ca513286547a531ba57e7a68cf0a7ed3bafcc2bbd18896f4d", size = 2127308, upload-time = "2026-08-11T21:14:53.879Z" }, + { url = "https://files.pythonhosted.org/packages/96/4e/226eda27654318ce525d043025221f689abef883da2c7126f9065121618c/sqlalchemy-2.0.52-cp313-cp313-win_amd64.whl", hash = "sha256:9876b09b9f1ce7398b0ffece585c0a911244c53191187341f6bcae640e133751", size = 2153876, upload-time = "2026-08-11T21:14:55.527Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f5/71cb30af58c9b80a4e1fac0b73bb48f86d497a774a6a2eb6d2f1e657bb73/sqlalchemy-2.0.52-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:410d52be41d17f1a236d19520fbe776257dc16516ed06bd16d433311842aefd9", size = 2169537, upload-time = "2026-08-11T20:58:13.855Z" }, + { url = "https://files.pythonhosted.org/packages/4c/93/d07ebd645d1b07b6b5ed63450a70f063a346a7e0f2c8810daf2e532400cb/sqlalchemy-2.0.52-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfe9ce533dbe4d0a2ae1486546619bd30b76bcd670539a44d910361376175f5e", size = 3319606, upload-time = "2026-08-11T21:02:45.829Z" }, + { url = "https://files.pythonhosted.org/packages/ae/5c/290c84c7c2566ecd3b65baaae0fddec9bc33b033b398a06123bb86fbfc6e/sqlalchemy-2.0.52-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:812bae5138bfc0aa46fb0686da0fc7f581f68e2bbb05bc24c3713bebaedd1437", size = 3323642, upload-time = "2026-08-11T21:17:05.675Z" }, + { url = "https://files.pythonhosted.org/packages/13/f5/2cc160590ca49173359557880b92a0572293ccb899e8f6cedf150c5a3ddf/sqlalchemy-2.0.52-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:50bff43b632a56fbf5ed9afdd76307e1512b62051bcd5afb341ae67205bbb6c8", size = 3268125, upload-time = "2026-08-11T21:02:47.649Z" }, + { url = "https://files.pythonhosted.org/packages/35/f3/ea8933fc9f7d1353e9c2ff9965eae687c4cef181120574591ed2fa0633e1/sqlalchemy-2.0.52-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:49565daf5af554f538e23aef1fc81a95a4e49658f152285e45c02f5fc44f04cd", size = 3289516, upload-time = "2026-08-11T21:17:07.267Z" }, + { url = "https://files.pythonhosted.org/packages/45/67/05cf86541c1e1716fca1e4a996954a439cd74501707cda607fb7cb02ef50/sqlalchemy-2.0.52-cp314-cp314-win32.whl", hash = "sha256:ab9da41e61b9979b910499d633b241df20c51ee5037e5405b11c2faac3cbe1a2", size = 2130249, upload-time = "2026-08-11T21:14:57.273Z" }, + { url = "https://files.pythonhosted.org/packages/96/d7/8ac6ffa1e36169e762ef65bd835046abb2251b1bc17f8f6708e14ed8d31f/sqlalchemy-2.0.52-cp314-cp314-win_amd64.whl", hash = "sha256:a593db51b3bae75db17a5738ad5f992244b3a03863f83c28117ee482c6a3f76d", size = 2156718, upload-time = "2026-08-11T21:14:58.667Z" }, + { url = "https://files.pythonhosted.org/packages/dc/4b/e01a737eef378e734cc6394a82248a6ce13b167dfa36c731075ce9fc9c64/sqlalchemy-2.0.52-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1e61d08bdf4ee2f41024569e3400de7d6734ba498144766b11260936ccfa582", size = 2190344, upload-time = "2026-08-11T19:53:21.393Z" }, + { url = "https://files.pythonhosted.org/packages/b3/3f/3582293d1e185e71d19d7c731c3e2ee20ba21981c4a1115c0806c1f62120/sqlalchemy-2.0.52-py3-none-any.whl", hash = "sha256:3b81b8363a919ce53453591cdb93702e6bd54ade6c4fa2f468fc053baee5ed89", size = 1950700, upload-time = "2026-08-11T20:47:21.603Z" }, +] + [[package]] name = "tenacity" version = "9.1.4" @@ -2103,6 +2226,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" }, ] +[[package]] +name = "tomli-w" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, +] + [[package]] name = "tqdm" version = "4.69.1"