From cffc38f11c2614a4760550b0da079e13234fc3b1 Mon Sep 17 00:00:00 2001 From: William Easton Date: Wed, 3 Sep 2025 10:22:41 -0500 Subject: [PATCH 01/20] Add FastMCP Toolset w/o tests --- .../pydantic_ai/toolsets/fastmcp.py | 257 ++++++++++++++++++ pydantic_ai_slim/pyproject.toml | 2 + 2 files changed, 259 insertions(+) create mode 100644 pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py diff --git a/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py b/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py new file mode 100644 index 0000000000..c0c85b6604 --- /dev/null +++ b/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py @@ -0,0 +1,257 @@ +from __future__ import annotations + +import base64 +import contextlib +from asyncio import Lock +from contextlib import AsyncExitStack +from enum import Enum +from typing import TYPE_CHECKING, Any, Self + +import pydantic_core +from mcp.types import ( + AudioContent, + ContentBlock, + EmbeddedResource, + ImageContent, + TextContent, + TextResourceContents, + Tool as MCPTool, +) + +from pydantic_ai.exceptions import ModelRetry +from pydantic_ai.mcp import TOOL_SCHEMA_VALIDATOR, messages +from pydantic_ai.tools import AgentDepsT, RunContext, ToolDefinition +from pydantic_ai.toolsets import AbstractToolset +from pydantic_ai.toolsets.abstract import ToolsetTool + +try: + from fastmcp.client import Client + from fastmcp.client.transports import MCPConfigTransport + from fastmcp.exceptions import ToolError + from fastmcp.mcp_config import MCPConfig + from fastmcp.server.server import FastMCP +except ImportError as _import_error: + raise ImportError( + 'Please install the `fastmcp` package to use the FastMCP server, ' + 'you can use the `fastmcp` optional group — `pip install "pydantic-ai-slim[fastmcp]"`' + ) from _import_error + + +if TYPE_CHECKING: + from fastmcp import FastMCP + from fastmcp.client.client import CallToolResult + from fastmcp.client.transports import FastMCPTransport + from fastmcp.mcp_config import MCPServerTypes + + +FastMCPToolResult = messages.BinaryContent | dict[str, Any] | str | None + +FastMCPToolResults = list[FastMCPToolResult] | FastMCPToolResult + + +class ToolErrorBehavior(str, Enum): + """The behavior to take when a tool error occurs.""" + + MODEL_RETRY = 'model-retry' + """Raise a `ModelRetry` containing the tool error message.""" + + ERROR = 'raise' + """Raise the tool error as an exception.""" + + +class FastMCPToolset(AbstractToolset[AgentDepsT]): + """A toolset that uses a FastMCP client as the underlying toolset.""" + + _fastmcp_client: Client[Any] | None = None + _tool_error_behavior: ToolErrorBehavior + + _tool_retries: int + + _enter_lock: Lock + _running_count: int + _exit_stack: AsyncExitStack | None + + def __init__( + self, fastmcp_client: Client[Any], tool_retries: int = 2, tool_error_behavior: ToolErrorBehavior | None = None + ): + self._tool_retries = tool_retries + self._fastmcp_client = fastmcp_client + self._enter_lock = Lock() + self._running_count = 0 + + self._tool_error_behavior = tool_error_behavior or ToolErrorBehavior.ERROR + + super().__init__() + + @property + def id(self) -> str | None: + return None + + async def __aenter__(self) -> Self: + async with self._enter_lock: + if self._running_count == 0 and self._fastmcp_client: + self._exit_stack = AsyncExitStack() + await self._exit_stack.enter_async_context(self._fastmcp_client) + self._running_count += 1 + + return self + + async def __aexit__(self, *args: Any) -> bool | None: + async with self._enter_lock: + self._running_count -= 1 + if self._running_count == 0 and self._exit_stack: + await self._exit_stack.aclose() + self._exit_stack = None + + return None + + @property + def fastmcp_client(self) -> Client[FastMCPTransport]: + if not self._fastmcp_client: + msg = 'FastMCP client not initialized' + raise RuntimeError(msg) + + return self._fastmcp_client + + async def get_tools(self, ctx: RunContext[AgentDepsT]) -> dict[str, ToolsetTool[AgentDepsT]]: + mcp_tools: list[MCPTool] = await self.fastmcp_client.list_tools() + + return { + tool.name: convert_mcp_tool_to_toolset_tool(toolset=self, mcp_tool=tool, retries=self._tool_retries) + for tool in mcp_tools + } + + async def call_tool( + self, name: str, tool_args: dict[str, Any], ctx: RunContext[AgentDepsT], tool: ToolsetTool[AgentDepsT] + ) -> Any: + try: + call_tool_result: CallToolResult = await self.fastmcp_client.call_tool(name=name, arguments=tool_args) + except ToolError as e: + if self._tool_error_behavior == ToolErrorBehavior.MODEL_RETRY: + raise ModelRetry(message=str(object=e)) from e + else: + raise e + + # We don't use call_tool_result.data at the moment because it requires the json schema to be translatable + # back into pydantic models otherwise it will be missing data. + + return call_tool_result.structured_content or _map_fastmcp_tool_results(parts=call_tool_result.content) + + @classmethod + def from_fastmcp_server( + cls, fastmcp_server: FastMCP[Any], tool_error_behavior: ToolErrorBehavior | None = None + ) -> Self: + """Build a FastMCPToolset from a FastMCP server. + + Example: + ```python + fastmcp_server = FastMCP('my_server') + @fastmcp_server.tool() + async def my_tool(a: int, b: int) -> int: + return a + b + + toolset = FastMCPToolset.from_fastmcp_server(fastmcp_server=fastmcp_server) + ``` + """ + fastmcp_client: Client[FastMCPTransport] = Client[FastMCPTransport](transport=fastmcp_server) + return cls(fastmcp_client=fastmcp_client, tool_retries=2, tool_error_behavior=tool_error_behavior) + + @classmethod + def from_mcp_server( + cls, + name: str, + mcp_server: MCPServerTypes | dict[str, Any], + tool_error_behavior: ToolErrorBehavior | None = None, + ) -> Self: + """Build a FastMCPToolset from an individual MCP server configuration. + + Example: + ```python + cls.from_mcp_server(name='my_server', mcp_server={ + 'cmd': 'uvx', + 'args': [ + "time-server-mcp", + ] + }) + ``` + """ + mcp_config: MCPConfig = MCPConfig.from_dict(config={name: mcp_server}) + + return cls.from_mcp_config(mcp_config=mcp_config, tool_error_behavior=tool_error_behavior) + + @classmethod + def from_mcp_config( + cls, mcp_config: MCPConfig | dict[str, Any], tool_error_behavior: ToolErrorBehavior | None = None + ) -> Self: + """Build a FastMCPToolset from an MCP json-derived / dictionary configuration object. + + Example: + ```python + cls.from_mcp_config(mcp_config={ + 'mcpServers': { + 'my_server': { + 'cmd': 'uvx', + 'args': [ + "time-server-mcp", + ] + } + } + }) + ``` + """ + fastmcp_client: Client[MCPConfigTransport] = Client[MCPConfigTransport](transport=mcp_config) + return cls(fastmcp_client=fastmcp_client, tool_retries=2, tool_error_behavior=tool_error_behavior) + + +def convert_mcp_tool_to_toolset_tool( + toolset: FastMCPToolset[AgentDepsT], + mcp_tool: MCPTool, + retries: int, +) -> ToolsetTool[AgentDepsT]: + """Convert an MCP tool to a toolset tool.""" + return ToolsetTool[AgentDepsT]( + tool_def=ToolDefinition( + name=mcp_tool.name, + description=mcp_tool.description, + parameters_json_schema=mcp_tool.inputSchema, + ), + toolset=toolset, + max_retries=retries, + args_validator=TOOL_SCHEMA_VALIDATOR, + ) + + +def _map_fastmcp_tool_results(parts: list[ContentBlock]) -> list[FastMCPToolResult]: + """Map FastMCP tool results to toolset tool results.""" + return [_map_fastmcp_tool_result(part) for part in parts] + + +def _map_fastmcp_tool_result(part: ContentBlock) -> FastMCPToolResult: + if isinstance(part, TextContent): + text = part.text + if text.startswith(('[', '{')): + with contextlib.suppress(ValueError): + result: Any = pydantic_core.from_json(text) + if isinstance(result, dict | list): + return result # pyright: ignore[reportUnknownVariableType, reportReturnType] + return text + + if isinstance(part, ImageContent): + return messages.BinaryContent(data=base64.b64decode(part.data), media_type=part.mimeType) + + if isinstance(part, AudioContent): + return messages.BinaryContent(data=base64.b64decode(part.data), media_type=part.mimeType) + + if isinstance(part, EmbeddedResource): + resource = part.resource + if isinstance(resource, TextResourceContents): + return resource.text + + # BlobResourceContents + return messages.BinaryContent( + data=base64.b64decode(resource.blob), + media_type=resource.mimeType or 'application/octet-stream', + ) + + msg = f'Unsupported/Unknown content block type: {type(part)}' + raise ValueError(msg) diff --git a/pydantic_ai_slim/pyproject.toml b/pydantic_ai_slim/pyproject.toml index bf35813b45..9785d6fe5e 100644 --- a/pydantic_ai_slim/pyproject.toml +++ b/pydantic_ai_slim/pyproject.toml @@ -89,6 +89,8 @@ cli = [ ] # MCP mcp = ["mcp>=1.12.3"] +# FastMCP +fastmcp = ["fastmcp>=2.12.0"] # Evals evals = ["pydantic-evals=={{ version }}"] # A2A From 456900e2168975d985fc2a81689cdaed915c1019 Mon Sep 17 00:00:00 2001 From: William Easton Date: Wed, 3 Sep 2025 13:53:54 -0500 Subject: [PATCH 02/20] Adding tests --- docs/toolsets.md | 43 ++ .../pydantic_ai/toolsets/fastmcp.py | 128 ++--- tests/test_fastmcp.py | 453 ++++++++++++++++++ 3 files changed, 569 insertions(+), 55 deletions(-) create mode 100644 tests/test_fastmcp.py diff --git a/docs/toolsets.md b/docs/toolsets.md index 7c1bce84d6..361e06db4f 100644 --- a/docs/toolsets.md +++ b/docs/toolsets.md @@ -663,6 +663,49 @@ If you want to reuse a network connection or session across tool listings and ca See the [MCP Client](./mcp/client.md) documentation for how to use MCP servers with Pydantic AI. +### FastMCP Tools {#fastmcp-tools} + +If you'd like to use tools from a [FastMCP](https://fastmcp.dev) Server, Client, or JSON MCP Configuration with Pydantic AI, you can use the [`FastMCPToolset`][pydantic_ai.toolsets.fastmcp.FastMCPToolset] [toolset](toolsets.md). + +You will need to install the `fastmcp` package and any others required by the tools in question. + +```python {test="skip"} +from fastmcp import FastMCP + +from pydantic_ai import Agent +from pydantic_ai.toolsets.fastmcp import FastMCPToolset + +fastmcp_server = FastMCP('my_server') +@fastmcp_server.tool() +async def my_tool(a: int, b: int) -> int: + return a + b + +toolset = FastMCPToolset.from_fastmcp_server(fastmcp_server) + +agent = Agent('openai:gpt-4o', toolsets=[toolset]) +``` + +You can also use the [`FastMCPToolset`][pydantic_ai.toolsets.fastmcp.FastMCPToolset] to create a toolset from a JSON MCP Configuration. + +```python {test="skip"} +from pydantic_ai import Agent +from pydantic_ai.toolsets.fastmcp import FastMCPToolset + +mcp_config = { + 'mcpServers': { + 'time_mcp_server': { + 'command': 'uvx', + 'args': ['mcp-server-time'] + } + } +} + +toolset = FastMCPToolset.from_mcp_config(mcp_config) + +agent = Agent('openai:gpt-4o', toolsets=[toolset]) +``` + + ### LangChain Tools {#langchain-tools} If you'd like to use tools or a [toolkit](https://python.langchain.com/docs/concepts/tools/#toolkits) from LangChain's [community tool library](https://python.langchain.com/docs/integrations/tools/) with Pydantic AI, you can use the [`LangChainToolset`][pydantic_ai.ext.langchain.LangChainToolset] which takes a list of LangChain tools. Note that Pydantic AI will not validate the arguments in this case -- it's up to the model to provide arguments matching the schema specified by the LangChain tool, and up to the LangChain tool to raise an error if the arguments are invalid. diff --git a/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py b/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py index c0c85b6604..611a17fbf6 100644 --- a/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py +++ b/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py @@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, Any, Self import pydantic_core +from fastmcp.client.transports import MCPConfigTransport from mcp.types import ( AudioContent, ContentBlock, @@ -26,7 +27,7 @@ try: from fastmcp.client import Client - from fastmcp.client.transports import MCPConfigTransport + from fastmcp.client.transports import FastMCPTransport, MCPConfigTransport from fastmcp.exceptions import ToolError from fastmcp.mcp_config import MCPConfig from fastmcp.server.server import FastMCP @@ -62,7 +63,7 @@ class ToolErrorBehavior(str, Enum): class FastMCPToolset(AbstractToolset[AgentDepsT]): """A toolset that uses a FastMCP client as the underlying toolset.""" - _fastmcp_client: Client[Any] | None = None + _fastmcp_client: Client[Any] _tool_error_behavior: ToolErrorBehavior _tool_retries: int @@ -92,7 +93,8 @@ async def __aenter__(self) -> Self: if self._running_count == 0 and self._fastmcp_client: self._exit_stack = AsyncExitStack() await self._exit_stack.enter_async_context(self._fastmcp_client) - self._running_count += 1 + + self._running_count += 1 return self @@ -105,37 +107,36 @@ async def __aexit__(self, *args: Any) -> bool | None: return None - @property - def fastmcp_client(self) -> Client[FastMCPTransport]: - if not self._fastmcp_client: - msg = 'FastMCP client not initialized' - raise RuntimeError(msg) - - return self._fastmcp_client - async def get_tools(self, ctx: RunContext[AgentDepsT]) -> dict[str, ToolsetTool[AgentDepsT]]: - mcp_tools: list[MCPTool] = await self.fastmcp_client.list_tools() + async with self: + mcp_tools: list[MCPTool] = await self._fastmcp_client.list_tools() - return { - tool.name: convert_mcp_tool_to_toolset_tool(toolset=self, mcp_tool=tool, retries=self._tool_retries) - for tool in mcp_tools - } + return { + tool.name: convert_mcp_tool_to_toolset_tool(toolset=self, mcp_tool=tool, retries=self._tool_retries) + for tool in mcp_tools + } async def call_tool( self, name: str, tool_args: dict[str, Any], ctx: RunContext[AgentDepsT], tool: ToolsetTool[AgentDepsT] ) -> Any: - try: - call_tool_result: CallToolResult = await self.fastmcp_client.call_tool(name=name, arguments=tool_args) - except ToolError as e: - if self._tool_error_behavior == ToolErrorBehavior.MODEL_RETRY: - raise ModelRetry(message=str(object=e)) from e - else: - raise e + async with self: + try: + call_tool_result: CallToolResult = await self._fastmcp_client.call_tool(name=name, arguments=tool_args) + except ToolError as e: + if self._tool_error_behavior == ToolErrorBehavior.MODEL_RETRY: + raise ModelRetry(message=str(object=e)) from e + else: + raise e # We don't use call_tool_result.data at the moment because it requires the json schema to be translatable # back into pydantic models otherwise it will be missing data. - return call_tool_result.structured_content or _map_fastmcp_tool_results(parts=call_tool_result.content) + if call_tool_result.structured_content: + return call_tool_result.structured_content + + mapped_results = _map_fastmcp_tool_results(parts=call_tool_result.content) + + return mapped_results[0] if len(mapped_results) == 1 else mapped_results @classmethod def from_fastmcp_server( @@ -145,6 +146,10 @@ def from_fastmcp_server( Example: ```python + from fastmcp import FastMCP + + from pydantic_ai.toolsets.fastmcp import FastMCPToolset + fastmcp_server = FastMCP('my_server') @fastmcp_server.tool() async def my_tool(a: int, b: int) -> int: @@ -153,7 +158,8 @@ async def my_tool(a: int, b: int) -> int: toolset = FastMCPToolset.from_fastmcp_server(fastmcp_server=fastmcp_server) ``` """ - fastmcp_client: Client[FastMCPTransport] = Client[FastMCPTransport](transport=fastmcp_server) + transport = FastMCPTransport(fastmcp_server) + fastmcp_client: Client[FastMCPTransport] = Client[FastMCPTransport](transport=transport) return cls(fastmcp_client=fastmcp_client, tool_retries=2, tool_error_behavior=tool_error_behavior) @classmethod @@ -167,13 +173,23 @@ def from_mcp_server( Example: ```python - cls.from_mcp_server(name='my_server', mcp_server={ - 'cmd': 'uvx', + from pydantic_ai import Agent + from pydantic_ai.toolsets.fastmcp import FastMCPToolset + + time_mcp_server = { + 'command': 'uvx', 'args': [ - "time-server-mcp", + 'mcp-server-time', ] - }) + } + + toolset = FastMCPToolset.from_mcp_server(name='time_server', mcp_server=time_mcp_server) + agent = Agent('openai:gpt-4o', toolsets=[toolset]) + async def main(): + async with agent: # (1)! + ... ``` + 1. This will start the MCP Server running over stdio. """ mcp_config: MCPConfig = MCPConfig.from_dict(config={name: mcp_server}) @@ -187,19 +203,38 @@ def from_mcp_config( Example: ```python - cls.from_mcp_config(mcp_config={ + from pydantic_ai import Agent + from pydantic_ai.toolsets.fastmcp import FastMCPToolset + + mcp_config = { 'mcpServers': { - 'my_server': { - 'cmd': 'uvx', + 'time_server': { + 'command': 'uvx', 'args': [ - "time-server-mcp", + 'mcp-server-time', + ] + }, + 'fetch_server': { + 'command': 'uvx', + 'args': [ + 'mcp-server-fetch', ] } } - }) + } + + fastmcp_toolset = FastMCPToolset.from_mcp_config(mcp_config) + + agent = Agent('openai:gpt-4o', toolsets=[fastmcp_toolset]) + async def main(): + async with agent: # (1)! + ... ``` + + 1. This will start both MCP Servers running over stdio`. """ - fastmcp_client: Client[MCPConfigTransport] = Client[MCPConfigTransport](transport=mcp_config) + transport: MCPConfigTransport = MCPConfigTransport(config=mcp_config) + fastmcp_client: Client[MCPConfigTransport] = Client[MCPConfigTransport](transport=transport) return cls(fastmcp_client=fastmcp_client, tool_retries=2, tool_error_behavior=tool_error_behavior) @@ -228,13 +263,7 @@ def _map_fastmcp_tool_results(parts: list[ContentBlock]) -> list[FastMCPToolResu def _map_fastmcp_tool_result(part: ContentBlock) -> FastMCPToolResult: if isinstance(part, TextContent): - text = part.text - if text.startswith(('[', '{')): - with contextlib.suppress(ValueError): - result: Any = pydantic_core.from_json(text) - if isinstance(result, dict | list): - return result # pyright: ignore[reportUnknownVariableType, reportReturnType] - return text + return part.text if isinstance(part, ImageContent): return messages.BinaryContent(data=base64.b64decode(part.data), media_type=part.mimeType) @@ -242,16 +271,5 @@ def _map_fastmcp_tool_result(part: ContentBlock) -> FastMCPToolResult: if isinstance(part, AudioContent): return messages.BinaryContent(data=base64.b64decode(part.data), media_type=part.mimeType) - if isinstance(part, EmbeddedResource): - resource = part.resource - if isinstance(resource, TextResourceContents): - return resource.text - - # BlobResourceContents - return messages.BinaryContent( - data=base64.b64decode(resource.blob), - media_type=resource.mimeType or 'application/octet-stream', - ) - - msg = f'Unsupported/Unknown content block type: {type(part)}' - raise ValueError(msg) + msg = f'Unsupported/Unknown content block type: {type(part)}' # pragma: no cover + raise ValueError(msg) # pragma: no cover diff --git a/tests/test_fastmcp.py b/tests/test_fastmcp.py new file mode 100644 index 0000000000..4324dfc6fa --- /dev/null +++ b/tests/test_fastmcp.py @@ -0,0 +1,453 @@ +"""Tests for the FastMCP Toolset implementation.""" + +from __future__ import annotations + +import base64 +from mcp.types import BlobResourceContents +from typing import Any + +import pytest +from pydantic import AnyUrl +from fastmcp.exceptions import ToolError +from inline_snapshot import snapshot + +from pydantic_ai._run_context import RunContext +from pydantic_ai.exceptions import ModelRetry +from pydantic_ai.models.test import TestModel +from pydantic_ai.usage import RunUsage + +from .conftest import try_import + +with try_import() as imports_successful: + from fastmcp.client import Client + from fastmcp.client.transports import FastMCPTransport, MCPConfigTransport + from fastmcp.mcp_config import MCPConfig + from fastmcp.server.server import FastMCP + from mcp.types import ( + ImageContent, + AudioContent, + BlobResourceContents, + ) + + # Import the content mapping functions for testing + from pydantic_ai.toolsets.fastmcp import ( + FastMCPToolset, + ToolErrorBehavior, + ) + + +pytestmark = [ + pytest.mark.skipif(not imports_successful(), reason='fastmcp not installed'), + pytest.mark.anyio, +] + + +@pytest.fixture +async def fastmcp_server() -> FastMCP: + """Create a real in-memory FastMCP server for testing.""" + server = FastMCP('test_server') + + @server.tool() + async def test_tool(param1: str, param2: int = 0) -> str: + """A test tool that returns a formatted string.""" + return f'param1={param1}, param2={param2}' + + @server.tool() + async def another_tool(value: float) -> dict[str, Any]: + """Another test tool that returns structured data.""" + return {'result': 'success', 'value': value, 'doubled': value * 2} + + @server.tool() + async def error_tool(should_fail: bool = False) -> str: + """A tool that can fail for testing error handling.""" + if should_fail: + raise ValueError('This is a test error') + return 'success' + + @server.tool() + async def binary_tool() -> ImageContent: + """A tool that returns binary content.""" + fake_image_data = b'fake_image_data' + encoded_data = base64.b64encode(fake_image_data).decode('utf-8') + return ImageContent(type='image', data=encoded_data, mimeType='image/png') + + @server.tool() + async def audio_tool() -> AudioContent: + """A tool that returns audio content.""" + fake_audio_data = b'fake_audio_data' + encoded_data = base64.b64encode(fake_audio_data).decode('utf-8') + return AudioContent(type='audio', data=encoded_data, mimeType='audio/mpeg') + + @server.tool() + async def text_tool(message: str) -> str: + """A tool that returns text content.""" + return f'Echo: {message}' + + @server.tool() + async def text_tool_wo_return_annotation(message: str): + """A tool that returns text content.""" + return f'Echo: {message}' + + @server.tool() + async def json_tool(data: dict[str, Any]) -> str: + """A tool that returns JSON data.""" + import json + + return json.dumps({'received': data, 'processed': True}) + + return server + + +@pytest.fixture +async def fastmcp_client(fastmcp_server: FastMCP) -> Client[FastMCPTransport]: + """Create a real FastMCP client connected to the test server.""" + return Client(transport=fastmcp_server) + + +@pytest.fixture +def run_context() -> RunContext[None]: + """Create a run context for testing.""" + return RunContext( + deps=None, + model=TestModel(), + usage=RunUsage(), + prompt=None, + messages=[], + run_step=0, + ) + + +def get_client_from_toolset(toolset: FastMCPToolset[None]) -> Client[FastMCPTransport]: + """Get the client from the toolset.""" + return toolset._fastmcp_client # pyright: ignore[reportPrivateUsage] + + +class TestFastMCPToolsetInitialization: + """Test FastMCP Toolset initialization and basic functionality.""" + + async def test_init_with_client(self, fastmcp_client: Client[FastMCPTransport]): + """Test initialization with a FastMCP client.""" + toolset = FastMCPToolset(fastmcp_client) + + # Test that the client is accessible via the property + assert toolset.id is None + + async def test_init_with_custom_retries_and_error_behavior(self, fastmcp_client: Client[FastMCPTransport]): + """Test initialization with custom retries and error behavior.""" + toolset = FastMCPToolset(fastmcp_client, tool_retries=5, tool_error_behavior=ToolErrorBehavior.MODEL_RETRY) + + # Test that the toolset was created successfully + assert get_client_from_toolset(toolset) is fastmcp_client + + async def test_id_property(self, fastmcp_client: Client[FastMCPTransport]): + """Test that the id property returns None.""" + toolset = FastMCPToolset(fastmcp_client) + assert toolset.id is None + + +class TestFastMCPToolsetContextManagement: + """Test FastMCP Toolset context management.""" + + async def test_context_manager_single_enter_exit( + self, fastmcp_client: Client[FastMCPTransport], run_context: RunContext[None] + ): + """Test single enter/exit cycle.""" + toolset = FastMCPToolset(fastmcp_client) + + async with toolset: + # Test that we can get tools when the context is active + tools = await toolset.get_tools(run_context) + assert len(tools) > 0 + assert 'test_tool' in tools + + # After exit, the toolset should still be usable but the client connection is closed + + async def test_context_manager_no_enter( + self, fastmcp_client: Client[FastMCPTransport], run_context: RunContext[None] + ): + """Test no enter/exit cycle.""" + toolset = FastMCPToolset(fastmcp_client) + + # Test that we can get tools when the context is not active + tools = await toolset.get_tools(run_context) + assert len(tools) > 0 + assert 'test_tool' in tools + + async def test_context_manager_nested_enter_exit( + self, fastmcp_client: Client[FastMCPTransport], run_context: RunContext[None] + ): + """Test nested enter/exit cycles.""" + toolset = FastMCPToolset(fastmcp_client) + + async with toolset: + tools1 = await toolset.get_tools(run_context) + async with toolset: + tools2 = await toolset.get_tools(run_context) + assert tools1 == tools2 + # Should still work after inner context exits + tools3 = await toolset.get_tools(run_context) + assert tools1 == tools3 + + +class TestFastMCPToolsetToolDiscovery: + """Test FastMCP Toolset tool discovery functionality.""" + + async def test_get_tools( + self, + fastmcp_client: Client[FastMCPTransport], + run_context: RunContext[None], + ): + """Test getting tools from the FastMCP client.""" + toolset = FastMCPToolset(fastmcp_client) + + async with toolset: + tools = await toolset.get_tools(run_context) + + # Should have all the tools we defined in the server + expected_tools = {'test_tool', 'another_tool', 'audio_tool', 'error_tool', 'binary_tool', 'text_tool', 'text_tool_wo_return_annotation', 'json_tool'} + assert set(tools.keys()) == expected_tools + + # Check tool definitions + test_tool = tools['test_tool'] + assert test_tool.tool_def.name == 'test_tool' + assert test_tool.tool_def.description is not None + assert 'test tool that returns a formatted string' in test_tool.tool_def.description + assert test_tool.max_retries == 2 + assert test_tool.toolset is toolset + + # Check that the tool has proper schema + schema = test_tool.tool_def.parameters_json_schema + assert schema['type'] == 'object' + assert 'param1' in schema['properties'] + assert 'param2' in schema['properties'] + + async def test_get_tools_with_empty_server(self, run_context: RunContext[None]): + """Test getting tools from an empty FastMCP server.""" + empty_server = FastMCP('empty_server') + empty_client = Client(transport=empty_server) + toolset = FastMCPToolset(empty_client) + + async with toolset: + tools = await toolset.get_tools(run_context) + assert len(tools) == 0 + + +class TestFastMCPToolsetToolCalling: + """Test FastMCP Toolset tool calling functionality.""" + + @pytest.fixture + async def fastmcp_toolset(self, fastmcp_client: Client[FastMCPTransport]) -> FastMCPToolset[None]: + """Create a FastMCP Toolset.""" + return FastMCPToolset(fastmcp_client) + + async def test_call_tool_success( + self, + fastmcp_toolset: FastMCPToolset[None], + run_context: RunContext[None], + ): + """Test successful tool call.""" + async with fastmcp_toolset: + tools = await fastmcp_toolset.get_tools(run_context) + test_tool = tools['test_tool'] + + result = await fastmcp_toolset.call_tool( + name='test_tool', tool_args={'param1': 'hello', 'param2': 42}, ctx=run_context, tool=test_tool + ) + + assert result == {'result': 'param1=hello, param2=42'} + + async def test_call_tool_with_structured_content( + self, + fastmcp_toolset: FastMCPToolset[None], + run_context: RunContext[None], + ): + """Test tool call with structured content.""" + async with fastmcp_toolset: + tools = await fastmcp_toolset.get_tools(run_context) + another_tool = tools['another_tool'] + + result = await fastmcp_toolset.call_tool( + name='another_tool', tool_args={'value': 3.14}, ctx=run_context, tool=another_tool + ) + + assert result == {'result': 'success', 'value': 3.14, 'doubled': 6.28} + + async def test_call_tool_with_binary_content( + self, + fastmcp_toolset: FastMCPToolset[None], + run_context: RunContext[None], + ): + """Test tool call that returns binary content.""" + async with fastmcp_toolset: + tools = await fastmcp_toolset.get_tools(run_context) + binary_tool = tools['binary_tool'] + + result = await fastmcp_toolset.call_tool( + name='binary_tool', tool_args={}, ctx=run_context, tool=binary_tool + ) + + assert isinstance(result, dict) + + assert result == snapshot( + { + 'type': 'image', + 'data': 'ZmFrZV9pbWFnZV9kYXRh', + 'mimeType': 'image/png', + 'annotations': None, + '_meta': None, + } + ) + + async def test_call_tool_with_audio_content( + self, + fastmcp_toolset: FastMCPToolset[None], + run_context: RunContext[None], + ): + """Test tool call that returns audio content.""" + async with fastmcp_toolset: + tools = await fastmcp_toolset.get_tools(run_context) + audio_tool = tools['audio_tool'] + + result = await fastmcp_toolset.call_tool( + name='audio_tool', tool_args={}, ctx=run_context, tool=audio_tool + ) + + assert isinstance(result, dict) + + assert result == snapshot( + { + 'type': 'audio', + 'data': 'ZmFrZV9hdWRpb19kYXRh', + 'mimeType': 'audio/mpeg', + 'annotations': None, + '_meta': None, + } + ) + + async def test_call_tool_with_text_content( + self, + fastmcp_toolset: FastMCPToolset[None], + run_context: RunContext[None], + ): + """Test tool call that returns text content.""" + async with fastmcp_toolset: + tools = await fastmcp_toolset.get_tools(run_context) + text_tool = tools['text_tool'] + + result = await fastmcp_toolset.call_tool( + name='text_tool', tool_args={'message': 'Hello World'}, ctx=run_context, tool=text_tool + ) + + assert result == snapshot({'result': 'Echo: Hello World'}) + + async def test_call_tool_with_unknown_text_content( + self, + fastmcp_toolset: FastMCPToolset[None], + run_context: RunContext[None], + ): + """Test tool call that returns text content.""" + async with fastmcp_toolset: + tools = await fastmcp_toolset.get_tools(run_context) + text_tool = tools['text_tool_wo_return_annotation'] + + result = await fastmcp_toolset.call_tool( + name='text_tool_wo_return_annotation', + tool_args={'message': 'Hello World'}, + ctx=run_context, + tool=text_tool, + ) + + assert result == snapshot('Echo: Hello World') + + async def test_call_tool_with_json_content( + self, + fastmcp_toolset: FastMCPToolset[None], + run_context: RunContext[None], + ): + """Test tool call that returns JSON content.""" + async with fastmcp_toolset: + tools = await fastmcp_toolset.get_tools(run_context) + json_tool = tools['json_tool'] + + result = await fastmcp_toolset.call_tool( + name='json_tool', tool_args={'data': {'key': 'value'}}, ctx=run_context, tool=json_tool + ) + + # Should parse the JSON string into a dict + assert result == snapshot({'result': '{"received": {"key": "value"}, "processed": true}'}) + + async def test_call_tool_with_error_behavior_raise( + self, + fastmcp_toolset: FastMCPToolset[None], + run_context: RunContext[None], + ): + """Test tool call with error behavior set to raise.""" + async with fastmcp_toolset: + tools = await fastmcp_toolset.get_tools(run_context) + error_tool = tools['error_tool'] + + with pytest.raises(ToolError, match='This is a test error'): + await fastmcp_toolset.call_tool( + name='error_tool', tool_args={'should_fail': True}, ctx=run_context, tool=error_tool + ) + + async def test_call_tool_with_error_behavior_model_retry( + self, + fastmcp_client: Client[FastMCPTransport], + run_context: RunContext[None], + ): + """Test tool call with error behavior set to model retry.""" + toolset = FastMCPToolset(fastmcp_client, tool_error_behavior=ToolErrorBehavior.MODEL_RETRY) + + async with toolset: + tools = await toolset.get_tools(run_context) + error_tool = tools['error_tool'] + + with pytest.raises(ModelRetry, match='This is a test error'): + await toolset.call_tool('error_tool', {'should_fail': True}, run_context, error_tool) + + +class TestFastMCPToolsetFactoryMethods: + """Test FastMCP Toolset factory methods.""" + + async def test_from_fastmcp_server(self, fastmcp_server: FastMCP, run_context: RunContext[None]): + """Test creating toolset from FastMCP server.""" + toolset = FastMCPToolset.from_fastmcp_server(fastmcp_server) + + assert isinstance(toolset, FastMCPToolset) + assert toolset.id is None + + async with toolset: + tools = await toolset.get_tools(run_context) + assert 'test_tool' in tools + + async def test_from_mcp_server(self, run_context: RunContext[None]): + """Test creating toolset from MCP server configuration.""" + + mcp_server_config = {'command': 'python', 'args': ['-c', 'print("test")']} + + toolset = FastMCPToolset.from_mcp_server(name='test_server', mcp_server=mcp_server_config) + assert isinstance(toolset, FastMCPToolset) + + client = get_client_from_toolset(toolset) + assert isinstance(client.transport, MCPConfigTransport) + + async def test_from_mcp_config_dict(self, run_context: RunContext[None]): + """Test creating toolset from MCP config dictionary.""" + + config_dict = {'mcpServers': {'test_server': {'command': 'python', 'args': ['-c', 'print("test")']}}} + + toolset = FastMCPToolset.from_mcp_config(mcp_config=config_dict) + client = get_client_from_toolset(toolset) + assert isinstance(client.transport, MCPConfigTransport) + + async def test_from_mcp_config_mcp_config(self, run_context: RunContext[None]): + """Test creating toolset from MCPConfig object.""" + # Create a real MCPConfig object + config = MCPConfig.from_dict( + {'mcpServers': {'test_server': {'command': 'python', 'args': ['-c', 'print("test")']}}} + ) + + toolset = FastMCPToolset.from_mcp_config(mcp_config=config) + client = get_client_from_toolset(toolset) + assert isinstance(client.transport, MCPConfigTransport) From 9bac437b7507f2b68868af95bae1fbc9603c3ba9 Mon Sep 17 00:00:00 2001 From: William Easton Date: Wed, 3 Sep 2025 19:22:49 -0500 Subject: [PATCH 03/20] PR Clean-up and coverage --- .../pydantic_ai/toolsets/fastmcp.py | 23 +++---- tests/test_fastmcp.py | 61 +++++++++++-------- 2 files changed, 46 insertions(+), 38 deletions(-) diff --git a/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py b/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py index 611a17fbf6..e17ab98beb 100644 --- a/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py +++ b/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py @@ -1,21 +1,17 @@ from __future__ import annotations import base64 -import contextlib from asyncio import Lock from contextlib import AsyncExitStack from enum import Enum from typing import TYPE_CHECKING, Any, Self -import pydantic_core from fastmcp.client.transports import MCPConfigTransport from mcp.types import ( AudioContent, ContentBlock, - EmbeddedResource, ImageContent, TextContent, - TextResourceContents, Tool as MCPTool, ) @@ -128,15 +124,15 @@ async def call_tool( else: raise e - # We don't use call_tool_result.data at the moment because it requires the json schema to be translatable - # back into pydantic models otherwise it will be missing data. + # If any of the results are not text content, let's map them to Pydantic AI binary message parts + if any(not isinstance(part, TextContent) for part in call_tool_result.content): + return _map_fastmcp_tool_results(parts=call_tool_result.content) + # Otherwise, if we have structured content, return that if call_tool_result.structured_content: return call_tool_result.structured_content - mapped_results = _map_fastmcp_tool_results(parts=call_tool_result.content) - - return mapped_results[0] if len(mapped_results) == 1 else mapped_results + return _map_fastmcp_tool_results(parts=call_tool_result.content) @classmethod def from_fastmcp_server( @@ -256,9 +252,14 @@ def convert_mcp_tool_to_toolset_tool( ) -def _map_fastmcp_tool_results(parts: list[ContentBlock]) -> list[FastMCPToolResult]: +def _map_fastmcp_tool_results(parts: list[ContentBlock]) -> list[FastMCPToolResult] | FastMCPToolResult: """Map FastMCP tool results to toolset tool results.""" - return [_map_fastmcp_tool_result(part) for part in parts] + mapped_results = [_map_fastmcp_tool_result(part) for part in parts] + + if len(mapped_results) == 1: + return mapped_results[0] + + return mapped_results def _map_fastmcp_tool_result(part: ContentBlock) -> FastMCPToolResult: diff --git a/tests/test_fastmcp.py b/tests/test_fastmcp.py index 4324dfc6fa..b68da67b71 100644 --- a/tests/test_fastmcp.py +++ b/tests/test_fastmcp.py @@ -3,16 +3,16 @@ from __future__ import annotations import base64 -from mcp.types import BlobResourceContents from typing import Any import pytest -from pydantic import AnyUrl from fastmcp.exceptions import ToolError from inline_snapshot import snapshot +from mcp.types import TextContent from pydantic_ai._run_context import RunContext from pydantic_ai.exceptions import ModelRetry +from pydantic_ai.messages import BinaryContent from pydantic_ai.models.test import TestModel from pydantic_ai.usage import RunUsage @@ -24,9 +24,8 @@ from fastmcp.mcp_config import MCPConfig from fastmcp.server.server import FastMCP from mcp.types import ( - ImageContent, AudioContent, - BlobResourceContents, + ImageContent, ) # Import the content mapping functions for testing @@ -77,12 +76,20 @@ async def audio_tool() -> AudioContent: fake_audio_data = b'fake_audio_data' encoded_data = base64.b64encode(fake_audio_data).decode('utf-8') return AudioContent(type='audio', data=encoded_data, mimeType='audio/mpeg') - + @server.tool() async def text_tool(message: str) -> str: """A tool that returns text content.""" return f'Echo: {message}' + @server.tool() + async def text_list_tool(message: str) -> list[TextContent]: + """A tool that returns text content without a return annotation.""" + return [ + TextContent(type='text', text=f'Echo: {message}'), + TextContent(type='text', text=f'Echo: {message} again'), + ] + @server.tool() async def text_tool_wo_return_annotation(message: str): """A tool that returns text content.""" @@ -204,7 +211,17 @@ async def test_get_tools( tools = await toolset.get_tools(run_context) # Should have all the tools we defined in the server - expected_tools = {'test_tool', 'another_tool', 'audio_tool', 'error_tool', 'binary_tool', 'text_tool', 'text_tool_wo_return_annotation', 'json_tool'} + expected_tools = { + 'test_tool', + 'another_tool', + 'audio_tool', + 'error_tool', + 'binary_tool', + 'text_tool', + 'text_list_tool', + 'text_tool_wo_return_annotation', + 'json_tool', + } assert set(tools.keys()) == expected_tools # Check tool definitions @@ -286,16 +303,8 @@ async def test_call_tool_with_binary_content( name='binary_tool', tool_args={}, ctx=run_context, tool=binary_tool ) - assert isinstance(result, dict) - assert result == snapshot( - { - 'type': 'image', - 'data': 'ZmFrZV9pbWFnZV9kYXRh', - 'mimeType': 'image/png', - 'annotations': None, - '_meta': None, - } + BinaryContent(data=b'fake_image_data', media_type='image/png', identifier='427d68') ) async def test_call_tool_with_audio_content( @@ -308,20 +317,10 @@ async def test_call_tool_with_audio_content( tools = await fastmcp_toolset.get_tools(run_context) audio_tool = tools['audio_tool'] - result = await fastmcp_toolset.call_tool( - name='audio_tool', tool_args={}, ctx=run_context, tool=audio_tool - ) - - assert isinstance(result, dict) + result = await fastmcp_toolset.call_tool(name='audio_tool', tool_args={}, ctx=run_context, tool=audio_tool) assert result == snapshot( - { - 'type': 'audio', - 'data': 'ZmFrZV9hdWRpb19kYXRh', - 'mimeType': 'audio/mpeg', - 'annotations': None, - '_meta': None, - } + BinaryContent(data=b'fake_audio_data', media_type='audio/mpeg', identifier='f1220f') ) async def test_call_tool_with_text_content( @@ -340,6 +339,14 @@ async def test_call_tool_with_text_content( assert result == snapshot({'result': 'Echo: Hello World'}) + text_list_tool = tools['text_list_tool'] + + result = await fastmcp_toolset.call_tool( + name='text_list_tool', tool_args={'message': 'Hello World'}, ctx=run_context, tool=text_list_tool + ) + + assert result == snapshot(['Echo: Hello World', 'Echo: Hello World again']) + async def test_call_tool_with_unknown_text_content( self, fastmcp_toolset: FastMCPToolset[None], From edd89f2f75dcbccf2dd78ba341b21f6266df7397 Mon Sep 17 00:00:00 2001 From: William Easton Date: Wed, 3 Sep 2025 19:27:26 -0500 Subject: [PATCH 04/20] Fix import --- pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py b/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py index e17ab98beb..c97c2f30e1 100644 --- a/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py +++ b/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py @@ -6,7 +6,6 @@ from enum import Enum from typing import TYPE_CHECKING, Any, Self -from fastmcp.client.transports import MCPConfigTransport from mcp.types import ( AudioContent, ContentBlock, From 9c4fe383534cb28a6153b77890f9d90db184d9ce Mon Sep 17 00:00:00 2001 From: William Easton Date: Thu, 4 Sep 2025 13:08:09 -0500 Subject: [PATCH 05/20] Fix module import error --- tests/test_fastmcp.py | 2 +- uv.lock | 348 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 345 insertions(+), 5 deletions(-) diff --git a/tests/test_fastmcp.py b/tests/test_fastmcp.py index b68da67b71..fdae2a1dfc 100644 --- a/tests/test_fastmcp.py +++ b/tests/test_fastmcp.py @@ -6,7 +6,6 @@ from typing import Any import pytest -from fastmcp.exceptions import ToolError from inline_snapshot import snapshot from mcp.types import TextContent @@ -21,6 +20,7 @@ with try_import() as imports_successful: from fastmcp.client import Client from fastmcp.client.transports import FastMCPTransport, MCPConfigTransport + from fastmcp.exceptions import ToolError from fastmcp.mcp_config import MCPConfig from fastmcp.server.server import FastMCP from mcp.types import ( diff --git a/uv.lock b/uv.lock index c2ef3431c9..556234c4d4 100644 --- a/uv.lock +++ b/uv.lock @@ -351,6 +351,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5d/35/be73b6015511aa0173ec595fc579133b797ad532996f2998fd6b8d1bbe6b/audioop_lts-0.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:78bfb3703388c780edf900be66e07de5a3d4105ca8e8720c5c4d67927e0b15d0", size = 23918, upload-time = "2024-08-04T21:14:42.803Z" }, ] +[[package]] +name = "authlib" +version = "1.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5d/c6/d9a9db2e71957827e23a34322bde8091b51cb778dcc38885b84c772a1ba9/authlib-1.6.3.tar.gz", hash = "sha256:9f7a982cc395de719e4c2215c5707e7ea690ecf84f1ab126f28c053f4219e610", size = 160836, upload-time = "2025-08-26T12:13:25.206Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/2f/efa9d26dbb612b774990741fd8f13c7cf4cfd085b870e4a5af5c82eaf5f1/authlib-1.6.3-py2.py3-none-any.whl", hash = "sha256:7ea0f082edd95a03b7b72edac65ec7f8f68d703017d7e37573aee4fc603f2a48", size = 240105, upload-time = "2025-08-26T12:13:23.889Z" }, +] + [[package]] name = "babel" version = "2.17.0" @@ -780,6 +792,53 @@ toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] +[[package]] +name = "cryptography" +version = "45.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/35/c495bffc2056f2dadb32434f1feedd79abde2a7f8363e1974afa9c33c7e2/cryptography-45.0.7.tar.gz", hash = "sha256:4b1654dfc64ea479c242508eb8c724044f1e964a47d1d1cacc5132292d851971", size = 744980, upload-time = "2025-09-01T11:15:03.146Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/91/925c0ac74362172ae4516000fe877912e33b5983df735ff290c653de4913/cryptography-45.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:3be4f21c6245930688bd9e162829480de027f8bf962ede33d4f8ba7d67a00cee", size = 7041105, upload-time = "2025-09-01T11:13:59.684Z" }, + { url = "https://files.pythonhosted.org/packages/fc/63/43641c5acce3a6105cf8bd5baeceeb1846bb63067d26dae3e5db59f1513a/cryptography-45.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:67285f8a611b0ebc0857ced2081e30302909f571a46bfa7a3cc0ad303fe015c6", size = 4205799, upload-time = "2025-09-01T11:14:02.517Z" }, + { url = "https://files.pythonhosted.org/packages/bc/29/c238dd9107f10bfde09a4d1c52fd38828b1aa353ced11f358b5dd2507d24/cryptography-45.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:577470e39e60a6cd7780793202e63536026d9b8641de011ed9d8174da9ca5339", size = 4430504, upload-time = "2025-09-01T11:14:04.522Z" }, + { url = "https://files.pythonhosted.org/packages/62/62/24203e7cbcc9bd7c94739428cd30680b18ae6b18377ae66075c8e4771b1b/cryptography-45.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4bd3e5c4b9682bc112d634f2c6ccc6736ed3635fc3319ac2bb11d768cc5a00d8", size = 4209542, upload-time = "2025-09-01T11:14:06.309Z" }, + { url = "https://files.pythonhosted.org/packages/cd/e3/e7de4771a08620eef2389b86cd87a2c50326827dea5528feb70595439ce4/cryptography-45.0.7-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:465ccac9d70115cd4de7186e60cfe989de73f7bb23e8a7aa45af18f7412e75bf", size = 3889244, upload-time = "2025-09-01T11:14:08.152Z" }, + { url = "https://files.pythonhosted.org/packages/96/b8/bca71059e79a0bb2f8e4ec61d9c205fbe97876318566cde3b5092529faa9/cryptography-45.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:16ede8a4f7929b4b7ff3642eba2bf79aa1d71f24ab6ee443935c0d269b6bc513", size = 4461975, upload-time = "2025-09-01T11:14:09.755Z" }, + { url = "https://files.pythonhosted.org/packages/58/67/3f5b26937fe1218c40e95ef4ff8d23c8dc05aa950d54200cc7ea5fb58d28/cryptography-45.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8978132287a9d3ad6b54fcd1e08548033cc09dc6aacacb6c004c73c3eb5d3ac3", size = 4209082, upload-time = "2025-09-01T11:14:11.229Z" }, + { url = "https://files.pythonhosted.org/packages/0e/e4/b3e68a4ac363406a56cf7b741eeb80d05284d8c60ee1a55cdc7587e2a553/cryptography-45.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b6a0e535baec27b528cb07a119f321ac024592388c5681a5ced167ae98e9fff3", size = 4460397, upload-time = "2025-09-01T11:14:12.924Z" }, + { url = "https://files.pythonhosted.org/packages/22/49/2c93f3cd4e3efc8cb22b02678c1fad691cff9dd71bb889e030d100acbfe0/cryptography-45.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a24ee598d10befaec178efdff6054bc4d7e883f615bfbcd08126a0f4931c83a6", size = 4337244, upload-time = "2025-09-01T11:14:14.431Z" }, + { url = "https://files.pythonhosted.org/packages/04/19/030f400de0bccccc09aa262706d90f2ec23d56bc4eb4f4e8268d0ddf3fb8/cryptography-45.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:fa26fa54c0a9384c27fcdc905a2fb7d60ac6e47d14bc2692145f2b3b1e2cfdbd", size = 4568862, upload-time = "2025-09-01T11:14:16.185Z" }, + { url = "https://files.pythonhosted.org/packages/29/56/3034a3a353efa65116fa20eb3c990a8c9f0d3db4085429040a7eef9ada5f/cryptography-45.0.7-cp311-abi3-win32.whl", hash = "sha256:bef32a5e327bd8e5af915d3416ffefdbe65ed975b646b3805be81b23580b57b8", size = 2936578, upload-time = "2025-09-01T11:14:17.638Z" }, + { url = "https://files.pythonhosted.org/packages/b3/61/0ab90f421c6194705a99d0fa9f6ee2045d916e4455fdbb095a9c2c9a520f/cryptography-45.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:3808e6b2e5f0b46d981c24d79648e5c25c35e59902ea4391a0dcb3e667bf7443", size = 3405400, upload-time = "2025-09-01T11:14:18.958Z" }, + { url = "https://files.pythonhosted.org/packages/63/e8/c436233ddf19c5f15b25ace33979a9dd2e7aa1a59209a0ee8554179f1cc0/cryptography-45.0.7-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bfb4c801f65dd61cedfc61a83732327fafbac55a47282e6f26f073ca7a41c3b2", size = 7021824, upload-time = "2025-09-01T11:14:20.954Z" }, + { url = "https://files.pythonhosted.org/packages/bc/4c/8f57f2500d0ccd2675c5d0cc462095adf3faa8c52294ba085c036befb901/cryptography-45.0.7-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:81823935e2f8d476707e85a78a405953a03ef7b7b4f55f93f7c2d9680e5e0691", size = 4202233, upload-time = "2025-09-01T11:14:22.454Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ac/59b7790b4ccaed739fc44775ce4645c9b8ce54cbec53edf16c74fd80cb2b/cryptography-45.0.7-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3994c809c17fc570c2af12c9b840d7cea85a9fd3e5c0e0491f4fa3c029216d59", size = 4423075, upload-time = "2025-09-01T11:14:24.287Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/d4f07ea21434bf891faa088a6ac15d6d98093a66e75e30ad08e88aa2b9ba/cryptography-45.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dad43797959a74103cb59c5dac71409f9c27d34c8a05921341fb64ea8ccb1dd4", size = 4204517, upload-time = "2025-09-01T11:14:25.679Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ac/924a723299848b4c741c1059752c7cfe09473b6fd77d2920398fc26bfb53/cryptography-45.0.7-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ce7a453385e4c4693985b4a4a3533e041558851eae061a58a5405363b098fcd3", size = 3882893, upload-time = "2025-09-01T11:14:27.1Z" }, + { url = "https://files.pythonhosted.org/packages/83/dc/4dab2ff0a871cc2d81d3ae6d780991c0192b259c35e4d83fe1de18b20c70/cryptography-45.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b04f85ac3a90c227b6e5890acb0edbaf3140938dbecf07bff618bf3638578cf1", size = 4450132, upload-time = "2025-09-01T11:14:28.58Z" }, + { url = "https://files.pythonhosted.org/packages/12/dd/b2882b65db8fc944585d7fb00d67cf84a9cef4e77d9ba8f69082e911d0de/cryptography-45.0.7-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:48c41a44ef8b8c2e80ca4527ee81daa4c527df3ecbc9423c41a420a9559d0e27", size = 4204086, upload-time = "2025-09-01T11:14:30.572Z" }, + { url = "https://files.pythonhosted.org/packages/5d/fa/1d5745d878048699b8eb87c984d4ccc5da4f5008dfd3ad7a94040caca23a/cryptography-45.0.7-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f3df7b3d0f91b88b2106031fd995802a2e9ae13e02c36c1fc075b43f420f3a17", size = 4449383, upload-time = "2025-09-01T11:14:32.046Z" }, + { url = "https://files.pythonhosted.org/packages/36/8b/fc61f87931bc030598e1876c45b936867bb72777eac693e905ab89832670/cryptography-45.0.7-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dd342f085542f6eb894ca00ef70236ea46070c8a13824c6bde0dfdcd36065b9b", size = 4332186, upload-time = "2025-09-01T11:14:33.95Z" }, + { url = "https://files.pythonhosted.org/packages/0b/11/09700ddad7443ccb11d674efdbe9a832b4455dc1f16566d9bd3834922ce5/cryptography-45.0.7-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1993a1bb7e4eccfb922b6cd414f072e08ff5816702a0bdb8941c247a6b1b287c", size = 4561639, upload-time = "2025-09-01T11:14:35.343Z" }, + { url = "https://files.pythonhosted.org/packages/71/ed/8f4c1337e9d3b94d8e50ae0b08ad0304a5709d483bfcadfcc77a23dbcb52/cryptography-45.0.7-cp37-abi3-win32.whl", hash = "sha256:18fcf70f243fe07252dcb1b268a687f2358025ce32f9f88028ca5c364b123ef5", size = 2926552, upload-time = "2025-09-01T11:14:36.929Z" }, + { url = "https://files.pythonhosted.org/packages/bc/ff/026513ecad58dacd45d1d24ebe52b852165a26e287177de1d545325c0c25/cryptography-45.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:7285a89df4900ed3bfaad5679b1e668cb4b38a8de1ccbfc84b05f34512da0a90", size = 3392742, upload-time = "2025-09-01T11:14:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/13/3e/e42f1528ca1ea82256b835191eab1be014e0f9f934b60d98b0be8a38ed70/cryptography-45.0.7-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:de58755d723e86175756f463f2f0bddd45cc36fbd62601228a3f8761c9f58252", size = 3572442, upload-time = "2025-09-01T11:14:39.836Z" }, + { url = "https://files.pythonhosted.org/packages/59/aa/e947693ab08674a2663ed2534cd8d345cf17bf6a1facf99273e8ec8986dc/cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a20e442e917889d1a6b3c570c9e3fa2fdc398c20868abcea268ea33c024c4083", size = 4142233, upload-time = "2025-09-01T11:14:41.305Z" }, + { url = "https://files.pythonhosted.org/packages/24/06/09b6f6a2fc43474a32b8fe259038eef1500ee3d3c141599b57ac6c57612c/cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:258e0dff86d1d891169b5af222d362468a9570e2532923088658aa866eb11130", size = 4376202, upload-time = "2025-09-01T11:14:43.047Z" }, + { url = "https://files.pythonhosted.org/packages/00/f2/c166af87e95ce6ae6d38471a7e039d3a0549c2d55d74e059680162052824/cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d97cf502abe2ab9eff8bd5e4aca274da8d06dd3ef08b759a8d6143f4ad65d4b4", size = 4141900, upload-time = "2025-09-01T11:14:45.089Z" }, + { url = "https://files.pythonhosted.org/packages/16/b9/e96e0b6cb86eae27ea51fa8a3151535a18e66fe7c451fa90f7f89c85f541/cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:c987dad82e8c65ebc985f5dae5e74a3beda9d0a2a4daf8a1115f3772b59e5141", size = 4375562, upload-time = "2025-09-01T11:14:47.166Z" }, + { url = "https://files.pythonhosted.org/packages/36/d0/36e8ee39274e9d77baf7d0dafda680cba6e52f3936b846f0d56d64fec915/cryptography-45.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c13b1e3afd29a5b3b2656257f14669ca8fa8d7956d509926f0b130b600b50ab7", size = 3322781, upload-time = "2025-09-01T11:14:48.747Z" }, + { url = "https://files.pythonhosted.org/packages/99/4e/49199a4c82946938a3e05d2e8ad9482484ba48bbc1e809e3d506c686d051/cryptography-45.0.7-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a862753b36620af6fc54209264f92c716367f2f0ff4624952276a6bbd18cbde", size = 3584634, upload-time = "2025-09-01T11:14:50.593Z" }, + { url = "https://files.pythonhosted.org/packages/16/ce/5f6ff59ea9c7779dba51b84871c19962529bdcc12e1a6ea172664916c550/cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:06ce84dc14df0bf6ea84666f958e6080cdb6fe1231be2a51f3fc1267d9f3fb34", size = 4149533, upload-time = "2025-09-01T11:14:52.091Z" }, + { url = "https://files.pythonhosted.org/packages/ce/13/b3cfbd257ac96da4b88b46372e662009b7a16833bfc5da33bb97dd5631ae/cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d0c5c6bac22b177bf8da7435d9d27a6834ee130309749d162b26c3105c0795a9", size = 4385557, upload-time = "2025-09-01T11:14:53.551Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c5/8c59d6b7c7b439ba4fc8d0cab868027fd095f215031bc123c3a070962912/cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:2f641b64acc00811da98df63df7d59fd4706c0df449da71cb7ac39a0732b40ae", size = 4149023, upload-time = "2025-09-01T11:14:55.022Z" }, + { url = "https://files.pythonhosted.org/packages/55/32/05385c86d6ca9ab0b4d5bb442d2e3d85e727939a11f3e163fc776ce5eb40/cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:f5414a788ecc6ee6bc58560e85ca624258a55ca434884445440a810796ea0e0b", size = 4385722, upload-time = "2025-09-01T11:14:57.319Z" }, + { url = "https://files.pythonhosted.org/packages/23/87/7ce86f3fa14bc11a5a48c30d8103c26e09b6465f8d8e9d74cf7a0714f043/cryptography-45.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:1f3d56f73595376f4244646dd5c5870c14c196949807be39e79e7bd9bac3da63", size = 3332908, upload-time = "2025-09-01T11:14:58.78Z" }, +] + [[package]] name = "cssselect2" version = "0.7.0" @@ -793,6 +852,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9d/3a/e39436efe51894243ff145a37c4f9a030839b97779ebcc4f13b3ba21c54e/cssselect2-0.7.0-py3-none-any.whl", hash = "sha256:fd23a65bfd444595913f02fc71f6b286c29261e354c41d722ca7a261a49b5969", size = 15586, upload-time = "2022-09-19T12:55:07.56Z" }, ] +[[package]] +name = "cyclopts" +version = "3.23.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "docstring-parser", marker = "python_full_version < '4'" }, + { name = "rich" }, + { name = "rich-rst" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ea/7a/28b63c43d4c17d6587abcfef648841d39543158bcc47b5d40a03b8831f7a/cyclopts-3.23.1.tar.gz", hash = "sha256:ca6a5e9b326caf156d79f3932e2f88b95629e59fd371c0b3a89732b7619edacb", size = 75161, upload-time = "2025-08-30T17:40:34.396Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/67/ac57fbef5414ce84fe0bdeb497918ab2c781ff2cbf23c1bd91334b225669/cyclopts-3.23.1-py3-none-any.whl", hash = "sha256:8e57c6ea47d72b4b565c6a6c8a9fd56ed048ab4316627991230f4ad24ce2bc29", size = 85222, upload-time = "2025-08-30T17:40:33.005Z" }, +] + [[package]] name = "datasets" version = "4.0.0" @@ -908,6 +983,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, ] +[[package]] +name = "dnspython" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/4a/263763cb2ba3816dd94b08ad3a33d5fdae34ecb856678773cc40a3605829/dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1", size = 345197, upload-time = "2024-10-05T20:14:59.362Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632, upload-time = "2024-10-05T20:14:57.687Z" }, +] + +[[package]] +name = "docstring-parser" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" } +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 = "docutils" +version = "0.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/86/5b41c32ecedcfdb4c77b28b6cb14234f252075f8cdb254531727a35547dd/docutils-0.22.tar.gz", hash = "sha256:ba9d57750e92331ebe7c08a1bbf7a7f8143b86c476acd51528b042216a6aad0f", size = 2277984, upload-time = "2025-07-29T15:20:31.06Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/57/8db39bc5f98f042e0153b1de9fb88e1a409a33cda4dd7f723c2ed71e01f6/docutils-0.22-py3-none-any.whl", hash = "sha256:4ed966a0e96a0477d852f7af31bdcb3adc049fbb35ccba358c2ea8a03287615e", size = 630709, upload-time = "2025-07-29T15:20:28.335Z" }, +] + [[package]] name = "duckdb" version = "1.3.2" @@ -958,6 +1060,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/75/21/fc2c821a2c92c021f8f8adf9fb36235d1b49525b7cd953e85624296aab94/duckduckgo_search-7.5.0-py3-none-any.whl", hash = "sha256:6a2d3f12ae29b3e076cd43be61f5f73cd95261e0a0f318fe0ad3648d7a5dff03", size = 20238, upload-time = "2025-02-24T14:50:48.179Z" }, ] +[[package]] +name = "email-validator" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, +] + [[package]] name = "eval-type-backport" version = "0.2.2" @@ -1054,6 +1169,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1f/2c/43927e22a2d57587b3aa09765098a6d833246b672d34c10c5f135414745a/fastavro-1.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:86baf8c9740ab570d0d4d18517da71626fe9be4d1142bea684db52bd5adb078f", size = 483967, upload-time = "2024-12-20T12:57:37.618Z" }, ] +[[package]] +name = "fastmcp" +version = "2.12.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "authlib" }, + { name = "cyclopts" }, + { name = "exceptiongroup" }, + { name = "httpx" }, + { name = "mcp" }, + { name = "openapi-core" }, + { name = "openapi-pydantic" }, + { name = "pydantic", extra = ["email"] }, + { name = "pyperclip" }, + { name = "python-dotenv" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/35/8a/c46759bb41a53187191e5b3d963c0bde54783ecc89186a93c4947607b8e4/fastmcp-2.12.2.tar.gz", hash = "sha256:6d13e2f9be57b99763fc22485f9f603daa23bfbca35a8172baa43b283d6fc1ff", size = 5244547, upload-time = "2025-09-03T21:28:09.869Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/0a/7a8d564b1b9909dbfc36eb93d76410a4acfada6b1e13ee451a753bb6dbc2/fastmcp-2.12.2-py3-none-any.whl", hash = "sha256:0b58d68e819c82078d1fd51989d3d81f2be7382d527308b06df55f4d0a4ec94f", size = 312029, upload-time = "2025-09-03T21:28:08.62Z" }, +] + [[package]] name = "ffmpy" version = "0.5.0" @@ -1494,6 +1631,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/71/ab/4ad6bb9808f242e659ca8437ee475efaa201f18ff20a0dd5553280c85ae5/inline_snapshot-0.23.0-py3-none-any.whl", hash = "sha256:b1a5feab675aee8d03a51f1b6291f412100ce750d846c2d58eab16c90ee2c4dd", size = 50119, upload-time = "2025-04-25T18:14:34.46Z" }, ] +[[package]] +name = "isodate" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, +] + [[package]] name = "jinja2" version = "3.1.5" @@ -1589,6 +1735,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fe/54/c86cd8e011fe98803d7e382fd67c0df5ceab8d2b7ad8c5a81524f791551c/jsonschema-4.25.0-py3-none-any.whl", hash = "sha256:24c2e8da302de79c8b9382fee3e76b355e44d2a4364bb207159ce10b517bd716", size = 89184, upload-time = "2025-07-18T15:39:42.956Z" }, ] +[[package]] +name = "jsonschema-path" +version = "0.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pathable" }, + { name = "pyyaml" }, + { name = "referencing" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/45/41ebc679c2a4fced6a722f624c18d658dee42612b83ea24c1caf7c0eb3a8/jsonschema_path-0.3.4.tar.gz", hash = "sha256:8365356039f16cc65fddffafda5f58766e34bebab7d6d105616ab52bc4297001", size = 11159, upload-time = "2025-01-24T14:33:16.547Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/58/3485da8cb93d2f393bce453adeef16896751f14ba3e2024bc21dc9597646/jsonschema_path-0.3.4-py3-none-any.whl", hash = "sha256:f502191fdc2b22050f9a81c9237be9d27145b9001c55842bece5e94e382e52f8", size = 14810, upload-time = "2025-01-24T14:33:14.652Z" }, +] + [[package]] name = "jsonschema-specifications" version = "2025.4.1" @@ -1601,6 +1762,51 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437, upload-time = "2025-04-23T12:34:05.422Z" }, ] +[[package]] +name = "lazy-object-proxy" +version = "1.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/08/a2/69df9c6ba6d316cfd81fe2381e464db3e6de5db45f8c43c6a23504abf8cb/lazy_object_proxy-1.12.0.tar.gz", hash = "sha256:1f5a462d92fd0cfb82f1fab28b51bfb209fabbe6aabf7f0d51472c0c124c0c61", size = 43681, upload-time = "2025-08-22T13:50:06.783Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/2b/d5e8915038acbd6c6a9fcb8aaf923dc184222405d3710285a1fec6e262bc/lazy_object_proxy-1.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:61d5e3310a4aa5792c2b599a7a78ccf8687292c8eb09cf187cca8f09cf6a7519", size = 26658, upload-time = "2025-08-22T13:42:23.373Z" }, + { url = "https://files.pythonhosted.org/packages/da/8f/91fc00eeea46ee88b9df67f7c5388e60993341d2a406243d620b2fdfde57/lazy_object_proxy-1.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1ca33565f698ac1aece152a10f432415d1a2aa9a42dfe23e5ba2bc255ab91f6", size = 68412, upload-time = "2025-08-22T13:42:24.727Z" }, + { url = "https://files.pythonhosted.org/packages/07/d2/b7189a0e095caedfea4d42e6b6949d2685c354263bdf18e19b21ca9b3cd6/lazy_object_proxy-1.12.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d01c7819a410f7c255b20799b65d36b414379a30c6f1684c7bd7eb6777338c1b", size = 67559, upload-time = "2025-08-22T13:42:25.875Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/b013840cc43971582ff1ceaf784d35d3a579650eb6cc348e5e6ed7e34d28/lazy_object_proxy-1.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:029d2b355076710505c9545aef5ab3f750d89779310e26ddf2b7b23f6ea03cd8", size = 66651, upload-time = "2025-08-22T13:42:27.427Z" }, + { url = "https://files.pythonhosted.org/packages/7e/6f/b7368d301c15612fcc4cd00412b5d6ba55548bde09bdae71930e1a81f2ab/lazy_object_proxy-1.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc6e3614eca88b1c8a625fc0a47d0d745e7c3255b21dac0e30b3037c5e3deeb8", size = 66901, upload-time = "2025-08-22T13:42:28.585Z" }, + { url = "https://files.pythonhosted.org/packages/61/1b/c6b1865445576b2fc5fa0fbcfce1c05fee77d8979fd1aa653dd0f179aefc/lazy_object_proxy-1.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:be5fe974e39ceb0d6c9db0663c0464669cf866b2851c73971409b9566e880eab", size = 26536, upload-time = "2025-08-22T13:42:29.636Z" }, + { url = "https://files.pythonhosted.org/packages/01/b3/4684b1e128a87821e485f5a901b179790e6b5bc02f89b7ee19c23be36ef3/lazy_object_proxy-1.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1cf69cd1a6c7fe2dbcc3edaa017cf010f4192e53796538cc7d5e1fedbfa4bcff", size = 26656, upload-time = "2025-08-22T13:42:30.605Z" }, + { url = "https://files.pythonhosted.org/packages/3a/03/1bdc21d9a6df9ff72d70b2ff17d8609321bea4b0d3cffd2cea92fb2ef738/lazy_object_proxy-1.12.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:efff4375a8c52f55a145dc8487a2108c2140f0bec4151ab4e1843e52eb9987ad", size = 68832, upload-time = "2025-08-22T13:42:31.675Z" }, + { url = "https://files.pythonhosted.org/packages/3d/4b/5788e5e8bd01d19af71e50077ab020bc5cce67e935066cd65e1215a09ff9/lazy_object_proxy-1.12.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1192e8c2f1031a6ff453ee40213afa01ba765b3dc861302cd91dbdb2e2660b00", size = 69148, upload-time = "2025-08-22T13:42:32.876Z" }, + { url = "https://files.pythonhosted.org/packages/79/0e/090bf070f7a0de44c61659cb7f74c2fe02309a77ca8c4b43adfe0b695f66/lazy_object_proxy-1.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3605b632e82a1cbc32a1e5034278a64db555b3496e0795723ee697006b980508", size = 67800, upload-time = "2025-08-22T13:42:34.054Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d2/b320325adbb2d119156f7c506a5fbfa37fcab15c26d13cf789a90a6de04e/lazy_object_proxy-1.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a61095f5d9d1a743e1e20ec6d6db6c2ca511961777257ebd9b288951b23b44fa", size = 68085, upload-time = "2025-08-22T13:42:35.197Z" }, + { url = "https://files.pythonhosted.org/packages/6a/48/4b718c937004bf71cd82af3713874656bcb8d0cc78600bf33bb9619adc6c/lazy_object_proxy-1.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:997b1d6e10ecc6fb6fe0f2c959791ae59599f41da61d652f6c903d1ee58b7370", size = 26535, upload-time = "2025-08-22T13:42:36.521Z" }, + { url = "https://files.pythonhosted.org/packages/0d/1b/b5f5bd6bda26f1e15cd3232b223892e4498e34ec70a7f4f11c401ac969f1/lazy_object_proxy-1.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ee0d6027b760a11cc18281e702c0309dd92da458a74b4c15025d7fc490deede", size = 26746, upload-time = "2025-08-22T13:42:37.572Z" }, + { url = "https://files.pythonhosted.org/packages/55/64/314889b618075c2bfc19293ffa9153ce880ac6153aacfd0a52fcabf21a66/lazy_object_proxy-1.12.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4ab2c584e3cc8be0dfca422e05ad30a9abe3555ce63e9ab7a559f62f8dbc6ff9", size = 71457, upload-time = "2025-08-22T13:42:38.743Z" }, + { url = "https://files.pythonhosted.org/packages/11/53/857fc2827fc1e13fbdfc0ba2629a7d2579645a06192d5461809540b78913/lazy_object_proxy-1.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14e348185adbd03ec17d051e169ec45686dcd840a3779c9d4c10aabe2ca6e1c0", size = 71036, upload-time = "2025-08-22T13:42:40.184Z" }, + { url = "https://files.pythonhosted.org/packages/2b/24/e581ffed864cd33c1b445b5763d617448ebb880f48675fc9de0471a95cbc/lazy_object_proxy-1.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c4fcbe74fb85df8ba7825fa05eddca764138da752904b378f0ae5ab33a36c308", size = 69329, upload-time = "2025-08-22T13:42:41.311Z" }, + { url = "https://files.pythonhosted.org/packages/78/be/15f8f5a0b0b2e668e756a152257d26370132c97f2f1943329b08f057eff0/lazy_object_proxy-1.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:563d2ec8e4d4b68ee7848c5ab4d6057a6d703cb7963b342968bb8758dda33a23", size = 70690, upload-time = "2025-08-22T13:42:42.51Z" }, + { url = "https://files.pythonhosted.org/packages/5d/aa/f02be9bbfb270e13ee608c2b28b8771f20a5f64356c6d9317b20043c6129/lazy_object_proxy-1.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:53c7fd99eb156bbb82cbc5d5188891d8fdd805ba6c1e3b92b90092da2a837073", size = 26563, upload-time = "2025-08-22T13:42:43.685Z" }, + { url = "https://files.pythonhosted.org/packages/f4/26/b74c791008841f8ad896c7f293415136c66cc27e7c7577de4ee68040c110/lazy_object_proxy-1.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:86fd61cb2ba249b9f436d789d1356deae69ad3231dc3c0f17293ac535162672e", size = 26745, upload-time = "2025-08-22T13:42:44.982Z" }, + { url = "https://files.pythonhosted.org/packages/9b/52/641870d309e5d1fb1ea7d462a818ca727e43bfa431d8c34b173eb090348c/lazy_object_proxy-1.12.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:81d1852fb30fab81696f93db1b1e55a5d1ff7940838191062f5f56987d5fcc3e", size = 71537, upload-time = "2025-08-22T13:42:46.141Z" }, + { url = "https://files.pythonhosted.org/packages/47/b6/919118e99d51c5e76e8bf5a27df406884921c0acf2c7b8a3b38d847ab3e9/lazy_object_proxy-1.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be9045646d83f6c2664c1330904b245ae2371b5c57a3195e4028aedc9f999655", size = 71141, upload-time = "2025-08-22T13:42:47.375Z" }, + { url = "https://files.pythonhosted.org/packages/e5/47/1d20e626567b41de085cf4d4fb3661a56c159feaa73c825917b3b4d4f806/lazy_object_proxy-1.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:67f07ab742f1adfb3966c40f630baaa7902be4222a17941f3d85fd1dae5565ff", size = 69449, upload-time = "2025-08-22T13:42:48.49Z" }, + { url = "https://files.pythonhosted.org/packages/58/8d/25c20ff1a1a8426d9af2d0b6f29f6388005fc8cd10d6ee71f48bff86fdd0/lazy_object_proxy-1.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:75ba769017b944fcacbf6a80c18b2761a1795b03f8899acdad1f1c39db4409be", size = 70744, upload-time = "2025-08-22T13:42:49.608Z" }, + { url = "https://files.pythonhosted.org/packages/c0/67/8ec9abe15c4f8a4bcc6e65160a2c667240d025cbb6591b879bea55625263/lazy_object_proxy-1.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:7b22c2bbfb155706b928ac4d74c1a63ac8552a55ba7fff4445155523ea4067e1", size = 26568, upload-time = "2025-08-22T13:42:57.719Z" }, + { url = "https://files.pythonhosted.org/packages/23/12/cd2235463f3469fd6c62d41d92b7f120e8134f76e52421413a0ad16d493e/lazy_object_proxy-1.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4a79b909aa16bde8ae606f06e6bbc9d3219d2e57fb3e0076e17879072b742c65", size = 27391, upload-time = "2025-08-22T13:42:50.62Z" }, + { url = "https://files.pythonhosted.org/packages/60/9e/f1c53e39bbebad2e8609c67d0830cc275f694d0ea23d78e8f6db526c12d3/lazy_object_proxy-1.12.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:338ab2f132276203e404951205fe80c3fd59429b3a724e7b662b2eb539bb1be9", size = 80552, upload-time = "2025-08-22T13:42:51.731Z" }, + { url = "https://files.pythonhosted.org/packages/4c/b6/6c513693448dcb317d9d8c91d91f47addc09553613379e504435b4cc8b3e/lazy_object_proxy-1.12.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c40b3c9faee2e32bfce0df4ae63f4e73529766893258eca78548bac801c8f66", size = 82857, upload-time = "2025-08-22T13:42:53.225Z" }, + { url = "https://files.pythonhosted.org/packages/12/1c/d9c4aaa4c75da11eb7c22c43d7c90a53b4fca0e27784a5ab207768debea7/lazy_object_proxy-1.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:717484c309df78cedf48396e420fa57fc8a2b1f06ea889df7248fdd156e58847", size = 80833, upload-time = "2025-08-22T13:42:54.391Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ae/29117275aac7d7d78ae4f5a4787f36ff33262499d486ac0bf3e0b97889f6/lazy_object_proxy-1.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a6b7ea5ea1ffe15059eb44bcbcb258f97bcb40e139b88152c40d07b1a1dfc9ac", size = 79516, upload-time = "2025-08-22T13:42:55.812Z" }, + { url = "https://files.pythonhosted.org/packages/19/40/b4e48b2c38c69392ae702ae7afa7b6551e0ca5d38263198b7c79de8b3bdf/lazy_object_proxy-1.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:08c465fb5cd23527512f9bd7b4c7ba6cec33e28aad36fbbe46bf7b858f9f3f7f", size = 27656, upload-time = "2025-08-22T13:42:56.793Z" }, + { url = "https://files.pythonhosted.org/packages/ef/3a/277857b51ae419a1574557c0b12e0d06bf327b758ba94cafc664cb1e2f66/lazy_object_proxy-1.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c9defba70ab943f1df98a656247966d7729da2fe9c2d5d85346464bf320820a3", size = 26582, upload-time = "2025-08-22T13:49:49.366Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b6/c5e0fa43535bb9c87880e0ba037cdb1c50e01850b0831e80eb4f4762f270/lazy_object_proxy-1.12.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6763941dbf97eea6b90f5b06eb4da9418cc088fce0e3883f5816090f9afcde4a", size = 71059, upload-time = "2025-08-22T13:49:50.488Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/7dcad19c685963c652624702f1a968ff10220b16bfcc442257038216bf55/lazy_object_proxy-1.12.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fdc70d81235fc586b9e3d1aeef7d1553259b62ecaae9db2167a5d2550dcc391a", size = 71034, upload-time = "2025-08-22T13:49:54.224Z" }, + { url = "https://files.pythonhosted.org/packages/12/ac/34cbfb433a10e28c7fd830f91c5a348462ba748413cbb950c7f259e67aa7/lazy_object_proxy-1.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0a83c6f7a6b2bfc11ef3ed67f8cbe99f8ff500b05655d8e7df9aab993a6abc95", size = 69529, upload-time = "2025-08-22T13:49:55.29Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6a/11ad7e349307c3ca4c0175db7a77d60ce42a41c60bcb11800aabd6a8acb8/lazy_object_proxy-1.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:256262384ebd2a77b023ad02fbcc9326282bcfd16484d5531154b02bc304f4c5", size = 70391, upload-time = "2025-08-22T13:49:56.35Z" }, + { url = "https://files.pythonhosted.org/packages/59/97/9b410ed8fbc6e79c1ee8b13f8777a80137d4bc189caf2c6202358e66192c/lazy_object_proxy-1.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:7601ec171c7e8584f8ff3f4e440aa2eebf93e854f04639263875b8c2971f819f", size = 26988, upload-time = "2025-08-22T13:49:57.302Z" }, + { url = "https://files.pythonhosted.org/packages/41/a0/b91504515c1f9a299fc157967ffbd2f0321bce0516a3d5b89f6f4cad0355/lazy_object_proxy-1.12.0-pp39.pp310.pp311.graalpy311-none-any.whl", hash = "sha256:c3b2e0af1f7f77c4263759c4824316ce458fabe0fceadcd24ef8ca08b2d1e402", size = 15072, upload-time = "2025-08-22T13:50:05.498Z" }, +] + [[package]] name = "logfire" version = "4.0.0" @@ -2065,6 +2271,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7a/c3/4d9fbb14285698b7ae9f64423048ca9d28f5eb08b99f768b978f9118f780/modal-1.0.4-py3-none-any.whl", hash = "sha256:6c0d96bb49b09fa47e407a13e49545e32fe0803803b4330fbeb38de5e71209cc", size = 579637, upload-time = "2025-06-13T14:46:58.712Z" }, ] +[[package]] +name = "more-itertools" +version = "10.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, +] + [[package]] name = "multidict" version = "6.1.0" @@ -2316,6 +2531,67 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e8/fb/df274ca10698ee77b07bff952f302ea627cc12dac6b85289485dd77db6de/openai-1.99.9-py3-none-any.whl", hash = "sha256:9dbcdb425553bae1ac5d947147bebbd630d91bbfc7788394d4c4f3a35682ab3a", size = 786816, upload-time = "2025-08-12T02:31:08.34Z" }, ] +[[package]] +name = "openapi-core" +version = "0.19.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "isodate" }, + { name = "jsonschema" }, + { name = "jsonschema-path" }, + { name = "more-itertools" }, + { name = "openapi-schema-validator" }, + { name = "openapi-spec-validator" }, + { name = "parse" }, + { name = "typing-extensions" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/35/1acaa5f2fcc6e54eded34a2ec74b479439c4e469fc4e8d0e803fda0234db/openapi_core-0.19.5.tar.gz", hash = "sha256:421e753da56c391704454e66afe4803a290108590ac8fa6f4a4487f4ec11f2d3", size = 103264, upload-time = "2025-03-20T20:17:28.193Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/6f/83ead0e2e30a90445ee4fc0135f43741aebc30cca5b43f20968b603e30b6/openapi_core-0.19.5-py3-none-any.whl", hash = "sha256:ef7210e83a59394f46ce282639d8d26ad6fc8094aa904c9c16eb1bac8908911f", size = 106595, upload-time = "2025-03-20T20:17:26.77Z" }, +] + +[[package]] +name = "openapi-pydantic" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/2e/58d83848dd1a79cb92ed8e63f6ba901ca282c5f09d04af9423ec26c56fd7/openapi_pydantic-0.5.1.tar.gz", hash = "sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d", size = 60892, upload-time = "2025-01-08T19:29:27.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381, upload-time = "2025-01-08T19:29:25.275Z" }, +] + +[[package]] +name = "openapi-schema-validator" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonschema" }, + { name = "jsonschema-specifications" }, + { name = "rfc3339-validator" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/f3/5507ad3325169347cd8ced61c232ff3df70e2b250c49f0fe140edb4973c6/openapi_schema_validator-0.6.3.tar.gz", hash = "sha256:f37bace4fc2a5d96692f4f8b31dc0f8d7400fd04f3a937798eaf880d425de6ee", size = 11550, upload-time = "2025-01-10T18:08:22.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/c6/ad0fba32775ae749016829dace42ed80f4407b171da41313d1a3a5f102e4/openapi_schema_validator-0.6.3-py3-none-any.whl", hash = "sha256:f3b9870f4e556b5a62a1c39da72a6b4b16f3ad9c73dc80084b1b11e74ba148a3", size = 8755, upload-time = "2025-01-10T18:08:19.758Z" }, +] + +[[package]] +name = "openapi-spec-validator" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonschema" }, + { name = "jsonschema-path" }, + { name = "lazy-object-proxy" }, + { name = "openapi-schema-validator" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/af/fe2d7618d6eae6fb3a82766a44ed87cd8d6d82b4564ed1c7cfb0f6378e91/openapi_spec_validator-0.7.2.tar.gz", hash = "sha256:cc029309b5c5dbc7859df0372d55e9d1ff43e96d678b9ba087f7c56fc586f734", size = 36855, upload-time = "2025-06-07T14:48:56.299Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/dd/b3fd642260cb17532f66cc1e8250f3507d1e580483e209dc1e9d13bd980d/openapi_spec_validator-0.7.2-py3-none-any.whl", hash = "sha256:4bbdc0894ec85f1d1bea1d6d9c8b2c3c8d7ccaa13577ef40da9c006c9fd0eb60", size = 39713, upload-time = "2025-06-07T14:48:54.077Z" }, +] + [[package]] name = "opentelemetry-api" version = "1.30.0" @@ -2639,6 +2915,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ab/5f/b38085618b950b79d2d9164a711c52b10aefc0ae6833b96f626b7021b2ed/pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a", size = 13098436, upload-time = "2024-09-20T13:09:48.112Z" }, ] +[[package]] +name = "parse" +version = "1.20.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4f/78/d9b09ba24bb36ef8b83b71be547e118d46214735b6dfb39e4bfde0e9b9dd/parse-1.20.2.tar.gz", hash = "sha256:b41d604d16503c79d81af5165155c0b20f6c8d6c559efa66b4b695c3e5a0a0ce", size = 29391, upload-time = "2024-06-11T04:41:57.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/31/ba45bf0b2aa7898d81cbbfac0e88c267befb59ad91a19e36e1bc5578ddb1/parse-1.20.2-py2.py3-none-any.whl", hash = "sha256:967095588cb802add9177d0c0b6133b5ba33b1ea9007ca800e526f42a85af558", size = 20126, upload-time = "2024-06-11T04:41:55.057Z" }, +] + +[[package]] +name = "pathable" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/67/93/8f2c2075b180c12c1e9f6a09d1a985bc2036906b13dff1d8917e395f2048/pathable-0.4.4.tar.gz", hash = "sha256:6905a3cd17804edfac7875b5f6c9142a218c7caef78693c2dbbbfbac186d88b2", size = 8124, upload-time = "2025-01-10T18:43:13.247Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/eb/b6260b31b1a96386c0a880edebe26f89669098acea8e0318bff6adb378fd/pathable-0.4.4-py3-none-any.whl", hash = "sha256:5ae9e94793b6ef5a4cbe0a7ce9dbbefc1eec38df253763fd0aeeacf2762dbbc2", size = 9592, upload-time = "2025-01-10T18:43:11.88Z" }, +] + [[package]] name = "pathspec" version = "0.12.1" @@ -2953,6 +3247,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" }, ] +[package.optional-dependencies] +email = [ + { name = "email-validator" }, +] + [[package]] name = "pydantic-ai" source = { editable = "." } @@ -3146,6 +3445,9 @@ duckduckgo = [ evals = [ { name = "pydantic-evals" }, ] +fastmcp = [ + { name = "fastmcp" }, +] google = [ { name = "google-genai" }, ] @@ -3192,6 +3494,7 @@ requires-dist = [ { name = "eval-type-backport", specifier = ">=0.2.0" }, { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "fasta2a", marker = "extra == 'a2a'", specifier = ">=0.4.1" }, + { name = "fastmcp", marker = "extra == 'fastmcp'", specifier = ">=2.12.0" }, { name = "genai-prices", specifier = ">=0.0.22" }, { name = "google-auth", marker = "extra == 'vertexai'", specifier = ">=2.36.0" }, { name = "google-genai", marker = "extra == 'google'", specifier = ">=1.31.0" }, @@ -3217,7 +3520,7 @@ requires-dist = [ { name = "tenacity", marker = "extra == 'retries'", specifier = ">=8.2.3" }, { name = "typing-inspection", specifier = ">=0.4.0" }, ] -provides-extras = ["a2a", "ag-ui", "anthropic", "bedrock", "cli", "cohere", "duckduckgo", "evals", "google", "groq", "huggingface", "logfire", "mcp", "mistral", "openai", "retries", "tavily", "temporal", "vertexai"] +provides-extras = ["a2a", "ag-ui", "anthropic", "bedrock", "cli", "cohere", "duckduckgo", "evals", "fastmcp", "google", "groq", "huggingface", "logfire", "mcp", "mistral", "openai", "retries", "tavily", "temporal", "vertexai"] [[package]] name = "pydantic-core" @@ -3515,11 +3818,11 @@ wheels = [ [[package]] name = "python-dotenv" -version = "1.0.1" +version = "1.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115, upload-time = "2024-01-23T06:33:00.505Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863, upload-time = "2024-01-23T06:32:58.246Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, ] [[package]] @@ -3716,6 +4019,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" }, ] +[[package]] +name = "rfc3339-validator" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513, upload-time = "2021-05-12T16:37:54.178Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490, upload-time = "2021-05-12T16:37:52.536Z" }, +] + [[package]] name = "rich" version = "13.9.4" @@ -3730,6 +4045,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424, upload-time = "2024-11-01T16:43:55.817Z" }, ] +[[package]] +name = "rich-rst" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b0/69/5514c3a87b5f10f09a34bb011bc0927bc12c596c8dae5915604e71abc386/rich_rst-1.3.1.tar.gz", hash = "sha256:fad46e3ba42785ea8c1785e2ceaa56e0ffa32dbe5410dec432f37e4107c4f383", size = 13839, upload-time = "2024-04-30T04:40:38.125Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/bc/cc4e3dbc5e7992398dcb7a8eda0cbcf4fb792a0cdb93f857b478bf3cf884/rich_rst-1.3.1-py3-none-any.whl", hash = "sha256:498a74e3896507ab04492d326e794c3ef76e7cda078703aa592d1853d91098c1", size = 11621, upload-time = "2024-04-30T04:40:32.619Z" }, +] + [[package]] name = "rpds-py" version = "0.26.0" @@ -4579,6 +4907,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/c8/d529f8a32ce40d98309f4470780631e971a5a842b60aec864833b3615786/websockets-14.2-py3-none-any.whl", hash = "sha256:7a6ceec4ea84469f15cf15807a747e9efe57e369c384fa86e022b3bea679b79b", size = 157416, upload-time = "2025-01-19T21:00:54.843Z" }, ] +[[package]] +name = "werkzeug" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/32/af/d4502dc713b4ccea7175d764718d5183caf8d0867a4f0190d5d4a45cea49/werkzeug-3.1.1.tar.gz", hash = "sha256:8cd39dfbdfc1e051965f156163e2974e52c210f130810e9ad36858f0fd3edad4", size = 806453, upload-time = "2024-11-01T16:40:45.462Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/ea/c67e1dee1ba208ed22c06d1d547ae5e293374bfc43e0eb0ef5e262b68561/werkzeug-3.1.1-py3-none-any.whl", hash = "sha256:a71124d1ef06008baafa3d266c02f56e1836a5984afd6dd6c9230669d60d9fb5", size = 224371, upload-time = "2024-11-01T16:40:43.994Z" }, +] + [[package]] name = "wrapt" version = "1.17.2" From 27592c767255e35f1b9248a5d6a8d71143ac207f Mon Sep 17 00:00:00 2001 From: William Easton Date: Thu, 4 Sep 2025 18:11:49 -0500 Subject: [PATCH 06/20] Trying to fix tests --- .../pydantic_ai/toolsets/fastmcp.py | 67 ++++++++----------- pyproject.toml | 1 + tests/test_fastmcp.py | 12 ++-- uv.lock | 2 + 4 files changed, 36 insertions(+), 46 deletions(-) diff --git a/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py b/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py index c97c2f30e1..036dece623 100644 --- a/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py +++ b/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py @@ -4,15 +4,9 @@ from asyncio import Lock from contextlib import AsyncExitStack from enum import Enum -from typing import TYPE_CHECKING, Any, Self +from typing import TYPE_CHECKING, Any -from mcp.types import ( - AudioContent, - ContentBlock, - ImageContent, - TextContent, - Tool as MCPTool, -) +from typing_extensions import Self from pydantic_ai.exceptions import ModelRetry from pydantic_ai.mcp import TOOL_SCHEMA_VALIDATOR, messages @@ -26,6 +20,14 @@ from fastmcp.exceptions import ToolError from fastmcp.mcp_config import MCPConfig from fastmcp.server.server import FastMCP + from mcp.types import ( + AudioContent, + ContentBlock, + ImageContent, + TextContent, + Tool as MCPTool, + ) + except ImportError as _import_error: raise ImportError( 'Please install the `fastmcp` package to use the FastMCP server, ' @@ -70,6 +72,13 @@ class FastMCPToolset(AbstractToolset[AgentDepsT]): def __init__( self, fastmcp_client: Client[Any], tool_retries: int = 2, tool_error_behavior: ToolErrorBehavior | None = None ): + """Build a new FastMCPToolset. + + Args: + fastmcp_client: The FastMCP client to use. + tool_retries: The number of times to retry a tool call. + tool_error_behavior: The behavior to take when a tool error occurs. + """ self._tool_retries = tool_retries self._fastmcp_client = fastmcp_client self._enter_lock = Lock() @@ -150,7 +159,7 @@ def from_fastmcp_server( async def my_tool(a: int, b: int) -> int: return a + b - toolset = FastMCPToolset.from_fastmcp_server(fastmcp_server=fastmcp_server) + FastMCPToolset.from_fastmcp_server(fastmcp_server=fastmcp_server) ``` """ transport = FastMCPTransport(fastmcp_server) @@ -168,23 +177,15 @@ def from_mcp_server( Example: ```python - from pydantic_ai import Agent from pydantic_ai.toolsets.fastmcp import FastMCPToolset time_mcp_server = { - 'command': 'uvx', - 'args': [ - 'mcp-server-time', - ] + 'command': 'uv', + 'args': ['run', 'mcp-run-python', 'stdio'], } - toolset = FastMCPToolset.from_mcp_server(name='time_server', mcp_server=time_mcp_server) - agent = Agent('openai:gpt-4o', toolsets=[toolset]) - async def main(): - async with agent: # (1)! - ... + FastMCPToolset.from_mcp_server(name='time_server', mcp_server=time_mcp_server) ``` - 1. This will start the MCP Server running over stdio. """ mcp_config: MCPConfig = MCPConfig.from_dict(config={name: mcp_server}) @@ -198,35 +199,23 @@ def from_mcp_config( Example: ```python - from pydantic_ai import Agent from pydantic_ai.toolsets.fastmcp import FastMCPToolset mcp_config = { 'mcpServers': { - 'time_server': { - 'command': 'uvx', - 'args': [ - 'mcp-server-time', - ] + 'first_server': { + 'command': 'uv', + 'args': ['run', 'mcp-run-python', 'stdio'], }, - 'fetch_server': { - 'command': 'uvx', - 'args': [ - 'mcp-server-fetch', - ] + 'second_server': { + 'command': 'uv', + 'args': ['run', 'mcp-run-python', 'stdio'], } } } - fastmcp_toolset = FastMCPToolset.from_mcp_config(mcp_config) - - agent = Agent('openai:gpt-4o', toolsets=[fastmcp_toolset]) - async def main(): - async with agent: # (1)! - ... + FastMCPToolset.from_mcp_config(mcp_config) ``` - - 1. This will start both MCP Servers running over stdio`. """ transport: MCPConfigTransport = MCPConfigTransport(config=mcp_config) fastmcp_client: Client[MCPConfigTransport] = Client[MCPConfigTransport](transport=transport) diff --git a/pyproject.toml b/pyproject.toml index 4237a55b64..3120d0d4a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,6 +89,7 @@ dev = [ "coverage[toml]>=7.10.3", "dirty-equals>=0.9.0", "duckduckgo-search>=7.0.0", + "fastmcp>=2.12.0", "inline-snapshot>=0.19.3", "pytest>=8.3.3", "pytest-examples>=0.0.18", diff --git a/tests/test_fastmcp.py b/tests/test_fastmcp.py index fdae2a1dfc..f3032b5233 100644 --- a/tests/test_fastmcp.py +++ b/tests/test_fastmcp.py @@ -7,7 +7,6 @@ import pytest from inline_snapshot import snapshot -from mcp.types import TextContent from pydantic_ai._run_context import RunContext from pydantic_ai.exceptions import ModelRetry @@ -26,6 +25,7 @@ from mcp.types import ( AudioContent, ImageContent, + TextContent, ) # Import the content mapping functions for testing @@ -57,11 +57,9 @@ async def another_tool(value: float) -> dict[str, Any]: return {'result': 'success', 'value': value, 'doubled': value * 2} @server.tool() - async def error_tool(should_fail: bool = False) -> str: + async def error_tool() -> str: """A tool that can fail for testing error handling.""" - if should_fail: - raise ValueError('This is a test error') - return 'success' + raise ValueError('This is a test error') @server.tool() async def binary_tool() -> ImageContent: @@ -395,7 +393,7 @@ async def test_call_tool_with_error_behavior_raise( with pytest.raises(ToolError, match='This is a test error'): await fastmcp_toolset.call_tool( - name='error_tool', tool_args={'should_fail': True}, ctx=run_context, tool=error_tool + name='error_tool', tool_args={}, ctx=run_context, tool=error_tool ) async def test_call_tool_with_error_behavior_model_retry( @@ -411,7 +409,7 @@ async def test_call_tool_with_error_behavior_model_retry( error_tool = tools['error_tool'] with pytest.raises(ModelRetry, match='This is a test error'): - await toolset.call_tool('error_tool', {'should_fail': True}, run_context, error_tool) + await toolset.call_tool('error_tool', {}, run_context, error_tool) class TestFastMCPToolsetFactoryMethods: diff --git a/uv.lock b/uv.lock index 556234c4d4..8109351330 100644 --- a/uv.lock +++ b/uv.lock @@ -3277,6 +3277,7 @@ dev = [ { name = "diff-cover" }, { name = "dirty-equals" }, { name = "duckduckgo-search" }, + { name = "fastmcp" }, { name = "genai-prices" }, { name = "inline-snapshot" }, { name = "mcp-run-python" }, @@ -3328,6 +3329,7 @@ dev = [ { name = "diff-cover", specifier = ">=9.2.0" }, { name = "dirty-equals", specifier = ">=0.9.0" }, { name = "duckduckgo-search", specifier = ">=7.0.0" }, + { name = "fastmcp", specifier = ">=2.12.0" }, { name = "genai-prices", specifier = ">=0.0.22" }, { name = "inline-snapshot", specifier = ">=0.19.3" }, { name = "mcp-run-python", specifier = ">=0.0.20" }, From 0362fd71d400563fb52ebcd47073d18a33711930 Mon Sep 17 00:00:00 2001 From: William Easton Date: Sat, 6 Sep 2025 07:16:36 -0500 Subject: [PATCH 07/20] Lint --- tests/test_fastmcp.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_fastmcp.py b/tests/test_fastmcp.py index f3032b5233..5b74910adb 100644 --- a/tests/test_fastmcp.py +++ b/tests/test_fastmcp.py @@ -392,9 +392,7 @@ async def test_call_tool_with_error_behavior_raise( error_tool = tools['error_tool'] with pytest.raises(ToolError, match='This is a test error'): - await fastmcp_toolset.call_tool( - name='error_tool', tool_args={}, ctx=run_context, tool=error_tool - ) + await fastmcp_toolset.call_tool(name='error_tool', tool_args={}, ctx=run_context, tool=error_tool) async def test_call_tool_with_error_behavior_model_retry( self, From 4bd0334935cda801500cf4ba254b5788a652611a Mon Sep 17 00:00:00 2001 From: William Easton Date: Wed, 10 Sep 2025 17:12:35 -0500 Subject: [PATCH 08/20] Address most PR Feedback --- docs/install.md | 1 + docs/toolsets.md | 8 +- .../pydantic_ai/toolsets/fastmcp.py | 86 ++++++++++--------- pyproject.toml | 2 +- tests/test_fastmcp.py | 25 +++--- 5 files changed, 66 insertions(+), 56 deletions(-) diff --git a/docs/install.md b/docs/install.md index 1fd12a499d..2dfb39a633 100644 --- a/docs/install.md +++ b/docs/install.md @@ -55,6 +55,7 @@ pip/uv-add "pydantic-ai-slim[openai]" * `tavily` - installs `tavily-python` [PyPI ↗](https://pypi.org/project/tavily-python){:target="_blank"} * `cli` - installs `rich` [PyPI ↗](https://pypi.org/project/rich){:target="_blank"}, `prompt-toolkit` [PyPI ↗](https://pypi.org/project/prompt-toolkit){:target="_blank"}, and `argcomplete` [PyPI ↗](https://pypi.org/project/argcomplete){:target="_blank"} * `mcp` - installs `mcp` [PyPI ↗](https://pypi.org/project/mcp){:target="_blank"} +* `fastmcp` - installs `fastmcp` [PyPI ↗](https://pypi.org/project/fastmcp){:target="_blank"} * `a2a` - installs `fasta2a` [PyPI ↗](https://pypi.org/project/fasta2a){:target="_blank"} * `ag-ui` - installs `ag-ui-protocol` [PyPI ↗](https://pypi.org/project/ag-ui-protocol){:target="_blank"} and `starlette` [PyPI ↗](https://pypi.org/project/starlette){:target="_blank"} diff --git a/docs/toolsets.md b/docs/toolsets.md index 361e06db4f..c99becd5dd 100644 --- a/docs/toolsets.md +++ b/docs/toolsets.md @@ -667,7 +667,7 @@ See the [MCP Client](./mcp/client.md) documentation for how to use MCP servers w If you'd like to use tools from a [FastMCP](https://fastmcp.dev) Server, Client, or JSON MCP Configuration with Pydantic AI, you can use the [`FastMCPToolset`][pydantic_ai.toolsets.fastmcp.FastMCPToolset] [toolset](toolsets.md). -You will need to install the `fastmcp` package and any others required by the tools in question. +To use the `FastMCPToolset`, you will need to install `pydantic-ai-slim[fastmcp]`. ```python {test="skip"} from fastmcp import FastMCP @@ -682,10 +682,10 @@ async def my_tool(a: int, b: int) -> int: toolset = FastMCPToolset.from_fastmcp_server(fastmcp_server) -agent = Agent('openai:gpt-4o', toolsets=[toolset]) +agent = Agent('openai:gpt-5', toolsets=[toolset]) ``` -You can also use the [`FastMCPToolset`][pydantic_ai.toolsets.fastmcp.FastMCPToolset] to create a toolset from a JSON MCP Configuration. +You can also use the [`FastMCPToolset`][pydantic_ai.toolsets.fastmcp.FastMCPToolset] to create a toolset from a JSON MCP Configuration. FastMCP supports additional capabilities on top of the MCP specification, like Tool Transformation in the MCP configuration that you can take advantage of with the `FastMCPToolset`. ```python {test="skip"} from pydantic_ai import Agent @@ -702,7 +702,7 @@ mcp_config = { toolset = FastMCPToolset.from_mcp_config(mcp_config) -agent = Agent('openai:gpt-4o', toolsets=[toolset]) +agent = Agent('openai:gpt-5', toolsets=[toolset]) ``` diff --git a/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py b/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py index 036dece623..1f6f4bfaed 100644 --- a/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py +++ b/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py @@ -3,8 +3,7 @@ import base64 from asyncio import Lock from contextlib import AsyncExitStack -from enum import Enum -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Literal from typing_extensions import Self @@ -44,59 +43,59 @@ FastMCPToolResult = messages.BinaryContent | dict[str, Any] | str | None -FastMCPToolResults = list[FastMCPToolResult] | FastMCPToolResult - - -class ToolErrorBehavior(str, Enum): - """The behavior to take when a tool error occurs.""" - - MODEL_RETRY = 'model-retry' - """Raise a `ModelRetry` containing the tool error message.""" - - ERROR = 'raise' - """Raise the tool error as an exception.""" +ToolErrorBehavior = Literal['model_retry', 'error'] class FastMCPToolset(AbstractToolset[AgentDepsT]): """A toolset that uses a FastMCP client as the underlying toolset.""" - _fastmcp_client: Client[Any] - _tool_error_behavior: ToolErrorBehavior + tool_error_behavior: Literal['model_retry', 'error'] + fastmcp_client: Client[Any] - _tool_retries: int + max_retries: int + + _id: str | None _enter_lock: Lock _running_count: int _exit_stack: AsyncExitStack | None def __init__( - self, fastmcp_client: Client[Any], tool_retries: int = 2, tool_error_behavior: ToolErrorBehavior | None = None + self, + fastmcp_client: Client[Any], + *, + max_retries: int = 2, + tool_error_behavior: ToolErrorBehavior | None = None, + id: str | None = None, ): """Build a new FastMCPToolset. Args: fastmcp_client: The FastMCP client to use. - tool_retries: The number of times to retry a tool call. + max_retries: The maximum number of retries for each tool during a run. tool_error_behavior: The behavior to take when a tool error occurs. + id: An optional unique ID for the toolset. A toolset needs to have an ID in order to be used in a durable execution environment like Temporal, + in which case the ID will be used to identify the toolset's activities within the workflow. """ - self._tool_retries = tool_retries - self._fastmcp_client = fastmcp_client + self.max_retries = max_retries + self.fastmcp_client = fastmcp_client self._enter_lock = Lock() self._running_count = 0 + self._id = id - self._tool_error_behavior = tool_error_behavior or ToolErrorBehavior.ERROR + self.tool_error_behavior = tool_error_behavior or 'error' super().__init__() @property def id(self) -> str | None: - return None + return self._id async def __aenter__(self) -> Self: async with self._enter_lock: - if self._running_count == 0 and self._fastmcp_client: + if self._running_count == 0 and self.fastmcp_client: self._exit_stack = AsyncExitStack() - await self._exit_stack.enter_async_context(self._fastmcp_client) + await self._exit_stack.enter_async_context(self.fastmcp_client) self._running_count += 1 @@ -113,10 +112,10 @@ async def __aexit__(self, *args: Any) -> bool | None: async def get_tools(self, ctx: RunContext[AgentDepsT]) -> dict[str, ToolsetTool[AgentDepsT]]: async with self: - mcp_tools: list[MCPTool] = await self._fastmcp_client.list_tools() + mcp_tools: list[MCPTool] = await self.fastmcp_client.list_tools() return { - tool.name: convert_mcp_tool_to_toolset_tool(toolset=self, mcp_tool=tool, retries=self._tool_retries) + tool.name: _convert_mcp_tool_to_toolset_tool(toolset=self, mcp_tool=tool, retries=self.max_retries) for tool in mcp_tools } @@ -125,10 +124,10 @@ async def call_tool( ) -> Any: async with self: try: - call_tool_result: CallToolResult = await self._fastmcp_client.call_tool(name=name, arguments=tool_args) + call_tool_result: CallToolResult = await self.fastmcp_client.call_tool(name=name, arguments=tool_args) except ToolError as e: - if self._tool_error_behavior == ToolErrorBehavior.MODEL_RETRY: - raise ModelRetry(message=str(object=e)) from e + if self.tool_error_behavior == 'model_retry': + raise ModelRetry(message=str(e)) from e else: raise e @@ -144,7 +143,11 @@ async def call_tool( @classmethod def from_fastmcp_server( - cls, fastmcp_server: FastMCP[Any], tool_error_behavior: ToolErrorBehavior | None = None + cls, + fastmcp_server: FastMCP[Any], + *, + tool_error_behavior: ToolErrorBehavior | None = None, + tool_retries: int = 2, ) -> Self: """Build a FastMCPToolset from a FastMCP server. @@ -164,14 +167,16 @@ async def my_tool(a: int, b: int) -> int: """ transport = FastMCPTransport(fastmcp_server) fastmcp_client: Client[FastMCPTransport] = Client[FastMCPTransport](transport=transport) - return cls(fastmcp_client=fastmcp_client, tool_retries=2, tool_error_behavior=tool_error_behavior) + return cls(fastmcp_client=fastmcp_client, max_retries=tool_retries, tool_error_behavior=tool_error_behavior) @classmethod def from_mcp_server( cls, name: str, mcp_server: MCPServerTypes | dict[str, Any], + *, tool_error_behavior: ToolErrorBehavior | None = None, + tool_retries: int = 2, ) -> Self: """Build a FastMCPToolset from an individual MCP server configuration. @@ -189,11 +194,17 @@ def from_mcp_server( """ mcp_config: MCPConfig = MCPConfig.from_dict(config={name: mcp_server}) - return cls.from_mcp_config(mcp_config=mcp_config, tool_error_behavior=tool_error_behavior) + return cls.from_mcp_config( + mcp_config=mcp_config, tool_error_behavior=tool_error_behavior, max_retries=tool_retries + ) @classmethod def from_mcp_config( - cls, mcp_config: MCPConfig | dict[str, Any], tool_error_behavior: ToolErrorBehavior | None = None + cls, + mcp_config: MCPConfig | dict[str, Any], + *, + tool_error_behavior: ToolErrorBehavior | None = None, + max_retries: int = 2, ) -> Self: """Build a FastMCPToolset from an MCP json-derived / dictionary configuration object. @@ -219,10 +230,10 @@ def from_mcp_config( """ transport: MCPConfigTransport = MCPConfigTransport(config=mcp_config) fastmcp_client: Client[MCPConfigTransport] = Client[MCPConfigTransport](transport=transport) - return cls(fastmcp_client=fastmcp_client, tool_retries=2, tool_error_behavior=tool_error_behavior) + return cls(fastmcp_client=fastmcp_client, max_retries=max_retries, tool_error_behavior=tool_error_behavior) -def convert_mcp_tool_to_toolset_tool( +def _convert_mcp_tool_to_toolset_tool( toolset: FastMCPToolset[AgentDepsT], mcp_tool: MCPTool, retries: int, @@ -254,10 +265,7 @@ def _map_fastmcp_tool_result(part: ContentBlock) -> FastMCPToolResult: if isinstance(part, TextContent): return part.text - if isinstance(part, ImageContent): - return messages.BinaryContent(data=base64.b64decode(part.data), media_type=part.mimeType) - - if isinstance(part, AudioContent): + if isinstance(part, ImageContent | AudioContent): return messages.BinaryContent(data=base64.b64decode(part.data), media_type=part.mimeType) msg = f'Unsupported/Unknown content block type: {type(part)}' # pragma: no cover diff --git a/pyproject.toml b/pyproject.toml index 3120d0d4a7..b660551ebb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ requires-python = ">=3.10" [tool.hatch.metadata.hooks.uv-dynamic-versioning] dependencies = [ - "pydantic-ai-slim[openai,vertexai,google,groq,anthropic,mistral,cohere,bedrock,huggingface,cli,mcp,evals,ag-ui,retries,temporal,logfire]=={{ version }}", + "pydantic-ai-slim[openai,vertexai,google,groq,anthropic,mistral,cohere,bedrock,huggingface,cli,mcp,fastmcp,evals,ag-ui,retries,temporal,logfire]=={{ version }}", ] [tool.hatch.metadata.hooks.uv-dynamic-versioning.optional-dependencies] diff --git a/tests/test_fastmcp.py b/tests/test_fastmcp.py index 5b74910adb..74541ba9ad 100644 --- a/tests/test_fastmcp.py +++ b/tests/test_fastmcp.py @@ -31,7 +31,6 @@ # Import the content mapping functions for testing from pydantic_ai.toolsets.fastmcp import ( FastMCPToolset, - ToolErrorBehavior, ) @@ -122,11 +121,6 @@ def run_context() -> RunContext[None]: ) -def get_client_from_toolset(toolset: FastMCPToolset[None]) -> Client[FastMCPTransport]: - """Get the client from the toolset.""" - return toolset._fastmcp_client # pyright: ignore[reportPrivateUsage] - - class TestFastMCPToolsetInitialization: """Test FastMCP Toolset initialization and basic functionality.""" @@ -137,12 +131,19 @@ async def test_init_with_client(self, fastmcp_client: Client[FastMCPTransport]): # Test that the client is accessible via the property assert toolset.id is None + async def test_init_with_id(self, fastmcp_client: Client[FastMCPTransport]): + """Test initialization with an id.""" + toolset = FastMCPToolset(fastmcp_client, id='test_id') + + # Test that the client is accessible via the property + assert toolset.id == 'test_id' + async def test_init_with_custom_retries_and_error_behavior(self, fastmcp_client: Client[FastMCPTransport]): """Test initialization with custom retries and error behavior.""" - toolset = FastMCPToolset(fastmcp_client, tool_retries=5, tool_error_behavior=ToolErrorBehavior.MODEL_RETRY) + toolset = FastMCPToolset(fastmcp_client, max_retries=5, tool_error_behavior='model_retry') # Test that the toolset was created successfully - assert get_client_from_toolset(toolset) is fastmcp_client + assert toolset.fastmcp_client is fastmcp_client async def test_id_property(self, fastmcp_client: Client[FastMCPTransport]): """Test that the id property returns None.""" @@ -400,7 +401,7 @@ async def test_call_tool_with_error_behavior_model_retry( run_context: RunContext[None], ): """Test tool call with error behavior set to model retry.""" - toolset = FastMCPToolset(fastmcp_client, tool_error_behavior=ToolErrorBehavior.MODEL_RETRY) + toolset = FastMCPToolset(fastmcp_client, tool_error_behavior='model_retry') async with toolset: tools = await toolset.get_tools(run_context) @@ -432,7 +433,7 @@ async def test_from_mcp_server(self, run_context: RunContext[None]): toolset = FastMCPToolset.from_mcp_server(name='test_server', mcp_server=mcp_server_config) assert isinstance(toolset, FastMCPToolset) - client = get_client_from_toolset(toolset) + client = toolset.fastmcp_client assert isinstance(client.transport, MCPConfigTransport) async def test_from_mcp_config_dict(self, run_context: RunContext[None]): @@ -441,7 +442,7 @@ async def test_from_mcp_config_dict(self, run_context: RunContext[None]): config_dict = {'mcpServers': {'test_server': {'command': 'python', 'args': ['-c', 'print("test")']}}} toolset = FastMCPToolset.from_mcp_config(mcp_config=config_dict) - client = get_client_from_toolset(toolset) + client = toolset.fastmcp_client assert isinstance(client.transport, MCPConfigTransport) async def test_from_mcp_config_mcp_config(self, run_context: RunContext[None]): @@ -452,5 +453,5 @@ async def test_from_mcp_config_mcp_config(self, run_context: RunContext[None]): ) toolset = FastMCPToolset.from_mcp_config(mcp_config=config) - client = get_client_from_toolset(toolset) + client = toolset.fastmcp_client assert isinstance(client.transport, MCPConfigTransport) From f2be96d711bc030c28a0239465e815a78b9528c5 Mon Sep 17 00:00:00 2001 From: William Easton Date: Thu, 11 Sep 2025 08:27:58 -0500 Subject: [PATCH 09/20] Address PR Feedback --- .../pydantic_ai/toolsets/fastmcp.py | 4 +- uv.lock | 52 +++++++++---------- 2 files changed, 29 insertions(+), 27 deletions(-) diff --git a/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py b/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py index 1f6f4bfaed..4e904b277e 100644 --- a/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py +++ b/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py @@ -7,8 +7,8 @@ from typing_extensions import Self +from pydantic_ai import messages from pydantic_ai.exceptions import ModelRetry -from pydantic_ai.mcp import TOOL_SCHEMA_VALIDATOR, messages from pydantic_ai.tools import AgentDepsT, RunContext, ToolDefinition from pydantic_ai.toolsets import AbstractToolset from pydantic_ai.toolsets.abstract import ToolsetTool @@ -27,6 +27,8 @@ Tool as MCPTool, ) + from pydantic_ai.mcp import TOOL_SCHEMA_VALIDATOR + except ImportError as _import_error: raise ImportError( 'Please install the `fastmcp` package to use the FastMCP server, ' diff --git a/uv.lock b/uv.lock index f26aa30fb2..547fccaabf 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.13' and platform_python_implementation == 'PyPy'", @@ -1035,6 +1035,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632, upload-time = "2024-10-05T20:14:57.687Z" }, ] +[[package]] +name = "docker" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "requests" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/9b/4a2ea29aeba62471211598dac5d96825bb49348fa07e906ea930394a83ce/docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c", size = 117834, upload-time = "2024-05-23T11:13:57.216Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774, upload-time = "2024-05-23T11:13:55.01Z" }, +] + [[package]] name = "docstring-parser" version = "0.17.0" @@ -1053,20 +1067,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/44/57/8db39bc5f98f042e0153b1de9fb88e1a409a33cda4dd7f723c2ed71e01f6/docutils-0.22-py3-none-any.whl", hash = "sha256:4ed966a0e96a0477d852f7af31bdcb3adc049fbb35ccba358c2ea8a03287615e", size = 630709, upload-time = "2025-07-29T15:20:28.335Z" }, ] -[[package]] -name = "docker" -version = "7.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pywin32", marker = "sys_platform == 'win32'" }, - { name = "requests" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/91/9b/4a2ea29aeba62471211598dac5d96825bb49348fa07e906ea930394a83ce/docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c", size = 117834, upload-time = "2024-05-23T11:13:57.216Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774, upload-time = "2024-05-23T11:13:55.01Z" }, -] - [[package]] name = "duckdb" version = "1.3.2" @@ -1824,21 +1824,21 @@ wheels = [ ] [[package]] -name = "isodate" -version = "0.7.2" +name = "invoke" +version = "2.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/42/127e6d792884ab860defc3f4d80a8f9812e48ace584ffc5a346de58cdc6c/invoke-2.2.0.tar.gz", hash = "sha256:ee6cbb101af1a859c7fe84f2a264c059020b0cb7fe3535f9424300ab568f6bd5", size = 299835, upload-time = "2023-07-12T18:05:17.998Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, + { url = "https://files.pythonhosted.org/packages/0a/66/7f8c48009c72d73bc6bbe6eb87ac838d6a526146f7dab14af671121eb379/invoke-2.2.0-py3-none-any.whl", hash = "sha256:6ea924cc53d4f78e3d98bc436b08069a03077e6f85ad1ddaa8a116d7dad15820", size = 160274, upload-time = "2023-07-12T18:05:16.294Z" }, ] [[package]] -name = "invoke" -version = "2.2.0" +name = "isodate" +version = "0.7.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f9/42/127e6d792884ab860defc3f4d80a8f9812e48ace584ffc5a346de58cdc6c/invoke-2.2.0.tar.gz", hash = "sha256:ee6cbb101af1a859c7fe84f2a264c059020b0cb7fe3535f9424300ab568f6bd5", size = 299835, upload-time = "2023-07-12T18:05:17.998Z" } +sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/66/7f8c48009c72d73bc6bbe6eb87ac838d6a526146f7dab14af671121eb379/invoke-2.2.0-py3-none-any.whl", hash = "sha256:6ea924cc53d4f78e3d98bc436b08069a03077e6f85ad1ddaa8a116d7dad15820", size = 160274, upload-time = "2023-07-12T18:05:16.294Z" }, + { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, ] [[package]] @@ -3549,7 +3549,7 @@ email = [ name = "pydantic-ai" source = { editable = "." } dependencies = [ - { name = "pydantic-ai-slim", extra = ["ag-ui", "anthropic", "bedrock", "cli", "cohere", "evals", "google", "groq", "huggingface", "logfire", "mcp", "mistral", "openai", "retries", "temporal", "vertexai"] }, + { name = "pydantic-ai-slim", extra = ["ag-ui", "anthropic", "bedrock", "cli", "cohere", "evals", "fastmcp", "google", "groq", "huggingface", "logfire", "mcp", "mistral", "openai", "retries", "temporal", "vertexai"] }, ] [package.optional-dependencies] @@ -3611,7 +3611,7 @@ lint = [ requires-dist = [ { name = "fasta2a", marker = "extra == 'a2a'", specifier = ">=0.4.1" }, { name = "pydantic-ai-examples", marker = "extra == 'examples'", editable = "examples" }, - { name = "pydantic-ai-slim", extras = ["ag-ui", "anthropic", "bedrock", "cli", "cohere", "evals", "google", "groq", "huggingface", "logfire", "mcp", "mistral", "openai", "retries", "temporal", "vertexai"], editable = "pydantic_ai_slim" }, + { name = "pydantic-ai-slim", extras = ["ag-ui", "anthropic", "bedrock", "cli", "cohere", "evals", "fastmcp", "google", "groq", "huggingface", "logfire", "mcp", "mistral", "openai", "retries", "temporal", "vertexai"], editable = "pydantic_ai_slim" }, { name = "pydantic-ai-slim", extras = ["dbos"], marker = "extra == 'dbos'", editable = "pydantic_ai_slim" }, ] provides-extras = ["a2a", "dbos", "examples"] @@ -3821,7 +3821,7 @@ requires-dist = [ { name = "tenacity", marker = "extra == 'retries'", specifier = ">=8.2.3" }, { name = "typing-inspection", specifier = ">=0.4.0" }, ] -provides-extras = ["a2a", "ag-ui", "anthropic", "bedrock", "cli", "cohere", "dbos", "duckduckgo", "evals", "google", "groq", "huggingface", "logfire", "mcp", "mistral", "openai", "retries", "tavily", "temporal", "vertexai"] +provides-extras = ["a2a", "ag-ui", "anthropic", "bedrock", "cli", "cohere", "dbos", "duckduckgo", "evals", "fastmcp", "google", "groq", "huggingface", "logfire", "mcp", "mistral", "openai", "retries", "tavily", "temporal", "vertexai"] [[package]] name = "pydantic-core" From dfcad616a330dfd989ad7ce33a76760130e0cdee Mon Sep 17 00:00:00 2001 From: William Easton Date: Sun, 28 Sep 2025 23:15:37 -0500 Subject: [PATCH 10/20] Address PR Feedback --- .../pydantic_ai/toolsets/fastmcp.py | 188 ++++-------------- tests/test_fastmcp.py | 120 ++++++++--- uv.lock | 6 +- 3 files changed, 133 insertions(+), 181 deletions(-) diff --git a/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py b/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py index 4e904b277e..b81724956a 100644 --- a/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py +++ b/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py @@ -3,8 +3,15 @@ import base64 from asyncio import Lock from contextlib import AsyncExitStack +from dataclasses import dataclass, field +from pathlib import Path from typing import TYPE_CHECKING, Any, Literal +from fastmcp.client.transports import ClientTransport +from fastmcp.mcp_config import MCPConfig +from fastmcp.server import FastMCP +from mcp.server.fastmcp import FastMCP as FastMCP1Server +from pydantic import AnyUrl from typing_extensions import Self from pydantic_ai import messages @@ -15,10 +22,7 @@ try: from fastmcp.client import Client - from fastmcp.client.transports import FastMCPTransport, MCPConfigTransport from fastmcp.exceptions import ToolError - from fastmcp.mcp_config import MCPConfig - from fastmcp.server.server import FastMCP from mcp.types import ( AudioContent, ContentBlock, @@ -37,10 +41,7 @@ if TYPE_CHECKING: - from fastmcp import FastMCP from fastmcp.client.client import CallToolResult - from fastmcp.client.transports import FastMCPTransport - from fastmcp.mcp_config import MCPServerTypes FastMCPToolResult = messages.BinaryContent | dict[str, Any] | str | None @@ -48,46 +49,32 @@ ToolErrorBehavior = Literal['model_retry', 'error'] +@dataclass class FastMCPToolset(AbstractToolset[AgentDepsT]): - """A toolset that uses a FastMCP client as the underlying toolset.""" - - tool_error_behavior: Literal['model_retry', 'error'] - fastmcp_client: Client[Any] - - max_retries: int - - _id: str | None - - _enter_lock: Lock - _running_count: int - _exit_stack: AsyncExitStack | None - - def __init__( - self, - fastmcp_client: Client[Any], - *, - max_retries: int = 2, - tool_error_behavior: ToolErrorBehavior | None = None, - id: str | None = None, - ): - """Build a new FastMCPToolset. - - Args: - fastmcp_client: The FastMCP client to use. - max_retries: The maximum number of retries for each tool during a run. - tool_error_behavior: The behavior to take when a tool error occurs. - id: An optional unique ID for the toolset. A toolset needs to have an ID in order to be used in a durable execution environment like Temporal, - in which case the ID will be used to identify the toolset's activities within the workflow. - """ - self.max_retries = max_retries - self.fastmcp_client = fastmcp_client - self._enter_lock = Lock() - self._running_count = 0 - self._id = id - - self.tool_error_behavior = tool_error_behavior or 'error' - - super().__init__() + """A FastMCP Toolset that uses the FastMCP Client to call tools from a local or remote MCP Server.""" + + mcp: Client[Any] | ClientTransport | FastMCP | FastMCP1Server | AnyUrl | Path | MCPConfig | dict[str, Any] | str + """The FastMCP transport to use. This can be a local or remote MCP Server configuration or a FastMCP Client.""" + + tool_error_behavior: Literal['model_retry', 'error'] = field(default='error') + """The behavior to take when a tool error occurs.""" + + max_retries: int = field(default=2) + """The maximum number of retries to attempt if a tool call fails.""" + + _id: str | None = field(default=None) + + def __post_init__(self): + self._enter_lock: Lock = Lock() + self._running_count: int = 0 + self._exit_stack: AsyncExitStack | None = None + + self._client: Client[Any] + + if isinstance(self.mcp, Client): + self._client = self.mcp + else: + self._client = Client[Any](transport=self.mcp) @property def id(self) -> str | None: @@ -95,9 +82,9 @@ def id(self) -> str | None: async def __aenter__(self) -> Self: async with self._enter_lock: - if self._running_count == 0 and self.fastmcp_client: + if self._running_count == 0 and self._client: self._exit_stack = AsyncExitStack() - await self._exit_stack.enter_async_context(self.fastmcp_client) + await self._exit_stack.enter_async_context(self._client) self._running_count += 1 @@ -114,7 +101,7 @@ async def __aexit__(self, *args: Any) -> bool | None: async def get_tools(self, ctx: RunContext[AgentDepsT]) -> dict[str, ToolsetTool[AgentDepsT]]: async with self: - mcp_tools: list[MCPTool] = await self.fastmcp_client.list_tools() + mcp_tools: list[MCPTool] = await self._client.list_tools() return { tool.name: _convert_mcp_tool_to_toolset_tool(toolset=self, mcp_tool=tool, retries=self.max_retries) @@ -126,114 +113,20 @@ async def call_tool( ) -> Any: async with self: try: - call_tool_result: CallToolResult = await self.fastmcp_client.call_tool(name=name, arguments=tool_args) + call_tool_result: CallToolResult = await self._client.call_tool(name=name, arguments=tool_args) except ToolError as e: if self.tool_error_behavior == 'model_retry': raise ModelRetry(message=str(e)) from e else: raise e - # If any of the results are not text content, let's map them to Pydantic AI binary message parts - if any(not isinstance(part, TextContent) for part in call_tool_result.content): - return _map_fastmcp_tool_results(parts=call_tool_result.content) - - # Otherwise, if we have structured content, return that + # If we have structured content, return that if call_tool_result.structured_content: return call_tool_result.structured_content + # Otherwise, return the content return _map_fastmcp_tool_results(parts=call_tool_result.content) - @classmethod - def from_fastmcp_server( - cls, - fastmcp_server: FastMCP[Any], - *, - tool_error_behavior: ToolErrorBehavior | None = None, - tool_retries: int = 2, - ) -> Self: - """Build a FastMCPToolset from a FastMCP server. - - Example: - ```python - from fastmcp import FastMCP - - from pydantic_ai.toolsets.fastmcp import FastMCPToolset - - fastmcp_server = FastMCP('my_server') - @fastmcp_server.tool() - async def my_tool(a: int, b: int) -> int: - return a + b - - FastMCPToolset.from_fastmcp_server(fastmcp_server=fastmcp_server) - ``` - """ - transport = FastMCPTransport(fastmcp_server) - fastmcp_client: Client[FastMCPTransport] = Client[FastMCPTransport](transport=transport) - return cls(fastmcp_client=fastmcp_client, max_retries=tool_retries, tool_error_behavior=tool_error_behavior) - - @classmethod - def from_mcp_server( - cls, - name: str, - mcp_server: MCPServerTypes | dict[str, Any], - *, - tool_error_behavior: ToolErrorBehavior | None = None, - tool_retries: int = 2, - ) -> Self: - """Build a FastMCPToolset from an individual MCP server configuration. - - Example: - ```python - from pydantic_ai.toolsets.fastmcp import FastMCPToolset - - time_mcp_server = { - 'command': 'uv', - 'args': ['run', 'mcp-run-python', 'stdio'], - } - - FastMCPToolset.from_mcp_server(name='time_server', mcp_server=time_mcp_server) - ``` - """ - mcp_config: MCPConfig = MCPConfig.from_dict(config={name: mcp_server}) - - return cls.from_mcp_config( - mcp_config=mcp_config, tool_error_behavior=tool_error_behavior, max_retries=tool_retries - ) - - @classmethod - def from_mcp_config( - cls, - mcp_config: MCPConfig | dict[str, Any], - *, - tool_error_behavior: ToolErrorBehavior | None = None, - max_retries: int = 2, - ) -> Self: - """Build a FastMCPToolset from an MCP json-derived / dictionary configuration object. - - Example: - ```python - from pydantic_ai.toolsets.fastmcp import FastMCPToolset - - mcp_config = { - 'mcpServers': { - 'first_server': { - 'command': 'uv', - 'args': ['run', 'mcp-run-python', 'stdio'], - }, - 'second_server': { - 'command': 'uv', - 'args': ['run', 'mcp-run-python', 'stdio'], - } - } - } - - FastMCPToolset.from_mcp_config(mcp_config) - ``` - """ - transport: MCPConfigTransport = MCPConfigTransport(config=mcp_config) - fastmcp_client: Client[MCPConfigTransport] = Client[MCPConfigTransport](transport=transport) - return cls(fastmcp_client=fastmcp_client, max_retries=max_retries, tool_error_behavior=tool_error_behavior) - def _convert_mcp_tool_to_toolset_tool( toolset: FastMCPToolset[AgentDepsT], @@ -246,6 +139,11 @@ def _convert_mcp_tool_to_toolset_tool( name=mcp_tool.name, description=mcp_tool.description, parameters_json_schema=mcp_tool.inputSchema, + metadata={ + 'meta': mcp_tool.meta, + 'annotations': mcp_tool.annotations.model_dump() if mcp_tool.annotations else None, + 'output_schema': mcp_tool.outputSchema or None, + }, ), toolset=toolset, max_retries=retries, diff --git a/tests/test_fastmcp.py b/tests/test_fastmcp.py index 74541ba9ad..e4f55ccdc0 100644 --- a/tests/test_fastmcp.py +++ b/tests/test_fastmcp.py @@ -3,6 +3,9 @@ from __future__ import annotations import base64 +import subprocess +from pathlib import Path +from tempfile import TemporaryDirectory from typing import Any import pytest @@ -20,7 +23,6 @@ from fastmcp.client import Client from fastmcp.client.transports import FastMCPTransport, MCPConfigTransport from fastmcp.exceptions import ToolError - from fastmcp.mcp_config import MCPConfig from fastmcp.server.server import FastMCP from mcp.types import ( AudioContent, @@ -126,14 +128,14 @@ class TestFastMCPToolsetInitialization: async def test_init_with_client(self, fastmcp_client: Client[FastMCPTransport]): """Test initialization with a FastMCP client.""" - toolset = FastMCPToolset(fastmcp_client) + toolset = FastMCPToolset(mcp=fastmcp_client) # Test that the client is accessible via the property assert toolset.id is None async def test_init_with_id(self, fastmcp_client: Client[FastMCPTransport]): """Test initialization with an id.""" - toolset = FastMCPToolset(fastmcp_client, id='test_id') + toolset = FastMCPToolset(fastmcp_client, _id='test_id') # Test that the client is accessible via the property assert toolset.id == 'test_id' @@ -143,7 +145,7 @@ async def test_init_with_custom_retries_and_error_behavior(self, fastmcp_client: toolset = FastMCPToolset(fastmcp_client, max_retries=5, tool_error_behavior='model_retry') # Test that the toolset was created successfully - assert toolset.fastmcp_client is fastmcp_client + assert toolset._client is fastmcp_client # type: ignore async def test_id_property(self, fastmcp_client: Client[FastMCPTransport]): """Test that the id property returns None.""" @@ -414,44 +416,96 @@ async def test_call_tool_with_error_behavior_model_retry( class TestFastMCPToolsetFactoryMethods: """Test FastMCP Toolset factory methods.""" - async def test_from_fastmcp_server(self, fastmcp_server: FastMCP, run_context: RunContext[None]): + async def test_python_stdio(self, run_context: RunContext[None]): """Test creating toolset from FastMCP server.""" - toolset = FastMCPToolset.from_fastmcp_server(fastmcp_server) + server_script = """ +from fastmcp import FastMCP - assert isinstance(toolset, FastMCPToolset) - assert toolset.id is None +server = FastMCP('test_server') - async with toolset: - tools = await toolset.get_tools(run_context) - assert 'test_tool' in tools - - async def test_from_mcp_server(self, run_context: RunContext[None]): - """Test creating toolset from MCP server configuration.""" +@server.tool() +async def test_tool(param1: str, param2: int = 0) -> str: + return f'param1={param1}, param2={param2}' - mcp_server_config = {'command': 'python', 'args': ['-c', 'print("test")']} +server.run()""" + with TemporaryDirectory() as temp_dir: + server_py = Path(temp_dir) / 'server.py' + server_py.write_text(server_script) + toolset = FastMCPToolset(mcp=server_py) - toolset = FastMCPToolset.from_mcp_server(name='test_server', mcp_server=mcp_server_config) - assert isinstance(toolset, FastMCPToolset) + assert isinstance(toolset, FastMCPToolset) + assert toolset.id is None - client = toolset.fastmcp_client - assert isinstance(client.transport, MCPConfigTransport) + async with toolset: + tools = await toolset.get_tools(run_context) + assert 'test_tool' in tools + + async def test_sse_transport(self, run_context: RunContext[None]): + """Test creating toolset from stdio transport.""" + server_script = """ +from fastmcp import FastMCP + +server = FastMCP('test_server') + +@server.tool() +async def test_tool(param1: str, param2: int = 0) -> str: + return f'param1={param1}, param2={param2}' + +server.run(transport='sse')""" + with TemporaryDirectory() as temp_dir: + server_py = Path(temp_dir) / 'server.py' + server_py.write_text(server_script) + + with subprocess.Popen(['python', server_py], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True): + toolset = FastMCPToolset(mcp='http://localhost:8000/sse') + async with toolset: + tools = await toolset.get_tools( + RunContext(deps=None, model=TestModel(), usage=RunUsage(), prompt=None, messages=[], run_step=0) + ) + assert 'test_tool' in tools + + async def test_streamable_http_transport(self, run_context: RunContext[None]): + """Test creating toolset from stdio transport.""" + server_script = """ +from fastmcp import FastMCP +import asyncio + +server = FastMCP('test_server') + +@server.tool() +async def test_tool(param1: str, param2: int = 0) -> str: + return f'param1={param1}, param2={param2}' + +asyncio.run(server.run_streamable_http_async(host='localhost', port=8001)) +""" + with TemporaryDirectory() as temp_dir: + server_py = Path(temp_dir) / 'server.py' + server_py.write_text(server_script) + + with subprocess.Popen(['python', server_py], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True): + toolset = FastMCPToolset(mcp='http://localhost:8001/mcp') + async with toolset: + tools = await toolset.get_tools(run_context) + assert 'test_tool' in tools + + async def test_in_memory_transport(self, run_context: RunContext[None]): + """Test creating toolset from stdio transport.""" + fastmcp_server = FastMCP('test_server') + + @fastmcp_server.tool() + def test_tool(param1: str, param2: int = 0) -> str: + return f'param1={param1}, param2={param2}' + + toolset = FastMCPToolset(mcp=fastmcp_server) + async with toolset: + tools = await toolset.get_tools(run_context) + assert 'test_tool' in tools - async def test_from_mcp_config_dict(self, run_context: RunContext[None]): + async def test_from_mcp_config_dict(self): """Test creating toolset from MCP config dictionary.""" config_dict = {'mcpServers': {'test_server': {'command': 'python', 'args': ['-c', 'print("test")']}}} - toolset = FastMCPToolset.from_mcp_config(mcp_config=config_dict) - client = toolset.fastmcp_client - assert isinstance(client.transport, MCPConfigTransport) - - async def test_from_mcp_config_mcp_config(self, run_context: RunContext[None]): - """Test creating toolset from MCPConfig object.""" - # Create a real MCPConfig object - config = MCPConfig.from_dict( - {'mcpServers': {'test_server': {'command': 'python', 'args': ['-c', 'print("test")']}}} - ) - - toolset = FastMCPToolset.from_mcp_config(mcp_config=config) - client = toolset.fastmcp_client + toolset = FastMCPToolset(mcp=config_dict) + client = toolset._client # type: ignore assert isinstance(client.transport, MCPConfigTransport) diff --git a/uv.lock b/uv.lock index 26a86a22c5..9151ab5ebb 100644 --- a/uv.lock +++ b/uv.lock @@ -1276,7 +1276,7 @@ wheels = [ [[package]] name = "fastmcp" -version = "2.12.2" +version = "2.12.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "authlib" }, @@ -1291,9 +1291,9 @@ dependencies = [ { name = "python-dotenv" }, { name = "rich" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/35/8a/c46759bb41a53187191e5b3d963c0bde54783ecc89186a93c4947607b8e4/fastmcp-2.12.2.tar.gz", hash = "sha256:6d13e2f9be57b99763fc22485f9f603daa23bfbca35a8172baa43b283d6fc1ff", size = 5244547, upload-time = "2025-09-03T21:28:09.869Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/b2/57845353a9bc63002995a982e66f3d0be4ec761e7bcb89e7d0638518d42a/fastmcp-2.12.4.tar.gz", hash = "sha256:b55fe89537038f19d0f4476544f9ca5ac171033f61811cc8f12bdeadcbea5016", size = 7167745, upload-time = "2025-09-26T16:43:27.71Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/0a/7a8d564b1b9909dbfc36eb93d76410a4acfada6b1e13ee451a753bb6dbc2/fastmcp-2.12.2-py3-none-any.whl", hash = "sha256:0b58d68e819c82078d1fd51989d3d81f2be7382d527308b06df55f4d0a4ec94f", size = 312029, upload-time = "2025-09-03T21:28:08.62Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c7/562ff39f25de27caec01e4c1e88cbb5fcae5160802ba3d90be33165df24f/fastmcp-2.12.4-py3-none-any.whl", hash = "sha256:56188fbbc1a9df58c537063f25958c57b5c4d715f73e395c41b51550b247d140", size = 329090, upload-time = "2025-09-26T16:43:25.314Z" }, ] [[package]] From 8776a679f7c593bd85119a8037d8c9ff59653398 Mon Sep 17 00:00:00 2001 From: William Easton Date: Sun, 28 Sep 2025 23:36:43 -0500 Subject: [PATCH 11/20] add't updates --- docs/toolsets.md | 45 ++++++++++++++----- .../pydantic_ai/toolsets/fastmcp.py | 13 ++++-- 2 files changed, 44 insertions(+), 14 deletions(-) diff --git a/docs/toolsets.md b/docs/toolsets.md index c99becd5dd..69ab48aae0 100644 --- a/docs/toolsets.md +++ b/docs/toolsets.md @@ -665,27 +665,32 @@ See the [MCP Client](./mcp/client.md) documentation for how to use MCP servers w ### FastMCP Tools {#fastmcp-tools} -If you'd like to use tools from a [FastMCP](https://fastmcp.dev) Server, Client, or JSON MCP Configuration with Pydantic AI, you can use the [`FastMCPToolset`][pydantic_ai.toolsets.fastmcp.FastMCPToolset] [toolset](toolsets.md). +The [FastMCP](https://fastmcp.dev) Client can also be used with Pydantic AI with the provided [`FastMCPToolset`][pydantic_ai.toolsets.fastmcp.FastMCPToolset] [toolset](toolsets.md). To use the `FastMCPToolset`, you will need to install `pydantic-ai-slim[fastmcp]`. -```python {test="skip"} -from fastmcp import FastMCP +A FastMCP Toolset can be created from: +- A FastMCP Client: `FastMCPToolset(mcp=Client(...))` +- A FastMCP Transport: `FastMCPToolset(mcp=StdioTransport(command='uv', args=['run', 'mcp-run-python', 'stdio']))` +- A FastMCP Server: `FastMCPToolset(mcp=FastMCP('my_server'))` +- An HTTP URL: `FastMCPToolset(mcp='http://localhost:8000/mcp')` +- An SSE URL: `FastMCPToolset(mcp='http://localhost:8000/sse')` +- A Python Script: `FastMCPToolset(mcp='my_server.py')` +- A Node.js Script: `FastMCPToolset(mcp='my_server.js')` +- A JSON MCP Configuration: `FastMCPToolset(mcp={'mcpServers': {'my_server': {'command': 'python', 'args': ['-c', 'print("test")']}}})` +Connecting your agent to an HTTP MCP Server is as simple as: + +```python {test="skip"} from pydantic_ai import Agent from pydantic_ai.toolsets.fastmcp import FastMCPToolset -fastmcp_server = FastMCP('my_server') -@fastmcp_server.tool() -async def my_tool(a: int, b: int) -> int: - return a + b - -toolset = FastMCPToolset.from_fastmcp_server(fastmcp_server) +toolset = FastMCPToolset(mcp='http://localhost:8000/mcp') agent = Agent('openai:gpt-5', toolsets=[toolset]) ``` -You can also use the [`FastMCPToolset`][pydantic_ai.toolsets.fastmcp.FastMCPToolset] to create a toolset from a JSON MCP Configuration. FastMCP supports additional capabilities on top of the MCP specification, like Tool Transformation in the MCP configuration that you can take advantage of with the `FastMCPToolset`. +You can also create a toolset from a JSON MCP Configuration. FastMCP supports additional capabilities on top of the MCP specification, like Tool Transformation in the MCP configuration that you can take advantage of with the `FastMCPToolset`. ```python {test="skip"} from pydantic_ai import Agent @@ -700,7 +705,25 @@ mcp_config = { } } -toolset = FastMCPToolset.from_mcp_config(mcp_config) +toolset = FastMCPToolset(mcp=mcp_config) + +agent = Agent('openai:gpt-5', toolsets=[toolset]) +``` + +Toolsets can also be created from a FastMCP Server: + +```python {test="skip"} +from fastmcp import FastMCP + +from pydantic_ai import Agent +from pydantic_ai.toolsets.fastmcp import FastMCPToolset + +fastmcp_server = FastMCP('my_server') +@fastmcp_server.tool() +async def my_tool(a: int, b: int) -> int: + return a + b + +toolset = FastMCPToolset(mcp=fastmcp_server) agent = Agent('openai:gpt-5', toolsets=[toolset]) ``` diff --git a/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py b/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py index b81724956a..ab83f7f934 100644 --- a/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py +++ b/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py @@ -3,7 +3,7 @@ import base64 from asyncio import Lock from contextlib import AsyncExitStack -from dataclasses import dataclass, field +from dataclasses import KW_ONLY, dataclass, field from pathlib import Path from typing import TYPE_CHECKING, Any, Literal @@ -51,10 +51,17 @@ @dataclass class FastMCPToolset(AbstractToolset[AgentDepsT]): - """A FastMCP Toolset that uses the FastMCP Client to call tools from a local or remote MCP Server.""" + """A FastMCP Toolset that uses the FastMCP Client to call tools from a local or remote MCP Server. + + The Toolset can accept a FastMCP Client, a FastMCP Transport, or any other object which a FastMCP Transport can be created from. + + See https://gofastmcp.com/clients/transports for a full list of transports available. + """ mcp: Client[Any] | ClientTransport | FastMCP | FastMCP1Server | AnyUrl | Path | MCPConfig | dict[str, Any] | str - """The FastMCP transport to use. This can be a local or remote MCP Server configuration or a FastMCP Client.""" + """The FastMCP transport to use. This can be a local or remote MCP Server configuration, a transport string, or a FastMCP Client.""" + + _: KW_ONLY tool_error_behavior: Literal['model_retry', 'error'] = field(default='error') """The behavior to take when a tool error occurs.""" From a1712725ea455ac4ff68a997b7f6c93276c82dbc Mon Sep 17 00:00:00 2001 From: William Easton Date: Mon, 29 Sep 2025 08:13:11 -0500 Subject: [PATCH 12/20] Add transport tests --- tests/test_fastmcp.py | 142 +++++++++++++++++++++++++++++------------- 1 file changed, 97 insertions(+), 45 deletions(-) diff --git a/tests/test_fastmcp.py b/tests/test_fastmcp.py index e4f55ccdc0..04afce51a3 100644 --- a/tests/test_fastmcp.py +++ b/tests/test_fastmcp.py @@ -3,12 +3,13 @@ from __future__ import annotations import base64 -import subprocess from pathlib import Path from tempfile import TemporaryDirectory from typing import Any import pytest +from fastmcp.client import PythonStdioTransport, SSETransport +from fastmcp.client.transports import ClientTransport from inline_snapshot import snapshot from pydantic_ai._run_context import RunContext @@ -21,7 +22,13 @@ with try_import() as imports_successful: from fastmcp.client import Client - from fastmcp.client.transports import FastMCPTransport, MCPConfigTransport + from fastmcp.client.transports import ( + FastMCPTransport, + MCPConfigTransport, + NodeStdioTransport, + StdioTransport, + StreamableHttpTransport, + ) from fastmcp.exceptions import ToolError from fastmcp.server.server import FastMCP from mcp.types import ( @@ -42,6 +49,16 @@ ] +def get_client_from_toolset(toolset: FastMCPToolset[None]) -> Client[FastMCPTransport]: + """Get the client from the toolset.""" + return toolset._client # type: ignore + + +def get_transport_from_toolset(toolset: FastMCPToolset[None]) -> ClientTransport: + """Get the transport from the toolset.""" + return get_client_from_toolset(toolset).transport + + @pytest.fixture async def fastmcp_server() -> FastMCP: """Create a real in-memory FastMCP server for testing.""" @@ -440,53 +457,88 @@ async def test_tool(param1: str, param2: int = 0) -> str: tools = await toolset.get_tools(run_context) assert 'test_tool' in tools - async def test_sse_transport(self, run_context: RunContext[None]): - """Test creating toolset from stdio transport.""" - server_script = """ -from fastmcp import FastMCP + async def test_transports(self): + """Test creating toolset from different transports.""" + toolset = FastMCPToolset(mcp='http://localhost:8000/mcp') + assert isinstance(get_transport_from_toolset(toolset), StreamableHttpTransport) -server = FastMCP('test_server') + toolset = FastMCPToolset(mcp='http://localhost:8000/sse') + assert isinstance(get_transport_from_toolset(toolset), SSETransport) -@server.tool() -async def test_tool(param1: str, param2: int = 0) -> str: - return f'param1={param1}, param2={param2}' + toolset = FastMCPToolset(mcp=StdioTransport(command='python', args=['-c', 'print("test")'])) + assert isinstance(get_transport_from_toolset(toolset), StdioTransport) -server.run(transport='sse')""" with TemporaryDirectory() as temp_dir: - server_py = Path(temp_dir) / 'server.py' - server_py.write_text(server_script) - - with subprocess.Popen(['python', server_py], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True): - toolset = FastMCPToolset(mcp='http://localhost:8000/sse') - async with toolset: - tools = await toolset.get_tools( - RunContext(deps=None, model=TestModel(), usage=RunUsage(), prompt=None, messages=[], run_step=0) - ) - assert 'test_tool' in tools - - async def test_streamable_http_transport(self, run_context: RunContext[None]): - """Test creating toolset from stdio transport.""" - server_script = """ -from fastmcp import FastMCP -import asyncio - -server = FastMCP('test_server') - -@server.tool() -async def test_tool(param1: str, param2: int = 0) -> str: - return f'param1={param1}, param2={param2}' - -asyncio.run(server.run_streamable_http_async(host='localhost', port=8001)) -""" - with TemporaryDirectory() as temp_dir: - server_py = Path(temp_dir) / 'server.py' - server_py.write_text(server_script) - - with subprocess.Popen(['python', server_py], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True): - toolset = FastMCPToolset(mcp='http://localhost:8001/mcp') - async with toolset: - tools = await toolset.get_tools(run_context) - assert 'test_tool' in tools + server_py: Path = Path(temp_dir) / 'server.py' + server_py.write_text(data='') + toolset = FastMCPToolset(mcp=server_py) + assert isinstance(get_transport_from_toolset(toolset), PythonStdioTransport) + toolset = FastMCPToolset(mcp=str(server_py)) + assert isinstance(get_transport_from_toolset(toolset), PythonStdioTransport) + + server_js: Path = Path(temp_dir) / 'server.js' + server_js.write_text(data='') + toolset = FastMCPToolset(mcp=server_js) + assert isinstance(get_transport_from_toolset(toolset), NodeStdioTransport) + toolset = FastMCPToolset(mcp=str(server_js)) + assert isinstance(get_transport_from_toolset(toolset), NodeStdioTransport) + + toolset = FastMCPToolset( + mcp={'mcpServers': {'test_server': {'command': 'python', 'args': ['-c', 'print("test")']}}} + ) + assert isinstance(get_transport_from_toolset(toolset), MCPConfigTransport) + + # async def test_sse_transport(self, run_context: RunContext[None]): + # """Test creating toolset from stdio transport.""" + # server_script = """ + # from fastmcp import FastMCP + + # server = FastMCP('test_server') + + # @server.tool() + # async def test_tool(param1: str, param2: int = 0) -> str: + # return f'param1={param1}, param2={param2}' + + # server.run(transport='sse')""" + # with TemporaryDirectory() as temp_dir: + # server_py = Path(temp_dir) / 'server.py' + # server_py.write_text(server_script) + + # with subprocess.Popen(['python', server_py], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True): + # toolset = FastMCPToolset(mcp='http://localhost:8000/sse') + # async with toolset: + # tools = await toolset.get_tools( + # RunContext(deps=None, model=TestModel(), usage=RunUsage(), prompt=None, messages=[], run_step=0) + # ) + # assert 'test_tool' in tools + + # async def test_streamable_http_transport(self, run_context: RunContext[None]): + # """Test creating toolset from stdio transport.""" + + # toolset = FastMCPToolset(mcp='http://localhost:8001/mcp') + + # assert isinstance(get_client_from_toolset(toolset).transport, StreamableHttpTransport) + # server_script = """ + # from fastmcp import FastMCP + # import asyncio + + # server = FastMCP('test_server') + + # @server.tool() + # async def test_tool(param1: str, param2: int = 0) -> str: + # return f'param1={param1}, param2={param2}' + + # asyncio.run(server.run_streamable_http_async(host='localhost', port=8001)) + # """ + # with TemporaryDirectory() as temp_dir: + # server_py = Path(temp_dir) / 'server.py' + # server_py.write_text(server_script) + + # with subprocess.Popen(['python', server_py], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True): + # toolset = FastMCPToolset(mcp='http://localhost:8001/mcp') + # async with toolset: + # tools = await toolset.get_tools(run_context) + # assert 'test_tool' in tools async def test_in_memory_transport(self, run_context: RunContext[None]): """Test creating toolset from stdio transport.""" From 6ea1dd321d77abd97739dceee54b499e432861c5 Mon Sep 17 00:00:00 2001 From: William Easton Date: Wed, 1 Oct 2025 09:41:42 -0500 Subject: [PATCH 13/20] Simplify init creation --- docs/toolsets.md | 22 ++--- .../pydantic_ai/toolsets/fastmcp.py | 82 +++++++++++++++---- pydantic_ai_slim/pydantic_ai/toolsets/test.py | 11 +++ tests/test_fastmcp.py | 78 ++++++++---------- 4 files changed, 122 insertions(+), 71 deletions(-) create mode 100644 pydantic_ai_slim/pydantic_ai/toolsets/test.py diff --git a/docs/toolsets.md b/docs/toolsets.md index 69ab48aae0..da7f76d463 100644 --- a/docs/toolsets.md +++ b/docs/toolsets.md @@ -670,14 +670,14 @@ The [FastMCP](https://fastmcp.dev) Client can also be used with Pydantic AI with To use the `FastMCPToolset`, you will need to install `pydantic-ai-slim[fastmcp]`. A FastMCP Toolset can be created from: -- A FastMCP Client: `FastMCPToolset(mcp=Client(...))` -- A FastMCP Transport: `FastMCPToolset(mcp=StdioTransport(command='uv', args=['run', 'mcp-run-python', 'stdio']))` -- A FastMCP Server: `FastMCPToolset(mcp=FastMCP('my_server'))` -- An HTTP URL: `FastMCPToolset(mcp='http://localhost:8000/mcp')` -- An SSE URL: `FastMCPToolset(mcp='http://localhost:8000/sse')` -- A Python Script: `FastMCPToolset(mcp='my_server.py')` -- A Node.js Script: `FastMCPToolset(mcp='my_server.js')` -- A JSON MCP Configuration: `FastMCPToolset(mcp={'mcpServers': {'my_server': {'command': 'python', 'args': ['-c', 'print("test")']}}})` +- A FastMCP Client: `FastMCPToolset(client=Client(...))` +- A FastMCP Transport: `FastMCPToolset(StdioTransport(command='uv', args=['run', 'mcp-run-python', 'stdio']))` +- A FastMCP Server: `FastMCPToolset(FastMCP('my_server'))` +- An HTTP URL: `FastMCPToolset('http://localhost:8000/mcp')` +- An SSE URL: `FastMCPToolset('http://localhost:8000/sse')` +- A Python Script: `FastMCPToolset('my_server.py')` +- A Node.js Script: `FastMCPToolset('my_server.js')` +- A JSON MCP Configuration: `FastMCPToolset({'mcpServers': {'my_server': {'command': 'python', 'args': ['-c', 'print("test")']}}})` Connecting your agent to an HTTP MCP Server is as simple as: @@ -685,7 +685,7 @@ Connecting your agent to an HTTP MCP Server is as simple as: from pydantic_ai import Agent from pydantic_ai.toolsets.fastmcp import FastMCPToolset -toolset = FastMCPToolset(mcp='http://localhost:8000/mcp') +toolset = FastMCPToolset('http://localhost:8000/mcp') agent = Agent('openai:gpt-5', toolsets=[toolset]) ``` @@ -705,7 +705,7 @@ mcp_config = { } } -toolset = FastMCPToolset(mcp=mcp_config) +toolset = FastMCPToolset(mcp_config) agent = Agent('openai:gpt-5', toolsets=[toolset]) ``` @@ -723,7 +723,7 @@ fastmcp_server = FastMCP('my_server') async def my_tool(a: int, b: int) -> int: return a + b -toolset = FastMCPToolset(mcp=fastmcp_server) +toolset = FastMCPToolset(fastmcp_server) agent = Agent('openai:gpt-5', toolsets=[toolset]) ``` diff --git a/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py b/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py index ab83f7f934..bc8e718844 100644 --- a/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py +++ b/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py @@ -5,7 +5,7 @@ from contextlib import AsyncExitStack from dataclasses import KW_ONLY, dataclass, field from pathlib import Path -from typing import TYPE_CHECKING, Any, Literal +from typing import TYPE_CHECKING, Any, Literal, overload from fastmcp.client.transports import ClientTransport from fastmcp.mcp_config import MCPConfig @@ -58,7 +58,7 @@ class FastMCPToolset(AbstractToolset[AgentDepsT]): See https://gofastmcp.com/clients/transports for a full list of transports available. """ - mcp: Client[Any] | ClientTransport | FastMCP | FastMCP1Server | AnyUrl | Path | MCPConfig | dict[str, Any] | str + client: Client[Any] """The FastMCP transport to use. This can be a local or remote MCP Server configuration, a transport string, or a FastMCP Client.""" _: KW_ONLY @@ -71,27 +71,79 @@ class FastMCPToolset(AbstractToolset[AgentDepsT]): _id: str | None = field(default=None) - def __post_init__(self): + @overload + def __init__( + self, + *, + client: Client[Any], + max_retries: int = 2, + tool_error_behavior: Literal['model_retry', 'error'] = 'error', + id: str | None = None, + ) -> None: ... + + @overload + def __init__( + self, + transport: ClientTransport + | FastMCP + | FastMCP1Server + | AnyUrl + | Path + | MCPConfig + | dict[str, Any] + | str + | None = None, + *, + max_retries: int = 2, + tool_error_behavior: Literal['model_retry', 'error'] = 'error', + id: str | None = None, + ) -> None: ... + + def __init__( + self, + transport: ClientTransport + | FastMCP + | FastMCP1Server + | AnyUrl + | Path + | MCPConfig + | dict[str, Any] + | str + | None = None, + *, + client: Client[Any] | None = None, + max_retries: int = 2, + tool_error_behavior: Literal['model_retry', 'error'] = 'error', + id: str | None = None, + ) -> None: + if not client and not transport: + raise ValueError('Either client or transport must be provided') + + if client and transport: + raise ValueError('Either client or transport must be provided, not both') + + if client: + self.client = client + else: + self.client = Client[Any](transport=transport) + + self._id = id + self.max_retries = max_retries + self.tool_error_behavior = tool_error_behavior + self._enter_lock: Lock = Lock() self._running_count: int = 0 self._exit_stack: AsyncExitStack | None = None - self._client: Client[Any] - - if isinstance(self.mcp, Client): - self._client = self.mcp - else: - self._client = Client[Any](transport=self.mcp) - @property def id(self) -> str | None: return self._id async def __aenter__(self) -> Self: async with self._enter_lock: - if self._running_count == 0 and self._client: + if self._running_count == 0 and self.client: self._exit_stack = AsyncExitStack() - await self._exit_stack.enter_async_context(self._client) + await self._exit_stack.enter_async_context(self.client) self._running_count += 1 @@ -108,7 +160,7 @@ async def __aexit__(self, *args: Any) -> bool | None: async def get_tools(self, ctx: RunContext[AgentDepsT]) -> dict[str, ToolsetTool[AgentDepsT]]: async with self: - mcp_tools: list[MCPTool] = await self._client.list_tools() + mcp_tools: list[MCPTool] = await self.client.list_tools() return { tool.name: _convert_mcp_tool_to_toolset_tool(toolset=self, mcp_tool=tool, retries=self.max_retries) @@ -120,7 +172,7 @@ async def call_tool( ) -> Any: async with self: try: - call_tool_result: CallToolResult = await self._client.call_tool(name=name, arguments=tool_args) + call_tool_result: CallToolResult = await self.client.call_tool(name=name, arguments=tool_args) except ToolError as e: if self.tool_error_behavior == 'model_retry': raise ModelRetry(message=str(e)) from e @@ -176,4 +228,4 @@ def _map_fastmcp_tool_result(part: ContentBlock) -> FastMCPToolResult: return messages.BinaryContent(data=base64.b64decode(part.data), media_type=part.mimeType) msg = f'Unsupported/Unknown content block type: {type(part)}' # pragma: no cover - raise ValueError(msg) # pragma: no cover + raise ValueError(msg) # pragma: no cover) diff --git a/pydantic_ai_slim/pydantic_ai/toolsets/test.py b/pydantic_ai_slim/pydantic_ai/toolsets/test.py new file mode 100644 index 0000000000..f7889a831a --- /dev/null +++ b/pydantic_ai_slim/pydantic_ai/toolsets/test.py @@ -0,0 +1,11 @@ +from pydantic_ai import Agent +from pydantic_ai.toolsets.fastmcp import FastMCPToolset +from fastmcp.client.transports import StdioTransport + +server = StdioTransport( # (1)! + command='uv', args=['run', 'mcp-run-python', 'stdio'] +) +agent = Agent('openai:gpt-4o', toolsets=[FastMCPToolset(mcp=server)]) + +async def main(): + async with agent: # (2)! \ No newline at end of file diff --git a/tests/test_fastmcp.py b/tests/test_fastmcp.py index 04afce51a3..16751d6ecf 100644 --- a/tests/test_fastmcp.py +++ b/tests/test_fastmcp.py @@ -9,7 +9,6 @@ import pytest from fastmcp.client import PythonStdioTransport, SSETransport -from fastmcp.client.transports import ClientTransport from inline_snapshot import snapshot from pydantic_ai._run_context import RunContext @@ -49,16 +48,6 @@ ] -def get_client_from_toolset(toolset: FastMCPToolset[None]) -> Client[FastMCPTransport]: - """Get the client from the toolset.""" - return toolset._client # type: ignore - - -def get_transport_from_toolset(toolset: FastMCPToolset[None]) -> ClientTransport: - """Get the transport from the toolset.""" - return get_client_from_toolset(toolset).transport - - @pytest.fixture async def fastmcp_server() -> FastMCP: """Create a real in-memory FastMCP server for testing.""" @@ -145,28 +134,28 @@ class TestFastMCPToolsetInitialization: async def test_init_with_client(self, fastmcp_client: Client[FastMCPTransport]): """Test initialization with a FastMCP client.""" - toolset = FastMCPToolset(mcp=fastmcp_client) + toolset = FastMCPToolset(client=fastmcp_client) # Test that the client is accessible via the property assert toolset.id is None async def test_init_with_id(self, fastmcp_client: Client[FastMCPTransport]): """Test initialization with an id.""" - toolset = FastMCPToolset(fastmcp_client, _id='test_id') + toolset = FastMCPToolset(client=fastmcp_client, id='test_id') # Test that the client is accessible via the property assert toolset.id == 'test_id' async def test_init_with_custom_retries_and_error_behavior(self, fastmcp_client: Client[FastMCPTransport]): """Test initialization with custom retries and error behavior.""" - toolset = FastMCPToolset(fastmcp_client, max_retries=5, tool_error_behavior='model_retry') + toolset = FastMCPToolset(client=fastmcp_client, max_retries=5, tool_error_behavior='model_retry') # Test that the toolset was created successfully - assert toolset._client is fastmcp_client # type: ignore + assert toolset.client is fastmcp_client async def test_id_property(self, fastmcp_client: Client[FastMCPTransport]): """Test that the id property returns None.""" - toolset = FastMCPToolset(fastmcp_client) + toolset = FastMCPToolset(client=fastmcp_client) assert toolset.id is None @@ -177,7 +166,7 @@ async def test_context_manager_single_enter_exit( self, fastmcp_client: Client[FastMCPTransport], run_context: RunContext[None] ): """Test single enter/exit cycle.""" - toolset = FastMCPToolset(fastmcp_client) + toolset = FastMCPToolset(client=fastmcp_client) async with toolset: # Test that we can get tools when the context is active @@ -191,7 +180,7 @@ async def test_context_manager_no_enter( self, fastmcp_client: Client[FastMCPTransport], run_context: RunContext[None] ): """Test no enter/exit cycle.""" - toolset = FastMCPToolset(fastmcp_client) + toolset = FastMCPToolset(client=fastmcp_client) # Test that we can get tools when the context is not active tools = await toolset.get_tools(run_context) @@ -202,7 +191,7 @@ async def test_context_manager_nested_enter_exit( self, fastmcp_client: Client[FastMCPTransport], run_context: RunContext[None] ): """Test nested enter/exit cycles.""" - toolset = FastMCPToolset(fastmcp_client) + toolset = FastMCPToolset(client=fastmcp_client) async with toolset: tools1 = await toolset.get_tools(run_context) @@ -223,7 +212,7 @@ async def test_get_tools( run_context: RunContext[None], ): """Test getting tools from the FastMCP client.""" - toolset = FastMCPToolset(fastmcp_client) + toolset = FastMCPToolset(client=fastmcp_client) async with toolset: tools = await toolset.get_tools(run_context) @@ -260,7 +249,7 @@ async def test_get_tools_with_empty_server(self, run_context: RunContext[None]): """Test getting tools from an empty FastMCP server.""" empty_server = FastMCP('empty_server') empty_client = Client(transport=empty_server) - toolset = FastMCPToolset(empty_client) + toolset = FastMCPToolset(client=empty_client) async with toolset: tools = await toolset.get_tools(run_context) @@ -273,7 +262,7 @@ class TestFastMCPToolsetToolCalling: @pytest.fixture async def fastmcp_toolset(self, fastmcp_client: Client[FastMCPTransport]) -> FastMCPToolset[None]: """Create a FastMCP Toolset.""" - return FastMCPToolset(fastmcp_client) + return FastMCPToolset(client=fastmcp_client) async def test_call_tool_success( self, @@ -420,7 +409,7 @@ async def test_call_tool_with_error_behavior_model_retry( run_context: RunContext[None], ): """Test tool call with error behavior set to model retry.""" - toolset = FastMCPToolset(fastmcp_client, tool_error_behavior='model_retry') + toolset = FastMCPToolset(client=fastmcp_client, tool_error_behavior='model_retry') async with toolset: tools = await toolset.get_tools(run_context) @@ -448,7 +437,7 @@ async def test_tool(param1: str, param2: int = 0) -> str: with TemporaryDirectory() as temp_dir: server_py = Path(temp_dir) / 'server.py' server_py.write_text(server_script) - toolset = FastMCPToolset(mcp=server_py) + toolset = FastMCPToolset(server_py) assert isinstance(toolset, FastMCPToolset) assert toolset.id is None @@ -459,34 +448,34 @@ async def test_tool(param1: str, param2: int = 0) -> str: async def test_transports(self): """Test creating toolset from different transports.""" - toolset = FastMCPToolset(mcp='http://localhost:8000/mcp') - assert isinstance(get_transport_from_toolset(toolset), StreamableHttpTransport) + toolset = FastMCPToolset('http://localhost:8000/mcp') + assert isinstance(toolset.client.transport, StreamableHttpTransport) - toolset = FastMCPToolset(mcp='http://localhost:8000/sse') - assert isinstance(get_transport_from_toolset(toolset), SSETransport) + toolset = FastMCPToolset('http://localhost:8000/sse') + assert isinstance(toolset.client.transport, SSETransport) - toolset = FastMCPToolset(mcp=StdioTransport(command='python', args=['-c', 'print("test")'])) - assert isinstance(get_transport_from_toolset(toolset), StdioTransport) + toolset = FastMCPToolset(StdioTransport(command='python', args=['-c', 'print("test")'])) + assert isinstance(toolset.client.transport, StdioTransport) with TemporaryDirectory() as temp_dir: server_py: Path = Path(temp_dir) / 'server.py' server_py.write_text(data='') - toolset = FastMCPToolset(mcp=server_py) - assert isinstance(get_transport_from_toolset(toolset), PythonStdioTransport) - toolset = FastMCPToolset(mcp=str(server_py)) - assert isinstance(get_transport_from_toolset(toolset), PythonStdioTransport) + toolset = FastMCPToolset(server_py) + assert isinstance(toolset.client.transport, PythonStdioTransport) + toolset = FastMCPToolset(str(server_py)) + assert isinstance(toolset.client.transport, PythonStdioTransport) server_js: Path = Path(temp_dir) / 'server.js' server_js.write_text(data='') - toolset = FastMCPToolset(mcp=server_js) - assert isinstance(get_transport_from_toolset(toolset), NodeStdioTransport) - toolset = FastMCPToolset(mcp=str(server_js)) - assert isinstance(get_transport_from_toolset(toolset), NodeStdioTransport) + toolset = FastMCPToolset(server_js) + assert isinstance(toolset.client.transport, NodeStdioTransport) + toolset = FastMCPToolset(str(server_js)) + assert isinstance(toolset.client.transport, NodeStdioTransport) toolset = FastMCPToolset( - mcp={'mcpServers': {'test_server': {'command': 'python', 'args': ['-c', 'print("test")']}}} + {'mcpServers': {'test_server': {'command': 'python', 'args': ['-c', 'print("test")']}}} ) - assert isinstance(get_transport_from_toolset(toolset), MCPConfigTransport) + assert isinstance(toolset.client.transport, MCPConfigTransport) # async def test_sse_transport(self, run_context: RunContext[None]): # """Test creating toolset from stdio transport.""" @@ -545,10 +534,9 @@ async def test_in_memory_transport(self, run_context: RunContext[None]): fastmcp_server = FastMCP('test_server') @fastmcp_server.tool() - def test_tool(param1: str, param2: int = 0) -> str: - return f'param1={param1}, param2={param2}' + def test_tool(param1: str, param2: int = 0) -> str: ... - toolset = FastMCPToolset(mcp=fastmcp_server) + toolset = FastMCPToolset(fastmcp_server) async with toolset: tools = await toolset.get_tools(run_context) assert 'test_tool' in tools @@ -558,6 +546,6 @@ async def test_from_mcp_config_dict(self): config_dict = {'mcpServers': {'test_server': {'command': 'python', 'args': ['-c', 'print("test")']}}} - toolset = FastMCPToolset(mcp=config_dict) - client = toolset._client # type: ignore + toolset = FastMCPToolset(config_dict) + client = toolset.client assert isinstance(client.transport, MCPConfigTransport) From 81d004d454abd75adf0fcd21d53b12cc0f719639 Mon Sep 17 00:00:00 2001 From: William Easton Date: Wed, 1 Oct 2025 09:54:29 -0500 Subject: [PATCH 14/20] Update lock --- uv.lock | 558 +++++++++++++++++++++----------------------------------- 1 file changed, 213 insertions(+), 345 deletions(-) diff --git a/uv.lock b/uv.lock index 9151ab5ebb..37d3f006ff 100644 --- a/uv.lock +++ b/uv.lock @@ -146,21 +146,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/6a/bc7e17a3e87a2985d3e8f4da4cd0f481060eb78fb08596c42be62c90a4d9/aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5", size = 7597, upload-time = "2024-12-13T17:10:38.469Z" }, ] -[[package]] -name = "alembic" -version = "1.16.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mako" }, - { name = "sqlalchemy" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9a/ca/4dc52902cf3491892d464f5265a81e9dff094692c8a049a3ed6a05fe7ee8/alembic-1.16.5.tar.gz", hash = "sha256:a88bb7f6e513bd4301ecf4c7f2206fe93f9913f9b48dac3b78babde2d6fe765e", size = 1969868, upload-time = "2025-08-27T18:02:05.668Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/39/4a/4c61d4c84cfd9befb6fa08a702535b27b21fff08c946bc2f6139decbf7f7/alembic-1.16.5-py3-none-any.whl", hash = "sha256:e845dfe090c5ffa7b92593ae6687c5cb1a101e91fa53868497dbd79847f9dbe3", size = 247355, upload-time = "2025-08-27T18:02:07.37Z" }, -] - [[package]] name = "algoliasearch" version = "4.13.2" @@ -189,20 +174,21 @@ wheels = [ [[package]] name = "anthropic" -version = "0.61.0" +version = "0.69.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "distro" }, + { name = "docstring-parser" }, { name = "httpx" }, { name = "jiter" }, { name = "pydantic" }, { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7a/9a/b384758ef93b8f931a523efc8782f7191b175714b3952ff11002899f638b/anthropic-0.61.0.tar.gz", hash = "sha256:af4b3b8f3bc4626cca6af2d412e301974da1747179341ad9e271bdf5cbd2f008", size = 426606, upload-time = "2025-08-05T16:29:37.958Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c8/9d/9ad1778b95f15c5b04e7d328c1b5f558f1e893857b7c33cd288c19c0057a/anthropic-0.69.0.tar.gz", hash = "sha256:c604d287f4d73640f40bd2c0f3265a2eb6ce034217ead0608f6b07a8bc5ae5f2", size = 480622, upload-time = "2025-09-29T16:53:45.282Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/07/c7907eee22f5c27a53118dd2576267052ae01f52811dbb06a2848012639e/anthropic-0.61.0-py3-none-any.whl", hash = "sha256:798c8e6cc61e6315143c3f5847d2f220c45f1e69f433436872a237413ca58803", size = 294935, upload-time = "2025-08-05T16:29:36.379Z" }, + { url = "https://files.pythonhosted.org/packages/9b/38/75129688de5637eb5b383e5f2b1570a5cc3aecafa4de422da8eea4b90a6c/anthropic-0.69.0-py3-none-any.whl", hash = "sha256:1f73193040f33f11e27c2cd6ec25f24fe7c3f193dc1c5cde6b7a08b18a16bcc5", size = 337265, upload-time = "2025-09-29T16:53:43.686Z" }, ] [[package]] @@ -368,14 +354,15 @@ wheels = [ [[package]] name = "authlib" -version = "1.6.3" +version = "1.6.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cryptography" }, + { name = "cryptography", version = "45.0.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "cryptography", version = "46.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5d/c6/d9a9db2e71957827e23a34322bde8091b51cb778dcc38885b84c772a1ba9/authlib-1.6.3.tar.gz", hash = "sha256:9f7a982cc395de719e4c2215c5707e7ea690ecf84f1ab126f28c053f4219e610", size = 160836, upload-time = "2025-08-26T12:13:25.206Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/bb/73a1f1c64ee527877f64122422dafe5b87a846ccf4ac933fe21bcbb8fee8/authlib-1.6.4.tar.gz", hash = "sha256:104b0442a43061dc8bc23b133d1d06a2b0a9c2e3e33f34c4338929e816287649", size = 164046, upload-time = "2025-09-17T09:59:23.897Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/2f/efa9d26dbb612b774990741fd8f13c7cf4cfd085b870e4a5af5c82eaf5f1/authlib-1.6.3-py2.py3-none-any.whl", hash = "sha256:7ea0f082edd95a03b7b72edac65ec7f8f68d703017d7e37573aee4fc603f2a48", size = 240105, upload-time = "2025-08-26T12:13:23.889Z" }, + { url = "https://files.pythonhosted.org/packages/0e/aa/91355b5f539caf1b94f0e66ff1e4ee39373b757fce08204981f7829ede51/authlib-1.6.4-py2.py3-none-any.whl", hash = "sha256:39313d2a2caac3ecf6d8f95fbebdfd30ae6ea6ae6a6db794d976405fdd9aa796", size = 243076, upload-time = "2025-09-17T09:59:22.259Z" }, ] [[package]] @@ -507,7 +494,8 @@ name = "cairocffi" version = "1.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi" }, + { name = "cffi", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or platform_python_implementation == 'PyPy'" }, + { name = "cffi", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_python_implementation != 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/70/c5/1a4dc131459e68a173cbdab5fad6b524f53f9c1ef7861b7698e998b837cc/cairocffi-1.7.1.tar.gz", hash = "sha256:2e48ee864884ec4a3a34bfa8c9ab9999f688286eb714a15a43ec9d068c36557b", size = 88096, upload-time = "2024-06-18T10:56:06.741Z" } wheels = [ @@ -543,8 +531,15 @@ wheels = [ name = "cffi" version = "1.17.1" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_python_implementation == 'PyPy'", + "python_full_version == '3.12.*' and platform_python_implementation == 'PyPy'", + "python_full_version == '3.11.*' and platform_python_implementation == 'PyPy'", + "python_full_version < '3.11' and platform_python_implementation != 'PyPy'", + "python_full_version < '3.11' and platform_python_implementation == 'PyPy'", +] dependencies = [ - { name = "pycparser" }, + { name = "pycparser", marker = "python_full_version < '3.11' or platform_python_implementation == 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } wheels = [ @@ -596,6 +591,93 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" }, ] +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_python_implementation != 'PyPy'", + "python_full_version == '3.12.*' and platform_python_implementation != 'PyPy'", + "python_full_version == '3.11.*' and platform_python_implementation != 'PyPy'", +] +dependencies = [ + { name = "pycparser", marker = "python_full_version >= '3.11' and implementation_name != 'PyPy' and platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + [[package]] name = "chardet" version = "5.2.0" @@ -811,8 +893,12 @@ toml = [ name = "cryptography" version = "45.0.7" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_python_implementation != 'PyPy'", + "python_full_version < '3.11' and platform_python_implementation == 'PyPy'", +] dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "cffi", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_python_implementation != 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a7/35/c495bffc2056f2dadb32434f1feedd79abde2a7f8363e1974afa9c33c7e2/cryptography-45.0.7.tar.gz", hash = "sha256:4b1654dfc64ea479c242508eb8c724044f1e964a47d1d1cacc5132292d851971", size = 744980, upload-time = "2025-09-01T11:15:03.146Z" } wheels = [ @@ -854,6 +940,78 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/23/87/7ce86f3fa14bc11a5a48c30d8103c26e09b6465f8d8e9d74cf7a0714f043/cryptography-45.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:1f3d56f73595376f4244646dd5c5870c14c196949807be39e79e7bd9bac3da63", size = 3332908, upload-time = "2025-09-01T11:14:58.78Z" }, ] +[[package]] +name = "cryptography" +version = "46.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_python_implementation == 'PyPy'", + "python_full_version >= '3.13' and platform_python_implementation != 'PyPy'", + "python_full_version == '3.12.*' and platform_python_implementation == 'PyPy'", + "python_full_version == '3.12.*' and platform_python_implementation != 'PyPy'", + "python_full_version == '3.11.*' and platform_python_implementation == 'PyPy'", + "python_full_version == '3.11.*' and platform_python_implementation != 'PyPy'", +] +dependencies = [ + { name = "cffi", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4a/9b/e301418629f7bfdf72db9e80ad6ed9d1b83c487c471803eaa6464c511a01/cryptography-46.0.2.tar.gz", hash = "sha256:21b6fc8c71a3f9a604f028a329e5560009cc4a3a828bfea5fcba8eb7647d88fe", size = 749293, upload-time = "2025-10-01T00:29:11.856Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/98/7a8df8c19a335c8028414738490fc3955c0cecbfdd37fcc1b9c3d04bd561/cryptography-46.0.2-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:f3e32ab7dd1b1ef67b9232c4cf5e2ee4cd517d4316ea910acaaa9c5712a1c663", size = 7261255, upload-time = "2025-10-01T00:27:22.947Z" }, + { url = "https://files.pythonhosted.org/packages/c6/38/b2adb2aa1baa6706adc3eb746691edd6f90a656a9a65c3509e274d15a2b8/cryptography-46.0.2-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1fd1a69086926b623ef8126b4c33d5399ce9e2f3fac07c9c734c2a4ec38b6d02", size = 4297596, upload-time = "2025-10-01T00:27:25.258Z" }, + { url = "https://files.pythonhosted.org/packages/e4/27/0f190ada240003119488ae66c897b5e97149292988f556aef4a6a2a57595/cryptography-46.0.2-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb7fb9cd44c2582aa5990cf61a4183e6f54eea3172e54963787ba47287edd135", size = 4450899, upload-time = "2025-10-01T00:27:27.458Z" }, + { url = "https://files.pythonhosted.org/packages/85/d5/e4744105ab02fdf6bb58ba9a816e23b7a633255987310b4187d6745533db/cryptography-46.0.2-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9066cfd7f146f291869a9898b01df1c9b0e314bfa182cef432043f13fc462c92", size = 4300382, upload-time = "2025-10-01T00:27:29.091Z" }, + { url = "https://files.pythonhosted.org/packages/33/fb/bf9571065c18c04818cb07de90c43fc042c7977c68e5de6876049559c72f/cryptography-46.0.2-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:97e83bf4f2f2c084d8dd792d13841d0a9b241643151686010866bbd076b19659", size = 4017347, upload-time = "2025-10-01T00:27:30.767Z" }, + { url = "https://files.pythonhosted.org/packages/35/72/fc51856b9b16155ca071080e1a3ad0c3a8e86616daf7eb018d9565b99baa/cryptography-46.0.2-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:4a766d2a5d8127364fd936572c6e6757682fc5dfcbdba1632d4554943199f2fa", size = 4983500, upload-time = "2025-10-01T00:27:32.741Z" }, + { url = "https://files.pythonhosted.org/packages/c1/53/0f51e926799025e31746d454ab2e36f8c3f0d41592bc65cb9840368d3275/cryptography-46.0.2-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:fab8f805e9675e61ed8538f192aad70500fa6afb33a8803932999b1049363a08", size = 4482591, upload-time = "2025-10-01T00:27:34.869Z" }, + { url = "https://files.pythonhosted.org/packages/86/96/4302af40b23ab8aa360862251fb8fc450b2a06ff24bc5e261c2007f27014/cryptography-46.0.2-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:1e3b6428a3d56043bff0bb85b41c535734204e599c1c0977e1d0f261b02f3ad5", size = 4300019, upload-time = "2025-10-01T00:27:37.029Z" }, + { url = "https://files.pythonhosted.org/packages/9b/59/0be12c7fcc4c5e34fe2b665a75bc20958473047a30d095a7657c218fa9e8/cryptography-46.0.2-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:1a88634851d9b8de8bb53726f4300ab191d3b2f42595e2581a54b26aba71b7cc", size = 4950006, upload-time = "2025-10-01T00:27:40.272Z" }, + { url = "https://files.pythonhosted.org/packages/55/1d/42fda47b0111834b49e31590ae14fd020594d5e4dadd639bce89ad790fba/cryptography-46.0.2-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:be939b99d4e091eec9a2bcf41aaf8f351f312cd19ff74b5c83480f08a8a43e0b", size = 4482088, upload-time = "2025-10-01T00:27:42.668Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/60f583f69aa1602c2bdc7022dae86a0d2b837276182f8c1ec825feb9b874/cryptography-46.0.2-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f13b040649bc18e7eb37936009b24fd31ca095a5c647be8bb6aaf1761142bd1", size = 4425599, upload-time = "2025-10-01T00:27:44.616Z" }, + { url = "https://files.pythonhosted.org/packages/d1/57/d8d4134cd27e6e94cf44adb3f3489f935bde85f3a5508e1b5b43095b917d/cryptography-46.0.2-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9bdc25e4e01b261a8fda4e98618f1c9515febcecebc9566ddf4a70c63967043b", size = 4697458, upload-time = "2025-10-01T00:27:46.209Z" }, + { url = "https://files.pythonhosted.org/packages/d1/2b/531e37408573e1da33adfb4c58875013ee8ac7d548d1548967d94a0ae5c4/cryptography-46.0.2-cp311-abi3-win32.whl", hash = "sha256:8b9bf67b11ef9e28f4d78ff88b04ed0929fcd0e4f70bb0f704cfc32a5c6311ee", size = 3056077, upload-time = "2025-10-01T00:27:48.424Z" }, + { url = "https://files.pythonhosted.org/packages/a8/cd/2f83cafd47ed2dc5a3a9c783ff5d764e9e70d3a160e0df9a9dcd639414ce/cryptography-46.0.2-cp311-abi3-win_amd64.whl", hash = "sha256:758cfc7f4c38c5c5274b55a57ef1910107436f4ae842478c4989abbd24bd5acb", size = 3512585, upload-time = "2025-10-01T00:27:50.521Z" }, + { url = "https://files.pythonhosted.org/packages/00/36/676f94e10bfaa5c5b86c469ff46d3e0663c5dc89542f7afbadac241a3ee4/cryptography-46.0.2-cp311-abi3-win_arm64.whl", hash = "sha256:218abd64a2e72f8472c2102febb596793347a3e65fafbb4ad50519969da44470", size = 2927474, upload-time = "2025-10-01T00:27:52.91Z" }, + { url = "https://files.pythonhosted.org/packages/6f/cc/47fc6223a341f26d103cb6da2216805e08a37d3b52bee7f3b2aee8066f95/cryptography-46.0.2-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:bda55e8dbe8533937956c996beaa20266a8eca3570402e52ae52ed60de1faca8", size = 7198626, upload-time = "2025-10-01T00:27:54.8Z" }, + { url = "https://files.pythonhosted.org/packages/93/22/d66a8591207c28bbe4ac7afa25c4656dc19dc0db29a219f9809205639ede/cryptography-46.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e7155c0b004e936d381b15425273aee1cebc94f879c0ce82b0d7fecbf755d53a", size = 4287584, upload-time = "2025-10-01T00:27:57.018Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3e/fac3ab6302b928e0398c269eddab5978e6c1c50b2b77bb5365ffa8633b37/cryptography-46.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a61c154cc5488272a6c4b86e8d5beff4639cdb173d75325ce464d723cda0052b", size = 4433796, upload-time = "2025-10-01T00:27:58.631Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d8/24392e5d3c58e2d83f98fe5a2322ae343360ec5b5b93fe18bc52e47298f5/cryptography-46.0.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:9ec3f2e2173f36a9679d3b06d3d01121ab9b57c979de1e6a244b98d51fea1b20", size = 4292126, upload-time = "2025-10-01T00:28:00.643Z" }, + { url = "https://files.pythonhosted.org/packages/ed/38/3d9f9359b84c16c49a5a336ee8be8d322072a09fac17e737f3bb11f1ce64/cryptography-46.0.2-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2fafb6aa24e702bbf74de4cb23bfa2c3beb7ab7683a299062b69724c92e0fa73", size = 3993056, upload-time = "2025-10-01T00:28:02.8Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a3/4c44fce0d49a4703cc94bfbe705adebf7ab36efe978053742957bc7ec324/cryptography-46.0.2-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:0c7ffe8c9b1fcbb07a26d7c9fa5e857c2fe80d72d7b9e0353dcf1d2180ae60ee", size = 4967604, upload-time = "2025-10-01T00:28:04.783Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c2/49d73218747c8cac16bb8318a5513fde3129e06a018af3bc4dc722aa4a98/cryptography-46.0.2-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:5840f05518caa86b09d23f8b9405a7b6d5400085aa14a72a98fdf5cf1568c0d2", size = 4465367, upload-time = "2025-10-01T00:28:06.864Z" }, + { url = "https://files.pythonhosted.org/packages/1b/64/9afa7d2ee742f55ca6285a54386ed2778556a4ed8871571cb1c1bfd8db9e/cryptography-46.0.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:27c53b4f6a682a1b645fbf1cd5058c72cf2f5aeba7d74314c36838c7cbc06e0f", size = 4291678, upload-time = "2025-10-01T00:28:08.982Z" }, + { url = "https://files.pythonhosted.org/packages/50/48/1696d5ea9623a7b72ace87608f6899ca3c331709ac7ebf80740abb8ac673/cryptography-46.0.2-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:512c0250065e0a6b286b2db4bbcc2e67d810acd53eb81733e71314340366279e", size = 4931366, upload-time = "2025-10-01T00:28:10.74Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/9dfc778401a334db3b24435ee0733dd005aefb74afe036e2d154547cb917/cryptography-46.0.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:07c0eb6657c0e9cca5891f4e35081dbf985c8131825e21d99b4f440a8f496f36", size = 4464738, upload-time = "2025-10-01T00:28:12.491Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b1/abcde62072b8f3fd414e191a6238ce55a0050e9738090dc6cded24c12036/cryptography-46.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:48b983089378f50cba258f7f7aa28198c3f6e13e607eaf10472c26320332ca9a", size = 4419305, upload-time = "2025-10-01T00:28:14.145Z" }, + { url = "https://files.pythonhosted.org/packages/c7/1f/3d2228492f9391395ca34c677e8f2571fb5370fe13dc48c1014f8c509864/cryptography-46.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e6f6775eaaa08c0eec73e301f7592f4367ccde5e4e4df8e58320f2ebf161ea2c", size = 4681201, upload-time = "2025-10-01T00:28:15.951Z" }, + { url = "https://files.pythonhosted.org/packages/de/77/b687745804a93a55054f391528fcfc76c3d6bfd082ce9fb62c12f0d29fc1/cryptography-46.0.2-cp314-cp314t-win32.whl", hash = "sha256:e8633996579961f9b5a3008683344c2558d38420029d3c0bc7ff77c17949a4e1", size = 3022492, upload-time = "2025-10-01T00:28:17.643Z" }, + { url = "https://files.pythonhosted.org/packages/60/a5/8d498ef2996e583de0bef1dcc5e70186376f00883ae27bf2133f490adf21/cryptography-46.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:48c01988ecbb32979bb98731f5c2b2f79042a6c58cc9a319c8c2f9987c7f68f9", size = 3496215, upload-time = "2025-10-01T00:28:19.272Z" }, + { url = "https://files.pythonhosted.org/packages/56/db/ee67aaef459a2706bc302b15889a1a8126ebe66877bab1487ae6ad00f33d/cryptography-46.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:8e2ad4d1a5899b7caa3a450e33ee2734be7cc0689010964703a7c4bcc8dd4fd0", size = 2919255, upload-time = "2025-10-01T00:28:21.115Z" }, + { url = "https://files.pythonhosted.org/packages/d5/bb/fa95abcf147a1b0bb94d95f53fbb09da77b24c776c5d87d36f3d94521d2c/cryptography-46.0.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a08e7401a94c002e79dc3bc5231b6558cd4b2280ee525c4673f650a37e2c7685", size = 7248090, upload-time = "2025-10-01T00:28:22.846Z" }, + { url = "https://files.pythonhosted.org/packages/b7/66/f42071ce0e3ffbfa80a88feadb209c779fda92a23fbc1e14f74ebf72ef6b/cryptography-46.0.2-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d30bc11d35743bf4ddf76674a0a369ec8a21f87aaa09b0661b04c5f6c46e8d7b", size = 4293123, upload-time = "2025-10-01T00:28:25.072Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/1fdbd2e5c1ba822828d250e5a966622ef00185e476d1cd2726b6dd135e53/cryptography-46.0.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bca3f0ce67e5a2a2cf524e86f44697c4323a86e0fd7ba857de1c30d52c11ede1", size = 4439524, upload-time = "2025-10-01T00:28:26.808Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c1/5e4989a7d102d4306053770d60f978c7b6b1ea2ff8c06e0265e305b23516/cryptography-46.0.2-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ff798ad7a957a5021dcbab78dfff681f0cf15744d0e6af62bd6746984d9c9e9c", size = 4297264, upload-time = "2025-10-01T00:28:29.327Z" }, + { url = "https://files.pythonhosted.org/packages/28/78/b56f847d220cb1d6d6aef5a390e116ad603ce13a0945a3386a33abc80385/cryptography-46.0.2-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cb5e8daac840e8879407acbe689a174f5ebaf344a062f8918e526824eb5d97af", size = 4011872, upload-time = "2025-10-01T00:28:31.479Z" }, + { url = "https://files.pythonhosted.org/packages/e1/80/2971f214b066b888944f7b57761bf709ee3f2cf805619a18b18cab9b263c/cryptography-46.0.2-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:3f37aa12b2d91e157827d90ce78f6180f0c02319468a0aea86ab5a9566da644b", size = 4978458, upload-time = "2025-10-01T00:28:33.267Z" }, + { url = "https://files.pythonhosted.org/packages/a5/84/0cb0a2beaa4f1cbe63ebec4e97cd7e0e9f835d0ba5ee143ed2523a1e0016/cryptography-46.0.2-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:5e38f203160a48b93010b07493c15f2babb4e0f2319bbd001885adb3f3696d21", size = 4472195, upload-time = "2025-10-01T00:28:36.039Z" }, + { url = "https://files.pythonhosted.org/packages/30/8b/2b542ddbf78835c7cd67b6fa79e95560023481213a060b92352a61a10efe/cryptography-46.0.2-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d19f5f48883752b5ab34cff9e2f7e4a7f216296f33714e77d1beb03d108632b6", size = 4296791, upload-time = "2025-10-01T00:28:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/78/12/9065b40201b4f4876e93b9b94d91feb18de9150d60bd842a16a21565007f/cryptography-46.0.2-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:04911b149eae142ccd8c9a68892a70c21613864afb47aba92d8c7ed9cc001023", size = 4939629, upload-time = "2025-10-01T00:28:39.654Z" }, + { url = "https://files.pythonhosted.org/packages/f6/9e/6507dc048c1b1530d372c483dfd34e7709fc542765015425f0442b08547f/cryptography-46.0.2-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:8b16c1ede6a937c291d41176934268e4ccac2c6521c69d3f5961c5a1e11e039e", size = 4471988, upload-time = "2025-10-01T00:28:41.822Z" }, + { url = "https://files.pythonhosted.org/packages/b1/86/d025584a5f7d5c5ec8d3633dbcdce83a0cd579f1141ceada7817a4c26934/cryptography-46.0.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:747b6f4a4a23d5a215aadd1d0b12233b4119c4313df83ab4137631d43672cc90", size = 4422989, upload-time = "2025-10-01T00:28:43.608Z" }, + { url = "https://files.pythonhosted.org/packages/4b/39/536370418b38a15a61bbe413006b79dfc3d2b4b0eafceb5581983f973c15/cryptography-46.0.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6b275e398ab3a7905e168c036aad54b5969d63d3d9099a0a66cc147a3cc983be", size = 4685578, upload-time = "2025-10-01T00:28:45.361Z" }, + { url = "https://files.pythonhosted.org/packages/15/52/ea7e2b1910f547baed566c866fbb86de2402e501a89ecb4871ea7f169a81/cryptography-46.0.2-cp38-abi3-win32.whl", hash = "sha256:0b507c8e033307e37af61cb9f7159b416173bdf5b41d11c4df2e499a1d8e007c", size = 3036711, upload-time = "2025-10-01T00:28:47.096Z" }, + { url = "https://files.pythonhosted.org/packages/71/9e/171f40f9c70a873e73c2efcdbe91e1d4b1777a03398fa1c4af3c56a2477a/cryptography-46.0.2-cp38-abi3-win_amd64.whl", hash = "sha256:f9b2dc7668418fb6f221e4bf701f716e05e8eadb4f1988a2487b11aedf8abe62", size = 3500007, upload-time = "2025-10-01T00:28:48.967Z" }, + { url = "https://files.pythonhosted.org/packages/3e/7c/15ad426257615f9be8caf7f97990cf3dcbb5b8dd7ed7e0db581a1c4759dd/cryptography-46.0.2-cp38-abi3-win_arm64.whl", hash = "sha256:91447f2b17e83c9e0c89f133119d83f94ce6e0fb55dd47da0a959316e6e9cfa1", size = 2918153, upload-time = "2025-10-01T00:28:51.003Z" }, + { url = "https://files.pythonhosted.org/packages/25/b2/067a7db693488f19777ecf73f925bcb6a3efa2eae42355bafaafa37a6588/cryptography-46.0.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f25a41f5b34b371a06dad3f01799706631331adc7d6c05253f5bca22068c7a34", size = 3701860, upload-time = "2025-10-01T00:28:53.003Z" }, + { url = "https://files.pythonhosted.org/packages/87/12/47c2aab2c285f97c71a791169529dbb89f48fc12e5f62bb6525c3927a1a2/cryptography-46.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e12b61e0b86611e3f4c1756686d9086c1d36e6fd15326f5658112ad1f1cc8807", size = 3429917, upload-time = "2025-10-01T00:28:55.03Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8c/1aabe338149a7d0f52c3e30f2880b20027ca2a485316756ed6f000462db3/cryptography-46.0.2-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1d3b3edd145953832e09607986f2bd86f85d1dc9c48ced41808b18009d9f30e5", size = 3714495, upload-time = "2025-10-01T00:28:57.222Z" }, + { url = "https://files.pythonhosted.org/packages/e3/0a/0d10eb970fe3e57da9e9ddcfd9464c76f42baf7b3d0db4a782d6746f788f/cryptography-46.0.2-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:fe245cf4a73c20592f0f48da39748b3513db114465be78f0a36da847221bd1b4", size = 4243379, upload-time = "2025-10-01T00:28:58.989Z" }, + { url = "https://files.pythonhosted.org/packages/7d/60/e274b4d41a9eb82538b39950a74ef06e9e4d723cb998044635d9deb1b435/cryptography-46.0.2-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2b9cad9cf71d0c45566624ff76654e9bae5f8a25970c250a26ccfc73f8553e2d", size = 4409533, upload-time = "2025-10-01T00:29:00.785Z" }, + { url = "https://files.pythonhosted.org/packages/19/9a/fb8548f762b4749aebd13b57b8f865de80258083fe814957f9b0619cfc56/cryptography-46.0.2-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9bd26f2f75a925fdf5e0a446c0de2714f17819bf560b44b7480e4dd632ad6c46", size = 4243120, upload-time = "2025-10-01T00:29:02.515Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/883f24147fd4a0c5cab74ac7e36a1ff3094a54ba5c3a6253d2ff4b19255b/cryptography-46.0.2-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:7282d8f092b5be7172d6472f29b0631f39f18512a3642aefe52c3c0e0ccfad5a", size = 4408940, upload-time = "2025-10-01T00:29:04.42Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b5/c5e179772ec38adb1c072b3aa13937d2860509ba32b2462bf1dda153833b/cryptography-46.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c4b93af7920cdf80f71650769464ccf1fb49a4b56ae0024173c24c48eb6b1612", size = 3438518, upload-time = "2025-10-01T00:29:06.139Z" }, +] + [[package]] name = "cssselect2" version = "0.7.0" @@ -869,7 +1027,7 @@ wheels = [ [[package]] name = "cyclopts" -version = "3.23.1" +version = "3.24.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -878,9 +1036,9 @@ dependencies = [ { name = "rich-rst" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ea/7a/28b63c43d4c17d6587abcfef648841d39543158bcc47b5d40a03b8831f7a/cyclopts-3.23.1.tar.gz", hash = "sha256:ca6a5e9b326caf156d79f3932e2f88b95629e59fd371c0b3a89732b7619edacb", size = 75161, upload-time = "2025-08-30T17:40:34.396Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/ca/7782da3b03242d5f0a16c20371dff99d4bd1fedafe26bc48ff82e42be8c9/cyclopts-3.24.0.tar.gz", hash = "sha256:de6964a041dfb3c57bf043b41e68c43548227a17de1bad246e3a0bfc5c4b7417", size = 76131, upload-time = "2025-09-08T15:40:57.75Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/63/67/ac57fbef5414ce84fe0bdeb497918ab2c781ff2cbf23c1bd91334b225669/cyclopts-3.23.1-py3-none-any.whl", hash = "sha256:8e57c6ea47d72b4b565c6a6c8a9fd56ed048ab4316627991230f4ad24ce2bc29", size = 85222, upload-time = "2025-08-30T17:40:33.005Z" }, + { url = "https://files.pythonhosted.org/packages/f0/8b/2c95f0645c6f40211896375e6fa51f504b8ccb29c21f6ae661fe87ab044e/cyclopts-3.24.0-py3-none-any.whl", hash = "sha256:809d04cde9108617106091140c3964ee6fceb33cecdd537f7ffa360bde13ed71", size = 86154, upload-time = "2025-09-08T15:40:56.41Z" }, ] [[package]] @@ -909,30 +1067,19 @@ wheels = [ [[package]] name = "dbos" -version = "1.14.0" +version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "alembic" }, - { name = "cryptography" }, - { name = "docker" }, - { name = "fastapi", extra = ["standard"] }, - { name = "jsonpickle" }, - { name = "jsonschema" }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-exporter-otlp-proto-http" }, - { name = "opentelemetry-sdk" }, { name = "psycopg", extra = ["binary"] }, - { name = "pyjwt" }, { name = "python-dateutil" }, { name = "pyyaml" }, - { name = "rich" }, - { name = "tomlkit" }, - { name = "typer" }, + { name = "sqlalchemy" }, + { name = "typer-slim" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6b/b7/2cfefeb96fe67481ec2cf009c427ccbb32c82e2026cf2c259acbe6b22045/dbos-1.14.0.tar.gz", hash = "sha256:16052bd8ad004c7dce6b9cd2c10b282c2c4bb3b1bd7c5f6714e4bc437b7ca6f4", size = 216135, upload-time = "2025-09-16T16:49:08.741Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/fa/30e82a440601fe01c4e4edd1ba1c60534d3af364ca16896db68c459fbabc/dbos-2.0.0.tar.gz", hash = "sha256:91c7ce2da4c7de4614a19546e496d1c9ac6160338c0faeed35caa72803ef5cd7", size = 208612, upload-time = "2025-09-25T15:22:58.895Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/7f/2461485e608594a4c43e65dac7a5c6614256f2ca9aeeddcd080e4fb52939/dbos-1.14.0-py3-none-any.whl", hash = "sha256:544c99a641636f6b9f528f64ae92430c444bc68486fc2511ab5415f61304e2bb", size = 151550, upload-time = "2025-09-16T16:49:05.806Z" }, + { url = "https://files.pythonhosted.org/packages/8b/38/c0ebffc1ac4bb6b37d19bffa19d67ffda3372ba5229ba5076fc98b788288/dbos-2.0.0-py3-none-any.whl", hash = "sha256:5e0d91a00c94e6dfad79eb158acb900045d5e9f727b875375be3bbd489bc00b9", size = 134240, upload-time = "2025-09-25T15:22:56.711Z" }, ] [[package]] @@ -1028,25 +1175,11 @@ wheels = [ [[package]] name = "dnspython" -version = "2.7.0" +version = "2.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b5/4a/263763cb2ba3816dd94b08ad3a33d5fdae34ecb856678773cc40a3605829/dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1", size = 345197, upload-time = "2024-10-05T20:14:59.362Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632, upload-time = "2024-10-05T20:14:57.687Z" }, -] - -[[package]] -name = "docker" -version = "7.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pywin32", marker = "sys_platform == 'win32'" }, - { name = "requests" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/91/9b/4a2ea29aeba62471211598dac5d96825bb49348fa07e906ea930394a83ce/docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c", size = 117834, upload-time = "2024-05-23T11:13:57.216Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774, upload-time = "2024-05-23T11:13:55.01Z" }, + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, ] [[package]] @@ -1060,11 +1193,11 @@ wheels = [ [[package]] name = "docutils" -version = "0.22" +version = "0.22.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e9/86/5b41c32ecedcfdb4c77b28b6cb14234f252075f8cdb254531727a35547dd/docutils-0.22.tar.gz", hash = "sha256:ba9d57750e92331ebe7c08a1bbf7a7f8143b86c476acd51528b042216a6aad0f", size = 2277984, upload-time = "2025-07-29T15:20:31.06Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/c0/89fe6215b443b919cb98a5002e107cb5026854ed1ccb6b5833e0768419d1/docutils-0.22.2.tar.gz", hash = "sha256:9fdb771707c8784c8f2728b67cb2c691305933d68137ef95a75db5f4dfbc213d", size = 2289092, upload-time = "2025-09-20T17:55:47.994Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/44/57/8db39bc5f98f042e0153b1de9fb88e1a409a33cda4dd7f723c2ed71e01f6/docutils-0.22-py3-none-any.whl", hash = "sha256:4ed966a0e96a0477d852f7af31bdcb3adc049fbb35ccba358c2ea8a03287615e", size = 630709, upload-time = "2025-07-29T15:20:28.335Z" }, + { url = "https://files.pythonhosted.org/packages/66/dd/f95350e853a4468ec37478414fc04ae2d61dad7a947b3015c3dcc51a09b9/docutils-0.22.2-py3-none-any.whl", hash = "sha256:b0e98d679283fc3bb0ead8a5da7f501baa632654e7056e9c5846842213d674d8", size = 632667, upload-time = "2025-09-20T17:55:43.052Z" }, ] [[package]] @@ -1194,54 +1327,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8f/7d/2d6ce181d7a5f51dedb8c06206cbf0ec026a99bf145edd309f9e17c3282f/fastapi-0.115.8-py3-none-any.whl", hash = "sha256:753a96dd7e036b34eeef8babdfcfe3f28ff79648f86551eb36bfc1b0bf4a8cbf", size = 94814, upload-time = "2025-01-30T14:06:38.564Z" }, ] -[package.optional-dependencies] -standard = [ - { name = "email-validator" }, - { name = "fastapi-cli", extra = ["standard"] }, - { name = "httpx" }, - { name = "jinja2" }, - { name = "python-multipart" }, - { name = "uvicorn", extra = ["standard"] }, -] - -[[package]] -name = "fastapi-cli" -version = "0.0.10" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "rich-toolkit" }, - { name = "typer" }, - { name = "uvicorn", extra = ["standard"] }, -] -sdist = { url = "https://files.pythonhosted.org/packages/31/b6/ed25b8874a27f684bf601990c48fcb3edb478edca2b9a38cc2ba196fb304/fastapi_cli-0.0.10.tar.gz", hash = "sha256:85a93df72ff834c3d2a356164512cabaf8f093d50eddad9309065a9c9ac5193a", size = 16994, upload-time = "2025-08-31T17:43:20.702Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/62/0f00036925c0614e333a2baf739c861453a6779331ffb47ec9a6147f860b/fastapi_cli-0.0.10-py3-none-any.whl", hash = "sha256:04bef56b49f7357c6c4acd4f793b4433ed3f511be431ed0af68db6d3f8bd44b3", size = 10851, upload-time = "2025-08-31T17:43:19.481Z" }, -] - -[package.optional-dependencies] -standard = [ - { name = "fastapi-cloud-cli" }, - { name = "uvicorn", extra = ["standard"] }, -] - -[[package]] -name = "fastapi-cloud-cli" -version = "0.1.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "httpx" }, - { name = "pydantic", extra = ["email"] }, - { name = "rich-toolkit" }, - { name = "rignore" }, - { name = "sentry-sdk" }, - { name = "typer" }, - { name = "uvicorn", extra = ["standard"] }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a9/2e/3b6e5016affc310e5109bc580f760586eabecea0c8a7ab067611cd849ac0/fastapi_cloud_cli-0.1.5.tar.gz", hash = "sha256:341ee585eb731a6d3c3656cb91ad38e5f39809bf1a16d41de1333e38635a7937", size = 22710, upload-time = "2025-07-28T13:30:48.216Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/a6/5aa862489a2918a096166fd98d9fe86b7fd53c607678b3fa9d8c432d88d5/fastapi_cloud_cli-0.1.5-py3-none-any.whl", hash = "sha256:d80525fb9c0e8af122370891f9fa83cf5d496e4ad47a8dd26c0496a6c85a012a", size = 18992, upload-time = "2025-07-28T13:30:47.427Z" }, -] - [[package]] name = "fastavro" version = "1.10.0" @@ -1399,16 +1484,16 @@ http = [ [[package]] name = "genai-prices" -version = "0.0.25" +version = "0.0.28" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "eval-type-backport", marker = "python_full_version < '3.11'" }, { name = "httpx" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/02/9e/f292acaf69bd209b354ef835cab4ebe845eced05c4db85e3b31585429806/genai_prices-0.0.25.tar.gz", hash = "sha256:caf5fe2fd2248e87f70b2b44bbf8b3b52871abfc078a5e35372c40aca4cc4450", size = 44693, upload-time = "2025-09-01T17:30:42.185Z" } +sdist = { url = "https://files.pythonhosted.org/packages/89/c8/245848db2b8b1b393bffed5397e77fe9ea3cba53974639f2b80cf804388e/genai_prices-0.0.28.tar.gz", hash = "sha256:9928aa1c4f0535bec5b01ddfd3dc163888ee90cdc6153501de9f004f00193953", size = 45584, upload-time = "2025-09-27T03:17:08.775Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/86/12/41fcfba4ae0f6b4805f09d11f0e6d6417df2572cea13208c0f439170ee0c/genai_prices-0.0.25-py3-none-any.whl", hash = "sha256:47b412e6927787caa00717a5d99b2e4c0858bed507bb16473b1bcaff48d5aae9", size = 47002, upload-time = "2025-09-01T17:30:41.012Z" }, + { url = "https://files.pythonhosted.org/packages/58/9b/6f161c4fd141c0249621687cdcdd91734e6d0e11af74ded8b036de39e9ce/genai_prices-0.0.28-py3-none-any.whl", hash = "sha256:5b879bd02e901ffc6740d42eaffb9f5dfeb8c99f4a6b9e680b0046c84fb2de1b", size = 48187, upload-time = "2025-09-27T03:17:07.472Z" }, ] [[package]] @@ -1684,42 +1769,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551, upload-time = "2024-11-15T12:30:45.782Z" }, ] -[[package]] -name = "httptools" -version = "0.6.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/9a/ce5e1f7e131522e6d3426e8e7a490b3a01f39a6696602e1c4f33f9e94277/httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c", size = 240639, upload-time = "2024-10-16T19:45:08.902Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/6f/972f8eb0ea7d98a1c6be436e2142d51ad2a64ee18e02b0e7ff1f62171ab1/httptools-0.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3c73ce323711a6ffb0d247dcd5a550b8babf0f757e86a52558fe5b86d6fefcc0", size = 198780, upload-time = "2024-10-16T19:44:06.882Z" }, - { url = "https://files.pythonhosted.org/packages/6a/b0/17c672b4bc5c7ba7f201eada4e96c71d0a59fbc185e60e42580093a86f21/httptools-0.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:345c288418f0944a6fe67be8e6afa9262b18c7626c3ef3c28adc5eabc06a68da", size = 103297, upload-time = "2024-10-16T19:44:08.129Z" }, - { url = "https://files.pythonhosted.org/packages/92/5e/b4a826fe91971a0b68e8c2bd4e7db3e7519882f5a8ccdb1194be2b3ab98f/httptools-0.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deee0e3343f98ee8047e9f4c5bc7cedbf69f5734454a94c38ee829fb2d5fa3c1", size = 443130, upload-time = "2024-10-16T19:44:09.45Z" }, - { url = "https://files.pythonhosted.org/packages/b0/51/ce61e531e40289a681a463e1258fa1e05e0be54540e40d91d065a264cd8f/httptools-0.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca80b7485c76f768a3bc83ea58373f8db7b015551117375e4918e2aa77ea9b50", size = 442148, upload-time = "2024-10-16T19:44:11.539Z" }, - { url = "https://files.pythonhosted.org/packages/ea/9e/270b7d767849b0c96f275c695d27ca76c30671f8eb8cc1bab6ced5c5e1d0/httptools-0.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:90d96a385fa941283ebd231464045187a31ad932ebfa541be8edf5b3c2328959", size = 415949, upload-time = "2024-10-16T19:44:13.388Z" }, - { url = "https://files.pythonhosted.org/packages/81/86/ced96e3179c48c6f656354e106934e65c8963d48b69be78f355797f0e1b3/httptools-0.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:59e724f8b332319e2875efd360e61ac07f33b492889284a3e05e6d13746876f4", size = 417591, upload-time = "2024-10-16T19:44:15.258Z" }, - { url = "https://files.pythonhosted.org/packages/75/73/187a3f620ed3175364ddb56847d7a608a6fc42d551e133197098c0143eca/httptools-0.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:c26f313951f6e26147833fc923f78f95604bbec812a43e5ee37f26dc9e5a686c", size = 88344, upload-time = "2024-10-16T19:44:16.54Z" }, - { url = "https://files.pythonhosted.org/packages/7b/26/bb526d4d14c2774fe07113ca1db7255737ffbb119315839af2065abfdac3/httptools-0.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069", size = 199029, upload-time = "2024-10-16T19:44:18.427Z" }, - { url = "https://files.pythonhosted.org/packages/a6/17/3e0d3e9b901c732987a45f4f94d4e2c62b89a041d93db89eafb262afd8d5/httptools-0.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a", size = 103492, upload-time = "2024-10-16T19:44:19.515Z" }, - { url = "https://files.pythonhosted.org/packages/b7/24/0fe235d7b69c42423c7698d086d4db96475f9b50b6ad26a718ef27a0bce6/httptools-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975", size = 462891, upload-time = "2024-10-16T19:44:21.067Z" }, - { url = "https://files.pythonhosted.org/packages/b1/2f/205d1f2a190b72da6ffb5f41a3736c26d6fa7871101212b15e9b5cd8f61d/httptools-0.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b0f7fe4fd38e6a507bdb751db0379df1e99120c65fbdc8ee6c1d044897a636", size = 459788, upload-time = "2024-10-16T19:44:22.958Z" }, - { url = "https://files.pythonhosted.org/packages/6e/4c/d09ce0eff09057a206a74575ae8f1e1e2f0364d20e2442224f9e6612c8b9/httptools-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40a5ec98d3f49904b9fe36827dcf1aadfef3b89e2bd05b0e35e94f97c2b14721", size = 433214, upload-time = "2024-10-16T19:44:24.513Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d2/84c9e23edbccc4a4c6f96a1b8d99dfd2350289e94f00e9ccc7aadde26fb5/httptools-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dacdd3d10ea1b4ca9df97a0a303cbacafc04b5cd375fa98732678151643d4988", size = 434120, upload-time = "2024-10-16T19:44:26.295Z" }, - { url = "https://files.pythonhosted.org/packages/d0/46/4d8e7ba9581416de1c425b8264e2cadd201eb709ec1584c381f3e98f51c1/httptools-0.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:288cd628406cc53f9a541cfaf06041b4c71d751856bab45e3702191f931ccd17", size = 88565, upload-time = "2024-10-16T19:44:29.188Z" }, - { url = "https://files.pythonhosted.org/packages/bb/0e/d0b71465c66b9185f90a091ab36389a7352985fe857e352801c39d6127c8/httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2", size = 200683, upload-time = "2024-10-16T19:44:30.175Z" }, - { url = "https://files.pythonhosted.org/packages/e2/b8/412a9bb28d0a8988de3296e01efa0bd62068b33856cdda47fe1b5e890954/httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44", size = 104337, upload-time = "2024-10-16T19:44:31.786Z" }, - { url = "https://files.pythonhosted.org/packages/9b/01/6fb20be3196ffdc8eeec4e653bc2a275eca7f36634c86302242c4fbb2760/httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1", size = 508796, upload-time = "2024-10-16T19:44:32.825Z" }, - { url = "https://files.pythonhosted.org/packages/f7/d8/b644c44acc1368938317d76ac991c9bba1166311880bcc0ac297cb9d6bd7/httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2", size = 510837, upload-time = "2024-10-16T19:44:33.974Z" }, - { url = "https://files.pythonhosted.org/packages/52/d8/254d16a31d543073a0e57f1c329ca7378d8924e7e292eda72d0064987486/httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81", size = 485289, upload-time = "2024-10-16T19:44:35.111Z" }, - { url = "https://files.pythonhosted.org/packages/5f/3c/4aee161b4b7a971660b8be71a92c24d6c64372c1ab3ae7f366b3680df20f/httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f", size = 489779, upload-time = "2024-10-16T19:44:36.253Z" }, - { url = "https://files.pythonhosted.org/packages/12/b7/5cae71a8868e555f3f67a50ee7f673ce36eac970f029c0c5e9d584352961/httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970", size = 88634, upload-time = "2024-10-16T19:44:37.357Z" }, - { url = "https://files.pythonhosted.org/packages/94/a3/9fe9ad23fd35f7de6b91eeb60848986058bd8b5a5c1e256f5860a160cc3e/httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660", size = 197214, upload-time = "2024-10-16T19:44:38.738Z" }, - { url = "https://files.pythonhosted.org/packages/ea/d9/82d5e68bab783b632023f2fa31db20bebb4e89dfc4d2293945fd68484ee4/httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083", size = 102431, upload-time = "2024-10-16T19:44:39.818Z" }, - { url = "https://files.pythonhosted.org/packages/96/c1/cb499655cbdbfb57b577734fde02f6fa0bbc3fe9fb4d87b742b512908dff/httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3", size = 473121, upload-time = "2024-10-16T19:44:41.189Z" }, - { url = "https://files.pythonhosted.org/packages/af/71/ee32fd358f8a3bb199b03261f10921716990808a675d8160b5383487a317/httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071", size = 473805, upload-time = "2024-10-16T19:44:42.384Z" }, - { url = "https://files.pythonhosted.org/packages/8a/0a/0d4df132bfca1507114198b766f1737d57580c9ad1cf93c1ff673e3387be/httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5", size = 448858, upload-time = "2024-10-16T19:44:43.959Z" }, - { url = "https://files.pythonhosted.org/packages/1e/6a/787004fdef2cabea27bad1073bf6a33f2437b4dbd3b6fb4a9d71172b1c7c/httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0", size = 452042, upload-time = "2024-10-16T19:44:45.071Z" }, - { url = "https://files.pythonhosted.org/packages/4d/dc/7decab5c404d1d2cdc1bb330b1bf70e83d6af0396fd4fc76fc60c0d522bf/httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8", size = 87682, upload-time = "2024-10-16T19:44:46.46Z" }, -] - [[package]] name = "httpx" version = "0.28.1" @@ -1921,15 +1970,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256, upload-time = "2022-06-17T18:00:10.251Z" }, ] -[[package]] -name = "jsonpickle" -version = "4.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e4/a6/d07afcfdef402900229bcca795f80506b207af13a838d4d99ad45abf530c/jsonpickle-4.1.1.tar.gz", hash = "sha256:f86e18f13e2b96c1c1eede0b7b90095bbb61d99fedc14813c44dc2f361dbbae1", size = 316885, upload-time = "2025-06-02T20:36:11.57Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/73/04df8a6fa66d43a9fd45c30f283cc4afff17da671886e451d52af60bdc7e/jsonpickle-4.1.1-py3-none-any.whl", hash = "sha256:bb141da6057898aa2438ff268362b126826c812a1721e31cf08a6e142910dc91", size = 47125, upload-time = "2025-06-02T20:36:08.647Z" }, -] - [[package]] name = "jsonschema" version = "4.25.0" @@ -2141,18 +2181,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/83/29/00b9b0322a473aee6cda87473401c9abb19506cd650cc69a8aa38277ea74/lxml-5.3.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:48fd46bf7155def2e15287c6f2b133a2f78e2d22cdf55647269977b873c65499", size = 3487718, upload-time = "2025-02-10T07:50:31.231Z" }, ] -[[package]] -name = "mako" -version = "1.3.10" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" }, -] - [[package]] name = "markdown" version = "3.7" @@ -3988,15 +4016,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" }, ] -[[package]] -name = "pyjwt" -version = "2.10.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, -] - [[package]] name = "pymdown-extensions" version = "10.14.3" @@ -4366,114 +4385,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fd/bc/cc4e3dbc5e7992398dcb7a8eda0cbcf4fb792a0cdb93f857b478bf3cf884/rich_rst-1.3.1-py3-none-any.whl", hash = "sha256:498a74e3896507ab04492d326e794c3ef76e7cda078703aa592d1853d91098c1", size = 11621, upload-time = "2024-04-30T04:40:32.619Z" }, ] -[[package]] -name = "rich-toolkit" -version = "0.15.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "rich" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/65/36/cdb3d51371ad0cccbf1541506304783bd72d55790709b8eb68c0d401a13a/rich_toolkit-0.15.0.tar.gz", hash = "sha256:3f5730e9f2d36d0bfe01cf723948b7ecf4cc355d2b71e2c00e094f7963128c09", size = 115118, upload-time = "2025-08-11T10:55:37.909Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/75/e4/b0794eefb3cf78566b15e5bf576492c1d4a92ce5f6da55675bc11e9ef5d8/rich_toolkit-0.15.0-py3-none-any.whl", hash = "sha256:ddb91008283d4a7989fd8ff0324a48773a7a2276229c6a3070755645538ef1bb", size = 29062, upload-time = "2025-08-11T10:55:37.152Z" }, -] - -[[package]] -name = "rignore" -version = "0.6.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/73/46/05a94dc55ac03cf931d18e43b86ecee5ee054cb88b7853fffd741e35009c/rignore-0.6.4.tar.gz", hash = "sha256:e893fdd2d7fdcfa9407d0b7600ef2c2e2df97f55e1c45d4a8f54364829ddb0ab", size = 11633, upload-time = "2025-07-19T19:24:46.219Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/27/55ec2871e42c0a01669f7741598a5948f04bd32f3975478a0bead9e7e251/rignore-0.6.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c201375cfe76e56e61fcdfe50d0882aafb49544b424bfc828e0508dc9fbc431b", size = 888088, upload-time = "2025-07-19T19:23:50.776Z" }, - { url = "https://files.pythonhosted.org/packages/3d/e0/6be3d7adf91f7d67f08833a29dea4f7c345554b385f9a797c397f6685f29/rignore-0.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4962d537e377394292c4828e1e9c620618dd8daa49ba746abe533733a89f8644", size = 824159, upload-time = "2025-07-19T19:23:44.395Z" }, - { url = "https://files.pythonhosted.org/packages/99/b7/fbb56b8cfa27971f9a19e87769dae0cb648343226eddda94ded32be2afc3/rignore-0.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a6dd2f213cff6ca3c4d257fa3f5b0c7d4f6c23fe83bf292425fbe8d0c9c908a", size = 892493, upload-time = "2025-07-19T19:22:32.061Z" }, - { url = "https://files.pythonhosted.org/packages/d5/cf/21f130801c29c1fcf22f00a41d7530cef576819ee1a26c86bdb7bb06a0f2/rignore-0.6.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:64d379193f86a21fc93762783f36651927f54d5eea54c4922fdccb5e37076ed2", size = 872810, upload-time = "2025-07-19T19:22:45.554Z" }, - { url = "https://files.pythonhosted.org/packages/e4/4a/474a627263ef13a0ac28a0ce3a20932fbe41f6043f7280da47c7aca1f586/rignore-0.6.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53c4f8682cf645b7a9160e0f1786af3201ed54a020bb4abd515c970043387127", size = 1160488, upload-time = "2025-07-19T19:22:58.359Z" }, - { url = "https://files.pythonhosted.org/packages/0b/c7/a10c180f77cbb456ab483c28e52efd6166cee787f11d21cb1d369b89e961/rignore-0.6.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af1246e672bd835a17d3ae91579b3c235ec55b10924ef22608d3e9ec90fa2699", size = 938780, upload-time = "2025-07-19T19:23:10.604Z" }, - { url = "https://files.pythonhosted.org/packages/32/68/8e67701e8cc9f157f12b3742e14f14e395c7f3a497720c7f6aab7e5cdec4/rignore-0.6.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82eed48fbc3097af418862e3c5c26fa81aa993e0d8b5f3a0a9a29cc6975eedff", size = 950347, upload-time = "2025-07-19T19:23:33.759Z" }, - { url = "https://files.pythonhosted.org/packages/1e/11/8eef123a2d029ed697b119806a0ca8a99d9457500c40b4d26cd21860eb89/rignore-0.6.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:df1215a071d42fd857fb6363c13803fbd915d48eaeaa9b103fb2266ba89c8995", size = 976679, upload-time = "2025-07-19T19:23:23.813Z" }, - { url = "https://files.pythonhosted.org/packages/09/7e/9584f4e4b3c1587ae09f286a14dab2376895d782be632289d151cb952432/rignore-0.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:82f2d318e66756066ed664015d8ca720078ab1d319377f1f61e3f4d01325faea", size = 1067469, upload-time = "2025-07-19T19:23:57.616Z" }, - { url = "https://files.pythonhosted.org/packages/c3/2c/d3515693b89c47761822219bb519cefd0cd45a38ff82c35a4ccdd8e95deb/rignore-0.6.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e7d4258fc81051097c4d4c6ad17f0100c40088dbd2c6c31fc3c888a1d5a16190", size = 1136199, upload-time = "2025-07-19T19:24:09.922Z" }, - { url = "https://files.pythonhosted.org/packages/e7/39/94ea41846547ebb87d16527a3e978c8918632a060f77669a492f8a90b8b9/rignore-0.6.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a0d0b9ec7929df8fd35ae89cb56619850dc140869139d61a2f4fa2941d2d1878", size = 1111179, upload-time = "2025-07-19T19:24:21.908Z" }, - { url = "https://files.pythonhosted.org/packages/ce/77/9acda68c7cea4d5dd027ef63163e0be30008f635acd75ea801e4c443fcdd/rignore-0.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8883d079b948ffcd56b67572831c9b8949eca7fe2e8f7bdbf7691c7a9388f054", size = 1121143, upload-time = "2025-07-19T19:24:33.958Z" }, - { url = "https://files.pythonhosted.org/packages/05/67/d1489e9224f33b9a87b7f870650bcab582ee3452df286bcb2fbb6a7ba257/rignore-0.6.4-cp310-cp310-win32.whl", hash = "sha256:5aeac5b354e15eb9f7857b02ad2af12ae2c2ed25a61921b0bd7e272774530f77", size = 643131, upload-time = "2025-07-19T19:24:54.437Z" }, - { url = "https://files.pythonhosted.org/packages/5d/d1/7d668bed51d3f0895e875e57c8e42f421635cdbcb96652ab24f297c9c5cf/rignore-0.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:90419f881d05a1febb0578a175aa3e51d149ded1875421ed75a8af4392b7fe56", size = 721109, upload-time = "2025-07-19T19:24:47.458Z" }, - { url = "https://files.pythonhosted.org/packages/be/11/66992d271dbc44eac33f3b6b871855bc17e511b9279a2a0982b44c2b0c01/rignore-0.6.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:85f684dfc2c497e35ad34ffd6744a3bcdcac273ec1dbe7d0464bfa20f3331434", size = 888239, upload-time = "2025-07-19T19:23:51.835Z" }, - { url = "https://files.pythonhosted.org/packages/cb/1b/a9bde714e474043f97a06097925cf11e4597f9453adc267427d05ff9f38e/rignore-0.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23954acc6debc852dbccbffbb70f0e26b12d230239e1ad0638eb5540694d0308", size = 824348, upload-time = "2025-07-19T19:23:45.54Z" }, - { url = "https://files.pythonhosted.org/packages/db/58/dabba227fee6553f9be069f58128419b6d4954c784c4cd566cfe59955c1f/rignore-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2bf793bd58dbf3dee063a758b23ea446b5f037370405ecefc78e1e8923fc658", size = 892419, upload-time = "2025-07-19T19:22:33.763Z" }, - { url = "https://files.pythonhosted.org/packages/2c/fa/e3c16368ee32d6d1146cf219b127fd5c7e6baf22cad7a7a5967782ff3b20/rignore-0.6.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1eaeaa5a904e098604ea2012383a721de06211c8b4013abf0d41c3cfeb982f4f", size = 873285, upload-time = "2025-07-19T19:22:46.67Z" }, - { url = "https://files.pythonhosted.org/packages/78/9d/ef43d760dc3d18011d8482692b478785a846bba64157844b3068e428739c/rignore-0.6.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a48bdbeb03093e3fac2b40d62a718c59b5bb4f29cfdc8e7cbb360e1ea7bf0056", size = 1160457, upload-time = "2025-07-19T19:22:59.457Z" }, - { url = "https://files.pythonhosted.org/packages/95/de/eca1b035705e0b4e6c630fd1fcec45d14cf354a4acea88cf29ea0a322fea/rignore-0.6.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c5f9452d116be405f0967160b449c46ac929b50eaf527f33ee4680e3716e39", size = 938833, upload-time = "2025-07-19T19:23:11.657Z" }, - { url = "https://files.pythonhosted.org/packages/d4/2d/58912efa4137e989616d679a5390b53e93d5150be47217dd686ff60cd4cd/rignore-0.6.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6cf1039bfbdaa0f9710a6fb75436c25ca26d364881ec4d1e66d466bb36a7fb98", size = 950603, upload-time = "2025-07-19T19:23:35.245Z" }, - { url = "https://files.pythonhosted.org/packages/6f/3d/9827cc1c7674d8d884d3d231a224a2db8ea8eae075a1611dfdcd0c301e20/rignore-0.6.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:136629eb0ec2b6ac6ab34e71ce8065a07106fe615a53eceefc30200d528a4612", size = 976867, upload-time = "2025-07-19T19:23:24.919Z" }, - { url = "https://files.pythonhosted.org/packages/75/47/9dcee35e24897b62d66f7578f127bc91465c942a9d702d516d3fe7dcaa00/rignore-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:35e3d0ebaf01086e6454c3fecae141e2db74a5ddf4a97c72c69428baeff0b7d4", size = 1067603, upload-time = "2025-07-19T19:23:58.765Z" }, - { url = "https://files.pythonhosted.org/packages/4b/68/f66e7c0b0fc009f3e19ba8e6c3078a227285e3aecd9f6498d39df808cdfd/rignore-0.6.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:7ed1f9010fa1ef5ea0b69803d1dfb4b7355921779e03a30396034c52691658bc", size = 1136289, upload-time = "2025-07-19T19:24:11.136Z" }, - { url = "https://files.pythonhosted.org/packages/a6/b7/6fff161fe3ae5c0e0a0dded9a428e41d31c7fefc4e57c7553b9ffb064139/rignore-0.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c16e9e898ed0afe2e20fa8d6412e02bd13f039f7e0d964a289368efd4d9ad320", size = 1111566, upload-time = "2025-07-19T19:24:23.065Z" }, - { url = "https://files.pythonhosted.org/packages/1f/c5/a5978ad65074a08dad46233a3333d154ae9cb9339325f3c181002a174746/rignore-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7e6bc0bdcd404a7a8268629e8e99967127bb41e02d9eb09a471364c4bc25e215", size = 1121142, upload-time = "2025-07-19T19:24:35.151Z" }, - { url = "https://files.pythonhosted.org/packages/e8/af/91f084374b95dc2477a4bd066957beb3b61b551f2364b4f7f5bc52c9e4c7/rignore-0.6.4-cp311-cp311-win32.whl", hash = "sha256:fdd59bd63d2a49cc6d4f3598f285552ccb1a41e001df1012e0e0345cf2cabf79", size = 643031, upload-time = "2025-07-19T19:24:55.541Z" }, - { url = "https://files.pythonhosted.org/packages/07/3a/31672aa957aebba8903005313697127bbbad9db3afcfc9857150301fab1d/rignore-0.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:7bf5be0e8a01845e57b5faa47ef9c623bb2070aa2f743c2fc73321ffaae45701", size = 721003, upload-time = "2025-07-19T19:24:48.867Z" }, - { url = "https://files.pythonhosted.org/packages/ec/6c/e5af4383cdd7829ef9aa63ac82a6507983e02dbc7c2e7b9aa64b7b8e2c7a/rignore-0.6.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:74720d074b79f32449d5d212ce732e0144a294a184246d1f1e7bcc1fc5c83b69", size = 885885, upload-time = "2025-07-19T19:23:53.236Z" }, - { url = "https://files.pythonhosted.org/packages/89/3e/1b02a868830e464769aa417ee195ac352fe71ff818df8ce50c4b998edb9c/rignore-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a8184fcf567bd6b6d7b85a0c138d98dd40f63054141c96b175844414c5530d7", size = 819736, upload-time = "2025-07-19T19:23:46.565Z" }, - { url = "https://files.pythonhosted.org/packages/e0/75/b9be0c523d97c09f3c6508a67ce376aba4efe41c333c58903a0d7366439a/rignore-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcb0d7d7ecc3fbccf6477bb187c04a091579ea139f15f139abe0b3b48bdfef69", size = 892779, upload-time = "2025-07-19T19:22:35.167Z" }, - { url = "https://files.pythonhosted.org/packages/91/f4/3064b06233697f2993485d132f06fe95061fef71631485da75aed246c4fd/rignore-0.6.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:feac73377a156fb77b3df626c76f7e5893d9b4e9e886ac8c0f9d44f1206a2a91", size = 872116, upload-time = "2025-07-19T19:22:47.828Z" }, - { url = "https://files.pythonhosted.org/packages/99/94/cb8e7af9a3c0a665f10e2366144e0ebc66167cf846aca5f1ac31b3661598/rignore-0.6.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:465179bc30beb1f7a3439e428739a2b5777ed26660712b8c4e351b15a7c04483", size = 1163345, upload-time = "2025-07-19T19:23:00.557Z" }, - { url = "https://files.pythonhosted.org/packages/86/6b/49faa7ad85ceb6ccef265df40091d9992232d7f6055fa664fe0a8b13781c/rignore-0.6.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4a4877b4dca9cf31a4d09845b300c677c86267657540d0b4d3e6d0ce3110e6e9", size = 939967, upload-time = "2025-07-19T19:23:13.494Z" }, - { url = "https://files.pythonhosted.org/packages/80/c8/b91afda10bd5ca1e3a80463340b899c0dc26a7750a9f3c94f668585c7f40/rignore-0.6.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:456456802b1e77d1e2d149320ee32505b8183e309e228129950b807d204ddd17", size = 949717, upload-time = "2025-07-19T19:23:36.404Z" }, - { url = "https://files.pythonhosted.org/packages/3f/f1/88bfdde58ae3fb1c1a92bb801f492eea8eafcdaf05ab9b75130023a4670b/rignore-0.6.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c1ff2fc223f1d9473d36923160af37bf765548578eb9d47a2f52e90da8ae408", size = 975534, upload-time = "2025-07-19T19:23:25.988Z" }, - { url = "https://files.pythonhosted.org/packages/aa/8f/a80b4a2e48ceba56ba19e096d41263d844757e10aa36ede212571b5d8117/rignore-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e445fbc214ae18e0e644a78086ea5d0f579e210229a4fbe86367d11a4cd03c11", size = 1067837, upload-time = "2025-07-19T19:23:59.888Z" }, - { url = "https://files.pythonhosted.org/packages/7d/90/0905597af0e78748909ef58418442a480ddd93e9fc89b0ca9ab170c357c0/rignore-0.6.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e07d9c5270fc869bc431aadcfb6ed0447f89b8aafaa666914c077435dc76a123", size = 1134959, upload-time = "2025-07-19T19:24:12.396Z" }, - { url = "https://files.pythonhosted.org/packages/cc/7d/0fa29adf9183b61947ce6dc8a1a9779a8ea16573f557be28ec893f6ddbaa/rignore-0.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7a6ccc0ea83d2c0c6df6b166f2acacedcc220a516436490f41e99a5ae73b6019", size = 1109708, upload-time = "2025-07-19T19:24:24.176Z" }, - { url = "https://files.pythonhosted.org/packages/4e/a7/92892ed86b2e36da403dd3a0187829f2d880414cef75bd612bfdf4dedebc/rignore-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:536392c5ec91755db48389546c833c4ab1426fe03e5a8522992b54ef8a244e7e", size = 1120546, upload-time = "2025-07-19T19:24:36.377Z" }, - { url = "https://files.pythonhosted.org/packages/31/1b/d29ae1fe901d523741d6d1d3ffe0d630734dd0ed6b047628a69c1e15ea44/rignore-0.6.4-cp312-cp312-win32.whl", hash = "sha256:f5f9dca46fc41c0a1e236767f68be9d63bdd2726db13a0ae3a30f68414472969", size = 642005, upload-time = "2025-07-19T19:24:56.671Z" }, - { url = "https://files.pythonhosted.org/packages/1a/41/a224944824688995374e4525115ce85fecd82442fc85edd5bcd81f4f256d/rignore-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:e02eecb9e1b9f9bf7c9030ae73308a777bed3b2486204cc74dfcfbe699ab1497", size = 720358, upload-time = "2025-07-19T19:24:49.959Z" }, - { url = "https://files.pythonhosted.org/packages/db/a3/edd7d0d5cc0720de132b6651cef95ee080ce5fca11c77d8a47db848e5f90/rignore-0.6.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:2b3b1e266ce45189240d14dfa1057f8013ea34b9bc8b3b44125ec8d25fdb3985", size = 885304, upload-time = "2025-07-19T19:23:54.268Z" }, - { url = "https://files.pythonhosted.org/packages/93/a1/d8d2fb97a6548307507d049b7e93885d4a0dfa1c907af5983fd9f9362a21/rignore-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45fe803628cc14714df10e8d6cdc23950a47eb9eb37dfea9a4779f4c672d2aa0", size = 818799, upload-time = "2025-07-19T19:23:47.544Z" }, - { url = "https://files.pythonhosted.org/packages/b1/cd/949981fcc180ad5ba7b31c52e78b74b2dea6b7bf744ad4c0c4b212f6da78/rignore-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e439f034277a947a4126e2da79dbb43e33d73d7c09d3d72a927e02f8a16f59aa", size = 892024, upload-time = "2025-07-19T19:22:36.18Z" }, - { url = "https://files.pythonhosted.org/packages/b0/d3/9042d701a8062d9c88f87760bbc2695ee2c23b3f002d34486b72a85f8efe/rignore-0.6.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:84b5121650ae24621154c7bdba8b8970b0739d8146505c9f38e0cda9385d1004", size = 871430, upload-time = "2025-07-19T19:22:49.62Z" }, - { url = "https://files.pythonhosted.org/packages/eb/50/3370249b984212b7355f3d9241aa6d02e706067c6d194a2614dfbc0f5b27/rignore-0.6.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52b0957b585ab48a445cf8ac1dbc33a272ab060835e583b4f95aa8c67c23fb2b", size = 1160559, upload-time = "2025-07-19T19:23:01.629Z" }, - { url = "https://files.pythonhosted.org/packages/6c/6f/2ad7f925838091d065524f30a8abda846d1813eee93328febf262b5cda21/rignore-0.6.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:50359e0d5287b5e2743bd2f2fbf05df619c8282fd3af12f6628ff97b9675551d", size = 939947, upload-time = "2025-07-19T19:23:14.608Z" }, - { url = "https://files.pythonhosted.org/packages/1f/01/626ec94d62475ae7ef8b00ef98cea61cbea52a389a666703c97c4673d406/rignore-0.6.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efe18096dcb1596757dfe0b412aab6d32564473ae7ee58dea0a8b4be5b1a2e3b", size = 949471, upload-time = "2025-07-19T19:23:37.521Z" }, - { url = "https://files.pythonhosted.org/packages/e8/c3/699c4f03b3c46f4b5c02f17a0a339225da65aad547daa5b03001e7c6a382/rignore-0.6.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b79c212d9990a273ad91e8d9765e1766ef6ecedd3be65375d786a252762ba385", size = 974912, upload-time = "2025-07-19T19:23:27.13Z" }, - { url = "https://files.pythonhosted.org/packages/cd/35/04626c12f9f92a9fc789afc2be32838a5d9b23b6fa8b2ad4a8625638d15b/rignore-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c6ffa7f2a8894c65aa5dc4e8ac8bbdf39a326c0c6589efd27686cfbb48f0197d", size = 1067281, upload-time = "2025-07-19T19:24:01.016Z" }, - { url = "https://files.pythonhosted.org/packages/fe/9c/8f17baf3b984afea151cb9094716f6f1fb8e8737db97fc6eb6d494bd0780/rignore-0.6.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a63f5720dffc8d8fb0a4d02fafb8370a4031ebf3f99a4e79f334a91e905b7349", size = 1134414, upload-time = "2025-07-19T19:24:13.534Z" }, - { url = "https://files.pythonhosted.org/packages/10/88/ef84ffa916a96437c12cefcc39d474122da9626d75e3a2ebe09ec5d32f1b/rignore-0.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ce33982da47ac5dc09d19b04fa8d7c9aa6292fc0bd1ecf33076989faa8886094", size = 1109330, upload-time = "2025-07-19T19:24:25.303Z" }, - { url = "https://files.pythonhosted.org/packages/27/43/2ada5a2ec03b82e903610a1c483f516f78e47700ee6db9823f739e08b3af/rignore-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d899621867aa266824fbd9150e298f19d25b93903ef0133c09f70c65a3416eca", size = 1120381, upload-time = "2025-07-19T19:24:37.798Z" }, - { url = "https://files.pythonhosted.org/packages/3b/99/e7bcc643085131cb14dbea772def72bf1f6fe9037171ebe177c4f228abc8/rignore-0.6.4-cp313-cp313-win32.whl", hash = "sha256:d0615a6bf4890ec5a90b5fb83666822088fbd4e8fcd740c386fcce51e2f6feea", size = 641761, upload-time = "2025-07-19T19:24:58.096Z" }, - { url = "https://files.pythonhosted.org/packages/d9/25/7798908044f27dea1a8abdc75c14523e33770137651e5f775a15143f4218/rignore-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:145177f0e32716dc2f220b07b3cde2385b994b7ea28d5c96fbec32639e9eac6f", size = 719876, upload-time = "2025-07-19T19:24:51.125Z" }, - { url = "https://files.pythonhosted.org/packages/b4/e3/ae1e30b045bf004ad77bbd1679b9afff2be8edb166520921c6f29420516a/rignore-0.6.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e55bf8f9bbd186f58ab646b4a08718c77131d28a9004e477612b0cbbd5202db2", size = 891776, upload-time = "2025-07-19T19:22:37.78Z" }, - { url = "https://files.pythonhosted.org/packages/45/a9/1193e3bc23ca0e6eb4f17cf4b99971237f97cfa6f241d98366dff90a6d09/rignore-0.6.4-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2521f7bf3ee1f2ab22a100a3a4eed39a97b025804e5afe4323528e9ce8f084a5", size = 871442, upload-time = "2025-07-19T19:22:50.972Z" }, - { url = "https://files.pythonhosted.org/packages/20/83/4c52ae429a0b2e1ce667e35b480e9a6846f9468c443baeaed5d775af9485/rignore-0.6.4-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cc35773a8a9c119359ef974d0856988d4601d4daa6f532c05f66b4587cf35bc", size = 1159844, upload-time = "2025-07-19T19:23:02.751Z" }, - { url = "https://files.pythonhosted.org/packages/c1/2f/c740f5751f464c937bfe252dc15a024ae081352cfe80d94aa16d6a617482/rignore-0.6.4-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b665b1ea14457d7b49e834baabc635a3b8c10cfb5cca5c21161fabdbfc2b850e", size = 939456, upload-time = "2025-07-19T19:23:15.72Z" }, - { url = "https://files.pythonhosted.org/packages/fc/dd/68dbb08ac0edabf44dd144ff546a3fb0253c5af708e066847df39fc9188f/rignore-0.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c7fd339f344a8548724f289495b835bed7b81174a0bc1c28c6497854bd8855db", size = 1067070, upload-time = "2025-07-19T19:24:02.803Z" }, - { url = "https://files.pythonhosted.org/packages/3b/3a/7e7ea6f0d31d3f5beb0f2cf2c4c362672f5f7f125714458673fc579e2bed/rignore-0.6.4-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:91dc94b1cc5af8d6d25ce6edd29e7351830f19b0a03b75cb3adf1f76d00f3007", size = 1134598, upload-time = "2025-07-19T19:24:15.039Z" }, - { url = "https://files.pythonhosted.org/packages/7e/06/1b3307f6437d29bede5a95738aa89e6d910ba68d4054175c9f60d8e2c6b1/rignore-0.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4d1918221a249e5342b60fd5fa513bf3d6bf272a8738e66023799f0c82ecd788", size = 1108862, upload-time = "2025-07-19T19:24:26.765Z" }, - { url = "https://files.pythonhosted.org/packages/b0/d5/b37c82519f335f2c472a63fc6215c6f4c51063ecf3166e3acf508011afbd/rignore-0.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:240777332b859dc89dcba59ab6e3f1e062bc8e862ffa3e5f456e93f7fd5cb415", size = 1120002, upload-time = "2025-07-19T19:24:38.952Z" }, - { url = "https://files.pythonhosted.org/packages/ac/72/2f05559ed5e69bdfdb56ea3982b48e6c0017c59f7241f7e1c5cae992b347/rignore-0.6.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66b0e548753e55cc648f1e7b02d9f74285fe48bb49cec93643d31e563773ab3f", size = 949454, upload-time = "2025-07-19T19:23:38.664Z" }, - { url = "https://files.pythonhosted.org/packages/0b/92/186693c8f838d670510ac1dfb35afbe964320fbffb343ba18f3d24441941/rignore-0.6.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6971ac9fdd5a0bd299a181096f091c4f3fd286643adceba98eccc03c688a6637", size = 974663, upload-time = "2025-07-19T19:23:28.24Z" }, - { url = "https://files.pythonhosted.org/packages/85/4d/5a69ea5ae7de78eddf0a0699b6dbd855f87c1436673425461188ea39662f/rignore-0.6.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40f493eef4b191777ba6d16879e3f73836142e04480d2e2f483675d652e6b559", size = 895408, upload-time = "2025-07-19T19:22:42.16Z" }, - { url = "https://files.pythonhosted.org/packages/a3/c3/b6cdf9b676d6774c5de3ca04a5f4dbaffae3bb06bdee395e095be24f098e/rignore-0.6.4-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6790635e4df35333e27cd9e8b31d1d559826cf8b52f2c374b81ab698ac0140cf", size = 873042, upload-time = "2025-07-19T19:22:54.663Z" }, - { url = "https://files.pythonhosted.org/packages/80/25/61182149b2f2ca86c22c6253b361ec0e983e60e913ca75588a7d559b41eb/rignore-0.6.4-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e326dab28787f07c6987c04686d4ad9d4b1e1caca1a15b85d443f91af2e133d2", size = 1162036, upload-time = "2025-07-19T19:23:06.916Z" }, - { url = "https://files.pythonhosted.org/packages/db/44/7fe55c2b7adc8c90dc8709ef2fac25fa526b0c8bfd1090af4e6b33c2e42f/rignore-0.6.4-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd24cb0f58c6036b0f64ac6fc3f759b7f0de5506fa9f5a65e9d57f8cf44a026d", size = 940381, upload-time = "2025-07-19T19:23:19.364Z" }, - { url = "https://files.pythonhosted.org/packages/3a/a3/8cc0c9a9db980a1589007d0fedcaf41475820e0cd4950a5f6eeb8ebc0ee0/rignore-0.6.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36cb95b0acae3c88b99a39f4246b395fd983848f3ec85ff26531d638b6584a45", size = 951924, upload-time = "2025-07-19T19:23:42.209Z" }, - { url = "https://files.pythonhosted.org/packages/07/f2/4f2c88307c84801d6c772c01e8d856deaa8e85117180b88aaa0f41d4f86f/rignore-0.6.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dfc954973429ce545d06163d87a6bae0ccea5703adbc957ee3d332c9592a58eb", size = 976515, upload-time = "2025-07-19T19:23:31.524Z" }, - { url = "https://files.pythonhosted.org/packages/a4/bd/f701ddf897cf5e3f394107e6dad147216b3a0d84e9d53d7a5fed7cc97d26/rignore-0.6.4-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:cbed37d7c128b58ab9ade80e131efc4a48b6d045cd0bd1d3254cbb6b4a0ad67e", size = 1069896, upload-time = "2025-07-19T19:24:06.24Z" }, - { url = "https://files.pythonhosted.org/packages/00/52/1ae54afad26aafcfee1b44a36b27bb0dd63f1c23081e1599dbf681368925/rignore-0.6.4-pp310-pypy310_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:a0db910ef867d6ca2d52fefd22d8b6b63b20ec61661e2ad57e5c425a4e39431a", size = 1136337, upload-time = "2025-07-19T19:24:18.529Z" }, - { url = "https://files.pythonhosted.org/packages/85/9a/3b74aabb69ed118d0b493afa62d1aacc3bf12b8f11bf682a3c02174c3068/rignore-0.6.4-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:d664443a0a71d0a7d669adf32be59c4249bbff8b2810960f1b91d413ee4cf6b8", size = 1111677, upload-time = "2025-07-19T19:24:30.21Z" }, - { url = "https://files.pythonhosted.org/packages/70/7d/bd0f6c1bc89c80b116b526b77cdd5263c0ad218d5416aebf4ca9cce9ca73/rignore-0.6.4-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:b9f6f1d91429b4a6772152848815cf1459663796b7b899a0e15d9198e32c9371", size = 1122823, upload-time = "2025-07-19T19:24:42.476Z" }, - { url = "https://files.pythonhosted.org/packages/33/a1/daaa2df10dfa6d87c896a5783c8407c284530d5a056307d1f55a8ef0c533/rignore-0.6.4-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b3da26d5a35ab15525b68d30b7352ad2247321f5201fc7e50ba6d547f78d5ea", size = 895772, upload-time = "2025-07-19T19:22:43.423Z" }, - { url = "https://files.pythonhosted.org/packages/35/e6/65130a50cd3ed11c967034dfd653e160abb7879fb4ee338a1cccaeda7acd/rignore-0.6.4-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43028f3587558231d9fa68accff58c901dc50fd7bbc5764d3ee3df95290f6ebf", size = 873093, upload-time = "2025-07-19T19:22:55.745Z" }, - { url = "https://files.pythonhosted.org/packages/32/c4/02ead1274ce935c59f2bb3deaaaa339df9194bc40e3c2d8d623e31e47ec4/rignore-0.6.4-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc56f1fcab7740751b98fead67b98ba64896424d8c834ea22089568db4e36dfa", size = 1162199, upload-time = "2025-07-19T19:23:08.376Z" }, - { url = "https://files.pythonhosted.org/packages/78/0c/94a4edce0e80af69f200cc35d8da4c727c52d28f0c9d819b388849ae8ef6/rignore-0.6.4-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6033f2280898535a5f69935e08830a4e49ff1e29ef2c3f9a2b9ced59de06fdbf", size = 940176, upload-time = "2025-07-19T19:23:20.862Z" }, - { url = "https://files.pythonhosted.org/packages/43/92/21ec579c999a3ed4d1b2a5926a9d0edced7c65d8ac353bc9120d49b05a64/rignore-0.6.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f5ac0c4e6a24be88f3821e101ef4665e9e1dc015f9e45109f32fed71dbcdafa", size = 951632, upload-time = "2025-07-19T19:23:43.32Z" }, - { url = "https://files.pythonhosted.org/packages/67/c4/72e7ba244222b9efdeb18f9974d6f1e30cf5a2289e1b482a1e8b3ebee90f/rignore-0.6.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8906ac8dd585ece83b1346e0470260a1951058cc0ef5a17542069bde4aa3f42f", size = 976923, upload-time = "2025-07-19T19:23:32.678Z" }, - { url = "https://files.pythonhosted.org/packages/8e/14/e754c12bc953c7fa309687cd30a6ea95e5721168fb0b2a99a34bff24be5c/rignore-0.6.4-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:14d095622969504a2e56f666286202dad583f08d3347b7be2d647ddfd7a9bf47", size = 1069861, upload-time = "2025-07-19T19:24:07.671Z" }, - { url = "https://files.pythonhosted.org/packages/a6/24/ba2bdaf04a19b5331c051b9d480e8daca832bed4aeaa156d6d679044c06c/rignore-0.6.4-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:30f3d688df7eb4850318f1b5864d14f2c5fe5dbf3803ed0fc8329d2a7ad560dc", size = 1136368, upload-time = "2025-07-19T19:24:19.68Z" }, - { url = "https://files.pythonhosted.org/packages/83/48/7cf52353299e02aa629150007fa75f4b91d99b4f2fa536f2e24ead810116/rignore-0.6.4-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:028f62a7b0a6235bb3f03c9e7f342352e7fa4b3f08c761c72f9de8faee40ed9c", size = 1111714, upload-time = "2025-07-19T19:24:31.717Z" }, - { url = "https://files.pythonhosted.org/packages/84/9c/3881ad34f01942af0cf713e25e476bf851e04e389cc3ff146c3b459ab861/rignore-0.6.4-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:7e6c425603db2c147eace4f752ca3cd4551e7568c9d332175d586c68bcbe3d8d", size = 1122433, upload-time = "2025-07-19T19:24:43.973Z" }, -] - [[package]] name = "rpds-py" version = "0.26.0" @@ -4670,19 +4581,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6a/23/8146aad7d88f4fcb3a6218f41a60f6c2d4e3a72de72da1825dc7c8f7877c/semantic_version-2.10.0-py2.py3-none-any.whl", hash = "sha256:de78a3b8e0feda74cabc54aab2da702113e33ac9d9eb9d2389bcf1f58b7d9177", size = 15552, upload-time = "2022-05-26T13:35:21.206Z" }, ] -[[package]] -name = "sentry-sdk" -version = "2.35.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bd/79/0ecb942f3f1ad26c40c27f81ff82392d85c01d26a45e3c72c2b37807e680/sentry_sdk-2.35.2.tar.gz", hash = "sha256:e9e8f3c795044beb59f2c8f4c6b9b0f9779e5e604099882df05eec525e782cc6", size = 343377, upload-time = "2025-09-01T11:00:58.633Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/91/a43308dc82a0e32d80cd0dfdcfca401ecbd0f431ab45f24e48bb97b7800d/sentry_sdk-2.35.2-py2.py3-none-any.whl", hash = "sha256:38c98e3cbb620dd3dd80a8d6e39c753d453dd41f8a9df581b0584c19a52bc926", size = 363975, upload-time = "2025-09-01T11:00:56.574Z" }, -] - [[package]] name = "shellingham" version = "1.5.4" @@ -5022,6 +4920,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/42/3efaf858001d2c2913de7f354563e3a3a2f0decae3efe98427125a8f441e/typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855", size = 46317, upload-time = "2025-05-26T14:30:30.523Z" }, ] +[[package]] +name = "typer-slim" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/75/d6/489402eda270c00555213bdd53061b23a0ae2b5dccbfe428ebcc9562d883/typer_slim-0.19.2.tar.gz", hash = "sha256:6f601e28fb8249a7507f253e35fb22ccc701403ce99bea6a9923909ddbfcd133", size = 104788, upload-time = "2025-09-23T09:47:42.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/19/7aef771b3293e1b7c749eebb2948bb7ccd0e9b56aa222eb4d5e015087730/typer_slim-0.19.2-py3-none-any.whl", hash = "sha256:1c9cdbbcd5b8d30f4118d3cb7c52dc63438b751903fbd980a35df1dfe10c6c91", size = 46806, upload-time = "2025-09-23T09:47:41.385Z" }, +] + [[package]] name = "types-awscrt" version = "0.23.10" @@ -5132,49 +5043,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315, upload-time = "2024-12-15T13:33:27.467Z" }, ] -[package.optional-dependencies] -standard = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "httptools" }, - { name = "python-dotenv" }, - { name = "pyyaml" }, - { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, - { name = "watchfiles" }, - { name = "websockets" }, -] - -[[package]] -name = "uvloop" -version = "0.21.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741, upload-time = "2024-10-14T23:38:35.489Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/76/44a55515e8c9505aa1420aebacf4dd82552e5e15691654894e90d0bd051a/uvloop-0.21.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f", size = 1442019, upload-time = "2024-10-14T23:37:20.068Z" }, - { url = "https://files.pythonhosted.org/packages/35/5a/62d5800358a78cc25c8a6c72ef8b10851bdb8cca22e14d9c74167b7f86da/uvloop-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d", size = 801898, upload-time = "2024-10-14T23:37:22.663Z" }, - { url = "https://files.pythonhosted.org/packages/f3/96/63695e0ebd7da6c741ccd4489b5947394435e198a1382349c17b1146bb97/uvloop-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26", size = 3827735, upload-time = "2024-10-14T23:37:25.129Z" }, - { url = "https://files.pythonhosted.org/packages/61/e0/f0f8ec84979068ffae132c58c79af1de9cceeb664076beea86d941af1a30/uvloop-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb", size = 3825126, upload-time = "2024-10-14T23:37:27.59Z" }, - { url = "https://files.pythonhosted.org/packages/bf/fe/5e94a977d058a54a19df95f12f7161ab6e323ad49f4dabc28822eb2df7ea/uvloop-0.21.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f", size = 3705789, upload-time = "2024-10-14T23:37:29.385Z" }, - { url = "https://files.pythonhosted.org/packages/26/dd/c7179618e46092a77e036650c1f056041a028a35c4d76945089fcfc38af8/uvloop-0.21.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c", size = 3800523, upload-time = "2024-10-14T23:37:32.048Z" }, - { url = "https://files.pythonhosted.org/packages/57/a7/4cf0334105c1160dd6819f3297f8700fda7fc30ab4f61fbf3e725acbc7cc/uvloop-0.21.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8", size = 1447410, upload-time = "2024-10-14T23:37:33.612Z" }, - { url = "https://files.pythonhosted.org/packages/8c/7c/1517b0bbc2dbe784b563d6ab54f2ef88c890fdad77232c98ed490aa07132/uvloop-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0", size = 805476, upload-time = "2024-10-14T23:37:36.11Z" }, - { url = "https://files.pythonhosted.org/packages/ee/ea/0bfae1aceb82a503f358d8d2fa126ca9dbdb2ba9c7866974faec1cb5875c/uvloop-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e", size = 3960855, upload-time = "2024-10-14T23:37:37.683Z" }, - { url = "https://files.pythonhosted.org/packages/8a/ca/0864176a649838b838f36d44bf31c451597ab363b60dc9e09c9630619d41/uvloop-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb", size = 3973185, upload-time = "2024-10-14T23:37:40.226Z" }, - { url = "https://files.pythonhosted.org/packages/30/bf/08ad29979a936d63787ba47a540de2132169f140d54aa25bc8c3df3e67f4/uvloop-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6", size = 3820256, upload-time = "2024-10-14T23:37:42.839Z" }, - { url = "https://files.pythonhosted.org/packages/da/e2/5cf6ef37e3daf2f06e651aae5ea108ad30df3cb269102678b61ebf1fdf42/uvloop-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d", size = 3937323, upload-time = "2024-10-14T23:37:45.337Z" }, - { url = "https://files.pythonhosted.org/packages/8c/4c/03f93178830dc7ce8b4cdee1d36770d2f5ebb6f3d37d354e061eefc73545/uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c", size = 1471284, upload-time = "2024-10-14T23:37:47.833Z" }, - { url = "https://files.pythonhosted.org/packages/43/3e/92c03f4d05e50f09251bd8b2b2b584a2a7f8fe600008bcc4523337abe676/uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2", size = 821349, upload-time = "2024-10-14T23:37:50.149Z" }, - { url = "https://files.pythonhosted.org/packages/a6/ef/a02ec5da49909dbbfb1fd205a9a1ac4e88ea92dcae885e7c961847cd51e2/uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d", size = 4580089, upload-time = "2024-10-14T23:37:51.703Z" }, - { url = "https://files.pythonhosted.org/packages/06/a7/b4e6a19925c900be9f98bec0a75e6e8f79bb53bdeb891916609ab3958967/uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc", size = 4693770, upload-time = "2024-10-14T23:37:54.122Z" }, - { url = "https://files.pythonhosted.org/packages/ce/0c/f07435a18a4b94ce6bd0677d8319cd3de61f3a9eeb1e5f8ab4e8b5edfcb3/uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb", size = 4451321, upload-time = "2024-10-14T23:37:55.766Z" }, - { url = "https://files.pythonhosted.org/packages/8f/eb/f7032be105877bcf924709c97b1bf3b90255b4ec251f9340cef912559f28/uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f", size = 4659022, upload-time = "2024-10-14T23:37:58.195Z" }, - { url = "https://files.pythonhosted.org/packages/3f/8d/2cbef610ca21539f0f36e2b34da49302029e7c9f09acef0b1c3b5839412b/uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281", size = 1468123, upload-time = "2024-10-14T23:38:00.688Z" }, - { url = "https://files.pythonhosted.org/packages/93/0d/b0038d5a469f94ed8f2b2fce2434a18396d8fbfb5da85a0a9781ebbdec14/uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af", size = 819325, upload-time = "2024-10-14T23:38:02.309Z" }, - { url = "https://files.pythonhosted.org/packages/50/94/0a687f39e78c4c1e02e3272c6b2ccdb4e0085fda3b8352fecd0410ccf915/uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6", size = 4582806, upload-time = "2024-10-14T23:38:04.711Z" }, - { url = "https://files.pythonhosted.org/packages/d2/19/f5b78616566ea68edd42aacaf645adbf71fbd83fc52281fba555dc27e3f1/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816", size = 4701068, upload-time = "2024-10-14T23:38:06.385Z" }, - { url = "https://files.pythonhosted.org/packages/47/57/66f061ee118f413cd22a656de622925097170b9380b30091b78ea0c6ea75/uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc", size = 4454428, upload-time = "2024-10-14T23:38:08.416Z" }, - { url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018, upload-time = "2024-10-14T23:38:10.888Z" }, -] - [[package]] name = "vcrpy" version = "5.1.0" From aade6d501c670ebbaece08a371f8f29810274d07 Mon Sep 17 00:00:00 2001 From: William Easton Date: Wed, 1 Oct 2025 10:03:33 -0500 Subject: [PATCH 15/20] Remove accidental test file commit --- pydantic_ai_slim/pydantic_ai/toolsets/test.py | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 pydantic_ai_slim/pydantic_ai/toolsets/test.py diff --git a/pydantic_ai_slim/pydantic_ai/toolsets/test.py b/pydantic_ai_slim/pydantic_ai/toolsets/test.py deleted file mode 100644 index f7889a831a..0000000000 --- a/pydantic_ai_slim/pydantic_ai/toolsets/test.py +++ /dev/null @@ -1,11 +0,0 @@ -from pydantic_ai import Agent -from pydantic_ai.toolsets.fastmcp import FastMCPToolset -from fastmcp.client.transports import StdioTransport - -server = StdioTransport( # (1)! - command='uv', args=['run', 'mcp-run-python', 'stdio'] -) -agent = Agent('openai:gpt-4o', toolsets=[FastMCPToolset(mcp=server)]) - -async def main(): - async with agent: # (2)! \ No newline at end of file From 31a317902342a2c05b254a036a109b5f42f9c271 Mon Sep 17 00:00:00 2001 From: William Easton Date: Sun, 12 Oct 2025 11:09:00 -0400 Subject: [PATCH 16/20] Updates to docs and tests --- docs/mcp/client.md | 617 +----------------- docs/mcp/fastmcp-client.md | 69 ++ docs/mcp/mcp-client.md | 616 +++++++++++++++++ docs/toolsets.md | 68 +- .../pydantic_ai/toolsets/fastmcp.py | 21 +- pyproject.toml | 2 +- tests/test_fastmcp.py | 165 +++-- uv.lock | 20 +- 8 files changed, 840 insertions(+), 738 deletions(-) create mode 100644 docs/mcp/fastmcp-client.md create mode 100644 docs/mcp/mcp-client.md diff --git a/docs/mcp/client.md b/docs/mcp/client.md index 6c47dcf4e3..94a5423756 100644 --- a/docs/mcp/client.md +++ b/docs/mcp/client.md @@ -1,616 +1,9 @@ -# Client +Pydantic AI provides two ways to connect to and call tools on local and remote MCP Servers. Both options support basic tool calling on local and remote MCP Servers, if you have more advanced requirements you may find that one of the options is a better fit for your use case. -Pydantic AI can act as an [MCP client](https://modelcontextprotocol.io/quickstart/client), connecting to MCP servers -to use their tools. +## MCP SDK-based Client -## Install +The [MCP SDK-based Client](./mcp-client.md) offers more direct control by leveraging the MCP SDK directly. -You need to either install [`pydantic-ai`](../install.md), or[`pydantic-ai-slim`](../install.md#slim-install) with the `mcp` optional group: +## FastMCP-based Client -```bash -pip/uv-add "pydantic-ai-slim[mcp]" -``` - -## Usage - -Pydantic AI comes with three ways to connect to MCP servers: - -- [`MCPServerStreamableHTTP`][pydantic_ai.mcp.MCPServerStreamableHTTP] which connects to an MCP server using the [Streamable HTTP](https://modelcontextprotocol.io/introduction#streamable-http) transport -- [`MCPServerSSE`][pydantic_ai.mcp.MCPServerSSE] which connects to an MCP server using the [HTTP SSE](https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#http-with-sse) transport -- [`MCPServerStdio`][pydantic_ai.mcp.MCPServerStdio] which runs the server as a subprocess and connects to it using the [stdio](https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#stdio) transport - -Examples of all three are shown below. - -Each MCP server instance is a [toolset](../toolsets.md) and can be registered with an [`Agent`][pydantic_ai.Agent] using the `toolsets` argument. - -You can use the [`async with agent`][pydantic_ai.Agent.__aenter__] context manager to open and close connections to all registered servers (and in the case of stdio servers, start and stop the subprocesses) around the context where they'll be used in agent runs. You can also use [`async with server`][pydantic_ai.mcp.MCPServer.__aenter__] to manage the connection or subprocess of a specific server, for example if you'd like to use it with multiple agents. If you don't explicitly enter one of these context managers to set up the server, this will be done automatically when it's needed (e.g. to list the available tools or call a specific tool), but it's more efficient to do so around the entire context where you expect the servers to be used. - -### Streamable HTTP Client - -[`MCPServerStreamableHTTP`][pydantic_ai.mcp.MCPServerStreamableHTTP] connects over HTTP using the -[Streamable HTTP](https://modelcontextprotocol.io/introduction#streamable-http) transport to a server. - -!!! note - [`MCPServerStreamableHTTP`][pydantic_ai.mcp.MCPServerStreamableHTTP] requires an MCP server to be running and accepting HTTP connections before running the agent. Running the server is not managed by Pydantic AI. - -Before creating the Streamable HTTP client, we need to run a server that supports the Streamable HTTP transport. - -```python {title="streamable_http_server.py" dunder_name="not_main"} -from mcp.server.fastmcp import FastMCP - -app = FastMCP() - -@app.tool() -def add(a: int, b: int) -> int: - return a + b - -if __name__ == '__main__': - app.run(transport='streamable-http') -``` - -Then we can create the client: - -```python {title="mcp_streamable_http_client.py"} -from pydantic_ai import Agent -from pydantic_ai.mcp import MCPServerStreamableHTTP - -server = MCPServerStreamableHTTP('http://localhost:8000/mcp') # (1)! -agent = Agent('openai:gpt-4o', toolsets=[server]) # (2)! - -async def main(): - async with agent: # (3)! - result = await agent.run('What is 7 plus 5?') - print(result.output) - #> The answer is 12. -``` - -1. Define the MCP server with the URL used to connect. -2. Create an agent with the MCP server attached. -3. Create a client session to connect to the server. - -_(This example is complete, it can be run "as is" — you'll need to add `asyncio.run(main())` to run `main`)_ - -**What's happening here?** - -- The model receives the prompt "What is 7 plus 5?" -- The model decides "Oh, I've got this `add` tool, that will be a good way to answer this question" -- The model returns a tool call -- Pydantic AI sends the tool call to the MCP server using the Streamable HTTP transport -- The model is called again with the return value of running the `add` tool (12) -- The model returns the final answer - -You can visualise this clearly, and even see the tool call, by adding three lines of code to instrument the example with [logfire](https://logfire.pydantic.dev/docs): - -```python {title="mcp_sse_client_logfire.py" test="skip"} -import logfire - -logfire.configure() -logfire.instrument_pydantic_ai() -``` - -### SSE Client - -[`MCPServerSSE`][pydantic_ai.mcp.MCPServerSSE] connects over HTTP using the [HTTP + Server Sent Events transport](https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#http-with-sse) to a server. - -!!! note - The SSE transport in MCP is deprecated, you should use Streamable HTTP instead. - -Before creating the SSE client, we need to run a server that supports the SSE transport. - - -```python {title="sse_server.py" dunder_name="not_main"} -from mcp.server.fastmcp import FastMCP - -app = FastMCP() - -@app.tool() -def add(a: int, b: int) -> int: - return a + b - -if __name__ == '__main__': - app.run(transport='sse') -``` - -Then we can create the client: - -```python {title="mcp_sse_client.py"} -from pydantic_ai import Agent -from pydantic_ai.mcp import MCPServerSSE - -server = MCPServerSSE('http://localhost:3001/sse') # (1)! -agent = Agent('openai:gpt-4o', toolsets=[server]) # (2)! - - -async def main(): - async with agent: # (3)! - result = await agent.run('What is 7 plus 5?') - print(result.output) - #> The answer is 12. -``` - -1. Define the MCP server with the URL used to connect. -2. Create an agent with the MCP server attached. -3. Create a client session to connect to the server. - -_(This example is complete, it can be run "as is" — you'll need to add `asyncio.run(main())` to run `main`)_ - -### MCP "stdio" Server - -MCP also offers [stdio transport](https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#stdio) where the server is run as a subprocess and communicates with the client over `stdin` and `stdout`. In this case, you'd use the [`MCPServerStdio`][pydantic_ai.mcp.MCPServerStdio] class. - -In this example [mcp-run-python](https://github.com/pydantic/mcp-run-python) is used as the MCP server. - -```python {title="mcp_stdio_client.py"} -from pydantic_ai import Agent -from pydantic_ai.mcp import MCPServerStdio - -server = MCPServerStdio( # (1)! - 'uv', args=['run', 'mcp-run-python', 'stdio'], timeout=10 -) -agent = Agent('openai:gpt-4o', toolsets=[server]) - - -async def main(): - async with agent: - result = await agent.run('How many days between 2000-01-01 and 2025-03-18?') - print(result.output) - #> There are 9,208 days between January 1, 2000, and March 18, 2025. -``` - -1. See [MCP Run Python](https://github.com/pydantic/mcp-run-python) for more information. - -## Loading MCP Servers from Configuration - -Instead of creating MCP server instances individually in code, you can load multiple servers from a JSON configuration file using [`load_mcp_servers()`][pydantic_ai.mcp.load_mcp_servers]. - -This is particularly useful when you need to manage multiple MCP servers or want to configure servers externally without modifying code. - -### Configuration Format - -The configuration file should be a JSON file with an `mcpServers` object containing server definitions. Each server is identified by a unique key and contains the configuration for that server type: - -```json {title="mcp_config.json"} -{ - "mcpServers": { - "python-runner": { - "command": "uv", - "args": ["run", "mcp-run-python", "stdio"] - }, - "weather-api": { - "url": "http://localhost:3001/sse" - }, - "calculator": { - "url": "http://localhost:8000/mcp" - } - } -} -``` - -!!! note - The MCP server is only inferred to be an SSE server because of the `/sse` suffix. - Any other server with the "url" field will be inferred to be a Streamable HTTP server. - - We made this decision given that the SSE transport is deprecated. - -### Usage - -```python {title="mcp_config_loader.py" test="skip"} -from pydantic_ai import Agent -from pydantic_ai.mcp import load_mcp_servers - -# Load all servers from configuration file -servers = load_mcp_servers('mcp_config.json') - -# Create agent with all loaded servers -agent = Agent('openai:gpt-5', toolsets=servers) - -async def main(): - async with agent: - result = await agent.run('What is 7 plus 5?') - print(result.output) -``` - -_(This example is complete, it can be run "as is" — you'll need to add `asyncio.run(main())` to run `main`)_ - -## Tool call customization - -The MCP servers provide the ability to set a `process_tool_call` which allows -the customization of tool call requests and their responses. - -A common use case for this is to inject metadata to the requests which the server -call needs: - -```python {title="mcp_process_tool_call.py"} -from typing import Any - -from pydantic_ai import Agent, RunContext -from pydantic_ai.mcp import CallToolFunc, MCPServerStdio, ToolResult -from pydantic_ai.models.test import TestModel - - -async def process_tool_call( - ctx: RunContext[int], - call_tool: CallToolFunc, - name: str, - tool_args: dict[str, Any], -) -> ToolResult: - """A tool call processor that passes along the deps.""" - return await call_tool(name, tool_args, {'deps': ctx.deps}) - - -server = MCPServerStdio('python', args=['mcp_server.py'], process_tool_call=process_tool_call) -agent = Agent( - model=TestModel(call_tools=['echo_deps']), - deps_type=int, - toolsets=[server] -) - - -async def main(): - async with agent: - result = await agent.run('Echo with deps set to 42', deps=42) - print(result.output) - #> {"echo_deps":{"echo":"This is an echo message","deps":42}} -``` - -How to access the metadata is MCP server SDK specific. For example with the [MCP Python -SDK](https://github.com/modelcontextprotocol/python-sdk), it is accessible via the -[`ctx: Context`](https://github.com/modelcontextprotocol/python-sdk#context) -argument that can be included on tool call handlers: - -```python {title="mcp_server.py"} -from typing import Any - -from mcp.server.fastmcp import Context, FastMCP -from mcp.server.session import ServerSession - -mcp = FastMCP('Pydantic AI MCP Server') -log_level = 'unset' - - -@mcp.tool() -async def echo_deps(ctx: Context[ServerSession, None]) -> dict[str, Any]: - """Echo the run context. - - Args: - ctx: Context object containing request and session information. - - Returns: - Dictionary with an echo message and the deps. - """ - await ctx.info('This is an info message') - - deps: Any = getattr(ctx.request_context.meta, 'deps') - return {'echo': 'This is an echo message', 'deps': deps} - -if __name__ == '__main__': - mcp.run() -``` - -## Using Tool Prefixes to Avoid Naming Conflicts - -When connecting to multiple MCP servers that might provide tools with the same name, you can use the `tool_prefix` parameter to avoid naming conflicts. This parameter adds a prefix to all tool names from a specific server. - -This allows you to use multiple servers that might have overlapping tool names without conflicts: - -```python {title="mcp_tool_prefix_http_client.py"} -from pydantic_ai import Agent -from pydantic_ai.mcp import MCPServerSSE - -# Create two servers with different prefixes -weather_server = MCPServerSSE( - 'http://localhost:3001/sse', - tool_prefix='weather' # Tools will be prefixed with 'weather_' -) - -calculator_server = MCPServerSSE( - 'http://localhost:3002/sse', - tool_prefix='calc' # Tools will be prefixed with 'calc_' -) - -# Both servers might have a tool named 'get_data', but they'll be exposed as: -# - 'weather_get_data' -# - 'calc_get_data' -agent = Agent('openai:gpt-4o', toolsets=[weather_server, calculator_server]) -``` - -## Tool metadata - -MCP tools can include metadata that provides additional information about the tool's characteristics, which can be useful when [filtering tools][pydantic_ai.toolsets.FilteredToolset]. The `meta`, `annotations`, and `output_schema` fields can be found on the `metadata` dict on the [`ToolDefinition`][pydantic_ai.tools.ToolDefinition] object that's passed to filter functions. - -## Custom TLS / SSL configuration - -In some environments you need to tweak how HTTPS connections are established – -for example to trust an internal Certificate Authority, present a client -certificate for **mTLS**, or (during local development only!) disable -certificate verification altogether. -All HTTP-based MCP client classes -([`MCPServerStreamableHTTP`][pydantic_ai.mcp.MCPServerStreamableHTTP] and -[`MCPServerSSE`][pydantic_ai.mcp.MCPServerSSE]) expose an `http_client` -parameter that lets you pass your own pre-configured -[`httpx.AsyncClient`](https://www.python-httpx.org/async/). - -```python {title="mcp_custom_tls_client.py"} -import ssl - -import httpx - -from pydantic_ai import Agent -from pydantic_ai.mcp import MCPServerSSE - -# Trust an internal / self-signed CA -ssl_ctx = ssl.create_default_context(cafile='/etc/ssl/private/my_company_ca.pem') - -# OPTIONAL: if the server requires **mutual TLS** load your client certificate -ssl_ctx.load_cert_chain(certfile='/etc/ssl/certs/client.crt', keyfile='/etc/ssl/private/client.key',) - -http_client = httpx.AsyncClient( - verify=ssl_ctx, - timeout=httpx.Timeout(10.0), -) - -server = MCPServerSSE( - 'http://localhost:3001/sse', - http_client=http_client, # (1)! -) -agent = Agent('openai:gpt-4o', toolsets=[server]) - -async def main(): - async with agent: - result = await agent.run('How many days between 2000-01-01 and 2025-03-18?') - print(result.output) - #> There are 9,208 days between January 1, 2000, and March 18, 2025. -``` - -1. When you supply `http_client`, Pydantic AI re-uses this client for every - request. Anything supported by **httpx** (`verify`, `cert`, custom - proxies, timeouts, etc.) therefore applies to all MCP traffic. - -## MCP Sampling - -!!! info "What is MCP Sampling?" - In MCP [sampling](https://modelcontextprotocol.io/docs/concepts/sampling) is a system by which an MCP server can make LLM calls via the MCP client - effectively proxying requests to an LLM via the client over whatever transport is being used. - - Sampling is extremely useful when MCP servers need to use Gen AI but you don't want to provision them each with their own LLM credentials or when a public MCP server would like the connecting client to pay for LLM calls. - - Confusingly it has nothing to do with the concept of "sampling" in observability, or frankly the concept of "sampling" in any other domain. - - ??? info "Sampling Diagram" - Here's a mermaid diagram that may or may not make the data flow clearer: - - ```mermaid - sequenceDiagram - participant LLM - participant MCP_Client as MCP client - participant MCP_Server as MCP server - - MCP_Client->>LLM: LLM call - LLM->>MCP_Client: LLM tool call response - - MCP_Client->>MCP_Server: tool call - MCP_Server->>MCP_Client: sampling "create message" - - MCP_Client->>LLM: LLM call - LLM->>MCP_Client: LLM text response - - MCP_Client->>MCP_Server: sampling response - MCP_Server->>MCP_Client: tool call response - ``` - -Pydantic AI supports sampling as both a client and server. See the [server](./server.md#mcp-sampling) documentation for details on how to use sampling within a server. - -Sampling is automatically supported by Pydantic AI agents when they act as a client. - -To be able to use sampling, an MCP server instance needs to have a [`sampling_model`][pydantic_ai.mcp.MCPServer.sampling_model] set. This can be done either directly on the server using the constructor keyword argument or the property, or by using [`agent.set_mcp_sampling_model()`][pydantic_ai.Agent.set_mcp_sampling_model] to set the agent's model or one specified as an argument as the sampling model on all MCP servers registered with that agent. - -Let's say we have an MCP server that wants to use sampling (in this case to generate an SVG as per the tool arguments). - -??? example "Sampling MCP Server" - - ```python {title="generate_svg.py"} - import re - from pathlib import Path - - from mcp import SamplingMessage - from mcp.server.fastmcp import Context, FastMCP - from mcp.types import TextContent - - app = FastMCP() - - - @app.tool() - async def image_generator(ctx: Context, subject: str, style: str) -> str: - prompt = f'{subject=} {style=}' - # `ctx.session.create_message` is the sampling call - result = await ctx.session.create_message( - [SamplingMessage(role='user', content=TextContent(type='text', text=prompt))], - max_tokens=1_024, - system_prompt='Generate an SVG image as per the user input', - ) - assert isinstance(result.content, TextContent) - - path = Path(f'{subject}_{style}.svg') - # remove triple backticks if the svg was returned within markdown - if m := re.search(r'^```\w*$(.+?)```$', result.content.text, re.S | re.M): - path.write_text(m.group(1)) - else: - path.write_text(result.content.text) - return f'See {path}' - - - if __name__ == '__main__': - # run the server via stdio - app.run() - ``` - -Using this server with an `Agent` will automatically allow sampling: - -```python {title="sampling_mcp_client.py" requires="generate_svg.py"} -from pydantic_ai import Agent -from pydantic_ai.mcp import MCPServerStdio - -server = MCPServerStdio('python', args=['generate_svg.py']) -agent = Agent('openai:gpt-4o', toolsets=[server]) - - -async def main(): - async with agent: - agent.set_mcp_sampling_model() - result = await agent.run('Create an image of a robot in a punk style.') - print(result.output) - #> Image file written to robot_punk.svg. -``` - -_(This example is complete, it can be run "as is")_ - -You can disallow sampling by setting [`allow_sampling=False`][pydantic_ai.mcp.MCPServer.allow_sampling] when creating the server reference, e.g.: - -```python {title="sampling_disallowed.py" hl_lines="6"} -from pydantic_ai.mcp import MCPServerStdio - -server = MCPServerStdio( - 'python', - args=['generate_svg.py'], - allow_sampling=False, -) -``` - -## Elicitation - -In MCP, [elicitation](https://modelcontextprotocol.io/docs/concepts/elicitation) allows a server to request for [structured input](https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation#supported-schema-types) from the client for missing or additional context during a session. - -Elicitation let models essentially say "Hold on - I need to know X before i can continue" rather than requiring everything upfront or taking a shot in the dark. - -### How Elicitation works - -Elicitation introduces a new protocol message type called [`ElicitRequest`](https://modelcontextprotocol.io/specification/2025-06-18/schema#elicitrequest), which is sent from the server to the client when it needs additional information. The client can then respond with an [`ElicitResult`](https://modelcontextprotocol.io/specification/2025-06-18/schema#elicitresult) or an `ErrorData` message. - -Here's a typical interaction: - -- User makes a request to the MCP server (e.g. "Book a table at that Italian place") -- The server identifies that it needs more information (e.g. "Which Italian place?", "What date and time?") -- The server sends an `ElicitRequest` to the client asking for the missing information. -- The client receives the request, presents it to the user (e.g. via a terminal prompt, GUI dialog, or web interface). -- User provides the requested information, `decline` or `cancel` the request. -- The client sends an `ElicitResult` back to the server with the user's response. -- With the structured data, the server can continue processing the original request. - -This allows for a more interactive and user-friendly experience, especially for multi-staged workflows. Instead of requiring all information upfront, the server can ask for it as needed, making the interaction feel more natural. - -### Setting up Elicitation - -To enable elicitation, provide an [`elicitation_callback`][pydantic_ai.mcp.MCPServer.elicitation_callback] function when creating your MCP server instance: - -```python {title="restaurant_server.py"} -from mcp.server.fastmcp import Context, FastMCP -from pydantic import BaseModel, Field - -mcp = FastMCP(name='Restaurant Booking') - - -class BookingDetails(BaseModel): - """Schema for restaurant booking information.""" - - restaurant: str = Field(description='Choose a restaurant') - party_size: int = Field(description='Number of people', ge=1, le=8) - date: str = Field(description='Reservation date (DD-MM-YYYY)') - - -@mcp.tool() -async def book_table(ctx: Context) -> str: - """Book a restaurant table with user input.""" - # Ask user for booking details using Pydantic schema - result = await ctx.elicit(message='Please provide your booking details:', schema=BookingDetails) - - if result.action == 'accept' and result.data: - booking = result.data - return f'✅ Booked table for {booking.party_size} at {booking.restaurant} on {booking.date}' - elif result.action == 'decline': - return 'No problem! Maybe another time.' - else: # cancel - return 'Booking cancelled.' - - -if __name__ == '__main__': - mcp.run(transport='stdio') -``` - -This server demonstrates elicitation by requesting structured booking details from the client when the `book_table` tool is called. Here's how to create a client that handles these elicitation requests: - -```python {title="client_example.py" requires="restaurant_server.py" test="skip"} -import asyncio -from typing import Any - -from mcp.client.session import ClientSession -from mcp.shared.context import RequestContext -from mcp.types import ElicitRequestParams, ElicitResult - -from pydantic_ai import Agent -from pydantic_ai.mcp import MCPServerStdio - - -async def handle_elicitation( - context: RequestContext[ClientSession, Any, Any], - params: ElicitRequestParams, -) -> ElicitResult: - """Handle elicitation requests from MCP server.""" - print(f'\n{params.message}') - - if not params.requestedSchema: - response = input('Response: ') - return ElicitResult(action='accept', content={'response': response}) - - # Collect data for each field - properties = params.requestedSchema['properties'] - data = {} - - for field, info in properties.items(): - description = info.get('description', field) - - value = input(f'{description}: ') - - # Convert to proper type based on JSON schema - if info.get('type') == 'integer': - data[field] = int(value) - else: - data[field] = value - - # Confirm - confirm = input('\nConfirm booking? (y/n/c): ').lower() - - if confirm == 'y': - print('Booking details:', data) - return ElicitResult(action='accept', content=data) - elif confirm == 'n': - return ElicitResult(action='decline') - else: - return ElicitResult(action='cancel') - - -# Set up MCP server connection -restaurant_server = MCPServerStdio( - 'python', args=['restaurant_server.py'], elicitation_callback=handle_elicitation -) - -# Create agent -agent = Agent('openai:gpt-4o', toolsets=[restaurant_server]) - - -async def main(): - """Run the agent to book a restaurant table.""" - async with agent: - result = await agent.run('Book me a table') - print(f'\nResult: {result.output}') - - -if __name__ == '__main__': - asyncio.run(main()) -``` - -### Supported Schema Types - -MCP elicitation supports string, number, boolean, and enum types with flat object structures only. These limitations ensure reliable cross-client compatibility. See [supported schema types](https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation#supported-schema-types) for details. - -### Security - -MCP Elicitation requires careful handling - servers must not request sensitive information, and clients must implement user approval controls with clear explanations. See [security considerations](https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation#security-considerations) for details. +The [FastMCP-based Client](./fastmcp-client.md) offers a higher-level API with additional capabilities like Tool Transformation, simpler OAuth configuration, and more. \ No newline at end of file diff --git a/docs/mcp/fastmcp-client.md b/docs/mcp/fastmcp-client.md new file mode 100644 index 0000000000..9cc1be597c --- /dev/null +++ b/docs/mcp/fastmcp-client.md @@ -0,0 +1,69 @@ +# FastMCP + +Pydantic AI can also use a [FastMCP Client](https://gofastmcp.com/clients/) to connect to local and remote MCP servers. FastMCP is a higher-level MCP Client framework that makes building and using MCP servers easier for humans, it supports additional capabilities on top of the MCP specification like [Tool Transformation](https://gofastmcp.com/patterns/tool-transformation), [oauth](https://gofastmcp.com/clients/auth/oauth) and more! + +### FastMCP Tools {#fastmcp-tools} + +The [FastMCP](https://fastmcp.dev) Client can also be used with Pydantic AI with the provided [`FastMCPToolset`][pydantic_ai.toolsets.fastmcp.FastMCPToolset] [toolset](toolsets.md). + +To use the `FastMCPToolset`, you will need to install `pydantic-ai-slim[fastmcp]`. + +A FastMCP Toolset can be created from: +- A FastMCP Client: `FastMCPToolset(client=Client(...))` +- A FastMCP Transport: `FastMCPToolset(StdioTransport(command='uv', args=['run', 'mcp-run-python', 'stdio']))` +- A FastMCP Server: `FastMCPToolset(FastMCP('my_server'))` +- An HTTP URL: `FastMCPToolset('http://localhost:8000/mcp')` +- An SSE URL: `FastMCPToolset('http://localhost:8000/sse')` +- A Python Script: `FastMCPToolset('my_server.py')` +- A Node.js Script: `FastMCPToolset('my_server.js')` +- A JSON MCP Configuration: `FastMCPToolset({'mcpServers': {'my_server': {'command': 'python', 'args': ['-c', 'print("test")']}}})` + +Connecting your agent to an HTTP MCP Server is as simple as: + +```python {test="skip"} +from pydantic_ai import Agent +from pydantic_ai.toolsets.fastmcp import FastMCPToolset + +toolset = FastMCPToolset('http://localhost:8000/mcp') + +agent = Agent('openai:gpt-5', toolsets=[toolset]) +``` + +You can also create a toolset from a JSON MCP Configuration. FastMCP supports additional capabilities on top of the MCP specification, like Tool Transformation in the MCP configuration that you can take advantage of with the `FastMCPToolset`. + +```python {test="skip"} +from pydantic_ai import Agent +from pydantic_ai.toolsets.fastmcp import FastMCPToolset + +mcp_config = { + 'mcpServers': { + 'time_mcp_server': { + 'command': 'uvx', + 'args': ['mcp-server-time'] + } + } +} + +toolset = FastMCPToolset(mcp_config) + +agent = Agent('openai:gpt-5', toolsets=[toolset]) +``` + +Toolsets can also be created from a FastMCP Server: + +```python {test="skip"} +from fastmcp import FastMCP + +from pydantic_ai import Agent +from pydantic_ai.toolsets.fastmcp import FastMCPToolset + +fastmcp_server = FastMCP('my_server') +@fastmcp_server.tool() +async def my_tool(a: int, b: int) -> int: + return a + b + +toolset = FastMCPToolset(fastmcp_server) + +agent = Agent('openai:gpt-5', toolsets=[toolset]) +``` + diff --git a/docs/mcp/mcp-client.md b/docs/mcp/mcp-client.md new file mode 100644 index 0000000000..6c47dcf4e3 --- /dev/null +++ b/docs/mcp/mcp-client.md @@ -0,0 +1,616 @@ +# Client + +Pydantic AI can act as an [MCP client](https://modelcontextprotocol.io/quickstart/client), connecting to MCP servers +to use their tools. + +## Install + +You need to either install [`pydantic-ai`](../install.md), or[`pydantic-ai-slim`](../install.md#slim-install) with the `mcp` optional group: + +```bash +pip/uv-add "pydantic-ai-slim[mcp]" +``` + +## Usage + +Pydantic AI comes with three ways to connect to MCP servers: + +- [`MCPServerStreamableHTTP`][pydantic_ai.mcp.MCPServerStreamableHTTP] which connects to an MCP server using the [Streamable HTTP](https://modelcontextprotocol.io/introduction#streamable-http) transport +- [`MCPServerSSE`][pydantic_ai.mcp.MCPServerSSE] which connects to an MCP server using the [HTTP SSE](https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#http-with-sse) transport +- [`MCPServerStdio`][pydantic_ai.mcp.MCPServerStdio] which runs the server as a subprocess and connects to it using the [stdio](https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#stdio) transport + +Examples of all three are shown below. + +Each MCP server instance is a [toolset](../toolsets.md) and can be registered with an [`Agent`][pydantic_ai.Agent] using the `toolsets` argument. + +You can use the [`async with agent`][pydantic_ai.Agent.__aenter__] context manager to open and close connections to all registered servers (and in the case of stdio servers, start and stop the subprocesses) around the context where they'll be used in agent runs. You can also use [`async with server`][pydantic_ai.mcp.MCPServer.__aenter__] to manage the connection or subprocess of a specific server, for example if you'd like to use it with multiple agents. If you don't explicitly enter one of these context managers to set up the server, this will be done automatically when it's needed (e.g. to list the available tools or call a specific tool), but it's more efficient to do so around the entire context where you expect the servers to be used. + +### Streamable HTTP Client + +[`MCPServerStreamableHTTP`][pydantic_ai.mcp.MCPServerStreamableHTTP] connects over HTTP using the +[Streamable HTTP](https://modelcontextprotocol.io/introduction#streamable-http) transport to a server. + +!!! note + [`MCPServerStreamableHTTP`][pydantic_ai.mcp.MCPServerStreamableHTTP] requires an MCP server to be running and accepting HTTP connections before running the agent. Running the server is not managed by Pydantic AI. + +Before creating the Streamable HTTP client, we need to run a server that supports the Streamable HTTP transport. + +```python {title="streamable_http_server.py" dunder_name="not_main"} +from mcp.server.fastmcp import FastMCP + +app = FastMCP() + +@app.tool() +def add(a: int, b: int) -> int: + return a + b + +if __name__ == '__main__': + app.run(transport='streamable-http') +``` + +Then we can create the client: + +```python {title="mcp_streamable_http_client.py"} +from pydantic_ai import Agent +from pydantic_ai.mcp import MCPServerStreamableHTTP + +server = MCPServerStreamableHTTP('http://localhost:8000/mcp') # (1)! +agent = Agent('openai:gpt-4o', toolsets=[server]) # (2)! + +async def main(): + async with agent: # (3)! + result = await agent.run('What is 7 plus 5?') + print(result.output) + #> The answer is 12. +``` + +1. Define the MCP server with the URL used to connect. +2. Create an agent with the MCP server attached. +3. Create a client session to connect to the server. + +_(This example is complete, it can be run "as is" — you'll need to add `asyncio.run(main())` to run `main`)_ + +**What's happening here?** + +- The model receives the prompt "What is 7 plus 5?" +- The model decides "Oh, I've got this `add` tool, that will be a good way to answer this question" +- The model returns a tool call +- Pydantic AI sends the tool call to the MCP server using the Streamable HTTP transport +- The model is called again with the return value of running the `add` tool (12) +- The model returns the final answer + +You can visualise this clearly, and even see the tool call, by adding three lines of code to instrument the example with [logfire](https://logfire.pydantic.dev/docs): + +```python {title="mcp_sse_client_logfire.py" test="skip"} +import logfire + +logfire.configure() +logfire.instrument_pydantic_ai() +``` + +### SSE Client + +[`MCPServerSSE`][pydantic_ai.mcp.MCPServerSSE] connects over HTTP using the [HTTP + Server Sent Events transport](https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#http-with-sse) to a server. + +!!! note + The SSE transport in MCP is deprecated, you should use Streamable HTTP instead. + +Before creating the SSE client, we need to run a server that supports the SSE transport. + + +```python {title="sse_server.py" dunder_name="not_main"} +from mcp.server.fastmcp import FastMCP + +app = FastMCP() + +@app.tool() +def add(a: int, b: int) -> int: + return a + b + +if __name__ == '__main__': + app.run(transport='sse') +``` + +Then we can create the client: + +```python {title="mcp_sse_client.py"} +from pydantic_ai import Agent +from pydantic_ai.mcp import MCPServerSSE + +server = MCPServerSSE('http://localhost:3001/sse') # (1)! +agent = Agent('openai:gpt-4o', toolsets=[server]) # (2)! + + +async def main(): + async with agent: # (3)! + result = await agent.run('What is 7 plus 5?') + print(result.output) + #> The answer is 12. +``` + +1. Define the MCP server with the URL used to connect. +2. Create an agent with the MCP server attached. +3. Create a client session to connect to the server. + +_(This example is complete, it can be run "as is" — you'll need to add `asyncio.run(main())` to run `main`)_ + +### MCP "stdio" Server + +MCP also offers [stdio transport](https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#stdio) where the server is run as a subprocess and communicates with the client over `stdin` and `stdout`. In this case, you'd use the [`MCPServerStdio`][pydantic_ai.mcp.MCPServerStdio] class. + +In this example [mcp-run-python](https://github.com/pydantic/mcp-run-python) is used as the MCP server. + +```python {title="mcp_stdio_client.py"} +from pydantic_ai import Agent +from pydantic_ai.mcp import MCPServerStdio + +server = MCPServerStdio( # (1)! + 'uv', args=['run', 'mcp-run-python', 'stdio'], timeout=10 +) +agent = Agent('openai:gpt-4o', toolsets=[server]) + + +async def main(): + async with agent: + result = await agent.run('How many days between 2000-01-01 and 2025-03-18?') + print(result.output) + #> There are 9,208 days between January 1, 2000, and March 18, 2025. +``` + +1. See [MCP Run Python](https://github.com/pydantic/mcp-run-python) for more information. + +## Loading MCP Servers from Configuration + +Instead of creating MCP server instances individually in code, you can load multiple servers from a JSON configuration file using [`load_mcp_servers()`][pydantic_ai.mcp.load_mcp_servers]. + +This is particularly useful when you need to manage multiple MCP servers or want to configure servers externally without modifying code. + +### Configuration Format + +The configuration file should be a JSON file with an `mcpServers` object containing server definitions. Each server is identified by a unique key and contains the configuration for that server type: + +```json {title="mcp_config.json"} +{ + "mcpServers": { + "python-runner": { + "command": "uv", + "args": ["run", "mcp-run-python", "stdio"] + }, + "weather-api": { + "url": "http://localhost:3001/sse" + }, + "calculator": { + "url": "http://localhost:8000/mcp" + } + } +} +``` + +!!! note + The MCP server is only inferred to be an SSE server because of the `/sse` suffix. + Any other server with the "url" field will be inferred to be a Streamable HTTP server. + + We made this decision given that the SSE transport is deprecated. + +### Usage + +```python {title="mcp_config_loader.py" test="skip"} +from pydantic_ai import Agent +from pydantic_ai.mcp import load_mcp_servers + +# Load all servers from configuration file +servers = load_mcp_servers('mcp_config.json') + +# Create agent with all loaded servers +agent = Agent('openai:gpt-5', toolsets=servers) + +async def main(): + async with agent: + result = await agent.run('What is 7 plus 5?') + print(result.output) +``` + +_(This example is complete, it can be run "as is" — you'll need to add `asyncio.run(main())` to run `main`)_ + +## Tool call customization + +The MCP servers provide the ability to set a `process_tool_call` which allows +the customization of tool call requests and their responses. + +A common use case for this is to inject metadata to the requests which the server +call needs: + +```python {title="mcp_process_tool_call.py"} +from typing import Any + +from pydantic_ai import Agent, RunContext +from pydantic_ai.mcp import CallToolFunc, MCPServerStdio, ToolResult +from pydantic_ai.models.test import TestModel + + +async def process_tool_call( + ctx: RunContext[int], + call_tool: CallToolFunc, + name: str, + tool_args: dict[str, Any], +) -> ToolResult: + """A tool call processor that passes along the deps.""" + return await call_tool(name, tool_args, {'deps': ctx.deps}) + + +server = MCPServerStdio('python', args=['mcp_server.py'], process_tool_call=process_tool_call) +agent = Agent( + model=TestModel(call_tools=['echo_deps']), + deps_type=int, + toolsets=[server] +) + + +async def main(): + async with agent: + result = await agent.run('Echo with deps set to 42', deps=42) + print(result.output) + #> {"echo_deps":{"echo":"This is an echo message","deps":42}} +``` + +How to access the metadata is MCP server SDK specific. For example with the [MCP Python +SDK](https://github.com/modelcontextprotocol/python-sdk), it is accessible via the +[`ctx: Context`](https://github.com/modelcontextprotocol/python-sdk#context) +argument that can be included on tool call handlers: + +```python {title="mcp_server.py"} +from typing import Any + +from mcp.server.fastmcp import Context, FastMCP +from mcp.server.session import ServerSession + +mcp = FastMCP('Pydantic AI MCP Server') +log_level = 'unset' + + +@mcp.tool() +async def echo_deps(ctx: Context[ServerSession, None]) -> dict[str, Any]: + """Echo the run context. + + Args: + ctx: Context object containing request and session information. + + Returns: + Dictionary with an echo message and the deps. + """ + await ctx.info('This is an info message') + + deps: Any = getattr(ctx.request_context.meta, 'deps') + return {'echo': 'This is an echo message', 'deps': deps} + +if __name__ == '__main__': + mcp.run() +``` + +## Using Tool Prefixes to Avoid Naming Conflicts + +When connecting to multiple MCP servers that might provide tools with the same name, you can use the `tool_prefix` parameter to avoid naming conflicts. This parameter adds a prefix to all tool names from a specific server. + +This allows you to use multiple servers that might have overlapping tool names without conflicts: + +```python {title="mcp_tool_prefix_http_client.py"} +from pydantic_ai import Agent +from pydantic_ai.mcp import MCPServerSSE + +# Create two servers with different prefixes +weather_server = MCPServerSSE( + 'http://localhost:3001/sse', + tool_prefix='weather' # Tools will be prefixed with 'weather_' +) + +calculator_server = MCPServerSSE( + 'http://localhost:3002/sse', + tool_prefix='calc' # Tools will be prefixed with 'calc_' +) + +# Both servers might have a tool named 'get_data', but they'll be exposed as: +# - 'weather_get_data' +# - 'calc_get_data' +agent = Agent('openai:gpt-4o', toolsets=[weather_server, calculator_server]) +``` + +## Tool metadata + +MCP tools can include metadata that provides additional information about the tool's characteristics, which can be useful when [filtering tools][pydantic_ai.toolsets.FilteredToolset]. The `meta`, `annotations`, and `output_schema` fields can be found on the `metadata` dict on the [`ToolDefinition`][pydantic_ai.tools.ToolDefinition] object that's passed to filter functions. + +## Custom TLS / SSL configuration + +In some environments you need to tweak how HTTPS connections are established – +for example to trust an internal Certificate Authority, present a client +certificate for **mTLS**, or (during local development only!) disable +certificate verification altogether. +All HTTP-based MCP client classes +([`MCPServerStreamableHTTP`][pydantic_ai.mcp.MCPServerStreamableHTTP] and +[`MCPServerSSE`][pydantic_ai.mcp.MCPServerSSE]) expose an `http_client` +parameter that lets you pass your own pre-configured +[`httpx.AsyncClient`](https://www.python-httpx.org/async/). + +```python {title="mcp_custom_tls_client.py"} +import ssl + +import httpx + +from pydantic_ai import Agent +from pydantic_ai.mcp import MCPServerSSE + +# Trust an internal / self-signed CA +ssl_ctx = ssl.create_default_context(cafile='/etc/ssl/private/my_company_ca.pem') + +# OPTIONAL: if the server requires **mutual TLS** load your client certificate +ssl_ctx.load_cert_chain(certfile='/etc/ssl/certs/client.crt', keyfile='/etc/ssl/private/client.key',) + +http_client = httpx.AsyncClient( + verify=ssl_ctx, + timeout=httpx.Timeout(10.0), +) + +server = MCPServerSSE( + 'http://localhost:3001/sse', + http_client=http_client, # (1)! +) +agent = Agent('openai:gpt-4o', toolsets=[server]) + +async def main(): + async with agent: + result = await agent.run('How many days between 2000-01-01 and 2025-03-18?') + print(result.output) + #> There are 9,208 days between January 1, 2000, and March 18, 2025. +``` + +1. When you supply `http_client`, Pydantic AI re-uses this client for every + request. Anything supported by **httpx** (`verify`, `cert`, custom + proxies, timeouts, etc.) therefore applies to all MCP traffic. + +## MCP Sampling + +!!! info "What is MCP Sampling?" + In MCP [sampling](https://modelcontextprotocol.io/docs/concepts/sampling) is a system by which an MCP server can make LLM calls via the MCP client - effectively proxying requests to an LLM via the client over whatever transport is being used. + + Sampling is extremely useful when MCP servers need to use Gen AI but you don't want to provision them each with their own LLM credentials or when a public MCP server would like the connecting client to pay for LLM calls. + + Confusingly it has nothing to do with the concept of "sampling" in observability, or frankly the concept of "sampling" in any other domain. + + ??? info "Sampling Diagram" + Here's a mermaid diagram that may or may not make the data flow clearer: + + ```mermaid + sequenceDiagram + participant LLM + participant MCP_Client as MCP client + participant MCP_Server as MCP server + + MCP_Client->>LLM: LLM call + LLM->>MCP_Client: LLM tool call response + + MCP_Client->>MCP_Server: tool call + MCP_Server->>MCP_Client: sampling "create message" + + MCP_Client->>LLM: LLM call + LLM->>MCP_Client: LLM text response + + MCP_Client->>MCP_Server: sampling response + MCP_Server->>MCP_Client: tool call response + ``` + +Pydantic AI supports sampling as both a client and server. See the [server](./server.md#mcp-sampling) documentation for details on how to use sampling within a server. + +Sampling is automatically supported by Pydantic AI agents when they act as a client. + +To be able to use sampling, an MCP server instance needs to have a [`sampling_model`][pydantic_ai.mcp.MCPServer.sampling_model] set. This can be done either directly on the server using the constructor keyword argument or the property, or by using [`agent.set_mcp_sampling_model()`][pydantic_ai.Agent.set_mcp_sampling_model] to set the agent's model or one specified as an argument as the sampling model on all MCP servers registered with that agent. + +Let's say we have an MCP server that wants to use sampling (in this case to generate an SVG as per the tool arguments). + +??? example "Sampling MCP Server" + + ```python {title="generate_svg.py"} + import re + from pathlib import Path + + from mcp import SamplingMessage + from mcp.server.fastmcp import Context, FastMCP + from mcp.types import TextContent + + app = FastMCP() + + + @app.tool() + async def image_generator(ctx: Context, subject: str, style: str) -> str: + prompt = f'{subject=} {style=}' + # `ctx.session.create_message` is the sampling call + result = await ctx.session.create_message( + [SamplingMessage(role='user', content=TextContent(type='text', text=prompt))], + max_tokens=1_024, + system_prompt='Generate an SVG image as per the user input', + ) + assert isinstance(result.content, TextContent) + + path = Path(f'{subject}_{style}.svg') + # remove triple backticks if the svg was returned within markdown + if m := re.search(r'^```\w*$(.+?)```$', result.content.text, re.S | re.M): + path.write_text(m.group(1)) + else: + path.write_text(result.content.text) + return f'See {path}' + + + if __name__ == '__main__': + # run the server via stdio + app.run() + ``` + +Using this server with an `Agent` will automatically allow sampling: + +```python {title="sampling_mcp_client.py" requires="generate_svg.py"} +from pydantic_ai import Agent +from pydantic_ai.mcp import MCPServerStdio + +server = MCPServerStdio('python', args=['generate_svg.py']) +agent = Agent('openai:gpt-4o', toolsets=[server]) + + +async def main(): + async with agent: + agent.set_mcp_sampling_model() + result = await agent.run('Create an image of a robot in a punk style.') + print(result.output) + #> Image file written to robot_punk.svg. +``` + +_(This example is complete, it can be run "as is")_ + +You can disallow sampling by setting [`allow_sampling=False`][pydantic_ai.mcp.MCPServer.allow_sampling] when creating the server reference, e.g.: + +```python {title="sampling_disallowed.py" hl_lines="6"} +from pydantic_ai.mcp import MCPServerStdio + +server = MCPServerStdio( + 'python', + args=['generate_svg.py'], + allow_sampling=False, +) +``` + +## Elicitation + +In MCP, [elicitation](https://modelcontextprotocol.io/docs/concepts/elicitation) allows a server to request for [structured input](https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation#supported-schema-types) from the client for missing or additional context during a session. + +Elicitation let models essentially say "Hold on - I need to know X before i can continue" rather than requiring everything upfront or taking a shot in the dark. + +### How Elicitation works + +Elicitation introduces a new protocol message type called [`ElicitRequest`](https://modelcontextprotocol.io/specification/2025-06-18/schema#elicitrequest), which is sent from the server to the client when it needs additional information. The client can then respond with an [`ElicitResult`](https://modelcontextprotocol.io/specification/2025-06-18/schema#elicitresult) or an `ErrorData` message. + +Here's a typical interaction: + +- User makes a request to the MCP server (e.g. "Book a table at that Italian place") +- The server identifies that it needs more information (e.g. "Which Italian place?", "What date and time?") +- The server sends an `ElicitRequest` to the client asking for the missing information. +- The client receives the request, presents it to the user (e.g. via a terminal prompt, GUI dialog, or web interface). +- User provides the requested information, `decline` or `cancel` the request. +- The client sends an `ElicitResult` back to the server with the user's response. +- With the structured data, the server can continue processing the original request. + +This allows for a more interactive and user-friendly experience, especially for multi-staged workflows. Instead of requiring all information upfront, the server can ask for it as needed, making the interaction feel more natural. + +### Setting up Elicitation + +To enable elicitation, provide an [`elicitation_callback`][pydantic_ai.mcp.MCPServer.elicitation_callback] function when creating your MCP server instance: + +```python {title="restaurant_server.py"} +from mcp.server.fastmcp import Context, FastMCP +from pydantic import BaseModel, Field + +mcp = FastMCP(name='Restaurant Booking') + + +class BookingDetails(BaseModel): + """Schema for restaurant booking information.""" + + restaurant: str = Field(description='Choose a restaurant') + party_size: int = Field(description='Number of people', ge=1, le=8) + date: str = Field(description='Reservation date (DD-MM-YYYY)') + + +@mcp.tool() +async def book_table(ctx: Context) -> str: + """Book a restaurant table with user input.""" + # Ask user for booking details using Pydantic schema + result = await ctx.elicit(message='Please provide your booking details:', schema=BookingDetails) + + if result.action == 'accept' and result.data: + booking = result.data + return f'✅ Booked table for {booking.party_size} at {booking.restaurant} on {booking.date}' + elif result.action == 'decline': + return 'No problem! Maybe another time.' + else: # cancel + return 'Booking cancelled.' + + +if __name__ == '__main__': + mcp.run(transport='stdio') +``` + +This server demonstrates elicitation by requesting structured booking details from the client when the `book_table` tool is called. Here's how to create a client that handles these elicitation requests: + +```python {title="client_example.py" requires="restaurant_server.py" test="skip"} +import asyncio +from typing import Any + +from mcp.client.session import ClientSession +from mcp.shared.context import RequestContext +from mcp.types import ElicitRequestParams, ElicitResult + +from pydantic_ai import Agent +from pydantic_ai.mcp import MCPServerStdio + + +async def handle_elicitation( + context: RequestContext[ClientSession, Any, Any], + params: ElicitRequestParams, +) -> ElicitResult: + """Handle elicitation requests from MCP server.""" + print(f'\n{params.message}') + + if not params.requestedSchema: + response = input('Response: ') + return ElicitResult(action='accept', content={'response': response}) + + # Collect data for each field + properties = params.requestedSchema['properties'] + data = {} + + for field, info in properties.items(): + description = info.get('description', field) + + value = input(f'{description}: ') + + # Convert to proper type based on JSON schema + if info.get('type') == 'integer': + data[field] = int(value) + else: + data[field] = value + + # Confirm + confirm = input('\nConfirm booking? (y/n/c): ').lower() + + if confirm == 'y': + print('Booking details:', data) + return ElicitResult(action='accept', content=data) + elif confirm == 'n': + return ElicitResult(action='decline') + else: + return ElicitResult(action='cancel') + + +# Set up MCP server connection +restaurant_server = MCPServerStdio( + 'python', args=['restaurant_server.py'], elicitation_callback=handle_elicitation +) + +# Create agent +agent = Agent('openai:gpt-4o', toolsets=[restaurant_server]) + + +async def main(): + """Run the agent to book a restaurant table.""" + async with agent: + result = await agent.run('Book me a table') + print(f'\nResult: {result.output}') + + +if __name__ == '__main__': + asyncio.run(main()) +``` + +### Supported Schema Types + +MCP elicitation supports string, number, boolean, and enum types with flat object structures only. These limitations ensure reliable cross-client compatibility. See [supported schema types](https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation#supported-schema-types) for details. + +### Security + +MCP Elicitation requires careful handling - servers must not request sensitive information, and clients must implement user approval controls with clear explanations. See [security considerations](https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation#security-considerations) for details. diff --git a/docs/toolsets.md b/docs/toolsets.md index d846ec9f67..5ff50b0d90 100644 --- a/docs/toolsets.md +++ b/docs/toolsets.md @@ -661,73 +661,7 @@ If you want to reuse a network connection or session across tool listings and ca ### MCP Servers -See the [MCP Client](./mcp/client.md) documentation for how to use MCP servers with Pydantic AI. - -### FastMCP Tools {#fastmcp-tools} - -The [FastMCP](https://fastmcp.dev) Client can also be used with Pydantic AI with the provided [`FastMCPToolset`][pydantic_ai.toolsets.fastmcp.FastMCPToolset] [toolset](toolsets.md). - -To use the `FastMCPToolset`, you will need to install `pydantic-ai-slim[fastmcp]`. - -A FastMCP Toolset can be created from: -- A FastMCP Client: `FastMCPToolset(client=Client(...))` -- A FastMCP Transport: `FastMCPToolset(StdioTransport(command='uv', args=['run', 'mcp-run-python', 'stdio']))` -- A FastMCP Server: `FastMCPToolset(FastMCP('my_server'))` -- An HTTP URL: `FastMCPToolset('http://localhost:8000/mcp')` -- An SSE URL: `FastMCPToolset('http://localhost:8000/sse')` -- A Python Script: `FastMCPToolset('my_server.py')` -- A Node.js Script: `FastMCPToolset('my_server.js')` -- A JSON MCP Configuration: `FastMCPToolset({'mcpServers': {'my_server': {'command': 'python', 'args': ['-c', 'print("test")']}}})` - -Connecting your agent to an HTTP MCP Server is as simple as: - -```python {test="skip"} -from pydantic_ai import Agent -from pydantic_ai.toolsets.fastmcp import FastMCPToolset - -toolset = FastMCPToolset('http://localhost:8000/mcp') - -agent = Agent('openai:gpt-5', toolsets=[toolset]) -``` - -You can also create a toolset from a JSON MCP Configuration. FastMCP supports additional capabilities on top of the MCP specification, like Tool Transformation in the MCP configuration that you can take advantage of with the `FastMCPToolset`. - -```python {test="skip"} -from pydantic_ai import Agent -from pydantic_ai.toolsets.fastmcp import FastMCPToolset - -mcp_config = { - 'mcpServers': { - 'time_mcp_server': { - 'command': 'uvx', - 'args': ['mcp-server-time'] - } - } -} - -toolset = FastMCPToolset(mcp_config) - -agent = Agent('openai:gpt-5', toolsets=[toolset]) -``` - -Toolsets can also be created from a FastMCP Server: - -```python {test="skip"} -from fastmcp import FastMCP - -from pydantic_ai import Agent -from pydantic_ai.toolsets.fastmcp import FastMCPToolset - -fastmcp_server = FastMCP('my_server') -@fastmcp_server.tool() -async def my_tool(a: int, b: int) -> int: - return a + b - -toolset = FastMCPToolset(fastmcp_server) - -agent = Agent('openai:gpt-5', toolsets=[toolset]) -``` - +Pydantic AI provides two ways to connect to and call tools on local and remote MCP Servers. There is the [MCP SDK-based Client](./mcp/mcp-client.md) which offers more direct control by leveraging the MCP SDK directly, and the [FastMCP-based Client](./mcp/fastmcp-client.md) which offers a higher-level API with additional capabilities like Tool Transformation, simpler OAuth configuration, and more. ### LangChain Tools {#langchain-tools} diff --git a/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py b/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py index bc8e718844..79a3173607 100644 --- a/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py +++ b/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py @@ -11,6 +11,7 @@ from fastmcp.mcp_config import MCPConfig from fastmcp.server import FastMCP from mcp.server.fastmcp import FastMCP as FastMCP1Server +from mcp.types import BlobResourceContents, EmbeddedResource, ResourceLink from pydantic import AnyUrl from typing_extensions import Self @@ -48,6 +49,8 @@ ToolErrorBehavior = Literal['model_retry', 'error'] +UNKNOWN_BINARY_MEDIA_TYPE = 'application/octet-stream' + @dataclass class FastMCPToolset(AbstractToolset[AgentDepsT]): @@ -113,7 +116,7 @@ def __init__( *, client: Client[Any] | None = None, max_retries: int = 2, - tool_error_behavior: Literal['model_retry', 'error'] = 'error', + tool_error_behavior: Literal['model_retry', 'error'] = 'model_retry', id: str | None = None, ) -> None: if not client and not transport: @@ -227,5 +230,21 @@ def _map_fastmcp_tool_result(part: ContentBlock) -> FastMCPToolResult: if isinstance(part, ImageContent | AudioContent): return messages.BinaryContent(data=base64.b64decode(part.data), media_type=part.mimeType) + if isinstance(part, EmbeddedResource): + if isinstance(part.resource, BlobResourceContents): + return messages.BinaryContent( + data=base64.b64decode(part.resource.blob), + media_type=part.resource.mimeType or UNKNOWN_BINARY_MEDIA_TYPE, + ) + + # If not a BlobResourceContents, it's a TextResourceContents + return part.resource.text + + if isinstance(part, ResourceLink): + # ResourceLink is not yet supported by the FastMCP toolset as reading resources is not yet supported. + raise NotImplementedError( + 'ResourceLink is not supported by the FastMCP toolset as reading resources is not yet supported.' + ) + msg = f'Unsupported/Unknown content block type: {type(part)}' # pragma: no cover raise ValueError(msg) # pragma: no cover) diff --git a/pyproject.toml b/pyproject.toml index 364685e97b..5e445f70d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -90,7 +90,6 @@ dev = [ "coverage[toml]>=7.10.7", "dirty-equals>=0.9.0", "duckduckgo-search>=7.0.0", - "fastmcp>=2.12.0", "inline-snapshot>=0.19.3", "pytest>=8.3.3", "pytest-examples>=0.0.18", @@ -232,6 +231,7 @@ filterwarnings = [ "ignore:unclosed list[TextContent]: TextContent(type='text', text=f'Echo: {message} again'), ] + @server.tool() + async def resource_link_tool(message: str) -> ResourceLink: + """A tool that returns text content without a return annotation.""" + return ResourceLink(type='resource_link', uri=AnyUrl('resource://message.txt'), name='message.txt') + + @server.tool() + async def resource_tool(message: str) -> EmbeddedResource: + """A tool that returns resource content.""" + return EmbeddedResource( + type='resource', resource=TextResourceContents(uri=AnyUrl('resource://message.txt'), text=message) + ) + + @server.tool() + async def resource_tool_blob(message: str) -> EmbeddedResource: + """A tool that returns blob content.""" + base64_message = base64.b64encode(message.encode('utf-8')).decode('utf-8') + return EmbeddedResource( + type='resource', resource=BlobResourceContents(uri=AnyUrl('resource://message.txt'), blob=base64_message) + ) + @server.tool() async def text_tool_wo_return_annotation(message: str): """A tool that returns text content.""" @@ -158,6 +179,18 @@ async def test_id_property(self, fastmcp_client: Client[FastMCPTransport]): toolset = FastMCPToolset(client=fastmcp_client) assert toolset.id is None + async def test_init_without_client_or_transport(self): + """Test initialization without a client or transport.""" + with pytest.raises(ValueError, match='Either client or transport must be provided'): + FastMCPToolset() + + async def test_init_with_client_and_transport(self): + """Test initialization with a client and transport.""" + with pytest.raises(ValueError, match='Either client or transport must be provided, not both'): + tmp_server = FastMCP('tmp_server') + client = Client(transport=tmp_server) + FastMCPToolset(client=client, transport=tmp_server) # pyright: ignore[reportCallIssue] + class TestFastMCPToolsetContextManagement: """Test FastMCP Toolset context management.""" @@ -228,6 +261,9 @@ async def test_get_tools( 'text_list_tool', 'text_tool_wo_return_annotation', 'json_tool', + 'resource_link_tool', + 'resource_tool', + 'resource_tool_blob', } assert set(tools.keys()) == expected_tools @@ -390,18 +426,76 @@ async def test_call_tool_with_json_content( # Should parse the JSON string into a dict assert result == snapshot({'result': '{"received": {"key": "value"}, "processed": true}'}) - async def test_call_tool_with_error_behavior_raise( + async def test_call_tool_with_resource_link( self, fastmcp_toolset: FastMCPToolset[None], run_context: RunContext[None], ): - """Test tool call with error behavior set to raise.""" + """Test tool call that returns resource link content.""" + async with fastmcp_toolset: + tools = await fastmcp_toolset.get_tools(run_context) + resource_link_tool = tools['resource_link_tool'] + + with pytest.raises( + NotImplementedError, + match='ResourceLink is not supported by the FastMCP toolset as reading resources is not yet supported.', + ): + await fastmcp_toolset.call_tool( + name='resource_link_tool', + tool_args={'message': 'Hello World'}, + ctx=run_context, + tool=resource_link_tool, + ) + + async def test_call_tool_with_embedded_resource( + self, + fastmcp_toolset: FastMCPToolset[None], + run_context: RunContext[None], + ): + """Test tool call that returns resource content.""" + async with fastmcp_toolset: + tools = await fastmcp_toolset.get_tools(run_context) + resource_tool = tools['resource_tool'] + + result = await fastmcp_toolset.call_tool( + name='resource_tool', tool_args={'message': 'Hello World'}, ctx=run_context, tool=resource_tool + ) + + assert result == snapshot('Hello World') + + async def test_call_tool_with_resource_tool_blob( + self, + fastmcp_toolset: FastMCPToolset[None], + run_context: RunContext[None], + ): + """Test tool call that returns resource blob content.""" async with fastmcp_toolset: tools = await fastmcp_toolset.get_tools(run_context) + resource_tool_blob = tools['resource_tool_blob'] + + result = await fastmcp_toolset.call_tool( + name='resource_tool_blob', + tool_args={'message': 'Hello World'}, + ctx=run_context, + tool=resource_tool_blob, + ) + + assert result == snapshot(BinaryContent(data=b'Hello World', media_type='application/octet-stream')) + + async def test_call_tool_with_error_behavior_raise( + self, + fastmcp_client: Client[FastMCPTransport], + run_context: RunContext[None], + ): + """Test tool call with error behavior set to raise.""" + toolset = FastMCPToolset(client=fastmcp_client, tool_error_behavior='error') + + async with toolset: + tools = await toolset.get_tools(run_context) error_tool = tools['error_tool'] with pytest.raises(ToolError, match='This is a test error'): - await fastmcp_toolset.call_tool(name='error_tool', tool_args={}, ctx=run_context, tool=error_tool) + await toolset.call_tool('error_tool', {}, run_context, error_tool) async def test_call_tool_with_error_behavior_model_retry( self, @@ -477,57 +571,18 @@ async def test_transports(self): ) assert isinstance(toolset.client.transport, MCPConfigTransport) - # async def test_sse_transport(self, run_context: RunContext[None]): - # """Test creating toolset from stdio transport.""" - # server_script = """ - # from fastmcp import FastMCP - - # server = FastMCP('test_server') - - # @server.tool() - # async def test_tool(param1: str, param2: int = 0) -> str: - # return f'param1={param1}, param2={param2}' - - # server.run(transport='sse')""" - # with TemporaryDirectory() as temp_dir: - # server_py = Path(temp_dir) / 'server.py' - # server_py.write_text(server_script) - - # with subprocess.Popen(['python', server_py], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True): - # toolset = FastMCPToolset(mcp='http://localhost:8000/sse') - # async with toolset: - # tools = await toolset.get_tools( - # RunContext(deps=None, model=TestModel(), usage=RunUsage(), prompt=None, messages=[], run_step=0) - # ) - # assert 'test_tool' in tools - - # async def test_streamable_http_transport(self, run_context: RunContext[None]): - # """Test creating toolset from stdio transport.""" - - # toolset = FastMCPToolset(mcp='http://localhost:8001/mcp') - - # assert isinstance(get_client_from_toolset(toolset).transport, StreamableHttpTransport) - # server_script = """ - # from fastmcp import FastMCP - # import asyncio - - # server = FastMCP('test_server') - - # @server.tool() - # async def test_tool(param1: str, param2: int = 0) -> str: - # return f'param1={param1}, param2={param2}' - - # asyncio.run(server.run_streamable_http_async(host='localhost', port=8001)) - # """ - # with TemporaryDirectory() as temp_dir: - # server_py = Path(temp_dir) / 'server.py' - # server_py.write_text(server_script) - - # with subprocess.Popen(['python', server_py], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True): - # toolset = FastMCPToolset(mcp='http://localhost:8001/mcp') - # async with toolset: - # tools = await toolset.get_tools(run_context) - # assert 'test_tool' in tools + @pytest.mark.parametrize( + 'invalid_transport', ['tomato_is_not_a_valid_transport', '/path/to/server.ini', 'ftp://localhost'] + ) + async def test_invalid_transports_uninferrable(self, invalid_transport: str | None): + """Test creating toolset from invalid transports.""" + with pytest.raises(ValueError, match='Could not infer a valid transport from:'): + FastMCPToolset(invalid_transport) + + async def test_bad_transports(self): + """Test creating toolset from invalid transports.""" + with pytest.raises(ValueError, match='No MCP servers defined in the config'): + FastMCPToolset({'bad_transport': 'bad_value'}) async def test_in_memory_transport(self, run_context: RunContext[None]): """Test creating toolset from stdio transport.""" diff --git a/uv.lock b/uv.lock index f1a06d13fd..c5019823c0 100644 --- a/uv.lock +++ b/uv.lock @@ -1264,6 +1264,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/75/21/fc2c821a2c92c021f8f8adf9fb36235d1b49525b7cd953e85624296aab94/duckduckgo_search-7.5.0-py3-none-any.whl", hash = "sha256:6a2d3f12ae29b3e076cd43be61f5f73cd95261e0a0f318fe0ad3648d7a5dff03", size = 20238, upload-time = "2025-02-24T14:50:48.179Z" }, ] +[[package]] +name = "email-validator" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, +] + [[package]] name = "eval-type-backport" version = "0.2.2" @@ -3569,6 +3582,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" }, ] +[package.optional-dependencies] +email = [ + { name = "email-validator" }, +] + [[package]] name = "pydantic-ai" source = { editable = "." } @@ -3597,7 +3615,6 @@ dev = [ { name = "diff-cover" }, { name = "dirty-equals" }, { name = "duckduckgo-search" }, - { name = "fastmcp" }, { name = "genai-prices" }, { name = "inline-snapshot" }, { name = "mcp-run-python" }, @@ -3650,7 +3667,6 @@ dev = [ { name = "diff-cover", specifier = ">=9.2.0" }, { name = "dirty-equals", specifier = ">=0.9.0" }, { name = "duckduckgo-search", specifier = ">=7.0.0" }, - { name = "fastmcp", specifier = ">=2.12.0" }, { name = "genai-prices", specifier = ">=0.0.28" }, { name = "inline-snapshot", specifier = ">=0.19.3" }, { name = "mcp-run-python", specifier = ">=0.0.20" }, From cc559d759ed922f3b625bfaff68cade6e9b2059a Mon Sep 17 00:00:00 2001 From: William Easton Date: Mon, 20 Oct 2025 10:12:59 -0500 Subject: [PATCH 17/20] Fix test when package is not installed --- docs/mcp/client.md | 2 +- tests/test_fastmcp.py | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/mcp/client.md b/docs/mcp/client.md index 94a5423756..974a86adbf 100644 --- a/docs/mcp/client.md +++ b/docs/mcp/client.md @@ -6,4 +6,4 @@ The [MCP SDK-based Client](./mcp-client.md) offers more direct control by levera ## FastMCP-based Client -The [FastMCP-based Client](./fastmcp-client.md) offers a higher-level API with additional capabilities like Tool Transformation, simpler OAuth configuration, and more. \ No newline at end of file +The [FastMCP-based Client](./fastmcp-client.md) offers a higher-level API with additional capabilities like Tool Transformation, simpler OAuth configuration, and more. The FastMCP Toolkit does not yet support integration with Pydantic AI for elicitation or sampling. diff --git a/tests/test_fastmcp.py b/tests/test_fastmcp.py index 22926adcc3..5cdfd88c7d 100644 --- a/tests/test_fastmcp.py +++ b/tests/test_fastmcp.py @@ -8,10 +8,7 @@ from typing import Any import pytest -from fastmcp.client import PythonStdioTransport, SSETransport -from fastmcp.exceptions import ToolError from inline_snapshot import snapshot -from mcp.types import AnyUrl, BlobResourceContents, EmbeddedResource, ResourceLink, TextResourceContents from pydantic_ai._run_context import RunContext from pydantic_ai.exceptions import ModelRetry @@ -22,7 +19,7 @@ from .conftest import try_import with try_import() as imports_successful: - from fastmcp.client import Client + from fastmcp.client import Client, PythonStdioTransport, SSETransport from fastmcp.client.transports import ( FastMCPTransport, MCPConfigTransport, @@ -30,11 +27,17 @@ StdioTransport, StreamableHttpTransport, ) + from fastmcp.exceptions import ToolError from fastmcp.server.server import FastMCP from mcp.types import ( + AnyUrl, AudioContent, + BlobResourceContents, + EmbeddedResource, ImageContent, + ResourceLink, TextContent, + TextResourceContents, ) # Import the content mapping functions for testing From 623524b69d75d70a9094d2f80fcd92b24ee59d7b Mon Sep 17 00:00:00 2001 From: William Easton Date: Mon, 20 Oct 2025 10:15:21 -0500 Subject: [PATCH 18/20] lint --- docs/mcp/fastmcp-client.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/mcp/fastmcp-client.md b/docs/mcp/fastmcp-client.md index 9cc1be597c..b468c79718 100644 --- a/docs/mcp/fastmcp-client.md +++ b/docs/mcp/fastmcp-client.md @@ -66,4 +66,3 @@ toolset = FastMCPToolset(fastmcp_server) agent = Agent('openai:gpt-5', toolsets=[toolset]) ``` - From 3766ba8e1a602a06fb1d837eb46cbcc07dc41c6d Mon Sep 17 00:00:00 2001 From: William Easton Date: Mon, 20 Oct 2025 10:50:29 -0500 Subject: [PATCH 19/20] fix coverage test --- tests/test_fastmcp.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/test_fastmcp.py b/tests/test_fastmcp.py index 5cdfd88c7d..d3f866315a 100644 --- a/tests/test_fastmcp.py +++ b/tests/test_fastmcp.py @@ -592,13 +592,19 @@ async def test_in_memory_transport(self, run_context: RunContext[None]): fastmcp_server = FastMCP('test_server') @fastmcp_server.tool() - def test_tool(param1: str, param2: int = 0) -> str: ... + def test_tool(param1: str, param2: int = 0) -> str: + return f'param1={param1}, param2={param2}' toolset = FastMCPToolset(fastmcp_server) async with toolset: tools = await toolset.get_tools(run_context) assert 'test_tool' in tools + result = await toolset.call_tool( + name='test_tool', tool_args={'param1': 'hello', 'param2': 42}, ctx=run_context, tool=tools['test_tool'] + ) + assert result == {'result': 'param1=hello, param2=42'} + async def test_from_mcp_config_dict(self): """Test creating toolset from MCP config dictionary.""" From 1b3e0fe2f4b0d31682c13fc41d3db7b8b0d952d8 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 24 Oct 2025 21:11:36 +0000 Subject: [PATCH 20/20] simplification --- docs/mcp/fastmcp-client.md | 4 +- .../pydantic_ai/toolsets/fastmcp.py | 99 ++++++------------- tests/test_fastmcp.py | 40 +++----- 3 files changed, 48 insertions(+), 95 deletions(-) diff --git a/docs/mcp/fastmcp-client.md b/docs/mcp/fastmcp-client.md index def82d7b9e..538b2afbbb 100644 --- a/docs/mcp/fastmcp-client.md +++ b/docs/mcp/fastmcp-client.md @@ -19,7 +19,7 @@ pip/uv-add "pydantic-ai-slim[fastmcp]" A `FastMCPToolset` can then be created from: - A FastMCP Server: `#!python FastMCPToolset(fastmcp.FastMCP('my_server'))` -- A FastMCP Client: `#!python FastMCPToolset(client=fastmcp.Client(...))` +- A FastMCP Client: `#!python FastMCPToolset(fastmcp.Client(...))` - A FastMCP Transport: `#!python FastMCPToolset(fastmcp.StdioTransport(command='uvx', args=['mcp-run-python', 'stdio']))` - A Streamable HTTP URL: `#!python FastMCPToolset('http://localhost:8000/mcp')` - An HTTP SSE URL: `#!python FastMCPToolset('http://localhost:8000/sse')` @@ -85,4 +85,4 @@ toolset = FastMCPToolset(mcp_config) agent = Agent('openai:gpt-5', toolsets=[toolset]) ``` -_(This example is complete, it can be run "as is" — you'll need to add `asyncio.run(main())` to run `main`)_ \ No newline at end of file +_(This example is complete, it can be run "as is" — you'll need to add `asyncio.run(main())` to run `main`)_ diff --git a/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py b/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py index 79a3173607..f751459541 100644 --- a/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py +++ b/pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py @@ -3,17 +3,12 @@ import base64 from asyncio import Lock from contextlib import AsyncExitStack -from dataclasses import KW_ONLY, dataclass, field +from dataclasses import KW_ONLY, dataclass from pathlib import Path -from typing import TYPE_CHECKING, Any, Literal, overload +from typing import TYPE_CHECKING, Any, Literal -from fastmcp.client.transports import ClientTransport -from fastmcp.mcp_config import MCPConfig -from fastmcp.server import FastMCP -from mcp.server.fastmcp import FastMCP as FastMCP1Server -from mcp.types import BlobResourceContents, EmbeddedResource, ResourceLink from pydantic import AnyUrl -from typing_extensions import Self +from typing_extensions import Self, assert_never from pydantic_ai import messages from pydantic_ai.exceptions import ModelRetry @@ -23,12 +18,20 @@ try: from fastmcp.client import Client + from fastmcp.client.transports import ClientTransport from fastmcp.exceptions import ToolError + from fastmcp.mcp_config import MCPConfig + from fastmcp.server import FastMCP + from mcp.server.fastmcp import FastMCP as FastMCP1Server from mcp.types import ( AudioContent, + BlobResourceContents, ContentBlock, + EmbeddedResource, ImageContent, + ResourceLink, TextContent, + TextResourceContents, Tool as MCPTool, ) @@ -52,7 +55,7 @@ UNKNOWN_BINARY_MEDIA_TYPE = 'application/octet-stream' -@dataclass +@dataclass(init=False) class FastMCPToolset(AbstractToolset[AgentDepsT]): """A FastMCP Toolset that uses the FastMCP Client to call tools from a local or remote MCP Server. @@ -62,73 +65,38 @@ class FastMCPToolset(AbstractToolset[AgentDepsT]): """ client: Client[Any] - """The FastMCP transport to use. This can be a local or remote MCP Server configuration, a transport string, or a FastMCP Client.""" + """The FastMCP client to use.""" _: KW_ONLY - tool_error_behavior: Literal['model_retry', 'error'] = field(default='error') + tool_error_behavior: Literal['model_retry', 'error'] """The behavior to take when a tool error occurs.""" - max_retries: int = field(default=2) + max_retries: int """The maximum number of retries to attempt if a tool call fails.""" - _id: str | None = field(default=None) + _id: str | None - @overload def __init__( self, - *, - client: Client[Any], - max_retries: int = 2, - tool_error_behavior: Literal['model_retry', 'error'] = 'error', - id: str | None = None, - ) -> None: ... - - @overload - def __init__( - self, - transport: ClientTransport - | FastMCP - | FastMCP1Server - | AnyUrl - | Path - | MCPConfig - | dict[str, Any] - | str - | None = None, - *, - max_retries: int = 2, - tool_error_behavior: Literal['model_retry', 'error'] = 'error', - id: str | None = None, - ) -> None: ... - - def __init__( - self, - transport: ClientTransport + client: Client[Any] + | ClientTransport | FastMCP | FastMCP1Server | AnyUrl | Path | MCPConfig | dict[str, Any] - | str - | None = None, + | str, *, - client: Client[Any] | None = None, - max_retries: int = 2, + max_retries: int = 1, tool_error_behavior: Literal['model_retry', 'error'] = 'model_retry', id: str | None = None, ) -> None: - if not client and not transport: - raise ValueError('Either client or transport must be provided') - - if client and transport: - raise ValueError('Either client or transport must be provided, not both') - - if client: + if isinstance(client, Client): self.client = client else: - self.client = Client[Any](transport=transport) + self.client = Client[Any](transport=client) self._id = id self.max_retries = max_retries @@ -144,7 +112,7 @@ def id(self) -> str | None: async def __aenter__(self) -> Self: async with self._enter_lock: - if self._running_count == 0 and self.client: + if self._running_count == 0: self._exit_stack = AsyncExitStack() await self._exit_stack.enter_async_context(self.client) @@ -226,25 +194,22 @@ def _map_fastmcp_tool_results(parts: list[ContentBlock]) -> list[FastMCPToolResu def _map_fastmcp_tool_result(part: ContentBlock) -> FastMCPToolResult: if isinstance(part, TextContent): return part.text - - if isinstance(part, ImageContent | AudioContent): + elif isinstance(part, ImageContent | AudioContent): return messages.BinaryContent(data=base64.b64decode(part.data), media_type=part.mimeType) - - if isinstance(part, EmbeddedResource): + elif isinstance(part, EmbeddedResource): if isinstance(part.resource, BlobResourceContents): return messages.BinaryContent( data=base64.b64decode(part.resource.blob), media_type=part.resource.mimeType or UNKNOWN_BINARY_MEDIA_TYPE, ) - - # If not a BlobResourceContents, it's a TextResourceContents - return part.resource.text - - if isinstance(part, ResourceLink): + elif isinstance(part.resource, TextResourceContents): + return part.resource.text + else: + assert_never(part.resource) + elif isinstance(part, ResourceLink): # ResourceLink is not yet supported by the FastMCP toolset as reading resources is not yet supported. raise NotImplementedError( 'ResourceLink is not supported by the FastMCP toolset as reading resources is not yet supported.' ) - - msg = f'Unsupported/Unknown content block type: {type(part)}' # pragma: no cover - raise ValueError(msg) # pragma: no cover) + else: + assert_never(part) diff --git a/tests/test_fastmcp.py b/tests/test_fastmcp.py index d3f866315a..1d2815cd6b 100644 --- a/tests/test_fastmcp.py +++ b/tests/test_fastmcp.py @@ -158,42 +158,30 @@ class TestFastMCPToolsetInitialization: async def test_init_with_client(self, fastmcp_client: Client[FastMCPTransport]): """Test initialization with a FastMCP client.""" - toolset = FastMCPToolset(client=fastmcp_client) + toolset = FastMCPToolset(fastmcp_client) # Test that the client is accessible via the property assert toolset.id is None async def test_init_with_id(self, fastmcp_client: Client[FastMCPTransport]): """Test initialization with an id.""" - toolset = FastMCPToolset(client=fastmcp_client, id='test_id') + toolset = FastMCPToolset(fastmcp_client, id='test_id') # Test that the client is accessible via the property assert toolset.id == 'test_id' async def test_init_with_custom_retries_and_error_behavior(self, fastmcp_client: Client[FastMCPTransport]): """Test initialization with custom retries and error behavior.""" - toolset = FastMCPToolset(client=fastmcp_client, max_retries=5, tool_error_behavior='model_retry') + toolset = FastMCPToolset(fastmcp_client, max_retries=5, tool_error_behavior='model_retry') # Test that the toolset was created successfully assert toolset.client is fastmcp_client async def test_id_property(self, fastmcp_client: Client[FastMCPTransport]): """Test that the id property returns None.""" - toolset = FastMCPToolset(client=fastmcp_client) + toolset = FastMCPToolset(fastmcp_client) assert toolset.id is None - async def test_init_without_client_or_transport(self): - """Test initialization without a client or transport.""" - with pytest.raises(ValueError, match='Either client or transport must be provided'): - FastMCPToolset() - - async def test_init_with_client_and_transport(self): - """Test initialization with a client and transport.""" - with pytest.raises(ValueError, match='Either client or transport must be provided, not both'): - tmp_server = FastMCP('tmp_server') - client = Client(transport=tmp_server) - FastMCPToolset(client=client, transport=tmp_server) # pyright: ignore[reportCallIssue] - class TestFastMCPToolsetContextManagement: """Test FastMCP Toolset context management.""" @@ -202,7 +190,7 @@ async def test_context_manager_single_enter_exit( self, fastmcp_client: Client[FastMCPTransport], run_context: RunContext[None] ): """Test single enter/exit cycle.""" - toolset = FastMCPToolset(client=fastmcp_client) + toolset = FastMCPToolset(fastmcp_client) async with toolset: # Test that we can get tools when the context is active @@ -216,7 +204,7 @@ async def test_context_manager_no_enter( self, fastmcp_client: Client[FastMCPTransport], run_context: RunContext[None] ): """Test no enter/exit cycle.""" - toolset = FastMCPToolset(client=fastmcp_client) + toolset = FastMCPToolset(fastmcp_client) # Test that we can get tools when the context is not active tools = await toolset.get_tools(run_context) @@ -227,7 +215,7 @@ async def test_context_manager_nested_enter_exit( self, fastmcp_client: Client[FastMCPTransport], run_context: RunContext[None] ): """Test nested enter/exit cycles.""" - toolset = FastMCPToolset(client=fastmcp_client) + toolset = FastMCPToolset(fastmcp_client) async with toolset: tools1 = await toolset.get_tools(run_context) @@ -248,7 +236,7 @@ async def test_get_tools( run_context: RunContext[None], ): """Test getting tools from the FastMCP client.""" - toolset = FastMCPToolset(client=fastmcp_client) + toolset = FastMCPToolset(fastmcp_client) async with toolset: tools = await toolset.get_tools(run_context) @@ -275,7 +263,7 @@ async def test_get_tools( assert test_tool.tool_def.name == 'test_tool' assert test_tool.tool_def.description is not None assert 'test tool that returns a formatted string' in test_tool.tool_def.description - assert test_tool.max_retries == 2 + assert test_tool.max_retries == 1 assert test_tool.toolset is toolset # Check that the tool has proper schema @@ -288,7 +276,7 @@ async def test_get_tools_with_empty_server(self, run_context: RunContext[None]): """Test getting tools from an empty FastMCP server.""" empty_server = FastMCP('empty_server') empty_client = Client(transport=empty_server) - toolset = FastMCPToolset(client=empty_client) + toolset = FastMCPToolset(empty_client) async with toolset: tools = await toolset.get_tools(run_context) @@ -301,7 +289,7 @@ class TestFastMCPToolsetToolCalling: @pytest.fixture async def fastmcp_toolset(self, fastmcp_client: Client[FastMCPTransport]) -> FastMCPToolset[None]: """Create a FastMCP Toolset.""" - return FastMCPToolset(client=fastmcp_client) + return FastMCPToolset(fastmcp_client) async def test_call_tool_success( self, @@ -491,7 +479,7 @@ async def test_call_tool_with_error_behavior_raise( run_context: RunContext[None], ): """Test tool call with error behavior set to raise.""" - toolset = FastMCPToolset(client=fastmcp_client, tool_error_behavior='error') + toolset = FastMCPToolset(fastmcp_client, tool_error_behavior='error') async with toolset: tools = await toolset.get_tools(run_context) @@ -506,7 +494,7 @@ async def test_call_tool_with_error_behavior_model_retry( run_context: RunContext[None], ): """Test tool call with error behavior set to model retry.""" - toolset = FastMCPToolset(client=fastmcp_client, tool_error_behavior='model_retry') + toolset = FastMCPToolset(fastmcp_client, tool_error_behavior='model_retry') async with toolset: tools = await toolset.get_tools(run_context) @@ -577,7 +565,7 @@ async def test_transports(self): @pytest.mark.parametrize( 'invalid_transport', ['tomato_is_not_a_valid_transport', '/path/to/server.ini', 'ftp://localhost'] ) - async def test_invalid_transports_uninferrable(self, invalid_transport: str | None): + async def test_invalid_transports_uninferrable(self, invalid_transport: str): """Test creating toolset from invalid transports.""" with pytest.raises(ValueError, match='Could not infer a valid transport from:'): FastMCPToolset(invalid_transport)