From 7d681be02bded3121ea95a46e0431977e5a275ec Mon Sep 17 00:00:00 2001 From: charles Date: Wed, 9 Jul 2025 21:47:41 -0700 Subject: [PATCH] Add vertex ai support --- app/api/schemas/provider_key.py | 42 +-- app/exceptions/exceptions.py | 38 ++- app/main.py | 39 ++- app/services/providers/adapter_factory.py | 4 + app/services/providers/anthropic_adapter.py | 75 +++-- app/services/providers/base.py | 41 ++- app/services/providers/vertex_adapter.py | 216 ++++++++++++++ pyproject.toml | 2 + uv.lock | 308 ++++++++++++++++++++ 9 files changed, 693 insertions(+), 72 deletions(-) create mode 100644 app/services/providers/vertex_adapter.py diff --git a/app/api/schemas/provider_key.py b/app/api/schemas/provider_key.py index aaaac2f..534fec4 100644 --- a/app/api/schemas/provider_key.py +++ b/app/api/schemas/provider_key.py @@ -9,46 +9,6 @@ logger = get_logger(name="provider_key") -# Constants for API key masking -API_KEY_MASK_PREFIX_LENGTH = 2 -API_KEY_MASK_SUFFIX_LENGTH = 4 -# Minimum length to apply the full prefix + suffix mask (e.g., pr******fix) -# This means if length is > (PREFIX + SUFFIX), we can apply the full rule. -MIN_KEY_LENGTH_FOR_FULL_MASK_LOGIC = ( - API_KEY_MASK_PREFIX_LENGTH + API_KEY_MASK_SUFFIX_LENGTH -) - - -# Helper function for masking API keys -def _mask_api_key_value(value: str | None) -> str | None: - if not value: - return None - - length = len(value) - - if length == 0: - return "" - - # If key is too short for any meaningful prefix/suffix masking - if length <= API_KEY_MASK_PREFIX_LENGTH: - return "*" * length - - # If key is long enough for prefix, but not for prefix + suffix - # e.g., length is 3, 4, 5, 6. For these, show prefix and mask the rest. - if length <= MIN_KEY_LENGTH_FOR_FULL_MASK_LOGIC: - return value[:API_KEY_MASK_PREFIX_LENGTH] + "*" * ( - length - API_KEY_MASK_PREFIX_LENGTH - ) - - # If key is long enough for the full prefix + ... + suffix mask - # number of asterisks = length - prefix_length - suffix_length - num_asterisks = length - API_KEY_MASK_PREFIX_LENGTH - API_KEY_MASK_SUFFIX_LENGTH - return ( - value[:API_KEY_MASK_PREFIX_LENGTH] - + "*" * num_asterisks - + value[-API_KEY_MASK_SUFFIX_LENGTH:] - ) - class ProviderKeyBase(BaseModel): provider_name: str = Field(..., min_length=1) @@ -108,7 +68,7 @@ def api_key(self) -> str | None: api_key, _ = provider_adapter_cls.deserialize_api_key_config( decrypted_value ) - return _mask_api_key_value(api_key) + return provider_adapter_cls.mask_api_key(api_key) except Exception as e: logger.error( f"Error deserializing API key for provider {self.provider_name}: {e}" diff --git a/app/exceptions/exceptions.py b/app/exceptions/exceptions.py index 5e6ebac..8ed511a 100644 --- a/app/exceptions/exceptions.py +++ b/app/exceptions/exceptions.py @@ -3,4 +3,40 @@ class InvalidProviderException(Exception): def __init__(self, identifier: str): self.identifier = identifier - super().__init__(f"Provider {identifier} is invalid or failed to extract provider info from model_id {identifier}") \ No newline at end of file + super().__init__(f"Provider {identifier} is invalid or failed to extract provider info from model_id {identifier}") + + +class ProviderAuthenticationException(Exception): + """Exception raised when a provider authentication fails.""" + + def __init__(self, provider_name: str, error: Exception): + self.provider_name = provider_name + self.error = error + super().__init__(f"Provider {provider_name} authentication failed: {error}") + + +class BaseInvalidProviderSetupException(Exception): + """Exception raised when a provider setup is invalid.""" + + def __init__(self, provider_name: str, error: Exception): + self.provider_name = provider_name + self.error = error + super().__init__(f"Provider {provider_name} setup is invalid: {error}") + +class InvalidProviderConfigException(BaseInvalidProviderSetupException): + """Exception raised when a provider config is invalid.""" + + def __init__(self, provider_name: str, error: Exception): + super().__init__(provider_name, error) + +class InvalidProviderAPIKeyException(BaseInvalidProviderSetupException): + """Exception raised when a provider API key is invalid.""" + + def __init__(self, provider_name: str, error: Exception): + super().__init__(provider_name, error) + +class ProviderAPIException(Exception): + """Exception raised when a provider API error occurs.""" + + def __init__(self, provider_name: str, error_code: int, error_message: str): + super().__init__(f"Provider {provider_name} API error: {error_code} {error_message}") diff --git a/app/main.py b/app/main.py index 69b6047..112ba6e 100644 --- a/app/main.py +++ b/app/main.py @@ -3,7 +3,7 @@ from collections.abc import Callable from dotenv import load_dotenv -from fastapi import APIRouter, FastAPI, Request +from fastapi import APIRouter, FastAPI, Request, HTTPException from fastapi.middleware.cors import CORSMiddleware from starlette.middleware.base import BaseHTTPMiddleware @@ -20,6 +20,7 @@ from app.core.database import engine from app.core.logger import get_logger from app.models.base import Base +from app.exceptions.exceptions import ProviderAuthenticationException, InvalidProviderException, BaseInvalidProviderSetupException, ProviderAPIException load_dotenv() @@ -77,6 +78,42 @@ async def dispatch(self, request: Request, call_next: Callable): openapi_url="/openapi.json" if not is_production else None, ) +### Exception handlers block ### + +# Add exception handler for ProviderAuthenticationException +@app.exception_handler(ProviderAuthenticationException) +async def provider_authentication_exception_handler(request: Request, exc: ProviderAuthenticationException): + return HTTPException( + status_code=401, + detail=f"Authentication failed for provider {exc.provider_name}" + ) + +# Add exception handler for InvalidProviderException +@app.exception_handler(InvalidProviderException) +async def invalid_provider_exception_handler(request: Request, exc: InvalidProviderException): + return HTTPException( + status_code=400, + detail=f"Invalid provider: {exc.identifier}" + ) + +# Add exception handler for BaseInvalidProviderSetupException +@app.exception_handler(BaseInvalidProviderSetupException) +async def base_invalid_provider_setup_exception_handler(request: Request, exc: BaseInvalidProviderSetupException): + return HTTPException( + status_code=400, + detail=f"Invalid provider setup: {exc.provider_name}" + ) + +# Add exception handler for ProviderAPIException +@app.exception_handler(ProviderAPIException) +async def provider_api_exception_handler(request: Request, exc: ProviderAPIException): + return HTTPException( + status_code=exc.error_code, + detail=f"Provider API error: {exc.provider_name} {exc.error_code} {exc.error_message}" + ) + +### Exception handlers block ends ### + # Middleware to log slow requests @app.middleware("http") diff --git a/app/services/providers/adapter_factory.py b/app/services/providers/adapter_factory.py index 6a2f0d5..e98bb32 100644 --- a/app/services/providers/adapter_factory.py +++ b/app/services/providers/adapter_factory.py @@ -13,6 +13,7 @@ from .perplexity_adapter import PerplexityAdapter from .tensorblock_adapter import TensorblockAdapter from .zhipu_adapter import ZhipuAdapter +from .vertex_adapter import VertexAdapter class ProviderAdapterFactory: @@ -185,6 +186,9 @@ class ProviderAdapterFactory: "bedrock": { "adapter": BedrockAdapter, }, + "vertex": { + "adapter": VertexAdapter, + }, "customized": { "adapter": OpenAIAdapter, }, diff --git a/app/services/providers/anthropic_adapter.py b/app/services/providers/anthropic_adapter.py index 9cb2c7e..4d39a8f 100644 --- a/app/services/providers/anthropic_adapter.py +++ b/app/services/providers/anthropic_adapter.py @@ -3,14 +3,18 @@ import uuid from collections.abc import AsyncGenerator from http import HTTPStatus -from typing import Any +from typing import Any, Callable import aiohttp +from app.core.logger import get_logger + from .base import ProviderAdapter ANTHROPIC_DEFAULT_MAX_TOKENS = 4096 +logger = get_logger(name="anthropic_adapter") + class AnthropicAdapter(ProviderAdapter): """Adapter for Anthropic API""" @@ -106,22 +110,10 @@ async def list_models(self, api_key: str) -> list[str]: self.cache_models(api_key, self._base_url, models) return models - - async def process_completion( - self, - endpoint: str, - payload: dict[str, Any], - api_key: str, - ) -> Any: - """Process a completion request using Anthropic API""" - headers = { - "x-api-key": api_key, - "Content-Type": "application/json", - "anthropic-version": "2023-06-01", - } - - # Convert OpenAI format to Anthropic format - streaming = payload.get("stream", False) + + @staticmethod + def convert_openai_payload_to_anthropic(payload: dict[str, Any]) -> dict[str, Any]: + """Convert Anthropic completion payload to OpenAI format""" anthropic_payload = { "model": payload["model"], "max_tokens": payload.get("max_completion_tokens", payload.get("max_tokens", ANTHROPIC_DEFAULT_MAX_TOKENS)), @@ -138,7 +130,7 @@ async def process_completion( for msg in payload["messages"]: role = msg["role"] content = msg["content"] - content = self.convert_openai_content_to_anthropic(content) + content = AnthropicAdapter.convert_openai_content_to_anthropic(content) if role == "system": # Anthropic requires a system message to be string @@ -159,6 +151,25 @@ async def process_completion( # Handle regular completion (legacy format) anthropic_payload["prompt"] = f"Human: {payload['prompt']}\n\nAssistant: " + return anthropic_payload + + async def process_completion( + self, + endpoint: str, + payload: dict[str, Any], + api_key: str, + ) -> Any: + """Process a completion request using Anthropic API""" + headers = { + "x-api-key": api_key, + "Content-Type": "application/json", + "anthropic-version": "2023-06-01", + } + + streaming = payload.get("stream", False) + # Convert OpenAI format to Anthropic format + anthropic_payload = self.convert_openai_payload_to_anthropic(payload) + # Choose the appropriate API endpoint - using ternary operator api_endpoint = "messages" if "messages" in anthropic_payload else "complete" @@ -167,17 +178,18 @@ async def process_completion( # Handle streaming requests if streaming and "messages" in anthropic_payload: anthropic_payload["stream"] = True - return await self._stream_anthropic_response( + return await self.stream_anthropic_response( url, headers, anthropic_payload, payload["model"] ) else: # For non-streaming, use the regular approach - return await self._process_regular_response( + return await self.process_regular_response( url, headers, anthropic_payload, payload["model"] ) - async def _stream_anthropic_response( - self, url, headers, anthropic_payload, model_name + @staticmethod + async def stream_anthropic_response( + url, headers, anthropic_payload, model_name, error_handler: Callable[[str, int], Any] | None = None ): """Handle streaming response from Anthropic API, including usage data.""" @@ -194,9 +206,12 @@ async def stream_response() -> AsyncGenerator[bytes, None]: ): if response.status != HTTPStatus.OK: error_text = await response.text() - raise ValueError( - f"Anthropic API error: {response.status} - {error_text}" - ) + if error_handler: + error_handler(error_text, response.status) + else: + raise ValueError( + f"Anthropic API error: {response.status} - {error_text}" + ) buffer = "" async for line_bytes in response.content: @@ -329,8 +344,9 @@ async def stream_response() -> AsyncGenerator[bytes, None]: return stream_response() - async def _process_regular_response( - self, url, headers, anthropic_payload, model_name + @staticmethod + async def process_regular_response( + url: str, headers: dict[str, str], anthropic_payload: dict[str, Any], model_name: str, error_handler: Callable[[str, int], Any] | None = None ): """Handle regular (non-streaming) response from Anthropic API""" # Single with statement for multiple contexts @@ -340,7 +356,10 @@ async def _process_regular_response( ): if response.status != HTTPStatus.OK: error_text = await response.text() - raise ValueError(f"Anthropic API error: {error_text}") + if error_handler: + error_handler(error_text, response.status) + else: + raise ValueError(f"Anthropic API error: {error_text}") anthropic_response = await response.json() diff --git a/app/services/providers/base.py b/app/services/providers/base.py index 05b6d4c..0ccb392 100644 --- a/app/services/providers/base.py +++ b/app/services/providers/base.py @@ -1,8 +1,16 @@ -import json import time from abc import ABC, abstractmethod from typing import Any, ClassVar +# Constants for API key masking +API_KEY_MASK_PREFIX_LENGTH = 2 +API_KEY_MASK_SUFFIX_LENGTH = 4 +# Minimum length to apply the full prefix + suffix mask (e.g., pr******fix) +# This means if length is > (PREFIX + SUFFIX), we can apply the full rule. +MIN_KEY_LENGTH_FOR_FULL_MASK_LOGIC = ( + API_KEY_MASK_PREFIX_LENGTH + API_KEY_MASK_SUFFIX_LENGTH +) + class ProviderAdapter(ABC): """Base class for all provider adapters""" @@ -66,6 +74,37 @@ def deserialize_api_key_config(serialized_api_key_config: str) -> tuple[str, dic def mask_config(config: dict[str, Any]) -> dict[str, Any]: """Mask the config for the given provider""" return config + + @staticmethod + def mask_api_key(api_key: str) -> str: + """Mask the API key for the given provider""" + if not api_key: + return None + + length = len(api_key) + + if length == 0: + return "" + + # If key is too short for any meaningful prefix/suffix masking + if length <= API_KEY_MASK_PREFIX_LENGTH: + return "*" * length + + # If key is long enough for prefix, but not for prefix + suffix + # e.g., length is 3, 4, 5, 6. For these, show prefix and mask the rest. + if length <= MIN_KEY_LENGTH_FOR_FULL_MASK_LOGIC: + return api_key[:API_KEY_MASK_PREFIX_LENGTH] + "*" * ( + length - API_KEY_MASK_PREFIX_LENGTH + ) + + # If key is long enough for the full prefix + ... + suffix mask + # number of asterisks = length - prefix_length - suffix_length + num_asterisks = length - API_KEY_MASK_PREFIX_LENGTH - API_KEY_MASK_SUFFIX_LENGTH + return ( + api_key[:API_KEY_MASK_PREFIX_LENGTH] + + "*" * num_asterisks + + api_key[-API_KEY_MASK_SUFFIX_LENGTH:] + ) def cache_models( self, api_key: str, base_url: str | None, models: list[str] diff --git a/app/services/providers/vertex_adapter.py b/app/services/providers/vertex_adapter.py new file mode 100644 index 0000000..2023c71 --- /dev/null +++ b/app/services/providers/vertex_adapter.py @@ -0,0 +1,216 @@ +import asyncio +import json +from collections.abc import AsyncGenerator +from typing import Any +import aiohttp +from google.oauth2 import service_account +from google.auth.transport.requests import Request +from app.exceptions.exceptions import ProviderAuthenticationException, InvalidProviderConfigException, InvalidProviderAPIKeyException, ProviderAPIException + +from app.core.logger import get_logger + +from .base import ProviderAdapter +from .anthropic_adapter import AnthropicAdapter + +logger = get_logger(name="vertex_adapter") + + +class VertexAdapter(ProviderAdapter): + """Adapter for Vertex AI API""" + + def __init__(self, provider_name: str, base_url: str | None = None, config: dict[str, str] | None = None): + self._provider_name = provider_name + self._base_url = base_url.rstrip("/") if base_url else None + self.config = config + self.parse_config(config) + + @property + def provider_name(self) -> str: + return self._provider_name + + @staticmethod + def validate_config(config: dict[str, str] | None): + """Validate the config for the given provider""" + try: + assert config is not None + assert config.get("publisher", "anthropic") is not None + assert config.get("location") is not None + except Exception as e: + raise InvalidProviderConfigException("Vertex", e) + + def parse_config(self, config: dict[str, str] | None): + """Validate the config for the given provider""" + self.validate_config(config) + self.publisher = config.get("publisher", "anthropic").lower() + self.location = config["location"].lower() + + @staticmethod + def validate_api_key(api_key: str): + """Validate the API key for the given provider""" + try: + cred_json = json.loads(api_key) + assert cred_json["type"] == "service_account" + assert cred_json["project_id"] is not None + assert cred_json["private_key_id"] is not None + assert cred_json["private_key"] is not None + assert cred_json["client_email"] is not None + assert cred_json["client_id"] is not None + assert cred_json["auth_uri"] is not None + assert cred_json["token_uri"] is not None + assert cred_json["auth_provider_x509_cert_url"] is not None + assert cred_json["client_x509_cert_url"] is not None + assert cred_json["universe_domain"] is not None + + return cred_json + except Exception as e: + raise InvalidProviderAPIKeyException("Vertex", e) + + def parse_api_key(self, api_key: str): + """Validate the API key for the given provider""" + try: + cred_json = self.validate_api_key(api_key) + self.project_id = cred_json["project_id"] + self.cred_json = cred_json + except Exception as e: + raise ProviderAuthenticationException("Vertex", e) + + @staticmethod + def serialize_api_key_config(api_key: str, config: dict[str, Any] | None) -> str: + """Serialize the API key for the given provider""" + VertexAdapter.validate_api_key(api_key) + VertexAdapter.validate_config(config) + return json.dumps({ + "api_key": api_key, + "publisher": config.get("publisher", "anthropic"), + "location": config["location"], + }) + + @staticmethod + def deserialize_api_key_config(serialized_api_key_config: str) -> tuple[str, dict[str, Any] | None]: + """Deserialize the API key for the given provider""" + deserialized_api_key_config = json.loads(serialized_api_key_config) + return deserialized_api_key_config["api_key"], { + "publisher": deserialized_api_key_config["publisher"], + "location": deserialized_api_key_config["location"], + } + + @staticmethod + def mask_config(config: dict[str, Any] | None) -> dict[str, Any] | None: + """Mask the config for the given provider""" + VertexAdapter.validate_config(config) + return { + "publisher": config.get("publisher", "anthropic"), + "location": config["location"], + } + + @staticmethod + def mask_api_key(api_key: str) -> str: + """Mask the API key for the given provider""" + cred_json = VertexAdapter.validate_api_key(api_key) + return json.dumps({ + "type": cred_json["type"], + "project_id": ProviderAdapter.mask_api_key(cred_json["project_id"]), + "private_key_id": ProviderAdapter.mask_api_key(cred_json["private_key_id"]), + "private_key": ProviderAdapter.mask_api_key(cred_json["private_key"]), + "client_email": ProviderAdapter.mask_api_key(cred_json["client_email"]), + "client_id": ProviderAdapter.mask_api_key(cred_json["client_id"]), + "auth_uri": cred_json["auth_uri"], + "token_uri": cred_json["token_uri"], + "auth_provider_x509_cert_url": cred_json["auth_provider_x509_cert_url"], + "client_x509_cert_url": ProviderAdapter.mask_api_key(cred_json["client_x509_cert_url"]), + "universe_domain": cred_json["universe_domain"], + }) + + async def vertex_authentication(self, api_key: str) -> str: + # validate api key + self.parse_api_key(api_key) + + # load credentials within scope + try: + credentials = service_account.Credentials.from_service_account_info(self.cred_json, scopes=["https://www.googleapis.com/auth/cloud-platform"]) + + # refresh token - run in thread pool to avoid blocking + await asyncio.to_thread(credentials.refresh, Request()) + return credentials.token + except Exception as e: + logger.error(f"Error authenticating with Vertex API: {e}") + raise ProviderAuthenticationException("Vertex", e) + + async def list_models(self, api_key: str) -> list[str]: + """List all models (verbosely) supported by the provider""" + # Check cache first + cached_models = self.get_cached_models(api_key, self._base_url) + if cached_models is not None: + return cached_models + + token = await self.vertex_authentication(api_key) + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + } + url = f"{self._base_url}/v1beta1/publishers/{self.publisher}/models" + models = [] + async with aiohttp.ClientSession() as session: + next_page_token = "###initial" + while next_page_token: + params = {} + if next_page_token and next_page_token != "###initial": + params["pageToken"] = next_page_token + async with session.get(url, headers=headers, params=params) as response: + results = await response.json() + next_page_token = results.get("nextPageToken") + for m in results["publisherModels"]: + name = m["name"] + version_id = m["versionId"] + model_id = f"{name.split('/')[-1]}@{version_id}" + models.append(model_id) + + self.cache_models(api_key, self._base_url, models) + return models + + async def process_completion(self, endpoint: str, payload: dict[str, Any], api_key: str) -> Any: + token = await self.vertex_authentication(api_key) + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + } + + streaming = payload.get("stream", False) + model_name = payload["model"] + anthropic_payload = AnthropicAdapter.convert_openai_payload_to_anthropic(payload) + + # vertex specific payload + anthropic_payload["anthropic_version"] = "vertex-2023-10-16" + del anthropic_payload["model"] + + def error_handler(error_text: str, http_status: int): + try: + error_json = json.loads(error_text) + error_message = error_json.get("error", {}).get("message", "Unknown error") + error_code = error_json.get("error", {}).get("code", http_status) + raise ProviderAPIException("Vertex", error_code, error_message) + except Exception: + raise ProviderAPIException("Vertex", http_status, error_text) + + if streaming: + # https://cloud.google.com/vertex-ai/docs/reference/rest/v1/projects.locations.endpoints/streamRawPredict + # vertex doesn't do actual streaming, it just returns a stream of json objects + url = f"{self._base_url}/v1/projects/{self.project_id}/locations/{self.location}/publishers/{self.publisher}/models/{model_name}:streamRawPredict" + async def custom_stream_response(url, headers, anthropic_payload, model_name): + async def stream_response() -> AsyncGenerator[bytes, None]: + resp = await AnthropicAdapter.process_regular_response(url, headers, anthropic_payload, model_name, error_handler) + resp['object'] = 'chat.completion.chunk' + for choice in resp['choices']: + choice['delta'] = choice['message'] + del choice['message'] + yield f"data: {json.dumps(resp)}\n\n".encode() + yield b"data: [DONE]\n\n" + return stream_response() + return await custom_stream_response(url, headers, anthropic_payload, model_name) + else: + url = f"{self._base_url}/v1/projects/{self.project_id}/locations/{self.location}/publishers/{self.publisher}/models/{model_name}:rawPredict" + return await AnthropicAdapter.process_regular_response(url, headers, anthropic_payload, model_name, error_handler) + + async def process_embeddings(self, payload: dict[str, Any]) -> Any: + """Process a embeddings request using Vertex API""" + raise NotImplementedError("Embedding for Vertex is not supported") diff --git a/pyproject.toml b/pyproject.toml index 0471c82..bbadb92 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,8 @@ dependencies = [ "redis>=4.6.0", # sync & async clients used by shared cache "loguru>=0.7.0", "aiobotocore~=2.0", + "google-generativeai>=0.3.0", + "google-genai>=0.3.0", ] [project.optional-dependencies] diff --git a/uv.lock b/uv.lock index a45c8d3..a5b73ac 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,10 @@ version = 1 revision = 1 requires-python = ">=3.12" +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version < '3.13'", +] [[package]] name = "aiobotocore" @@ -235,6 +239,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/83/a753562020b69fa90cebc39e8af2c753b24dcdc74bee8355ee3f6cefdf34/botocore-1.38.27-py3-none-any.whl", hash = "sha256:a785d5e9a5eda88ad6ab9ed8b87d1f2ac409d0226bba6ff801c55359e94d91a8", size = 13580545 }, ] +[[package]] +name = "cachetools" +version = "5.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080 }, +] + [[package]] name = "certifi" version = "2025.1.31" @@ -522,6 +535,8 @@ dependencies = [ { name = "cryptography" }, { name = "email-validator" }, { name = "fastapi" }, + { name = "google-genai" }, + { name = "google-generativeai" }, { name = "gunicorn" }, { name = "loguru" }, { name = "passlib" }, @@ -563,6 +578,8 @@ requires-dist = [ { name = "email-validator", specifier = ">=2.0.0" }, { name = "fastapi", specifier = ">=0.95.0" }, { name = "flake8", marker = "extra == 'dev'" }, + { name = "google-genai", specifier = ">=0.3.0" }, + { name = "google-generativeai", specifier = ">=0.3.0" }, { name = "gunicorn", specifier = ">=20.0.0" }, { name = "isort", marker = "extra == 'dev'" }, { name = "loguru", specifier = ">=0.7.0" }, @@ -625,6 +642,135 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c6/c8/a5be5b7550c10858fcf9b0ea054baccab474da77d37f1e828ce043a3a5d4/frozenlist-1.5.0-py3-none-any.whl", hash = "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3", size = 11901 }, ] +[[package]] +name = "google-ai-generativelanguage" +version = "0.6.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/11/d1/48fe5d7a43d278e9f6b5ada810b0a3530bbeac7ed7fcbcd366f932f05316/google_ai_generativelanguage-0.6.15.tar.gz", hash = "sha256:8f6d9dc4c12b065fe2d0289026171acea5183ebf2d0b11cefe12f3821e159ec3", size = 1375443 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/a3/67b8a6ff5001a1d8864922f2d6488dc2a14367ceb651bc3f09a947f2f306/google_ai_generativelanguage-0.6.15-py3-none-any.whl", hash = "sha256:5a03ef86377aa184ffef3662ca28f19eeee158733e45d7947982eb953c6ebb6c", size = 1327356 }, +] + +[[package]] +name = "google-api-core" +version = "2.25.1" +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/dc/21/e9d043e88222317afdbdb567165fdbc3b0aad90064c7e0c9eb0ad9955ad8/google_api_core-2.25.1.tar.gz", hash = "sha256:d2aaa0b13c78c61cb3f4282c464c046e45fbd75755683c9c525e6e8f7ed0a5e8", size = 165443 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/4b/ead00905132820b623732b175d66354e9d3e69fcf2a5dcdab780664e7896/google_api_core-2.25.1-py3-none-any.whl", hash = "sha256:8a2a56c1fef82987a524371f99f3bd0143702fecc670c72e600c1cda6bf8dbb7", size = 160807 }, +] + +[package.optional-dependencies] +grpc = [ + { name = "grpcio" }, + { name = "grpcio-status" }, +] + +[[package]] +name = "google-api-python-client" +version = "2.176.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core" }, + { name = "google-auth" }, + { name = "google-auth-httplib2" }, + { name = "httplib2" }, + { name = "uritemplate" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/38/daf70faf6d05556d382bac640bc6765f09fcfb9dfb51ac4a595d3453a2a9/google_api_python_client-2.176.0.tar.gz", hash = "sha256:2b451cdd7fd10faeb5dd20f7d992f185e1e8f4124c35f2cdcc77c843139a4cf1", size = 13154773 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/2c/758f415a19a12c3c6d06902794b0dd4c521d912a59b98ab752bba48812df/google_api_python_client-2.176.0-py3-none-any.whl", hash = "sha256:e22239797f1d085341e12cd924591fc65c56d08e0af02549d7606092e6296510", size = 13678445 }, +] + +[[package]] +name = "google-auth" +version = "2.40.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachetools" }, + { name = "pyasn1-modules" }, + { name = "rsa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/9b/e92ef23b84fa10a64ce4831390b7a4c2e53c0132568d99d4ae61d04c8855/google_auth-2.40.3.tar.gz", hash = "sha256:500c3a29adedeb36ea9cf24b8d10858e152f2412e3ca37829b3fa18e33d63b77", size = 281029 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/63/b19553b658a1692443c62bd07e5868adaa0ad746a0751ba62c59568cd45b/google_auth-2.40.3-py2.py3-none-any.whl", hash = "sha256:1370d4593e86213563547f97a92752fc658456fe4514c809544f330fed45a7ca", size = 216137 }, +] + +[[package]] +name = "google-auth-httplib2" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "httplib2" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/be/217a598a818567b28e859ff087f347475c807a5649296fb5a817c58dacef/google-auth-httplib2-0.2.0.tar.gz", hash = "sha256:38aa7badf48f974f1eb9861794e9c0cb2a0511a4ec0679b1f886d108f5640e05", size = 10842 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/8a/fe34d2f3f9470a27b01c9e76226965863f153d5fbe276f83608562e49c04/google_auth_httplib2-0.2.0-py2.py3-none-any.whl", hash = "sha256:b65a0a2123300dd71281a7bf6e64d65a0759287df52729bdd1ae2e47dc311a3d", size = 9253 }, +] + +[[package]] +name = "google-genai" +version = "1.24.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "google-auth" }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/cf/37ac8cd4752e28e547b8a52765fe48a2ada2d0d286ea03f46e4d8c69ff4f/google_genai-1.24.0.tar.gz", hash = "sha256:bc896e30ad26d05a2af3d17c2ba10ea214a94f1c0cdb93d5c004dc038774e75a", size = 226740 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/28/a35f64fc02e599808101617a21d447d241dadeba2aac1f4dc2d1179b8218/google_genai-1.24.0-py3-none-any.whl", hash = "sha256:98be8c51632576289ecc33cd84bcdaf4356ef0bef04ac7578660c49175af22b9", size = 226065 }, +] + +[[package]] +name = "google-generativeai" +version = "0.8.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-ai-generativelanguage" }, + { name = "google-api-core" }, + { name = "google-api-python-client" }, + { name = "google-auth" }, + { name = "protobuf" }, + { name = "pydantic" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/40/c42ff9ded9f09ec9392879a8e6538a00b2dc185e834a3392917626255419/google_generativeai-0.8.5-py3-none-any.whl", hash = "sha256:22b420817fb263f8ed520b33285f45976d5b21e904da32b80d4fd20c055123a2", size = 155427 }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.70.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/24/33db22342cf4a2ea27c9955e6713140fedd51e8b141b5ce5260897020f1a/googleapis_common_protos-1.70.0.tar.gz", hash = "sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257", size = 145903 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/f1/62a193f0227cf15a920390abe675f386dec35f7ae3ffe6da582d3ade42c7/googleapis_common_protos-1.70.0-py3-none-any.whl", hash = "sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8", size = 294530 }, +] + [[package]] name = "greenlet" version = "3.1.1" @@ -658,6 +804,48 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ac/38/08cc303ddddc4b3d7c628c3039a61a3aae36c241ed01393d00c2fd663473/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6", size = 1142112 }, ] +[[package]] +name = "grpcio" +version = "1.73.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/e8/b43b851537da2e2f03fa8be1aef207e5cbfb1a2e014fbb6b40d24c177cd3/grpcio-1.73.1.tar.gz", hash = "sha256:7fce2cd1c0c1116cf3850564ebfc3264fba75d3c74a7414373f1238ea365ef87", size = 12730355 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/41/456caf570c55d5ac26f4c1f2db1f2ac1467d5bf3bcd660cba3e0a25b195f/grpcio-1.73.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:921b25618b084e75d424a9f8e6403bfeb7abef074bb6c3174701e0f2542debcf", size = 5334621 }, + { url = "https://files.pythonhosted.org/packages/2a/c2/9a15e179e49f235bb5e63b01590658c03747a43c9775e20c4e13ca04f4c4/grpcio-1.73.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:277b426a0ed341e8447fbf6c1d6b68c952adddf585ea4685aa563de0f03df887", size = 10601131 }, + { url = "https://files.pythonhosted.org/packages/0c/1d/1d39e90ef6348a0964caa7c5c4d05f3bae2c51ab429eb7d2e21198ac9b6d/grpcio-1.73.1-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:96c112333309493c10e118d92f04594f9055774757f5d101b39f8150f8c25582", size = 5759268 }, + { url = "https://files.pythonhosted.org/packages/8a/2b/2dfe9ae43de75616177bc576df4c36d6401e0959833b2e5b2d58d50c1f6b/grpcio-1.73.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f48e862aed925ae987eb7084409a80985de75243389dc9d9c271dd711e589918", size = 6409791 }, + { url = "https://files.pythonhosted.org/packages/6e/66/e8fe779b23b5a26d1b6949e5c70bc0a5fd08f61a6ec5ac7760d589229511/grpcio-1.73.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83a6c2cce218e28f5040429835fa34a29319071079e3169f9543c3fbeff166d2", size = 6003728 }, + { url = "https://files.pythonhosted.org/packages/a9/39/57a18fcef567784108c4fc3f5441cb9938ae5a51378505aafe81e8e15ecc/grpcio-1.73.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:65b0458a10b100d815a8426b1442bd17001fdb77ea13665b2f7dc9e8587fdc6b", size = 6103364 }, + { url = "https://files.pythonhosted.org/packages/c5/46/28919d2aa038712fc399d02fa83e998abd8c1f46c2680c5689deca06d1b2/grpcio-1.73.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:0a9f3ea8dce9eae9d7cb36827200133a72b37a63896e0e61a9d5ec7d61a59ab1", size = 6749194 }, + { url = "https://files.pythonhosted.org/packages/3d/56/3898526f1fad588c5d19a29ea0a3a4996fb4fa7d7c02dc1be0c9fd188b62/grpcio-1.73.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:de18769aea47f18e782bf6819a37c1c528914bfd5683b8782b9da356506190c8", size = 6283902 }, + { url = "https://files.pythonhosted.org/packages/dc/64/18b77b89c5870d8ea91818feb0c3ffb5b31b48d1b0ee3e0f0d539730fea3/grpcio-1.73.1-cp312-cp312-win32.whl", hash = "sha256:24e06a5319e33041e322d32c62b1e728f18ab8c9dbc91729a3d9f9e3ed336642", size = 3668687 }, + { url = "https://files.pythonhosted.org/packages/3c/52/302448ca6e52f2a77166b2e2ed75f5d08feca4f2145faf75cb768cccb25b/grpcio-1.73.1-cp312-cp312-win_amd64.whl", hash = "sha256:303c8135d8ab176f8038c14cc10d698ae1db9c480f2b2823f7a987aa2a4c5646", size = 4334887 }, + { url = "https://files.pythonhosted.org/packages/37/bf/4ca20d1acbefabcaba633ab17f4244cbbe8eca877df01517207bd6655914/grpcio-1.73.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:b310824ab5092cf74750ebd8a8a8981c1810cb2b363210e70d06ef37ad80d4f9", size = 5335615 }, + { url = "https://files.pythonhosted.org/packages/75/ed/45c345f284abec5d4f6d77cbca9c52c39b554397eb7de7d2fcf440bcd049/grpcio-1.73.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:8f5a6df3fba31a3485096ac85b2e34b9666ffb0590df0cd044f58694e6a1f6b5", size = 10595497 }, + { url = "https://files.pythonhosted.org/packages/a4/75/bff2c2728018f546d812b755455014bc718f8cdcbf5c84f1f6e5494443a8/grpcio-1.73.1-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:052e28fe9c41357da42250a91926a3e2f74c046575c070b69659467ca5aa976b", size = 5765321 }, + { url = "https://files.pythonhosted.org/packages/70/3b/14e43158d3b81a38251b1d231dfb45a9b492d872102a919fbf7ba4ac20cd/grpcio-1.73.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c0bf15f629b1497436596b1cbddddfa3234273490229ca29561209778ebe182", size = 6415436 }, + { url = "https://files.pythonhosted.org/packages/e5/3f/81d9650ca40b54338336fd360f36773be8cb6c07c036e751d8996eb96598/grpcio-1.73.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ab860d5bfa788c5a021fba264802e2593688cd965d1374d31d2b1a34cacd854", size = 6007012 }, + { url = "https://files.pythonhosted.org/packages/55/f4/59edf5af68d684d0f4f7ad9462a418ac517201c238551529098c9aa28cb0/grpcio-1.73.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:ad1d958c31cc91ab050bd8a91355480b8e0683e21176522bacea225ce51163f2", size = 6105209 }, + { url = "https://files.pythonhosted.org/packages/e4/a8/700d034d5d0786a5ba14bfa9ce974ed4c976936c2748c2bd87aa50f69b36/grpcio-1.73.1-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:f43ffb3bd415c57224c7427bfb9e6c46a0b6e998754bfa0d00f408e1873dcbb5", size = 6753655 }, + { url = "https://files.pythonhosted.org/packages/1f/29/efbd4ac837c23bc48e34bbaf32bd429f0dc9ad7f80721cdb4622144c118c/grpcio-1.73.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:686231cdd03a8a8055f798b2b54b19428cdf18fa1549bee92249b43607c42668", size = 6287288 }, + { url = "https://files.pythonhosted.org/packages/d8/61/c6045d2ce16624bbe18b5d169c1a5ce4d6c3a47bc9d0e5c4fa6a50ed1239/grpcio-1.73.1-cp313-cp313-win32.whl", hash = "sha256:89018866a096e2ce21e05eabed1567479713ebe57b1db7cbb0f1e3b896793ba4", size = 3668151 }, + { url = "https://files.pythonhosted.org/packages/c2/d7/77ac689216daee10de318db5aa1b88d159432dc76a130948a56b3aa671a2/grpcio-1.73.1-cp313-cp313-win_amd64.whl", hash = "sha256:4a68f8c9966b94dff693670a5cf2b54888a48a5011c5d9ce2295a1a1465ee84f", size = 4335747 }, +] + +[[package]] +name = "grpcio-status" +version = "1.71.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/d1/b6e9877fedae3add1afdeae1f89d1927d296da9cf977eca0eb08fb8a460e/grpcio_status-1.71.2.tar.gz", hash = "sha256:c7a97e176df71cdc2c179cd1847d7fc86cca5832ad12e9798d7fed6b7a1aab50", size = 13677 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/58/317b0134129b556a93a3b0afe00ee675b5657f0155509e22fcb853bafe2d/grpcio_status-1.71.2-py3-none-any.whl", hash = "sha256:803c98cb6a8b7dc6dbb785b1111aed739f241ab5e9da0bba96888aa74704cfd3", size = 14424 }, +] + [[package]] name = "gunicorn" version = "23.0.0" @@ -692,6 +880,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/18/8d/f052b1e336bb2c1fc7ed1aaed898aa570c0b61a09707b108979d9fc6e308/httpcore-1.0.8-py3-none-any.whl", hash = "sha256:5254cf149bcb5f75e9d1b2b9f729ea4a4b883d1ad7379fc632b727cec23674be", size = 78732 }, ] +[[package]] +name = "httplib2" +version = "0.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyparsing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/ad/2371116b22d616c194aa25ec410c9c6c37f23599dcd590502b74db197584/httplib2-0.22.0.tar.gz", hash = "sha256:d7a10bc5ef5ab08322488bde8c726eeee5c8618723fdb399597ec58f3d82df81", size = 351116 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/6c/d2fbdaaa5959339d53ba38e94c123e4e84b8fbc4b84beb0e70d7c1608486/httplib2-0.22.0-py3-none-any.whl", hash = "sha256:14ae0a53c1ba8f3d37e9e27cf37eabb0fb9980f435ba405d546948b009dd64dc", size = 96854 }, +] + [[package]] name = "httpx" version = "0.28.1" @@ -1040,6 +1240,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b8/d3/c3cb8f1d6ae3b37f83e1de806713a9b3642c5895f0215a62e1a4bd6e5e34/propcache-0.3.1-py3-none-any.whl", hash = "sha256:9a8ecf38de50a7f518c21568c80f985e776397b902f1ce0b01f799aba1608b40", size = 12376 }, ] +[[package]] +name = "proto-plus" +version = "1.26.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/ac/87285f15f7cce6d4a008f33f1757fb5a13611ea8914eb58c3d0d26243468/proto_plus-1.26.1.tar.gz", hash = "sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012", size = 56142 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/6d/280c4c2ce28b1593a19ad5239c8b826871fc6ec275c21afc8e1820108039/proto_plus-1.26.1-py3-none-any.whl", hash = "sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66", size = 50163 }, +] + +[[package]] +name = "protobuf" +version = "5.29.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/29/d09e70352e4e88c9c7a198d5645d7277811448d76c23b00345670f7c8a38/protobuf-5.29.5.tar.gz", hash = "sha256:bc1463bafd4b0929216c35f437a8e28731a2b7fe3d98bb77a600efced5a15c84", size = 425226 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/11/6e40e9fc5bba02988a214c07cf324595789ca7820160bfd1f8be96e48539/protobuf-5.29.5-cp310-abi3-win32.whl", hash = "sha256:3f1c6468a2cfd102ff4703976138844f78ebd1fb45f49011afc5139e9e283079", size = 422963 }, + { url = "https://files.pythonhosted.org/packages/81/7f/73cefb093e1a2a7c3ffd839e6f9fcafb7a427d300c7f8aef9c64405d8ac6/protobuf-5.29.5-cp310-abi3-win_amd64.whl", hash = "sha256:3f76e3a3675b4a4d867b52e4a5f5b78a2ef9565549d4037e06cf7b0942b1d3fc", size = 434818 }, + { url = "https://files.pythonhosted.org/packages/dd/73/10e1661c21f139f2c6ad9b23040ff36fee624310dc28fba20d33fdae124c/protobuf-5.29.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e38c5add5a311f2a6eb0340716ef9b039c1dfa428b28f25a7838ac329204a671", size = 418091 }, + { url = "https://files.pythonhosted.org/packages/6c/04/98f6f8cf5b07ab1294c13f34b4e69b3722bb609c5b701d6c169828f9f8aa/protobuf-5.29.5-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:fa18533a299d7ab6c55a238bf8629311439995f2e7eca5caaff08663606e9015", size = 319824 }, + { url = "https://files.pythonhosted.org/packages/85/e4/07c80521879c2d15f321465ac24c70efe2381378c00bf5e56a0f4fbac8cd/protobuf-5.29.5-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:63848923da3325e1bf7e9003d680ce6e14b07e55d0473253a690c3a8b8fd6e61", size = 319942 }, + { url = "https://files.pythonhosted.org/packages/7e/cc/7e77861000a0691aeea8f4566e5d3aa716f2b1dece4a24439437e41d3d25/protobuf-5.29.5-py3-none-any.whl", hash = "sha256:6cf42630262c59b2d8de33954443d94b746c952b01434fc58a417fdbd2e84bd5", size = 172823 }, +] + [[package]] name = "psycopg2-binary" version = "2.9.10" @@ -1080,6 +1306,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/1e/a94a8d635fa3ce4cfc7f506003548d0a2447ae76fd5ca53932970fe3053f/pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d", size = 77145 }, ] +[[package]] +name = "pyasn1-modules" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/67/6afbf0d507f73c32d21084a79946bfcfca5fbc62a72057e9c23797a737c9/pyasn1_modules-0.4.1.tar.gz", hash = "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c", size = 310028 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/89/bc88a6711935ba795a679ea6ebee07e128050d6382eaa35a0a47c8032bdc/pyasn1_modules-0.4.1-py3-none-any.whl", hash = "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd", size = 181537 }, +] + [[package]] name = "pycodestyle" version = "2.14.0" @@ -1164,6 +1402,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551 }, ] +[[package]] +name = "pyparsing" +version = "3.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/22/f1129e69d94ffff626bdb5c835506b3a5b4f3d070f17ea295e12c2c6f60f/pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be", size = 1088608 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120 }, +] + [[package]] name = "pytest" version = "8.3.5" @@ -1424,6 +1671,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/56/f3/5633e45bc01825c4464b6b1e98e05052e532139e827c4ea8c54f5eafb022/svix-1.67.0-py3-none-any.whl", hash = "sha256:4f195bea0ac7c33c54f29bb486e3814e9c50123be303bfba5064d1e607274668", size = 95009 }, ] +[[package]] +name = "tenacity" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/4d/6a19536c50b849338fcbe9290d562b52cbdcf30d8963d3588a68a4107df1/tenacity-8.5.0.tar.gz", hash = "sha256:8bc6c0c8a09b31e6cad13c47afbed1a567518250a9a171418582ed8d9c20ca78", size = 47309 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/3f/8ba87d9e287b9d385a02a7114ddcef61b26f86411e121c9003eb509a1773/tenacity-8.5.0-py3-none-any.whl", hash = "sha256:b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687", size = 28165 }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 }, +] + [[package]] name = "types-deprecated" version = "1.2.15.20250304" @@ -1463,6 +1731,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125 }, ] +[[package]] +name = "uritemplate" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/60/f174043244c5306c9988380d2cb10009f91563fc4b31293d27e17201af56/uritemplate-4.2.0.tar.gz", hash = "sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e", size = 33267 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/99/3ae339466c9183ea5b8ae87b34c0b897eda475d2aec2307cae60e5cd4f29/uritemplate-4.2.0-py3-none-any.whl", hash = "sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686", size = 11488 }, +] + [[package]] name = "urllib3" version = "2.3.0" @@ -1499,6 +1776,37 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c2/eb/c6db6e3001d58c6a9e67c74bb7b4206767caa3ccc28c6b9eaf4c23fb4e34/virtualenv-20.29.3-py3-none-any.whl", hash = "sha256:3e3d00f5807e83b234dfb6122bf37cfadf4be216c53a49ac059d02414f819170", size = 4301458 }, ] +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437 }, + { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096 }, + { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332 }, + { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152 }, + { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096 }, + { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523 }, + { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790 }, + { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165 }, + { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160 }, + { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395 }, + { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841 }, + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440 }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098 }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329 }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111 }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054 }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496 }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829 }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217 }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195 }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393 }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837 }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743 }, +] + [[package]] name = "win32-setctime" version = "1.2.0"