diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 7231e08..1c5a2ee 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -8,8 +8,9 @@ on: - "v*" paths-ignore: - "**/*.md" - - ".github/workflows/ruff.yaml" - - ".github/workflows/track.yml" + - ".github/*" + - "LICENSE" + - ".gitignore" env: REGISTRY: ghcr.io @@ -26,6 +27,9 @@ jobs: - name: Checkout repository uses: actions/checkout@v6 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -46,6 +50,7 @@ jobs: type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}} + type=raw,value={{date 'YYYYMMDD'}}-{{sha}} type=raw,value=latest,enable={{is_default_branch}} - name: Build and push Docker image diff --git a/.github/workflows/ruff.yaml b/.github/workflows/ruff.yaml index 6b9e536..d451cdc 100644 --- a/.github/workflows/ruff.yaml +++ b/.github/workflows/ruff.yaml @@ -19,12 +19,12 @@ jobs: - name: Set up Python uses: actions/setup-python@v6 with: - python-version: "3.12" + python-version: "3.13" - name: Install Ruff run: | python -m pip install --upgrade pip - pip install "ruff>=0.11.7" + pip install "ruff>=0.15.1" - name: Run Ruff run: ruff check . diff --git a/Dockerfile b/Dockerfile index 938bc2f..ef7f41e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,26 @@ -FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim +FROM ghcr.io/astral-sh/uv:python3.13-trixie-slim LABEL org.opencontainers.image.description="Web-based Gemini models wrapped into an OpenAI-compatible API." WORKDIR /app -# Install dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + tini \ + && rm -rf /var/lib/apt/lists/* + +ENV UV_COMPILE_BYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 + COPY pyproject.toml uv.lock ./ -RUN uv sync --no-cache --no-dev +RUN uv sync --no-cache --frozen --no-install-project --no-dev COPY app/ app/ COPY config/ config/ COPY run.py . -# Command to run the application +EXPOSE 8000 + +ENTRYPOINT ["/usr/bin/tini", "--"] + CMD ["uv", "run", "--no-dev", "run.py"] diff --git a/README.md b/README.md index 330e9c8..91f687c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Gemini-FastAPI -[![Python 3.12](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/) +[![Python 3.13](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/downloads/) [![FastAPI](https://img.shields.io/badge/FastAPI-0.115+-green.svg)](https://fastapi.tiangolo.com/) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) @@ -24,7 +24,7 @@ Web-based Gemini models wrapped into an OpenAI-compatible API. Powered by [Hanao ### Prerequisites -- Python 3.12 +- Python 3.13 - Google account with Gemini access on web - `secure_1psid` and `secure_1psidts` cookies from Gemini web interface diff --git a/README.zh.md b/README.zh.md index 2f9e1b5..d23bec1 100644 --- a/README.zh.md +++ b/README.zh.md @@ -1,6 +1,6 @@ # Gemini-FastAPI -[![Python 3.12](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/) +[![Python 3.13](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/downloads/) [![FastAPI](https://img.shields.io/badge/FastAPI-0.115+-green.svg)](https://fastapi.tiangolo.com/) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) @@ -24,7 +24,7 @@ ### 前置条件 -- Python 3.12 +- Python 3.13 - 拥有网页版 Gemini 访问权限的 Google 账号 - 从 Gemini 网页获取的 `secure_1psid` 和 `secure_1psidts` Cookie diff --git a/app/main.py b/app/main.py index f4e6711..0634ce2 100644 --- a/app/main.py +++ b/app/main.py @@ -43,7 +43,7 @@ async def _run_retention_cleanup(stop_event: asyncio.Event) -> None: stop_event.wait(), timeout=RETENTION_CLEANUP_INTERVAL_SECONDS, ) - except asyncio.TimeoutError: + except TimeoutError: continue logger.info("LMDB retention cleanup task stopped.") diff --git a/app/models/__init__.py b/app/models/__init__.py index c6a3640..3896de1 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -1 +1,59 @@ -from .models import * # noqa: F403 +from .models import ( + ChatCompletionRequest, + ChatCompletionResponse, + Choice, + ContentItem, + ConversationInStore, + FunctionCall, + HealthCheckResponse, + Message, + ModelData, + ModelListResponse, + ResponseCreateRequest, + ResponseCreateResponse, + ResponseImageGenerationCall, + ResponseImageTool, + ResponseInputContent, + ResponseInputItem, + ResponseOutputContent, + ResponseOutputMessage, + ResponseToolCall, + ResponseToolChoice, + ResponseUsage, + Tool, + ToolCall, + ToolChoiceFunction, + ToolChoiceFunctionDetail, + ToolFunctionDefinition, + Usage, +) + +__all__ = [ + "ChatCompletionRequest", + "ChatCompletionResponse", + "Choice", + "ContentItem", + "ConversationInStore", + "FunctionCall", + "HealthCheckResponse", + "Message", + "ModelData", + "ModelListResponse", + "ResponseCreateRequest", + "ResponseCreateResponse", + "ResponseImageGenerationCall", + "ResponseImageTool", + "ResponseInputContent", + "ResponseInputItem", + "ResponseOutputContent", + "ResponseOutputMessage", + "ResponseToolCall", + "ResponseToolChoice", + "ResponseUsage", + "Tool", + "ToolCall", + "ToolChoiceFunction", + "ToolChoiceFunctionDetail", + "ToolFunctionDefinition", + "Usage", +] diff --git a/app/models/models.py b/app/models/models.py index 64ceaa9..3b3e627 100644 --- a/app/models/models.py +++ b/app/models/models.py @@ -1,7 +1,7 @@ from __future__ import annotations from datetime import datetime -from typing import Any, Dict, List, Literal, Optional, Union +from typing import Any, Literal from pydantic import BaseModel, Field, model_validator @@ -10,28 +10,28 @@ class ContentItem(BaseModel): """Individual content item (text, image, or file) within a message.""" type: Literal["text", "image_url", "file", "input_audio"] - text: Optional[str] = None - image_url: Optional[Dict[str, str]] = None - input_audio: Optional[Dict[str, Any]] = None - file: Optional[Dict[str, str]] = None - annotations: List[Dict[str, Any]] = Field(default_factory=list) + text: str | None = Field(default=None) + image_url: dict[str, Any] | None = Field(default=None) + input_audio: dict[str, Any] | None = Field(default=None) + file: dict[str, Any] | None = Field(default=None) + annotations: list[dict[str, Any]] = Field(default_factory=list) class Message(BaseModel): """Message model""" role: str - content: Union[str, List[ContentItem], None] = None - name: Optional[str] = None - tool_calls: Optional[List["ToolCall"]] = None - tool_call_id: Optional[str] = None - refusal: Optional[str] = None - reasoning_content: Optional[str] = None - audio: Optional[Dict[str, Any]] = None - annotations: List[Dict[str, Any]] = Field(default_factory=list) + content: str | list[ContentItem] | None = Field(default=None) + name: str | None = Field(default=None) + tool_calls: list[ToolCall] | None = Field(default=None) + tool_call_id: str | None = Field(default=None) + refusal: str | None = Field(default=None) + reasoning_content: str | None = Field(default=None) + audio: dict[str, Any] | None = Field(default=None) + annotations: list[dict[str, Any]] = Field(default_factory=list) @model_validator(mode="after") - def normalize_role(self) -> "Message": + def normalize_role(self) -> Message: """Normalize 'developer' role to 'system' for Gemini compatibility.""" if self.role == "developer": self.role = "system" @@ -44,7 +44,7 @@ class Choice(BaseModel): index: int message: Message finish_reason: str - logprobs: Optional[Dict[str, Any]] = None + logprobs: dict[str, Any] | None = Field(default=None) class FunctionCall(BaseModel): @@ -66,8 +66,8 @@ class ToolFunctionDefinition(BaseModel): """Function definition for tool.""" name: str - description: Optional[str] = None - parameters: Optional[Dict[str, Any]] = None + description: str | None = Field(default=None) + parameters: dict[str, Any] | None = Field(default=None) class Tool(BaseModel): @@ -96,8 +96,8 @@ class Usage(BaseModel): prompt_tokens: int completion_tokens: int total_tokens: int - prompt_tokens_details: Optional[Dict[str, int]] = None - completion_tokens_details: Optional[Dict[str, int]] = None + prompt_tokens_details: dict[str, int] | None = Field(default=None) + completion_tokens_details: dict[str, int] | None = Field(default=None) class ModelData(BaseModel): @@ -113,17 +113,17 @@ class ChatCompletionRequest(BaseModel): """Chat completion request model""" model: str - messages: List[Message] - stream: Optional[bool] = False - user: Optional[str] = None - temperature: Optional[float] = 0.7 - top_p: Optional[float] = 1.0 - max_tokens: Optional[int] = None - tools: Optional[List["Tool"]] = None - tool_choice: Optional[ - Union[Literal["none"], Literal["auto"], Literal["required"], "ToolChoiceFunction"] - ] = None - response_format: Optional[Dict[str, Any]] = None + messages: list[Message] + stream: bool | None = Field(default=False) + user: str | None = Field(default=None) + temperature: float | None = Field(default=0.7) + top_p: float | None = Field(default=1.0) + max_tokens: int | None = Field(default=None) + tools: list[Tool] | None = Field(default=None) + tool_choice: ( + Literal["none"] | Literal["auto"] | Literal["required"] | ToolChoiceFunction | None + ) = Field(default=None) + response_format: dict[str, Any] | None = Field(default=None) class ChatCompletionResponse(BaseModel): @@ -133,7 +133,7 @@ class ChatCompletionResponse(BaseModel): object: str = "chat.completion" created: int model: str - choices: List[Choice] + choices: list[Choice] usage: Usage @@ -141,23 +141,23 @@ class ModelListResponse(BaseModel): """Model list model""" object: str = "list" - data: List[ModelData] + data: list[ModelData] class HealthCheckResponse(BaseModel): """Health check response model""" ok: bool - storage: Optional[Dict[str, str | int]] = None - clients: Optional[Dict[str, bool]] = None - error: Optional[str] = None + storage: dict[str, Any] | None = Field(default=None) + clients: dict[str, bool] | None = Field(default=None) + error: str | None = Field(default=None) class ConversationInStore(BaseModel): """Conversation model for storing in the database.""" - created_at: Optional[datetime] = Field(default=None) - updated_at: Optional[datetime] = Field(default=None) + created_at: datetime | None = Field(default=None) + updated_at: datetime | None = Field(default=None) # Gemini Web API does not support changing models once a conversation is created. model: str = Field(..., description="Model used for the conversation") @@ -172,13 +172,13 @@ class ResponseInputContent(BaseModel): """Content item for Responses API input.""" type: Literal["input_text", "input_image", "input_file"] - text: Optional[str] = None - image_url: Optional[str] = None - detail: Optional[Literal["auto", "low", "high"]] = None - file_url: Optional[str] = None - file_data: Optional[str] = None - filename: Optional[str] = None - annotations: List[Dict[str, Any]] = Field(default_factory=list) + text: str | None = Field(default=None) + image_url: str | None = Field(default=None) + detail: Literal["auto", "low", "high"] | None = Field(default=None) + file_url: str | None = Field(default=None) + file_data: str | None = Field(default=None) + filename: str | None = Field(default=None) + annotations: list[dict[str, Any]] = Field(default_factory=list) @model_validator(mode="before") @classmethod @@ -192,42 +192,42 @@ def normalize_output_text(cls, data: Any) -> Any: class ResponseInputItem(BaseModel): """Single input item for Responses API.""" - type: Optional[Literal["message"]] = "message" + type: Literal["message"] | None = Field(default="message") role: Literal["user", "assistant", "system", "developer"] - content: Union[str, List[ResponseInputContent]] + content: str | list[ResponseInputContent] class ResponseToolChoice(BaseModel): """Tool choice enforcing a specific tool in Responses API.""" type: Literal["function", "image_generation"] - function: Optional[ToolChoiceFunctionDetail] = None + function: ToolChoiceFunctionDetail | None = Field(default=None) class ResponseImageTool(BaseModel): """Image generation tool specification for Responses API.""" type: Literal["image_generation"] - model: Optional[str] = None - output_format: Optional[str] = None + model: str | None = Field(default=None) + output_format: str | None = Field(default=None) class ResponseCreateRequest(BaseModel): """Responses API request payload.""" model: str - input: Union[str, List[ResponseInputItem]] - instructions: Optional[Union[str, List[ResponseInputItem]]] = None - temperature: Optional[float] = 0.7 - top_p: Optional[float] = 1.0 - max_output_tokens: Optional[int] = None - stream: Optional[bool] = False - tool_choice: Optional[Union[str, ResponseToolChoice]] = None - tools: Optional[List[Union[Tool, ResponseImageTool]]] = None - store: Optional[bool] = None - user: Optional[str] = None - response_format: Optional[Dict[str, Any]] = None - metadata: Optional[Dict[str, Any]] = None + input: str | list[ResponseInputItem] + instructions: str | list[ResponseInputItem] | None = Field(default=None) + temperature: float | None = Field(default=0.7) + top_p: float | None = Field(default=1.0) + max_output_tokens: int | None = Field(default=None) + stream: bool | None = Field(default=False) + tool_choice: str | ResponseToolChoice | None = Field(default=None) + tools: list[Tool | ResponseImageTool] | None = Field(default=None) + store: bool | None = Field(default=None) + user: str | None = Field(default=None) + response_format: dict[str, Any] | None = Field(default=None) + metadata: dict[str, Any] | None = Field(default=None) class ResponseUsage(BaseModel): @@ -242,8 +242,8 @@ class ResponseOutputContent(BaseModel): """Content item for Responses API output.""" type: Literal["output_text"] - text: Optional[str] = "" - annotations: List[Dict[str, Any]] = Field(default_factory=list) + text: str | None = Field(default="") + annotations: list[dict[str, Any]] = Field(default_factory=list) class ResponseOutputMessage(BaseModel): @@ -252,27 +252,29 @@ class ResponseOutputMessage(BaseModel): id: str type: Literal["message"] role: Literal["assistant"] - content: List[ResponseOutputContent] + content: list[ResponseOutputContent] class ResponseImageGenerationCall(BaseModel): """Image generation call record emitted in Responses API.""" id: str - type: Literal["image_generation_call"] = "image_generation_call" - status: Literal["completed", "in_progress", "generating", "failed"] = "completed" - result: Optional[str] = None - output_format: Optional[str] = None - size: Optional[str] = None - revised_prompt: Optional[str] = None + type: Literal["image_generation_call"] = Field(default="image_generation_call") + status: Literal["completed", "in_progress", "generating", "failed"] = Field(default="completed") + result: str | None = Field(default=None) + output_format: str | None = Field(default=None) + size: str | None = Field(default=None) + revised_prompt: str | None = Field(default=None) class ResponseToolCall(BaseModel): """Tool call record emitted in Responses API.""" id: str - type: Literal["tool_call"] = "tool_call" - status: Literal["in_progress", "completed", "failed", "requires_action"] = "completed" + type: Literal["tool_call"] = Field(default="tool_call") + status: Literal["in_progress", "completed", "failed", "requires_action"] = Field( + default="completed" + ) function: FunctionCall @@ -280,10 +282,10 @@ class ResponseCreateResponse(BaseModel): """Responses API response payload.""" id: str - object: Literal["response"] = "response" + object: Literal["response"] = Field(default="response") created_at: int model: str - output: List[Union[ResponseOutputMessage, ResponseImageGenerationCall, ResponseToolCall]] + output: list[ResponseOutputMessage | ResponseImageGenerationCall | ResponseToolCall] status: Literal[ "in_progress", "completed", @@ -291,13 +293,13 @@ class ResponseCreateResponse(BaseModel): "incomplete", "cancelled", "requires_action", - ] = "completed" - tool_choice: Optional[Union[str, ResponseToolChoice]] = None - tools: Optional[List[Union[Tool, ResponseImageTool]]] = None + ] = Field(default="completed") + tool_choice: str | ResponseToolChoice | None = Field(default=None) + tools: list[Tool | ResponseImageTool] | None = Field(default=None) usage: ResponseUsage - error: Optional[Dict[str, Any]] = None - metadata: Optional[Dict[str, Any]] = None - input: Optional[Union[str, List[ResponseInputItem]]] = None + error: dict[str, Any] | None = Field(default=None) + metadata: dict[str, Any] | None = Field(default=None) + input: str | list[ResponseInputItem] | None = Field(default=None) # Rebuild models with forward references diff --git a/app/server/chat.py b/app/server/chat.py index 934091b..3849af5 100644 --- a/app/server/chat.py +++ b/app/server/chat.py @@ -3,10 +3,11 @@ import io import reprlib import uuid +from collections.abc import AsyncGenerator from dataclasses import dataclass -from datetime import datetime, timezone +from datetime import UTC, datetime from pathlib import Path -from typing import Any, AsyncGenerator +from typing import Any import orjson from fastapi import APIRouter, Depends, HTTPException, Request, status @@ -17,7 +18,7 @@ from gemini_webapi.types.image import GeneratedImage, Image from loguru import logger -from ..models import ( +from app.models import ( ChatCompletionRequest, ContentItem, ConversationInStore, @@ -38,9 +39,15 @@ Tool, ToolChoiceFunction, ) -from ..services import GeminiClientPool, GeminiClientWrapper, LMDBConversationStore -from ..utils import g_config -from ..utils.helper import ( +from app.server.middleware import ( + get_image_store_dir, + get_image_token, + get_temp_dir, + verify_api_key, +) +from app.services import GeminiClientPool, GeminiClientWrapper, LMDBConversationStore +from app.utils import g_config +from app.utils.helper import ( TOOL_HINT_LINE_END, TOOL_HINT_LINE_START, TOOL_HINT_STRIPPED, @@ -53,7 +60,6 @@ strip_system_hints, text_from_message, ) -from .middleware import get_image_store_dir, get_image_token, get_temp_dir, verify_api_key MAX_CHARS_PER_REQUEST = int(g_config.gemini.max_chars_per_request * 0.9) METADATA_TTL_MINUTES = 15 @@ -98,11 +104,7 @@ async def _image_to_base64( if not suffix: detected_ext = detect_image_extension(data) - if detected_ext: - suffix = detected_ext - else: - # Fallback if detection fails - suffix = ".png" if isinstance(image, GeneratedImage) else ".jpg" + suffix = detected_ext or (".png" if isinstance(image, GeneratedImage) else ".jpg") random_name = f"img_{uuid.uuid4().hex}{suffix}" new_path = temp_dir / random_name @@ -628,7 +630,7 @@ def _get_model_by_name(name: str) -> Model: def _get_available_models() -> list[ModelData]: """Return a list of available models based on configuration strategy.""" - now = int(datetime.now(tz=timezone.utc).timestamp()) + now = int(datetime.now(tz=UTC).timestamp()) strategy = g_config.gemini.model_strategy models_data = [] @@ -712,7 +714,7 @@ async def _send_with_split( text: str, files: list[Path | str | io.BytesIO] | None = None, stream: bool = False, -) -> AsyncGenerator[ModelOutput, None] | ModelOutput: +) -> AsyncGenerator[ModelOutput] | ModelOutput: """Send text to Gemini, splitting or converting to attachment if too long.""" if len(text) <= MAX_CHARS_PER_REQUEST: try: @@ -1013,9 +1015,7 @@ def flush(self) -> str: res = "" if self.state in ("IN_TOOL", "IN_ORPHAN", "IN_RESP", "IN_HINT", "IN_ARG", "IN_RESULT"): res = "" - elif self.state == "IN_BLOCK" and self.current_role != "tool": - res = self.buffer - elif self.state == "NORMAL": + elif (self.state == "IN_BLOCK" and self.current_role != "tool") or self.state == "NORMAL": res = self.buffer self.buffer = "" @@ -1027,7 +1027,7 @@ def flush(self) -> str: def _create_real_streaming_response( - generator: AsyncGenerator[ModelOutput, None], + generator: AsyncGenerator[ModelOutput], completion_id: str, created_time: int, model_name: str, @@ -1221,7 +1221,7 @@ async def generate_stream(): def _create_responses_real_streaming_response( - generator: AsyncGenerator[ModelOutput, None], + generator: AsyncGenerator[ModelOutput], response_id: str, created_time: int, model_name: str, @@ -1455,10 +1455,12 @@ async def create_chat_completion( m_input, files = await GeminiClientWrapper.process_conversation(msgs, tmp_dir) except Exception as e: logger.exception("Error in preparing conversation") - raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=str(e)) + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=str(e) + ) from e completion_id = f"chatcmpl-{uuid.uuid4()}" - created_time = int(datetime.now(tz=timezone.utc).timestamp()) + created_time = int(datetime.now(tz=UTC).timestamp()) try: assert session and client @@ -1470,7 +1472,7 @@ async def create_chat_completion( ) except Exception as e: logger.exception("Gemini API error") - raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(e)) + raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(e)) from e if request.stream: return _create_real_streaming_response( @@ -1620,10 +1622,12 @@ async def create_response( m_input, files = await GeminiClientWrapper.process_conversation(messages, tmp_dir) except Exception as e: logger.exception("Error in preparing conversation") - raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=str(e)) + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=str(e) + ) from e response_id = f"resp_{uuid.uuid4().hex}" - created_time = int(datetime.now(tz=timezone.utc).timestamp()) + created_time = int(datetime.now(tz=UTC).timestamp()) try: assert session and client @@ -1635,7 +1639,7 @@ async def create_response( ) except Exception as e: logger.exception("Gemini API error") - raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(e)) + raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(e)) from e if request.stream: return _create_responses_real_streaming_response( diff --git a/app/server/health.py b/app/server/health.py index f521db1..444c938 100644 --- a/app/server/health.py +++ b/app/server/health.py @@ -1,8 +1,8 @@ from fastapi import APIRouter from loguru import logger -from ..models import HealthCheckResponse -from ..services import GeminiClientPool, LMDBConversationStore +from app.models import HealthCheckResponse +from app.services import GeminiClientPool, LMDBConversationStore router = APIRouter() diff --git a/app/server/images.py b/app/server/images.py index fe078f7..e1c161c 100644 --- a/app/server/images.py +++ b/app/server/images.py @@ -1,7 +1,7 @@ from fastapi import APIRouter, HTTPException, Query from fastapi.responses import FileResponse -from ..server.middleware import get_image_store_dir, verify_image_token +from app.server.middleware import get_image_store_dir, verify_image_token router = APIRouter() diff --git a/app/server/middleware.py b/app/server/middleware.py index 630e1f5..4bc358d 100644 --- a/app/server/middleware.py +++ b/app/server/middleware.py @@ -10,7 +10,7 @@ from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from loguru import logger -from ..utils import g_config +from app.utils import g_config # Persistent directory for storing generated images IMAGE_STORE_DIR = Path(tempfile.gettempdir()) / "ai_generated_images" diff --git a/app/services/client.py b/app/services/client.py index 70dfce9..49d9e87 100644 --- a/app/services/client.py +++ b/app/services/client.py @@ -5,9 +5,9 @@ from gemini_webapi import GeminiClient, ModelOutput from loguru import logger -from ..models import Message -from ..utils import g_config -from ..utils.helper import ( +from app.models import Message +from app.utils import g_config +from app.utils.helper import ( add_tag, normalize_llm_text, save_file_to_tempfile, @@ -146,9 +146,8 @@ async def process_message( model_input = "\n".join(fragment for fragment in text_fragments if fragment is not None) - if model_input or message.role == "tool": - if tagged: - model_input = add_tag(message.role, model_input) + if (model_input or message.role == "tool") and tagged: + model_input = add_tag(message.role, model_input) return model_input, files diff --git a/app/services/lmdb.py b/app/services/lmdb.py index abf8859..87a1449 100644 --- a/app/services/lmdb.py +++ b/app/services/lmdb.py @@ -4,22 +4,22 @@ from contextlib import contextmanager from datetime import datetime, timedelta from pathlib import Path -from typing import Any, Dict, List, Optional +from typing import Any import lmdb import orjson from loguru import logger -from ..models import ContentItem, ConversationInStore, Message -from ..utils import g_config -from ..utils.helper import ( +from app.models import ContentItem, ConversationInStore, Message +from app.utils import g_config +from app.utils.helper import ( extract_tool_calls, normalize_llm_text, remove_tool_call_blocks, strip_system_hints, unescape_text, ) -from ..utils.singleton import Singleton +from app.utils.singleton import Singleton _VOLATILE_TRANS_TABLE = str.maketrans("", "", string.whitespace + string.punctuation) @@ -125,7 +125,7 @@ def _hash_message(message: Message, fuzzy: bool = False) -> str: def _hash_conversation( - client_id: str, model: str, messages: List[Message], fuzzy: bool = False + client_id: str, model: str, messages: list[Message], fuzzy: bool = False ) -> str: """Generate a hash for a list of messages and model name, tied to a specific client_id.""" combined_hash = hashlib.sha256() @@ -145,9 +145,9 @@ class LMDBConversationStore(metaclass=Singleton): def __init__( self, - db_path: Optional[str] = None, - max_db_size: Optional[int] = None, - retention_days: Optional[int] = None, + db_path: str | None = None, + max_db_size: int | None = None, + retention_days: int | None = None, ): """ Initialize LMDB store. @@ -219,7 +219,7 @@ def _get_transaction(self, write: bool = False): raise @staticmethod - def _decode_index_value(data: bytes) -> List[str]: + def _decode_index_value(data: bytes) -> list[str]: """Decode index value, handling both legacy single-string and new list-of-strings formats.""" if not data: return [] @@ -238,7 +238,7 @@ def _decode_index_value(data: bytes) -> List[str]: @staticmethod def _update_index(txn: lmdb.Transaction, prefix: str, hash_val: str, storage_key: str): """Add a storage key to the index for a given hash, avoiding duplicates.""" - idx_key = f"{prefix}{hash_val}".encode("utf-8") + idx_key = f"{prefix}{hash_val}".encode() existing = txn.get(idx_key) keys = LMDBConversationStore._decode_index_value(existing) if existing else [] if storage_key not in keys: @@ -248,7 +248,7 @@ def _update_index(txn: lmdb.Transaction, prefix: str, hash_val: str, storage_key @staticmethod def _remove_from_index(txn: lmdb.Transaction, prefix: str, hash_val: str, storage_key: str): """Remove a specific storage key from the index for a given hash.""" - idx_key = f"{prefix}{hash_val}".encode("utf-8") + idx_key = f"{prefix}{hash_val}".encode() existing = txn.get(idx_key) if not existing: return @@ -263,7 +263,7 @@ def _remove_from_index(txn: lmdb.Transaction, prefix: str, hash_val: str, storag def store( self, conv: ConversationInStore, - custom_key: Optional[str] = None, + custom_key: str | None = None, ) -> str: """ Store a conversation model in LMDB. @@ -312,7 +312,7 @@ def store( ) raise - def get(self, key: str) -> Optional[ConversationInStore]: + def get(self, key: str) -> ConversationInStore | None: """ Retrieve conversation data by key. @@ -340,7 +340,7 @@ def get(self, key: str) -> Optional[ConversationInStore]: logger.error(f"Unexpected error retrieving messages with key {key[:12]}: {e}") return None - def find(self, model: str, messages: List[Message]) -> Optional[ConversationInStore]: + def find(self, model: str, messages: list[Message]) -> ConversationInStore | None: """ Search conversation data by message list. Tries raw matching, then sanitized matching, and finally fuzzy matching. @@ -360,12 +360,13 @@ def find(self, model: str, messages: List[Message]) -> Optional[ConversationInSt return conv cleaned_messages = self.sanitize_messages(messages) - if cleaned_messages != messages: - if conv := self._find_by_message_list(model, cleaned_messages): - logger.debug( - f"Session found for '{model}' with {len(cleaned_messages)} cleaned messages." - ) - return conv + if cleaned_messages != messages and ( + conv := self._find_by_message_list(model, cleaned_messages) + ): + logger.debug( + f"Session found for '{model}' with {len(cleaned_messages)} cleaned messages." + ) + return conv if conv := self._find_by_message_list(model, messages, fuzzy=True): logger.debug( @@ -379,9 +380,9 @@ def find(self, model: str, messages: List[Message]) -> Optional[ConversationInSt def _find_by_message_list( self, model: str, - messages: List[Message], + messages: list[Message], fuzzy: bool = False, - ) -> Optional[ConversationInStore]: + ) -> ConversationInStore | None: """ Internal find implementation based on a message list. @@ -440,7 +441,7 @@ def exists(self, key: str) -> bool: logger.error(f"Failed to check existence of key {key}: {e}") return False - def delete(self, key: str) -> Optional[ConversationInStore]: + def delete(self, key: str) -> ConversationInStore | None: """Delete conversation model by key.""" try: with self._get_transaction(write=True) as txn: @@ -466,7 +467,7 @@ def delete(self, key: str) -> Optional[ConversationInStore]: logger.error(f"Failed to delete messages with key {key[:12]}: {e}") return None - def keys(self, prefix: str = "", limit: Optional[int] = None) -> List[str]: + def keys(self, prefix: str = "", limit: int | None = None) -> list[str]: """List all keys in the store, optionally filtered by prefix.""" keys = [] try: @@ -492,7 +493,7 @@ def keys(self, prefix: str = "", limit: Optional[int] = None) -> List[str]: logger.error(f"Failed to list keys: {e}") return keys - def cleanup_expired(self, retention_days: Optional[int] = None) -> int: + def cleanup_expired(self, retention_days: int | None = None) -> int: """Delete conversations older than the given retention period.""" retention_value = ( self.retention_days if retention_days is None else max(0, int(retention_days)) @@ -561,7 +562,7 @@ def cleanup_expired(self, retention_days: Optional[int] = None) -> int: return removed - def stats(self) -> Dict[str, Any]: + def stats(self) -> dict[str, Any]: """Get database statistics.""" if not self._env: logger.error("LMDB environment not initialized") diff --git a/app/services/pool.py b/app/services/pool.py index decc21a..3b4197c 100644 --- a/app/services/pool.py +++ b/app/services/pool.py @@ -1,11 +1,11 @@ import asyncio from collections import deque -from typing import Dict, List, Optional from loguru import logger -from ..utils import g_config -from ..utils.singleton import Singleton +from app.utils import g_config +from app.utils.singleton import Singleton + from .client import GeminiClientWrapper @@ -13,10 +13,10 @@ class GeminiClientPool(metaclass=Singleton): """Pool of GeminiClient instances identified by unique ids.""" def __init__(self) -> None: - self._clients: List[GeminiClientWrapper] = [] - self._id_map: Dict[str, GeminiClientWrapper] = {} + self._clients: list[GeminiClientWrapper] = [] + self._id_map: dict[str, GeminiClientWrapper] = {} self._round_robin: deque[GeminiClientWrapper] = deque() - self._restart_locks: Dict[str, asyncio.Lock] = {} + self._restart_locks: dict[str, asyncio.Lock] = {} if len(g_config.gemini.clients) == 0: raise ValueError("No Gemini clients configured") @@ -55,7 +55,7 @@ async def init(self) -> None: if success_count == 0: raise RuntimeError("Failed to initialize any Gemini clients") - async def acquire(self, client_id: Optional[str] = None) -> GeminiClientWrapper: + async def acquire(self, client_id: str | None = None) -> GeminiClientWrapper: """Return a healthy client by id or using round-robin.""" if not self._round_robin: raise RuntimeError("No Gemini clients configured") @@ -106,10 +106,10 @@ async def _ensure_client_ready(self, client: GeminiClientWrapper) -> bool: return False @property - def clients(self) -> List[GeminiClientWrapper]: + def clients(self) -> list[GeminiClientWrapper]: """Return managed clients.""" return self._clients - def status(self) -> Dict[str, bool]: + def status(self) -> dict[str, bool]: """Return running status for each client.""" return {client.id: client.running() for client in self._clients} diff --git a/app/utils/config.py b/app/utils/config.py index 4c1709f..21d2891 100644 --- a/app/utils/config.py +++ b/app/utils/config.py @@ -1,7 +1,7 @@ import ast import os import sys -from typing import Any, Literal, Optional +from typing import Any, Literal import orjson from loguru import logger @@ -28,7 +28,7 @@ class ServerConfig(BaseModel): host: str = Field(default="0.0.0.0", description="Server host address") port: int = Field(default=8000, ge=1, le=65535, description="Server port number") - api_key: Optional[str] = Field( + api_key: str | None = Field( default=None, description="API key for authentication, if set, will enable API key validation", ) @@ -41,11 +41,11 @@ class GeminiClientSettings(BaseModel): id: str = Field(..., description="Unique identifier for the client") secure_1psid: str = Field(..., description="Gemini Secure 1PSID") secure_1psidts: str = Field(..., description="Gemini Secure 1PSIDTS") - proxy: Optional[str] = Field(default=None, description="Proxy URL for this Gemini client") + proxy: str | None = Field(default=None, description="Proxy URL for this Gemini client") @field_validator("proxy", mode="before") @classmethod - def _blank_proxy_to_none(cls, value: Optional[str]) -> Optional[str]: + def _blank_proxy_to_none(cls, value: str | None) -> str | None: if value is None: return None stripped = value.strip() @@ -55,8 +55,8 @@ def _blank_proxy_to_none(cls, value: Optional[str]) -> Optional[str]: class GeminiModelConfig(BaseModel): """Configuration for a custom Gemini model.""" - model_name: Optional[str] = Field(default=None, description="Name of the model") - model_header: Optional[dict[str, Optional[str]]] = Field( + model_name: str | None = Field(default=None, description="Name of the model") + model_header: dict[str, str | None] | None = Field( default=None, description="Header for the model" ) diff --git a/app/utils/helper.py b/app/utils/helper.py index 64df4f7..002d401 100644 --- a/app/utils/helper.py +++ b/app/utils/helper.py @@ -14,7 +14,7 @@ import orjson from loguru import logger -from ..models import FunctionCall, Message, ToolCall +from app.models import FunctionCall, Message, ToolCall VALID_TAG_ROLES = {"user", "assistant", "system", "tool"} TOOL_WRAP_HINT = ( @@ -67,6 +67,7 @@ ) CHATML_END_RE = re.compile(r"<\|im_end\|>|\\<\\\|im\\_end\\\|\\>", re.IGNORECASE) COMMONMARK_UNESCAPE_RE = re.compile(r"\\([!\"#$%&'()*+,\-./:;<=>?@\[\\\]^_`{|}~])") +PARAM_FENCE_RE = re.compile(r"^(?P`{3,})") TOOL_HINT_STRIPPED = TOOL_WRAP_HINT.strip() _hint_lines = [line.strip() for line in TOOL_WRAP_HINT.split("\n") if line.strip()] TOOL_HINT_LINE_START = _hint_lines[0] if _hint_lines else "" @@ -113,7 +114,7 @@ def _strip_param_fences(s: str) -> str: if not s: return "" - match = re.match(r"^(?P`{3,})", s) + match = PARAM_FENCE_RE.match(s) if not match or not s.endswith(match.group("fence")): return s @@ -272,7 +273,7 @@ def _create_tool_call(name: str, raw_args: str) -> None: arguments = "{}" index = len(tool_calls) - seed = f"{name}:{arguments}:{index}".encode("utf-8") + seed = f"{name}:{arguments}:{index}".encode() call_id = f"call_{hashlib.sha256(seed).hexdigest()[:24]}" tool_calls.append( diff --git a/app/utils/singleton.py b/app/utils/singleton.py index 489e87e..2a258af 100644 --- a/app/utils/singleton.py +++ b/app/utils/singleton.py @@ -1,10 +1,10 @@ -from typing import ClassVar, Dict +from typing import ClassVar class Singleton(type): - _instances: ClassVar[Dict[type, object]] = {} + _instances: ClassVar[dict[type, object]] = {} def __call__(cls, *args, **kwargs): if cls not in cls._instances: - cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) + cls._instances[cls] = super().__call__(*args, **kwargs) return cls._instances[cls] diff --git a/pyproject.toml b/pyproject.toml index 0cae786..a1ae29d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,33 +3,61 @@ name = "gemini-fastapi" version = "1.0.0" description = "FastAPI Server built on Gemini Web API" readme = "README.md" -requires-python = "==3.12.*" +requires-python = "==3.13.*" dependencies = [ "fastapi>=0.129.0", "gemini-webapi>=1.19.2", + "httptools>=0.7.1", "lmdb>=1.7.5", "loguru>=0.7.3", "orjson>=3.11.7", - "pydantic-settings[yaml]>=2.12.0", - "uvicorn>=0.40.0", + "pydantic-settings[yaml]>=2.13.0", + "uvicorn>=0.41.0", "uvloop>=0.22.1; sys_platform != 'win32'", ] +[project.urls] +Repository = "https://github.com/Nativu5/Gemini-FastAPI" + [project.optional-dependencies] dev = [ - "ruff>=0.15.0", + "pytest>=9.0.2", + "ruff>=0.15.1", +] + +[dependency-groups] +dev = [ + "gemini-fastapi[dev]", ] [tool.ruff] line-length = 100 -lint.select = ["E", "F", "W", "I", "RUF"] -lint.ignore = ["E501"] +target-version = "py313" + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "F", # pyflakes + "W", # pycodestyle warnings + "I", # isort + "UP", # pyupgrade + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "SIM", # flake8-simplify + "RUF", # ruff-specific rules + "TID", # flake8-tidy-imports +] +ignore = [ + "E501", # line too long +] + +[tool.ruff.lint.flake8-bugbear] +extend-immutable-calls = [ + "fastapi.Depends", + "fastapi.Query", + "fastapi.security.HTTPBearer", +] [tool.ruff.format] quote-style = "double" indent-style = "space" - -[dependency-groups] -dev = [ - "ruff>=0.15.1", -] diff --git a/scripts/dump_lmdb.py b/scripts/dump_lmdb.py index a331325..889af4f 100644 --- a/scripts/dump_lmdb.py +++ b/scripts/dump_lmdb.py @@ -1,6 +1,7 @@ import argparse +from collections.abc import Iterable from pathlib import Path -from typing import Any, Iterable, List +from typing import Any import lmdb import orjson @@ -14,17 +15,17 @@ def _decode_value(value: bytes) -> Any: return value.decode("utf-8", errors="replace") -def _dump_all(txn: lmdb.Transaction) -> List[dict[str, Any]]: +def _dump_all(txn: lmdb.Transaction) -> list[dict[str, Any]]: """Return all records from the database.""" - result: List[dict[str, Any]] = [] + result: list[dict[str, Any]] = [] for key, value in txn.cursor(): result.append({"key": key.decode("utf-8"), "value": _decode_value(value)}) return result -def _dump_selected(txn: lmdb.Transaction, keys: Iterable[str]) -> List[dict[str, Any]]: +def _dump_selected(txn: lmdb.Transaction, keys: Iterable[str]) -> list[dict[str, Any]]: """Return records for the provided keys.""" - result: List[dict[str, Any]] = [] + result: list[dict[str, Any]] = [] for key in keys: raw = txn.get(key.encode("utf-8")) if raw is not None: @@ -36,10 +37,7 @@ def dump_lmdb(path: Path, keys: Iterable[str] | None = None) -> None: """Print selected or all key-value pairs from the LMDB database.""" env = lmdb.open(str(path), readonly=True, lock=False) with env.begin() as txn: - if keys: - records = _dump_selected(txn, keys) - else: - records = _dump_all(txn) + records = _dump_selected(txn, keys) if keys else _dump_all(txn) env.close() print(orjson.dumps(records, option=orjson.OPT_INDENT_2).decode("utf-8")) diff --git a/uv.lock b/uv.lock index 5b687e4..4c819e7 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,6 @@ version = 1 revision = 3 -requires-python = "==3.12.*" +requires-python = "==3.13.*" [[package]] name = "annotated-doc" @@ -26,7 +26,6 @@ version = "4.12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, - { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } wheels = [ @@ -86,6 +85,7 @@ source = { virtual = "." } dependencies = [ { name = "fastapi" }, { name = "gemini-webapi" }, + { name = "httptools" }, { name = "lmdb" }, { name = "loguru" }, { name = "orjson" }, @@ -96,30 +96,33 @@ dependencies = [ [package.optional-dependencies] dev = [ + { name = "pytest" }, { name = "ruff" }, ] [package.dev-dependencies] dev = [ - { name = "ruff" }, + { name = "gemini-fastapi", extra = ["dev"] }, ] [package.metadata] requires-dist = [ { name = "fastapi", specifier = ">=0.129.0" }, { name = "gemini-webapi", specifier = ">=1.19.2" }, + { name = "httptools", specifier = ">=0.7.1" }, { name = "lmdb", specifier = ">=1.7.5" }, { name = "loguru", specifier = ">=0.7.3" }, { name = "orjson", specifier = ">=3.11.7" }, - { name = "pydantic-settings", extras = ["yaml"], specifier = ">=2.12.0" }, - { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.15.0" }, - { name = "uvicorn", specifier = ">=0.40.0" }, + { name = "pydantic-settings", extras = ["yaml"], specifier = ">=2.13.0" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0.2" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.15.1" }, + { name = "uvicorn", specifier = ">=0.41.0" }, { name = "uvloop", marker = "sys_platform != 'win32'", specifier = ">=0.22.1" }, ] provides-extras = ["dev"] [package.metadata.requires-dev] -dev = [{ name = "ruff", specifier = ">=0.15.1" }] +dev = [{ name = "gemini-fastapi", extras = ["dev"] }] [[package]] name = "gemini-webapi" @@ -180,6 +183,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, ] +[[package]] +name = "httptools" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" }, + { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" }, + { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" }, + { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" }, + { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" }, + { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" }, +] + [[package]] name = "httpx" version = "0.28.1" @@ -218,18 +236,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + [[package]] name = "lmdb" version = "1.7.5" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c7/a3/3756f2c6adba4a1413dba55e6c81a20b38a868656517308533e33cb59e1c/lmdb-1.7.5.tar.gz", hash = "sha256:f0604751762cb097059d5412444c4057b95f386c7ed958363cf63f453e5108da", size = 883490, upload-time = "2025-10-15T03:39:44.038Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/34/b4/8b862c4d7fd6f68cb33e2a919169fda8924121dc5ff61e3cc105304a6dd4/lmdb-1.7.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b48c2359eea876d7b634b49f84019ecc8c1626da97c795fc7b39a793676815df", size = 100910, upload-time = "2025-10-15T03:39:00.727Z" }, - { url = "https://files.pythonhosted.org/packages/27/64/8ab5da48180d5f13a293ea00a9f8758b1bee080e76ea0ab0d6be0d51b55f/lmdb-1.7.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f84793baeb430ba984eb6c1b4e08c0a508b1c03e79ce79fcda0f29ecc06a95a", size = 99376, upload-time = "2025-10-15T03:39:01.791Z" }, - { url = "https://files.pythonhosted.org/packages/43/e0/51bc942fe5ed3fce69c631b54f52d97785de3d94487376139be6de1e199a/lmdb-1.7.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68cc21314a33faac1b749645a976b7655e7fa7cc104a72365d2429d2db7f6342", size = 298556, upload-time = "2025-10-15T03:39:02.787Z" }, - { url = "https://files.pythonhosted.org/packages/66/c5/19ea75c88b91d12da5c6f4bbe2aca633047b6b270fd613d557583d32cc5c/lmdb-1.7.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f2d9b7e102fcfe5e0cfb3acdebd403eb55ccbe5f7202d8f49d60bdafb1546d1e", size = 299449, upload-time = "2025-10-15T03:39:03.903Z" }, - { url = "https://files.pythonhosted.org/packages/1b/74/365194203dbff47d3a1621366d6a1133cdcce261f4ac0e1d0496f01e6ace/lmdb-1.7.5-cp312-cp312-win_amd64.whl", hash = "sha256:69de89cc79e03e191fc6f95797f1bef91b45c415d1ea9d38872b00b2d989a50f", size = 99328, upload-time = "2025-10-15T03:39:04.949Z" }, - { url = "https://files.pythonhosted.org/packages/3f/3a/a441afebff5bd761f7f58d194fed7ac265279964957479a5c8a51c42f9ad/lmdb-1.7.5-cp312-cp312-win_arm64.whl", hash = "sha256:0c880ee4b309e900f2d58a710701f5e6316a351878588c6a95a9c0bcb640680b", size = 94191, upload-time = "2025-10-15T03:39:05.975Z" }, + { url = "https://files.pythonhosted.org/packages/38/f8/03275084218eacdbdf7e185d693e1db4cb79c35d18fac47fa0d388522a0d/lmdb-1.7.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:66ae02fa6179e46bb69fe446b7e956afe8706ae17ec1d4cd9f7056e161019156", size = 101508, upload-time = "2025-10-15T03:39:07.228Z" }, + { url = "https://files.pythonhosted.org/packages/20/b9/bc33ae2e4940359ba2fc412e6a755a2f126bc5062b4aaf35edd3a791f9a5/lmdb-1.7.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf65c573311ac8330c7908257f76b28ae3576020123400a81a6b650990dc028c", size = 100105, upload-time = "2025-10-15T03:39:08.491Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f6/22f84b776a64d3992f052ecb637c35f1764a39df4f2190ecc5a3a1295bd7/lmdb-1.7.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97bcb3fc12841a8828db918e494fe0fd016a73d2680ad830d75719bb3bf4e76a", size = 301500, upload-time = "2025-10-15T03:39:09.463Z" }, + { url = "https://files.pythonhosted.org/packages/2a/4d/8e6be8d7d5a30d47fa0ce4b55e3a8050ad689556e6e979d206b4ac67b733/lmdb-1.7.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:865f374f6206ab4aacb92ffb1dc612ee1a31a421db7c89733abe06b81ac87cb0", size = 302285, upload-time = "2025-10-15T03:39:10.856Z" }, + { url = "https://files.pythonhosted.org/packages/5e/dc/7e04fb31a8f88951db81ac677e3ccb3e09248eda40e6ad52f74fd9370c32/lmdb-1.7.5-cp313-cp313-win_amd64.whl", hash = "sha256:82a04d5ca2a6a799c8db7f209354c48aebb49ff338530f5813721fc4c68e4450", size = 99447, upload-time = "2025-10-15T03:39:12.151Z" }, + { url = "https://files.pythonhosted.org/packages/5b/50/e3f97efab17b3fad4afde99b3c957ecac4ffbefada6874a57ad0c695660a/lmdb-1.7.5-cp313-cp313-win_arm64.whl", hash = "sha256:0ad85a15acbfe8a42fdef92ee5e869610286d38507e976755f211be0fc905ca7", size = 94145, upload-time = "2025-10-15T03:39:13.461Z" }, { url = "https://files.pythonhosted.org/packages/bd/2c/982cb5afed533d0cb8038232b40c19b5b85a2d887dec74dfd39e8351ef4b/lmdb-1.7.5-py3-none-any.whl", hash = "sha256:fc344bb8bc0786c87c4ccb19b31f09a38c08bd159ada6f037d669426fea06f03", size = 148539, upload-time = "2025-10-15T03:39:42.982Z" }, ] @@ -252,21 +279,39 @@ version = "3.11.7" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/53/45/b268004f745ede84e5798b48ee12b05129d19235d0e15267aa57dcdb400b/orjson-3.11.7.tar.gz", hash = "sha256:9b1a67243945819ce55d24a30b59d6a168e86220452d2c96f4d1f093e71c0c49", size = 6144992, upload-time = "2026-02-02T15:38:49.29Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/80/bf/76f4f1665f6983385938f0e2a5d7efa12a58171b8456c252f3bae8a4cf75/orjson-3.11.7-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bd03ea7606833655048dab1a00734a2875e3e86c276e1d772b2a02556f0d895f", size = 228545, upload-time = "2026-02-02T15:37:46.376Z" }, - { url = "https://files.pythonhosted.org/packages/79/53/6c72c002cb13b5a978a068add59b25a8bdf2800ac1c9c8ecdb26d6d97064/orjson-3.11.7-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:89e440ebc74ce8ab5c7bc4ce6757b4a6b1041becb127df818f6997b5c71aa60b", size = 125224, upload-time = "2026-02-02T15:37:47.697Z" }, - { url = "https://files.pythonhosted.org/packages/2c/83/10e48852865e5dd151bdfe652c06f7da484578ed02c5fca938e3632cb0b8/orjson-3.11.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ede977b5fe5ac91b1dffc0a517ca4542d2ec8a6a4ff7b2652d94f640796342a", size = 128154, upload-time = "2026-02-02T15:37:48.954Z" }, - { url = "https://files.pythonhosted.org/packages/6e/52/a66e22a2b9abaa374b4a081d410edab6d1e30024707b87eab7c734afe28d/orjson-3.11.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b7b1dae39230a393df353827c855a5f176271c23434cfd2db74e0e424e693e10", size = 123548, upload-time = "2026-02-02T15:37:50.187Z" }, - { url = "https://files.pythonhosted.org/packages/de/38/605d371417021359f4910c496f764c48ceb8997605f8c25bf1dfe58c0ebe/orjson-3.11.7-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed46f17096e28fb28d2975834836a639af7278aa87c84f68ab08fbe5b8bd75fa", size = 129000, upload-time = "2026-02-02T15:37:51.426Z" }, - { url = "https://files.pythonhosted.org/packages/44/98/af32e842b0ffd2335c89714d48ca4e3917b42f5d6ee5537832e069a4b3ac/orjson-3.11.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3726be79e36e526e3d9c1aceaadbfb4a04ee80a72ab47b3f3c17fefb9812e7b8", size = 141686, upload-time = "2026-02-02T15:37:52.607Z" }, - { url = "https://files.pythonhosted.org/packages/96/0b/fc793858dfa54be6feee940c1463370ece34b3c39c1ca0aa3845f5ba9892/orjson-3.11.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0724e265bc548af1dedebd9cb3d24b4e1c1e685a343be43e87ba922a5c5fff2f", size = 130812, upload-time = "2026-02-02T15:37:53.944Z" }, - { url = "https://files.pythonhosted.org/packages/dc/91/98a52415059db3f374757d0b7f0f16e3b5cd5976c90d1c2b56acaea039e6/orjson-3.11.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7745312efa9e11c17fbd3cb3097262d079da26930ae9ae7ba28fb738367cbad", size = 133440, upload-time = "2026-02-02T15:37:55.615Z" }, - { url = "https://files.pythonhosted.org/packages/dc/b6/cb540117bda61791f46381f8c26c8f93e802892830a6055748d3bb1925ab/orjson-3.11.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f904c24bdeabd4298f7a977ef14ca2a022ca921ed670b92ecd16ab6f3d01f867", size = 138386, upload-time = "2026-02-02T15:37:56.814Z" }, - { url = "https://files.pythonhosted.org/packages/63/1a/50a3201c334a7f17c231eee5f841342190723794e3b06293f26e7cf87d31/orjson-3.11.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b9fc4d0f81f394689e0814617aadc4f2ea0e8025f38c226cbf22d3b5ddbf025d", size = 408853, upload-time = "2026-02-02T15:37:58.291Z" }, - { url = "https://files.pythonhosted.org/packages/87/cd/8de1c67d0be44fdc22701e5989c0d015a2adf391498ad42c4dc589cd3013/orjson-3.11.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:849e38203e5be40b776ed2718e587faf204d184fc9a008ae441f9442320c0cab", size = 144130, upload-time = "2026-02-02T15:38:00.163Z" }, - { url = "https://files.pythonhosted.org/packages/0f/fe/d605d700c35dd55f51710d159fc54516a280923cd1b7e47508982fbb387d/orjson-3.11.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4682d1db3bcebd2b64757e0ddf9e87ae5f00d29d16c5cdf3a62f561d08cc3dd2", size = 134818, upload-time = "2026-02-02T15:38:01.507Z" }, - { url = "https://files.pythonhosted.org/packages/e4/e4/15ecc67edb3ddb3e2f46ae04475f2d294e8b60c1825fbe28a428b93b3fbd/orjson-3.11.7-cp312-cp312-win32.whl", hash = "sha256:f4f7c956b5215d949a1f65334cf9d7612dde38f20a95f2315deef167def91a6f", size = 127923, upload-time = "2026-02-02T15:38:02.75Z" }, - { url = "https://files.pythonhosted.org/packages/34/70/2e0855361f76198a3965273048c8e50a9695d88cd75811a5b46444895845/orjson-3.11.7-cp312-cp312-win_amd64.whl", hash = "sha256:bf742e149121dc5648ba0a08ea0871e87b660467ef168a3a5e53bc1fbd64bb74", size = 125007, upload-time = "2026-02-02T15:38:04.032Z" }, - { url = "https://files.pythonhosted.org/packages/68/40/c2051bd19fc467610fed469dc29e43ac65891571138f476834ca192bc290/orjson-3.11.7-cp312-cp312-win_arm64.whl", hash = "sha256:26c3b9132f783b7d7903bf1efb095fed8d4a3a85ec0d334ee8beff3d7a4749d5", size = 126089, upload-time = "2026-02-02T15:38:05.297Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/6e0e52cac5aab51d7b6dcd257e855e1dec1c2060f6b28566c509b4665f62/orjson-3.11.7-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1d98b30cc1313d52d4af17d9c3d307b08389752ec5f2e5febdfada70b0f8c733", size = 228390, upload-time = "2026-02-02T15:38:06.8Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/a77f48d2fc8a05bbc529e5ff481fb43d914f9e383ea2469d4f3d51df3d00/orjson-3.11.7-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:d897e81f8d0cbd2abb82226d1860ad2e1ab3ff16d7b08c96ca00df9d45409ef4", size = 125189, upload-time = "2026-02-02T15:38:08.181Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/0a16e0729a0e6a1504f9d1a13cdd365f030068aab64cec6958396b9969d7/orjson-3.11.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:814be4b49b228cfc0b3c565acf642dd7d13538f966e3ccde61f4f55be3e20785", size = 128106, upload-time = "2026-02-02T15:38:09.41Z" }, + { url = "https://files.pythonhosted.org/packages/66/da/a2e505469d60666a05ab373f1a6322eb671cb2ba3a0ccfc7d4bc97196787/orjson-3.11.7-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d06e5c5fed5caedd2e540d62e5b1c25e8c82431b9e577c33537e5fa4aa909539", size = 123363, upload-time = "2026-02-02T15:38:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/23/bf/ed73f88396ea35c71b38961734ea4a4746f7ca0768bf28fd551d37e48dd0/orjson-3.11.7-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31c80ce534ac4ea3739c5ee751270646cbc46e45aea7576a38ffec040b4029a1", size = 129007, upload-time = "2026-02-02T15:38:12.138Z" }, + { url = "https://files.pythonhosted.org/packages/73/3c/b05d80716f0225fc9008fbf8ab22841dcc268a626aa550561743714ce3bf/orjson-3.11.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f50979824bde13d32b4320eedd513431c921102796d86be3eee0b58e58a3ecd1", size = 141667, upload-time = "2026-02-02T15:38:13.398Z" }, + { url = "https://files.pythonhosted.org/packages/61/e8/0be9b0addd9bf86abfc938e97441dcd0375d494594b1c8ad10fe57479617/orjson-3.11.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e54f3808e2b6b945078c41aa8d9b5834b28c50843846e97807e5adb75fa9705", size = 130832, upload-time = "2026-02-02T15:38:14.698Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ec/c68e3b9021a31d9ec15a94931db1410136af862955854ed5dd7e7e4f5bff/orjson-3.11.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12b80df61aab7b98b490fe9e4879925ba666fccdfcd175252ce4d9035865ace", size = 133373, upload-time = "2026-02-02T15:38:16.109Z" }, + { url = "https://files.pythonhosted.org/packages/d2/45/f3466739aaafa570cc8e77c6dbb853c48bf56e3b43738020e2661e08b0ac/orjson-3.11.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:996b65230271f1a97026fd0e6a753f51fbc0c335d2ad0c6201f711b0da32693b", size = 138307, upload-time = "2026-02-02T15:38:17.453Z" }, + { url = "https://files.pythonhosted.org/packages/e1/84/9f7f02288da1ffb31405c1be07657afd1eecbcb4b64ee2817b6fe0f785fa/orjson-3.11.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ab49d4b2a6a1d415ddb9f37a21e02e0d5dbfe10b7870b21bf779fc21e9156157", size = 408695, upload-time = "2026-02-02T15:38:18.831Z" }, + { url = "https://files.pythonhosted.org/packages/18/07/9dd2f0c0104f1a0295ffbe912bc8d63307a539b900dd9e2c48ef7810d971/orjson-3.11.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:390a1dce0c055ddf8adb6aa94a73b45a4a7d7177b5c584b8d1c1947f2ba60fb3", size = 144099, upload-time = "2026-02-02T15:38:20.28Z" }, + { url = "https://files.pythonhosted.org/packages/a5/66/857a8e4a3292e1f7b1b202883bcdeb43a91566cf59a93f97c53b44bd6801/orjson-3.11.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1eb80451a9c351a71dfaf5b7ccc13ad065405217726b59fdbeadbcc544f9d223", size = 134806, upload-time = "2026-02-02T15:38:22.186Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5b/6ebcf3defc1aab3a338ca777214966851e92efb1f30dc7fc8285216e6d1b/orjson-3.11.7-cp313-cp313-win32.whl", hash = "sha256:7477aa6a6ec6139c5cb1cc7b214643592169a5494d200397c7fc95d740d5fcf3", size = 127914, upload-time = "2026-02-02T15:38:23.511Z" }, + { url = "https://files.pythonhosted.org/packages/00/04/c6f72daca5092e3117840a1b1e88dfc809cc1470cf0734890d0366b684a1/orjson-3.11.7-cp313-cp313-win_amd64.whl", hash = "sha256:b9f95dcdea9d4f805daa9ddf02617a89e484c6985fa03055459f90e87d7a0757", size = 124986, upload-time = "2026-02-02T15:38:24.836Z" }, + { url = "https://files.pythonhosted.org/packages/03/ba/077a0f6f1085d6b806937246860fafbd5b17f3919c70ee3f3d8d9c713f38/orjson-3.11.7-cp313-cp313-win_arm64.whl", hash = "sha256:800988273a014a0541483dc81021247d7eacb0c845a9d1a34a422bc718f41539", size = 126045, upload-time = "2026-02-02T15:38:26.216Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] [[package]] @@ -293,38 +338,34 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, - { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, - { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, - { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, - { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, - { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, - { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, - { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, - { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, - { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, - { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, - { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, - { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, - { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, - { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, - { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, - { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, - { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, ] [[package]] name = "pydantic-settings" -version = "2.12.0" +version = "2.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" } +sdist = { url = "https://files.pythonhosted.org/packages/96/a1/ae859ffac5a3338a66b74c5e29e244fd3a3cc483c89feaf9f56c39898d75/pydantic_settings-2.13.0.tar.gz", hash = "sha256:95d875514610e8595672800a5c40b073e99e4aae467fa7c8f9c263061ea2e1fe", size = 222450, upload-time = "2026-02-15T12:11:23.476Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1a/dd1b9d7e627486cf8e7523d09b70010e05a4bc41414f4ae6ce184cf0afb6/pydantic_settings-2.13.0-py3-none-any.whl", hash = "sha256:d67b576fff39cd086b595441bf9c75d4193ca9c0ed643b90360694d0f1240246", size = 58429, upload-time = "2026-02-15T12:11:22.133Z" }, ] [package.optional-dependencies] @@ -332,6 +373,31 @@ yaml = [ { name = "pyyaml" }, ] +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + [[package]] name = "python-dotenv" version = "1.2.1" @@ -347,16 +413,16 @@ version = "6.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, - { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, - { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, - { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, - { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, - { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, - { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, - { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, - { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, ] [[package]] @@ -390,7 +456,6 @@ version = "0.52.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, - { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" } wheels = [ @@ -420,15 +485,15 @@ wheels = [ [[package]] name = "uvicorn" -version = "0.40.0" +version = "0.41.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } +sdist = { url = "https://files.pythonhosted.org/packages/32/ce/eeb58ae4ac36fe09e3842eb02e0eb676bf2c53ae062b98f1b2531673efdd/uvicorn-0.41.0.tar.gz", hash = "sha256:09d11cf7008da33113824ee5a1c6422d89fbc2ff476540d69a34c87fab8b571a", size = 82633, upload-time = "2026-02-16T23:07:24.1Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, + { url = "https://files.pythonhosted.org/packages/83/e4/d04a086285c20886c0daad0e026f250869201013d18f81d9ff5eada73a88/uvicorn-0.41.0-py3-none-any.whl", hash = "sha256:29e35b1d2c36a04b9e180d4007ede3bcb32a85fbdfd6c6aeb3f26839de088187", size = 68783, upload-time = "2026-02-16T23:07:22.357Z" }, ] [[package]] @@ -437,12 +502,12 @@ version = "0.22.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, - { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, - { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, - { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, - { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, - { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, ] [[package]]