From 8bcd6bfbce5d273f0e9003360ce37efc11b2ce21 Mon Sep 17 00:00:00 2001 From: skolton Date: Fri, 17 Apr 2026 13:38:28 +0200 Subject: [PATCH] Add GoogleModel support This PR adds support to Google Models via: - Gemini API - VertexAI The actual backend used depends on the parameters passed to GoogleModel --- .basedpyright/baseline.json | 10 -- pyproject.toml | 3 +- splunklib/ai/README.md | 83 +++++++++++ splunklib/ai/engines/langchain.py | 29 +++- splunklib/ai/model.py | 38 ++++- .../unit/ai/engine/test_langchain_backend.py | 52 ++++++- uv.lock | 137 +++++++++++++++++- 7 files changed, 335 insertions(+), 17 deletions(-) diff --git a/.basedpyright/baseline.json b/.basedpyright/baseline.json index 6b319a36..cb34da59 100644 --- a/.basedpyright/baseline.json +++ b/.basedpyright/baseline.json @@ -72,16 +72,6 @@ } } ], - "./splunklib/ai/model.py": [ - { - "code": "reportDeprecated", - "range": { - "startColumn": 24, - "endColumn": 31, - "lineCount": 1 - } - } - ], "./splunklib/ai/serialized_service.py": [ { "code": "reportPrivateUsage", diff --git a/pyproject.toml b/pyproject.toml index 708f0d14..81e519ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ compat = ["six>=1.17.0"] ai = ["httpx==0.28.1", "langchain>=1.2.15", "mcp>=1.27.0", "pydantic>=2.13.1"] anthropic = ["splunk-sdk[ai]>=2.1.1", "langchain-anthropic>=1.4.0"] openai = ["splunk-sdk[ai]>=2.1.1", "langchain-openai>=1.1.13"] +google = ["splunk-sdk[ai]>=2.1.1", "langchain-google-genai>=4.2.2", "google-auth>=2.0.0"] # Treat the same as NPM's `devDependencies` [dependency-groups] @@ -50,7 +51,7 @@ release = ["build>=1.4.3", "jinja2>=3.1.6", "sphinx>=9.1.0", "twine>=6.2.0"] lint = ["basedpyright>=1.39.0", "ruff>=0.15.10"] dev = [ "rich>=14.3.3", - "splunk-sdk[openai, anthropic]", + "splunk-sdk[openai, anthropic, google]", { include-group = "test" }, { include-group = "lint" }, { include-group = "release" }, diff --git a/splunklib/ai/README.md b/splunklib/ai/README.md index 16610b0e..8d3d9c13 100644 --- a/splunklib/ai/README.md +++ b/splunklib/ai/README.md @@ -47,6 +47,7 @@ We support following predefined models: - `OpenAIModel` - works with OpenAI and any [OpenAI-compatible API](https://platform.openai.com/docs/api-reference). - `AnthropicModel` - works with Anthropic and any [Anthropic-compatible API](https://docs.anthropic.com/en/api). +- `GoogleModel` - works with Google's Gemini models via the [Gemini API](https://ai.google.dev/gemini-api/docs) or [Vertex AI](https://cloud.google.com/vertex-ai/generative-ai/docs/overview). ### OpenAI @@ -76,6 +77,88 @@ model = AnthropicModel( async with Agent(model=model) as agent: .... ``` +### Google + +`GoogleModel` supports two backends: the [Gemini API](https://ai.google.dev/gemini-api/docs) and [Vertex AI](https://cloud.google.com/vertex-ai/generative-ai/docs/overview). +The backend is selected automatically based on the parameters you provide, or you can +force it with the `vertexai` flag. + +Requires the `google` optional extra: + +```sh +pip install "splunk-sdk[google]" +# or with uv: +uv add splunk-sdk[google] +``` + +#### Gemini API + +Use this when you have a Google AI Studio API key and do not need Vertex AI infrastructure. +Only `model` and `api_key` are required. + +```py +from splunklib.ai import Agent, GoogleModel + +model = GoogleModel( + model="gemini-2.0-flash", + api_key="YOUR_GOOGLE_API_KEY", +) + +async with Agent(model=model) as agent: ... +``` + +#### Vertex AI - API key + +Use this to route requests through Vertex AI with an API key. Providing `project` is enough +for the SDK to switch to the Vertex AI backend automatically. + +```py +from splunklib.ai import Agent, GoogleModel + +model = GoogleModel( + model="gemini-2.0-flash", + api_key="YOUR_VERTEX_API_KEY", + project="your-gcp-project-id", + # location="us-central1", # optional, defaults to us-central1 +) + +async with Agent(model=model) as agent: ... +``` + +#### Vertex AI - service account credentials + +Use this when authenticating with a service account key file (or any +`google.auth.credentials.Credentials`-compatible object). No `api_key` is needed. + +```py +from google.oauth2 import service_account +from splunklib.ai import Agent, GoogleModel + +credentials = service_account.Credentials.from_service_account_file( + "path/to/service-account.json", + scopes=["https://www.googleapis.com/auth/cloud-platform"], +) + +model = GoogleModel( + model="gemini-2.0-flash", + project="your-gcp-project-id", + credentials=credentials, + # location="us-central1", # optional, defaults to us-central1 +) + +async with Agent(model=model) as agent: ... +``` + +#### Backend selection rules + +| `project` | `credentials` | `vertexai` | Backend used | +|---|---|---|---| +| not set | not set | `None` (default) | Gemini API | +| set | - | `None` (default) | Vertex AI | +| - | set | `None` (default) | Vertex AI | +| any | any | `True` | Vertex AI (forced) | +| any | any | `False` | Gemini API (forced) | + ### Self-hosted models via Ollama [Ollama](https://ollama.com/) can serve local models with both OpenAI and Anthropic-compatible endpoints, so either model class works. diff --git a/splunklib/ai/engines/langchain.py b/splunklib/ai/engines/langchain.py index 76fa100b..ee854e71 100644 --- a/splunklib/ai/engines/langchain.py +++ b/splunklib/ai/engines/langchain.py @@ -109,7 +109,7 @@ subagent_middleware, tool_middleware, ) -from splunklib.ai.model import AnthropicModel, OpenAIModel, PredefinedModel +from splunklib.ai.model import AnthropicModel, GoogleModel, OpenAIModel, PredefinedModel from splunklib.ai.security import create_structured_prompt from splunklib.ai.structured_output import ( StructuredOutputGenerationException, @@ -1694,6 +1694,33 @@ def _create_langchain_model(model: PredefinedModel) -> BaseChatModel: + "# or if using uv:\n" + "uv add splunk-sdk[anthropic]" ) + case GoogleModel(): + try: + from langchain_google_genai import ChatGoogleGenerativeAI + + google_kwargs: dict[str, Any] = {"model": model.model} + if model.api_key is not None: + google_kwargs["google_api_key"] = model.api_key + if model.project is not None: + google_kwargs["project"] = model.project + if model.location is not None: + google_kwargs["location"] = model.location + if model.credentials is not None: + google_kwargs["credentials"] = model.credentials + if model.vertexai is not None: + google_kwargs["vertexai"] = model.vertexai + if model.temperature is not None: + google_kwargs["temperature"] = model.temperature + + return ChatGoogleGenerativeAI(**google_kwargs) + except ImportError: + raise ImportError( + "Google GenAI support is not installed.\n" + + "To enable Google / Gemini models, install the optional extra:\n" + + 'pip install "splunk-sdk[google]"\n' + + "# or if using uv:\n" + + "uv add splunk-sdk[google]" + ) case _: raise InvalidModelError( "Cannot create langchain model - invalid SDK model provided" diff --git a/splunklib/ai/model.py b/splunklib/ai/model.py index c701f5d0..9de7062f 100644 --- a/splunklib/ai/model.py +++ b/splunklib/ai/model.py @@ -12,11 +12,15 @@ # License for the specific language governing permissions and limitations # under the License. +from collections.abc import Mapping from dataclasses import dataclass -from typing import Any, Mapping +from typing import TYPE_CHECKING, Any import httpx +if TYPE_CHECKING: + from google.oauth2 import service_account + @dataclass(frozen=True) class PredefinedModel: @@ -63,8 +67,40 @@ class AnthropicModel(PredefinedModel): temperature: float | None = None +@dataclass(frozen=True) +class GoogleModel(PredefinedModel): + """Predefined Google Model + + Supports the Gemini API and Vertex AI. The backend is chosen + automatically: Vertex AI when ``project`` or ``credentials`` is set, + otherwise the Gemini API. Override with ``vertexai=True/False``. + + See the README for full usage examples and authentication options. + """ + + model: str + api_key: str | None = None + """API key for the Gemini API or Vertex AI.""" + + project: str | None = None + """Google Cloud project ID (Vertex AI only).""" + + location: str | None = None + """Vertex AI region, e.g. ``"us-central1"`` or ``"europe-west4"``.""" + + credentials: "service_account.Credentials | None" = None + """Service account credentials for Vertex AI. When set, ``api_key`` is not required.""" + + vertexai: bool | None = None + """Force backend selection: ``True`` for Vertex AI, ``False`` for Gemini API, ``None`` to auto-detect.""" + + temperature: float | None = None + """Sampling temperature in the range ``[0.0, 2.0]``.""" + + __all__ = [ "AnthropicModel", + "GoogleModel", "OpenAIModel", "PredefinedModel", ] diff --git a/tests/unit/ai/engine/test_langchain_backend.py b/tests/unit/ai/engine/test_langchain_backend.py index c02426bd..447ddf43 100644 --- a/tests/unit/ai/engine/test_langchain_backend.py +++ b/tests/unit/ai/engine/test_langchain_backend.py @@ -39,7 +39,7 @@ ToolMessage, ToolResult, ) -from splunklib.ai.model import AnthropicModel, OpenAIModel, PredefinedModel +from splunklib.ai.model import AnthropicModel, GoogleModel, OpenAIModel, PredefinedModel from splunklib.ai.tools import ToolType @@ -387,6 +387,56 @@ def test_create_langchain_model_anthropic_with_base_url(self) -> None: # ChatAnthropic stores base_url in anthropic_api_url assert result.anthropic_api_url == model.base_url + def test_create_langchain_model_google_gemini_api(self) -> None: + pytest.importorskip("langchain_google_genai") + import langchain_google_genai + + model = GoogleModel(model="gemini-2.0-flash", api_key="test-key") + result = lc._create_langchain_model(model) + + assert isinstance(result, langchain_google_genai.ChatGoogleGenerativeAI) + assert result.model == model.model + assert result._use_vertexai is False # pyright: ignore[reportAttributeAccessIssue] + + def test_create_langchain_model_google_vertex_ai_via_project(self) -> None: + pytest.importorskip("langchain_google_genai") + import langchain_google_genai + + model = GoogleModel( + model="gemini-2.0-flash", + api_key="test-key", + project="my-project", + ) + result = lc._create_langchain_model(model) + + assert isinstance(result, langchain_google_genai.ChatGoogleGenerativeAI) + assert result.project == model.project + assert result._use_vertexai is True # pyright: ignore[reportAttributeAccessIssue] + + def test_create_langchain_model_google_vertex_ai_explicit_flag(self) -> None: + pytest.importorskip("langchain_google_genai") + import langchain_google_genai + + model = GoogleModel( + model="gemini-2.0-flash", + api_key="test-key", + vertexai=True, + ) + result = lc._create_langchain_model(model) + + assert isinstance(result, langchain_google_genai.ChatGoogleGenerativeAI) + assert result._use_vertexai is True # pyright: ignore[reportAttributeAccessIssue] + + def test_create_langchain_model_google_temperature(self) -> None: + pytest.importorskip("langchain_google_genai") + import langchain_google_genai + + model = GoogleModel(model="gemini-2.0-flash", api_key="test-key", temperature=0.5) + result = lc._create_langchain_model(model) + + assert isinstance(result, langchain_google_genai.ChatGoogleGenerativeAI) + assert result.temperature == model.temperature + @pytest.mark.parametrize( ("name", "tool_type", "expected_name"), diff --git a/uv.lock b/uv.lock index 348d058a..0c121bb7 100644 --- a/uv.lock +++ b/uv.lock @@ -376,6 +376,54 @@ 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 = "filetype" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/29/745f7d30d47fe0f251d3ad3dc2978a23141917661998763bebb6da007eb1/filetype-1.2.0.tar.gz", hash = "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb", size = 998020, upload-time = "2022-11-02T17:34:04.141Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/79/1b8fa1bb3568781e84c9200f951c735f3f157429f44be0495da55894d620/filetype-1.2.0-py2.py3-none-any.whl", hash = "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25", size = 19970, upload-time = "2022-11-02T17:34:01.425Z" }, +] + +[[package]] +name = "google-auth" +version = "2.49.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyasn1-modules" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/fc/e925290a1ad95c975c459e2df070fac2b90954e13a0370ac505dff78cb99/google_auth-2.49.2.tar.gz", hash = "sha256:c1ae38500e73065dcae57355adb6278cf8b5c8e391994ae9cbadbcb9631ab409", size = 333958, upload-time = "2026-04-10T00:41:21.888Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/76/d241a5c927433420507215df6cac1b1fa4ac0ba7a794df42a84326c68da8/google_auth-2.49.2-py3-none-any.whl", hash = "sha256:c2720924dfc82dedb962c9f52cabb2ab16714fd0a6a707e40561d217574ed6d5", size = 240638, upload-time = "2026-04-10T00:41:14.501Z" }, +] + +[package.optional-dependencies] +requests = [ + { name = "requests" }, +] + +[[package]] +name = "google-genai" +version = "1.73.1" +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/3d/d8/40f5f107e5a2976bbac52d421f04d14fc221b55a8f05e66be44b2f739fe6/google_genai-1.73.1.tar.gz", hash = "sha256:b637e3a3b9e2eccc46f27136d470165803de84eca52abfed2e7352081a4d5a15", size = 530998, upload-time = "2026-04-14T21:06:19.153Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/af/508e0528015240d710c6763f7c89ff44fab9a94a80b4377e265d692cbfd6/google_genai-1.73.1-py3-none-any.whl", hash = "sha256:af2d2287d25e42a187de19811ef33beb2e347c7e2bdb4dc8c467d78254e43a2c", size = 783595, upload-time = "2026-04-14T21:06:17.464Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -678,6 +726,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/46/e988e9f024e762750f9f53878316980bdaea2ab1f19600df01a7c39eda89/langchain_core-1.2.30-py3-none-any.whl", hash = "sha256:26fa50894449b29b31b3712fa4975db679d26abe8241a966ea2c5978b68d8394", size = 513005, upload-time = "2026-04-15T20:37:12.396Z" }, ] +[[package]] +name = "langchain-google-genai" +version = "4.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filetype" }, + { name = "google-genai" }, + { name = "langchain-core" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/78/dfe068937338727b0dee637d971d59fe2fa275f9d0f0edee3fa80e811846/langchain_google_genai-4.2.2.tar.gz", hash = "sha256:5fc774bf41d1dc1c1a5ba8d7b9f2017dfa77e30653c9b44d2dfbaf0e877e7388", size = 267457, upload-time = "2026-04-15T15:08:32.18Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/5c/adf81d68ab89b4cf505e690f8c1956d11b5969c831c951c7b4b1b1818080/langchain_google_genai-4.2.2-py3-none-any.whl", hash = "sha256:c8d09aac0304d26f1c2483e41a350f15587af1fbe034c39a304e1e17a3b743f3", size = 67605, upload-time = "2026-04-15T15:08:31.346Z" }, +] + [[package]] name = "langchain-openai" version = "1.1.13" @@ -1030,6 +1093,27 @@ 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 = "pyasn1" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685, upload-time = "2026-03-17T01:06:53.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + [[package]] name = "pycparser" version = "3.0" @@ -1665,6 +1749,14 @@ anthropic = [ compat = [ { name = "six" }, ] +google = [ + { name = "google-auth" }, + { name = "httpx" }, + { name = "langchain" }, + { name = "langchain-google-genai" }, + { name = "mcp" }, + { name = "pydantic" }, +] openai = [ { name = "httpx" }, { name = "langchain" }, @@ -1685,7 +1777,7 @@ dev = [ { name = "rich" }, { name = "ruff" }, { name = "sphinx" }, - { name = "splunk-sdk", extra = ["ai", "anthropic", "openai"] }, + { name = "splunk-sdk", extra = ["ai", "anthropic", "google", "openai"] }, { name = "twine" }, ] lint = [ @@ -1708,17 +1800,20 @@ test = [ [package.metadata] requires-dist = [ + { name = "google-auth", marker = "extra == 'google'", specifier = ">=2.0.0" }, { name = "httpx", marker = "extra == 'ai'", specifier = "==0.28.1" }, { name = "langchain", marker = "extra == 'ai'", specifier = ">=1.2.15" }, { name = "langchain-anthropic", marker = "extra == 'anthropic'", specifier = ">=1.4.0" }, + { name = "langchain-google-genai", marker = "extra == 'google'", specifier = ">=4.2.2" }, { name = "langchain-openai", marker = "extra == 'openai'", specifier = ">=1.1.13" }, { name = "mcp", marker = "extra == 'ai'", specifier = ">=1.27.0" }, { name = "pydantic", marker = "extra == 'ai'", specifier = ">=2.13.1" }, { name = "six", marker = "extra == 'compat'", specifier = ">=1.17.0" }, { name = "splunk-sdk", extras = ["ai"], marker = "extra == 'anthropic'", specifier = ">=2.1.1" }, + { name = "splunk-sdk", extras = ["ai"], marker = "extra == 'google'", specifier = ">=2.1.1" }, { name = "splunk-sdk", extras = ["ai"], marker = "extra == 'openai'", specifier = ">=2.1.1" }, ] -provides-extras = ["compat", "ai", "anthropic", "openai"] +provides-extras = ["compat", "ai", "anthropic", "openai", "google"] [package.metadata.requires-dev] dev = [ @@ -1733,7 +1828,7 @@ dev = [ { name = "ruff", specifier = ">=0.15.10" }, { name = "sphinx", specifier = ">=9.1.0" }, { name = "splunk-sdk", extras = ["ai"] }, - { name = "splunk-sdk", extras = ["openai", "anthropic"] }, + { name = "splunk-sdk", extras = ["openai", "anthropic", "google"] }, { name = "twine", specifier = ">=6.2.0" }, ] lint = [ @@ -1925,6 +2020,42 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0a/89/f8827ccff89c1586027a105e5630ff6139a64da2515e24dafe860bd9ae4d/uvicorn-0.42.0-py3-none-any.whl", hash = "sha256:96c30f5c7abe6f74ae8900a70e92b85ad6613b745d4879eb9b16ccad15645359", size = 68830, upload-time = "2026-03-16T06:19:48.325Z" }, ] +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] + [[package]] name = "xxhash" version = "3.6.0"