From 222dc8edad7170f993b0bbd7f9bd161c26fb519a Mon Sep 17 00:00:00 2001 From: Elvis Pranskevichus Date: Sat, 16 May 2026 16:41:33 -0700 Subject: [PATCH] Replace pyright with ty, add more Ruff rules, fix issues --- .github/workflows/ci.yml | 4 +- CLAUDE.md | 5 +- examples/fastapi-vite/backend/agent.py | 8 +- examples/fastapi-vite/backend/main.py | 9 +- examples/fastapi-vite/backend/storage.py | 6 +- examples/multiagent-textual/client.py | 27 +- examples/multiagent-textual/server.py | 16 +- examples/multiagent-textual/test-e2e.py | 8 +- examples/run-examples.py | 37 +- examples/run-with-patched-model.py | 88 ++-- examples/samples/agent_custom_loop.py | 25 +- examples/samples/agent_hooks_inline.py | 7 +- examples/samples/agent_hooks_serverless.py | 24 +- examples/samples/agent_nested.py | 7 +- examples/samples/builtin_web_search.py | 4 +- examples/samples/check_connection.py | 2 +- examples/samples/coding_agent_minimal.py | 4 +- examples/samples/explicit_client.py | 8 +- examples/samples/image_edit.py | 15 +- examples/samples/image_generation.py | 6 +- examples/samples/inline_image.py | 11 +- examples/samples/mcp_tools.py | 3 +- examples/samples/prompt_caching.py | 3 +- examples/samples/streaming_tool.py | 13 +- examples/samples/structured_output.py | 2 +- examples/samples/video_generation.py | 6 +- .../temporal-direct/_durability_worker.py | 10 +- examples/temporal-direct/main.py | 12 +- examples/temporal-direct/test_durability.py | 14 +- pyproject.toml | 92 +++- src/ai/__init__.py | 82 +-- src/ai/_modelsdev.py | 16 +- src/ai/agents/__init__.py | 8 +- src/ai/agents/_middleware.py | 11 +- src/ai/agents/agent.py | 110 ++-- src/ai/agents/hooks.py | 21 +- src/ai/agents/mcp/__init__.py | 4 +- src/ai/agents/mcp/client.py | 61 ++- src/ai/agents/runtime.py | 8 +- src/ai/agents/ui/ai_sdk/__init__.py | 4 +- src/ai/agents/ui/ai_sdk/_approvals.py | 2 +- src/ai/agents/ui/ai_sdk/_parts.py | 11 +- src/ai/agents/ui/ai_sdk/inbound.py | 45 +- src/ai/agents/ui/ai_sdk/outbound/__init__.py | 2 +- src/ai/agents/ui/ai_sdk/outbound/_state.py | 16 +- src/ai/agents/ui/ai_sdk/outbound/history.py | 6 +- src/ai/agents/ui/ai_sdk/outbound/sse.py | 13 +- src/ai/agents/ui/ai_sdk/outbound/stream.py | 10 +- src/ai/agents/ui/ai_sdk/protocol.py | 30 +- src/ai/agents/ui/ai_sdk/ui_message.py | 12 +- src/ai/errors.py | 13 +- src/ai/models/core/__init__.py | 2 +- src/ai/models/core/api.py | 67 ++- src/ai/models/core/helpers/files.py | 5 +- src/ai/models/core/model.py | 10 +- src/ai/providers/_optional.py | 9 +- src/ai/providers/ai_gateway/client/_client.py | 3 +- src/ai/providers/ai_gateway/client/errors.py | 2 +- src/ai/providers/ai_gateway/errors.py | 8 +- src/ai/providers/ai_gateway/protocol.py | 71 ++- src/ai/providers/ai_gateway/provider.py | 17 +- src/ai/providers/ai_gateway/tools.py | 3 +- src/ai/providers/anthropic/_sdk.py | 2 +- src/ai/providers/anthropic/errors.py | 23 +- src/ai/providers/anthropic/protocol.py | 54 +- src/ai/providers/anthropic/provider.py | 17 +- src/ai/providers/base.py | 32 +- src/ai/providers/openai/_sdk.py | 11 +- src/ai/providers/openai/errors.py | 12 +- src/ai/providers/openai/protocol.py | 166 ++++-- src/ai/providers/openai/provider.py | 24 +- src/ai/providers/openai/tools.py | 4 +- src/ai/types/builders.py | 8 +- src/ai/types/events.py | 8 +- src/ai/types/integrity.py | 4 +- src/ai/types/media.py | 68 ++- src/ai/types/messages.py | 21 +- src/ai/types/tools.py | 4 +- src/ai/util.py | 21 +- tests/agents/mcp/test_client.py | 32 +- tests/agents/test_aggregate_marker.py | 35 +- tests/agents/test_generator_tools.py | 18 +- tests/agents/test_hooks.py | 24 +- tests/agents/test_runtime.py | 24 +- tests/agents/test_tools.py | 15 +- .../agents/test_validation_error_approval.py | 31 +- .../agents/ui/ai_sdk/outbound/test_history.py | 8 +- .../agents/ui/ai_sdk/outbound/test_stream.py | 12 +- tests/agents/ui/ai_sdk/test_inbound.py | 22 +- tests/conftest.py | 21 +- tests/models/core/test_api.py | 42 +- tests/models/test_resolution.py | 8 +- tests/providers/ai_gateway/test_errors.py | 43 +- .../ai_gateway/test_generate_image.py | 20 +- .../ai_gateway/test_generate_video.py | 42 +- tests/providers/ai_gateway/test_probe.py | 8 +- tests/providers/ai_gateway/test_protocol.py | 28 +- tests/providers/ai_gateway/test_provider.py | 12 +- tests/providers/ai_gateway/test_stream.py | 75 ++- tests/providers/anthropic/conftest.py | 8 +- tests/providers/anthropic/test_adapter.py | 12 +- tests/providers/anthropic/test_provider.py | 10 +- tests/providers/anthropic/test_stream.py | 14 +- tests/providers/anthropic/test_tools.py | 6 +- tests/providers/openai/test_adapter.py | 37 +- tests/providers/openai/test_provider.py | 9 +- tests/test_middleware.py | 35 +- tests/test_util.py | 33 +- tests/types/test_builders.py | 17 +- tests/types/test_integrity.py | 54 +- tests/types/test_media.py | 4 +- tests/types/test_messages.py | 4 +- uv.lock | 498 +++++++++++++----- 113 files changed, 2069 insertions(+), 829 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 91e14913..0a0491c8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,8 +20,8 @@ jobs: - run: uv run ruff format --check src tests examples - run: uv run ruff check src tests examples - - run: uv run mypy src tests - - run: uv run pyright src tests + - run: uv run mypy + - run: uv run ty check - run: uv run pytest diff --git a/CLAUDE.md b/CLAUDE.md index 11077764..4d6bc74d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,8 +6,8 @@ 2. after making changes run format, lint, and typecheck like ci: - uv run ruff format --check src tests examples - uv run ruff check src tests examples - - uv run mypy src tests - - uv run pyright src tests + - uv run mypy + - uv run ty check 3. imports: - import by module, using the shortest unambiguous relative path. `from ..core import helpers`, `from . import streaming` - UNLESS it's `typing` — then `from typing import Foo` (there are too many of them). @@ -37,4 +37,3 @@ ensure state is easy to serialize and deserialize, modify, and compose at any le move normalization and translation complexity inside the framework and keep the public data model minimal. - *example*: public data model consists of a single unified `Message` type. the framework does not expose events and other intermediate steps unless the user is writing a custom adapter. - diff --git a/examples/fastapi-vite/backend/agent.py b/examples/fastapi-vite/backend/agent.py index fa256e70..c5b5c19b 100644 --- a/examples/fastapi-vite/backend/agent.py +++ b/examples/fastapi-vite/backend/agent.py @@ -32,14 +32,18 @@ async def get_weather(city: str) -> str: """Get current weather for a city.""" await asyncio.sleep(2) - return f"Sunny, 72F in {city}" if city == "Tokyo" else f"Cloudy, 55F in {city}" + return ( + f"Sunny, 72F in {city}" if city == "Tokyo" else f"Cloudy, 55F in {city}" + ) @ai.tool async def get_population(city: str) -> int: """Get population of a city.""" await asyncio.sleep(1) - return {"new york": 8_336_817, "tokyo": 13_960_000}.get(city.lower(), 1_000_000) + return {"new york": 8_336_817, "tokyo": 13_960_000}.get( + city.lower(), 1_000_000 + ) @ai.tool(require_approval=True) diff --git a/examples/fastapi-vite/backend/main.py b/examples/fastapi-vite/backend/main.py index 65ac9667..b0c5bb65 100644 --- a/examples/fastapi-vite/backend/main.py +++ b/examples/fastapi-vite/backend/main.py @@ -3,7 +3,7 @@ from __future__ import annotations import sys -from collections.abc import AsyncGenerator +from typing import TYPE_CHECKING import agent as agent_ import fastapi @@ -14,6 +14,9 @@ import ai +if TYPE_CHECKING: + from collections.abc import AsyncGenerator + app = fastapi.FastAPI( title="py-ai-fastapi-chat", description="Chat demo using Python Vercel AI SDK", @@ -38,7 +41,9 @@ async def log_validation_errors( file=sys.stderr, flush=True, ) - return fastapi.responses.JSONResponse({"detail": exc.errors()}, status_code=422) + return fastapi.responses.JSONResponse( + {"detail": exc.errors()}, status_code=422 + ) @app.get("/health") diff --git a/examples/fastapi-vite/backend/storage.py b/examples/fastapi-vite/backend/storage.py index 5bad3d8a..2c2618e5 100644 --- a/examples/fastapi-vite/backend/storage.py +++ b/examples/fastapi-vite/backend/storage.py @@ -1,5 +1,4 @@ -""" -Pluggable storage for checkpoints and session data. +"""Pluggable storage for checkpoints and session data. Provides a minimal Storage protocol and a FileStorage implementation that persists data as JSON files on disk. Swap in any backend that @@ -25,8 +24,7 @@ async def delete(self, key: str) -> None: ... class FileStorage: - """ - JSON-file-per-key storage backend. + """JSON-file-per-key storage backend. Each key is stored as ``{directory}/{key}.json``. Good enough for local development; replace with a real database for production. diff --git a/examples/multiagent-textual/client.py b/examples/multiagent-textual/client.py index 629ffd1c..fa5391ec 100644 --- a/examples/multiagent-textual/client.py +++ b/examples/multiagent-textual/client.py @@ -12,12 +12,13 @@ import asyncio import json import os -from typing import Any +from typing import Any, ClassVar import pydantic import rich.text import textual import textual.app +import textual.binding import textual.containers import textual.widgets import textual.worker @@ -107,11 +108,15 @@ class MultiAgentApp(textual.app.App[None]): } """ - BINDINGS = [("q", "quit", "quit")] + BINDINGS: ClassVar[ + list[textual.binding.Binding | tuple[str, str] | tuple[str, str, str]] + ] = [("q", "quit", "quit")] def __init__(self) -> None: super().__init__() - self._hook_queue: asyncio.Queue[ai.messages.HookPart[Any]] = asyncio.Queue() + self._hook_queue: asyncio.Queue[ai.messages.HookPart[Any]] = ( + asyncio.Queue() + ) self._current_hook: ai.messages.HookPart[Any] | None = None self._ws: websockets.ClientConnection | None = None self._event_adapter: pydantic.TypeAdapter[ai.events.AgentEvent] = ( @@ -186,9 +191,13 @@ def _render(self, label: str, event: ai.events.AgentEvent) -> None: if panel is not None: for part in event.message.parts: match part: - case ai.messages.ToolCallPart(tool_name=name, tool_args=args): + case ai.messages.ToolCallPart( + tool_name=name, tool_args=args + ): panel.append_line(f"> {name}({args})") - case ai.messages.ToolResultPart(tool_name=name, result=result): + case ai.messages.ToolResultPart( + tool_name=name, result=result + ): panel.append_line(f"< {name} = {result}") return @@ -221,7 +230,9 @@ def _on_hook_pending(self, hook_part: ai.messages.HookPart[Any]) -> None: panel = self._get_panel(branch) if panel: - panel.append_line(f"!! approval required: {tool}", style="dim yellow") + panel.append_line( + f"!! approval required: {tool}", style="dim yellow" + ) panel.status = "awaiting approval" self._hook_queue.put_nowait(hook_part) @@ -270,7 +281,9 @@ def _maybe_activate_next_hook(self) -> None: inp.placeholder = f"approve {branch}/{tool}? [y/n]" inp.focus() - async def on_input_submitted(self, event: textual.widgets.Input.Submitted) -> None: + async def on_input_submitted( + self, event: textual.widgets.Input.Submitted + ) -> None: if self._current_hook is None: event.input.clear() return diff --git a/examples/multiagent-textual/server.py b/examples/multiagent-textual/server.py index a1473578..4265b845 100644 --- a/examples/multiagent-textual/server.py +++ b/examples/multiagent-textual/server.py @@ -14,14 +14,16 @@ import contextlib import json import warnings -from collections.abc import AsyncGenerator -from typing import Any +from typing import TYPE_CHECKING, Any import fastapi import pydantic import ai +if TYPE_CHECKING: + from collections.abc import AsyncGenerator + # ToolResultPart.result is typed as dict but tools can return plain strings. warnings.filterwarnings("ignore", category=UserWarning, module="pydantic") @@ -149,7 +151,9 @@ async def loop( class Orchestrator(ai.Agent): """Run two gated agents in parallel, then summarise their results.""" - async def loop(self, context: ai.Context) -> AsyncGenerator[ai.events.AgentEvent]: + async def loop( + self, context: ai.Context + ) -> AsyncGenerator[ai.events.AgentEvent]: query = context.messages[-1].text # Fan out: both branches stream concurrently via yield_from. @@ -219,9 +223,11 @@ async def loop(self, context: ai.Context) -> AsyncGenerator[ai.events.AgentEvent def _normalise_message(data: dict[str, Any]) -> dict[str, Any]: - """Ensure ToolResultPart.result is always a dict for safe deserialisation.""" + """Ensure ToolResultPart.result is always safe to deserialize.""" for part in data.get("parts", []): - if part.get("kind") == "tool_result" and isinstance(part.get("result"), str): + if part.get("kind") == "tool_result" and isinstance( + part.get("result"), str + ): part["result"] = {"value": part["result"]} return data diff --git a/examples/multiagent-textual/test-e2e.py b/examples/multiagent-textual/test-e2e.py index 0ce31730..4612d9a4 100755 --- a/examples/multiagent-textual/test-e2e.py +++ b/examples/multiagent-textual/test-e2e.py @@ -19,12 +19,14 @@ import tempfile import time import urllib.request -from http.client import HTTPResponse from pathlib import Path -from typing import cast +from typing import TYPE_CHECKING, cast import ai +if TYPE_CHECKING: + from http.client import HTTPResponse + HERE = Path(__file__).resolve().parent SESSION = f"multiagent-e2e-{os.getpid()}" SERVER_PORT = os.environ.get("SERVER_PORT", "8000") @@ -34,7 +36,7 @@ def _check_health() -> bool: try: with urllib.request.urlopen(f"{SERVER_URL}/api/health", timeout=1) as r: - return cast(HTTPResponse, r).status == 200 + return cast("HTTPResponse", r).status == 200 except Exception: return False diff --git a/examples/run-examples.py b/examples/run-examples.py index 2b2a79e9..05328a24 100755 --- a/examples/run-examples.py +++ b/examples/run-examples.py @@ -161,7 +161,9 @@ def _sample_path(name: str) -> Path: return SAMPLES / path -def _select_sample(name: str, known_samples: dict[str, Sample]) -> Sample | None: +def _select_sample( + name: str, known_samples: dict[str, Sample] +) -> Sample | None: sample = known_samples.get(name) if sample is not None: return sample @@ -178,7 +180,9 @@ def _select_sample(name: str, known_samples: dict[str, Sample]) -> Sample | None return None -def _sample_cmd(sample: Sample, model: str | None, protocol: str | None) -> list[str]: +def _sample_cmd( + sample: Sample, model: str | None, protocol: str | None +) -> list[str]: if sample.cmd is not None: return sample.cmd base = [ @@ -258,11 +262,21 @@ def run_sample_quiet( def main() -> None: parser = argparse.ArgumentParser(description="Run example samples.") - parser.add_argument("--text", action="store_true", help="include text samples") - parser.add_argument("--image", action="store_true", help="include image samples") - parser.add_argument("--video", action="store_true", help="include video samples") - parser.add_argument("--broken", action="store_true", help="include broken samples") - parser.add_argument("--e2e", action="store_true", help="include e2e test scripts") + parser.add_argument( + "--text", action="store_true", help="include text samples" + ) + parser.add_argument( + "--image", action="store_true", help="include image samples" + ) + parser.add_argument( + "--video", action="store_true", help="include video samples" + ) + parser.add_argument( + "--broken", action="store_true", help="include broken samples" + ) + parser.add_argument( + "--e2e", action="store_true", help="include e2e test scripts" + ) parser.add_argument("--all", action="store_true", help="run all samples") parser.add_argument( "--parallel", action="store_true", help="run samples in parallel" @@ -287,11 +301,16 @@ def main() -> None: "examples", nargs="*", metavar="example", - help="example file(s) to run, e.g. stream.py or examples/samples/stream.py", + help=( + "example file(s) to run, e.g. stream.py or " + "examples/samples/stream.py" + ), ) args = parser.parse_args() - has_category = args.text or args.image or args.video or args.broken or args.e2e + has_category = ( + args.text or args.image or args.video or args.broken or args.e2e + ) samples: list[Sample] = [] if args.examples: diff --git a/examples/run-with-patched-model.py b/examples/run-with-patched-model.py index 82c24a25..7769e1e9 100644 --- a/examples/run-with-patched-model.py +++ b/examples/run-with-patched-model.py @@ -10,23 +10,32 @@ uv run examples/run-with-patched-model.py --protocol=responses Example: - uv run examples/run-with-patched-model.py \\ gateway:openai/gpt-5.4-mini \\ examples/samples/stream.py + """ import argparse import runpy import sys from collections.abc import Callable -from typing import Any +from typing import Any, cast import ai from ai import models from ai.models import core from ai.models.core import api as _api from ai.models.core import model as _model +from ai.providers.anthropic import ( + AnthropicCompatibleProvider, + AnthropicMessagesProtocol, +) +from ai.providers.openai import ( + OpenAIChatCompletionsProtocol, + OpenAICompatibleProvider, + OpenAIResponsesProtocol, +) PROTOCOLS = ("chat", "messages", "responses") @@ -38,16 +47,10 @@ def _protocol_factory( return None if name == "chat": - from ai.providers.openai import OpenAIChatCompletionsProtocol - return OpenAIChatCompletionsProtocol if name == "messages": - from ai.providers.anthropic import AnthropicMessagesProtocol - return AnthropicMessagesProtocol if name == "responses": - from ai.providers.openai import OpenAIResponsesProtocol - return OpenAIResponsesProtocol raise ValueError(f"unsupported protocol: {name}") @@ -81,7 +84,6 @@ def main() -> None: original_get_model = _model.get_model original_stream = _api.stream original_generate = _api.generate - original_model = _model.Model def selected_protocol() -> ai.ProviderProtocol[Any] | None: if protocol_factory is None: @@ -93,32 +95,36 @@ def selected_protocol_for_provider( ) -> ai.ProviderProtocol[Any] | None: if args.protocol is None: return None - if args.protocol in ("chat", "responses"): - from ai.providers.openai import OpenAICompatibleProvider - - if isinstance(provider, OpenAICompatibleProvider): - return selected_protocol() - if args.protocol == "messages": - from ai.providers.anthropic import AnthropicCompatibleProvider - - if isinstance(provider, AnthropicCompatibleProvider): - return selected_protocol() + if args.protocol in ("chat", "responses") and isinstance( + provider, OpenAICompatibleProvider + ): + return selected_protocol() + if args.protocol == "messages" and isinstance( + provider, AnthropicCompatibleProvider + ): + return selected_protocol() return None - def selected_protocol_for_model(model: Any) -> ai.ProviderProtocol[Any] | None: + def selected_protocol_for_model( + model: Any, + ) -> ai.ProviderProtocol[Any] | None: provider = getattr(model, "provider", None) if provider is None: return None return selected_protocol_for_provider(provider) def patched_get_model(*_args: Any, **_kwargs: Any) -> ai.Model: - model_id = args.model or (_args[0] if _args else _kwargs.get("model_id")) + model_id = args.model or ( + _args[0] if _args else _kwargs.get("model_id") + ) model = original_get_model(model_id) model.protocol = selected_protocol_for_model(model) return model def patched_stream(*args: Any, **kwargs: Any) -> Any: - model = args[0] if args else getattr(kwargs.get("context"), "model", None) + model = ( + args[0] if args else getattr(kwargs.get("context"), "model", None) + ) protocol = selected_protocol_for_model(model) if protocol is not None: kwargs["protocol"] = protocol @@ -131,7 +137,7 @@ async def patched_generate(*args: Any, **kwargs: Any) -> Any: kwargs["protocol"] = protocol return await original_generate(*args, **kwargs) - class PatchedModel(original_model): + class PatchedModel(_model.Model): def __init__( self, id: str, @@ -145,26 +151,26 @@ def __init__( protocol=selected_protocol_for_provider(provider) or protocol, ) - ai.get_model = patched_get_model - models.get_model = patched_get_model - core.get_model = patched_get_model - _model.get_model = patched_get_model + cast("Any", ai).get_model = patched_get_model + cast("Any", models).get_model = patched_get_model + cast("Any", core).get_model = patched_get_model + cast("Any", _model).get_model = patched_get_model if args.protocol is not None: - ai.Model = PatchedModel - models.Model = PatchedModel - core.Model = PatchedModel - _model.Model = PatchedModel - - ai.stream = patched_stream - models.stream = patched_stream - core.stream = patched_stream - _api.stream = patched_stream - - ai.generate = patched_generate - models.generate = patched_generate - core.generate = patched_generate - _api.generate = patched_generate + cast("Any", ai).Model = PatchedModel + cast("Any", models).Model = PatchedModel + cast("Any", core).Model = PatchedModel + cast("Any", _model).Model = PatchedModel + + cast("Any", ai).stream = patched_stream + cast("Any", models).stream = patched_stream + cast("Any", core).stream = patched_stream + cast("Any", _api).stream = patched_stream + + cast("Any", ai).generate = patched_generate + cast("Any", models).generate = patched_generate + cast("Any", core).generate = patched_generate + cast("Any", _api).generate = patched_generate sys.argv = [args.file] runpy.run_path(args.file, run_name="__main__") diff --git a/examples/samples/agent_custom_loop.py b/examples/samples/agent_custom_loop.py index 82e57e68..ebb2b10f 100644 --- a/examples/samples/agent_custom_loop.py +++ b/examples/samples/agent_custom_loop.py @@ -2,6 +2,7 @@ import asyncio from collections.abc import AsyncGenerator +from typing import ClassVar import ai @@ -10,20 +11,26 @@ async def get_weather(city: str) -> str: """Get current weather for a city.""" await asyncio.sleep(2) - return f"Sunny, 72F in {city}" if city == "Tokyo" else f"Cloudy, 55F in {city}" + return ( + f"Sunny, 72F in {city}" if city == "Tokyo" else f"Cloudy, 55F in {city}" + ) @ai.tool async def get_population(city: str) -> int: """Get population of a city.""" await asyncio.sleep(1) - return {"new york": 8_336_817, "tokyo": 13_960_000}.get(city.lower(), 1_000_000) + return {"new york": 8_336_817, "tokyo": 13_960_000}.get( + city.lower(), 1_000_000 + ) class CustomAgent(ai.Agent): - TOOLS = [get_weather, get_population] + TOOLS: ClassVar[list[ai.AgentTool]] = [get_weather, get_population] - async def loop(self, context: ai.Context) -> AsyncGenerator[ai.events.AgentEvent]: + async def loop( + self, context: ai.Context + ) -> AsyncGenerator[ai.events.AgentEvent]: """Stream, execute tools with logging, repeat.""" while context.keep_running(): async with ( @@ -35,7 +42,9 @@ async def loop(self, context: ai.Context) -> AsyncGenerator[ai.events.AgentEvent if isinstance(event, ai.events.ToolEnd): call = event.tool_call - print(f"Launching tool {call.tool_name}({call.tool_args})") + print( + f"Launching tool {call.tool_name}({call.tool_args})" + ) tool = context.resolve(call) tr.schedule(tool) @@ -53,7 +62,11 @@ async def main() -> None: async with my_agent.run( model, - [ai.user_message("Compare the weather and population of New York and Tokyo.")], + [ + ai.user_message( + "Compare the weather and population of New York and Tokyo." + ) + ], ) as stream: async for event in stream: if ( diff --git a/examples/samples/agent_hooks_inline.py b/examples/samples/agent_hooks_inline.py index 5c1d3ca7..d3f7bf0f 100644 --- a/examples/samples/agent_hooks_inline.py +++ b/examples/samples/agent_hooks_inline.py @@ -18,6 +18,7 @@ import asyncio from collections.abc import AsyncGenerator +from typing import ClassVar import pydantic @@ -36,9 +37,11 @@ async def contact_mothership(query: str) -> str: class ApprovalAgent(ai.Agent): - TOOLS = [contact_mothership] + TOOLS: ClassVar[list[ai.AgentTool]] = [contact_mothership] - async def loop(self, context: ai.Context) -> AsyncGenerator[ai.events.AgentEvent]: + async def loop( + self, context: ai.Context + ) -> AsyncGenerator[ai.events.AgentEvent]: while context.keep_running(): async with ( ai.stream(context=context) as s, diff --git a/examples/samples/agent_hooks_serverless.py b/examples/samples/agent_hooks_serverless.py index bea92b9d..03bd3de9 100644 --- a/examples/samples/agent_hooks_serverless.py +++ b/examples/samples/agent_hooks_serverless.py @@ -16,7 +16,7 @@ import ai -FILES_DELETED = set() +FILES_DELETED: set[str] = set() @ai.tool(require_approval=True) @@ -27,7 +27,7 @@ async def delete_file(path: str) -> str: return f"Deleted {path}" -AUDIT_LOG = [] +AUDIT_LOG: list[str] = [] @ai.tool @@ -75,10 +75,12 @@ async def main() -> None: # next run replays from the same point. messages = stream.messages - print("\n Run interrupted; approval will be pre-registered for re-entry.\n") - assert len(AUDIT_LOG) == 1 and "/tmp/old_logs.txt" in AUDIT_LOG[0], ( - f"Bad audit log: {AUDIT_LOG}" + print( + "\n Run interrupted; approval will be pre-registered for re-entry.\n" ) + assert ( + len(AUDIT_LOG) == 1 and "/tmp/old_logs.txt" in AUDIT_LOG[0] + ), f"Bad audit log: {AUDIT_LOG}" # -- Second run: pre-register resolution, replay from checkpoint -- print("--- Run 2: pre-register approval, resume from checkpoint ---") @@ -95,12 +97,12 @@ async def main() -> None: print(f" Hook {event.hook.status}: {event.hook.hook_id}") print() - assert {"/tmp/old_logs.txt"} == FILES_DELETED, ( - f"Wrong files deleted: {FILES_DELETED}" - ) - assert len(AUDIT_LOG) == 1 and "/tmp/old_logs.txt" in AUDIT_LOG[0], ( - f"Bad audit log: {AUDIT_LOG}" - ) + assert { + "/tmp/old_logs.txt" + } == FILES_DELETED, f"Wrong files deleted: {FILES_DELETED}" + assert ( + len(AUDIT_LOG) == 1 and "/tmp/old_logs.txt" in AUDIT_LOG[0] + ), f"Bad audit log: {AUDIT_LOG}" if __name__ == "__main__": diff --git a/examples/samples/agent_nested.py b/examples/samples/agent_nested.py index b6e2155d..df3dead0 100644 --- a/examples/samples/agent_nested.py +++ b/examples/samples/agent_nested.py @@ -1,4 +1,4 @@ -"""Nested agents: a research tool that is itself an agent streaming through the sink.""" +"""Nested agents with a research tool that streams through the sink.""" import asyncio @@ -11,7 +11,10 @@ async def get_facts(topic: str) -> str: """Look up facts about a topic.""" facts = { - "mars": "Mars has two moons: Phobos and Deimos. A day on Mars is 24.6 hours.", + "mars": ( + "Mars has two moons: Phobos and Deimos. " + "A day on Mars is 24.6 hours." + ), "venus": "Venus rotates backwards. Its surface temperature is 450C.", } return facts.get(topic.lower(), f"No facts found for {topic}.") diff --git a/examples/samples/builtin_web_search.py b/examples/samples/builtin_web_search.py index 2fd24876..bba7ad86 100644 --- a/examples/samples/builtin_web_search.py +++ b/examples/samples/builtin_web_search.py @@ -26,7 +26,9 @@ def _strip_encrypted(value: object) -> object: """Some fields are encrypted, strip them for clarity""" if isinstance(value, dict): return { - k: _strip_encrypted(v) for k, v in value.items() if k not in _ENCRYPTED_KEYS + k: _strip_encrypted(v) + for k, v in value.items() + if k not in _ENCRYPTED_KEYS } if isinstance(value, list): return [_strip_encrypted(v) for v in value] diff --git a/examples/samples/check_connection.py b/examples/samples/check_connection.py index de6fb629..85b82fff 100644 --- a/examples/samples/check_connection.py +++ b/examples/samples/check_connection.py @@ -1,4 +1,4 @@ -"""Check connection and list models — verify credentials and model availability.""" +"""Check credentials and model availability.""" import asyncio import sys diff --git a/examples/samples/coding_agent_minimal.py b/examples/samples/coding_agent_minimal.py index 1d87df4f..e5cf41bc 100644 --- a/examples/samples/coding_agent_minimal.py +++ b/examples/samples/coding_agent_minimal.py @@ -43,7 +43,9 @@ async def shell(cmd: str) -> str: agent = ai.agent(tools=[shell]) -async def step(messages: list[ai.messages.Message]) -> list[ai.messages.Message]: +async def step( + messages: list[ai.messages.Message], +) -> list[ai.messages.Message]: async with agent.run(model, messages, params=STREAM_PARAMS) as stream: async for event in stream: if isinstance(event, ai.events.TextDelta): diff --git a/examples/samples/explicit_client.py b/examples/samples/explicit_client.py index 8c61464a..dbbadd18 100644 --- a/examples/samples/explicit_client.py +++ b/examples/samples/explicit_client.py @@ -12,7 +12,9 @@ async def main() -> None: # Example for local OpenAI-compatible servers like LM Studio. provider = ai.get_provider( "openai", - base_url=os.environ.get("LOCAL_OPENAI_BASE_URL", "http://localhost:1234/v1"), + base_url=os.environ.get( + "LOCAL_OPENAI_BASE_URL", "http://localhost:1234/v1" + ), api_key=os.environ.get("LOCAL_OPENAI_API_KEY", "some-key"), headers={"X-Custom-Header": "example"}, ) @@ -26,7 +28,9 @@ async def main() -> None: try: await ai.probe(model) except ai.ProviderError as exc: - print(f"[SKIP] local OpenAI-compatible server is unavailable: {exc}") + print( + f"[SKIP] local OpenAI-compatible server is unavailable: {exc}" + ) return async with ai.stream(model, messages) as s: diff --git a/examples/samples/image_edit.py b/examples/samples/image_edit.py index cd4bb6cd..bc99fbd7 100644 --- a/examples/samples/image_edit.py +++ b/examples/samples/image_edit.py @@ -16,9 +16,8 @@ async def main() -> None: # Load an existing image to use as input for editing. - # In practice you would load a real image file: - # image_data = pathlib.Path("my_photo.png").read_bytes() - # input_image = ai.file_part(image_data, media_type="image/png") + # In practice you would load a real image file and pass its bytes + # with media_type="image/png". input_image = ai.messages.FilePart( data="https://picsum.photos/id/237/400/300.jpg", media_type="image/jpeg", @@ -33,12 +32,18 @@ async def main() -> None: ), ] - result = await ai.generate(model, messages, ai.ImageParams(size="1024x1024")) + result = await ai.generate( + model, messages, ai.ImageParams(size="1024x1024") + ) print(f"Generated {len(result.images)} edited image(s)") for i, img in enumerate(result.images): filename = f"watercolor_edit_{i}.png" - data = img.data if isinstance(img.data, bytes) else base64.b64decode(img.data) + data = ( + img.data + if isinstance(img.data, bytes) + else base64.b64decode(img.data) + ) pathlib.Path(filename).write_bytes(data) print(f" {filename}: {img.media_type}, {len(data)} bytes") diff --git a/examples/samples/image_generation.py b/examples/samples/image_generation.py index 5cfcc974..94dd41b1 100644 --- a/examples/samples/image_generation.py +++ b/examples/samples/image_generation.py @@ -25,7 +25,11 @@ async def main() -> None: print(f"Generated {len(result.images)} image(s)") for i, img in enumerate(result.images): filename = f"generated_{i}.png" - data = img.data if isinstance(img.data, bytes) else base64.b64decode(img.data) + data = ( + img.data + if isinstance(img.data, bytes) + else base64.b64decode(img.data) + ) pathlib.Path(filename).write_bytes(data) print(f" {filename}: {img.media_type}, {len(data)} bytes") diff --git a/examples/samples/inline_image.py b/examples/samples/inline_image.py index 8c17af91..171e6433 100644 --- a/examples/samples/inline_image.py +++ b/examples/samples/inline_image.py @@ -16,9 +16,12 @@ messages = [ ai.system_message( - "You are an art assistant. When asked to draw or create an image, generate it." + "You are an art assistant. When asked to draw or create an image, " + "generate it." + ), + ai.user_message( + "Draw a cat sitting in a field of cherry blossoms at sunset." ), - ai.user_message("Draw a cat sitting in a field of cherry blossoms at sunset."), ] @@ -37,7 +40,9 @@ async def main() -> None: for i, img in enumerate(s.message.images): filename = f"inline_{i}.png" data = ( - img.data if isinstance(img.data, bytes) else base64.b64decode(img.data) + img.data + if isinstance(img.data, bytes) + else base64.b64decode(img.data) ) pathlib.Path(filename).write_bytes(data) print(f"Saved {filename} ({img.media_type}, {len(data)} bytes)") diff --git a/examples/samples/mcp_tools.py b/examples/samples/mcp_tools.py index 21948918..9706fb4d 100644 --- a/examples/samples/mcp_tools.py +++ b/examples/samples/mcp_tools.py @@ -19,7 +19,8 @@ async def main() -> None: messages = [ ai.system_message( - "You are a helpful assistant. Use context7 to look up documentation." + "You are a helpful assistant. " + "Use context7 to look up documentation." ), ai.user_message("How do I create middleware in Next.js?"), ] diff --git a/examples/samples/prompt_caching.py b/examples/samples/prompt_caching.py index a103f0c0..140e734d 100644 --- a/examples/samples/prompt_caching.py +++ b/examples/samples/prompt_caching.py @@ -160,7 +160,8 @@ def _show(label: str, usage: ai.types.usage.Usage | None) -> None: return print( f" {label}: input={usage.input_tokens} output={usage.output_tokens} " - f"cache_write={usage.cache_write_tokens} cache_read={usage.cache_read_tokens}" + f"cache_write={usage.cache_write_tokens} " + f"cache_read={usage.cache_read_tokens}" ) diff --git a/examples/samples/streaming_tool.py b/examples/samples/streaming_tool.py index dd3b8872..b827087a 100644 --- a/examples/samples/streaming_tool.py +++ b/examples/samples/streaming_tool.py @@ -6,7 +6,8 @@ the final tool result that goes back to the model. Here the aggregator is declared via the :data:`ai.StreamingStatusTool` -return-type alias — equivalent to ``@ai.tool(aggregator=ai.agents.LastAggregator)``. +return-type alias. This is equivalent to declaring the aggregator in +``@ai.tool(...)``. """ import asyncio @@ -17,7 +18,11 @@ @ai.tool async def talk_to_mothership(question: str) -> ai.StreamingStatusTool[str]: """Ask the mothership a question. Streams progress back to the caller.""" - for step in ["Connecting...", "Transmitting...", f"Asking: {question!r}..."]: + for step in [ + "Connecting...", + "Transmitting...", + f"Asking: {question!r}...", + ]: yield step await asyncio.sleep(0.3) @@ -31,7 +36,9 @@ async def main() -> None: my_agent = ai.agent(tools=[talk_to_mothership]) messages = [ - ai.system_message("Use the mothership tool when asked about the future."), + ai.system_message( + "Use the mothership tool when asked about the future." + ), ai.user_message("When will the robots take over?"), ] diff --git a/examples/samples/structured_output.py b/examples/samples/structured_output.py index 47ed5e6d..9d075b4d 100644 --- a/examples/samples/structured_output.py +++ b/examples/samples/structured_output.py @@ -20,7 +20,7 @@ class Recipe(pydantic.BaseModel): async def main() -> None: - # Stream with structured output — watch JSON arrive, get validated at the end. + # Stream structured output: watch JSON arrive, validate at the end. async with ai.stream(model, messages, output_type=Recipe) as s: async for event in s: if isinstance(event, ai.events.TextDelta): diff --git a/examples/samples/video_generation.py b/examples/samples/video_generation.py index 3715d3cb..8e46ffa6 100644 --- a/examples/samples/video_generation.py +++ b/examples/samples/video_generation.py @@ -29,7 +29,11 @@ async def main() -> None: for i, vid in enumerate(result.videos): ext = "mp4" if "mp4" in vid.media_type else "webm" filename = f"generated_{i}.{ext}" - data = vid.data if isinstance(vid.data, bytes) else base64.b64decode(vid.data) + data = ( + vid.data + if isinstance(vid.data, bytes) + else base64.b64decode(vid.data) + ) pathlib.Path(filename).write_bytes(data) print(f" {filename}: {vid.media_type}, {len(data)} bytes") diff --git a/examples/temporal-direct/_durability_worker.py b/examples/temporal-direct/_durability_worker.py index 7ce77fa0..57d16ebe 100644 --- a/examples/temporal-direct/_durability_worker.py +++ b/examples/temporal-direct/_durability_worker.py @@ -26,14 +26,16 @@ import contextlib import os import threading -from collections.abc import Callable -from typing import Any +from typing import TYPE_CHECKING, Any import main as ex import temporalio.activity import temporalio.client import temporalio.worker +if TYPE_CHECKING: + from collections.abc import Callable + _log_lock = threading.Lock() @@ -82,7 +84,9 @@ async def logged_llm_call(params: ex.LLMParams) -> ex.LLMResult: async def amain(server_addr: str, namespace: str) -> None: - client = await temporalio.client.Client.connect(server_addr, namespace=namespace) + client = await temporalio.client.Client.connect( + server_addr, namespace=namespace + ) async with temporalio.worker.Worker( client, task_queue=ex.TASK_QUEUE, diff --git a/examples/temporal-direct/main.py b/examples/temporal-direct/main.py index 706a1e9f..9285166e 100644 --- a/examples/temporal-direct/main.py +++ b/examples/temporal-direct/main.py @@ -31,8 +31,7 @@ import json import sys import uuid -from collections.abc import AsyncGenerator -from typing import Any +from typing import TYPE_CHECKING, Any, ClassVar import temporalio.activity import temporalio.client @@ -40,6 +39,9 @@ import temporalio.worker import temporalio.workflow +if TYPE_CHECKING: + from collections.abc import AsyncGenerator + with temporalio.workflow.unsafe.imports_passed_through(): import ai @@ -131,9 +133,11 @@ async def llm_call_activity(params: LLMParams) -> LLMResult: class WeatherAgent(ai.Agent): - TOOLS = [get_weather, get_population] + TOOLS: ClassVar[list[ai.AgentTool]] = [get_weather, get_population] - async def loop(self, context: ai.Context) -> AsyncGenerator[ai.events.AgentEvent]: + async def loop( + self, context: ai.Context + ) -> AsyncGenerator[ai.events.AgentEvent]: tool_schemas = [ {"name": t.name, "args": t.args.model_dump(mode="json")} for t in context.tools diff --git a/examples/temporal-direct/test_durability.py b/examples/temporal-direct/test_durability.py index 0e2c3ea5..2c23556d 100644 --- a/examples/temporal-direct/test_durability.py +++ b/examples/temporal-direct/test_durability.py @@ -51,7 +51,7 @@ import temporalio.client import temporalio.testing import temporalio.worker -from _durability_worker import LOGGED_ACTIVITIES +from _durability_worker import LOGGED_ACTIVITIES # noqa: PLC2701 # ── Helpers ────────────────────────────────────────────────────── @@ -93,9 +93,9 @@ async def test_happy_path( ) assert result, "expected non-empty text" - assert "8,336,817" in result or "8336817" in result, ( - f"expected NYC population in result, got: {result!r}" - ) + assert ( + "8,336,817" in result or "8336817" in result + ), f"expected NYC population in result, got: {result!r}" print(f" ✓ workflow {wid} produced {len(result)} chars") print(f" ✓ activity calls: {dict(read_activity_log(log_file))}") return wid @@ -200,7 +200,7 @@ async def test_activity_caching( ) # If worker2 ignored history and re-ran everything, total_post would - # be roughly 2× total_pre (worker1's executions + worker2 redoing + # be roughly 2x total_pre (worker1's executions + worker2 redoing # them all). Catch that case loudly. expected_double_run = total_pre * 2 assert total_post < expected_double_run, ( @@ -241,7 +241,9 @@ async def main() -> None: os.environ["DURABILITY_ACTIVITY_LOG"] = str(log_file) print("Starting embedded Temporal dev server...") - async with await temporalio.testing.WorkflowEnvironment.start_local() as env: + async with ( + await temporalio.testing.WorkflowEnvironment.start_local() as env + ): client = env.client wid = await test_happy_path(client, log_file) diff --git a/pyproject.toml b/pyproject.toml index 13edcceb..8ab8ce83 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,31 +58,105 @@ dev = [ "pytest>=8.0", "pytest-asyncio>=0.24", "rich>=14.2.0", - "mypy>=1.11", + "mypy~=2.1.0", "openai>=2.14.0", - "ruff>=0.8", - "pyright>=1.1.408", + "ruff~=0.8.0", "async-solipsism>=0.9", + "ty~=0.0.37", ] +examples = [ + "fastapi>=0.136.1", + "temporalio>=1.27.2", + "textual>=8.2.6", + "websockets>=16.0", +] + +[tool.uv] +default-groups = ["dev", "examples"] [tool.mypy] python_version = "3.12" strict = true plugins = ["pydantic.mypy"] +files = ["src", "tests", "examples"] +exclude = ["examples/fastapi-vite/backend/main.py"] -[tool.pyright] -pythonVersion = "3.12" -typeCheckingMode = "standard" -reportMissingTypeStubs = false -reportUnusedCallResult = false +[tool.ty.environment] +python-version = "3.12" +root = ["./src", "./"] [tool.pytest.ini_options] asyncio_mode = "auto" testpaths = ["tests"] [tool.ruff] +line-length = 80 +indent-width = 4 target-version = "py312" src = ["src"] +exclude = [ + ".github", + ".git", + "build", + "dist", + ".eggs", +] [tool.ruff.lint] -select = ["E", "F", "I", "UP", "B", "SIM"] +preview = true +extend-select = [ + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "D", # pydocstyle + "E", # error + "ERA", # flake8-eradicate + "F", # pyflakes + "FBT", # flake8-boolean-trap + "G", # flake8-logging-format + "I", # isort + "N", # pep8-naming + "PGH", # pygrep-hooks + "PIE", # flake8-pie + "PLE", # pylint-error + "PLW", # pylint-warning + "PLC", # pylint-convention + "RUF", # ruff specific + "SIM", # flake8-simplify + "T20", # flake8-print + "TC", # flake8-type-checking + "UP", # pyupgrade + "W", # warning +] + +extend-ignore = [ + "D100", # public API docs are not yet enforced everywhere + "D101", # public API docs are not yet enforced everywhere + "D102", # public API docs are not yet enforced everywhere + "D103", # public API docs are not yet enforced everywhere + "D104", # public API docs are not yet enforced everywhere + "D105", # public API docs are not yet enforced everywhere + "D107", # public API docs are not yet enforced everywhere + "D417", # public API docs are not yet enforced everywhere + "D203", # incorrect-blank-line-before-class + "D213", # multi-line-summary-second-line + "F541", # f-string-missing-placeholders + "PLW1514", # unspecified-encoding (UTF-8 is Python 3 default) + "PLW2901", # redefined-loop-name (intentional pattern for line processing) + "RUF029", # async generators/protocol hooks may not need an await +] + +[tool.ruff.lint.per-file-ignores] +"examples/**/*.py" = [ + "D", # examples are executable samples, not public API + "PLW1510", # example subprocess helpers intentionally handle failures manually + "RUF029", # example async tools often demonstrate async entry points + "T201", # examples are command-line scripts and demos +] +"tests/**/*.py" = [ + "D", # docstrings not required in tests + "FBT", # tests intentionally cover boolean-typed tool parameters + "PLC0415", # local imports keep test-only dependencies scoped + "PLC2701", # allow private imports in tests + "RUF029", # async stubs/generators exercise async code paths + "TC", # test annotations often double as runtime documentation +] diff --git a/src/ai/__init__.py b/src/ai/__init__.py index 6449ff83..066d9aaa 100644 --- a/src/ai/__init__.py +++ b/src/ai/__init__.py @@ -72,21 +72,19 @@ ) __all__ = [ - # Builders (from types/builders) - "user_message", - "assistant_message", - "system_message", - "tool_message", - "tool_result", - "tool_result_part", - "pending_tool_result", - "file_part", - "thinking", # Models (from models/) "AIError", + # Agents — primary API + "Agent", + # Agents — tools + "AgentTool", "ConfigurationError", + "Context", "HTTPErrorContext", + "ImageParams", "InstallationError", + "Model", + "Provider", "ProviderAPIError", "ProviderAuthenticationError", "ProviderBadRequestError", @@ -100,6 +98,7 @@ "ProviderNotFoundError", "ProviderOverloadedError", "ProviderPermissionDeniedError", + "ProviderProtocol", "ProviderRateLimitError", "ProviderRequestTooLargeError", "ProviderResponseError", @@ -107,45 +106,46 @@ "ProviderStatusError", "ProviderTimeoutError", "ProviderUnprocessableEntityError", - "UnsupportedProviderError", - "Model", - "Provider", - "ProviderProtocol", - "ImageParams", - "VideoParams", "Stream", - "stream", - "generate", - "get_model", - "probe", - "get_provider", - "models", - "providers", - # Agents — primary API - "Agent", - "agent", - "Context", - # Agents — tools - "AgentTool", + "StreamingStatusTool", + "StreamingTextTool", + "SubAgentTool", "Tool", "ToolCall", "ToolRunner", - "tool", - "StreamingTextTool", - "SubAgentTool", - "StreamingStatusTool", - # Agents — composition - "yield_from", - # Agents — hooks - "hook", - "resolve_hook", - "cancel_hook", + "UnsupportedProviderError", + "VideoParams", "abort_pending_hook", + "agent", + "assistant_message", + "cancel_hook", + "errors", # Submodules "events", - "errors", - "messages", + "file_part", + "generate", + "get_model", + "get_provider", + # Agents — hooks + "hook", "mcp", + "messages", + "models", + "pending_tool_result", + "probe", + "providers", + "resolve_hook", + "stream", + "system_message", + "thinking", + "tool", + "tool_message", + "tool_result", + "tool_result_part", "tools", + # Builders (from types/builders) + "user_message", "util", + # Agents — composition + "yield_from", ] diff --git a/src/ai/_modelsdev.py b/src/ai/_modelsdev.py index cd063702..8f1be079 100644 --- a/src/ai/_modelsdev.py +++ b/src/ai/_modelsdev.py @@ -39,7 +39,10 @@ def provider_base_url( provider: modelsdotdev.Provider, model_provider_config: modelsdotdev.ModelProviderConfig | None = None, ) -> str | None: - if model_provider_config is not None and model_provider_config.api is not None: + if ( + model_provider_config is not None + and model_provider_config.api is not None + ): return model_provider_config.api return provider.api @@ -48,7 +51,7 @@ def provider_config( provider: modelsdotdev.Provider, model_provider_config: modelsdotdev.ModelProviderConfig | None = None, ) -> tuple[str | None, tuple[str, ...]]: - """Return ``api_key_env`` and non-secret config envs from models.dev data.""" + """Return API key and config envs from models.dev data.""" api = provider_base_url(provider, model_provider_config) envs = _provider_envs(provider, api) api_key_env = _api_key_env(envs, api) @@ -60,12 +63,17 @@ def provider_npm( provider: modelsdotdev.Provider, model_provider_config: modelsdotdev.ModelProviderConfig | None = None, ) -> str: - if model_provider_config is not None and model_provider_config.npm is not None: + if ( + model_provider_config is not None + and model_provider_config.npm is not None + ): return model_provider_config.npm return provider.npm -def _provider_envs(provider: modelsdotdev.Provider, api: str | None) -> tuple[str, ...]: +def _provider_envs( + provider: modelsdotdev.Provider, api: str | None +) -> tuple[str, ...]: envs = list(provider.env) for env in _ENV_REFERENCE_RE.findall(api or ""): if env not in envs: diff --git a/src/ai/agents/__init__.py b/src/ai/agents/__init__.py index 88a42eab..3d785520 100644 --- a/src/ai/agents/__init__.py +++ b/src/ai/agents/__init__.py @@ -33,25 +33,25 @@ ) __all__ = [ + "TOOL_APPROVAL_HOOK_TYPE", "Agent", "AgentTool", "Aggregate", + "BoundToolCall", "ConcatAggregator", "Context", + "GatedToolCall", "LastAggregator", "MessageAggregator", "MessageBundle", "SimpleAggregator", + "StreamingStatusTool", "StreamingTextTool", "SubAgentTool", - "BoundToolCall", - "GatedToolCall", "Tool", "ToolCall", "ToolCallCallable", "ToolRunner", - "StreamingStatusTool", - "TOOL_APPROVAL_HOOK_TYPE", "abort_pending_hook", "agent", "cancel_hook", diff --git a/src/ai/agents/_middleware.py b/src/ai/agents/_middleware.py index 5ec0f2a5..4aa0245f 100644 --- a/src/ai/agents/_middleware.py +++ b/src/ai/agents/_middleware.py @@ -20,10 +20,7 @@ from collections.abc import AsyncGenerator, Awaitable, Callable, Sequence from typing import TYPE_CHECKING, Any -import pydantic - from ..types import messages as messages_ -from ..types.tools import Tool # Compat shim: ``StreamResultLike`` was removed from ``ai.types.proto`` when # the model layer was reworked. Middleware is dead code under the new @@ -43,8 +40,11 @@ # --------------------------------------------------------------------------- if TYPE_CHECKING: + import pydantic + from ..models.core.model import Model from ..types import events as events_ + from ..types.tools import Tool from .agent import Context @@ -218,7 +218,7 @@ def get() -> list[_Middleware]: def activate(mw: list[_Middleware]) -> Token: - """Set the middleware stack for the current run. Returns a token for reset.""" + """Set the middleware stack and return a reset token.""" return _active.set(mw) @@ -272,7 +272,8 @@ def _build_generate_chain( for m in reversed(mw): def _make( - m: _Middleware, nxt: Callable[[GenerateContext], Awaitable[_Message]] + m: _Middleware, + nxt: Callable[[GenerateContext], Awaitable[_Message]], ) -> Callable[[GenerateContext], Awaitable[_Message]]: async def _wrapped(call: GenerateContext) -> _Message: return await m.wrap_generate(call, nxt) diff --git a/src/ai/agents/agent.py b/src/ai/agents/agent.py index 899f5c7f..922e3dc9 100644 --- a/src/ai/agents/agent.py +++ b/src/ai/agents/agent.py @@ -33,7 +33,7 @@ # ``typing.TypeVar`` lacks the ``default=`` kwarg on Python <3.13. # Use the typing_extensions backport so this works on 3.12 too. -from typing_extensions import TypeVar # noqa: UP035 +from typing_extensions import TypeVar from .. import models, types, util from ..types import builders @@ -82,8 +82,9 @@ def _error_tool_result( def _process_interrupted_hooks(messages: list[types.messages.Message]) -> None: - """Detect a bailed-out-on-hook tail and mangle ``messages`` in place - so the next agent run resumes correctly. + """Detect an interrupted hook tail and mangle ``messages`` in place. + + This prepares the next agent run to resume correctly. Two shapes are recognised: @@ -124,7 +125,9 @@ def _process_interrupted_hooks(messages: list[types.messages.Message]) -> None: return completed_by_id = { - r.tool_call_id: r for r in last.tool_results if not r.is_hook_pending + r.tool_call_id: r + for r in last.tool_results + if not r.is_hook_pending } new_parts: list[types.messages.Part] = [] @@ -138,7 +141,9 @@ def _process_interrupted_hooks(messages: list[types.messages.Message]) -> None: ) new_parts.append(part) - messages[-2] = prev.model_copy(update={"parts": new_parts, "replay": True}) + messages[-2] = prev.model_copy( + update={"parts": new_parts, "replay": True} + ) messages.pop() @@ -218,7 +223,9 @@ class MessageBundle(pydantic.BaseModel): messages: tuple[types.messages.Message, ...] -class MessageAggregator(events_.Aggregator[events_.AgentEvent, MessageBundle, str]): +class MessageAggregator( + events_.Aggregator[events_.AgentEvent, MessageBundle, str] +): def __init__(self) -> None: self._messages: list[types.messages.Message] = [] @@ -247,7 +254,7 @@ def to_model_input(cls, snapshot: MessageBundle | Any) -> str: class Aggregate: - """Marker for declaring an aggregator on a tool's return type. + r"""Marker for declaring an aggregator on a tool's return type. Place inside ``Annotated`` metadata to attach an aggregator factory to an async-generator tool:: @@ -282,10 +289,15 @@ def __call__(self) -> events_.Aggregator[Any, Any, Any]: def __repr__(self) -> str: kw = ", ".join(f"{k}={v!r}" for k, v in self._kwargs.items()) sep = ", " if kw else "" - return f"Aggregate({self._factory.__name__}{sep}{kw})" + name = getattr( + self._factory, "__name__", self._factory.__class__.__name__ + ) + return f"Aggregate({name}{sep}{kw})" -type StreamingStatusTool[T] = Annotated[AsyncGenerator[T], Aggregate(LastAggregator)] +type StreamingStatusTool[T] = Annotated[ + AsyncGenerator[T], Aggregate(LastAggregator) +] """Async-generator tool whose final yielded value becomes the tool result. Intermediate yields stream to the consumer as ``PartialToolCallResult`` @@ -316,7 +328,9 @@ async def research(topic: str) -> SubAgentTool: """ -type StreamingTextTool = Annotated[AsyncGenerator[str], Aggregate(ConcatAggregator)] +type StreamingTextTool = Annotated[ + AsyncGenerator[str], Aggregate(ConcatAggregator) +] """Async-generator tool whose yielded chunks concatenate into the result. Each yield streams to the consumer as a ``PartialToolCallResult``; @@ -359,8 +373,9 @@ def _aggregate_from_return_type(fn: Callable[..., Any]) -> Aggregate | None: metadata = getattr(ret, "__metadata__", ()) matches = [m for m in metadata if isinstance(m, Aggregate)] if len(matches) > 1: + name = getattr(fn, "__name__", fn.__class__.__name__) raise TypeError( - f"Tool {fn.__name__!r} has multiple Aggregate markers in its " + f"Tool {name!r} has multiple Aggregate markers in its " "return-type annotation; expected at most one" ) return matches[0] if matches else None @@ -414,29 +429,33 @@ def tool[**P, T](fn: Callable[P, AsyncGenerator[T]], /) -> AgentTool: ... @overload -def tool[**P](*, require_approval: bool) -> Callable[[Callable[P, Any]], AgentTool]: ... +def tool[**P]( + *, require_approval: bool +) -> Callable[[Callable[P, Any]], AgentTool]: ... @overload -def tool[**P, T, R]( +def tool[**P]( *, - aggregator: Callable[[], events_.Aggregator[T, Any, R]], + aggregator: Callable[[], events_.Aggregator[Any, Any, Any]], require_approval: bool = False, -) -> Callable[[Callable[P, AsyncGenerator[T]]], AgentTool]: ... +) -> Callable[[Callable[P, AsyncGenerator[Any]]], AgentTool]: ... def tool[**P, T, R]( - fn: Callable[P, Awaitable[R]] | Callable[P, AsyncGenerator[T]] | None = None, + fn: Callable[P, Awaitable[R]] + | Callable[P, AsyncGenerator[T]] + | None = None, /, *, - aggregator: Callable[[], events_.Aggregator[T, Any, R]] | None = None, + aggregator: Callable[[], events_.Aggregator[Any, Any, Any]] | None = None, require_approval: bool = False, ) -> ( - Callable[[Callable[P, AsyncGenerator[T]]], AgentTool] + Callable[[Callable[P, AsyncGenerator[Any]]], AgentTool] | Callable[[Callable[P, Awaitable[R]]], AgentTool] | AgentTool ): - """Decorator: turn an async function into a :class:`Tool`. + """Turn an async function into a :class:`Tool`. For async-generator tools, declare the aggregator either via the ``aggregator=`` keyword argument or by annotating the return type @@ -522,7 +541,9 @@ def fn(self) -> Callable[..., Awaitable[Any]]: @property def kwargs(self) -> dict[str, Any]: if self._kwargs is None: - kwargs = json.loads(self._part.tool_args) if self._part.tool_args else {} + kwargs = ( + json.loads(self._part.tool_args) if self._part.tool_args else {} + ) self._kwargs = _validate_kwargs(self._tool, kwargs) return dict(self._kwargs) @@ -548,7 +569,9 @@ async def __call__(self, **overrides: Any) -> events_.ToolCallResult: if overrides: # Overrides come from user code, not the model — validate # eagerly so programming errors surface immediately. - base_kwargs = _validate_kwargs(self._tool, {**base_kwargs, **overrides}) + base_kwargs = _validate_kwargs( + self._tool, {**base_kwargs, **overrides} + ) call = middleware_.ToolContext( tool_call_id=self._part.tool_call_id, @@ -558,7 +581,9 @@ async def __call__(self, **overrides: Any) -> events_.ToolCallResult: tool = self._tool - async def _real(call: middleware_.ToolContext) -> events_.ToolCallResult: + async def _real( + call: middleware_.ToolContext, + ) -> events_.ToolCallResult: result: Any model_input: Any try: @@ -645,7 +670,9 @@ async def __call__(self) -> events_.ToolCallResult: metadata={"tool": tc.name, "kwargs": hook_kwargs}, ) except hooks_.HookPendingError as e: - return pending_tool_result(e.hook, tool_call_id=tc.id, tool_name=tc.name) + return pending_tool_result( + e.hook, tool_call_id=tc.id, tool_name=tc.name + ) if approval.granted: return await tc() return tool_result( @@ -741,7 +768,9 @@ def add_result(self, res: events_.ToolCallResult) -> None: def get_tool_message(self) -> types.messages.Message | None: if self._tool_results: - return builders.tool_message(*[t.message for t in self._tool_results]) + return builders.tool_message( + *[t.message for t in self._tool_results] + ) return None async def _iterate(self) -> AsyncGenerator[events_.ToolCallResult]: @@ -759,7 +788,9 @@ async def _iterate(self) -> AsyncGenerator[events_.ToolCallResult]: self._new_results = [] for n in new: yield n - self._sched_waiter = asyncio.get_running_loop().create_future() + self._sched_waiter = ( + asyncio.get_running_loop().create_future() + ) else: try: res = t.result() @@ -804,7 +835,10 @@ def keep_running(self) -> bool: # those are resolved and we get called again. if any(r.is_hook_pending for r in last_message.tool_results): return False - return last_message.replay or last_message.role not in ("assistant", "internal") + return last_message.replay or last_message.role not in ( + "assistant", + "internal", + ) @overload def resolve(self, tool_part: types.messages.ToolCallPart) -> ToolCall: ... @@ -815,14 +849,16 @@ def resolve( def resolve( self, - tool_part: types.messages.ToolCallPart | Sequence[types.messages.ToolCallPart], + tool_part: types.messages.ToolCallPart + | Sequence[types.messages.ToolCallPart], ) -> ToolCall | list[ToolCall]: """Resolve ToolCallPart(s) into callable ToolCall object(s).""" if isinstance(tool_part, types.messages.ToolCallPart): tool = self._agent_tools_by_name.get(tool_part.tool_name) if tool is None: raise KeyError( - f"No agent executor registered for tool {tool_part.tool_name!r}" + "No agent executor registered for tool " + f"{tool_part.tool_name!r}" ) tc = BoundToolCall(part=tool_part, tool=tool) if tool.require_approval: @@ -831,7 +867,10 @@ def resolve( return [self.resolve(tp) for tp in tool_part] def add( - self, message: types.messages.Message | Sequence[types.messages.Message] | None + self, + message: types.messages.Message + | Sequence[types.messages.Message] + | None, ) -> None: """Append message(s) to the context, skipping any flagged ``replay``. @@ -844,7 +883,9 @@ def add( if message is None: return msgs = ( - [message] if isinstance(message, types.messages.Message) else list(message) + [message] + if isinstance(message, types.messages.Message) + else list(message) ) for msg in msgs: if msg.replay: @@ -908,7 +949,7 @@ def messages(self) -> list[types.messages.Message]: @property def output(self) -> AgentOutputT: - """Return the run's output, parsed as the ``output_type`` given to ``run``. + """Return the run's output. Defaults to the final assistant message's concatenated text. When an ``output_type`` was passed, the assistant message's text @@ -916,7 +957,7 @@ def output(self) -> AgentOutputT: is returned. """ last = self._context.messages[-1] - return cast(AgentOutputT, last.get_output(self._context.output_type)) + return cast("AgentOutputT", last.get_output(self._context.output_type)) def tool_result( @@ -1106,7 +1147,9 @@ def tools(self) -> list[AgentTool]: """The agent's registered tools (read-only copy).""" return list(self._tools) - async def loop(self, context: Context) -> AsyncGenerator[events_.AgentEvent]: + async def loop( + self, context: Context + ) -> AsyncGenerator[events_.AgentEvent]: """Stream, execute tools, repeat. Override in a subclass to customise the agent's control flow. @@ -1177,6 +1220,7 @@ def run( To attribute a sub-agent's events to a branch, wrap the run in ``yield_from(..., label=...)`` — the label flows via ``PartialToolCallResult`` rather than on individual messages. + """ return self._run( model, diff --git a/src/ai/agents/hooks.py b/src/ai/agents/hooks.py index 1f53b9e4..8eca5244 100644 --- a/src/ai/agents/hooks.py +++ b/src/ai/agents/hooks.py @@ -2,7 +2,9 @@ Usage inside an agent loop:: - result = await hook("approve_delete", payload=ToolApproval, metadata={"tool": "rm"}) + result = await hook( + "approve_delete", payload=ToolApproval, metadata={"tool": "rm"} + ) if result.granted: ... @@ -19,7 +21,7 @@ from __future__ import annotations import asyncio -from typing import Any +from typing import Any, cast import pydantic @@ -55,7 +57,7 @@ class HookPendingError(Exception): - """Exception for aborting due to a hook""" + """Exception for aborting due to a hook.""" type: str = "gateway_error" @@ -89,6 +91,7 @@ async def hook[T: pydantic.BaseModel]( metadata: Arbitrary metadata surfaced in the pending signal message and checkpoint. Useful for UI rendering (e.g. which tool needs approval, what arguments it received). + """ call = middleware_.HookContext( label=label, @@ -98,7 +101,7 @@ async def hook[T: pydantic.BaseModel]( chain = middleware_._build_hook_chain(_hook_impl) result = await chain(call) - return result # type: ignore[return-value] + return cast("T", result) async def _hook_impl(call: middleware_.HookContext) -> pydantic.BaseModel: @@ -181,6 +184,7 @@ def resolve_hook( exception to raise in the awaiter. payload: Optional pydantic model class for validation. Ignored when *data* is an exception. + """ resolution: dict[str, Any] | BaseException if isinstance(data, BaseException): @@ -195,7 +199,9 @@ def resolve_hook( else: resolution = data else: - raise TypeError(f"Expected dict or pydantic model, got {type(data).__name__}") + raise TypeError( + f"Expected dict or pydantic model, got {type(data).__name__}" + ) # Path 1: live hook — resolve the future directly. if label in _live_hooks: @@ -211,8 +217,9 @@ def resolve_hook( def abort_pending_hook(hook_part: messages_.HookPart[Any]) -> None: - """Abort the hook identified by ``hook_part.hook_id`` with a - :class:`HookPendingError` carrying *hook_part*. + """Abort the hook identified by ``hook_part.hook_id``. + + The abort carries a :class:`HookPendingError` wrapping *hook_part*. Convenience wrapper around :func:`resolve_hook` for the serverless pattern where a caller has a :class:`~ai.messages.HookPart` (e.g. diff --git a/src/ai/agents/mcp/__init__.py b/src/ai/agents/mcp/__init__.py index 873e7008..1039d939 100644 --- a/src/ai/agents/mcp/__init__.py +++ b/src/ai/agents/mcp/__init__.py @@ -1,7 +1,7 @@ from .client import close_connections, get_http_tools, get_stdio_tools __all__ = [ - "get_stdio_tools", - "get_http_tools", "close_connections", + "get_http_tools", + "get_stdio_tools", ] diff --git a/src/ai/agents/mcp/client.py b/src/ai/agents/mcp/client.py index 3e503d43..cd2b2695 100644 --- a/src/ai/agents/mcp/client.py +++ b/src/ai/agents/mcp/client.py @@ -5,22 +5,21 @@ import contextvars import dataclasses import json -from collections.abc import AsyncIterator, Awaitable, Callable from typing import TYPE_CHECKING, Any if TYPE_CHECKING: + from collections.abc import AsyncIterator, Awaitable, Callable + import mcp.client.session - import mcp.client.stdio - import mcp.client.streamable_http import mcp.types from ... import types from ..agent import AgentTool, Tool __all__ = [ - "get_stdio_tools", - "get_http_tools", "close_connections", + "get_http_tools", + "get_stdio_tools", ] @@ -33,8 +32,8 @@ class _Connection: # Connection pool stored in contextvar, scoped to Agent.run() -_pool: contextvars.ContextVar[dict[str, _Connection] | None] = contextvars.ContextVar( - "mcp_connections", default=None +_pool: contextvars.ContextVar[dict[str, _Connection] | None] = ( + contextvars.ContextVar("mcp_connections", default=None) ) _pool_lock = asyncio.Lock() @@ -56,10 +55,12 @@ async def ensure_connection_pool() -> AsyncIterator[dict[str, _Connection]]: async def _get_or_create_connection( key: str, - transport_factory: Callable[[], contextlib.AbstractAsyncContextManager[Any]], + transport_factory: Callable[ + [], contextlib.AbstractAsyncContextManager[Any] + ], ) -> mcp.client.session.ClientSession: """Get an existing connection or create a new one.""" - import mcp.client.session as _mcp_session + import mcp.client.session as _mcp_session # noqa: PLC0415 pool = _pool.get() @@ -97,14 +98,18 @@ async def _get_or_create_connection( def _make_tool_fn( connection_key: str, tool_name: str, - transport_factory: Callable[[], contextlib.AbstractAsyncContextManager[Any]], + transport_factory: Callable[ + [], contextlib.AbstractAsyncContextManager[Any] + ], ) -> Callable[..., Awaitable[Any]]: """Create a tool function that manages its own connection.""" async def call_tool(**kwargs: Any) -> Any: - import mcp.types as _mcp_types + import mcp.types as _mcp_types # noqa: PLC0415 - client = await _get_or_create_connection(connection_key, transport_factory) + client = await _get_or_create_connection( + connection_key, transport_factory + ) try: result = await asyncio.wait_for( client.call_tool(tool_name, kwargs), @@ -121,7 +126,9 @@ async def call_tool(**kwargs: Any) -> Any: for part in result.content if isinstance(part, _mcp_types.TextContent) ) - raise RuntimeError(f"MCP tool error: {error_text or 'Unknown error'}") + raise RuntimeError( + f"MCP tool error: {error_text or 'Unknown error'}" + ) if result.structuredContent is not None: return result.structuredContent @@ -168,8 +175,9 @@ async def get_stdio_tools( tools = await ai.mcp.get_stdio_tools( "npx", "-y", "@anthropic/mcp-server-filesystem", "/tmp" ) + """ - import mcp.client.stdio as _mcp_stdio + import mcp.client.stdio as _mcp_stdio # noqa: PLC0415 connection_key = f"stdio:{command}:{':'.join(args)}" @@ -187,7 +195,9 @@ def transport_factory() -> contextlib.AbstractAsyncContextManager[Any]: result = await client.list_tools() return [ - _mcp_tool_to_native(mcp_tool, connection_key, transport_factory, tool_prefix) + _mcp_tool_to_native( + mcp_tool, connection_key, transport_factory, tool_prefix + ) for mcp_tool in result.tools ] @@ -217,22 +227,29 @@ async def get_http_tools( "http://localhost:3000/mcp", headers={"Authorization": "Bearer xxx"} ) + """ - import httpx as _httpx - import mcp.client.streamable_http as _mcp_http + import httpx as _httpx # noqa: PLC0415 + import mcp.client.streamable_http as _mcp_http # noqa: PLC0415 connection_key = f"http:{url}" def transport_factory() -> contextlib.AbstractAsyncContextManager[Any]: http_client = _httpx.AsyncClient(headers=headers) if headers else None - return _mcp_http.streamable_http_client(url=url, http_client=http_client) + return _mcp_http.streamable_http_client( + url=url, http_client=http_client + ) async with ensure_connection_pool(): - client = await _get_or_create_connection(connection_key, transport_factory) + client = await _get_or_create_connection( + connection_key, transport_factory + ) result = await client.list_tools() return [ - _mcp_tool_to_native(mcp_tool, connection_key, transport_factory, tool_prefix) + _mcp_tool_to_native( + mcp_tool, connection_key, transport_factory, tool_prefix + ) for mcp_tool in result.tools ] @@ -240,7 +257,9 @@ def transport_factory() -> contextlib.AbstractAsyncContextManager[Any]: def _mcp_tool_to_native( mcp_tool: mcp.types.Tool, connection_key: str, - transport_factory: Callable[[], contextlib.AbstractAsyncContextManager[Any]], + transport_factory: Callable[ + [], contextlib.AbstractAsyncContextManager[Any] + ], tool_prefix: str | None, ) -> AgentTool: """Convert an MCP tool to a native AgentTool. diff --git a/src/ai/agents/runtime.py b/src/ai/agents/runtime.py index de48fe08..33e3e760 100644 --- a/src/ai/agents/runtime.py +++ b/src/ai/agents/runtime.py @@ -4,8 +4,7 @@ import asyncio import contextvars -from collections.abc import AsyncGenerator, AsyncIterable, Awaitable -from typing import Any +from typing import TYPE_CHECKING, Any from .. import util from ..types import events as events_ @@ -13,6 +12,9 @@ from . import hooks as hooks_ from .mcp import client as mcp_client +if TYPE_CHECKING: + from collections.abc import AsyncGenerator, AsyncIterable, Awaitable + class Runtime: """Central event queue. Producers put events, run() yields them.""" @@ -66,7 +68,7 @@ async def _stop_when_done(runtime: Runtime, task: Awaitable[None]) -> None: async def run( source: AsyncIterable[events_.AgentEvent], ) -> AsyncGenerator[events_.AgentEvent]: - """Run *source* and yield every event that gets put into the Runtime queue.""" + """Run *source* and yield events put into the Runtime queue.""" rt = Runtime() async def _drain() -> None: diff --git a/src/ai/agents/ui/ai_sdk/__init__.py b/src/ai/agents/ui/ai_sdk/__init__.py index 8f283199..711e7892 100644 --- a/src/ai/agents/ui/ai_sdk/__init__.py +++ b/src/ai/agents/ui/ai_sdk/__init__.py @@ -1,4 +1,4 @@ -"""AI SDK UI adapter — ``ai.Messages`` in, ``ai.Messages`` out, SSE on the wire.""" +"""AI SDK UI adapter for messages and SSE streams.""" from .inbound import ( ApprovalResponse, @@ -11,9 +11,9 @@ from .ui_message import UIMessage __all__ = [ + "UI_MESSAGE_STREAM_HEADERS", "ApprovalResponse", "UIMessage", - "UI_MESSAGE_STREAM_HEADERS", "apply_approvals", "extract_approvals", "to_messages", diff --git a/src/ai/agents/ui/ai_sdk/_approvals.py b/src/ai/agents/ui/ai_sdk/_approvals.py index 8ad0d10c..f0694657 100644 --- a/src/ai/agents/ui/ai_sdk/_approvals.py +++ b/src/ai/agents/ui/ai_sdk/_approvals.py @@ -24,7 +24,7 @@ def tool_call_id_for(hook_part: messages_.HookPart[Any]) -> str | None: def is_tool_approval_message(msg: messages_.Message) -> bool: - """True if every part of ``msg`` is a ToolApproval HookPart.""" + """Return whether every part of ``msg`` is a ToolApproval HookPart.""" if not msg.parts: return False return all( diff --git a/src/ai/agents/ui/ai_sdk/_parts.py b/src/ai/agents/ui/ai_sdk/_parts.py index d63db855..3637174b 100644 --- a/src/ai/agents/ui/ai_sdk/_parts.py +++ b/src/ai/agents/ui/ai_sdk/_parts.py @@ -8,7 +8,7 @@ from __future__ import annotations import json -from typing import Any +from typing import Any, cast from ....types import messages as messages_ from . import _approvals, ui_message @@ -34,7 +34,9 @@ def to_ui_parts(parts: list[messages_.Part]) -> list[ui_message.UIMessagePart]: if isinstance(part, messages_.TextPart) and part.text: result.append(ui_message.UITextPart(type="text", text=part.text)) elif isinstance(part, messages_.ReasoningPart) and part.text: - result.append(ui_message.UIReasoningPart(type="reasoning", text=part.text)) + result.append( + ui_message.UIReasoningPart(type="reasoning", text=part.text) + ) elif isinstance(part, messages_.ToolCallPart): result.append( ui_message.UIToolPart.model_validate( @@ -125,7 +127,10 @@ def merge_approval_signals( updates["state"] = "approval-requested" updates["approval"] = ui_message.UIToolApproval(id=part.hook_id) elif part.status == "resolved": - resolution = part.resolution or {} + resolution = cast( + "dict[str, Any]", + part.resolution if isinstance(part.resolution, dict) else {}, + ) updates["approval"] = ui_message.UIToolApproval( id=part.hook_id, approved=resolution.get("granted"), diff --git a/src/ai/agents/ui/ai_sdk/inbound.py b/src/ai/agents/ui/ai_sdk/inbound.py index b138e1e9..e4918a22 100644 --- a/src/ai/agents/ui/ai_sdk/inbound.py +++ b/src/ai/agents/ui/ai_sdk/inbound.py @@ -1,4 +1,4 @@ -"""Inbound adapter: AI SDK v6 UIMessages → internal ``ai.messages.Message`` list. +"""Inbound adapter from AI SDK v6 UIMessages to internal messages. The primary entry point is :func:`to_messages`, which bundles normalization, approval extraction, parsing, and pre-registration of approval resolutions. @@ -19,7 +19,9 @@ _TOOL_RESULT_STATES: frozenset[str] = frozenset({"output-available"}) -_TOOL_ERROR_STATES: frozenset[str] = frozenset({"output-error", "output-denied"}) +_TOOL_ERROR_STATES: frozenset[str] = frozenset( + {"output-error", "output-denied"} +) def _is_tool_completed(state: ui_message.UIToolInvocationState) -> bool: @@ -81,7 +83,9 @@ def _decode_wire_output(output: Any) -> Any: return MessageBundle(messages=tuple(inner)) -def _approval_hook_part(tp: ui_message.UIToolPart) -> messages_.HookPart[Any] | None: +def _approval_hook_part( + tp: ui_message.UIToolPart, +) -> messages_.HookPart[Any] | None: """Reconstruct approval hook state from a UI tool part when possible.""" approval = tp.approval if approval is None: @@ -125,7 +129,7 @@ def _approval_hook_part(tp: ui_message.UIToolPart) -> messages_.HookPart[Any] | class ApprovalResponse(NamedTuple): - """Approval response extracted from a UIToolPart in ``approval-responded`` state.""" + """Approval response extracted from a responded UIToolPart.""" hook_id: str granted: bool @@ -178,7 +182,7 @@ def apply_approvals(approvals: list[ApprovalResponse]) -> None: def _normalize_ui_messages( ui_messages: list[ui_message.UIMessage], ) -> list[ui_message.UIMessage]: - """Heal stale tool-part states from previously persisted assistant history.""" + """Heal stale tool-part states from persisted assistant history.""" normalized: list[ui_message.UIMessage] = [] for message in ui_messages: new_parts = [] @@ -210,7 +214,9 @@ def _normalize_ui_messages( new_parts.append(part) normalized.append( - message.model_copy(update={"parts": new_parts}) if changed else message + message.model_copy(update={"parts": new_parts}) + if changed + else message ) return normalized @@ -252,8 +258,10 @@ def _patch_pending_hook_aborts( messages: list[messages_.Message], approvals: list[ApprovalResponse], ) -> None: - """Inject ``is_hook_pending=True`` placeholders for tool calls whose - approval was responded to but whose tool result is still missing. + """Inject pending-hook placeholders for unresolved tool calls. + + This handles tool calls whose approval was responded to but whose tool + result is still missing. This deals with the case where a prior run emitted multiple tool calls, some of which succeeded and some of which aborted on an @@ -300,7 +308,7 @@ def _patch_pending_hook_aborts( def _is_approval_response(msg: messages_.Message) -> bool: - """Internal message that records a resolved tool-approval hook.""" + """Return whether ``msg`` records a resolved tool-approval hook.""" if msg.role != "internal" or len(msg.parts) != 1: return False part = msg.parts[0] @@ -350,7 +358,9 @@ def _build_result_part( assistant_parts.append(messages_.TextPart(text=text)) case ui_message.UIReasoningPart(text=reasoning) if reasoning: - assistant_parts.append(messages_.ReasoningPart(text=reasoning)) + assistant_parts.append( + messages_.ReasoningPart(text=reasoning) + ) case ui_message.UIToolInvocationPart() as inv: tool_args = json.dumps(inv.args) if inv.args else "{}" @@ -420,7 +430,8 @@ def _build_result_part( if ui_msg.role in ("user", "system") and not assistant_parts: raise ValueError( - f"Message '{ui_msg.id}' has role '{ui_msg.role}' but no content. " + f"Message {ui_msg.id!r} has role {ui_msg.role!r} " + "but no content. " "User and system messages require non-empty content." ) @@ -490,7 +501,9 @@ def _split_assistant_parts( messages.append( messages_.Message(role="assistant", parts=current, id=msg_id) ) - messages.append(messages_.Message(role="tool", parts=list(current_results))) + messages.append( + messages_.Message(role="tool", parts=list(current_results)) + ) current = [] current_results = [] seen_tool_call = False @@ -503,8 +516,12 @@ def _split_assistant_parts( current_results.append(results_by_id[part.tool_call_id]) if current: - messages.append(messages_.Message(role="assistant", parts=current, id=msg_id)) + messages.append( + messages_.Message(role="assistant", parts=current, id=msg_id) + ) if current_results: - messages.append(messages_.Message(role="tool", parts=list(current_results))) + messages.append( + messages_.Message(role="tool", parts=list(current_results)) + ) return messages diff --git a/src/ai/agents/ui/ai_sdk/outbound/__init__.py b/src/ai/agents/ui/ai_sdk/outbound/__init__.py index 3e620c92..abb6f69c 100644 --- a/src/ai/agents/ui/ai_sdk/outbound/__init__.py +++ b/src/ai/agents/ui/ai_sdk/outbound/__init__.py @@ -4,4 +4,4 @@ from .sse import to_sse from .stream import to_stream -__all__ = ["to_stream", "to_sse", "to_ui_messages"] +__all__ = ["to_sse", "to_stream", "to_ui_messages"] diff --git a/src/ai/agents/ui/ai_sdk/outbound/_state.py b/src/ai/agents/ui/ai_sdk/outbound/_state.py index 3302c986..baf8ab1c 100644 --- a/src/ai/agents/ui/ai_sdk/outbound/_state.py +++ b/src/ai/agents/ui/ai_sdk/outbound/_state.py @@ -64,7 +64,9 @@ def __init__(self) -> None: # Per-tool-call aggregators for streaming generator tools. Each # PartialToolCallResult feeds its value into the aggregator and # the snapshot goes out as a preliminary tool output. - self.partial_aggregators: dict[str, events_.Aggregator[Any, Any, Any]] = {} + self.partial_aggregators: dict[ + str, events_.Aggregator[Any, Any, Any] + ] = {} # -- boundary helpers ---------------------------------------------------- @@ -109,7 +111,9 @@ def _ensure_started(self) -> list[protocol.UIMessageStreamPart]: # -- phase: streaming events -------------------------------------------- - def on_event(self, event: events_.Event) -> list[protocol.UIMessageStreamPart]: + def on_event( + self, event: events_.Event + ) -> list[protocol.UIMessageStreamPart]: out: list[protocol.UIMessageStreamPart] = [] # Lazily open the UI message on the first streaming event. @@ -254,7 +258,7 @@ def on_tool_result( def on_partial_tool_result( self, event: events_.PartialToolCallResult ) -> list[protocol.UIMessageStreamPart]: - """Feed the value into the tool's aggregator and emit a preliminary output. + """Feed the value and emit a preliminary output. Each PartialToolCallResult carries one yielded value plus the aggregator factory the tool was declared with. We instantiate @@ -295,7 +299,9 @@ def on_partial_tool_result( # -- phase: hooks ------------------------------------------------------- - def on_hook(self, event: events_.HookEvent) -> list[protocol.UIMessageStreamPart]: + def on_hook( + self, event: events_.HookEvent + ) -> list[protocol.UIMessageStreamPart]: """Handle a ``HookEvent`` — emit approval parts.""" hook_part = event.hook out: list[protocol.UIMessageStreamPart] = [] @@ -319,7 +325,7 @@ def on_hook(self, event: events_.HookEvent) -> list[protocol.UIMessageStreamPart ) elif hook_part.status == "resolved": resolution: dict[str, Any] = hook_part.resolution or {} - if not resolution.get("granted", False): + if not resolution.get("granted"): out.append(protocol.ToolOutputDeniedPart(tool_call_id=tc_id)) elif hook_part.status == "cancelled": out.append( diff --git a/src/ai/agents/ui/ai_sdk/outbound/history.py b/src/ai/agents/ui/ai_sdk/outbound/history.py index c5d5809b..eb5a20a4 100644 --- a/src/ai/agents/ui/ai_sdk/outbound/history.py +++ b/src/ai/agents/ui/ai_sdk/outbound/history.py @@ -2,9 +2,13 @@ from __future__ import annotations -from .....types import messages as messages_ +from typing import TYPE_CHECKING + from .. import _parts, ui_message +if TYPE_CHECKING: + from .....types import messages as messages_ + def to_ui_messages( messages: list[messages_.Message], diff --git a/src/ai/agents/ui/ai_sdk/outbound/sse.py b/src/ai/agents/ui/ai_sdk/outbound/sse.py index 4e784aba..88207c54 100644 --- a/src/ai/agents/ui/ai_sdk/outbound/sse.py +++ b/src/ai/agents/ui/ai_sdk/outbound/sse.py @@ -4,15 +4,18 @@ import dataclasses import json -from collections.abc import AsyncGenerator, AsyncIterable -from typing import Any +from typing import TYPE_CHECKING, Any import pydantic -from .....types import events as events_ from .. import protocol from .stream import to_stream +if TYPE_CHECKING: + from collections.abc import AsyncGenerator, AsyncIterable + + from .....types import events as events_ + def _to_camel_case(snake_str: str) -> str: components = snake_str.split("_") @@ -28,7 +31,9 @@ def _json_default(obj: Any) -> Any: """ if isinstance(obj, pydantic.BaseModel): return obj.model_dump(mode="json", by_alias=True) - raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable") + raise TypeError( + f"Object of type {type(obj).__name__} is not JSON serializable" + ) def serialize_part(part: protocol.UIMessageStreamPart) -> str: diff --git a/src/ai/agents/ui/ai_sdk/outbound/stream.py b/src/ai/agents/ui/ai_sdk/outbound/stream.py index 91747547..4b70f920 100644 --- a/src/ai/agents/ui/ai_sdk/outbound/stream.py +++ b/src/ai/agents/ui/ai_sdk/outbound/stream.py @@ -1,13 +1,17 @@ -"""Convert an internal ``ai.events.Event`` stream into AI SDK UI protocol parts.""" +"""Convert internal event streams into AI SDK UI protocol parts.""" from __future__ import annotations -from collections.abc import AsyncGenerator, AsyncIterable +from typing import TYPE_CHECKING from .....types import events as events_ -from .. import protocol from ._state import _StreamState +if TYPE_CHECKING: + from collections.abc import AsyncGenerator, AsyncIterable + + from .. import protocol + async def to_stream( events: AsyncIterable[events_.AgentEvent], diff --git a/src/ai/agents/ui/ai_sdk/protocol.py b/src/ai/agents/ui/ai_sdk/protocol.py index 32a49266..e0eb8902 100644 --- a/src/ai/agents/ui/ai_sdk/protocol.py +++ b/src/ai/agents/ui/ai_sdk/protocol.py @@ -34,7 +34,9 @@ class TextStartPart: """Indicates the beginning of a text block.""" id: str - type: Literal["text-start"] = dataclasses.field(default="text-start", init=False) + type: Literal["text-start"] = dataclasses.field( + default="text-start", init=False + ) provider_metadata: dict[str, Any] | None = None @@ -44,7 +46,9 @@ class TextDeltaPart: id: str delta: str - type: Literal["text-delta"] = dataclasses.field(default="text-delta", init=False) + type: Literal["text-delta"] = dataclasses.field( + default="text-delta", init=False + ) provider_metadata: dict[str, Any] | None = None @@ -53,7 +57,9 @@ class TextEndPart: """Indicates the completion of a text block.""" id: str - type: Literal["text-end"] = dataclasses.field(default="text-end", init=False) + type: Literal["text-end"] = dataclasses.field( + default="text-end", init=False + ) provider_metadata: dict[str, Any] | None = None @@ -97,7 +103,9 @@ class SourceUrlPart: source_id: str url: str - type: Literal["source-url"] = dataclasses.field(default="source-url", init=False) + type: Literal["source-url"] = dataclasses.field( + default="source-url", init=False + ) title: str | None = None provider_metadata: dict[str, Any] | None = None @@ -128,9 +136,9 @@ class FilePart: @dataclasses.dataclass class DataPart: - """ - Custom data parts allow streaming of arbitrary structured data with type-specific - handling. + """Custom data part for arbitrary structured data. + + Data parts support type-specific handling. The wire type is ``data-{data_type}`` (e.g. ``data-custom``), exposed via the ``type`` property so that ``DataPart`` is uniform with every @@ -258,14 +266,18 @@ class ToolApprovalRequestPart: class StartStepPart: """A part indicating the start of a step.""" - type: Literal["start-step"] = dataclasses.field(default="start-step", init=False) + type: Literal["start-step"] = dataclasses.field( + default="start-step", init=False + ) @dataclasses.dataclass class FinishStepPart: """A part indicating that a step has been completed.""" - type: Literal["finish-step"] = dataclasses.field(default="finish-step", init=False) + type: Literal["finish-step"] = dataclasses.field( + default="finish-step", init=False + ) @dataclasses.dataclass diff --git a/src/ai/agents/ui/ai_sdk/ui_message.py b/src/ai/agents/ui/ai_sdk/ui_message.py index 7463a67d..ea3ccd0f 100644 --- a/src/ai/agents/ui/ai_sdk/ui_message.py +++ b/src/ai/agents/ui/ai_sdk/ui_message.py @@ -1,5 +1,4 @@ -""" -Pydantic models for parsing AI SDK v6 UI messages. +"""Pydantic models for parsing AI SDK v6 UI messages. Reference: https://ai-sdk.dev/docs/reference/ai-sdk-core/ui-message @@ -57,9 +56,10 @@ class UIReasoningPart(pydantic.BaseModel): class UIToolInvocationPart(pydantic.BaseModel): - """Tool invocation part in AI SDK v6 format (legacy type: "tool-invocation"). + """Tool invocation part in AI SDK v6 format. Note: The AI SDK frontend typically sends tool-{toolName} format instead. + The legacy type is ``tool-invocation``. This model is kept for backwards compatibility. Reference: https://ai-sdk.dev/docs/reference/ai-sdk-core/ui-message @@ -192,7 +192,7 @@ def _parse_ui_part(part_data: dict[str, Any]) -> UIMessagePart | None: part_type = part_data.get("type", "") if model_cls := _STATIC_UI_PART_TYPES.get(part_type): - return cast(UIMessagePart, model_cls.model_validate(part_data)) + return cast("UIMessagePart", model_cls.model_validate(part_data)) match part_type: case str() as t if t.startswith("tool-"): @@ -214,7 +214,9 @@ class UIMessage(pydantic.BaseModel): model_config = pydantic.ConfigDict(populate_by_name=True) - id: str = pydantic.Field(default_factory=lambda: messages_.generate_id("msg")) + id: str = pydantic.Field( + default_factory=lambda: messages_.generate_id("msg") + ) role: Literal["user", "assistant", "system"] parts: list[UIMessagePart] = pydantic.Field(default_factory=list) diff --git a/src/ai/errors.py b/src/ai/errors.py index 4567e02b..cdb110e5 100644 --- a/src/ai/errors.py +++ b/src/ai/errors.py @@ -3,8 +3,10 @@ from __future__ import annotations import dataclasses +from typing import TYPE_CHECKING -import httpx +if TYPE_CHECKING: + import httpx @dataclasses.dataclass(frozen=True) @@ -54,6 +56,7 @@ def __init__( message: Human-readable error message. provider: Provider name or id associated with the failure, when known. + """ super().__init__(message) self.message = message @@ -129,6 +132,7 @@ def __init__( succeed. When omitted, this defaults from ``http_context.status_code`` when a status is present; provider mappers may override it for transport-level failures. + """ super().__init__(message, provider=provider) self.request_id = request_id @@ -138,7 +142,9 @@ def __init__( self.param = param self.type = error_type self.is_retryable = ( - _is_retryable_status(http_context.status_code if http_context else None) + _is_retryable_status( + http_context.status_code if http_context else None + ) if is_retryable is None else is_retryable ) @@ -215,6 +221,7 @@ def __init__( is_retryable: Whether retrying the same request may reasonably succeed. When omitted, this defaults from the HTTP status when present. + """ super().__init__( message, @@ -321,8 +328,8 @@ def _is_retryable_status(status_code: int | None) -> bool: "ProviderError", "ProviderInternalServerError", "ProviderModelNotFoundError", - "ProviderNotFoundError", "ProviderNotConfiguredError", + "ProviderNotFoundError", "ProviderOverloadedError", "ProviderPermissionDeniedError", "ProviderRateLimitError", diff --git a/src/ai/models/core/__init__.py b/src/ai/models/core/__init__.py index 07c79e1e..03eecffc 100644 --- a/src/ai/models/core/__init__.py +++ b/src/ai/models/core/__init__.py @@ -30,7 +30,7 @@ "VideoParams", "generate", "get_model", + "helpers", "probe", "stream", - "helpers", ] diff --git a/src/ai/models/core/api.py b/src/ai/models/core/api.py index c9129ccf..94c23c72 100644 --- a/src/ai/models/core/api.py +++ b/src/ai/models/core/api.py @@ -2,7 +2,6 @@ import contextlib import dataclasses -from collections.abc import AsyncGenerator, AsyncIterator, Sequence from contextlib import AbstractAsyncContextManager from typing import ( TYPE_CHECKING, @@ -19,15 +18,17 @@ # ``typing.TypeVar`` lacks the ``default=`` kwarg on Python <3.13. # Use the typing_extensions backport so this works on 3.12 too. -from typing_extensions import TypeVar # noqa: UP035 +from typing_extensions import TypeVar from ... import types from ...types import integrity -from . import model as model_ -from . import params as params_ if TYPE_CHECKING: + from collections.abc import AsyncGenerator, AsyncIterator, Sequence + from ...providers import base as provider_base + from . import model as model_ + from . import params as params_ # Stream output type. Defaults to ``str``: when the stream was opened # without an ``output_type``, ``Stream.output`` returns the concatenated @@ -85,7 +86,9 @@ async def _do_stream( ): yield ev - async def _do_generate(self, request: GenerateRequest) -> types.messages.Message: + async def _do_generate( + self, request: GenerateRequest + ) -> types.messages.Message: return await request.model.provider.generate( request.model, request.messages, @@ -122,14 +125,14 @@ def __init__( the concatenated text content unchanged. """ self._gen = gen - self._message: types.messages.Message = seed_message or types.messages.Message( - role="assistant", parts=[] + self._message: types.messages.Message = ( + seed_message or types.messages.Message(role="assistant", parts=[]) ) self._parts: dict[str, types.messages.Part] = {} # ``output_type`` is typed against the public ``StreamOutputT`` type # param for ergonomics; internally we know it's a Pydantic model # subclass (or None for the text-default case). - self._output_type = cast(type[pydantic.BaseModel] | None, output_type) + self._output_type = cast("type[pydantic.BaseModel] | None", output_type) async def aclose(self) -> None: await self._gen.aclose() @@ -183,7 +186,9 @@ def output(self) -> StreamOutputT: model subclass was passed, validates the streamed JSON against it and returns the parsed instance. """ - return cast(StreamOutputT, self._message.get_output(self._output_type)) + return cast( + "StreamOutputT", self._message.get_output(self._output_type) + ) def _aggregate_event(self, event: types.events.Event) -> dict[str, Any]: updates: dict[str, Any] = {} @@ -199,10 +204,14 @@ def _aggregate_event(self, event: types.events.Event) -> dict[str, Any]: match event: case types.events.TextStart(block_id=bid, provider_metadata=pm): - tp = types.messages.TextPart(id=bid, text="", provider_metadata=pm) + tp = types.messages.TextPart( + id=bid, text="", provider_metadata=pm + ) self._message.parts.append(tp) self._parts[bid] = tp - case types.events.TextDelta(block_id=bid, chunk=c, provider_metadata=pm): + case types.events.TextDelta( + block_id=bid, chunk=c, provider_metadata=pm + ): existing_text = self._parts.get(bid) if isinstance(existing_text, types.messages.TextPart): existing_text.text += c @@ -215,8 +224,12 @@ def _aggregate_event(self, event: types.events.Event) -> dict[str, Any]: and pm is not None ): existing_text.provider_metadata = pm - case types.events.ReasoningStart(block_id=bid, provider_metadata=pm): - rp = types.messages.ReasoningPart(id=bid, text="", provider_metadata=pm) + case types.events.ReasoningStart( + block_id=bid, provider_metadata=pm + ): + rp = types.messages.ReasoningPart( + id=bid, text="", provider_metadata=pm + ) self._message.parts.append(rp) self._parts[bid] = rp case types.events.ReasoningDelta( @@ -283,13 +296,17 @@ def _aggregate_event(self, event: types.events.Event) -> dict[str, Any]: existing_btc.tool_args += c if pm is not None: existing_btc.provider_metadata = pm - case types.events.BuiltinToolEnd(tool_call_id=tcid, provider_metadata=pm): + case types.events.BuiltinToolEnd( + tool_call_id=tcid, provider_metadata=pm + ): existing_btc = self._parts.get(tcid) if isinstance(existing_btc, types.messages.BuiltinToolCallPart): updates["tool_call"] = existing_btc if pm is not None: existing_btc.provider_metadata = pm - case types.events.BuiltinToolResult(result=res, provider_metadata=pm): + case types.events.BuiltinToolResult( + result=res, provider_metadata=pm + ): if pm is not None: res = res.model_copy(update={"provider_metadata": pm}) self._message.parts.append(res) @@ -425,7 +442,8 @@ def stream( if context is not None: if model is not None or messages is not None or tools is not None: raise TypeError( - "stream() takes either model/messages/tools or context=, not both" + "stream() takes either model/messages/tools or context=, " + "not both" ) model = context.model messages = context.messages @@ -435,7 +453,9 @@ def stream( if params is None: params = context.params elif model is None or messages is None: - raise TypeError("stream() requires either model and messages or context=") + raise TypeError( + "stream() requires either model and messages or context=" + ) return _stream( model=model, @@ -461,15 +481,20 @@ async def _stream( ) -> AsyncIterator[Stream[Any]]: if messages and messages[-1].replay: last = messages[-1] - s = Stream( + s: Stream[Any] = Stream( _replay_tool_calls(last), seed_message=last.model_copy(deep=True), - output_type=output_type, + output_type=cast("type[Any] | None", output_type), ) else: prepared = integrity.prepare_messages(messages) - request = StreamRequest(model, prepared, tools, output_type, params, protocol) - s = Stream(executor._do_stream(request), output_type=output_type) + request = StreamRequest( + model, prepared, tools, output_type, params, protocol + ) + s = Stream( + executor._do_stream(request), + output_type=cast("type[Any] | None", output_type), + ) try: yield s finally: diff --git a/src/ai/models/core/helpers/files.py b/src/ai/models/core/helpers/files.py index 147b448a..c308ca98 100644 --- a/src/ai/models/core/helpers/files.py +++ b/src/ai/models/core/helpers/files.py @@ -4,6 +4,8 @@ :mod:`ai.types.media`. """ +from urllib.parse import urlparse + import httpx DEFAULT_MAX_BYTES = 100 * 1024 * 1024 # 100 MiB (matches TS SDK) @@ -35,8 +37,6 @@ def __init__( def _validate_url(url: str) -> None: """Reject non-HTTP(S) URLs (SSRF prevention).""" - from urllib.parse import urlparse - parsed = urlparse(url) if parsed.scheme not in _ALLOWED_SCHEMES: raise DownloadError( @@ -60,6 +60,7 @@ async def download( Raises: DownloadError: On any failure (network, HTTP status, size, etc.). + """ _validate_url(url) diff --git a/src/ai/models/core/model.py b/src/ai/models/core/model.py index d8902bbe..9ab4bf23 100644 --- a/src/ai/models/core/model.py +++ b/src/ai/models/core/model.py @@ -61,11 +61,13 @@ def get_model( protocol: Optional wire-protocol override for this model. When omitted, the provider chooses its default protocol. + Raises: Raises :class:`ai.ConfigurationError` when ``model_id`` and ``AI_SDK_DEFAULT_MODEL`` is empty or malformed. Raises a :class:`ai.UnsupportedProviderError` when the provider is unrecognized or otherwise unsupported. + """ if model_id is None: model_id = os.environ.get(_DEFAULT_MODEL_ENV) @@ -86,8 +88,12 @@ def get_model( provider_id = ref.provider_id provider_model_id = ref.model_id - model_info = _modelsdev.get_model_by_id(f"{provider_id}:{provider_model_id}") - model_provider_config = None if model_info is None else model_info.provider_config + model_info = _modelsdev.get_model_by_id( + f"{provider_id}:{provider_model_id}" + ) + model_provider_config = ( + None if model_info is None else model_info.provider_config + ) provider = base.Provider.from_id( provider_id, diff --git a/src/ai/providers/_optional.py b/src/ai/providers/_optional.py index 52e8e574..d3f48140 100644 --- a/src/ai/providers/_optional.py +++ b/src/ai/providers/_optional.py @@ -3,12 +3,17 @@ from __future__ import annotations import importlib -from types import ModuleType +from typing import TYPE_CHECKING from .. import errors as ai_errors +if TYPE_CHECKING: + from types import ModuleType -def import_optional_sdk(module_name: str, *, provider: str, extra: str) -> ModuleType: + +def import_optional_sdk( + module_name: str, *, provider: str, extra: str +) -> ModuleType: """Import an optional upstream SDK or raise a helpful installation error.""" root_module = module_name.partition(".")[0] try: diff --git a/src/ai/providers/ai_gateway/client/_client.py b/src/ai/providers/ai_gateway/client/_client.py index 5bfe650b..d90972c8 100644 --- a/src/ai/providers/ai_gateway/client/_client.py +++ b/src/ai/providers/ai_gateway/client/_client.py @@ -3,7 +3,6 @@ from __future__ import annotations import json -from collections.abc import AsyncGenerator, AsyncIterator, Mapping from contextlib import asynccontextmanager from typing import TYPE_CHECKING, Any, Literal from urllib.parse import urlparse @@ -13,6 +12,8 @@ from . import errors if TYPE_CHECKING: + from collections.abc import AsyncGenerator, AsyncIterator, Mapping + from ....models.core import model as model_ _PROTOCOL_VERSION = "0.0.1" diff --git a/src/ai/providers/ai_gateway/client/errors.py b/src/ai/providers/ai_gateway/client/errors.py index 23085dbf..a7000366 100644 --- a/src/ai/providers/ai_gateway/client/errors.py +++ b/src/ai/providers/ai_gateway/client/errors.py @@ -265,7 +265,7 @@ def create_gateway_error( """ # Parse the response body body: Any = response_body - if isinstance(body, (str, bytes)): + if isinstance(body, str | bytes): try: body = json.loads(body) except (json.JSONDecodeError, ValueError): diff --git a/src/ai/providers/ai_gateway/errors.py b/src/ai/providers/ai_gateway/errors.py index 4fec465d..acfe78f1 100644 --- a/src/ai/providers/ai_gateway/errors.py +++ b/src/ai/providers/ai_gateway/errors.py @@ -26,7 +26,9 @@ def map_error(exc: client_errors.GatewayError) -> ai_errors.ProviderAPIError: if isinstance(exc, client_errors.GatewayInternalServerError): return _mapped(ai_errors.ProviderInternalServerError, exc) if isinstance(exc, client_errors.GatewayResponseError): - return _mapped(ai_errors.ProviderResponseError, exc, body=exc.response_body) + return _mapped( + ai_errors.ProviderResponseError, exc, body=exc.response_body + ) if isinstance(exc, client_errors.GatewayTimeoutError): return _mapped(ai_errors.ProviderTimeoutError, exc) return _mapped(ai_errors.ProviderAPIError, exc) @@ -48,7 +50,9 @@ def _mapped( ) -def _http_context(exc: client_errors.GatewayError) -> ai_errors.HTTPErrorContext: +def _http_context( + exc: client_errors.GatewayError, +) -> ai_errors.HTTPErrorContext: return ai_errors.HTTPErrorContext(status_code=exc.status_code) diff --git a/src/ai/providers/ai_gateway/protocol.py b/src/ai/providers/ai_gateway/protocol.py index 76d65467..a19e8a8d 100644 --- a/src/ai/providers/ai_gateway/protocol.py +++ b/src/ai/providers/ai_gateway/protocol.py @@ -1,7 +1,8 @@ """AI Gateway v3 protocol. Converts internal messages to AI Gateway wire payloads and maps gateway -responses back to public event/message types.""" +responses back to public event/message types. +""" import base64 import json @@ -51,7 +52,7 @@ def _extract_input_files( def _file_part_to_wire(part: types.messages.FilePart) -> dict[str, Any]: - """Convert a :class:`FilePart` to the gateway wire format for input files.""" + """Convert a :class:`FilePart` to gateway input-file wire format.""" data = part.data if isinstance(data, str) and types.media.is_url(data): return {"type": "url", "url": data} @@ -96,7 +97,9 @@ async def _messages_to_prompt( match msg.role: case "system": text = "".join( - p.text for p in msg.parts if isinstance(p, types.messages.TextPart) + p.text + for p in msg.parts + if isinstance(p, types.messages.TextPart) ) result.append({"role": "system", "content": text}) @@ -118,7 +121,9 @@ async def _messages_to_prompt( {"type": "reasoning", "text": text} ) case types.messages.TextPart(text=text): - assistant_content.append({"type": "text", "text": text}) + assistant_content.append( + {"type": "text", "text": text} + ) case types.messages.ToolCallPart() as tp: tool_input: Any = ( json.loads(tp.tool_args) if tp.tool_args else {} @@ -133,7 +138,9 @@ async def _messages_to_prompt( ) case types.messages.BuiltinToolCallPart() as btp: btp_input: Any = ( - json.loads(btp.tool_args) if btp.tool_args else {} + json.loads(btp.tool_args) + if btp.tool_args + else {} ) assistant_content.append( { @@ -157,7 +164,9 @@ async def _messages_to_prompt( "providerExecuted": True, } ) - result.append({"role": "assistant", "content": assistant_content}) + result.append( + {"role": "assistant", "content": assistant_content} + ) case "tool": tool_results: list[dict[str, Any]] = [] @@ -168,7 +177,9 @@ async def _messages_to_prompt( { "type": "error-text", "value": ( - str(model_input) if model_input is not None else "" + str(model_input) + if model_input is not None + else "" ), } if part.is_error @@ -288,11 +299,15 @@ def _expand_tool_call( provider_executed_ids = set() tool_name = data.get("toolName", "") tool_input = data.get("input", "") - args_str = tool_input if isinstance(tool_input, str) else json.dumps(tool_input) + args_str = ( + tool_input if isinstance(tool_input, str) else json.dumps(tool_input) + ) if _is_provider_executed(data) or tc_id in provider_executed_ids: provider_executed_ids.add(tc_id) return [ - types.events.BuiltinToolStart(tool_call_id=tc_id, tool_name=tool_name), + types.events.BuiltinToolStart( + tool_call_id=tc_id, tool_name=tool_name + ), types.events.BuiltinToolDelta(tool_call_id=tc_id, chunk=args_str), types.events.BuiltinToolEnd( tool_call_id=tc_id, @@ -320,7 +335,9 @@ def _parse_usage(data: Any) -> types.usage.Usage: input_tokens_obj = data.get("inputTokens") output_tokens_obj = data.get("outputTokens") - if isinstance(input_tokens_obj, dict) or isinstance(output_tokens_obj, dict): + if isinstance(input_tokens_obj, dict) or isinstance( + output_tokens_obj, dict + ): inp = input_tokens_obj if isinstance(input_tokens_obj, dict) else {} out = output_tokens_obj if isinstance(output_tokens_obj, dict) else {} return types.usage.Usage( @@ -334,7 +351,9 @@ def _parse_usage(data: Any) -> types.usage.Usage: return types.usage.Usage( input_tokens=data.get("prompt_tokens") or data.get("inputTokens") or 0, - output_tokens=(data.get("completion_tokens") or data.get("outputTokens") or 0), + output_tokens=( + data.get("completion_tokens") or data.get("outputTokens") or 0 + ), raw=data, ) @@ -363,7 +382,11 @@ def _parse_stream_part( return [types.events.TextEnd(block_id=data.get("id", "text"))] case "reasoning-start": - return [types.events.ReasoningStart(block_id=data.get("id", "reasoning"))] + return [ + types.events.ReasoningStart( + block_id=data.get("id", "reasoning") + ) + ] case "reasoning-delta": return [ @@ -374,7 +397,9 @@ def _parse_stream_part( ] case "reasoning-end": - return [types.events.ReasoningEnd(block_id=data.get("id", "reasoning"))] + return [ + types.events.ReasoningEnd(block_id=data.get("id", "reasoning")) + ] case "tool-input-start": tcid = data.get("id", "") @@ -430,7 +455,9 @@ def _parse_stream_part( ] case "tool-call": - return _expand_tool_call(data, streamed_tool_ids, provider_executed_ids) + return _expand_tool_call( + data, streamed_tool_ids, provider_executed_ids + ) case "tool-result": tcid = data.get("toolCallId", "") @@ -456,7 +483,9 @@ def _parse_stream_part( return [ types.events.FileEvent( block_id=data.get("id", ""), - media_type=data.get("mediaType", "application/octet-stream"), + media_type=data.get( + "mediaType", "application/octet-stream" + ), data=data.get("data", ""), ) ] @@ -555,7 +584,9 @@ async def _generate_image( parts: list[types.messages.Part] = [] for img_b64 in raw_images: media_type = types.media.detect_image_media_type(img_b64) or "image/png" - parts.append(types.messages.FilePart(data=img_b64, media_type=media_type)) + parts.append( + types.messages.FilePart(data=img_b64, media_type=media_type) + ) return types.messages.Message(role="assistant", parts=parts, usage=usage) @@ -614,11 +645,15 @@ async def _generate_video( if content_type: media_type = content_type parts.append( - types.messages.FilePart(data=downloaded_bytes, media_type=media_type) + types.messages.FilePart( + data=downloaded_bytes, media_type=media_type + ) ) else: raw_data = video_data.get("data", "") - parts.append(types.messages.FilePart(data=raw_data, media_type=media_type)) + parts.append( + types.messages.FilePart(data=raw_data, media_type=media_type) + ) return types.messages.Message(role="assistant", parts=parts) diff --git a/src/ai/providers/ai_gateway/provider.py b/src/ai/providers/ai_gateway/provider.py index 131844e1..e61c0d40 100644 --- a/src/ai/providers/ai_gateway/provider.py +++ b/src/ai/providers/ai_gateway/provider.py @@ -1,15 +1,12 @@ """AI Gateway provider. -Defines the callable :data:`ai_gateway` provider.""" +Defines the callable :data:`ai_gateway` provider. +""" from __future__ import annotations -from collections.abc import AsyncGenerator, Mapping, Sequence -from types import ModuleType from typing import TYPE_CHECKING, Any, ClassVar -import httpx - from ... import errors as ai_errors from .. import base from . import client as gateway_client @@ -18,6 +15,10 @@ from .client import errors as client_errors if TYPE_CHECKING: + from collections.abc import AsyncGenerator, Mapping, Sequence + from types import ModuleType + + import httpx import modelsdotdev import pydantic @@ -105,7 +106,9 @@ async def generate( protocol: base.ProviderProtocol[Any] | None = None, ) -> messages_.Message: """Generate media via the AI Gateway v3 protocol.""" - return await super().generate(model, messages, params, protocol=protocol) + return await super().generate( + model, messages, params, protocol=protocol + ) @classmethod def from_modelsdev_provider( @@ -137,7 +140,7 @@ def tools(self) -> ModuleType: These tools are executed server-side by the gateway and work with any gateway-routed model. """ - from . import tools as tools_module + from . import tools as tools_module # noqa: PLC0415 return tools_module diff --git a/src/ai/providers/ai_gateway/tools.py b/src/ai/providers/ai_gateway/tools.py index 70788122..41beccfd 100644 --- a/src/ai/providers/ai_gateway/tools.py +++ b/src/ai/providers/ai_gateway/tools.py @@ -73,7 +73,8 @@ def perplexity_search( country: str | None = None, search_domain_filter: list[str] | None = None, search_language_filter: list[str] | None = None, - search_recency_filter: Literal["day", "week", "month", "year"] | None = None, + search_recency_filter: Literal["day", "week", "month", "year"] + | None = None, ) -> types.tools.Tool: return types.tools.Tool( kind="provider", diff --git a/src/ai/providers/anthropic/_sdk.py b/src/ai/providers/anthropic/_sdk.py index e269c3aa..ed39a135 100644 --- a/src/ai/providers/anthropic/_sdk.py +++ b/src/ai/providers/anthropic/_sdk.py @@ -22,7 +22,7 @@ class AnthropicSDK(Protocol): def import_sdk(*, provider: str = "anthropic") -> AnthropicSDK: return cast( - AnthropicSDK, + "AnthropicSDK", _optional.import_optional_sdk( "anthropic", provider=provider, diff --git a/src/ai/providers/anthropic/errors.py b/src/ai/providers/anthropic/errors.py index bf4f0eb5..4a194276 100644 --- a/src/ai/providers/anthropic/errors.py +++ b/src/ai/providers/anthropic/errors.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, cast import httpx @@ -72,9 +72,13 @@ def _map_status_error( model_id: str | None, ) -> ai_errors.ProviderAPIError: if exc.status_code == 404 and model_id is not None: - cls: type[ai_errors.ProviderAPIError] = ai_errors.ProviderModelNotFoundError + cls: type[ai_errors.ProviderAPIError] = ( + ai_errors.ProviderModelNotFoundError + ) else: - cls = ai_errors.http_status_to_provider_status_error_class(exc.status_code) + cls = ai_errors.http_status_to_provider_status_error_class( + exc.status_code + ) return _provider_error(cls, exc, provider=provider, model_id=model_id) @@ -89,7 +93,9 @@ def _provider_error( body = getattr(exc, "body", None) if issubclass(cls, ai_errors.ProviderModelNotFoundError): if model_id is None: # pragma: no cover - guarded by _map_status_error - raise RuntimeError("model_id is required for ProviderModelNotFoundError") + raise RuntimeError( + "model_id is required for ProviderModelNotFoundError" + ) return cls( _message(exc), model_id=model_id, @@ -115,7 +121,9 @@ def _provider_error( ) -def _http_context(exc: anthropic.AnthropicError) -> ai_errors.HTTPErrorContext | None: +def _http_context( + exc: anthropic.AnthropicError, +) -> ai_errors.HTTPErrorContext | None: status_code = getattr(exc, "status_code", None) if not isinstance(status_code, int): return None @@ -131,9 +139,10 @@ def _http_context(exc: anthropic.AnthropicError) -> ai_errors.HTTPErrorContext | def _body_value(body: object | None, key: str) -> str | None: if not isinstance(body, dict): return None - value = body.get(key) + body_map = cast("dict[str, Any]", body) + value = body_map.get(key) if value is None: - error = body.get("error") + error = body_map.get("error") if isinstance(error, dict): value = error.get(key) if isinstance(value, str): diff --git a/src/ai/providers/anthropic/protocol.py b/src/ai/providers/anthropic/protocol.py index 045c8f1e..05351b21 100644 --- a/src/ai/providers/anthropic/protocol.py +++ b/src/ai/providers/anthropic/protocol.py @@ -9,12 +9,11 @@ import base64 import json from collections.abc import AsyncGenerator, Mapping, Sequence -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, cast import pydantic from ... import types -from ...models import core from ...types import events from .. import base from . import _sdk, errors @@ -23,6 +22,8 @@ if TYPE_CHECKING: import anthropic + from ...models import core + PROVIDER_NAME = "anthropic" # Anthropic block types that carry server-tool results. We track these @@ -64,7 +65,7 @@ def _split_tools( def _custom_tools_to_anthropic( tools: Sequence[types.tools.Tool], ) -> list[dict[str, Any]]: - """Convert custom (host-executed) Tool objects to Anthropic tool schema format.""" + """Convert host-executed tools to Anthropic tool schema format.""" result: list[dict[str, Any]] = [] for tool in tools: args = tool.args @@ -194,7 +195,9 @@ async def _messages_to_anthropic( match msg.role: case "system": system_prompt = "".join( - p.text for p in msg.parts if isinstance(p, types.messages.TextPart) + p.text + for p in msg.parts + if isinstance(p, types.messages.TextPart) ) case "assistant": content: list[dict[str, Any]] = [] @@ -204,7 +207,9 @@ async def _messages_to_anthropic( text=text, provider_metadata=provider_metadata, ): - signature = (provider_metadata or {}).get("signature") + signature = (provider_metadata or {}).get( + "signature" + ) if signature: content.append( { @@ -217,7 +222,9 @@ async def _messages_to_anthropic( content.append({"type": "text", "text": text}) case types.messages.ToolCallPart(): tool_input = ( - json.loads(part.tool_args) if part.tool_args else {} + json.loads(part.tool_args) + if part.tool_args + else {} ) content.append( { @@ -229,7 +236,9 @@ async def _messages_to_anthropic( ) case types.messages.BuiltinToolCallPart(): btc_input = ( - json.loads(part.tool_args) if part.tool_args else {} + json.loads(part.tool_args) + if part.tool_args + else {} ) content.append( { @@ -292,7 +301,9 @@ async def _messages_to_anthropic( for p in msg.parts: match p: case types.messages.TextPart(text=text): - user_content.append({"type": "text", "text": text}) + user_content.append( + {"type": "text", "text": text} + ) case types.messages.FilePart(): user_content.append(_file_part_to_anthropic(p)) result.append({"role": "user", "content": user_content}) @@ -327,7 +338,7 @@ def _merge_consecutive_roles( def _to_content_list(content: Any) -> list[dict[str, Any]]: """Normalize Anthropic message content to list-of-blocks.""" if isinstance(content, list): - return list(content) + return cast("list[dict[str, Any]]", list(content)) return [{"type": "text", "text": content}] @@ -399,7 +410,9 @@ async def stream( system_prompt, anthropic_messages = await _messages_to_anthropic(messages) custom_tools, builtin_tools = _split_tools(tools or ()) - wire_tools = _custom_tools_to_anthropic(custom_tools) if custom_tools else [] + wire_tools = ( + _custom_tools_to_anthropic(custom_tools) if custom_tools else [] + ) builtin_betas: set[str] = set() if builtin_tools: builtin_wire, builtin_betas = _builtin_tools_to_anthropic(builtin_tools) @@ -436,6 +449,7 @@ async def stream( async for event in sdk_stream: match event.type: case "content_block_start": + event = cast("Any", event) block = event.content_block idx = event.index block_types[idx] = block.type @@ -464,6 +478,7 @@ async def stream( # complete; we emit on stop so we have full content. case "content_block_delta": + event = cast("Any", event) delta = event.delta idx = event.index @@ -480,7 +495,8 @@ async def stream( ) case "signature_delta": signature_buffer[idx] = ( - signature_buffer.get(idx, "") + delta.signature + signature_buffer.get(idx, "") + + delta.signature ) case "input_json_delta": tool_id = tool_ids.get(idx) @@ -498,6 +514,7 @@ async def stream( ) case "content_block_stop": + event = cast("Any", event) idx = event.index block_type = block_types.get(idx) if block_type == "text": @@ -536,20 +553,25 @@ async def stream( # the canonical tool name. snap = sdk_stream.current_message_snapshot result_block = ( - snap.content[idx] if idx < len(snap.content) else None + snap.content[idx] + if idx < len(snap.content) + else None ) if result_block is None: continue tool_use_id = ( getattr(result_block, "tool_use_id", None) or "" ) - content_payload = _result_block_content(result_block) + content_payload = _result_block_content( + result_block + ) # Look up the corresponding server_tool_use # block to recover the tool name. tool_name = "" for cb in snap.content: if ( - getattr(cb, "type", None) == "server_tool_use" + getattr(cb, "type", None) + == "server_tool_use" and getattr(cb, "id", None) == tool_use_id ): tool_name = getattr(cb, "name", "") or "" @@ -571,7 +593,9 @@ async def stream( usage = types.usage.Usage( input_tokens=sdk_usage.input_tokens or 0, output_tokens=sdk_usage.output_tokens or 0, - cache_read_tokens=getattr(sdk_usage, "cache_read_input_tokens", None), + cache_read_tokens=getattr( + sdk_usage, "cache_read_input_tokens", None + ), cache_write_tokens=getattr( sdk_usage, "cache_creation_input_tokens", None ), diff --git a/src/ai/providers/anthropic/provider.py b/src/ai/providers/anthropic/provider.py index 1d9623f0..be7f5cb7 100644 --- a/src/ai/providers/anthropic/provider.py +++ b/src/ai/providers/anthropic/provider.py @@ -2,8 +2,6 @@ from __future__ import annotations -from collections.abc import AsyncGenerator, Iterable, Mapping, Sequence -from types import ModuleType from typing import TYPE_CHECKING, Any, ClassVar import httpx @@ -15,6 +13,9 @@ from . import tools as tools_module if TYPE_CHECKING: + from collections.abc import AsyncGenerator, Iterable, Mapping, Sequence + from types import ModuleType + import anthropic import modelsdotdev import pydantic @@ -88,7 +89,9 @@ def __init__( env=env, ) self.anthropic_version = anthropic_version - self._close_client_on_aclose = sdk_client is None and http_client is None + self._close_client_on_aclose = ( + sdk_client is None and http_client is None + ) if sdk_client is None: sdk_client = self._make_sdk_client(http_client=http_client) self._set_client(sdk_client) @@ -166,8 +169,12 @@ def from_modelsdev_provider( if resolved_base_url is None and provider.id == "anthropic": resolved_base_url = _BASE_URL if resolved_base_url is None: - raise ValueError(f"provider {provider.id!r} does not declare an API URL") - api_key_env, config_envs = base.provider_config(provider, model_provider_config) + raise ValueError( + f"provider {provider.id!r} does not declare an API URL" + ) + api_key_env, config_envs = base.provider_config( + provider, model_provider_config + ) return cls( name=provider.id, default_base_url=resolved_base_url, diff --git a/src/ai/providers/base.py b/src/ai/providers/base.py index a6cf2507..f59299c8 100644 --- a/src/ai/providers/base.py +++ b/src/ai/providers/base.py @@ -3,15 +3,18 @@ from __future__ import annotations import os -from collections.abc import AsyncGenerator, Iterable, Mapping, Sequence from typing import TYPE_CHECKING, Any, ClassVar, Generic -from typing_extensions import TypeVar # noqa: UP035 - default= is needed on 3.12 +from typing_extensions import ( + TypeVar, +) from .. import _modelsdev from ..errors import UnsupportedProviderError if TYPE_CHECKING: + from collections.abc import AsyncGenerator, Iterable, Mapping, Sequence + import modelsdotdev import pydantic @@ -91,7 +94,9 @@ def __init__( client: ClientT | None = None, ) -> None: if type(self) is Provider: - raise TypeError("Provider is a base class; implement a subclass instead") + raise TypeError( + "Provider is a base class; implement a subclass instead" + ) self._name = name self._base_url = base_url self._protocol = protocol @@ -145,7 +150,9 @@ def api_key(self) -> str | None: return self._api_key if self.api_key_env is None: return None - return self._env.get(self.api_key_env) or os.environ.get(self.api_key_env) + return self._env.get(self.api_key_env) or os.environ.get( + self.api_key_env + ) def is_configured(self) -> bool: """Return ``True`` when all required provider config is available.""" @@ -189,7 +196,9 @@ def name(self) -> str: def protocol(self) -> ProviderProtocol[ClientT]: """Default wire protocol used by this provider.""" if self._protocol is None: - raise RuntimeError(f"provider {self.name!r} does not have a protocol") + raise RuntimeError( + f"provider {self.name!r} does not have a protocol" + ) return self._protocol async def list_models(self) -> list[str]: @@ -239,15 +248,16 @@ async def generate( async def probe(self, model: model_.Model) -> None: """Probe if provider is online and can serve given model. - A probe function verifies that *model* can reach its provider and that it - is available there. It returns successfully when credentials are valid - **and** the model exists on the remote side. + A probe function verifies that *model* can reach its provider and + that it is available there. It returns successfully when credentials + are valid **and** the model exists on the remote side. The check must be **free** — it should only hit metadata / listing endpoints that don't consume tokens or credits. - Failures should raise provider errors; catch ``ProviderModelNotFoundError`` - to distinguish missing models from other failures. + Failures should raise provider errors; catch + ``ProviderModelNotFoundError`` to distinguish missing models from + other failures. """ raise NotImplementedError @@ -337,7 +347,7 @@ def provider_config( provider: modelsdotdev.Provider, model_provider_config: modelsdotdev.ModelProviderConfig | None = None, ) -> tuple[str | None, tuple[str, ...]]: - """Return ``api_key_env`` and non-secret config envs from models.dev data.""" + """Return API key and config envs from models.dev data.""" return _modelsdev.provider_config(provider, model_provider_config) diff --git a/src/ai/providers/openai/_sdk.py b/src/ai/providers/openai/_sdk.py index ac232bf2..92c2ac23 100644 --- a/src/ai/providers/openai/_sdk.py +++ b/src/ai/providers/openai/_sdk.py @@ -2,12 +2,13 @@ from __future__ import annotations -from collections.abc import Callable from typing import TYPE_CHECKING, Any, Protocol, cast from .. import _optional if TYPE_CHECKING: + from collections.abc import Callable + import openai @@ -27,14 +28,16 @@ class OpenAIPydantic(Protocol): def import_sdk(*, provider: str = "openai") -> OpenAISDK: return cast( - OpenAISDK, - _optional.import_optional_sdk("openai", provider=provider, extra="openai"), + "OpenAISDK", + _optional.import_optional_sdk( + "openai", provider=provider, extra="openai" + ), ) def import_pydantic(*, provider: str = "openai") -> OpenAIPydantic: return cast( - OpenAIPydantic, + "OpenAIPydantic", _optional.import_optional_sdk( "openai.lib._pydantic", provider=provider, diff --git a/src/ai/providers/openai/errors.py b/src/ai/providers/openai/errors.py index fdbdbf21..91dfcb5e 100644 --- a/src/ai/providers/openai/errors.py +++ b/src/ai/providers/openai/errors.py @@ -83,11 +83,15 @@ def _map_status_error( model_id: str | None, ) -> ai_errors.ProviderAPIError: if exc.status_code == 404 and model_id is not None: - cls: type[ai_errors.ProviderAPIError] = ai_errors.ProviderModelNotFoundError + cls: type[ai_errors.ProviderAPIError] = ( + ai_errors.ProviderModelNotFoundError + ) else: cls = _STATUS_ERROR_MAP.get( type(exc).__name__ - ) or ai_errors.http_status_to_provider_status_error_class(exc.status_code) + ) or ai_errors.http_status_to_provider_status_error_class( + exc.status_code + ) return _provider_error(cls, exc, provider=provider, model_id=model_id) @@ -102,7 +106,9 @@ def _provider_error( body = getattr(exc, "body", None) if issubclass(cls, ai_errors.ProviderModelNotFoundError): if model_id is None: # pragma: no cover - guarded by _map_status_error - raise RuntimeError("model_id is required for ProviderModelNotFoundError") + raise RuntimeError( + "model_id is required for ProviderModelNotFoundError" + ) return cls( _message(exc), model_id=model_id, diff --git a/src/ai/providers/openai/protocol.py b/src/ai/providers/openai/protocol.py index b18bde7e..41be970e 100644 --- a/src/ai/providers/openai/protocol.py +++ b/src/ai/providers/openai/protocol.py @@ -9,9 +9,7 @@ import base64 import json from collections.abc import AsyncGenerator, Mapping, Sequence -from typing import TYPE_CHECKING, Any - -import pydantic +from typing import TYPE_CHECKING, Any, cast from ... import errors as ai_errors from ... import types @@ -22,6 +20,7 @@ if TYPE_CHECKING: import openai + import pydantic # --------------------------------------------------------------------------- # Message / tool conversion — internal types → OpenAI wire format @@ -179,7 +178,9 @@ async def _messages_to_openai( case "system": content_text = "".join( - p.text for p in msg.parts if isinstance(p, types.messages.TextPart) + p.text + for p in msg.parts + if isinstance(p, types.messages.TextPart) ) result.append({"role": "system", "content": content_text}) @@ -225,7 +226,7 @@ async def stream( params: Any = None, provider: str, ) -> AsyncGenerator[types.events.Event]: - """Stream through the OpenAI chat completions protocol using *sdk_client*.""" + """Stream through the OpenAI chat completions protocol.""" openai_sdk = _sdk.import_sdk(provider=provider) if tools and any(t.kind == "provider" for t in tools): raise NotImplementedError( @@ -325,7 +326,9 @@ async def stream( if not text_started: text_started = True yield types.events.TextStart(block_id="text") - yield types.events.TextDelta(chunk=delta.content, block_id="text") + yield types.events.TextDelta( + chunk=delta.content, block_id="text" + ) if delta.tool_calls: for tc in delta.tool_calls: @@ -472,12 +475,16 @@ def _provider_metadata_for_item( return {_OPENAI_METADATA_KEY: data} -def _provider_metadata_for_response(response: Mapping[str, Any]) -> dict[str, Any]: +def _provider_metadata_for_response( + response: Mapping[str, Any], +) -> dict[str, Any]: response_id = response.get("id") model = response.get("model") status = response.get("status") data = { - **({"response_id": response_id} if isinstance(response_id, str) else {}), + **( + {"response_id": response_id} if isinstance(response_id, str) else {} + ), **({"model": model} if isinstance(model, str) else {}), **({"status": status} if isinstance(status, str) else {}), } @@ -515,7 +522,9 @@ def _stringify_tool_result(result: Any) -> str: async def _file_part_to_responses( part: types.messages.FilePart, ) -> dict[str, Any]: - media_type = "image/jpeg" if part.media_type == "image/*" else part.media_type + media_type = ( + "image/jpeg" if part.media_type == "image/*" else part.media_type + ) data = part.data if media_type.startswith("image/"): @@ -542,7 +551,9 @@ async def _file_part_to_responses( text_content = base64.b64decode(data).decode("utf-8") return {"type": "input_text", "text": text_content} - raise ValueError(f"Unsupported media type for OpenAI Responses: {media_type}") + raise ValueError( + f"Unsupported media type for OpenAI Responses: {media_type}" + ) async def _messages_to_responses( @@ -556,7 +567,9 @@ async def _messages_to_responses( match msg.role: case "system": text = "".join( - p.text for p in msg.parts if isinstance(p, types.messages.TextPart) + p.text + for p in msg.parts + if isinstance(p, types.messages.TextPart) ) if text: result.append({"role": "system", "content": text}) @@ -604,7 +617,10 @@ async def _messages_to_responses( { "type": "reasoning", "summary": [ - {"type": "summary_text", "text": text} + { + "type": "summary_text", + "text": text, + } ], "encrypted_content": encrypted_content, } @@ -679,13 +695,17 @@ def _tools_to_responses( args = tool.args tool_id = getattr(type(args), "openai_id", None) if not isinstance(args, openai_tools.OpenAIProviderArgs): - raise TypeError(f"provider tool {tool.name!r} is not an OpenAI tool") + raise TypeError( + f"provider tool {tool.name!r} is not an OpenAI tool" + ) match tool_id: case "openai.web_search": result.append({"type": "web_search", **_model_dump(args)}) case "openai.web_search_preview": - result.append({"type": "web_search_preview", **_model_dump(args)}) + result.append( + {"type": "web_search_preview", **_model_dump(args)} + ) case "openai.file_search": data = _model_dump(args) ranking = data.pop("ranking", None) @@ -710,7 +730,9 @@ def _tools_to_responses( case "openai.tool_search": result.append({"type": "tool_search", **_model_dump(args)}) case _: - raise NotImplementedError(f"unsupported OpenAI provider tool {tool_id}") + raise NotImplementedError( + f"unsupported OpenAI provider tool {tool_id}" + ) return result @@ -727,11 +749,14 @@ def _event_to_dict(event: Any) -> dict[str, Any]: return { key: value for key in dir(event) - if not key.startswith("_") and not callable(value := getattr(event, key, None)) + if not key.startswith("_") + and not callable(value := getattr(event, key, None)) } -def _usage_from_response(response: Mapping[str, Any]) -> types.usage.Usage | None: +def _usage_from_response( + response: Mapping[str, Any], +) -> types.usage.Usage | None: usage = response.get("usage") if not isinstance(usage, Mapping): return None @@ -814,20 +839,33 @@ def _builtin_tool_args(item: Mapping[str, Any]) -> str: match item_type: case "code_interpreter_call": return _json_dumps( - {"code": item.get("code"), "container_id": item.get("container_id")} + { + "code": item.get("code"), + "container_id": item.get("container_id"), + } ) case "mcp_call" | "mcp_approval_request": arguments = item.get("arguments") - return arguments if isinstance(arguments, str) else _json_dumps(arguments) + return ( + arguments + if isinstance(arguments, str) + else _json_dumps(arguments) + ) case "local_shell_call" | "shell_call": return _json_dumps({"action": item.get("action")}) case "apply_patch_call": return _json_dumps( - {"call_id": item.get("call_id"), "operation": item.get("operation")} + { + "call_id": item.get("call_id"), + "operation": item.get("operation"), + } ) case "tool_search_call": return _json_dumps( - {"arguments": item.get("arguments"), "call_id": item.get("call_id")} + { + "arguments": item.get("arguments"), + "call_id": item.get("call_id"), + } ) case _: return "{}" @@ -839,7 +877,10 @@ def _builtin_tool_result(item: Mapping[str, Any]) -> Any: case "web_search_call": return {"action": item.get("action")} case "file_search_call": - return {"queries": item.get("queries"), "results": item.get("results")} + return { + "queries": item.get("queries"), + "results": item.get("results"), + } case "code_interpreter_call": return { "container_id": item.get("container_id"), @@ -917,10 +958,14 @@ async def _stream_responses( messages, use_item_references=use_item_references, ) - response_tools = _tools_to_responses(request_tools) if request_tools else None + response_tools = ( + _tools_to_responses(request_tools) if request_tools else None + ) api_kwargs: dict[str, Any] = dict(stream_params) - api_kwargs.update({"model": model.id, "input": response_input, "stream": True}) + api_kwargs.update( + {"model": model.id, "input": response_input, "stream": True} + ) if response_tools: api_kwargs["tools"] = response_tools @@ -958,21 +1003,27 @@ async def _stream_responses( if event_type == "response.created": response = data.get("response") if isinstance(response, Mapping): - response_metadata = _provider_metadata_for_response(response) + response_metadata = _provider_metadata_for_response( + response + ) continue if event_type in {"response.completed", "response.incomplete"}: response = data.get("response") if isinstance(response, Mapping): usage = _usage_from_response(response) or usage - response_metadata = _provider_metadata_for_response(response) + response_metadata = _provider_metadata_for_response( + response + ) continue if event_type == "response.failed": response = data.get("response") if isinstance(response, Mapping): usage = _usage_from_response(response) or usage - response_metadata = _provider_metadata_for_response(response) + response_metadata = _provider_metadata_for_response( + response + ) continue if event_type == "error": @@ -981,7 +1032,9 @@ async def _stream_responses( message = error.get("message") or error.get("code") or error else: message = error or data - raise ai_errors.ProviderResponseError(str(message), provider=provider) + raise ai_errors.ProviderResponseError( + str(message), provider=provider + ) if event_type == "response.output_item.added": item = data.get("item") @@ -1008,7 +1061,9 @@ async def _stream_responses( block_id=block_id, provider_metadata=_provider_metadata_for_item( item, - reasoning_encrypted_content=item.get("encrypted_content"), + reasoning_encrypted_content=item.get( + "encrypted_content" + ), ), ) continue @@ -1084,7 +1139,11 @@ async def _stream_responses( data, ) delta = data.get("delta") - if function_state is not None and isinstance(delta, str) and delta: + if ( + function_state is not None + and isinstance(delta, str) + and delta + ): function_state["arguments"] += delta function_state["delta_emitted"] = True yield types.events.ToolDelta( @@ -1118,7 +1177,9 @@ async def _stream_responses( continue if event_type == "response.reasoning_summary_part.added": - block_id = f"{data.get('item_id')}:{data.get('summary_index', 0)}" + block_id = ( + f"{data.get('item_id')}:{data.get('summary_index', 0)}" + ) if block_id not in reasoning_blocks: reasoning_blocks.add(block_id) yield types.events.ReasoningStart(block_id=block_id) @@ -1128,18 +1189,24 @@ async def _stream_responses( "response.reasoning_summary_text.delta", "response.reasoning_text.delta", }: - block_id = f"{data.get('item_id')}:{data.get('summary_index', 0)}" + block_id = ( + f"{data.get('item_id')}:{data.get('summary_index', 0)}" + ) if block_id not in reasoning_blocks: reasoning_blocks.add(block_id) yield types.events.ReasoningStart(block_id=block_id) delta = data.get("delta") if isinstance(delta, str) and delta: reasoning_delta_blocks.add(block_id) - yield types.events.ReasoningDelta(block_id=block_id, chunk=delta) + yield types.events.ReasoningDelta( + block_id=block_id, chunk=delta + ) continue if event_type == "response.reasoning_summary_part.done": - block_id = f"{data.get('item_id')}:{data.get('summary_index', 0)}" + block_id = ( + f"{data.get('item_id')}:{data.get('summary_index', 0)}" + ) reasoning_blocks.discard(block_id) reasoning_ended_blocks.add(block_id) yield types.events.ReasoningEnd(block_id=block_id) @@ -1163,7 +1230,11 @@ async def _stream_responses( data, ) delta = data.get("delta") - if builtin_state is not None and isinstance(delta, str) and delta: + if ( + builtin_state is not None + and isinstance(delta, str) + and delta + ): builtin_state["arguments"] += delta continue @@ -1205,10 +1276,14 @@ async def _stream_responses( ), ), ) - if block_id not in reasoning_delta_blocks and isinstance( - summary, Mapping + if ( + block_id not in reasoning_delta_blocks + and isinstance(summary, Mapping) ): - text = summary.get("text") + summary_mapping = cast( + "Mapping[str, Any]", summary + ) + text = summary_mapping.get("text") if isinstance(text, str) and text: yield types.events.ReasoningDelta( block_id=block_id, @@ -1306,7 +1381,8 @@ async def _stream_responses( tool_call_id = builtin_state["tool_call_id"] tool_name = builtin_state["tool_name"] if arguments and ( - builtin_state is None or not builtin_state["delta_emitted"] + builtin_state is None + or not builtin_state["delta_emitted"] ): yield types.events.BuiltinToolDelta( tool_call_id=tool_call_id, @@ -1330,7 +1406,9 @@ async def _stream_responses( block_id=str(item.get("id") or tool_call_id), media_type=image_media_type, data=result, - provider_metadata=_provider_metadata_for_item(item), + provider_metadata=_provider_metadata_for_item( + item + ), ) result_payload = _builtin_tool_result(item) @@ -1341,7 +1419,9 @@ async def _stream_responses( tool_call_id=tool_call_id, tool_name=tool_name, result=result_payload, - provider_metadata=_provider_metadata_for_item(item), + provider_metadata=_provider_metadata_for_item( + item + ), ), ) continue @@ -1356,7 +1436,9 @@ async def _stream_responses( provider_metadata=response_metadata, ) except openai_sdk.OpenAIError as exc: - raise errors.map_error(exc, provider=provider, model_id=model.id) from exc + raise errors.map_error( + exc, provider=provider, model_id=model.id + ) from exc class OpenAIResponsesProtocol(base.ProviderProtocol[Any]): diff --git a/src/ai/providers/openai/provider.py b/src/ai/providers/openai/provider.py index 58f18e10..51fb3c95 100644 --- a/src/ai/providers/openai/provider.py +++ b/src/ai/providers/openai/provider.py @@ -2,8 +2,6 @@ from __future__ import annotations -from collections.abc import AsyncGenerator, Iterable, Mapping, Sequence -from types import ModuleType from typing import TYPE_CHECKING, Any, ClassVar import httpx @@ -15,6 +13,9 @@ from . import tools as tools_module if TYPE_CHECKING: + from collections.abc import AsyncGenerator, Iterable, Mapping, Sequence + from types import ModuleType + import modelsdotdev import openai import pydantic @@ -62,7 +63,9 @@ def __init__( if client is not None and not isinstance(client, httpx.AsyncClient): openai_sdk = _sdk.import_sdk(provider=name) - if openai_sdk is not None and isinstance(client, openai_sdk.AsyncOpenAI): + if openai_sdk is not None and isinstance( + client, openai_sdk.AsyncOpenAI + ): sdk_client = client http_client = None self._has_user_sdk_client = True @@ -72,7 +75,8 @@ def __init__( self._has_user_sdk_client = False else: raise TypeError( - "OpenAI providers require an httpx.AsyncClient or openai.AsyncOpenAI" + "OpenAI providers require an httpx.AsyncClient or " + "openai.AsyncOpenAI" ) super().__init__( @@ -86,7 +90,9 @@ def __init__( headers=headers, env=env, ) - self._close_client_on_aclose = sdk_client is None and http_client is None + self._close_client_on_aclose = ( + sdk_client is None and http_client is None + ) if sdk_client is None: sdk_client = self._make_sdk_client(http_client=http_client) self._set_client(sdk_client) @@ -161,8 +167,12 @@ def from_modelsdev_provider( if resolved_base_url is None and provider.id == "openai": resolved_base_url = _BASE_URL if resolved_base_url is None: - raise ValueError(f"provider {provider.id!r} does not declare an API URL") - api_key_env, config_envs = base.provider_config(provider, model_provider_config) + raise ValueError( + f"provider {provider.id!r} does not declare an API URL" + ) + api_key_env, config_envs = base.provider_config( + provider, model_provider_config + ) return cls( name=provider.id, default_base_url=resolved_base_url, diff --git a/src/ai/providers/openai/tools.py b/src/ai/providers/openai/tools.py index 4b19f7f4..a5ac708f 100644 --- a/src/ai/providers/openai/tools.py +++ b/src/ai/providers/openai/tools.py @@ -251,7 +251,9 @@ def image_generation( def local_shell() -> types.tools.Tool: - return types.tools.Tool(kind="provider", name="local_shell", args=LocalShellArgs()) + return types.tools.Tool( + kind="provider", name="local_shell", args=LocalShellArgs() + ) def shell(*, environment: str | None = None) -> types.tools.Tool: diff --git a/src/ai/types/builders.py b/src/ai/types/builders.py index 174ca10d..eeb83c10 100644 --- a/src/ai/types/builders.py +++ b/src/ai/types/builders.py @@ -101,7 +101,9 @@ def thinking( return ReasoningPart(text=text, provider_metadata=provider_metadata) -def _tool_results_from_messages(messages: list[Message]) -> list[ToolResultPart]: +def _tool_results_from_messages( + messages: list[Message], +) -> list[ToolResultPart]: parts: list[ToolResultPart] = [] for message in messages: if message.role != "tool": @@ -153,7 +155,9 @@ def tool_message( ) if not items: - raise TypeError("tool_message() requires at least one tool message or result") + raise TypeError( + "tool_message() requires at least one tool message or result" + ) flattened_messages: list[Message] = [] result_parts: list[ToolResultPart] = [] diff --git a/src/ai/types/events.py b/src/ai/types/events.py index 70e10fc1..14aa220c 100644 --- a/src/ai/types/events.py +++ b/src/ai/types/events.py @@ -138,7 +138,7 @@ class BuiltinToolResult(BaseEvent): class FileEvent(BaseEvent): - """A complete generated file from the LLM (e.g. inline image from Gemini/GPT).""" + """A complete generated file from the LLM.""" block_id: str = "" media_type: str @@ -186,7 +186,7 @@ class HookResolution(BaseEvent): async def replay_message_events( msg: messages.Message, ) -> AsyncGenerator[Event]: - """Synthesize the events ``ai.models.stream`` would have emitted for ``msg``. + """Synthesize stream events for ``msg``. Use when you have a complete ``Message`` from a non-streaming source — e.g., the result of a Temporal activity, a cached LLM response, or an @@ -302,8 +302,8 @@ class PartialToolCallResult(pydantic.BaseModel): def key(self) -> object: return (self.tool_call_id, self.label) - aggregator_factory: Callable[[], Aggregator[Any, Any, Any]] | None = pydantic.Field( - default=None, exclude=True, repr=False + aggregator_factory: Callable[[], Aggregator[Any, Any, Any]] | None = ( + pydantic.Field(default=None, exclude=True, repr=False) ) kind: Literal["partial_tool_call_result"] = "partial_tool_call_result" diff --git a/src/ai/types/integrity.py b/src/ai/types/integrity.py index 914a4b0b..a5328cc4 100644 --- a/src/ai/types/integrity.py +++ b/src/ai/types/integrity.py @@ -43,8 +43,7 @@ def __init__(self, issues: list[IssueKind]) -> None: def _clean_messages( messages: list[messages_.Message], mode: Mode ) -> tuple[list[messages_.Message], list[IssueKind]]: - """Strip internal messages, fix broken tool args""" - + """Strip internal messages, fix broken tool args.""" issues: list[IssueKind] = [] result: list[messages_.Message] = [] @@ -100,7 +99,6 @@ def _clean_messages( def _validate_tool_ids(messages: list[messages_.Message]) -> list[IssueKind]: """Check for fatal issues: duplicate tool ids, orphaned tool results.""" - issues: list[IssueKind] = [] seen_call_ids: set[str] = set() seen_result_ids: set[str] = set() diff --git a/src/ai/types/media.py b/src/ai/types/media.py index 0d343183..e5bbf11b 100644 --- a/src/ai/types/media.py +++ b/src/ai/types/media.py @@ -36,7 +36,6 @@ def split_data_url(url: str) -> tuple[str | None, str | None]: return None, None try: header, b64_content = url.split(",", 1) - # header = "data:image/png;base64" mt = header.split(";")[0].split(":", 1)[1] return (mt or None), (b64_content or None) except (ValueError, IndexError): @@ -95,7 +94,8 @@ def infer_media_type(url: str) -> str: if guessed: return guessed raise ValueError( - f"Cannot infer media_type from URL: {url!r}. Provide media_type explicitly." + f"Cannot infer media_type from URL: {url!r}. " + "Provide media_type explicitly." ) @@ -115,18 +115,57 @@ def infer_media_type(url: str) -> str: ("image/jpeg", (0xFF, 0xD8)), ( "image/webp", - (0x52, 0x49, 0x46, 0x46, None, None, None, None, 0x57, 0x45, 0x42, 0x50), + ( + 0x52, + 0x49, + 0x46, + 0x46, + None, + None, + None, + None, + 0x57, + 0x45, + 0x42, + 0x50, + ), ), ("image/bmp", (0x42, 0x4D)), ("image/tiff", (0x49, 0x49, 0x2A, 0x00)), # little-endian ("image/tiff", (0x4D, 0x4D, 0x00, 0x2A)), # big-endian ( "image/avif", - (0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70, 0x61, 0x76, 0x69, 0x66), + ( + 0x00, + 0x00, + 0x00, + 0x20, + 0x66, + 0x74, + 0x79, + 0x70, + 0x61, + 0x76, + 0x69, + 0x66, + ), ), ( "image/heic", - (0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70, 0x68, 0x65, 0x69, 0x63), + ( + 0x00, + 0x00, + 0x00, + 0x20, + 0x66, + 0x74, + 0x79, + 0x70, + 0x68, + 0x65, + 0x69, + 0x63, + ), ), ] @@ -139,7 +178,20 @@ def infer_media_type(url: str) -> str: ("audio/mpeg", (0xFF, 0xE2)), ( "audio/wav", - (0x52, 0x49, 0x46, 0x46, None, None, None, None, 0x57, 0x41, 0x56, 0x45), + ( + 0x52, + 0x49, + 0x46, + 0x46, + None, + None, + None, + None, + 0x57, + 0x41, + 0x56, + 0x45, + ), ), ("audio/ogg", (0x4F, 0x67, 0x67, 0x53)), ("audio/flac", (0x66, 0x4C, 0x61, 0x43)), @@ -245,6 +297,7 @@ def detect_media_type( Returns: The matched IANA media type, or ``None`` if no signature matches. + """ # Strip ID3 tags for audio detection if signatures is AUDIO_SIGNATURES: @@ -261,7 +314,8 @@ def detect_media_type( if len(raw) < len(prefix): continue if all( - expected is None or raw[i] == expected for i, expected in enumerate(prefix) + expected is None or raw[i] == expected + for i, expected in enumerate(prefix) ): return media_type diff --git a/src/ai/types/messages.py b/src/ai/types/messages.py index bbb12faa..bae4c3c0 100644 --- a/src/ai/types/messages.py +++ b/src/ai/types/messages.py @@ -43,7 +43,9 @@ class ToolResultPart(pydantic.BaseModel): # registry) rather than carried across serialization. ``default_factory`` # preserves singleton identity so the unset sentinel survives pydantic's # default-copying. - _model_input: Any = pydantic.PrivateAttr(default_factory=lambda: _MODEL_INPUT_UNSET) + _model_input: Any = pydantic.PrivateAttr( + default_factory=lambda: _MODEL_INPUT_UNSET + ) kind: Literal["tool_result"] = "tool_result" model_config = pydantic.ConfigDict(frozen=True) @@ -148,7 +150,8 @@ class FilePart(pydantic.BaseModel): ``data`` accepts: - * **str** -- a URL (``http(s)://...`` or ``data:...``) *or* raw base-64 text. + * **str** -- a URL (``http(s)://...`` or ``data:...``) *or* raw + base-64 text. * **bytes** -- raw binary data (will be base-64 encoded when serialized to JSON for providers that need it). """ @@ -195,7 +198,8 @@ def from_bytes( ) or media.detect_audio_media_type(data) if media_type is None: raise ValueError( - "Cannot detect media_type from bytes. Provide media_type explicitly." + "Cannot detect media_type from bytes. " + "Provide media_type explicitly." ) return cls(data=data, media_type=media_type, filename=filename) @@ -237,7 +241,9 @@ def text(self) -> str: @property def reasoning(self) -> str: """Concatenated reasoning parts.""" - return "".join(p.text for p in self.parts if isinstance(p, ReasoningPart)) + return "".join( + p.text for p in self.parts if isinstance(p, ReasoningPart) + ) @property def tool_calls(self) -> list[ToolCallPart]: @@ -271,7 +277,9 @@ def videos(self) -> list[FilePart]: def get_output(self, output_type: None = None) -> str: ... @overload def get_output[T: pydantic.BaseModel](self, output_type: type[T]) -> T: ... - def get_output(self, output_type: type[pydantic.BaseModel] | None = None) -> Any: + def get_output( + self, output_type: type[pydantic.BaseModel] | None = None + ) -> Any: """Return the final output of this assistant turn. With no ``output_type``, returns the concatenated text content. @@ -285,7 +293,8 @@ def get_output(self, output_type: type[pydantic.BaseModel] | None = None) -> Any raise ValueError( "get_output() requires a final assistant message " "(role='assistant' with no tool calls); " - f"got role={self.role!r} with {len(self.tool_calls)} tool call(s)" + f"got role={self.role!r} with " + f"{len(self.tool_calls)} tool call(s)" ) if output_type is None: return self.text diff --git a/src/ai/types/tools.py b/src/ai/types/tools.py index b8193bbf..3016909d 100644 --- a/src/ai/types/tools.py +++ b/src/ai/types/tools.py @@ -34,6 +34,8 @@ def validate_args_shape(self) -> Self: case "provider": if isinstance(self.args, FunctionToolArgs): - raise ValueError("provider tools cannot use FunctionToolArgs") + raise ValueError( + "provider tools cannot use FunctionToolArgs" + ) return self diff --git a/src/ai/util.py b/src/ai/util.py index fd3fc661..ddaa3583 100644 --- a/src/ai/util.py +++ b/src/ai/util.py @@ -1,12 +1,14 @@ -"""Utility functions""" +"""Utility functions.""" from __future__ import annotations import asyncio import contextlib import dataclasses -from collections.abc import AsyncIterable, AsyncIterator -from typing import Any +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from collections.abc import AsyncIterable, AsyncIterator _EMPTY: Any = object() @@ -48,7 +50,7 @@ async def astop(self) -> None: @contextlib.asynccontextmanager async def unwrap_generator_exit() -> AsyncIterator[None]: - """Convert a ``BaseExceptionGroup`` of only ``GeneratorExit``s into a single one. + """Unwrap ``BaseExceptionGroup`` containing only ``GeneratorExit``. ``asyncio.TaskGroup``'s ``__aexit__`` wraps any body exception (including ``GeneratorExit``) into a ``BaseExceptionGroup``. Inside an async @@ -68,7 +70,9 @@ async def unwrap_generator_exit() -> AsyncIterator[None]: @contextlib.asynccontextmanager -async def maybe_aclosing[T: AsyncIterable[Any]](iter: T) -> AsyncIterator[T]: +async def maybe_aclosing( + iter: AsyncIterable[Any], +) -> AsyncIterator[AsyncIterable[Any]]: """Like ``contextlib.aclosing`` but a no-op if ``iter`` has no ``aclose``. Useful when consuming an arbitrary ``AsyncIterable[T]`` whose concrete @@ -139,7 +143,7 @@ async def worker() -> None: async def merge[T]( *aiterables: AsyncIterable[T], restart: bool = True ) -> AsyncIterator[T]: - """Produce a stream that yields elements from async iterables as they arrive. + """Yield elements from async iterables as they arrive. Additionally, if `restart` is True (the default), attempt to *restart* finished iterables when other iterables produce elements. @@ -152,7 +156,6 @@ async def merge[T]( iterators (importantly, this means that async generators are not restarted). """ - # We use unwrap_generator_exit() to keep a GeneratorExit that gets # packaged in an ExceptionGroup from causing grief. But maybe we # ought to not use a TaskGroup? @@ -197,7 +200,9 @@ async def merge[T]( # they've had a chance to trigger things, and we do it # after *all* tasks have been handled, so that if a # task *just* finished, we still restart it. - for idx, (ok, otask) in enumerate(zip(restartable, tasks, strict=True)): + for idx, (ok, otask) in enumerate( + zip(restartable, tasks, strict=True) + ): if ok and otask is None and idx not in fired: niter = decouple(aiterables[idx], task_group=tg) aiters[idx] = niter diff --git a/tests/agents/mcp/test_client.py b/tests/agents/mcp/test_client.py index 238d5700..4af10dc4 100644 --- a/tests/agents/mcp/test_client.py +++ b/tests/agents/mcp/test_client.py @@ -11,7 +11,13 @@ import ai from ai.agents.mcp.client import _mcp_tool_to_native -from ...conftest import MOCK_MODEL, collect_messages, mock_llm, text_msg, tool_call_msg +from ...conftest import ( + MOCK_MODEL, + collect_messages, + mock_llm, + text_msg, + tool_call_msg, +) def _fake_mcp_tool( @@ -40,7 +46,9 @@ def _noop_transport_factory() -> contextlib.AbstractAsyncContextManager[Any]: def test_mcp_tool_to_native_basic() -> None: """Converting an MCP tool to native produces a Tool with correct schema.""" mcp_tool = _fake_mcp_tool(name="mcp_basic_test") - native = _mcp_tool_to_native(mcp_tool, "test:key", _noop_transport_factory, None) + native = _mcp_tool_to_native( + mcp_tool, "test:key", _noop_transport_factory, None + ) assert native.name == "mcp_basic_test" assert _function_args(native).description == "Echo input" @@ -49,7 +57,9 @@ def test_mcp_tool_to_native_basic() -> None: def test_mcp_tool_to_native_with_prefix() -> None: """Tool prefix is prepended to the name.""" mcp_tool = _fake_mcp_tool(name="echo") - native = _mcp_tool_to_native(mcp_tool, "test:key", _noop_transport_factory, "ctx7") + native = _mcp_tool_to_native( + mcp_tool, "test:key", _noop_transport_factory, "ctx7" + ) assert native.name == "ctx7_echo" @@ -57,7 +67,9 @@ def test_mcp_tool_to_native_with_prefix() -> None: def test_mcp_tool_to_native_schema_preserved() -> None: """The inputSchema from the MCP tool is passed through as params.""" mcp_tool = _fake_mcp_tool() - native = _mcp_tool_to_native(mcp_tool, "test:key", _noop_transport_factory, None) + native = _mcp_tool_to_native( + mcp_tool, "test:key", _noop_transport_factory, None + ) assert _function_args(native).params == mcp_tool.inputSchema assert _function_args(native).description == "Echo input" @@ -77,19 +89,25 @@ async def fake_fn(**kwargs: str) -> str: # Build a tool the same way the MCP client does, # but with a fake fn so we don't need a real MCP server. mcp_tool = _fake_mcp_tool(name="mcp_e2e_echo") - native = _mcp_tool_to_native(mcp_tool, "test:key", _noop_transport_factory, None) + native = _mcp_tool_to_native( + mcp_tool, "test:key", _noop_transport_factory, None + ) # Replace the executor (which would try to connect) with our fake. native = dataclasses.replace(native, fn=fake_fn) my_agent = ai.agent(tools=[native]) call1 = [ - tool_call_msg(tc_id="tc-mcp-1", name="mcp_e2e_echo", args='{"text": "hello"}') + tool_call_msg( + tc_id="tc-mcp-1", name="mcp_e2e_echo", args='{"text": "hello"}' + ) ] call2 = [text_msg("Done.", id="msg-2")] llm = mock_llm([call1, call2]) - async with my_agent.run(MOCK_MODEL, [ai.user_message("echo hello")]) as stream: + async with my_agent.run( + MOCK_MODEL, [ai.user_message("echo hello")] + ) as stream: msgs = await collect_messages(stream) # Tool was called with the right args. diff --git a/tests/agents/test_aggregate_marker.py b/tests/agents/test_aggregate_marker.py index 83fa3865..e0fe90a2 100644 --- a/tests/agents/test_aggregate_marker.py +++ b/tests/agents/test_aggregate_marker.py @@ -28,9 +28,11 @@ def test_aggregate_marker_extracted_from_direct_annotated() -> None: """Bare ``Annotated[..., Aggregate(...)]`` on the return type.""" @ai.tool - async def t() -> Annotated[ - AsyncGenerator[str], ai.agents.Aggregate(ai.agents.LastAggregator) - ]: + async def t() -> ( + Annotated[ + AsyncGenerator[str], ai.agents.Aggregate(ai.agents.LastAggregator) + ] + ): yield "x" assert isinstance(_factory(t), ai.agents.LastAggregator) @@ -75,9 +77,12 @@ def test_aggregate_kwarg_passed_to_factory() -> None: """Extra kwargs on Aggregate flow through to the factory.""" @ai.tool - async def t() -> Annotated[ - AsyncGenerator[str], ai.agents.Aggregate(ai.agents.ConcatAggregator, delim="|") - ]: + async def t() -> ( + Annotated[ + AsyncGenerator[str], + ai.agents.Aggregate(ai.agents.ConcatAggregator, delim="|"), + ] + ): yield "a" yield "b" @@ -102,11 +107,13 @@ def test_multiple_aggregate_markers_raise() -> None: with pytest.raises(TypeError, match="multiple Aggregate markers"): @ai.tool - async def t() -> Annotated[ - AsyncGenerator[str], - ai.agents.Aggregate(ai.agents.LastAggregator), - ai.agents.Aggregate(ai.agents.ConcatAggregator), - ]: + async def t() -> ( + Annotated[ + AsyncGenerator[str], + ai.agents.Aggregate(ai.agents.LastAggregator), + ai.agents.Aggregate(ai.agents.ConcatAggregator), + ] + ): yield "x" @@ -118,7 +125,7 @@ async def alias_progress_tool(query: str) -> ai.StreamingStatusTool[str]: async def test_alias_declared_tool_runs_end_to_end() -> None: - """An alias-declared streaming tool behaves identically to the kwarg form.""" + """An alias-declared streaming tool behaves like the kwarg form.""" my_agent = ai.agent(tools=[alias_progress_tool]) call = [ @@ -137,7 +144,9 @@ async def test_alias_declared_tool_runs_end_to_end() -> None: assert llm.call_count == 2 progress = [ - e for e in all_events if isinstance(e, agent_events_.PartialToolCallResult) + e + for e in all_events + if isinstance(e, agent_events_.PartialToolCallResult) ] assert [p.value for p in progress] == ["Working...", "Answer for test"] diff --git a/tests/agents/test_generator_tools.py b/tests/agents/test_generator_tools.py index 0b73cbfa..0f2bc9d6 100644 --- a/tests/agents/test_generator_tools.py +++ b/tests/agents/test_generator_tools.py @@ -42,7 +42,11 @@ async def test_generator_tool_streams_and_returns_result() -> None: # Turn 1: LLM calls progress_tool # Turn 2: LLM produces final text after seeing the tool result - call = [tool_call_msg(tc_id="tc-1", name="progress_tool", args='{"query": "test"}')] + call = [ + tool_call_msg( + tc_id="tc-1", name="progress_tool", args='{"query": "test"}' + ) + ] reply = [text_msg("Done!", id="msg-2")] llm = mock_llm([call, reply]) @@ -56,7 +60,9 @@ async def test_generator_tool_streams_and_returns_result() -> None: # Intermediate progress events were forwarded to consumer, wrapped # in PartialToolCallResult and attributed to the originating tool call. progress_wrappers = [ - e for e in all_events if isinstance(e, agent_events_.PartialToolCallResult) + e + for e in all_events + if isinstance(e, agent_events_.PartialToolCallResult) ] assert len(progress_wrappers) == 2 assert progress_wrappers[0].value == "Working..." @@ -138,7 +144,9 @@ async def test_yield_from_nested_agent() -> None: # Outer agent turn 1: calls research_tool outer_call = [ - tool_call_msg(tc_id="otc-1", name="research_tool", args='{"topic": "mars"}') + tool_call_msg( + tc_id="otc-1", name="research_tool", args='{"topic": "mars"}' + ) ] # Outer agent turn 2: final answer (after seeing tool result) outer_reply = [text_msg("Summary: Mars has two moons.", id="outer-msg-2")] @@ -147,7 +155,9 @@ async def test_yield_from_nested_agent() -> None: MOCK_PROVIDER._stream_impl = adapter.stream all_events: list[agent_events_.AgentEvent] = [] - async with outer.run(MOCK_MODEL, [ai.user_message("Tell me about Mars")]) as stream: + async with outer.run( + MOCK_MODEL, [ai.user_message("Tell me about Mars")] + ) as stream: async for event in stream: all_events.append(event) diff --git a/tests/agents/test_hooks.py b/tests/agents/test_hooks.py index fa6375a6..3ea7b138 100644 --- a/tests/agents/test_hooks.py +++ b/tests/agents/test_hooks.py @@ -28,7 +28,9 @@ async def test_resolve_live_future() -> None: resolved_value: Confirmation | None = None class MyAgent(ai.Agent): - async def loop(self, context: ai.Context) -> AsyncGenerator[ai.events.Event]: + async def loop( + self, context: ai.Context + ) -> AsyncGenerator[ai.events.Event]: nonlocal resolved_value async with ai.models.stream(context=context) as stream: async for event in stream: @@ -46,7 +48,9 @@ async def loop(self, context: ai.Context) -> AsyncGenerator[ai.events.Event]: continue # When we see the pending hook, resolve it. if event.hook.status == "pending": - ai.resolve_hook("confirm_1", {"approved": True, "reason": "looks good"}) + ai.resolve_hook( + "confirm_1", {"approved": True, "reason": "looks good"} + ) assert resolved_value is not None assert resolved_value.approved is True @@ -61,7 +65,9 @@ async def test_cancel_live_hook() -> None: was_cancelled = False class MyAgent(ai.Agent): - async def loop(self, context: ai.Context) -> AsyncGenerator[ai.events.Event]: + async def loop( + self, context: ai.Context + ) -> AsyncGenerator[ai.events.Event]: nonlocal was_cancelled async with ai.models.stream(context=context) as stream: async for event in stream: @@ -101,7 +107,9 @@ async def test_pre_registered_resolution_consumed() -> None: resolved_value: Confirmation | None = None class MyAgent(ai.Agent): - async def loop(self, context: ai.Context) -> AsyncGenerator[ai.events.Event]: + async def loop( + self, context: ai.Context + ) -> AsyncGenerator[ai.events.Event]: nonlocal resolved_value async with ai.models.stream(context=context) as stream: async for event in stream: @@ -143,7 +151,9 @@ async def test_resolved_hook_emits_message() -> None: """After resolution, a 'resolved' HookPart message is emitted.""" class MyAgent(ai.Agent): - async def loop(self, context: ai.Context) -> AsyncGenerator[ai.events.Event]: + async def loop( + self, context: ai.Context + ) -> AsyncGenerator[ai.events.Event]: async with ai.models.stream(context=context) as stream: async for event in stream: yield event @@ -172,7 +182,9 @@ async def loop(self, context: ai.Context) -> AsyncGenerator[ai.events.Event]: async def test_hook_metadata_in_pending() -> None: class MyAgent(ai.Agent): - async def loop(self, context: ai.Context) -> AsyncGenerator[ai.events.Event]: + async def loop( + self, context: ai.Context + ) -> AsyncGenerator[ai.events.Event]: async with ai.models.stream(context=context) as stream: async for event in stream: yield event diff --git a/tests/agents/test_runtime.py b/tests/agents/test_runtime.py index 17ebef75..9a4096b9 100644 --- a/tests/agents/test_runtime.py +++ b/tests/agents/test_runtime.py @@ -5,7 +5,13 @@ import ai from ai.types import messages -from ..conftest import MOCK_MODEL, collect_messages, mock_llm, text_msg, tool_call_msg +from ..conftest import ( + MOCK_MODEL, + collect_messages, + mock_llm, + text_msg, + tool_call_msg, +) # -- Tool definitions for tests -------------------------------------------- @@ -47,7 +53,9 @@ async def test_agent_tool_then_text() -> None: call2 = [text_msg("The answer is 10.")] llm = mock_llm([call1, call2]) - async with my_agent.run(MOCK_MODEL, [ai.user_message("Double 5")]) as stream: + async with my_agent.run( + MOCK_MODEL, [ai.user_message("Double 5")] + ) as stream: msgs = await collect_messages(stream) assert llm.call_count == 2 tool_results = [m for m in msgs if m.role == "tool" and m.tool_results] @@ -81,7 +89,9 @@ async def test_agent_parallel_tools() -> None: call2 = [text_msg("6 and 14", id="msg-2")] llm = mock_llm([[two_tools], call2]) - async with my_agent.run(MOCK_MODEL, [ai.user_message("Double 3 and 7")]) as stream: + async with my_agent.run( + MOCK_MODEL, [ai.user_message("Double 3 and 7")] + ) as stream: msgs = await collect_messages(stream) assert llm.call_count == 2 tool_result_msgs = [m for m in msgs if m.role == "tool" and m.tool_results] @@ -96,9 +106,13 @@ async def test_agent_multi_turn() -> None: my_agent = ai.agent(tools=[double, concat]) turn1 = [ - tool_call_msg(tc_id="tc-1", name="concat", args='{"a": "hello", "b": " world"}') + tool_call_msg( + tc_id="tc-1", name="concat", args='{"a": "hello", "b": " world"}' + ) + ] + turn2 = [ + tool_call_msg(tc_id="tc-2", name="double", args='{"x": 3}', id="msg-2") ] - turn2 = [tool_call_msg(tc_id="tc-2", name="double", args='{"x": 3}', id="msg-2")] turn3 = [text_msg("Done: hello world, 6", id="msg-3")] llm = mock_llm([turn1, turn2, turn3]) diff --git a/tests/agents/test_tools.py b/tests/agents/test_tools.py index 7a5d1efb..eee0b332 100644 --- a/tests/agents/test_tools.py +++ b/tests/agents/test_tools.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio -from typing import Any +from typing import Any, cast import pydantic import pytest @@ -95,7 +95,7 @@ async def double(x: int) -> int: tc = ai.agents.BoundToolCall(part=part, tool=double) result = await tc() - assert tc.fn.__name__ == "double" + assert cast(Any, tc.fn).__name__ == "double" assert tc.kwargs == {"x": 5} assert result.message.role == "tool" assert len(result.results) == 1 @@ -256,14 +256,17 @@ async def store(items: list[_NestedItem]) -> str: part = ai.messages.ToolCallPart( tool_call_id="tc-nested", tool_name="store", - tool_args='{"items": [{"key": "a", "value": "1"}, {"key": "b", "value": "2"}]}', + tool_args=( + '{"items": [{"key": "a", "value": "1"}, ' + '{"key": "b", "value": "2"}]}' + ), ) result = await ai.agents.BoundToolCall(part=part, tool=store)() assert not result.results[0].is_error assert len(received) == 2 - assert all(isinstance(item, _NestedItem) for item in received), ( - f"expected _NestedItem instances, got {[type(i) for i in received]}" - ) + assert all( + isinstance(item, _NestedItem) for item in received + ), f"expected _NestedItem instances, got {[type(i) for i in received]}" assert received[0].key == "a" assert received[1].value == "2" diff --git a/tests/agents/test_validation_error_approval.py b/tests/agents/test_validation_error_approval.py index 178b5727..5f89dcc9 100644 --- a/tests/agents/test_validation_error_approval.py +++ b/tests/agents/test_validation_error_approval.py @@ -19,8 +19,8 @@ class TextEdit(pydantic.BaseModel): - oldText: str - newText: str + old_text: str = pydantic.Field(alias="oldText") + new_text: str = pydantic.Field(alias="newText") @ai.tool(require_approval=True) @@ -55,9 +55,14 @@ async def test_invalid_args_with_approval_returns_error_result() -> None: hook_events: list[events_.HookEvent] = [] - async with my_agent.run(MOCK_MODEL, [ai.user_message("edit something")]) as stream: + async with my_agent.run( + MOCK_MODEL, [ai.user_message("edit something")] + ) as stream: async for event in stream: - if isinstance(event, events_.HookEvent) and event.hook.status == "pending": + if ( + isinstance(event, events_.HookEvent) + and event.hook.status == "pending" + ): hook_events.append(event) ai.resolve_hook( event.hook.hook_id, @@ -102,9 +107,14 @@ async def test_invalid_args_skips_approval_hook() -> None: hook_fired = False - async with my_agent.run(MOCK_MODEL, [ai.user_message("edit something")]) as stream: + async with my_agent.run( + MOCK_MODEL, [ai.user_message("edit something")] + ) as stream: async for event in stream: - if isinstance(event, events_.HookEvent) and event.hook.status == "pending": + if ( + isinstance(event, events_.HookEvent) + and event.hook.status == "pending" + ): hook_fired = True ai.resolve_hook( event.hook.hook_id, @@ -138,9 +148,14 @@ async def test_completely_invalid_json_with_approval() -> None: final = text_msg("Let me try again.", id="msg-2") llm = mock_llm([[bad_call], [final]]) - async with my_agent.run(MOCK_MODEL, [ai.user_message("edit something")]) as stream: + async with my_agent.run( + MOCK_MODEL, [ai.user_message("edit something")] + ) as stream: async for event in stream: - if isinstance(event, events_.HookEvent) and event.hook.status == "pending": + if ( + isinstance(event, events_.HookEvent) + and event.hook.status == "pending" + ): ai.resolve_hook( event.hook.hook_id, ai.tools.ToolApproval(granted=True, reason="auto"), diff --git a/tests/agents/ui/ai_sdk/outbound/test_history.py b/tests/agents/ui/ai_sdk/outbound/test_history.py index 7ebdf3e8..7665bc13 100644 --- a/tests/agents/ui/ai_sdk/outbound/test_history.py +++ b/tests/agents/ui/ai_sdk/outbound/test_history.py @@ -10,7 +10,9 @@ def test_to_ui_messages_user_and_assistant() -> None: msgs = [ - messages_.Message(id="u1", role="user", parts=[messages_.TextPart(text="hi")]), + messages_.Message( + id="u1", role="user", parts=[messages_.TextPart(text="hi")] + ), messages_.Message( id="a1", role="assistant", @@ -102,7 +104,9 @@ def test_to_ui_messages_internal_role_merges_approval() -> None: def test_to_ui_messages_user_message_uses_own_id() -> None: msgs = [ - messages_.Message(id="u1", role="user", parts=[messages_.TextPart(text="a")]) + messages_.Message( + id="u1", role="user", parts=[messages_.TextPart(text="a")] + ) ] result = to_ui_messages(msgs) assert result[0].id == "u1" diff --git a/tests/agents/ui/ai_sdk/outbound/test_stream.py b/tests/agents/ui/ai_sdk/outbound/test_stream.py index 3603e037..1022de46 100644 --- a/tests/agents/ui/ai_sdk/outbound/test_stream.py +++ b/tests/agents/ui/ai_sdk/outbound/test_stream.py @@ -146,14 +146,16 @@ async def test_approval_request_hook_emits_approval_part() -> None: ), ] ) - approval_parts = [p for p in out if isinstance(p, protocol.ToolApprovalRequestPart)] + approval_parts = [ + p for p in out if isinstance(p, protocol.ToolApprovalRequestPart) + ] assert len(approval_parts) == 1 assert approval_parts[0].tool_call_id == "tc1" assert approval_parts[0].approval_id == "approve_tc1" async def test_partial_tool_results_emit_preliminary_outputs() -> None: - """Each PartialToolCallResult feeds the aggregator and yields a preliminary part.""" + """Each partial result yields a preliminary part.""" out = await _collect( [ agent_events_.PartialToolCallResult( @@ -191,7 +193,7 @@ async def test_partial_tool_results_emit_preliminary_outputs() -> None: async def test_partial_message_bundle_becomes_ui_message() -> None: - """MessageAggregator's MessageBundle snapshot collapses to a single UIMessage.""" + """MessageAggregator's snapshot collapses to one UIMessage.""" from ai.agents.ui.ai_sdk.ui_message import UIMessage inner_msg = messages_.Message( @@ -204,7 +206,9 @@ async def test_partial_message_bundle_becomes_ui_message() -> None: agent_events_.PartialToolCallResult( tool_call_id="tc1", tool_name="research", - value=agent_events_.ToolCallResult(message=inner_msg, results=[]), + value=agent_events_.ToolCallResult( + message=inner_msg, results=[] + ), aggregator_factory=ai.agents.MessageAggregator, ), ] diff --git a/tests/agents/ui/ai_sdk/test_inbound.py b/tests/agents/ui/ai_sdk/test_inbound.py index 27b4222d..64f8a556 100644 --- a/tests/agents/ui/ai_sdk/test_inbound.py +++ b/tests/agents/ui/ai_sdk/test_inbound.py @@ -15,7 +15,9 @@ def _ui(role: str, *parts: dict[str, Any], id: str = "m1") -> UIMessage: - return UIMessage.model_validate({"id": id, "role": role, "parts": list(parts)}) + return UIMessage.model_validate( + {"id": id, "role": role, "parts": list(parts)} + ) def _text(text: str) -> dict[str, Any]: @@ -88,7 +90,7 @@ def test_to_messages_keeps_pending_approval_tombstone() -> None: def test_to_messages_drops_resolved_approval_tombstone() -> None: - """Resolved approvals come back via the side-channel; the tombstone is dead.""" + """Resolved approvals come back via the side-channel.""" messages, approvals = to_messages( [ _ui( @@ -120,7 +122,11 @@ def test_to_messages_keeps_trailing_assistant_when_approved() -> None: "delete", "tc1", "approval-responded", - approval={"id": "approve_tc1", "approved": True, "reason": None}, + approval={ + "id": "approve_tc1", + "approved": True, + "reason": None, + }, ), id="a1", ), @@ -181,7 +187,7 @@ def test_to_messages_decodes_subagent_tool_output() -> None: populating it requires the tool registry, which lives in :meth:`Agent.run`. """ - # Wire shape: a tool-_research_tool part with output = UIMessage{parts=[text]}. + # Wire shape: tool-_research_tool with output = UIMessage{parts=[text]}. ui = [ _ui("user", _text("research mars"), id="u1"), _ui( @@ -216,7 +222,13 @@ def test_to_messages_passthrough_keeps_wire_shape() -> None: _ui("user", _text("hi"), id="u1"), _ui( "assistant", - _tool("ping", "tc1", "output-available", input={}, output={"pong": True}), + _tool( + "ping", + "tc1", + "output-available", + input={}, + output={"pong": True}, + ), id="a1", ), ] diff --git a/tests/conftest.py b/tests/conftest.py index 6f6eeb2f..7fc2ffd7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -59,9 +59,11 @@ def stream( provider=self.name, ) if self._stream_impl is None: - raise RuntimeError("MockProvider: no stream implementation configured") + raise RuntimeError( + "MockProvider: no stream implementation configured" + ) return cast( - AsyncGenerator[events_.Event], + "AsyncGenerator[events_.Event]", self._stream_impl( model, messages, @@ -88,9 +90,12 @@ async def generate( provider=self.name, ) if self._generate_impl is None: - raise RuntimeError("MockProvider: no generate implementation configured") + raise RuntimeError( + "MockProvider: no generate implementation configured" + ) return cast( - messages_.Message, await self._generate_impl(model, messages, params) + "messages_.Message", + await self._generate_impl(model, messages, params), ) @@ -145,7 +150,9 @@ async def emit_events_for_messages( tool_call_id=part.tool_call_id, chunk=part.tool_args, ) - yield events_.ToolEnd(tool_call_id=part.tool_call_id, tool_call=part) + yield events_.ToolEnd( + tool_call_id=part.tool_call_id, tool_call=part + ) elif isinstance(part, messages_.FilePart): yield events_.FileEvent( @@ -226,7 +233,9 @@ async def generate( params: Any = None, ) -> messages_.Message: if self._call_index >= len(self._responses): - raise RuntimeError("MockGenerateAdapter: no more responses configured") + raise RuntimeError( + "MockGenerateAdapter: no more responses configured" + ) self.call_count += 1 msg = self._responses[self._call_index] self._call_index += 1 diff --git a/tests/models/core/test_api.py b/tests/models/core/test_api.py index 37fd72f2..5858597d 100644 --- a/tests/models/core/test_api.py +++ b/tests/models/core/test_api.py @@ -1,7 +1,7 @@ from __future__ import annotations from collections.abc import AsyncGenerator, Sequence -from typing import Any +from typing import Any, cast import pydantic import pytest @@ -11,7 +11,13 @@ from ai.types import events as events_ from ai.types import messages as messages_ -from ...conftest import MOCK_MODEL, MOCK_PROVIDER, MockProvider, mock_llm, text_msg +from ...conftest import ( + MOCK_MODEL, + MOCK_PROVIDER, + MockProvider, + mock_llm, + text_msg, +) def _test_provider_metadata(marker: str) -> dict[str, Any]: @@ -63,7 +69,9 @@ async def _tool_stream( MOCK_PROVIDER._stream_impl = _tool_stream tool_end: events_.ToolEnd | None = None - async with models.stream(MOCK_MODEL, [ai.user_message("Check weather")]) as stream: + async with models.stream( + MOCK_MODEL, [ai.user_message("Check weather")] + ) as stream: async for event in stream: if isinstance(event, events_.ToolEnd): tool_end = event @@ -135,7 +143,9 @@ async def _metadata_stream() -> AsyncGenerator[events_.Event]: async for _ in stream: pass - assert _provider_metadata_marker(stream.message.provider_metadata) == "message" + assert ( + _provider_metadata_marker(stream.message.provider_metadata) == "message" + ) text = stream.message.parts[0] assert isinstance(text, messages_.TextPart) @@ -145,7 +155,10 @@ async def _metadata_stream() -> AsyncGenerator[events_.Event]: reasoning = stream.message.parts[1] assert isinstance(reasoning, messages_.ReasoningPart) assert reasoning.text == "thinking" - assert _provider_metadata_marker(reasoning.provider_metadata) == "reasoning-end" + assert ( + _provider_metadata_marker(reasoning.provider_metadata) + == "reasoning-end" + ) tool_call = stream.message.parts[2] assert isinstance(tool_call, messages_.ToolCallPart) @@ -210,14 +223,16 @@ async def test_stream_accepts_context() -> None: async def test_stream_rejects_context_with_positional_args() -> None: - """Passing both positional model/messages and ``context=`` is a TypeError.""" + """Passing positional args with ``context=`` is a TypeError.""" ctx = ai.Context( model=MOCK_MODEL, messages=[ai.user_message("Hi")], tools=[], ) - with pytest.raises(TypeError, match="either model/messages/tools or context="): - async with models.stream( # type: ignore[call-overload] + with pytest.raises( + TypeError, match="either model/messages/tools or context=" + ): + async with cast(Any, models.stream)( MOCK_MODEL, [ai.user_message("Hi")], context=ctx ): pass @@ -225,8 +240,10 @@ async def test_stream_rejects_context_with_positional_args() -> None: async def test_stream_requires_model_messages_or_context() -> None: """Passing nothing is a TypeError.""" - with pytest.raises(TypeError, match="either model and messages or context="): - async with models.stream(): # type: ignore[call-overload] + with pytest.raises( + TypeError, match="either model and messages or context=" + ): + async with cast(Any, models.stream)(): pass @@ -378,7 +395,10 @@ async def _spy_stream( async with models.stream( MOCK_MODEL, - [ai.user_message("Hi"), assistant_msg.model_copy(update={"replay": True})], + [ + ai.user_message("Hi"), + assistant_msg.model_copy(update={"replay": True}), + ], ) as stream: events: list[events_.Event] = [event async for event in stream] diff --git a/tests/models/test_resolution.py b/tests/models/test_resolution.py index 20ac6432..98a286f3 100644 --- a/tests/models/test_resolution.py +++ b/tests/models/test_resolution.py @@ -4,7 +4,10 @@ from ai import ConfigurationError, models from ai.providers.ai_gateway import GatewayV3Protocol from ai.providers.anthropic import AnthropicMessagesProtocol -from ai.providers.openai import OpenAIChatCompletionsProtocol, OpenAIResponsesProtocol +from ai.providers.openai import ( + OpenAIChatCompletionsProtocol, + OpenAIResponsesProtocol, +) def test_get_resolves_provider_qualified_model_id() -> None: @@ -90,7 +93,8 @@ def test_provider_from_id_detects_token_env_after_url_env() -> None: provider = ai.get_provider("databricks") assert ( - provider.default_base_url == "https://${DATABRICKS_HOST}/ai-gateway/mlflow/v1" + provider.default_base_url + == "https://${DATABRICKS_HOST}/ai-gateway/mlflow/v1" ) assert provider.api_key_env == "DATABRICKS_TOKEN" assert provider.config_envs == ("DATABRICKS_HOST",) diff --git a/tests/providers/ai_gateway/test_errors.py b/tests/providers/ai_gateway/test_errors.py index 63728eb0..2c737a29 100644 --- a/tests/providers/ai_gateway/test_errors.py +++ b/tests/providers/ai_gateway/test_errors.py @@ -19,13 +19,16 @@ class TestGatewayErrorBase: """Base class behaviour that all concrete errors inherit.""" def test_generation_id_in_message(self) -> None: - err = client_errors.GatewayInternalServerError("boom", generation_id="gen-123") + err = client_errors.GatewayInternalServerError( + "boom", generation_id="gen-123" + ) assert "[gen-123]" in str(err) assert err.generation_id == "gen-123" def test_gateway_errors_are_independent(self) -> None: assert isinstance( - client_errors.GatewayAuthenticationError(), client_errors.GatewayError + client_errors.GatewayAuthenticationError(), + client_errors.GatewayError, ) assert not isinstance( client_errors.GatewayAuthenticationError(), ai.ProviderError @@ -92,7 +95,9 @@ def test_invalid_request_error(self) -> None: "type": "invalid_request_error", } } - err = client_errors.create_gateway_error(response_body=body, status_code=400) + err = client_errors.create_gateway_error( + response_body=body, status_code=400 + ) assert isinstance(err, client_errors.GatewayInvalidRequestError) assert err.status_code == 400 @@ -103,7 +108,9 @@ def test_rate_limit_error(self) -> None: "type": "rate_limit_exceeded", } } - err = client_errors.create_gateway_error(response_body=body, status_code=429) + err = client_errors.create_gateway_error( + response_body=body, status_code=429 + ) assert isinstance(err, client_errors.GatewayRateLimitError) def test_model_not_found_extracts_model_id(self) -> None: @@ -114,7 +121,9 @@ def test_model_not_found_extracts_model_id(self) -> None: "param": {"modelId": "xyz"}, } } - err = client_errors.create_gateway_error(response_body=body, status_code=404) + err = client_errors.create_gateway_error( + response_body=body, status_code=404 + ) assert isinstance(err, client_errors.GatewayModelNotFoundError) assert err.model_id == "xyz" @@ -125,7 +134,9 @@ def test_model_not_found_without_param(self) -> None: "type": "model_not_found", } } - err = client_errors.create_gateway_error(response_body=body, status_code=404) + err = client_errors.create_gateway_error( + response_body=body, status_code=404 + ) assert isinstance(err, client_errors.GatewayModelNotFoundError) assert err.model_id is None @@ -136,7 +147,9 @@ def test_internal_server_error(self) -> None: "type": "internal_server_error", } } - err = client_errors.create_gateway_error(response_body=body, status_code=500) + err = client_errors.create_gateway_error( + response_body=body, status_code=500 + ) assert isinstance(err, client_errors.GatewayInternalServerError) def test_unknown_type_falls_back_to_internal(self) -> None: @@ -146,7 +159,9 @@ def test_unknown_type_falls_back_to_internal(self) -> None: "type": "alien_error", } } - err = client_errors.create_gateway_error(response_body=body, status_code=500) + err = client_errors.create_gateway_error( + response_body=body, status_code=500 + ) assert isinstance(err, client_errors.GatewayInternalServerError) def test_malformed_json_string(self) -> None: @@ -157,7 +172,9 @@ def test_malformed_json_string(self) -> None: def test_missing_error_field(self) -> None: body = {"ferror": {"message": "oops"}} - err = client_errors.create_gateway_error(response_body=body, status_code=404) + err = client_errors.create_gateway_error( + response_body=body, status_code=404 + ) assert isinstance(err, client_errors.GatewayResponseError) def test_generation_id_extracted(self) -> None: @@ -168,11 +185,15 @@ def test_generation_id_extracted(self) -> None: }, "generationId": "gen-abc", } - err = client_errors.create_gateway_error(response_body=body, status_code=429) + err = client_errors.create_gateway_error( + response_body=body, status_code=429 + ) assert err.generation_id == "gen-abc" def test_response_error_mapping_preserves_response_body(self) -> None: - err = client_errors.GatewayResponseError("bad", response_body={"raw": True}) + err = client_errors.GatewayResponseError( + "bad", response_body={"raw": True} + ) mapped = errors.map_error(err) assert isinstance(mapped, ai.ProviderResponseError) assert mapped.body == {"raw": True} diff --git a/tests/providers/ai_gateway/test_generate_image.py b/tests/providers/ai_gateway/test_generate_image.py index d59f446c..2f751e93 100644 --- a/tests/providers/ai_gateway/test_generate_image.py +++ b/tests/providers/ai_gateway/test_generate_image.py @@ -53,8 +53,12 @@ def handler(req: httpx.Request) -> httpx.Response: json={"images": [_PNG_B64]}, ) - model = mock_model(httpx.MockTransport(handler), model_id=_IMAGE_MODEL_ID) - msg = await ai.generate(model, [user_msg("A sunset over Tokyo")], ImageParams()) + model = mock_model( + httpx.MockTransport(handler), model_id=_IMAGE_MODEL_ID + ) + msg = await ai.generate( + model, [user_msg("A sunset over Tokyo")], ImageParams() + ) assert msg.role == "assistant" assert len(msg.images) == 1 @@ -162,7 +166,9 @@ def handler(req: httpx.Request) -> httpx.Response: assert captured_body["size"] == "1024x1024" assert captured_body["aspectRatio"] == "16:9" assert captured_body["seed"] == 42 - assert captured_body["providerOptions"] == {"google": {"style": "vivid"}} + assert captured_body["providerOptions"] == { + "google": {"style": "vivid"} + } assert "files" in captured_body assert len(captured_body["files"]) == 1 assert captured_body["files"][0]["type"] == "file" @@ -204,7 +210,9 @@ def handler(req: httpx.Request) -> httpx.Response: with pytest.raises(ai.ProviderAuthenticationError): await ai.generate( - mock_model(httpx.MockTransport(handler), model_id=_IMAGE_MODEL_ID), + mock_model( + httpx.MockTransport(handler), model_id=_IMAGE_MODEL_ID + ), [user_msg("test")], ImageParams(), ) @@ -223,7 +231,9 @@ def handler(req: httpx.Request) -> httpx.Response: with pytest.raises(ai.ProviderRateLimitError): await ai.generate( - mock_model(httpx.MockTransport(handler), model_id=_IMAGE_MODEL_ID), + mock_model( + httpx.MockTransport(handler), model_id=_IMAGE_MODEL_ID + ), [user_msg("test")], ImageParams(), ) diff --git a/tests/providers/ai_gateway/test_generate_video.py b/tests/providers/ai_gateway/test_generate_video.py index 6404f380..04d2a7a3 100644 --- a/tests/providers/ai_gateway/test_generate_video.py +++ b/tests/providers/ai_gateway/test_generate_video.py @@ -53,7 +53,11 @@ async def test_basic_video_generation_base64(self) -> None: { "type": "result", "videos": [ - {"type": "base64", "data": _MP4_B64, "mediaType": "video/mp4"} + { + "type": "base64", + "data": _MP4_B64, + "mediaType": "video/mp4", + } ], } ) @@ -90,7 +94,9 @@ async def test_video_generation_url(self) -> None: def handler(req: httpx.Request) -> httpx.Response: return httpx.Response(200, text=body) - model = mock_model(httpx.MockTransport(handler), model_id=_VIDEO_MODEL_ID) + model = mock_model( + httpx.MockTransport(handler), model_id=_VIDEO_MODEL_ID + ) with patch( "ai.models.core.helpers.files.download", @@ -113,8 +119,16 @@ async def test_multiple_videos(self) -> None: { "type": "result", "videos": [ - {"type": "base64", "data": _MP4_B64, "mediaType": "video/mp4"}, - {"type": "base64", "data": _WEBM_B64, "mediaType": "video/webm"}, + { + "type": "base64", + "data": _MP4_B64, + "mediaType": "video/mp4", + }, + { + "type": "base64", + "data": _WEBM_B64, + "mediaType": "video/webm", + }, ], } ) @@ -176,7 +190,9 @@ def handler(req: httpx.Request) -> httpx.Response: assert captured["accept"] == "text/event-stream" assert captured["ai-gateway-auth-method"] == "api-key" - async def test_request_body_forwards_parameters_and_input_image(self) -> None: + async def test_request_body_forwards_parameters_and_input_image( + self, + ) -> None: captured_body: dict[str, Any] = {} def handler(req: httpx.Request) -> httpx.Response: @@ -226,7 +242,9 @@ def handler(req: httpx.Request) -> httpx.Response: assert captured_body["duration"] == 5 assert captured_body["fps"] == 30 assert captured_body["seed"] == 42 - assert captured_body["providerOptions"] == {"google": {"enhancePrompt": True}} + assert captured_body["providerOptions"] == { + "google": {"enhancePrompt": True} + } assert "image" in captured_body assert captured_body["image"]["type"] == "file" assert captured_body["image"]["mediaType"] == "image/png" @@ -283,7 +301,9 @@ def handler(req: httpx.Request) -> httpx.Response: with pytest.raises(ai.ProviderBadRequestError, match="Content policy"): await ai.generate( - mock_model(httpx.MockTransport(handler), model_id=_VIDEO_MODEL_ID), + mock_model( + httpx.MockTransport(handler), model_id=_VIDEO_MODEL_ID + ), [user_msg("test")], params=VideoParams(), ) @@ -302,7 +322,9 @@ def handler(req: httpx.Request) -> httpx.Response: with pytest.raises(ai.ProviderAuthenticationError): await ai.generate( - mock_model(httpx.MockTransport(handler), model_id=_VIDEO_MODEL_ID), + mock_model( + httpx.MockTransport(handler), model_id=_VIDEO_MODEL_ID + ), [user_msg("test")], params=VideoParams(), ) @@ -315,7 +337,9 @@ def handler(req: httpx.Request) -> httpx.Response: with pytest.raises(ai.ProviderResponseError, match="SSE stream ended"): await ai.generate( - mock_model(httpx.MockTransport(handler), model_id=_VIDEO_MODEL_ID), + mock_model( + httpx.MockTransport(handler), model_id=_VIDEO_MODEL_ID + ), [user_msg("test")], params=VideoParams(), ) diff --git a/tests/providers/ai_gateway/test_probe.py b/tests/providers/ai_gateway/test_probe.py index 57362d24..2803c914 100644 --- a/tests/providers/ai_gateway/test_probe.py +++ b/tests/providers/ai_gateway/test_probe.py @@ -49,7 +49,9 @@ async def test_auth_ok_model_absent() -> None: await model.provider.probe(model) assert exc_info.value.model_id == model.id - assert isinstance(exc_info.value.__cause__, errors.GatewayModelNotFoundError) + assert isinstance( + exc_info.value.__cause__, errors.GatewayModelNotFoundError + ) @pytest.mark.parametrize("status", [401, 403]) @@ -58,7 +60,9 @@ async def test_credits_auth_error_raises(status: int) -> None: with pytest.raises(ai.ProviderAuthenticationError) as exc_info: await model.provider.probe(model) - assert isinstance(exc_info.value.__cause__, errors.GatewayAuthenticationError) + assert isinstance( + exc_info.value.__cause__, errors.GatewayAuthenticationError + ) async def test_missing_configuration_raises_not_configured( diff --git a/tests/providers/ai_gateway/test_protocol.py b/tests/providers/ai_gateway/test_protocol.py index ba00592f..5c881bc2 100644 --- a/tests/providers/ai_gateway/test_protocol.py +++ b/tests/providers/ai_gateway/test_protocol.py @@ -144,7 +144,8 @@ async def test_user_message_with_image_url(self) -> None: parts=[ messages.TextPart(text="Look at this"), messages.FilePart( - data="https://example.com/cat.jpg", media_type="image/jpeg" + data="https://example.com/cat.jpg", + media_type="image/jpeg", ), ], ) @@ -168,7 +169,9 @@ async def test_user_message_with_file_bytes(self) -> None: role="user", parts=[ messages.FilePart( - data=b"\x89PNG", media_type="image/png", filename="pic.png" + data=b"\x89PNG", + media_type="image/png", + filename="pic.png", ), ], ) @@ -200,11 +203,6 @@ async def test_pending_tool_call_no_tool_message(self) -> None: assert result[0]["role"] == "assistant" -# --------------------------------------------------------------------------- -# _build_request_body -- output_type (not tested in test_stream.py) -# --------------------------------------------------------------------------- - - class TestBuildRequestBody: async def test_with_output_type(self) -> None: class WeatherResult(pydantic.BaseModel): @@ -217,7 +215,9 @@ class WeatherResult(pydantic.BaseModel): parts=[messages.TextPart(text="Weather?")], ) ] - body = await protocol._build_request_body(msgs, output_type=WeatherResult) + body = await protocol._build_request_body( + msgs, output_type=WeatherResult + ) assert "responseFormat" in body rf = body["responseFormat"] @@ -228,7 +228,7 @@ class WeatherResult(pydantic.BaseModel): class TestParseStreamPartComplex: - def test_text_delta_uses_textDelta_key(self) -> None: + def test_text_delta_uses_text_delta_key(self) -> None: """The gateway sends ``textDelta`` (camelCase), not ``delta``.""" events = protocol._parse_stream_part( {"type": "text-delta", "id": "t1", "textDelta": "Hello"}, set() @@ -259,7 +259,11 @@ def test_tool_call_skipped_when_already_streamed(self) -> None: """A ``tool-call`` that duplicates a streamed tool is dropped.""" seen: set[str] = set() protocol._parse_stream_part( - {"type": "tool-input-start", "id": "tc-1", "toolName": "get_weather"}, + { + "type": "tool-input-start", + "id": "tc-1", + "toolName": "get_weather", + }, seen, ) events = protocol._parse_stream_part( @@ -358,7 +362,9 @@ def test_unknown_types_produce_no_events(self) -> None: class TestParseUsage: def test_flat_format(self) -> None: - usage = protocol._parse_usage({"prompt_tokens": 10, "completion_tokens": 20}) + usage = protocol._parse_usage( + {"prompt_tokens": 10, "completion_tokens": 20} + ) assert usage.input_tokens == 10 assert usage.output_tokens == 20 diff --git a/tests/providers/ai_gateway/test_provider.py b/tests/providers/ai_gateway/test_provider.py index c20a2a41..0d6d7054 100644 --- a/tests/providers/ai_gateway/test_provider.py +++ b/tests/providers/ai_gateway/test_provider.py @@ -7,7 +7,9 @@ from ai.providers.ai_gateway.client import errors -async def test_list_models_gets_config_with_gateway_headers_and_sorts_ids() -> None: +async def test_list_models_gets_config_with_gateway_headers_and_sorts_ids() -> ( + None +): captured_urls: list[str] = [] captured_headers: dict[str, str] = {} @@ -48,7 +50,9 @@ async def test_list_models_remaps_gateway_errors() -> None: def _handler(request: httpx.Request) -> httpx.Response: return httpx.Response( 401, - json={"error": {"message": "bad key", "type": "authentication_error"}}, + json={ + "error": {"message": "bad key", "type": "authentication_error"} + }, ) provider = ai.get_provider( @@ -64,4 +68,6 @@ def _handler(request: httpx.Request) -> httpx.Response: finally: await provider.aclose() - assert isinstance(exc_info.value.__cause__, errors.GatewayAuthenticationError) + assert isinstance( + exc_info.value.__cause__, errors.GatewayAuthenticationError + ) diff --git a/tests/providers/ai_gateway/test_stream.py b/tests/providers/ai_gateway/test_stream.py index b3ac5fe0..10962a6a 100644 --- a/tests/providers/ai_gateway/test_stream.py +++ b/tests/providers/ai_gateway/test_stream.py @@ -103,7 +103,9 @@ async def test_reasoning_then_text(self) -> None: def handler(req: httpx.Request) -> httpx.Response: return httpx.Response(200, text=body) - final = await _final(mock_client(httpx.MockTransport(handler)), [user_msg("?")]) + final = await _final( + mock_client(httpx.MockTransport(handler)), [user_msg("?")] + ) assert final.reasoning == "think" assert final.text == "42" @@ -140,7 +142,11 @@ async def test_inline_file_stream(self) -> None: alongside text in the language model stream.""" body = sse( {"type": "text-start", "id": "t1"}, - {"type": "text-delta", "id": "t1", "textDelta": "Here is an image:"}, + { + "type": "text-delta", + "id": "t1", + "textDelta": "Here is an image:", + }, {"type": "text-end", "id": "t1"}, { "type": "file", @@ -192,7 +198,7 @@ def handler(req: httpx.Request) -> httpx.Response: assert json.loads(final.tool_calls[0].tool_args) == {"city": "SF"} async def test_provider_executed_tool_call_streaming(self) -> None: - """``providerExecuted: true`` routes ``tool-input-*`` to BuiltinTool* events. + """Route ``tool-input-*`` to BuiltinTool* events. ``tool-result`` with ``providerExecuted: true`` aggregates into ``Message.builtin_tool_returns``. @@ -205,7 +211,11 @@ async def test_provider_executed_tool_call_streaming(self) -> None: "toolName": "web_search", "providerExecuted": True, }, - {"type": "tool-input-delta", "id": "tc-1", "delta": '{"q":"weather"}'}, + { + "type": "tool-input-delta", + "id": "tc-1", + "delta": '{"q":"weather"}', + }, {"type": "tool-input-end", "id": "tc-1"}, { "type": "tool-result", @@ -236,7 +246,7 @@ def handler(req: httpx.Request) -> httpx.Response: assert ret.result == result_payload async def test_provider_executed_one_shot_tool_call(self) -> None: - """One-shot ``tool-call`` with ``providerExecuted`` expands to BuiltinTool*.""" + """Expand provider-executed one-shot calls to BuiltinTool*.""" body = sse( { "type": "tool-call", @@ -277,7 +287,9 @@ def handler(req: httpx.Request) -> httpx.Response: captured.update(dict(req.headers)) return httpx.Response( 200, - text=sse({"type": "finish", "finishReason": "stop", "usage": {}}), + text=sse( + {"type": "finish", "finishReason": "stop", "usage": {}} + ), ) model = mock_model( @@ -301,10 +313,14 @@ def handler(req: httpx.Request) -> httpx.Response: captured_body.update(json.loads(req.content)) return httpx.Response( 200, - text=sse({"type": "finish", "finishReason": "stop", "usage": {}}), + text=sse( + {"type": "finish", "finishReason": "stop", "usage": {}} + ), ) - await _collect(mock_client(httpx.MockTransport(handler)), [user_msg("Hello")]) + await _collect( + mock_client(httpx.MockTransport(handler)), [user_msg("Hello")] + ) assert captured_body["prompt"] == [ { @@ -320,7 +336,9 @@ def handler(req: httpx.Request) -> httpx.Response: captured_body.update(json.loads(req.content)) return httpx.Response( 200, - text=sse({"type": "finish", "finishReason": "stop", "usage": {}}), + text=sse( + {"type": "finish", "finishReason": "stop", "usage": {}} + ), ) model = mock_model( @@ -378,7 +396,9 @@ def handler(req: httpx.Request) -> httpx.Response: async with models.stream( model, [user_msg("Hi")], - params=[{"providerOptions": {"openai": {"serviceTier": "auto"}}}], + params=[ + {"providerOptions": {"openai": {"serviceTier": "auto"}}} + ], ) as stream: async for _ in stream: pass @@ -398,7 +418,9 @@ def handler(req: httpx.Request) -> httpx.Response: captured_body.update(json.loads(req.content)) return httpx.Response( 200, - text=sse({"type": "finish", "finishReason": "stop", "usage": {}}), + text=sse( + {"type": "finish", "finishReason": "stop", "usage": {}} + ), ) await _collect( @@ -422,7 +444,9 @@ def handler(req: httpx.Request) -> httpx.Response: captured_body.update(json.loads(req.content)) return httpx.Response( 200, - text=sse({"type": "finish", "finishReason": "stop", "usage": {}}), + text=sse( + {"type": "finish", "finishReason": "stop", "usage": {}} + ), ) tool_call = messages.ToolCallPart( @@ -456,14 +480,17 @@ def handler(req: httpx.Request) -> httpx.Response: async def test_multi_turn_round_trip_builtin_parts(self) -> None: """``BuiltinToolCallPart``/``BuiltinToolReturnPart`` serialize as v3 - ``tool-call``/``tool-result`` blocks tagged ``providerExecuted: true``.""" + ``tool-call``/``tool-result`` blocks. + """ captured_body: dict[str, Any] = {} def handler(req: httpx.Request) -> httpx.Response: captured_body.update(json.loads(req.content)) return httpx.Response( 200, - text=sse({"type": "finish", "finishReason": "stop", "usage": {}}), + text=sse( + {"type": "finish", "finishReason": "stop", "usage": {}} + ), ) call = messages.BuiltinToolCallPart( @@ -484,7 +511,9 @@ def handler(req: httpx.Request) -> httpx.Response: await _collect(mock_client(httpx.MockTransport(handler)), convo) - assistant = next(m for m in captured_body["prompt"] if m["role"] == "assistant") + assistant = next( + m for m in captured_body["prompt"] if m["role"] == "assistant" + ) assert assistant["content"] == [ { "type": "tool-call", @@ -525,7 +554,9 @@ def handler(req: httpx.Request) -> httpx.Response: ) with pytest.raises(ai.ProviderAuthenticationError): - await _collect(mock_client(httpx.MockTransport(handler)), [user_msg("Hi")]) + await _collect( + mock_client(httpx.MockTransport(handler)), [user_msg("Hi")] + ) async def test_429_rate_limit_error(self) -> None: def handler(req: httpx.Request) -> httpx.Response: @@ -540,7 +571,9 @@ def handler(req: httpx.Request) -> httpx.Response: ) with pytest.raises(ai.ProviderRateLimitError): - await _collect(mock_client(httpx.MockTransport(handler)), [user_msg("Hi")]) + await _collect( + mock_client(httpx.MockTransport(handler)), [user_msg("Hi")] + ) async def test_404_model_not_found(self) -> None: def handler(req: httpx.Request) -> httpx.Response: @@ -556,7 +589,9 @@ def handler(req: httpx.Request) -> httpx.Response: ) with pytest.raises(ai.ProviderModelNotFoundError) as exc_info: - await _collect(mock_client(httpx.MockTransport(handler)), [user_msg("Hi")]) + await _collect( + mock_client(httpx.MockTransport(handler)), [user_msg("Hi")] + ) assert exc_info.value.model_id == "xyz" async def test_500_malformed_response(self) -> None: @@ -564,4 +599,6 @@ def handler(req: httpx.Request) -> httpx.Response: return httpx.Response(500, text="Not JSON") with pytest.raises(ai.ProviderResponseError): - await _collect(mock_client(httpx.MockTransport(handler)), [user_msg("Hi")]) + await _collect( + mock_client(httpx.MockTransport(handler)), [user_msg("Hi")] + ) diff --git a/tests/providers/anthropic/conftest.py b/tests/providers/anthropic/conftest.py index 76a5525d..bcffcf6e 100644 --- a/tests/providers/anthropic/conftest.py +++ b/tests/providers/anthropic/conftest.py @@ -103,7 +103,9 @@ def __init__( captured: dict[str, Any] | None = None, stream: FakeStream | None = None, ) -> None: - self.messages = FakeMessages(captured if captured is not None else {}, stream) + self.messages = FakeMessages( + captured if captured is not None else {}, stream + ) self.closed = False async def close(self) -> None: @@ -118,7 +120,9 @@ async def close(self) -> None: def block_start(index: int, block_type: str, **fields: Any) -> SimpleNamespace: """Build an SDK ``content_block_start`` event.""" block = SimpleNamespace(type=block_type, **fields) - return SimpleNamespace(type="content_block_start", index=index, content_block=block) + return SimpleNamespace( + type="content_block_start", index=index, content_block=block + ) def block_stop(index: int) -> SimpleNamespace: diff --git a/tests/providers/anthropic/test_adapter.py b/tests/providers/anthropic/test_adapter.py index af6afea9..01d23da6 100644 --- a/tests/providers/anthropic/test_adapter.py +++ b/tests/providers/anthropic/test_adapter.py @@ -42,7 +42,7 @@ def _patch_client( _ = monkeypatch captured: dict[str, Any] = {} fake = FakeAnthropicClient(captured) - return cast(anthropic.AsyncAnthropic, fake), captured + return cast("anthropic.AsyncAnthropic", fake), captured _MODEL = ai.Model("claude-sonnet-4-6", provider=ai.get_provider("anthropic")) @@ -154,7 +154,7 @@ async def test_reasoning_signature_round_trips_from_provider_metadata( async def test_builtin_tool_parts_round_trip( monkeypatch: pytest.MonkeyPatch, ) -> None: - """``BuiltinToolCallPart``/``BuiltinToolReturnPart`` serialize back to wire.""" + """Built-in tool parts serialize back to wire.""" fake, captured = _patch_client(monkeypatch) call = messages.BuiltinToolCallPart( @@ -180,7 +180,9 @@ async def test_builtin_tool_parts_round_trip( await _drain(protocol.stream(fake, _MODEL, convo, provider="anthropic")) - assistant = next(m for m in captured["messages"] if m["role"] == "assistant") + assistant = next( + m for m in captured["messages"] if m["role"] == "assistant" + ) assert assistant["content"] == [ { "type": "server_tool_use", @@ -215,7 +217,7 @@ async def test_sdk_errors_are_mapped_to_provider_hierarchy( with pytest.raises(ai.ProviderOverloadedError) as exc_info: await _drain( protocol.stream( - cast(anthropic.AsyncAnthropic, fake), + cast("anthropic.AsyncAnthropic", fake), _MODEL, [ai.user_message("Hi")], provider="anthropic", @@ -251,7 +253,7 @@ async def test_model_404_is_mapped_to_model_not_found( with pytest.raises(ai.ProviderModelNotFoundError) as exc_info: await _drain( protocol.stream( - cast(anthropic.AsyncAnthropic, fake), + cast("anthropic.AsyncAnthropic", fake), _MODEL, [ai.user_message("Hi")], provider="anthropic", diff --git a/tests/providers/anthropic/test_provider.py b/tests/providers/anthropic/test_provider.py index fe25bf5d..1692b48d 100644 --- a/tests/providers/anthropic/test_provider.py +++ b/tests/providers/anthropic/test_provider.py @@ -13,7 +13,7 @@ ) -async def test_list_models_gets_models_with_provider_headers_and_sorts_ids() -> None: +async def test_list_models_gets_provider_headers_and_sorts_ids() -> None: captured_urls: list[str] = [] captured_headers: dict[str, str] = {} @@ -54,7 +54,9 @@ async def test_list_models_maps_sdk_errors_to_provider_hierarchy() -> None: def _handler(request: httpx.Request) -> httpx.Response: return httpx.Response( 529, - json={"error": {"message": "overloaded", "type": "overloaded_error"}}, + json={ + "error": {"message": "overloaded", "type": "overloaded_error"} + }, headers={"request-id": "req-anthropic"}, ) @@ -146,7 +148,9 @@ def test_provider_is_configured_requires_api_key( monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False) assert ai.get_provider("anthropic").is_configured() is False - assert ai.get_provider("anthropic", api_key="sk-test").is_configured() is True + assert ( + ai.get_provider("anthropic", api_key="sk-test").is_configured() is True + ) def test_get_provider_raises_installation_error_when_anthropic_sdk_missing( diff --git a/tests/providers/anthropic/test_stream.py b/tests/providers/anthropic/test_stream.py index 4ac959f4..58ae600b 100644 --- a/tests/providers/anthropic/test_stream.py +++ b/tests/providers/anthropic/test_stream.py @@ -31,12 +31,14 @@ _MODEL = ai.Model("claude-sonnet-4-6", provider=ai.get_provider("anthropic")) -async def _drain(stream: FakeStream, monkeypatch: pytest.MonkeyPatch) -> models.Stream: +async def _drain( + stream: FakeStream, monkeypatch: pytest.MonkeyPatch +) -> models.Stream: fake = FakeAnthropicClient(stream=stream) _ = monkeypatch s = models.Stream( protocol.stream( - cast(anthropic.AsyncAnthropic, fake), + cast("anthropic.AsyncAnthropic", fake), _MODEL, [ai.user_message("Hi")], provider="anthropic", @@ -89,7 +91,9 @@ async def test_tool_result_block_emits_builtin_result( block_start(1, "web_search_tool_result"), block_stop(1), ] - s = await _drain(FakeStream(sdk_events, snapshot_content=snapshot), monkeypatch) + s = await _drain( + FakeStream(sdk_events, snapshot_content=snapshot), monkeypatch + ) returns = s.message.builtin_tool_returns assert len(returns) == 1 @@ -134,7 +138,7 @@ async def test_event_kinds_in_order(monkeypatch: pytest.MonkeyPatch) -> None: seen: list[type] = [] async for event in protocol.stream( - cast(anthropic.AsyncAnthropic, fake), + cast("anthropic.AsyncAnthropic", fake), _MODEL, [ai.user_message("Hi")], provider="anthropic", @@ -165,7 +169,7 @@ async def test_builtin_tool_end_carries_call_part( end_event: events.BuiltinToolEnd | None = None s = models.Stream( protocol.stream( - cast(anthropic.AsyncAnthropic, fake), + cast("anthropic.AsyncAnthropic", fake), _MODEL, [ai.user_message("Hi")], provider="anthropic", diff --git a/tests/providers/anthropic/test_tools.py b/tests/providers/anthropic/test_tools.py index 26f6a5ec..509a4fbf 100644 --- a/tests/providers/anthropic/test_tools.py +++ b/tests/providers/anthropic/test_tools.py @@ -34,7 +34,7 @@ async def _capture_tools( ) -> dict[str, Any]: _ = monkeypatch captured: dict[str, Any] = {} - fake = cast(anthropic.AsyncAnthropic, FakeAnthropicClient(captured)) + fake = cast("anthropic.AsyncAnthropic", FakeAnthropicClient(captured)) stream = protocol.stream( fake, _MODEL, @@ -132,7 +132,9 @@ async def test_text_editor_name_differs_from_class( monkeypatch: pytest.MonkeyPatch, ) -> None: """``TextEditor`` ships under the ``str_replace_based_edit_tool`` name.""" - captured = await _capture_tools(monkeypatch, [anthropic_tools.text_editor()]) + captured = await _capture_tools( + monkeypatch, [anthropic_tools.text_editor()] + ) assert captured["tools"] == [ {"type": "text_editor_20250728", "name": "str_replace_based_edit_tool"} diff --git a/tests/providers/openai/test_adapter.py b/tests/providers/openai/test_adapter.py index 65799226..902c24ad 100644 --- a/tests/providers/openai/test_adapter.py +++ b/tests/providers/openai/test_adapter.py @@ -71,7 +71,9 @@ async def close(self) -> None: class _FakeResponses: - def __init__(self, captured: dict[str, Any], items: list[dict[str, Any]]) -> None: + def __init__( + self, captured: dict[str, Any], items: list[dict[str, Any]] + ) -> None: self._captured = captured self._items = items @@ -81,7 +83,9 @@ async def create(self, **kwargs: Any) -> _ListStream: class _FakeResponsesClient: - def __init__(self, captured: dict[str, Any], items: list[dict[str, Any]]) -> None: + def __init__( + self, captured: dict[str, Any], items: list[dict[str, Any]] + ) -> None: self.responses = _FakeResponses(captured, items) @@ -116,7 +120,7 @@ def _patch( _ = monkeypatch captured: dict[str, Any] = {} fake = _FakeOpenAIClient(captured) - return cast(openai.AsyncOpenAI, fake), captured + return cast("openai.AsyncOpenAI", fake), captured def _patch_responses( @@ -124,7 +128,7 @@ def _patch_responses( ) -> tuple[openai.AsyncOpenAI, dict[str, Any]]: captured: dict[str, Any] = {} fake = _FakeResponsesClient(captured, items or []) - return cast(openai.AsyncOpenAI, fake), captured + return cast("openai.AsyncOpenAI", fake), captured async def _drain(stream: Any) -> None: @@ -239,7 +243,11 @@ async def test_responses_streams_text_and_usage() -> None: "output_index": 0, "item": {"id": "msg_1", "type": "message", "role": "assistant"}, }, - {"type": "response.output_text.delta", "item_id": "msg_1", "delta": "Hi"}, + { + "type": "response.output_text.delta", + "item_id": "msg_1", + "delta": "Hi", + }, { "type": "response.output_item.done", "output_index": 0, @@ -440,7 +448,10 @@ async def test_raw_params_pass_through_to_sdk_kwargs( assert captured["max_completion_tokens"] == 128 assert captured["extra_body"] == {"future_option": True} assert captured["extra_headers"] == {"x-openai-feature": "enabled"} - assert captured["stream_options"] == {"include_usage": False, "custom": True} + assert captured["stream_options"] == { + "include_usage": False, + "custom": True, + } async def test_strict_json_schema_flows_into_response_format( @@ -481,7 +492,7 @@ async def test_non_dict_params_rejected_by_adapter( async def test_builtin_tool_in_request_raises( monkeypatch: pytest.MonkeyPatch, ) -> None: - """Chat-completions adapter rejects OpenAI built-in tools at the boundary.""" + """Chat-completions rejects OpenAI built-in tools at the boundary.""" fake, _ = _patch(monkeypatch) stream = protocol.stream( @@ -525,7 +536,9 @@ async def test_sdk_errors_are_mapped_to_provider_hierarchy( _ = monkeypatch response = httpx.Response( 429, - request=httpx.Request("POST", "https://openai.test/v1/chat/completions"), + request=httpx.Request( + "POST", "https://openai.test/v1/chat/completions" + ), headers={"x-request-id": "req-openai"}, ) sdk_error = openai.RateLimitError( @@ -538,7 +551,7 @@ async def test_sdk_errors_are_mapped_to_provider_hierarchy( with pytest.raises(ai.ProviderRateLimitError) as exc_info: await _drain( protocol.stream( - cast(openai.AsyncOpenAI, fake), + cast("openai.AsyncOpenAI", fake), _MODEL, [ai.user_message("Hi")], provider="openai", @@ -561,7 +574,9 @@ async def test_model_404_is_mapped_to_model_not_found( _ = monkeypatch response = httpx.Response( 404, - request=httpx.Request("POST", "https://openai.test/v1/chat/completions"), + request=httpx.Request( + "POST", "https://openai.test/v1/chat/completions" + ), ) sdk_error = openai.NotFoundError( "model not found", @@ -573,7 +588,7 @@ async def test_model_404_is_mapped_to_model_not_found( with pytest.raises(ai.ProviderModelNotFoundError) as exc_info: await _drain( protocol.stream( - cast(openai.AsyncOpenAI, fake), + cast("openai.AsyncOpenAI", fake), _MODEL, [ai.user_message("Hi")], provider="openai", diff --git a/tests/providers/openai/test_provider.py b/tests/providers/openai/test_provider.py index ef3d26f6..b7ff6429 100644 --- a/tests/providers/openai/test_provider.py +++ b/tests/providers/openai/test_provider.py @@ -7,7 +7,10 @@ import pytest import ai -from ai.providers.openai import OpenAICompatibleProvider, OpenAIResponsesProtocol +from ai.providers.openai import ( + OpenAICompatibleProvider, + OpenAIResponsesProtocol, +) async def test_list_models_gets_models_with_auth_header_and_sorts_ids() -> None: @@ -194,7 +197,9 @@ def _missing_openai(name: str, package: str | None = None) -> object: }, ) - assert "required to use the cloudflare-workers-ai provider" in str(exc_info.value) + assert "required to use the cloudflare-workers-ai provider" in str( + exc_info.value + ) def test_get_provider_accepts_base_url_and_api_key() -> None: diff --git a/tests/test_middleware.py b/tests/test_middleware.py index 7dd88617..ba82e78a 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -37,7 +37,9 @@ async def test_wrap_tool_is_called() -> None: tool_calls: list[middleware.ToolContext] = [] class Spy(middleware._Middleware): - async def wrap_tool(self, call: middleware.ToolContext, next: Any) -> Any: + async def wrap_tool( + self, call: middleware.ToolContext, next: Any + ) -> Any: tool_calls.append(call) return await next(call) @@ -71,12 +73,16 @@ async def test_wrap_hook_is_called() -> None: hook_calls: list[middleware.HookContext] = [] class Spy(middleware._Middleware): - async def wrap_hook(self, call: middleware.HookContext, next: Any) -> Any: + async def wrap_hook( + self, call: middleware.HookContext, next: Any + ) -> Any: hook_calls.append(call) return await next(call) class MyAgent(ai.Agent): - async def loop(self, context: ai.Context) -> AsyncGenerator[ai.events.Event]: + async def loop( + self, context: ai.Context + ) -> AsyncGenerator[ai.events.Event]: async with ai.models.stream(context=context) as stream: async for event in stream: yield event @@ -134,14 +140,21 @@ async def wrap_agent_run( async for _m in stream: pass - assert order == ["outer-before", "inner-before", "inner-after", "outer-after"] + assert order == [ + "outer-before", + "inner-before", + "inner-after", + "outer-after", + ] async def test_wrap_tool_context_fields_flow_to_result() -> None: """ToolContext.tool_name is used in the result message.""" class Rewriter(middleware._Middleware): - async def wrap_tool(self, call: middleware.ToolContext, next: Any) -> Any: + async def wrap_tool( + self, call: middleware.ToolContext, next: Any + ) -> Any: # Rewrite the tool_name via dataclasses.replace. modified = dataclasses.replace(call, tool_name="rewritten-name") return await next(modified) @@ -171,7 +184,9 @@ async def test_wrap_tool_rewriting_tool_call_id_breaks_history() -> None: """tool_call_id is a correlation key and must stay stable.""" class Rewriter(middleware._Middleware): - async def wrap_tool(self, call: middleware.ToolContext, next: Any) -> Any: + async def wrap_tool( + self, call: middleware.ToolContext, next: Any + ) -> Any: modified = dataclasses.replace(call, tool_call_id="rewritten-id") return await next(modified) @@ -204,7 +219,9 @@ async def test_model_context_messages_are_isolated() -> None: original_messages = [ai.user_message("Hello")] class Mutator(middleware._Middleware): - async def wrap_model(self, call: middleware.ModelContext, next: Any) -> Any: + async def wrap_model( + self, call: middleware.ModelContext, next: Any + ) -> Any: # Try to mutate the context's messages list in place. call.messages.append(ai.system_message("injected")) return await next(call) @@ -230,7 +247,9 @@ async def test_middleware_can_fix_bad_tool_kwargs() -> None: """A middleware that rewrites call.kwargs can fix malformed tool args.""" class ArgFixer(middleware._Middleware): - async def wrap_tool(self, call: middleware.ToolContext, next: Any) -> Any: + async def wrap_tool( + self, call: middleware.ToolContext, next: Any + ) -> Any: # If kwargs are empty (parse failed), supply valid ones. if not call.kwargs: fixed = dataclasses.replace(call, kwargs={"x": 99}) diff --git a/tests/test_util.py b/tests/test_util.py index 08a2775a..d8afe555 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -5,7 +5,7 @@ import asyncio import contextvars from collections.abc import AsyncIterable, AsyncIterator -from typing import Any +from typing import Any, cast import async_solipsism # type: ignore[import-untyped] import pytest @@ -22,7 +22,6 @@ async def _from_list(items: list[Any], delay: float = 0) -> AsyncIterable[Any]: for item in items: if delay: await asyncio.sleep(delay) - print(asyncio.get_event_loop().time(), item) yield item @@ -93,7 +92,7 @@ async def test_simulated_clock_advances() -> None: ) ) elapsed = loop.time() - t0 - assert elapsed == 20.0 + assert elapsed == pytest.approx(20.0) async def test_ordering_shorter_delay_first() -> None: @@ -123,7 +122,7 @@ async def good() -> AsyncIterable[str]: async def bad() -> AsyncIterable[str]: await asyncio.sleep(1) raise RuntimeError("boom") - yield "unreachable" # noqa: B027 + yield "unreachable" with pytest.raises(ExceptionGroup) as exc_info: await _collect(util.merge(good(), bad())) @@ -185,7 +184,7 @@ async def __anext__(self) -> int: async def failing() -> AsyncIterable[int]: raise RuntimeError("boom") - yield 0 # noqa: B027 + yield 0 with pytest.raises(ExceptionGroup) as exc_info: await _collect(util.merge(SimpleIter(), failing())) @@ -197,7 +196,7 @@ async def failing() -> AsyncIterable[int]: async def test_unwrap_generator_exit_pure_generator_exit() -> None: - """A BaseExceptionGroup containing only GeneratorExit unwraps to GeneratorExit.""" + """A group containing only GeneratorExit unwraps to GeneratorExit.""" with pytest.raises(GeneratorExit): async with util.unwrap_generator_exit(): raise BaseExceptionGroup("group", [GeneratorExit()]) @@ -217,7 +216,9 @@ async def test_unwrap_generator_exit_mixed_propagates() -> None: """A group with non-GeneratorExit exceptions propagates as-is.""" with pytest.raises(BaseExceptionGroup) as exc_info: async with util.unwrap_generator_exit(): - raise BaseExceptionGroup("group", [GeneratorExit(), ValueError("x")]) + raise BaseExceptionGroup( + "group", [GeneratorExit(), ValueError("x")] + ) assert exc_info.group_contains(ValueError, match="x") @@ -292,7 +293,9 @@ async def gen() -> AsyncIterator[int]: async def test_decouple_yields_all_items() -> None: """Basic: every item from the source is yielded in order.""" - result = await _collect(util.decouple(_from_list([1, 2, 3]), task_group=None)) + result = await _collect( + util.decouple(_from_list([1, 2, 3]), task_group=None) + ) assert result == [1, 2, 3] @@ -301,7 +304,9 @@ async def test_decouple_with_task_group() -> None: async def consume() -> list[int]: async with asyncio.TaskGroup() as tg: - return await _collect(util.decouple(_from_list([1, 2, 3]), task_group=tg)) + return await _collect( + util.decouple(_from_list([1, 2, 3]), task_group=tg) + ) assert await consume() == [1, 2, 3] @@ -321,7 +326,7 @@ async def failing() -> AsyncIterable[int]: async def test_decouple_contextvar_stable_across_yields() -> None: - """ContextVars set inside the source persist across yields under decouple.""" + """ContextVars set in the source persist across decouple yields.""" var: contextvars.ContextVar[str] = contextvars.ContextVar("test") async def src() -> AsyncIterator[str]: @@ -389,7 +394,7 @@ async def src() -> AsyncIterator[int]: break # Should complete without raising BaseExceptionGroup. - await m.aclose() # type: ignore[attr-defined] + await cast(Any, m).aclose() async def test_merge_preserves_contextvar_across_yields() -> None: @@ -418,7 +423,7 @@ async def src() -> AsyncIterator[int]: class _Restartable: - """An iterable whose ``__aiter__`` returns a fresh async generator each call. + """An iterable returning a fresh async generator each call. Items can be queued via ``push``; each iteration drains the queue and stops. Tracks how many times ``__aiter__`` has been called. @@ -539,7 +544,7 @@ async def driver() -> AsyncIterator[str]: async def test_merge_restart_only_after_other_iterable_yields() -> None: - """Restart is triggered by another iterable yielding, not by self-completion.""" + """Restart is triggered by another iterable, not self-completion.""" src = _Restartable() src.push("r1") @@ -614,7 +619,7 @@ async def driver() -> AsyncIterator[str]: def test_merge_cleanup_on_asyncio_shutdown() -> None: - """A leaked partially-consumed merge gen is cleaned up correctly on shutdown. + """A partially consumed merge gen is cleaned up on shutdown. The consumer breaks out of ``async for x in merge(src())`` without explicitly aclose'ing the merge gen, so cleanup is left to asyncio.run's diff --git a/tests/types/test_builders.py b/tests/types/test_builders.py index 46b2cc59..2aae1275 100644 --- a/tests/types/test_builders.py +++ b/tests/types/test_builders.py @@ -2,13 +2,17 @@ from __future__ import annotations +from typing import Any, cast + import pytest from ai.types import builders, messages def test_user_message_mixed_content() -> None: - fp = messages.FilePart(data="https://example.com/img.png", media_type="image/png") + fp = messages.FilePart( + data="https://example.com/img.png", media_type="image/png" + ) msg = builders.user_message("Describe this:", fp, "Thanks") assert len(msg.parts) == 3 assert isinstance(msg.parts[0], messages.TextPart) @@ -48,7 +52,10 @@ def test_tool_message_merges_tool_messages() -> None: merged = builders.tool_message(m1, m2) assert merged.role == "tool" - assert [part.tool_call_id for part in merged.tool_results] == ["tc-1", "tc-2"] + assert [part.tool_call_id for part in merged.tool_results] == [ + "tc-1", + "tc-2", + ] def test_tool_message_rejects_non_tool_message() -> None: @@ -57,11 +64,13 @@ def test_tool_message_rejects_non_tool_message() -> None: def test_tool_message_rejects_non_result_parts() -> None: - invalid = messages.Message(role="tool", parts=[messages.TextPart(text="bad")]) + invalid = messages.Message( + role="tool", parts=[messages.TextPart(text="bad")] + ) with pytest.raises(TypeError, match="ToolResultPart"): builders.tool_message(invalid) def test_invalid_type_raises() -> None: with pytest.raises(TypeError): - builders.user_message(42) # type: ignore[arg-type] + builders.user_message(cast(Any, 42)) diff --git a/tests/types/test_integrity.py b/tests/types/test_integrity.py index 4a06bbbc..8e41101d 100644 --- a/tests/types/test_integrity.py +++ b/tests/types/test_integrity.py @@ -15,7 +15,13 @@ from ai.types import events as events_ from ai.types.integrity import IntegrityError, prepare_messages -from ..conftest import MOCK_MODEL, MOCK_PROVIDER, mock_generate, mock_llm, text_msg +from ..conftest import ( + MOCK_MODEL, + MOCK_PROVIDER, + mock_generate, + mock_llm, + text_msg, +) # --------------------------------------------------------------------------- # Helpers @@ -101,7 +107,9 @@ def test_idempotent() -> None: def test_drops_internal_messages() -> None: msgs = [ builders.user_message("hi"), - messages.Message(role="internal", parts=[messages.TextPart(text="internal")]), + messages.Message( + role="internal", parts=[messages.TextPart(text="internal")] + ), builders.assistant_message("hello"), ] result = prepare_messages(msgs) @@ -129,7 +137,9 @@ def test_strips_internal_parts() -> None: role="assistant", parts=[ messages.TextPart(text="hi"), - messages.HookPart(hook_id="h1", hook_type="confirm", status="resolved"), + messages.HookPart( + hook_id="h1", hook_type="confirm", status="resolved" + ), ], ) result = prepare_messages([msg]) @@ -143,7 +153,9 @@ def test_strips_internal_parts_drops_empty_message() -> None: msg = messages.Message( role="assistant", parts=[ - messages.HookPart(hook_id="h1", hook_type="confirm", status="resolved"), + messages.HookPart( + hook_id="h1", hook_type="confirm", status="resolved" + ), ], ) result = prepare_messages([msg]) @@ -154,7 +166,9 @@ def test_internal_parts_strict_raises() -> None: msg = messages.Message( role="assistant", parts=[ - messages.HookPart(hook_id="h1", hook_type="confirm", status="resolved"), + messages.HookPart( + hook_id="h1", hook_type="confirm", status="resolved" + ), ], ) with pytest.raises(IntegrityError) as exc_info: @@ -226,7 +240,7 @@ def test_inserts_synthetic_result_before_user_interruption() -> None: def test_inserts_synthetic_result_before_next_assistant() -> None: - """New assistant message while tool calls pending triggers synthetic results.""" + """New assistant message with pending tool calls adds synthetic results.""" msgs = [ builders.user_message("calc 2+2"), _assistant_with_tool_call(), @@ -243,8 +257,12 @@ def test_multiple_orphaned_calls_get_individual_results() -> None: msg = messages.Message( role="assistant", parts=[ - messages.ToolCallPart(tool_call_id="tc-1", tool_name="a", tool_args="{}"), - messages.ToolCallPart(tool_call_id="tc-2", tool_name="b", tool_args="{}"), + messages.ToolCallPart( + tool_call_id="tc-1", tool_name="a", tool_args="{}" + ), + messages.ToolCallPart( + tool_call_id="tc-2", tool_name="b", tool_args="{}" + ), ], ) result = prepare_messages([builders.user_message("go"), msg]) @@ -275,7 +293,6 @@ def test_partial_results_only_fills_missing() -> None: builders.user_message("stop"), ] result = prepare_messages(msgs) - # user, assistant, tool(tc-1), synthetic-tool(tc-2), user assert len(result) == 5 synthetic = result[3] assert synthetic.role == "tool" @@ -337,7 +354,12 @@ def test_complete_tool_flow_unchanged() -> None: ] result = prepare_messages(msgs) assert len(result) == 4 - assert [m.role for m in result] == ["user", "assistant", "tool", "assistant"] + assert [m.role for m in result] == [ + "user", + "assistant", + "tool", + "assistant", + ] # --------------------------------------------------------------------------- @@ -352,7 +374,9 @@ def test_strict_collects_all_issues() -> None: role="assistant", parts=[ messages.TextPart(text="hi"), - messages.HookPart(hook_id="h1", hook_type="confirm", status="resolved"), + messages.HookPart( + hook_id="h1", hook_type="confirm", status="resolved" + ), ], ), ] @@ -514,7 +538,9 @@ async def _spy_stream( msgs = [ ai.user_message("hi"), - messages.Message(role="internal", parts=[messages.TextPart(text="internal")]), + messages.Message( + role="internal", parts=[messages.TextPart(text="internal")] + ), ai.assistant_message("hello"), ] async with models.stream(MOCK_MODEL, msgs) as s: @@ -563,7 +589,9 @@ async def _spy_gen( msgs = [ ai.user_message("A cat"), - messages.Message(role="internal", parts=[messages.TextPart(text="internal")]), + messages.Message( + role="internal", parts=[messages.TextPart(text="internal")] + ), ] await models.generate(MOCK_MODEL, msgs, models.ImageParams(n=1)) diff --git a/tests/types/test_media.py b/tests/types/test_media.py index de386afc..39ef8f4f 100644 --- a/tests/types/test_media.py +++ b/tests/types/test_media.py @@ -45,7 +45,9 @@ def test_detect_image_media_type_base64() -> None: def test_detect_audio_media_type_strips_id3() -> None: - id3_header = bytes([0x49, 0x44, 0x33, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + id3_header = bytes( + [0x49, 0x44, 0x33, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] + ) assert media.detect_audio_media_type(id3_header + bytes([0xFF, 0xFB])) == ( "audio/mpeg" ) diff --git a/tests/types/test_messages.py b/tests/types/test_messages.py index 8da5e148..f684f884 100644 --- a/tests/types/test_messages.py +++ b/tests/types/test_messages.py @@ -77,7 +77,9 @@ def test_from_url_infers_from_data_url() -> None: def test_from_url_explicit_media_type_overrides() -> None: - fp = messages.FilePart.from_url("https://example.com/img", media_type="image/webp") + fp = messages.FilePart.from_url( + "https://example.com/img", media_type="image/webp" + ) assert fp.media_type == "image/webp" diff --git a/uv.lock b/uv.lock index 27ab2504..ca3e468c 100644 --- a/uv.lock +++ b/uv.lock @@ -2,7 +2,8 @@ version = 1 revision = 3 requires-python = ">=3.12" resolution-markers = [ - "python_full_version >= '3.14'", + "python_full_version >= '3.15'", + "python_full_version == '3.14.*'", "python_full_version < '3.14'", ] @@ -31,12 +32,18 @@ dev = [ { name = "async-solipsism" }, { name = "mypy" }, { name = "openai" }, - { name = "pyright" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "python-dotenv" }, { name = "rich" }, { name = "ruff" }, + { name = "ty" }, +] +examples = [ + { name = "fastapi" }, + { name = "temporalio" }, + { name = "textual" }, + { name = "websockets" }, ] [package.metadata] @@ -55,14 +62,29 @@ provides-extras = ["anthropic", "openai"] dev = [ { name = "anthropic", specifier = ">=0.83.0" }, { name = "async-solipsism", specifier = ">=0.9" }, - { name = "mypy", specifier = ">=1.11" }, + { name = "mypy", specifier = "~=2.1.0" }, { name = "openai", specifier = ">=2.14.0" }, - { name = "pyright", specifier = ">=1.1.408" }, { name = "pytest", specifier = ">=8.0" }, { name = "pytest-asyncio", specifier = ">=0.24" }, { name = "python-dotenv", specifier = ">=1.2.1" }, { name = "rich", specifier = ">=14.2.0" }, - { name = "ruff", specifier = ">=0.8" }, + { name = "ruff", specifier = "~=0.8.0" }, + { name = "ty", specifier = "~=0.0.37" }, +] +examples = [ + { name = "fastapi", specifier = ">=0.136.1" }, + { name = "temporalio", specifier = ">=1.27.2" }, + { name = "textual", specifier = ">=8.2.6" }, + { name = "websockets", specifier = ">=16.0" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, ] [[package]] @@ -106,6 +128,46 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, ] +[[package]] +name = "ast-serialize" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/9d/09e27731bd5864a9ce04e3244074e674bb8936bf62b45e0357248717adac/ast_serialize-0.5.0.tar.gz", hash = "sha256:5880091bfe6f4f986f22866375c2e884843e7a0b6343ae41aeea659613d879b6", size = 61157, upload-time = "2026-05-17T17:48:29.429Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/9a/13dde51ba9e15f8b97957ab7cb0120d0e381524d651c6bd630b9c359227f/ast_serialize-0.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8f5c14f169eb0972c0c21bada5358b23d6047c76583b005234f865b11f1fa00a", size = 1183520, upload-time = "2026-05-17T17:47:30.831Z" }, + { url = "https://files.pythonhosted.org/packages/37/de/5a7f0a9fe68944f536632a5af84676739c7d2582be42deb082634bf3a754/ast_serialize-0.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7d1a2de9de5be04652f0ed60738356ef94f66db37924a9499fffe98dc491aa0b", size = 1175779, upload-time = "2026-05-17T17:47:32.551Z" }, + { url = "https://files.pythonhosted.org/packages/9c/81/0bb853e76e4f6e9a1855d569003c59e19ffac45f7079d91505d1bb212f92/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be5173fb66f9b49026d9d5a2ff0fc7c7009077107c0eb285b2d60fdf1fe10bd1", size = 1233750, upload-time = "2026-05-17T17:47:34.731Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d3/4cf705beeccc08754d0bbda99aefff26110e209b9a07ac8a6b60eec48531/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8015cd071ac1339924ee2b8098c93e00e155f30a16f40ec9816fcf84f4753f6", size = 1235942, upload-time = "2026-05-17T17:47:36.287Z" }, + { url = "https://files.pythonhosted.org/packages/26/c8/ee097e437ea27dd2b8b227865c875492b585650a5802a22d82b304c8201b/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5499e8797edff2a9186aa313ed382c6b422e798e9332d9953badcee6e69a88f2", size = 1442517, upload-time = "2026-05-17T17:47:38.17Z" }, + { url = "https://files.pythonhosted.org/packages/ff/bd/68063442838f1ba68ec72b5436430bc75b3bb17a1a3c3063f09b0c05ae2b/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6848f2a093fb5548751a9a09bff8fcd229e2bbeb0e3331f391b6ae6d26cd9903", size = 1254081, upload-time = "2026-05-17T17:47:39.826Z" }, + { url = "https://files.pythonhosted.org/packages/50/e2/1e520793bc6a4e4524a6ab022391e827825eaa0c3811828bfdc6852eca26/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:832d4c998e0b091fd60a6d6bceee535483c4d490de9ba85003af835225719261", size = 1259910, upload-time = "2026-05-17T17:47:41.369Z" }, + { url = "https://files.pythonhosted.org/packages/4e/e1/49b60f467979979cfe6913b43948ff25bca971ad0591d181812f163a988e/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:16db7c62ec0b8efe1d7afd283a388d8f74f2605d56032e5a37747d2de8dba027", size = 1250678, upload-time = "2026-05-17T17:47:43.702Z" }, + { url = "https://files.pythonhosted.org/packages/74/ba/66ab9555de6275677566f6574e5ef6c29cb185ea866f643bc06f8280a8ee/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:baf5eb061eb5bccade4128ad42da33787d72f6013809cd1b590376ece8b3c937", size = 1301603, upload-time = "2026-05-17T17:47:46.256Z" }, + { url = "https://files.pythonhosted.org/packages/66/42/6aca9b9abc710014b2be9059689e5dd1679339e78f567ffb4d255a9e2050/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:104e4a35bd7c124173c41760ef9aaea17ddb3f86c65cb643671d59afbe3ee94c", size = 1410332, upload-time = "2026-05-17T17:47:47.899Z" }, + { url = "https://files.pythonhosted.org/packages/47/68/2f76594432a22581ecf878b5e75a9b8601c24b2241cf0bbeb1e21fcf370c/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:36be371028fc1675acb38a331bde160dbab7ff907fdf00b67eb6911aa106951b", size = 1509979, upload-time = "2026-05-17T17:47:50.942Z" }, + { url = "https://files.pythonhosted.org/packages/40/ac/a93c9b58292653f6c595752f677a08e608f903b710594909e9231a389b3b/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:061ee58bdb52341c8201a6df41182a977736bae3b7ded87ca7176ca25a8a47ab", size = 1505002, upload-time = "2026-05-17T17:47:54.093Z" }, + { url = "https://files.pythonhosted.org/packages/14/2e/b278f68c497ee2f1d1576cbbef8db5281cd4a5f2db040537592ac9c8862e/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b15219e9cdc9f53f6f4cb51c009203507228226148c05c5e8fe451c28b435eb3", size = 1456231, upload-time = "2026-05-17T17:47:56.311Z" }, + { url = "https://files.pythonhosted.org/packages/0b/43/419be1c566a4c504cd8fd60ce2f84e790f295495c0f327cfaeadf3d51012/ast_serialize-0.5.0-cp314-cp314t-win32.whl", hash = "sha256:842d1c004bb466c7df036f95fabef789570541922b10976b12f5592a69cf0b38", size = 1058668, upload-time = "2026-05-17T17:47:58.305Z" }, + { url = "https://files.pythonhosted.org/packages/03/6f/c9d4d549295ed05111aeb8853232d1afd9d0a179fddb01eeffbb3a4a6842/ast_serialize-0.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b0c06d760909b095cc466356dfccd05a1c7233a6ca191c020dca2c6a6f16c24c", size = 1101075, upload-time = "2026-05-17T17:48:00.35Z" }, + { url = "https://files.pythonhosted.org/packages/d0/8e/d00c5ab30c58222e07d62956fca86c59d91b9ad32997e633c38b526623a3/ast_serialize-0.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:787baedb0262cc49e8ce37cc15c00ae818e46a165a3b36f5e21ed174998104cb", size = 1075347, upload-time = "2026-05-17T17:48:01.753Z" }, + { url = "https://files.pythonhosted.org/packages/e0/9e/dc2530acb3a60dc6e46d65abf27d1d9f86721694757906a148d90a6860de/ast_serialize-0.5.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:0668aa9459cfa8c9c49ddd2163ebcf43088ba045ef7492af6fe22e0098303101", size = 1191380, upload-time = "2026-05-17T17:48:03.738Z" }, + { url = "https://files.pythonhosted.org/packages/26/0a/bd3d18a582f273d6c843d16bb9e22e9e16365ff7991e92f18f798e9f1224/ast_serialize-0.5.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:bf683d6363edf2b39eed6b6d4fe22d34b6203867a67e27134d9e2a2680c4bc4a", size = 1183879, upload-time = "2026-05-17T17:48:05.463Z" }, + { url = "https://files.pythonhosted.org/packages/40/ae/1f919100f8620887af58fcc381c61a1f218cdf89c6e155f87b213e61010a/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc22cf0c9be65e71cf88fda130af60d61eb4a79370ad4cfe7900d48a4aa2211", size = 1244529, upload-time = "2026-05-17T17:48:07.008Z" }, + { url = "https://files.pythonhosted.org/packages/c6/ca/6376559dcce707cdbc1d0d9a13c8d3baaaa501e949ce0ebdc4230cd881aa/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f66173891548c9f2726bf27957b41cabce12fa679dc6da505ddbde4d4b3b31cf", size = 1240560, upload-time = "2026-05-17T17:48:08.46Z" }, + { url = "https://files.pythonhosted.org/packages/35/b2/a620e206b5aeb7efbf2710336df57d457cffbb3991076bbcc1147ef9abd4/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e42d729ef2be96a14efbad355093284739e3670ece3e534f82cc8832790911d9", size = 1451172, upload-time = "2026-05-17T17:48:09.922Z" }, + { url = "https://files.pythonhosted.org/packages/fa/e0/4ad5c04c24a40481b2935ce9a0ccdb6023dc8b667167d06ae530cc3512f2/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b725026bafa801dbd7310eb13a75f0a2e370e7e51b2cb225f9d21fcfadf919ee", size = 1265072, upload-time = "2026-05-17T17:48:11.469Z" }, + { url = "https://files.pythonhosted.org/packages/b2/71/4d1d479aa56d0101c40e17720c3d6ac2af7269ea0487a80b18e7bfd1a5b7/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b54f60c1d78767a53b67eaa663f0dfac3afe606aa07f1301572f588b73d64809", size = 1270488, upload-time = "2026-05-17T17:48:13.575Z" }, + { url = "https://files.pythonhosted.org/packages/6d/4f/0de1bbe06f6edef9fde4ed12ca8e7b3ec7e6e2bd4e672c5af487f7957665/ast_serialize-0.5.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:27d51654fc240a1e87e742d353d98eb45b75f62f129086b3596ab53df2ac2a43", size = 1260702, upload-time = "2026-05-17T17:48:15.141Z" }, + { url = "https://files.pythonhosted.org/packages/75/61/e00872439cfdddcc3c1b6cdaa6e5d904ba8e26a18807c67c4e14409d0ca8/ast_serialize-0.5.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c36237c46dd1674542f2109740ea5ea485a169bf1431939ada0434e17934", size = 1311182, upload-time = "2026-05-17T17:48:16.779Z" }, + { url = "https://files.pythonhosted.org/packages/76/8e/699a5b955f7926956c95e9e1d74132acad73c2fe7a426f94da89123c20aa/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1943db345233cc7194a470f13afa9c59772c0b123dea0c9414c4d4ca54369759", size = 1421410, upload-time = "2026-05-17T17:48:18.527Z" }, + { url = "https://files.pythonhosted.org/packages/a9/ae/d5b7626874478997adc7a29ab28accf21e596fb590c944290401dfd0b29e/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:df1c00022cbbcb064bfaa505aa9c9295362443ce5dacb459d1331d3da353f887", size = 1516587, upload-time = "2026-05-17T17:48:20.133Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ce/b59e02a82d9c4244d64cde502e0b00e83e38816abe19155ceb5437402c7f/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:cae65289fc456fde04af979a2be09302ef5d8ab92ef23e596d6746dc267ada27", size = 1515171, upload-time = "2026-05-17T17:48:21.921Z" }, + { url = "https://files.pythonhosted.org/packages/8b/38/d8d90042747d05aa08d4efcf1c99035a5f670a6bf4c214d31644392afbca/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:239a4c354e8d676e9d94631d1d4a64edc6b266f86ff3a5a80aedd344f342c01d", size = 1464668, upload-time = "2026-05-17T17:48:23.544Z" }, + { url = "https://files.pythonhosted.org/packages/dd/51/5b840c4df7334104cecffa28f23904fe81ca89ca223d2450e288de39fd3c/ast_serialize-0.5.0-cp39-abi3-win32.whl", hash = "sha256:143a4ef63285a075871908fda3672dc21864b83a8ec3ee12304aa3e4c5387b9a", size = 1068311, upload-time = "2026-05-17T17:48:25.027Z" }, + { url = "https://files.pythonhosted.org/packages/41/11/ca5672c7d491825bc4cd6702dea106a6b60d928707712ec257c7833ae476/ast_serialize-0.5.0-cp39-abi3-win_amd64.whl", hash = "sha256:cf25572c526add400f26a4750dc6ce0c3bb93fc1f75e7ae0cad4ce4f2cd5c590", size = 1108931, upload-time = "2026-05-17T17:48:26.591Z" }, + { url = "https://files.pythonhosted.org/packages/45/19/cc8bd127d28a43da249aa955cfd164cf8fd534e79e42cea96c4854d72fd0/ast_serialize-0.5.0-cp39-abi3-win_arm64.whl", hash = "sha256:92a31c9c20d25a076edaeec76b128a3535d74a24f340b9a8a7e96c9b86dc9642", size = 1081181, upload-time = "2026-05-17T17:48:28.122Z" }, +] + [[package]] name = "async-solipsism" version = "0.9" @@ -285,6 +347,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, ] +[[package]] +name = "fastapi" +version = "0.136.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5d/45/c130091c2dfa061bbfe3150f2a5091ef1adf149f2a8d2ae769ecaf6e99a2/fastapi-0.136.1.tar.gz", hash = "sha256:7af665ad7acfa0a3baf8983d393b6b471b9da10ede59c60045f49fbc89a0fa7f", size = 397448, upload-time = "2026-04-23T16:49:44.046Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/ff/2e4eca3ade2c22fe1dea7043b8ee9dabe47753349eb1b56a202de8af6349/fastapi-0.136.1-py3-none-any.whl", hash = "sha256:a6e9d7eeada96c93a4d69cb03836b44fa34e2854accb7244a1ece36cd4781c3f", size = 117683, upload-time = "2026-04-23T16:49:42.437Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -446,62 +524,74 @@ wheels = [ [[package]] name = "librt" -version = "0.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8a/3f/4ca7dd7819bf8ff303aca39c3c60e5320e46e766ab7f7dd627d3b9c11bdf/librt-0.8.0.tar.gz", hash = "sha256:cb74cdcbc0103fc988e04e5c58b0b31e8e5dd2babb9182b6f9490488eb36324b", size = 177306, upload-time = "2026-02-12T14:53:54.743Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/53/f3bc0c4921adb0d4a5afa0656f2c0fbe20e18e3e0295e12985b9a5dc3f55/librt-0.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:17269dd2745dbe8e42475acb28e419ad92dfa38214224b1b01020b8cac70b645", size = 66511, upload-time = "2026-02-12T14:52:30.34Z" }, - { url = "https://files.pythonhosted.org/packages/89/4b/4c96357432007c25a1b5e363045373a6c39481e49f6ba05234bb59a839c1/librt-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f4617cef654fca552f00ce5ffdf4f4b68770f18950e4246ce94629b789b92467", size = 68628, upload-time = "2026-02-12T14:52:31.491Z" }, - { url = "https://files.pythonhosted.org/packages/47/16/52d75374d1012e8fc709216b5eaa25f471370e2a2331b8be00f18670a6c7/librt-0.8.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5cb11061a736a9db45e3c1293cfcb1e3caf205912dfa085734ba750f2197ff9a", size = 198941, upload-time = "2026-02-12T14:52:32.489Z" }, - { url = "https://files.pythonhosted.org/packages/fc/11/d5dd89e5a2228567b1228d8602d896736247424484db086eea6b8010bcba/librt-0.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4bb00bd71b448f16749909b08a0ff16f58b079e2261c2e1000f2bbb2a4f0a45", size = 210009, upload-time = "2026-02-12T14:52:33.634Z" }, - { url = "https://files.pythonhosted.org/packages/49/d8/fc1a92a77c3020ee08ce2dc48aed4b42ab7c30fb43ce488d388673b0f164/librt-0.8.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95a719a049f0eefaf1952673223cf00d442952273cbd20cf2ed7ec423a0ef58d", size = 224461, upload-time = "2026-02-12T14:52:34.868Z" }, - { url = "https://files.pythonhosted.org/packages/7f/98/eb923e8b028cece924c246104aa800cf72e02d023a8ad4ca87135b05a2fe/librt-0.8.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bd32add59b58fba3439d48d6f36ac695830388e3da3e92e4fc26d2d02670d19c", size = 217538, upload-time = "2026-02-12T14:52:36.078Z" }, - { url = "https://files.pythonhosted.org/packages/fd/67/24e80ab170674a1d8ee9f9a83081dca4635519dbd0473b8321deecddb5be/librt-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4f764b2424cb04524ff7a486b9c391e93f93dc1bd8305b2136d25e582e99aa2f", size = 225110, upload-time = "2026-02-12T14:52:37.301Z" }, - { url = "https://files.pythonhosted.org/packages/d8/c7/6fbdcbd1a6e5243c7989c21d68ab967c153b391351174b4729e359d9977f/librt-0.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f04ca50e847abc486fa8f4107250566441e693779a5374ba211e96e238f298b9", size = 217758, upload-time = "2026-02-12T14:52:38.89Z" }, - { url = "https://files.pythonhosted.org/packages/4b/bd/4d6b36669db086e3d747434430073e14def032dd58ad97959bf7e2d06c67/librt-0.8.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9ab3a3475a55b89b87ffd7e6665838e8458e0b596c22e0177e0f961434ec474a", size = 218384, upload-time = "2026-02-12T14:52:40.637Z" }, - { url = "https://files.pythonhosted.org/packages/50/2d/afe966beb0a8f179b132f3e95c8dd90738a23e9ebdba10f89a3f192f9366/librt-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e36a8da17134ffc29373775d88c04832f9ecfab1880470661813e6c7991ef79", size = 241187, upload-time = "2026-02-12T14:52:43.55Z" }, - { url = "https://files.pythonhosted.org/packages/02/d0/6172ea4af2b538462785ab1a68e52d5c99cfb9866a7caf00fdf388299734/librt-0.8.0-cp312-cp312-win32.whl", hash = "sha256:4eb5e06ebcc668677ed6389164f52f13f71737fc8be471101fa8b4ce77baeb0c", size = 54914, upload-time = "2026-02-12T14:52:44.676Z" }, - { url = "https://files.pythonhosted.org/packages/d4/cb/ceb6ed6175612a4337ad49fb01ef594712b934b4bc88ce8a63554832eb44/librt-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:0a33335eb59921e77c9acc05d0e654e4e32e45b014a4d61517897c11591094f8", size = 62020, upload-time = "2026-02-12T14:52:45.676Z" }, - { url = "https://files.pythonhosted.org/packages/f1/7e/61701acbc67da74ce06ddc7ba9483e81c70f44236b2d00f6a4bfee1aacbf/librt-0.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:24a01c13a2a9bdad20997a4443ebe6e329df063d1978bbe2ebbf637878a46d1e", size = 52443, upload-time = "2026-02-12T14:52:47.218Z" }, - { url = "https://files.pythonhosted.org/packages/6d/32/3edb0bcb4113a9c8bdcd1750663a54565d255027657a5df9d90f13ee07fa/librt-0.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7f820210e21e3a8bf8fde2ae3c3d10106d4de9ead28cbfdf6d0f0f41f5b12fa1", size = 66522, upload-time = "2026-02-12T14:52:48.219Z" }, - { url = "https://files.pythonhosted.org/packages/30/ab/e8c3d05e281f5d405ebdcc5bc8ab36df23e1a4b40ac9da8c3eb9928b72b9/librt-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4831c44b8919e75ca0dfb52052897c1ef59fdae19d3589893fbd068f1e41afbf", size = 68658, upload-time = "2026-02-12T14:52:50.351Z" }, - { url = "https://files.pythonhosted.org/packages/7c/d3/74a206c47b7748bbc8c43942de3ed67de4c231156e148b4f9250869593df/librt-0.8.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:88c6e75540f1f10f5e0fc5e87b4b6c290f0e90d1db8c6734f670840494764af8", size = 199287, upload-time = "2026-02-12T14:52:51.938Z" }, - { url = "https://files.pythonhosted.org/packages/fa/29/ef98a9131cf12cb95771d24e4c411fda96c89dc78b09c2de4704877ebee4/librt-0.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9646178cd794704d722306c2c920c221abbf080fede3ba539d5afdec16c46dad", size = 210293, upload-time = "2026-02-12T14:52:53.128Z" }, - { url = "https://files.pythonhosted.org/packages/5b/3e/89b4968cb08c53d4c2d8b02517081dfe4b9e07a959ec143d333d76899f6c/librt-0.8.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e1af31a710e17891d9adf0dbd9a5fcd94901a3922a96499abdbf7ce658f4e01", size = 224801, upload-time = "2026-02-12T14:52:54.367Z" }, - { url = "https://files.pythonhosted.org/packages/6d/28/f38526d501f9513f8b48d78e6be4a241e15dd4b000056dc8b3f06ee9ce5d/librt-0.8.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:507e94f4bec00b2f590fbe55f48cd518a208e2474a3b90a60aa8f29136ddbada", size = 218090, upload-time = "2026-02-12T14:52:55.758Z" }, - { url = "https://files.pythonhosted.org/packages/02/ec/64e29887c5009c24dc9c397116c680caffc50286f62bd99c39e3875a2854/librt-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f1178e0de0c271231a660fbef9be6acdfa1d596803464706862bef6644cc1cae", size = 225483, upload-time = "2026-02-12T14:52:57.375Z" }, - { url = "https://files.pythonhosted.org/packages/ee/16/7850bdbc9f1a32d3feff2708d90c56fc0490b13f1012e438532781aa598c/librt-0.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:71fc517efc14f75c2f74b1f0a5d5eb4a8e06aa135c34d18eaf3522f4a53cd62d", size = 218226, upload-time = "2026-02-12T14:52:58.534Z" }, - { url = "https://files.pythonhosted.org/packages/1c/4a/166bffc992d65ddefa7c47052010a87c059b44a458ebaf8f5eba384b0533/librt-0.8.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:0583aef7e9a720dd40f26a2ad5a1bf2ccbb90059dac2b32ac516df232c701db3", size = 218755, upload-time = "2026-02-12T14:52:59.701Z" }, - { url = "https://files.pythonhosted.org/packages/da/5d/9aeee038bcc72a9cfaaee934463fe9280a73c5440d36bd3175069d2cb97b/librt-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5d0f76fc73480d42285c609c0ea74d79856c160fa828ff9aceab574ea4ecfd7b", size = 241617, upload-time = "2026-02-12T14:53:00.966Z" }, - { url = "https://files.pythonhosted.org/packages/64/ff/2bec6b0296b9d0402aa6ec8540aa19ebcb875d669c37800cb43d10d9c3a3/librt-0.8.0-cp313-cp313-win32.whl", hash = "sha256:e79dbc8f57de360f0ed987dc7de7be814b4803ef0e8fc6d3ff86e16798c99935", size = 54966, upload-time = "2026-02-12T14:53:02.042Z" }, - { url = "https://files.pythonhosted.org/packages/08/8d/bf44633b0182996b2c7ea69a03a5c529683fa1f6b8e45c03fe874ff40d56/librt-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:25b3e667cbfc9000c4740b282df599ebd91dbdcc1aa6785050e4c1d6be5329ab", size = 62000, upload-time = "2026-02-12T14:53:03.822Z" }, - { url = "https://files.pythonhosted.org/packages/5c/fd/c6472b8e0eac0925001f75e366cf5500bcb975357a65ef1f6b5749389d3a/librt-0.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:e9a3a38eb4134ad33122a6d575e6324831f930a771d951a15ce232e0237412c2", size = 52496, upload-time = "2026-02-12T14:53:04.889Z" }, - { url = "https://files.pythonhosted.org/packages/e0/13/79ebfe30cd273d7c0ce37a5f14dc489c5fb8b722a008983db2cfd57270bb/librt-0.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:421765e8c6b18e64d21c8ead315708a56fc24f44075059702e421d164575fdda", size = 66078, upload-time = "2026-02-12T14:53:06.085Z" }, - { url = "https://files.pythonhosted.org/packages/4b/8f/d11eca40b62a8d5e759239a80636386ef88adecb10d1a050b38cc0da9f9e/librt-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:48f84830a8f8ad7918afd743fd7c4eb558728bceab7b0e38fd5a5cf78206a556", size = 68309, upload-time = "2026-02-12T14:53:07.121Z" }, - { url = "https://files.pythonhosted.org/packages/9c/b4/f12ee70a3596db40ff3c88ec9eaa4e323f3b92f77505b4d900746706ec6a/librt-0.8.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9f09d4884f882baa39a7e36bbf3eae124c4ca2a223efb91e567381d1c55c6b06", size = 196804, upload-time = "2026-02-12T14:53:08.164Z" }, - { url = "https://files.pythonhosted.org/packages/8b/7e/70dbbdc0271fd626abe1671ad117bcd61a9a88cdc6a10ccfbfc703db1873/librt-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:693697133c3b32aa9b27f040e3691be210e9ac4d905061859a9ed519b1d5a376", size = 206915, upload-time = "2026-02-12T14:53:09.333Z" }, - { url = "https://files.pythonhosted.org/packages/79/13/6b9e05a635d4327608d06b3c1702166e3b3e78315846373446cf90d7b0bf/librt-0.8.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5512aae4648152abaf4d48b59890503fcbe86e85abc12fb9b096fe948bdd816", size = 221200, upload-time = "2026-02-12T14:53:10.68Z" }, - { url = "https://files.pythonhosted.org/packages/35/6c/e19a3ac53e9414de43a73d7507d2d766cd22d8ca763d29a4e072d628db42/librt-0.8.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:995d24caa6bbb34bcdd4a41df98ac6d1af637cfa8975cb0790e47d6623e70e3e", size = 214640, upload-time = "2026-02-12T14:53:12.342Z" }, - { url = "https://files.pythonhosted.org/packages/30/f0/23a78464788619e8c70f090cfd099cce4973eed142c4dccb99fc322283fd/librt-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b9aef96d7593584e31ef6ac1eb9775355b0099fee7651fae3a15bc8657b67b52", size = 221980, upload-time = "2026-02-12T14:53:13.603Z" }, - { url = "https://files.pythonhosted.org/packages/03/32/38e21420c5d7aa8a8bd2c7a7d5252ab174a5a8aaec8b5551968979b747bf/librt-0.8.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:4f6e975377fbc4c9567cb33ea9ab826031b6c7ec0515bfae66a4fb110d40d6da", size = 215146, upload-time = "2026-02-12T14:53:14.8Z" }, - { url = "https://files.pythonhosted.org/packages/bb/00/bd9ecf38b1824c25240b3ad982fb62c80f0a969e6679091ba2b3afb2b510/librt-0.8.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:daae5e955764be8fd70a93e9e5133c75297f8bce1e802e1d3683b98f77e1c5ab", size = 215203, upload-time = "2026-02-12T14:53:16.087Z" }, - { url = "https://files.pythonhosted.org/packages/b9/60/7559bcc5279d37810b98d4a52616febd7b8eef04391714fd6bdf629598b1/librt-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7bd68cebf3131bb920d5984f75fe302d758db33264e44b45ad139385662d7bc3", size = 237937, upload-time = "2026-02-12T14:53:17.236Z" }, - { url = "https://files.pythonhosted.org/packages/41/cc/be3e7da88f1abbe2642672af1dc00a0bccece11ca60241b1883f3018d8d5/librt-0.8.0-cp314-cp314-win32.whl", hash = "sha256:1e6811cac1dcb27ca4c74e0ca4a5917a8e06db0d8408d30daee3a41724bfde7a", size = 50685, upload-time = "2026-02-12T14:53:18.888Z" }, - { url = "https://files.pythonhosted.org/packages/38/27/e381d0df182a8f61ef1f6025d8b138b3318cc9d18ad4d5f47c3bf7492523/librt-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:178707cda89d910c3b28bf5aa5f69d3d4734e0f6ae102f753ad79edef83a83c7", size = 57872, upload-time = "2026-02-12T14:53:19.942Z" }, - { url = "https://files.pythonhosted.org/packages/c5/0c/ca9dfdf00554a44dea7d555001248269a4bab569e1590a91391feb863fa4/librt-0.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:3e8b77b5f54d0937b26512774916041756c9eb3e66f1031971e626eea49d0bf4", size = 48056, upload-time = "2026-02-12T14:53:21.473Z" }, - { url = "https://files.pythonhosted.org/packages/f2/ed/6cc9c4ad24f90c8e782193c7b4a857408fd49540800613d1356c63567d7b/librt-0.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:789911e8fa40a2e82f41120c936b1965f3213c67f5a483fc5a41f5839a05dcbb", size = 68307, upload-time = "2026-02-12T14:53:22.498Z" }, - { url = "https://files.pythonhosted.org/packages/84/d8/0e94292c6b3e00b6eeea39dd44d5703d1ec29b6dafce7eea19dc8f1aedbd/librt-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2b37437e7e4ef5e15a297b36ba9e577f73e29564131d86dd75875705e97402b5", size = 70999, upload-time = "2026-02-12T14:53:23.603Z" }, - { url = "https://files.pythonhosted.org/packages/0e/f4/6be1afcbdeedbdbbf54a7c9d73ad43e1bf36897cebf3978308cd64922e02/librt-0.8.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:671a6152edf3b924d98a5ed5e6982ec9cb30894085482acadce0975f031d4c5c", size = 220782, upload-time = "2026-02-12T14:53:25.133Z" }, - { url = "https://files.pythonhosted.org/packages/f0/8d/f306e8caa93cfaf5c6c9e0d940908d75dc6af4fd856baa5535c922ee02b1/librt-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8992ca186a1678107b0af3d0c9303d8c7305981b9914989b9788319ed4d89546", size = 235420, upload-time = "2026-02-12T14:53:27.047Z" }, - { url = "https://files.pythonhosted.org/packages/d6/f2/65d86bd462e9c351326564ca805e8457442149f348496e25ccd94583ffa2/librt-0.8.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:001e5330093d887b8b9165823eca6c5c4db183fe4edea4fdc0680bbac5f46944", size = 246452, upload-time = "2026-02-12T14:53:28.341Z" }, - { url = "https://files.pythonhosted.org/packages/03/94/39c88b503b4cb3fcbdeb3caa29672b6b44ebee8dcc8a54d49839ac280f3f/librt-0.8.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d920789eca7ef71df7f31fd547ec0d3002e04d77f30ba6881e08a630e7b2c30e", size = 238891, upload-time = "2026-02-12T14:53:29.625Z" }, - { url = "https://files.pythonhosted.org/packages/e3/c6/6c0d68190893d01b71b9569b07a1c811e280c0065a791249921c83dc0290/librt-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:82fb4602d1b3e303a58bfe6165992b5a78d823ec646445356c332cd5f5bbaa61", size = 250249, upload-time = "2026-02-12T14:53:30.93Z" }, - { url = "https://files.pythonhosted.org/packages/52/7a/f715ed9e039035d0ea637579c3c0155ab3709a7046bc408c0fb05d337121/librt-0.8.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:4d3e38797eb482485b486898f89415a6ab163bc291476bd95712e42cf4383c05", size = 240642, upload-time = "2026-02-12T14:53:32.174Z" }, - { url = "https://files.pythonhosted.org/packages/c2/3c/609000a333debf5992efe087edc6467c1fdbdddca5b610355569bbea9589/librt-0.8.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a905091a13e0884701226860836d0386b88c72ce5c2fdfba6618e14c72be9f25", size = 239621, upload-time = "2026-02-12T14:53:33.39Z" }, - { url = "https://files.pythonhosted.org/packages/b9/df/87b0673d5c395a8f34f38569c116c93142d4dc7e04af2510620772d6bd4f/librt-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:375eda7acfce1f15f5ed56cfc960669eefa1ec8732e3e9087c3c4c3f2066759c", size = 262986, upload-time = "2026-02-12T14:53:34.617Z" }, - { url = "https://files.pythonhosted.org/packages/09/7f/6bbbe9dcda649684773aaea78b87fff4d7e59550fbc2877faa83612087a3/librt-0.8.0-cp314-cp314t-win32.whl", hash = "sha256:2ccdd20d9a72c562ffb73098ac411de351b53a6fbb3390903b2d33078ef90447", size = 51328, upload-time = "2026-02-12T14:53:36.15Z" }, - { url = "https://files.pythonhosted.org/packages/bb/f3/e1981ab6fa9b41be0396648b5850267888a752d025313a9e929c4856208e/librt-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:25e82d920d4d62ad741592fcf8d0f3bda0e3fc388a184cb7d2f566c681c5f7b9", size = 58719, upload-time = "2026-02-12T14:53:37.183Z" }, - { url = "https://files.pythonhosted.org/packages/94/d1/433b3c06e78f23486fe4fdd19bc134657eb30997d2054b0dbf52bbf3382e/librt-0.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:92249938ab744a5890580d3cb2b22042f0dce71cdaa7c1369823df62bedf7cbc", size = 48753, upload-time = "2026-02-12T14:53:38.539Z" }, +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/40/08/9e7f6b5d2b5bed6ad055cdd5925f192bb403a51280f86b56554d9d0699a2/librt-0.11.0.tar.gz", hash = "sha256:075dc3ef4458a278e0195cbf6ac9d38808d9b906c5a6c7f7f79c3888276a3fb1", size = 200139, upload-time = "2026-05-10T18:17:25.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/d0/07c77e067f0838949b43bd89232c29d72efebb9d2801a9750184eb706b71/librt-0.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b87504f1690a23b9a2cca841191a04f83895d4fc2dd04df91d82b1a04ca2ad46", size = 144147, upload-time = "2026-05-10T18:15:53.227Z" }, + { url = "https://files.pythonhosted.org/packages/7a/24/8493538fa4f62f982686398a5b8f68008138a75086abdea19ade64bf4255/librt-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40071fc5fe0ce8daa6de616702314a01e1250711682b0523d6ab8d4525910cb3", size = 143614, upload-time = "2026-05-10T18:15:54.657Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1e/f8bad050810d9171f34a1648ed910e56814c2ba61639f2bd53c6377ae24b/librt-0.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:137e79445c896a0ea7b265f52d23954e05b64222ee1af69e2cb34219067cbb67", size = 485538, upload-time = "2026-05-10T18:15:56.117Z" }, + { url = "https://files.pythonhosted.org/packages/c0/fe/3594ebfbaf03084ba4b120c9ba5c3183fd938a48725e9bbe6ff0a5159ad8/librt-0.11.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:cca6644054e78746d8d4ef238681f9c34ff8b584fe6b988ecebb8db3b15e622a", size = 479623, upload-time = "2026-05-10T18:15:57.544Z" }, + { url = "https://files.pythonhosted.org/packages/b0/da/5d1876984b3746c85dbd219dbfcb73c85f54ee263fd32e5b2a632ec14571/librt-0.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5b0eea49f5562861ee8d757a32ef7d559c1d35be2aaaa1ec28941d74c9ffc8a", size = 513082, upload-time = "2026-05-10T18:15:58.805Z" }, + { url = "https://files.pythonhosted.org/packages/19/6e/55bdf5d5ca00c3e18430690bf2c953d8d3ffd3c337418173d33dec985dc9/librt-0.11.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0d1029d7e1ae1a7e647ed6fb5df8c4ce2dffefb7a9f5fd1376a4554d96dac09f", size = 508105, upload-time = "2026-05-10T18:16:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/f1f23a7c595ee90ece4d35c851e5d104b1311a887ed1b4ac4c35bbd13da8/librt-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bc3ce6b33c5828d9e80592011a5c584cb2ce86edbc4088405f70da47dc1d1b3b", size = 522268, upload-time = "2026-05-10T18:16:01.708Z" }, + { url = "https://files.pythonhosted.org/packages/b6/02/5720f5697a7f54b78b3aefbe20df3a48cedcff1276618c4aa481177942ed/librt-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:936c5995f3514a42111f20099397d8177c79b4d7e70961e396c6f5a0a3566766", size = 527348, upload-time = "2026-05-10T18:16:03.496Z" }, + { url = "https://files.pythonhosted.org/packages/50/db/b4a47c6f91db4ff76348a0b3dd0cc65e090a078b765a810a62ff9434c3d3/librt-0.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9bc0ca6ad9381cbe8e4aa6e5726e4c80c78115a6e9723c599ed1d73e092bc49d", size = 516294, upload-time = "2026-05-10T18:16:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/9e/58/9384b2f4eb1ed1d273d40948a7c5c4b2360213b402ef3be4641c06299f9c/librt-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:070aa8c26c0a74774317a72df8851facc7f0f012a5b406557ac56992d92e1ec8", size = 553608, upload-time = "2026-05-10T18:16:06.839Z" }, + { url = "https://files.pythonhosted.org/packages/21/7b/5aa8848a7c6a9278c79375146da1812e695754ceec5f005e6043461a7315/librt-0.11.0-cp312-cp312-win32.whl", hash = "sha256:6bf14feb84b05ae945277395451998c89c54d0def4070eb5c08de544930b245a", size = 101879, upload-time = "2026-05-10T18:16:08.103Z" }, + { url = "https://files.pythonhosted.org/packages/37/33/8a745436944947575b584231750a41417de1a38cf6a2e9251d1065651c09/librt-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:75672f0bc524ede266287d532d7923dbce94c7514ad07627bac3d0c6d92cc4d9", size = 119831, upload-time = "2026-05-10T18:16:09.174Z" }, + { url = "https://files.pythonhosted.org/packages/59/67/a6739ac96e28b7855808bdb0370e250606104a859750d209e5a0716fe7ab/librt-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:2f10cf143e4a9bb0f4f5af568a00df94a2d69ef41c2579584454bb0fe5cc642c", size = 103470, upload-time = "2026-05-10T18:16:10.369Z" }, + { url = "https://files.pythonhosted.org/packages/82/61/e59168d4d0bf2bf90f4f0caf7a001bfc60254c3af4586013b04dc3ef517b/librt-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:78dc31f7fdfe9c9d0eb0e8f42d139db230e826415bbcabd9f0e9faaaee909894", size = 144119, upload-time = "2026-05-10T18:16:11.771Z" }, + { url = "https://files.pythonhosted.org/packages/61/fd/caa1d60b12f7dd79ccea23054e06eeaebe266a5f52c40a6b651069200ce5/librt-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fa475675db22290c3158e1d42326d0f5a65f04f44a0e68c3630a25b53560fb9c", size = 143565, upload-time = "2026-05-10T18:16:13.334Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a9/dc744f5c2b4978d48db970be29f22716d3413d28b14ad99740817315cf2c/librt-0.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:621db29691044bdeda22e789e482e1b0f3a985d90e3426c9c6d17606416205ea", size = 485395, upload-time = "2026-05-10T18:16:14.729Z" }, + { url = "https://files.pythonhosted.org/packages/8f/21/7f8e97a1e4dae952a5a95948f6f8507a173bc1e669f54340bba6ca1ca31b/librt-0.11.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a9010e2ed5b3a9e158c5fd966b3ab7e834bb3d3aacc8f66c91dd4b57a3799230", size = 479383, upload-time = "2026-05-10T18:16:16.321Z" }, + { url = "https://files.pythonhosted.org/packages/a6/6d/d8ee9c114bebf2c50e29ec2aa940826fccb62a645c3e4c18760987d0e16d/librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c39513d8b7477a2e1ed8c43fc21c524e8d5a0f8d4e8b7b074dbdbe7820a08e2", size = 513010, upload-time = "2026-05-10T18:16:17.647Z" }, + { url = "https://files.pythonhosted.org/packages/f0/43/0b5708af2bd30a46400e72ba6bdaa8f066f15fb9a688527e34220e8d6c06/librt-0.11.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7aef3cf1d5af86e770ab04bfd993dfc4ae8b8c17f66fb77dd4a7d50de7bbb1a3", size = 508433, upload-time = "2026-05-10T18:16:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/4a/50/356187247d09013490481033183b3532b58acf8028bcb34b2b56a375c9b2/librt-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:557183ddc36babe46b27dd60facbd5adb4492181a5be887587d57cda6e092f21", size = 522595, upload-time = "2026-05-10T18:16:20.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/e7/c6ac4240899c7f3248079d5a9900debe0dadb3fdeaf856684c987105ba47/librt-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83d3e1f72bd42f6c5c0b7daec530c3f829bd02db42c70b8ddf0c2d90a2459930", size = 527255, upload-time = "2026-05-10T18:16:22.352Z" }, + { url = "https://files.pythonhosted.org/packages/eb/b5/a81322dbeedeeaf9c1ee6f001734d28a09d8383ac9e6779bc24bbd0743c6/librt-0.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4ce1f21fbe589bc1afd7872dece84fb0e1144f794a288e58a10d2c54a55c43be", size = 516847, upload-time = "2026-05-10T18:16:23.627Z" }, + { url = "https://files.pythonhosted.org/packages/ae/66/6e6323787d592b55204a42595ff1102da5115601b53a7e9ddebc889a6da5/librt-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b09f7044ea2b64c9da42fd3d335666518cfd1c6e8a182c95da73d0214b41e", size = 553920, upload-time = "2026-05-10T18:16:25.025Z" }, + { url = "https://files.pythonhosted.org/packages/9c/21/623f8ca230857102066d9ca8c6c1734995908c4d0d1bee7bb2ef0021cb33/librt-0.11.0-cp313-cp313-win32.whl", hash = "sha256:78fddc31cd4d3caa897ad5d31f856b1faadc9474021ad6cb182b9018793e254e", size = 101898, upload-time = "2026-05-10T18:16:26.649Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1d/b4ebd44dd723f768469007515cb92251e0ae286c94c140f374801140fa74/librt-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ca8aa88751a775870b764e93bad5135385f563cb8dcee399abf034ea4d3cb47", size = 119812, upload-time = "2026-05-10T18:16:27.859Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e4/b2f4ca7965ca373b491cdb4bc25cdb30c1649ca81a8782056a83850292a9/librt-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:96f044bb325fd9cf1a723015638c219e9143f0dfbc0ca54c565df2b7fc748b44", size = 103448, upload-time = "2026-05-10T18:16:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/29/eb/dbce197da4e227779e56b5735f2decc3eb36e55a1cdbf1bd65d6639d76c1/librt-0.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4a017a95e5837dc15a8c5661d60e05daa96b90908b1aa6b7acdf443cd25c8ebd", size = 143345, upload-time = "2026-05-10T18:16:30.674Z" }, + { url = "https://files.pythonhosted.org/packages/76/a3/254bebd0c11c8ba684018efb8006ff22e466abce445215cca6c778e7d9de/librt-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ecbd9819deccc39b7542bf4d2a740d8a620694d39989e58661d3763458f8d4", size = 143131, upload-time = "2026-05-10T18:16:32.037Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3f/f77d6122d21ac7bf6ae8a7dfced1bd2a7ac545d3273ebdcaf8042f6d619f/librt-0.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7da327dacd7be8f8ec36547373550744a3cc0e536d54665cd83f8bcd961200e8", size = 477024, upload-time = "2026-05-10T18:16:33.493Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0a/2c996dadebaa7d9bbbd43ef2d4f3e66b6da545f838a41694ef6172cebec8/librt-0.11.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0dc56b1f8d06e60db362cc3fdae206681817f86ce4725d34511473487f12a34b", size = 474221, upload-time = "2026-05-10T18:16:34.864Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7e/f5d92af8486b8272c23b3e686b46ff72d89c8169585eb61eef01a2ac7147/librt-0.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05fb8fb2ab90e21c8d12ea240d744ad514da9baf381ebfa70d91d20d21713175", size = 505174, upload-time = "2026-05-10T18:16:36.705Z" }, + { url = "https://files.pythonhosted.org/packages/af/1a/cb0734fe86398eb33193ab753b7326255c74cac5eb09e76b9b16536e7adb/librt-0.11.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cae74872be221df4374d10fec61f93ed1513b9546ea84f2c0bf73ab3e9bd0b03", size = 497216, upload-time = "2026-05-10T18:16:38.418Z" }, + { url = "https://files.pythonhosted.org/packages/18/06/094820f91558b66e29943c0ec41c9914f460f48dd51fc503c3101e10842d/librt-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32bcc918c0148eb7e3d57385125bac7e5f9e4359d05f07448b09f6f778c2f31c", size = 513921, upload-time = "2026-05-10T18:16:39.848Z" }, + { url = "https://files.pythonhosted.org/packages/0b/c2/00de9018871a282f530cacb457d5ec0428f6ac7e6fedde9aff7468d9fb04/librt-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f9743fc99135d5f78d2454435615f6dec0473ca507c26ce9d92b10b562a280d3", size = 520850, upload-time = "2026-05-10T18:16:41.471Z" }, + { url = "https://files.pythonhosted.org/packages/51/9d/64631832348fd1834fb3a61b996434edddaaf25a31d03b0a76273159d2cf/librt-0.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5ba067f4aadae8fda802d91d2124c90c42195ff32d9161d3549e6d05cfe26f96", size = 504237, upload-time = "2026-05-10T18:16:43.15Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ec/ae5525eb16edc827a044e7bb8777a455ff95d4bca9379e7e6bddd7383647/librt-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:de3bf945454d032f9e390b85c4072e0a0570bf825421c8be0e71209fa65e1abe", size = 546261, upload-time = "2026-05-10T18:16:44.408Z" }, + { url = "https://files.pythonhosted.org/packages/5a/09/adce371f27ca039411da9659f7430fcc2ba6cd0c7b3e4467a0f091be7fa9/librt-0.11.0-cp314-cp314-win32.whl", hash = "sha256:d2277a05f6dcb9fd13db9566aac4fabd68c3ea1ea46ee5567d4eef8efa495a2f", size = 96965, upload-time = "2026-05-10T18:16:46.039Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ee/8ac720d98548f173c7ce2e632a7ca94673f74cacd5c8162a84af5b35958a/librt-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ab73e8db5e3f564d812c1f5c3a175930a5f9bc96ccb5e3b22a34d7858b401cf7", size = 115151, upload-time = "2026-05-10T18:16:47.133Z" }, + { url = "https://files.pythonhosted.org/packages/94/20/c900cf14efeb09b6bef2b2dff20779f73464b97fd58d1c6bccc379588ae3/librt-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:aea3caa317752e3a466fa8af45d91ee0ea8c7fdd96e42b0a8dd9b76a7931eba1", size = 98850, upload-time = "2026-05-10T18:16:48.597Z" }, + { url = "https://files.pythonhosted.org/packages/0c/71/944bfe4b64e12abffcd3c15e1cce07f72f3d55655083786285f4dedeb532/librt-0.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d1b36540d7aaf9b9101b3a6f376c8d8e9f7a9aec93ed05918f2c69d493ffef72", size = 151138, upload-time = "2026-05-10T18:16:49.839Z" }, + { url = "https://files.pythonhosted.org/packages/b6/10/99e64a5c86989357fda078c8143c533389585f6473b7439172dd8f3b3b2d/librt-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:efbb343ab2ce3540f4ecbe6315d677ed70f37cd9a72b1e58066c918ca83acbaa", size = 151976, upload-time = "2026-05-10T18:16:51.062Z" }, + { url = "https://files.pythonhosted.org/packages/21/31/5072ad880946d83e5ea4147d6d018c78eefce85b77819b19bdd0ee229435/librt-0.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0dd688aab3f7914d3e6e5e3554978e0383312fb8e771d84be008a35b9ee548", size = 557927, upload-time = "2026-05-10T18:16:52.632Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8d/70b5fb7cfbab60edbe7381614ab985da58e144fbf465c86d44c95f43cdca/librt-0.11.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f5fb36b8c6c63fdcbb1d526d94c0d1331610d43f4118cc1beb4efef4f3faacb2", size = 539698, upload-time = "2026-05-10T18:16:53.934Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a3/ba3495a0b3edbd24a4cae0d1d3c64f39a9fc45d06e812101289b50c1a619/librt-0.11.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a9a237d13addb93715b6fee74023d5ee3469b53fce527626c0e088aa585805f", size = 577162, upload-time = "2026-05-10T18:16:55.589Z" }, + { url = "https://files.pythonhosted.org/packages/f7/db/36e25fb81f99937ff1b96612a1dc9fd66f039cb9cc3aee12c01fac31aab9/librt-0.11.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5ddd17bd87b2c56ddd60e546a7984a2e64c4e8eab92fb4cf3830a48ad5469d51", size = 566494, upload-time = "2026-05-10T18:16:56.975Z" }, + { url = "https://files.pythonhosted.org/packages/33/0d/3f622b47f0b013eeb9cf4cc07ae9bfe378d832a4eec998b2b209fe84244d/librt-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd43992b4473d42f12ff9e68326079f0696d9d4e6000e8f39a0238d482ba6ee2", size = 596858, upload-time = "2026-05-10T18:16:58.374Z" }, + { url = "https://files.pythonhosted.org/packages/a9/02/71b90bc93039c46a2000651f6ad60122b114c8f54c4ad306e0e96f5b75ad/librt-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f8e3e8056dd674e279741485e2e512d6e9a751c7455809d0114e6ebf8d781085", size = 590318, upload-time = "2026-05-10T18:16:59.676Z" }, + { url = "https://files.pythonhosted.org/packages/04/04/418cb3f75621e2b761fb1ab0f017f4d70a1a72a6e7c74ee4f7e8d198c2f3/librt-0.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c1f708d8ae9c56cf38a903c44297243d2ec83fd82b396b977e0144a3e76217e3", size = 575115, upload-time = "2026-05-10T18:17:01.007Z" }, + { url = "https://files.pythonhosted.org/packages/cc/2c/5a2183ac58dd911f26b5d7e7d7d8f1d87fcecdddd99d6c12169a258ff62c/librt-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0add982e0e7b9fc14cf4b33789d5f13f66581889b88c2f58099f6ce8f92617bd", size = 617918, upload-time = "2026-05-10T18:17:02.682Z" }, + { url = "https://files.pythonhosted.org/packages/15/1f/dc6771a52592a4451be6effa200cbfc9cec61e4393d3033d81a9d307961d/librt-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:2b481d846ac894c4e8403c5fd0e87c5d11d6499e404b474602508a224ff531c8", size = 103562, upload-time = "2026-05-10T18:17:03.99Z" }, + { url = "https://files.pythonhosted.org/packages/62/4a/7d1415567027286a75ba1093ec4aca11f073e0f559c530cf3e0a757ad55c/librt-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:28edb433edde181112a908c78907af28f964eabc15f4dd16c9d66c834302677c", size = 124327, upload-time = "2026-05-10T18:17:05.465Z" }, + { url = "https://files.pythonhosted.org/packages/ce/62/b40b382fa0c66fee1478073eb8db352a4a6beda4a1adccf1df911d8c289c/librt-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dee008f20b542e3cd162ba338a7f9ec0f6d23d395f66fe8aeeec3c9d067ea253", size = 102572, upload-time = "2026-05-10T18:17:06.809Z" }, +] + +[[package]] +name = "linkify-it-py" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "uc-micro-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/c9/06ea13676ef354f0af6169587ae292d3e2406e212876a413bf9eece4eb23/linkify_it_py-2.1.0.tar.gz", hash = "sha256:43360231720999c10e9328dc3691160e27a718e280673d444c38d7d3aaa3b98b", size = 29158, upload-time = "2026-03-01T07:48:47.683Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/de/88b3be5c31b22333b3ca2f6ff1de4e863d8fe45aaea7485f591970ec1d3e/linkify_it_py-2.1.0-py3-none-any.whl", hash = "sha256:0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e", size = 19878, upload-time = "2026-03-01T07:48:46.098Z" }, ] [[package]] @@ -516,6 +606,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, ] +[package.optional-dependencies] +linkify = [ + { name = "linkify-it-py" }, +] + [[package]] name = "mcp" version = "1.25.0" @@ -541,6 +636,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e2/fc/6dc7659c2ae5ddf280477011f4213a74f806862856b796ef08f028e664bf/mcp-1.25.0-py3-none-any.whl", hash = "sha256:b37c38144a666add0862614cc79ec276e97d72aa8ca26d622818d4e278b9721a", size = 233076, upload-time = "2025-12-19T10:19:55.416Z" }, ] +[[package]] +name = "mdit-py-plugins" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d", size = 66663, upload-time = "2026-05-13T09:03:37.76Z" }, +] + [[package]] name = "mdurl" version = "0.1.2" @@ -561,35 +668,46 @@ wheels = [ [[package]] name = "mypy" -version = "1.19.1" +version = "2.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "ast-serialize" }, { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, { name = "mypy-extensions" }, { name = "pathspec" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, - { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, - { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, - { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, - { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, - { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, - { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, - { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, - { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, - { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, - { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, - { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, - { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, - { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, - { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, - { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, - { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, - { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/82/15/cca9d88503549ed6fedeaa1d448cdddd542ee8a490232d732e278036fbf2/mypy-2.1.0.tar.gz", hash = "sha256:81e76ad12c2d804512e9b13240d1588316531bfba07558286078bfbce9613633", size = 3898359, upload-time = "2026-05-11T18:37:36.237Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/b1/55861beb5c339b44f9a2ba92df9e2cb1eeb4ae1eee674cdf7772c797778b/mypy-2.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:244358bf1c0da7722230bce60683d52e8e9fd030554926f15b747a84efb5b3af", size = 14874381, upload-time = "2026-05-11T18:37:31.784Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b3/b7f770114b7d0ac92d0f76e8d93c2780844a70488a90e91821927850da86/mypy-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ec7c57657493c7a75534df2751c8ae2cda383c16ecc55d2106c54476b1b16f6", size = 13665501, upload-time = "2026-05-11T18:34:23.063Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f3/8ae2037967e2126689a0c11d99e2b707134a565191e92c60ca2572aec60a/mypy-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8161b6ff4392410023224f0969d17db93e1e154bc3e4ba62598e720723ae211", size = 14045750, upload-time = "2026-05-11T18:31:48.151Z" }, + { url = "https://files.pythonhosted.org/packages/a0/32/615eb5911859e43d054941b0d0a7d06cfa2870eba86529cf385b052b111c/mypy-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf03e12003084a67395184d3eb8cbd6a489dc3655b5664b28c210a9e2403ab0b", size = 15061630, upload-time = "2026-05-11T18:37:06.898Z" }, + { url = "https://files.pythonhosted.org/packages/d4/03/4eafbfff8bfab1b87082741eae6e6a624028c984e6708b73bce2a8570c9d/mypy-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:20509760fd791c51579d573153407d226385ec1f8bcce55d730b354f3336bc22", size = 15288831, upload-time = "2026-05-11T18:31:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/919661478e5891a3c96e549c036e467e64563ab85995b10c53c8358e16a3/mypy-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:6753d0c1fdd6b1a23b9e4f283ce80b2153b724adcb2653b20b85a8a28ac6436b", size = 11135228, upload-time = "2026-05-11T18:34:31.23Z" }, + { url = "https://files.pythonhosted.org/packages/24/0a/6a12b9782ca0831a553192f351679f4548abc9d19a7cc93bb7feb02084c7/mypy-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:98ebb6589bb3b6d0c6f0c459d53ca55b8091fbc13d277c4041c885392e8195e8", size = 10040684, upload-time = "2026-05-11T18:36:48.199Z" }, + { url = "https://files.pythonhosted.org/packages/6e/dd/c7191469c777f07689c032a8f7326e393ea34c92d6d76eb7ce5ba57ea66d/mypy-2.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35aac3bb114e03888f535d5eb51b8bafbb3266586b599da1940f9b1be3ec5bd5", size = 14852174, upload-time = "2026-05-11T18:31:38.929Z" }, + { url = "https://files.pythonhosted.org/packages/55/8c/aed55408879043d72bb9135f4d0d19a02b886dd569631e113e3d2706cb8d/mypy-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8de55a8c861f2a49331f807be98d90caeceeef520bde13d43a160207f8af613e", size = 13651542, upload-time = "2026-05-11T18:36:04.636Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8e/f371a824b1f1fa8ea6e3dbb8703d232977d572be2329554a3bc4d960302f/mypy-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fdf2941a07434af755837d9880f7d7d25f1dacb1af9dcd4b9b66f2220a3024e", size = 14033929, upload-time = "2026-05-11T18:35:55.742Z" }, + { url = "https://files.pythonhosted.org/packages/94/21/f54be870d6dd53a82c674407e0f8eed7174b05ec78d42e5abd7b42e84fd5/mypy-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e195b817c13f02352a9c124301f9f30f078405444679b6753c1b96b6eed37285", size = 15039200, upload-time = "2026-05-11T18:33:10.281Z" }, + { url = "https://files.pythonhosted.org/packages/17/99/bf21748626a40ce59fd29a39386ab46afec88b7bd2f0fa6c3a97c995523f/mypy-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5431d42af987ebd92ba2f71d45c85ed41d8e6ca9f5fd209a69f68f707d2469e5", size = 15272690, upload-time = "2026-05-11T18:32:07.205Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d7/9e90d2cf47100bea550ed2bc7b0d4de3a62181d84d5e37da0003e8462637/mypy-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:767fe8c66dc3e01e19e1737d4c38ebefead16125e1b8e58ad421903b376f5c65", size = 11147435, upload-time = "2026-05-11T18:33:56.477Z" }, + { url = "https://files.pythonhosted.org/packages/ec/46/e5c449e858798e35ffc90946282a27c62a77be743fe17480e4977374eb91/mypy-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:ecfe70d43775ab99562ab128ce49854a362044c9f894961f68f898c23cb7429d", size = 10035052, upload-time = "2026-05-11T18:32:30.049Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ca/b279a672e874aedd5498ae25f722dacc8aa86bbffb939b3f97cbb1cf6686/mypy-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7354c5a7f69d9345c3d6e69921d57088eea3ddeeb6b20d34c1b3855b02c36ec2", size = 14848422, upload-time = "2026-05-11T18:35:45.984Z" }, + { url = "https://files.pythonhosted.org/packages/27/e6/3efe56c631d959b9b4454e208b0ac4b7f4f58b404c89f8bec7b49efdfc21/mypy-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:49890d4f76ac9e06ec117f9e09f3174da70a620a0c300953d8595c926e80947f", size = 13677374, upload-time = "2026-05-11T18:36:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/84/7f/8107ea87a44fd1f1b59882442f033c9c3488c127201b1d1d15f1cbd6022e/mypy-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:761be68e023ef5d94678772396a8af1220030f80837a3afd8d0aef3b419666f4", size = 14055743, upload-time = "2026-05-11T18:35:18.361Z" }, + { url = "https://files.pythonhosted.org/packages/51/4d/b6d34db183133b83761b9199a82d31557cdbb70a380d8c3b3438e11882a3/mypy-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c90345fc182dc363b891350457ec69c35140858538f38b4540845afcc32b1aef", size = 15020937, upload-time = "2026-05-11T18:34:59.618Z" }, + { url = "https://files.pythonhosted.org/packages/ff/d7/f08360c691d758acb02f45022c34d98b92892f4ea756644e1000d4b9f3d8/mypy-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b84802e7b5a6daf1f5e15bc9fcd7ddae77be13981ffab037f1c67bb84d67d135", size = 15253371, upload-time = "2026-05-11T18:36:41.081Z" }, + { url = "https://files.pythonhosted.org/packages/67/1b/09460a13719530a19bce27bd3bc8449e83569dd2ba7faf51c9c3c30c0b61/mypy-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:022c771234936ceac541ebaf836fe9e2abeb3f5e09aff21588fe543ff006fe21", size = 11326429, upload-time = "2026-05-11T18:34:13.526Z" }, + { url = "https://files.pythonhosted.org/packages/40/62/75dbf0f82f7b6680340efc614af29dd0b3c17b8a4f1cd09b8bd2fd6bc814/mypy-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:498207db725cec88829a6a5c2fc771205fd043719ef98bc49aba8fb9fc4e6d57", size = 10218799, upload-time = "2026-05-11T18:32:23.491Z" }, + { url = "https://files.pythonhosted.org/packages/b2/66/caca04ed7d972fb6eb6dd1ccd6df1de5c38fae8c5b3dc1c4e8e0d85ee6b9/mypy-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d5e5cad0efeba72b93cd17490cc0d69c5ac9ca132994fe3fb0314808aeeb83e", size = 15923458, upload-time = "2026-05-11T18:35:28.64Z" }, + { url = "https://files.pythonhosted.org/packages/ed/52/2d90cbe49d014b13ed7ff337930c30bad35893fe38a1e4641e756bb62191/mypy-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ff715050c127d724fd260a2e666e7747fdd83511c0c47d449d98238970aef780", size = 14757697, upload-time = "2026-05-11T18:36:14.208Z" }, + { url = "https://files.pythonhosted.org/packages/ac/37/d98f4a14e081b238992d0ed96b6d39c7cc0148c9699eb71eaa68629665ea/mypy-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82208da9e09414d520e912d3e462d454854bed0810b71540bb016dcbca7308fd", size = 15405638, upload-time = "2026-05-11T18:33:48.249Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c2/15c46613b24a84fad2aea1248bf9619b99c2767ae9071fe224c179a0b7d4/mypy-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e79ebc1b904b84f0310dff7469655a9c36c7a68bddb37bdd42b67a332df61d08", size = 16215852, upload-time = "2026-05-11T18:32:50.296Z" }, + { url = "https://files.pythonhosted.org/packages/5c/90/9c16a57f482c76d25f6379762b56bbf65c711d8158cf271fb2802cfb0640/mypy-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e583edc957cfb0deb142079162ae826f58449b116c1d442f2d91c69d9fced081", size = 16452695, upload-time = "2026-05-11T18:33:38.182Z" }, + { url = "https://files.pythonhosted.org/packages/0f/4c/215a4eeb63cacc5f17f516691ea7285d11e249802b942476bff15922a314/mypy-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b33b6cd332695bba180d55e717a79d3038e479a2c49cc5eb3d53603409b9a5d7", size = 12866622, upload-time = "2026-05-11T18:34:39.945Z" }, + { url = "https://files.pythonhosted.org/packages/4b/50/1043e1db5f455ffe4c9ab22747cd8ca2bc492b1e4f4e21b130a44ee2b217/mypy-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4f910fe825376a7b66ef7ca8c98e5a149e8cd64c19ae71d84047a74ee060d4e6", size = 10610798, upload-time = "2026-05-11T18:36:31.444Z" }, + { url = "https://files.pythonhosted.org/packages/0d/2a/13ca1f292f6db1b98ff495ef3467736b331621c5917cad984b7043e7348d/mypy-2.1.0-py3-none-any.whl", hash = "sha256:a663814603a5c563fb87a4f96fb473eeb30d1f5a4885afcf44f9db000a366289", size = 2693302, upload-time = "2026-05-11T18:31:29.246Z" }, ] [[package]] @@ -602,12 +720,15 @@ wheels = [ ] [[package]] -name = "nodeenv" -version = "1.10.0" +name = "nexus-rpc" +version = "1.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/35/d5/cd1ffb202b76ebc1b33c1332a3416e55a39929006982adc2b1eb069aaa9b/nexus_rpc-1.4.0.tar.gz", hash = "sha256:3b8b373d4865671789cc43623e3dc0bcbf192562e40e13727e17f1c149050fba", size = 82367, upload-time = "2026-02-25T22:01:34.053Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, + { url = "https://files.pythonhosted.org/packages/11/52/6327a5f4fda01207205038a106a99848a41c83e933cd23ea2cab3d2ebc6c/nexus_rpc-1.4.0-py3-none-any.whl", hash = "sha256:14c953d3519113f8ccec533a9efdb6b10c28afef75d11cdd6d422640c40b3a49", size = 29645, upload-time = "2026-02-25T22:01:33.122Z" }, ] [[package]] @@ -647,6 +768,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, ] +[[package]] +name = "platformdirs" +version = "4.9.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -656,6 +786,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "protobuf" +version = "6.33.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" }, + { url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" }, + { url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" }, + { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, +] + [[package]] name = "pycparser" version = "2.23" @@ -788,19 +933,6 @@ crypto = [ { name = "cryptography" }, ] -[[package]] -name = "pyright" -version = "1.1.408" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "nodeenv" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/74/b2/5db700e52554b8f025faa9c3c624c59f1f6c8841ba81ab97641b54322f16/pyright-1.1.408.tar.gz", hash = "sha256:f28f2321f96852fa50b5829ea492f6adb0e6954568d1caa3f3af3a5f555eb684", size = 4400578, upload-time = "2026-01-08T08:07:38.795Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/82/a2c93e32800940d9573fb28c346772a14778b84ba7524e691b324620ab89/pyright-1.1.408-py3-none-any.whl", hash = "sha256:090b32865f4fdb1e0e6cd82bf5618480d48eecd2eb2e70f960982a3d9a4c17c1", size = 6399144, upload-time = "2026-01-08T08:07:37.082Z" }, -] - [[package]] name = "pytest" version = "9.0.2" @@ -974,27 +1106,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/04/dc/4e6ac71b511b141cf626357a3946679abeba4cf67bc7cc5a17920f31e10d/ruff-0.15.1.tar.gz", hash = "sha256:c590fe13fb57c97141ae975c03a1aedb3d3156030cabd740d6ff0b0d601e203f", size = 4540855, upload-time = "2026-02-12T23:09:09.998Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/23/bf/e6e4324238c17f9d9120a9d60aa99a7daaa21204c07fcd84e2ef03bb5fd1/ruff-0.15.1-py3-none-linux_armv6l.whl", hash = "sha256:b101ed7cf4615bda6ffe65bdb59f964e9f4a0d3f85cbf0e54f0ab76d7b90228a", size = 10367819, upload-time = "2026-02-12T23:09:03.598Z" }, - { url = "https://files.pythonhosted.org/packages/b3/ea/c8f89d32e7912269d38c58f3649e453ac32c528f93bb7f4219258be2e7ed/ruff-0.15.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:939c995e9277e63ea632cc8d3fae17aa758526f49a9a850d2e7e758bfef46602", size = 10798618, upload-time = "2026-02-12T23:09:22.928Z" }, - { url = "https://files.pythonhosted.org/packages/5e/0f/1d0d88bc862624247d82c20c10d4c0f6bb2f346559d8af281674cf327f15/ruff-0.15.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d83466455fdefe60b8d9c8df81d3c1bbb2115cede53549d3b522ce2bc703899", size = 10148518, upload-time = "2026-02-12T23:08:58.339Z" }, - { url = "https://files.pythonhosted.org/packages/f5/c8/291c49cefaa4a9248e986256df2ade7add79388fe179e0691be06fae6f37/ruff-0.15.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9457e3c3291024866222b96108ab2d8265b477e5b1534c7ddb1810904858d16", size = 10518811, upload-time = "2026-02-12T23:09:31.865Z" }, - { url = "https://files.pythonhosted.org/packages/c3/1a/f5707440e5ae43ffa5365cac8bbb91e9665f4a883f560893829cf16a606b/ruff-0.15.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92c92b003e9d4f7fbd33b1867bb15a1b785b1735069108dfc23821ba045b29bc", size = 10196169, upload-time = "2026-02-12T23:09:17.306Z" }, - { url = "https://files.pythonhosted.org/packages/2a/ff/26ddc8c4da04c8fd3ee65a89c9fb99eaa5c30394269d424461467be2271f/ruff-0.15.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fe5c41ab43e3a06778844c586251eb5a510f67125427625f9eb2b9526535779", size = 10990491, upload-time = "2026-02-12T23:09:25.503Z" }, - { url = "https://files.pythonhosted.org/packages/fc/00/50920cb385b89413f7cdb4bb9bc8fc59c1b0f30028d8bccc294189a54955/ruff-0.15.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66a6dd6df4d80dc382c6484f8ce1bcceb55c32e9f27a8b94c32f6c7331bf14fb", size = 11843280, upload-time = "2026-02-12T23:09:19.88Z" }, - { url = "https://files.pythonhosted.org/packages/5d/6d/2f5cad8380caf5632a15460c323ae326f1e1a2b5b90a6ee7519017a017ca/ruff-0.15.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a4a42cbb8af0bda9bcd7606b064d7c0bc311a88d141d02f78920be6acb5aa83", size = 11274336, upload-time = "2026-02-12T23:09:14.907Z" }, - { url = "https://files.pythonhosted.org/packages/a3/1d/5f56cae1d6c40b8a318513599b35ea4b075d7dc1cd1d04449578c29d1d75/ruff-0.15.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ab064052c31dddada35079901592dfba2e05f5b1e43af3954aafcbc1096a5b2", size = 11137288, upload-time = "2026-02-12T23:09:07.475Z" }, - { url = "https://files.pythonhosted.org/packages/cd/20/6f8d7d8f768c93b0382b33b9306b3b999918816da46537d5a61635514635/ruff-0.15.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5631c940fe9fe91f817a4c2ea4e81f47bee3ca4aa646134a24374f3c19ad9454", size = 11070681, upload-time = "2026-02-12T23:08:55.43Z" }, - { url = "https://files.pythonhosted.org/packages/9a/67/d640ac76069f64cdea59dba02af2e00b1fa30e2103c7f8d049c0cff4cafd/ruff-0.15.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68138a4ba184b4691ccdc39f7795c66b3c68160c586519e7e8444cf5a53e1b4c", size = 10486401, upload-time = "2026-02-12T23:09:27.927Z" }, - { url = "https://files.pythonhosted.org/packages/65/3d/e1429f64a3ff89297497916b88c32a5cc88eeca7e9c787072d0e7f1d3e1e/ruff-0.15.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:518f9af03bfc33c03bdb4cb63fabc935341bb7f54af500f92ac309ecfbba6330", size = 10197452, upload-time = "2026-02-12T23:09:12.147Z" }, - { url = "https://files.pythonhosted.org/packages/78/83/e2c3bade17dad63bf1e1c2ffaf11490603b760be149e1419b07049b36ef2/ruff-0.15.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:da79f4d6a826caaea95de0237a67e33b81e6ec2e25fc7e1993a4015dffca7c61", size = 10693900, upload-time = "2026-02-12T23:09:34.418Z" }, - { url = "https://files.pythonhosted.org/packages/a1/27/fdc0e11a813e6338e0706e8b39bb7a1d61ea5b36873b351acee7e524a72a/ruff-0.15.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3dd86dccb83cd7d4dcfac303ffc277e6048600dfc22e38158afa208e8bf94a1f", size = 11227302, upload-time = "2026-02-12T23:09:36.536Z" }, - { url = "https://files.pythonhosted.org/packages/f6/58/ac864a75067dcbd3b95be5ab4eb2b601d7fbc3d3d736a27e391a4f92a5c1/ruff-0.15.1-py3-none-win32.whl", hash = "sha256:660975d9cb49b5d5278b12b03bb9951d554543a90b74ed5d366b20e2c57c2098", size = 10462555, upload-time = "2026-02-12T23:09:29.899Z" }, - { url = "https://files.pythonhosted.org/packages/e0/5e/d4ccc8a27ecdb78116feac4935dfc39d1304536f4296168f91ed3ec00cd2/ruff-0.15.1-py3-none-win_amd64.whl", hash = "sha256:c820fef9dd5d4172a6570e5721704a96c6679b80cf7be41659ed439653f62336", size = 11599956, upload-time = "2026-02-12T23:09:01.157Z" }, - { url = "https://files.pythonhosted.org/packages/2a/07/5bda6a85b220c64c65686bc85bd0bbb23b29c62b3a9f9433fa55f17cda93/ruff-0.15.1-py3-none-win_arm64.whl", hash = "sha256:5ff7d5f0f88567850f45081fac8f4ec212be8d0b963e385c3f7d0d2eb4899416", size = 10874604, upload-time = "2026-02-12T23:09:05.515Z" }, +version = "0.8.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/00/089db7890ea3be5709e3ece6e46408d6f1e876026ec3fd081ee585fef209/ruff-0.8.6.tar.gz", hash = "sha256:dcad24b81b62650b0eb8814f576fc65cfee8674772a6e24c9b747911801eeaa5", size = 3473116, upload-time = "2025-01-04T12:23:00.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/28/aa07903694637c2fa394a9f4fe93cf861ad8b09f1282fa650ef07ff9fe97/ruff-0.8.6-py3-none-linux_armv6l.whl", hash = "sha256:defed167955d42c68b407e8f2e6f56ba52520e790aba4ca707a9c88619e580e3", size = 10628735, upload-time = "2025-01-04T12:21:53.632Z" }, + { url = "https://files.pythonhosted.org/packages/2b/43/827bb1448f1fcb0fb42e9c6edf8fb067ca8244923bf0ddf12b7bf949065c/ruff-0.8.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:54799ca3d67ae5e0b7a7ac234baa657a9c1784b48ec954a094da7c206e0365b1", size = 10386758, upload-time = "2025-01-04T12:22:00.349Z" }, + { url = "https://files.pythonhosted.org/packages/df/93/fc852a81c3cd315b14676db3b8327d2bb2d7508649ad60bfdb966d60738d/ruff-0.8.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e88b8f6d901477c41559ba540beeb5a671e14cd29ebd5683903572f4b40a9807", size = 10007808, upload-time = "2025-01-04T12:22:04.413Z" }, + { url = "https://files.pythonhosted.org/packages/94/e9/e0ed4af1794335fb280c4fac180f2bf40f6a3b859cae93a5a3ada27325ae/ruff-0.8.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0509e8da430228236a18a677fcdb0c1f102dd26d5520f71f79b094963322ed25", size = 10861031, upload-time = "2025-01-04T12:22:09.252Z" }, + { url = "https://files.pythonhosted.org/packages/82/68/da0db02f5ecb2ce912c2bef2aa9fcb8915c31e9bc363969cfaaddbc4c1c2/ruff-0.8.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:91a7ddb221779871cf226100e677b5ea38c2d54e9e2c8ed847450ebbdf99b32d", size = 10388246, upload-time = "2025-01-04T12:22:12.63Z" }, + { url = "https://files.pythonhosted.org/packages/ac/1d/b85383db181639019b50eb277c2ee48f9f5168f4f7c287376f2b6e2a6dc2/ruff-0.8.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:248b1fb3f739d01d528cc50b35ee9c4812aa58cc5935998e776bf8ed5b251e75", size = 11424693, upload-time = "2025-01-04T12:22:17.244Z" }, + { url = "https://files.pythonhosted.org/packages/ac/b7/30bc78a37648d31bfc7ba7105b108cb9091cd925f249aa533038ebc5a96f/ruff-0.8.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:bc3c083c50390cf69e7e1b5a5a7303898966be973664ec0c4a4acea82c1d4315", size = 12141921, upload-time = "2025-01-04T12:22:20.456Z" }, + { url = "https://files.pythonhosted.org/packages/60/b3/ee0a14cf6a1fbd6965b601c88d5625d250b97caf0534181e151504498f86/ruff-0.8.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52d587092ab8df308635762386f45f4638badb0866355b2b86760f6d3c076188", size = 11692419, upload-time = "2025-01-04T12:22:23.62Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d6/c597062b2931ba3e3861e80bd2b147ca12b3370afc3889af46f29209037f/ruff-0.8.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:61323159cf21bc3897674e5adb27cd9e7700bab6b84de40d7be28c3d46dc67cf", size = 12981648, upload-time = "2025-01-04T12:22:26.663Z" }, + { url = "https://files.pythonhosted.org/packages/68/84/21f578c2a4144917985f1f4011171aeff94ab18dfa5303ac632da2f9af36/ruff-0.8.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ae4478b1471fc0c44ed52a6fb787e641a2ac58b1c1f91763bafbc2faddc5117", size = 11251801, upload-time = "2025-01-04T12:22:29.59Z" }, + { url = "https://files.pythonhosted.org/packages/6c/aa/1ac02537c8edeb13e0955b5db86b5c050a1dcba54f6d49ab567decaa59c1/ruff-0.8.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0c000a471d519b3e6cfc9c6680025d923b4ca140ce3e4612d1a2ef58e11f11fe", size = 10849857, upload-time = "2025-01-04T12:22:33.536Z" }, + { url = "https://files.pythonhosted.org/packages/eb/00/020cb222252d833956cb3b07e0e40c9d4b984fbb2dc3923075c8f944497d/ruff-0.8.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:9257aa841e9e8d9b727423086f0fa9a86b6b420fbf4bf9e1465d1250ce8e4d8d", size = 10470852, upload-time = "2025-01-04T12:22:36.374Z" }, + { url = "https://files.pythonhosted.org/packages/00/56/e6d6578202a0141cd52299fe5acb38b2d873565f4670c7a5373b637cf58d/ruff-0.8.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:45a56f61b24682f6f6709636949ae8cc82ae229d8d773b4c76c09ec83964a95a", size = 10972997, upload-time = "2025-01-04T12:22:41.424Z" }, + { url = "https://files.pythonhosted.org/packages/be/31/dd0db1f4796bda30dea7592f106f3a67a8f00bcd3a50df889fbac58e2786/ruff-0.8.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:496dd38a53aa173481a7d8866bcd6451bd934d06976a2505028a50583e001b76", size = 11317760, upload-time = "2025-01-04T12:22:44.541Z" }, + { url = "https://files.pythonhosted.org/packages/d4/70/cfcb693dc294e034c6fed837fa2ec98b27cc97a26db5d049345364f504bf/ruff-0.8.6-py3-none-win32.whl", hash = "sha256:e169ea1b9eae61c99b257dc83b9ee6c76f89042752cb2d83486a7d6e48e8f764", size = 8799729, upload-time = "2025-01-04T12:22:49.016Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/ae6bcaa0edc83af42751bd193138bfb7598b2990939d3e40494d6c00698c/ruff-0.8.6-py3-none-win_amd64.whl", hash = "sha256:f1d70bef3d16fdc897ee290d7d20da3cbe4e26349f62e8a0274e7a3f4ce7a905", size = 9673857, upload-time = "2025-01-04T12:22:53.052Z" }, + { url = "https://files.pythonhosted.org/packages/91/f8/3765e053acd07baa055c96b2065c7fab91f911b3c076dfea71006666f5b0/ruff-0.8.6-py3-none-win_arm64.whl", hash = "sha256:7d7fc2377a04b6e04ffe588caad613d0c460eb2ecba4c0ccbbfe2bc973cbc162", size = 9149556, upload-time = "2025-01-04T12:22:57.173Z" }, ] [[package]] @@ -1032,6 +1164,42 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/18/c4/09985a03dba389d4fe16a9014147a7b02fa76ef3519bf5846462a485876d/starlette-0.51.0-py3-none-any.whl", hash = "sha256:fb460a3d6fd3c958d729fdd96aee297f89a51b0181f16401fe8fd4cb6129165d", size = 74133, upload-time = "2026-01-10T20:23:13.445Z" }, ] +[[package]] +name = "temporalio" +version = "1.27.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nexus-rpc" }, + { name = "protobuf" }, + { name = "types-protobuf" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/62/2bc1a9ad29382a3a99f088907ef2024a94420cfef340be1b33026c632828/temporalio-1.27.2.tar.gz", hash = "sha256:633bf2379492f3db1e887d1e64fdac00d9c2ddc3e9382b831d5af68256912e92", size = 2503041, upload-time = "2026-05-14T02:17:57.565Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/85/9da14f9fbdfae95435d29353bb1c55891581ad6b23c86ca56e72d83035ed/temporalio-1.27.2-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:860f706380faafec8f183f9194d0883c8033a4211c5d19c2c962c45b06cf99e9", size = 14602829, upload-time = "2026-05-14T02:17:45.624Z" }, + { url = "https://files.pythonhosted.org/packages/24/51/b7437991e71eea082dc53222da11f064974917cd59063ba57e13e5895fbc/temporalio-1.27.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:a8dc0c680e351f3132809861888d8326dbd5030dd4e570663597e7d4768d9502", size = 13997680, upload-time = "2026-05-14T02:17:53.968Z" }, + { url = "https://files.pythonhosted.org/packages/8c/5d/358065040e6f0cedbf669acd333622999eec737ff868ca7829d727b77746/temporalio-1.27.2-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:805f3de4d193dec52e040e41dbfc9ab44be0206d2e81142ceefaf7b7208058d1", size = 14252199, upload-time = "2026-05-14T02:17:36.972Z" }, + { url = "https://files.pythonhosted.org/packages/72/8a/85d2eab07c3e23fc1124203e76857c69ab9b22d8ccebad0835e294edb754/temporalio-1.27.2-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5bc996cb501b8a918f50037ccee6facb05bb70984acada4c2a3e01f5e7957a38", size = 14779945, upload-time = "2026-05-14T02:18:05.513Z" }, + { url = "https://files.pythonhosted.org/packages/67/81/c9b08609e2a92ecf62c97c59cabfa0608337c8d5cc9941eed5d9a7778840/temporalio-1.27.2-cp310-abi3-win_amd64.whl", hash = "sha256:62a84ae9a60c17932971e4ca3b0f3cd6f32f173b8183e759989376503fb95af6", size = 14981897, upload-time = "2026-05-14T02:17:27.333Z" }, +] + +[[package]] +name = "textual" +version = "8.2.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", extra = ["linkify"] }, + { name = "mdit-py-plugins" }, + { name = "platformdirs" }, + { name = "pygments" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/b3/b62658f6cf808d28e4d16a07509728a7b17824f55a6d3533f017fd4566b0/textual-8.2.6.tar.gz", hash = "sha256:cef3714498a120a99278b98d4c165c278844e73db50f1db039aaabd89f2d1b63", size = 1856990, upload-time = "2026-05-13T09:56:12.281Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/b4/c2b876f445e52522824cb900f2c7db3a7c24f89d20449ef278b4195d0ecb/textual-8.2.6-py3-none-any.whl", hash = "sha256:17c92bec7ff1617bd7db2a3d9734b0c3b7d2c274c67d5eba94371ea2f99a63fd", size = 729855, upload-time = "2026-05-13T09:56:14.687Z" }, +] + [[package]] name = "tqdm" version = "4.67.1" @@ -1044,6 +1212,40 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, ] +[[package]] +name = "ty" +version = "0.0.37" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/c3/60bc4829e0c1a8ff80b592067e1185a7b5ea64608acb0c676c44d5137d52/ty-0.0.37.tar.gz", hash = "sha256:f873f69627bd7f4ef8d57f716c63e5c63d7d1b7327ab3de185c7287a75223011", size = 5655422, upload-time = "2026-05-16T05:57:21.315Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/fe/180dd6914f9db33ad0200fbeaa429dd1fb0a4e6d98320dc1775f100a91af/ty-0.0.37-py3-none-linux_armv6l.whl", hash = "sha256:66cf7310189856e15f690559ddf37735476d2644db917d92f7cef13e5c834adf", size = 11246028, upload-time = "2026-05-16T05:57:41.744Z" }, + { url = "https://files.pythonhosted.org/packages/ef/a2/fa0cfd31467ad99b2db8c81ee9e2b4574589974a3eb9723be825e15b300c/ty-0.0.37-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2048f3c44ee6c7dde6e0ca064f99c6cada8f6de8ccdcfad2d856a429f8a4ac82", size = 11001460, upload-time = "2026-05-16T05:57:35.27Z" }, + { url = "https://files.pythonhosted.org/packages/10/3f/db60ba9be8b95a464ece0ba103e534047c34b49fee12f5e101f83f8d66db/ty-0.0.37-py3-none-macosx_11_0_arm64.whl", hash = "sha256:32c7b9b5b626aacdec334b44a2698e5f7b80df55bf7338267084d00d4b9546b3", size = 10446549, upload-time = "2026-05-16T05:57:37.252Z" }, + { url = "https://files.pythonhosted.org/packages/56/6f/11dd7174b20ebcb37a3d3b68f60b3940e37e4356e0accd03e2d7f9f70690/ty-0.0.37-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9fba1bebccf1e656bc5e3787acc5a191c491041ee4d12fe8fe2eff64e7b190d", size = 10961016, upload-time = "2026-05-16T05:57:16.394Z" }, + { url = "https://files.pythonhosted.org/packages/65/dd/3c17ce2860c525817c42c82d7075391b1f5615d36c03aa2d26647a224e8a/ty-0.0.37-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f987c5fb59aa5017ee8e8c5b57a07390f584e58e572255acd0fa44b3e0b238df", size = 11022093, upload-time = "2026-05-16T05:57:32.741Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a8/e7a40b0b57660921dd3482d219add963973b52ae8507abd88f48439704b5/ty-0.0.37-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4168f53146e7a3f52560ff433f238352591c9b1a9ed09397fbb776ddef4f89c", size = 11486333, upload-time = "2026-05-16T05:57:18.839Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/2c406b98244bc1ad42afdd35f466bcef88664210957dcbb5172254ff2462/ty-0.0.37-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11e487eafdb80a48223ce68a01f9287528216ffe0126d1629ff11e4f7c1dd3cf", size = 12093526, upload-time = "2026-05-16T05:57:04.456Z" }, + { url = "https://files.pythonhosted.org/packages/d3/3c/5c492a38e1b21a26370727dd4b77a53f05262e53e3be232047f22e7fa1b3/ty-0.0.37-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b49f388d063668676daaa7eef57385089d1b844279c0185bd84d4dbc3bcede6", size = 11725957, upload-time = "2026-05-16T05:57:23.356Z" }, + { url = "https://files.pythonhosted.org/packages/b2/00/8a3d9ba265cd0582342c14e4980cc0351aaaa45c6305712d398c9e2446c7/ty-0.0.37-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b96bfc1cc725d9d859abef4e3aa32a6da0f7472eaaafae2d9a6cffd729c7c61", size = 11610336, upload-time = "2026-05-16T05:57:27.888Z" }, + { url = "https://files.pythonhosted.org/packages/91/4b/6ee172935cb842f5c1553b0d37215b45e9dde05a4c74fdb47fd271907122/ty-0.0.37-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:c55f39b519107cf234b794718793e11793c055e89028a282a309f690def48117", size = 11797856, upload-time = "2026-05-16T05:57:11.109Z" }, + { url = "https://files.pythonhosted.org/packages/34/ef/75a7425bf9fe74483404ff11a8cbe3aa307354e0801697d6063384157776/ty-0.0.37-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c79204350de060a077bff7f027a1d53e216cad147d826ec9862be0af2f9c3c1e", size = 10941848, upload-time = "2026-05-16T05:57:30.653Z" }, + { url = "https://files.pythonhosted.org/packages/e0/2c/7ea9dccd55961375067f99ed00fb8eabb491f6a06d0e5f09c797d2b900a6/ty-0.0.37-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:49a21b4dcb2cd94cd0298c96dfb71a2dd25f08bf7e6eefd0c33c519d058908c6", size = 11058248, upload-time = "2026-05-16T05:57:01.785Z" }, + { url = "https://files.pythonhosted.org/packages/98/d7/848fde96c6610b2b1fd75823d44d8977a4525c4397f27332f054ccd6cf9c/ty-0.0.37-py3-none-musllinux_1_2_i686.whl", hash = "sha256:119332095c5974fe1dabfe4fd00c6759eeec5b99f7d7a80b2833feee5a58abdb", size = 11168423, upload-time = "2026-05-16T05:57:39.297Z" }, + { url = "https://files.pythonhosted.org/packages/29/11/c1613ac4b64357b9067df68bac97bcb458cc426cd468a2782847238c539b/ty-0.0.37-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ac5dc593675414f68862c2f71cc04912b0e5ec5520a9c49fc71ed79205b95c33", size = 11698565, upload-time = "2026-05-16T05:57:14.206Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ac/961205863903881996adb5a6f9cfe570c132882922ac226540346f15df20/ty-0.0.37-py3-none-win32.whl", hash = "sha256:33b57e4095179f06c2ae01c334833645cad94bf7d7467e073cdc3aaabea565d3", size = 10518308, upload-time = "2026-05-16T05:57:25.824Z" }, + { url = "https://files.pythonhosted.org/packages/39/cd/f308edd0cd86e402fe3a1b5c54e0a0dfa0177d80c1557c4849510bb2a147/ty-0.0.37-py3-none-win_amd64.whl", hash = "sha256:3b159351e99cf6eed7aacfb69ae8437725d15599ac4f21c8b2e909b300498b6c", size = 11607159, upload-time = "2026-05-16T05:57:06.76Z" }, + { url = "https://files.pythonhosted.org/packages/1a/ed/5ec4b501479bc5dad55467e2fe72e797cb9c178468c0d1a514536872ebc5/ty-0.0.37-py3-none-win_arm64.whl", hash = "sha256:6c3c2b997f68c71e14242b96d48cba3c086439556af02bb4613aa458950d5c23", size = 10958817, upload-time = "2026-05-16T05:57:08.907Z" }, +] + +[[package]] +name = "types-protobuf" +version = "6.32.1.20260221" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/e2/9aa4a3b2469508bd7b4e2ae11cbedaf419222a09a1b94daffcd5efca4023/types_protobuf-6.32.1.20260221.tar.gz", hash = "sha256:6d5fb060a616bfb076cbb61b4b3c3969f5fc8bec5810f9a2f7e648ee5cbcbf6e", size = 64408, upload-time = "2026-02-21T03:55:13.916Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/e8/1fd38926f9cf031188fbc5a96694203ea6f24b0e34bd64a225ec6f6291ba/types_protobuf-6.32.1.20260221-py3-none-any.whl", hash = "sha256:da7cdd947975964a93c30bfbcc2c6841ee646b318d3816b033adc2c4eb6448e4", size = 77956, upload-time = "2026-02-21T03:55:12.894Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0" @@ -1065,6 +1267,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] +[[package]] +name = "uc-micro-py" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/67/9a363818028526e2d4579334460df777115bdec1bb77c08f9db88f6389f2/uc_micro_py-2.0.0.tar.gz", hash = "sha256:c53691e495c8db60e16ffc4861a35469b0ba0821fe409a8a7a0a71864d33a811", size = 6611, upload-time = "2026-03-01T06:31:27.526Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/73/d21edf5b204d1467e06500080a50f79d49ef2b997c79123a536d4a17d97c/uc_micro_py-2.0.0-py3-none-any.whl", hash = "sha256:3603a3859af53e5a39bc7677713c78ea6589ff188d70f4fee165db88e22b242c", size = 6383, upload-time = "2026-03-01T06:31:26.257Z" }, +] + [[package]] name = "uvicorn" version = "0.40.0" @@ -1077,3 +1288,48 @@ sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e66 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" }, ] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +]