diff --git a/.github/workflows/python-merge-tests.yml b/.github/workflows/python-merge-tests.yml index e4cae65fa3..2592dc0a1e 100644 --- a/.github/workflows/python-merge-tests.yml +++ b/.github/workflows/python-merge-tests.yml @@ -58,6 +58,7 @@ jobs: UV_PYTHON: ${{ matrix.python-version }} OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }} OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }} + LOCAL_MCP_URL: ${{ vars.LOCAL_MCP__URL }} PACKAGE_NAME: "main" defaults: run: diff --git a/python/.cspell.json b/python/.cspell.json new file mode 100644 index 0000000000..ab7106ad7b --- /dev/null +++ b/python/.cspell.json @@ -0,0 +1,77 @@ +{ + "version": "0.2", + "languageSettings": [ + { + "languageId": "py", + "allowCompoundWords": true, + "locale": "en-US" + } + ], + "language": "en-US", + "patterns": [ + { + "name": "import", + "pattern": "import [a-zA-Z0-9_]+" + }, + { + "name": "from import", + "pattern": "from [a-zA-Z0-9_]+ import [a-zA-Z0-9_]+" + } + ], + "ignorePaths": [ + "samples/**", + "notebooks/**" + ], + "words": [ + "aeiou", + "aiplatform", + "azuredocindex", + "azuredocs", + "boto", + "contentvector", + "contoso", + "datamodel", + "desync", + "dotenv", + "endregion", + "entra", + "faiss", + "genai", + "generativeai", + "hnsw", + "httpx", + "huggingface", + "Instrumentor", + "logit", + "logprobs", + "lowlevel", + "Magentic", + "mistralai", + "mongocluster", + "nd", + "ndarray", + "nopep", + "NOSQL", + "ollama", + "Onnx", + "onyourdatatest", + "OPENAI", + "opentelemetry", + "OTEL", + "protos", + "pydantic", + "pytestmark", + "qdrant", + "retrywrites", + "streamable", + "serde", + "templating", + "uninstrument", + "vectordb", + "vectorizable", + "vectorizer", + "vectorstoremodel", + "vertexai", + "Weaviate" + ] +} diff --git a/python/.vscode/launch.json b/python/.vscode/launch.json index 6b76b4fabc..b9788cad64 100644 --- a/python/.vscode/launch.json +++ b/python/.vscode/launch.json @@ -9,7 +9,8 @@ "type": "debugpy", "request": "launch", "program": "${file}", - "console": "integratedTerminal" + "console": "integratedTerminal", + "justMyCode": false } ] -} \ No newline at end of file +} diff --git a/python/packages/azure/tests/test_azure_responses_client.py b/python/packages/azure/tests/test_azure_responses_client.py index 7380a18cf7..acd37b4a61 100644 --- a/python/packages/azure/tests/test_azure_responses_client.py +++ b/python/packages/azure/tests/test_azure_responses_client.py @@ -4,7 +4,14 @@ import os from typing import Annotated import pytest -from agent_framework import ChatClient, ChatMessage, ChatResponse, ChatResponseUpdate, TextContent, ai_function +from agent_framework import ( + ChatClient, + ChatMessage, + ChatResponse, + ChatResponseUpdate, + TextContent, + ai_function, +) from agent_framework.azure import AzureResponsesClient from agent_framework.exceptions import ServiceInitializationError from azure.identity import DefaultAzureCredential @@ -132,17 +139,17 @@ async def test_azure_responses_client_response() -> None: messages.append(ChatMessage(role="user", text="The weather in New York is sunny")) messages.append(ChatMessage(role="user", text="What is the weather in New York?")) - # Test that the client can be used to get a response - response = await azure_responses_client.get_response( + # Test that the client can be used to get a structured response + structured_response = await azure_responses_client.get_response( # type: ignore[reportAssignmentType] messages=messages, response_format=OutputStruct, ) - assert response is not None - assert isinstance(response, ChatResponse) - output = OutputStruct.model_validate_json(response.text) - assert output.location == "New York" - assert "sunny" in output.weather.lower() + assert structured_response is not None + assert isinstance(structured_response, ChatResponse) + assert isinstance(structured_response.value, OutputStruct) + assert structured_response.value.location == "New York" + assert "sunny" in structured_response.value.weather.lower() @skip_if_azure_integration_tests_disabled @@ -170,18 +177,18 @@ async def test_azure_responses_client_response_tools() -> None: messages.append(ChatMessage(role="user", text="What is the weather in Seattle?")) # Test that the client can be used to get a response - response = await azure_responses_client.get_response( + structured_response: ChatResponse = await azure_responses_client.get_response( # type: ignore[reportAssignmentType] messages=messages, tools=[get_weather], tool_choice="auto", response_format=OutputStruct, ) - assert response is not None - assert isinstance(response, ChatResponse) - output = OutputStruct.model_validate_json(response.text) - assert "Seattle" in output.location - assert "sunny" in output.weather.lower() + assert structured_response is not None + assert isinstance(structured_response, ChatResponse) + assert isinstance(structured_response.value, OutputStruct) + assert "Seattle" in structured_response.value.location + assert "sunny" in structured_response.value.weather.lower() @skip_if_azure_integration_tests_disabled @@ -220,21 +227,18 @@ async def test_azure_responses_client_streaming() -> None: messages.append(ChatMessage(role="user", text="The weather in Seattle is sunny")) messages.append(ChatMessage(role="user", text="What is the weather in Seattle?")) - response = azure_responses_client.get_streaming_response( - messages=messages, - response_format=OutputStruct, + structured_response = await ChatResponse.from_chat_response_generator( + azure_responses_client.get_streaming_response( + messages=messages, + response_format=OutputStruct, + ), + output_format_type=OutputStruct, ) - full_message = "" - async for chunk in response: - assert chunk is not None - assert isinstance(chunk, ChatResponseUpdate) - for content in chunk.contents: - if isinstance(content, TextContent) and content.text: - full_message += content.text - - output = OutputStruct.model_validate_json(full_message) - assert "Seattle" in output.location - assert "sunny" in output.weather.lower() + assert structured_response is not None + assert isinstance(structured_response, ChatResponse) + assert isinstance(structured_response.value, OutputStruct) + assert "Seattle" in structured_response.value.location + assert "sunny" in structured_response.value.weather.lower() @skip_if_azure_integration_tests_disabled @@ -265,14 +269,14 @@ async def test_azure_responses_client_streaming_tools() -> None: messages.clear() messages.append(ChatMessage(role="user", text="What is the weather in Seattle?")) - response = azure_responses_client.get_streaming_response( + structured_response = azure_responses_client.get_streaming_response( messages=messages, tools=[get_weather], tool_choice="auto", response_format=OutputStruct, ) full_message = "" - async for chunk in response: + async for chunk in structured_response: assert chunk is not None assert isinstance(chunk, ChatResponseUpdate) for content in chunk.contents: diff --git a/python/packages/main/agent_framework/__init__.py b/python/packages/main/agent_framework/__init__.py index 2e2734f47c..7a9a3d027f 100644 --- a/python/packages/main/agent_framework/__init__.py +++ b/python/packages/main/agent_framework/__init__.py @@ -11,5 +11,6 @@ except importlib.metadata.PackageNotFoundError: from ._agents import * # noqa: F403 from ._clients import * # noqa: F403 from ._logging import * # noqa: F403 +from ._mcp import * # noqa: F403 from ._tools import * # noqa: F403 from ._types import * # noqa: F403 diff --git a/python/packages/main/agent_framework/_agents.py b/python/packages/main/agent_framework/_agents.py index bb0add314b..948b6e5d59 100644 --- a/python/packages/main/agent_framework/_agents.py +++ b/python/packages/main/agent_framework/_agents.py @@ -2,14 +2,16 @@ import sys from collections.abc import AsyncIterable, Callable, MutableMapping, Sequence -from contextlib import AbstractAsyncContextManager +from contextlib import AbstractAsyncContextManager, AsyncExitStack from enum import Enum +from itertools import chain from typing import Any, ClassVar, Literal, Protocol, TypeVar, runtime_checkable from uuid import uuid4 -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, PrivateAttr from ._clients import ChatClient +from ._mcp import McpTool from ._pydantic import AFBaseModel from ._tools import AITool from ._types import ( @@ -315,6 +317,8 @@ class ChatClientAgent(AgentBase): chat_client: ChatClient instructions: str | None = None chat_options: ChatOptions + _local_mcp_tools: list[McpTool] = PrivateAttr(default_factory=list) # type: ignore[reportUnknownVariableType] + _async_exit_stack: AsyncExitStack = PrivateAttr(default_factory=AsyncExitStack) def __init__( self, @@ -383,6 +387,11 @@ class ChatClientAgent(AgentBase): """ kwargs.update(additional_properties or {}) + # We ignore the MCP Servers here and store them separately, + # we add their functions to the tools list at runtime + normalized_tools = [] if tools is None else tools if isinstance(tools, list) else [tools] + local_mcp_tools = [tool for tool in normalized_tools if isinstance(tool, McpTool)] + final_tools = [tool for tool in normalized_tools if not isinstance(tool, McpTool)] args: dict[str, Any] = { "chat_client": chat_client, "chat_options": ChatOptions( @@ -398,7 +407,7 @@ class ChatClientAgent(AgentBase): store=store, temperature=temperature, tool_choice=tool_choice, - tools=tools, # type: ignore + tools=final_tools, # type: ignore[reportArgumentType] top_p=top_p, user=user, additional_properties=kwargs, @@ -415,23 +424,27 @@ class ChatClientAgent(AgentBase): super().__init__(**args) self._update_agent_name() + self._local_mcp_tools = local_mcp_tools # type: ignore[assignment] async def __aenter__(self) -> "Self": """Async context manager entry. - If the chat_client supports async context management, enter its context. + If either the chat_client or the local_mcp_tools are context managers, + they will be entered into the async exit stack to ensure proper cleanup. + + This list might be extended in the future. """ - if isinstance(self.chat_client, AbstractAsyncContextManager): - await self.chat_client.__aenter__() # type: ignore[reportUnknownMemberType] + for context_manager in chain([self.chat_client], self._local_mcp_tools): + if isinstance(context_manager, AbstractAsyncContextManager): + await self._async_exit_stack.enter_async_context(context_manager) return self async def __aexit__(self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: Any) -> None: """Async context manager exit. - If the chat_client supports async context management, exit its context. + Close the async exit stack to ensure all context managers are exited properly. """ - if isinstance(self.chat_client, AbstractAsyncContextManager): - await self.chat_client.__aexit__(exc_type, exc_val, exc_tb) # type: ignore[reportUnknownMemberType] + await self._async_exit_stack.aclose() def _update_agent_name(self) -> None: """Update the agent name in a chat client. @@ -506,6 +519,19 @@ class ChatClientAgent(AgentBase): thread, thread_messages = await self._prepare_thread_and_messages(thread=thread, input_messages=input_messages) agent_name = self._get_agent_name() + # Resolve final tool list (runtime provided tools + local MCP server tools) + final_tools: list[AITool | dict[str, Any] | Callable[..., Any]] = [] + # Normalize tools argument to a list without mutating the original parameter + normalized_tools = [] if tools is None else tools if isinstance(tools, list) else [tools] + for tool in normalized_tools: + if isinstance(tool, McpTool): + final_tools.extend(tool.functions) # type: ignore + else: + final_tools.append(tool) # type: ignore + + for mcp_server in self._local_mcp_tools: + final_tools.extend(mcp_server.functions) + response = await self.chat_client.get_response( messages=thread_messages, chat_options=self.chat_options @@ -523,7 +549,7 @@ class ChatClientAgent(AgentBase): store=store, temperature=temperature, tool_choice=tool_choice, - tools=tools, # type: ignore + tools=final_tools, # type: ignore[reportArgumentType] top_p=top_p, user=user, additional_properties=additional_properties or {}, @@ -617,6 +643,19 @@ class ChatClientAgent(AgentBase): agent_name = self._get_agent_name() response_updates: list[ChatResponseUpdate] = [] + # Resolve final tool list (runtime provided tools + local MCP server tools) + final_tools: list[AITool | MutableMapping[str, Any] | Callable[..., Any]] = [] + # Normalize tools argument to a list without mutating the original parameter + normalized_tools = [] if tools is None else tools if isinstance(tools, list) else [tools] + for tool in normalized_tools: + if isinstance(tool, McpTool): + final_tools.extend(tool.functions) # type: ignore + else: + final_tools.append(tool) + + for mcp_server in self._local_mcp_tools: + final_tools.extend(mcp_server.functions) + async for update in self.chat_client.get_streaming_response( messages=thread_messages, chat_options=self.chat_options @@ -634,7 +673,7 @@ class ChatClientAgent(AgentBase): store=store, temperature=temperature, tool_choice=tool_choice, - tools=tools, # type: ignore + tools=final_tools, # type: ignore[reportArgumentType] top_p=top_p, user=user, additional_properties=additional_properties or {}, diff --git a/python/packages/main/agent_framework/_clients.py b/python/packages/main/agent_framework/_clients.py index 031e23a9f6..61a077f15b 100644 --- a/python/packages/main/agent_framework/_clients.py +++ b/python/packages/main/agent_framework/_clients.py @@ -285,13 +285,13 @@ class ChatClient(Protocol): Args: messages: The sequence of input messages to send. + response_format: the format of the response. frequency_penalty: the frequency penalty to use. logit_bias: the logit bias to use. max_tokens: The maximum number of tokens to generate. metadata: additional metadata to include in the request. model: The model to use for the agent. presence_penalty: the presence penalty to use. - response_format: the format of the response. seed: the random seed to use. stop: the stop sequence(s) for the request. store: whether to store the response. diff --git a/python/packages/main/agent_framework/_mcp.py b/python/packages/main/agent_framework/_mcp.py new file mode 100644 index 0000000000..628a04542b --- /dev/null +++ b/python/packages/main/agent_framework/_mcp.py @@ -0,0 +1,784 @@ +# Copyright (c) Microsoft. All rights reserved. + +import json +import logging +import re +import sys +from abc import abstractmethod +from contextlib import AsyncExitStack, _AsyncGeneratorContextManager # type: ignore +from datetime import timedelta +from functools import partial +from typing import TYPE_CHECKING, Any + +from mcp import types +from mcp.client.session import ClientSession +from mcp.client.sse import sse_client +from mcp.client.stdio import StdioServerParameters, stdio_client +from mcp.client.streamable_http import streamablehttp_client +from mcp.client.websocket import websocket_client +from mcp.shared.context import RequestContext +from mcp.shared.exceptions import McpError +from mcp.shared.session import RequestResponder +from pydantic import BaseModel, create_model + +from ._tools import AIFunction +from ._types import AIContents, ChatMessage, ChatRole, DataContent, TextContent, UriContent +from .exceptions import ToolException, ToolExecutionException + +if sys.version_info >= (3, 11): + from typing import Self # pragma: no cover +else: + from typing_extensions import Self # pragma: no cover + +if TYPE_CHECKING: + from ._clients import ChatClient + +logger = logging.getLogger(__name__) + +# region: Helpers + +LOG_LEVEL_MAPPING: dict[types.LoggingLevel, int] = { + "debug": logging.DEBUG, + "info": logging.INFO, + "notice": logging.INFO, + "warning": logging.WARNING, + "error": logging.ERROR, + "critical": logging.CRITICAL, + "alert": logging.CRITICAL, + "emergency": logging.CRITICAL, +} + +__all__ = [ + "McpSseTools", + "McpStdioTool", + "McpStreamableHttpTool", + "McpWebsocketTool", +] + + +def _mcp_prompt_message_to_chat_message( + mcp_type: types.PromptMessage | types.SamplingMessage, +) -> ChatMessage: + """Convert a MCP container type to a Agent Framework type.""" + return ChatMessage( + role=ChatRole(value=mcp_type.role), + contents=[_mcp_type_to_ai_content(mcp_type.content)], # type: ignore[call-arg] + raw_representation=mcp_type, + ) + + +def _mcp_call_tool_result_to_ai_contents( + mcp_type: types.CallToolResult, +) -> list[AIContents]: + """Convert a MCP container type to a Agent Framework type.""" + return [_mcp_type_to_ai_content(item) for item in mcp_type.content] + + +def _mcp_type_to_ai_content( + mcp_type: types.ImageContent | types.TextContent | types.AudioContent | types.EmbeddedResource | types.ResourceLink, +) -> AIContents: + """Convert a MCP type to a Agent Framework type.""" + match mcp_type: + case types.TextContent(): + return TextContent(text=mcp_type.text, raw_representation=mcp_type) + case types.ImageContent() | types.AudioContent(): + return DataContent(uri=mcp_type.data, media_type=mcp_type.mimeType, raw_representation=mcp_type) + case types.ResourceLink(): + return UriContent( + uri=str(mcp_type.uri), media_type=mcp_type.mimeType or "application/json", raw_representation=mcp_type + ) + case _: + match mcp_type.resource: + case types.TextResourceContents(): + return TextContent( + text=mcp_type.resource.text, + raw_representation=mcp_type, + additional_properties=mcp_type.annotations.model_dump() if mcp_type.annotations else None, + ) + case types.BlobResourceContents(): + return DataContent( + uri=mcp_type.resource.blob, + media_type=mcp_type.resource.mimeType, + raw_representation=mcp_type, + additional_properties=mcp_type.annotations.model_dump() if mcp_type.annotations else None, + ) + + +def _ai_content_to_mcp_types( + content: AIContents, +) -> types.TextContent | types.ImageContent | types.AudioContent | types.EmbeddedResource | types.ResourceLink | None: + """Convert a AIContent type to a MCP type.""" + match content: + case TextContent(): + return types.TextContent(type="text", text=content.text) + case DataContent(): + if content.media_type and content.media_type.startswith("image/"): + return types.ImageContent(type="image", data=content.uri, mimeType=content.media_type) + if content.media_type and content.media_type.startswith("audio/"): + return types.AudioContent(type="audio", data=content.uri, mimeType=content.media_type) + if content.media_type and content.media_type.startswith("application/"): + return types.EmbeddedResource( + type="resource", + resource=types.BlobResourceContents( + blob=content.uri, + mimeType=content.media_type, + # uri's are not limited in MCP but they have to be set. + # the uri of data content, contains the data uri, which + # is not the uri meant here, UriContent would match this. + uri=content.additional_properties.get("uri", "af://binary") + if content.additional_properties + else "af://binary", # type: ignore[reportArgumentType] + ), + ) + return None + case UriContent(): + return types.ResourceLink( + type="resource_link", + uri=content.uri, # type: ignore[reportArgumentType] + mimeType=content.media_type, + name=content.additional_properties.get("name", "Unknown") + if content.additional_properties + else "Unknown", + ) + case _: + return None + + +def _chat_message_to_mcp_types( + content: ChatMessage, +) -> list[types.TextContent | types.ImageContent | types.AudioContent | types.EmbeddedResource | types.ResourceLink]: + """Convert a ChatMessage to a list of MCP types.""" + messages: list[ + types.TextContent | types.ImageContent | types.AudioContent | types.EmbeddedResource | types.ResourceLink + ] = [] + for item in content.contents: + mcp_content = _ai_content_to_mcp_types(item) + if mcp_content: + messages.append(mcp_content) + return messages + + +def _get_input_model_from_mcp_prompt(prompt: types.Prompt) -> type[BaseModel]: + """Creates a Pydantic model from a prompt's parameters.""" + # Check if 'arguments' is missing or empty + if not prompt.arguments: + return create_model(f"{prompt.name}_input") + + field_definitions: dict[str, Any] = {} + for prompt_argument in prompt.arguments: + # For prompts, all arguments are typically required and string type + # unless specified otherwise in the prompt argument + python_type = str # Default type for prompt arguments + + # Create field definition for create_model + if prompt_argument.required: + field_definitions[prompt_argument.name] = (python_type, ...) + else: + field_definitions[prompt_argument.name] = (python_type, None) + + return create_model(f"{prompt.name}_input", **field_definitions) + + +def _get_input_model_from_mcp_tool(tool: types.Tool) -> type[BaseModel]: + """Creates a Pydantic model from a tools parameters.""" + properties = tool.inputSchema.get("properties", None) + required = tool.inputSchema.get("required", []) + # Check if 'properties' is missing or not a dictionary + if not properties: + return create_model(f"{tool.name}_input") + + field_definitions: dict[str, Any] = {} + for prop_name, prop_details in properties.items(): + prop_details = json.loads(prop_details) if isinstance(prop_details, str) else prop_details + + # Map JSON Schema types to Python types + json_type = prop_details.get("type", "string") + python_type: type = str # default + if json_type == "integer": + python_type = int + elif json_type == "number": + python_type = float + elif json_type == "boolean": + python_type = bool + elif json_type == "array": + python_type = list + elif json_type == "object": + python_type = dict + + # Create field definition for create_model + if prop_name in required: + field_definitions[prop_name] = (python_type, ...) + else: + default_value = prop_details.get("default", None) + field_definitions[prop_name] = (python_type, default_value) + + return create_model(f"{tool.name}_input", **field_definitions) + + +def _normalize_mcp_name(name: str) -> str: + """Normalize MCP tool/prompt names to allowed identifier pattern (A-Za-z0-9_.-).""" + return re.sub(r"[^A-Za-z0-9_.-]", "-", name) + + +# region: MCP Plugin + + +class McpTool: + """Base class with the MCP logic.""" + + def __init__( + self, + name: str, + description: str | None = None, + additional_properties: dict[str, Any] | None = None, + load_tools: bool = True, + load_prompts: bool = True, + session: ClientSession | None = None, + request_timeout: int | None = None, + chat_client: "ChatClient | None" = None, + ) -> None: + """Initialize the MCP Plugin Base.""" + self.name = name + self.description = description or "" + self.additional_properties = additional_properties + self.load_tools_flag = load_tools + self.load_prompts_flag = load_prompts + self._exit_stack = AsyncExitStack() + self.session = session + self.request_timeout = request_timeout + self.chat_client = chat_client + self.functions: list[AIFunction[Any, Any]] = [] + + def __str__(self) -> str: + return f"McpTool(name={self.name}, description={self.description})" + + async def connect(self) -> None: + """Connect to the MCP server.""" + if not self.session: + try: + transport = await self._exit_stack.enter_async_context(self.get_mcp_client()) + except Exception as ex: + await self._exit_stack.aclose() + raise ToolException( + "Failed to connect to the MCP server. Please check your configuration.", inner_exception=ex + ) from ex + try: + session = await self._exit_stack.enter_async_context( + ClientSession( + read_stream=transport[0], + write_stream=transport[1], + read_timeout_seconds=timedelta(seconds=self.request_timeout) if self.request_timeout else None, + message_handler=self.message_handler, + logging_callback=self.logging_callback, + sampling_callback=self.sampling_callback, + ) + ) + except Exception as ex: + await self._exit_stack.aclose() + raise ToolException( + message="Failed to create a session. Please check your configuration.", inner_exception=ex + ) from ex + await session.initialize() + self.session = session + elif self.session._request_id == 0: # type: ignore[reportPrivateUsage] + # If the session is not initialized, we need to reinitialize it + await self.session.initialize() + logger.debug("Connected to MCP server: %s", self.session) + if self.load_tools_flag: + await self.load_tools() + if self.load_prompts_flag: + await self.load_prompts() + + if logger.level != logging.NOTSET: + try: + await self.session.set_logging_level( + next(level for level, value in LOG_LEVEL_MAPPING.items() if value == logger.level) + ) + except Exception as exc: + logger.warning("Failed to set log level to %s", logger.level, exc_info=exc) + + async def sampling_callback( + self, context: RequestContext[ClientSession, Any], params: types.CreateMessageRequestParams + ) -> types.CreateMessageResult | types.ErrorData: + """Callback function for sampling. + + This function is called when the MCP server needs to get a message completed. + + This is a simple version of this function, it can be overridden to allow more complex sampling. + It get's added to the session at initialization time, so overriding it is the best way to do this. + """ + if not self.chat_client: + return types.ErrorData( + code=types.INTERNAL_ERROR, + message="No chat client available. Please set a chat client.", + ) + logger.debug("Sampling callback called with params: %s", params) + messages: list[ChatMessage] = [] + for msg in params.messages: + messages.append(_mcp_prompt_message_to_chat_message(msg)) + try: + response = await self.chat_client.get_response( + messages, + temperature=params.temperature, + max_tokens=params.maxTokens, + stop=params.stopSequences, + ) + except Exception as ex: + return types.ErrorData( + code=types.INTERNAL_ERROR, + message=f"Failed to get chat message content: {ex}", + ) + if not response or not response.messages: + return types.ErrorData( + code=types.INTERNAL_ERROR, + message="Failed to get chat message content.", + ) + mcp_contents = _chat_message_to_mcp_types(response.messages[0]) + # grab the first content that is of type TextContent or ImageContent + mcp_content = next( + (content for content in mcp_contents if isinstance(content, (types.TextContent, types.ImageContent))), + None, + ) + if not mcp_content: + return types.ErrorData( + code=types.INTERNAL_ERROR, + message="Failed to get right content types from the response.", + ) + return types.CreateMessageResult( + role="assistant", + content=mcp_content, + model=response.ai_model_id or "unknown", + ) + + async def logging_callback(self, params: types.LoggingMessageNotificationParams) -> None: + """Callback function for logging. + + This function is called when the MCP Server sends a log message. + By default it will log the message to the logger with the level set in the params. + + Please subclass the MCP*Plugin and override this function if you want to adapt the behavior. + """ + logger.log(LOG_LEVEL_MAPPING[params.level], params.data) + + async def message_handler( + self, + message: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception, + ) -> None: + """Handle messages from the MCP server. + + By default this function will handle exceptions on the server, by logging those. + + And it will trigger a reload of the tools and prompts when the list changed notification is received. + + If you want to extend this behavior you can subclass the MCPPlugin and override this function, + if you want to keep the default behavior, make sure to call `super().message_handler(message)`. + """ + if isinstance(message, Exception): + logger.error("Error from MCP server: %s", message, exc_info=message) + return + if isinstance(message, types.ServerNotification): + match message.root.method: + case "notifications/tools/list_changed": + await self.load_tools() + case "notifications/prompts/list_changed": + await self.load_prompts() + case _: + logger.debug("Unhandled notification: %s", message.root.method) + + async def load_prompts(self) -> None: + """Load prompts from the MCP server.""" + if not self.session: + raise ToolExecutionException("MCP server not connected, please call connect() before using this method.") + try: + prompt_list = await self.session.list_prompts() + except Exception as exc: + logger.info( + "Prompt could not be loaded, you can exclude trying to load, by setting: load_prompts=False", + exc_info=exc, + ) + prompt_list = None + for prompt in prompt_list.prompts if prompt_list else []: + local_name = _normalize_mcp_name(prompt.name) + input_model = _get_input_model_from_mcp_prompt(prompt) + func: AIFunction[BaseModel, list[ChatMessage]] = AIFunction( + func=partial(self.get_prompt, prompt.name), + name=local_name, + description=prompt.description or "", + input_model=input_model, + ) + self.functions.append(func) + + async def load_tools(self) -> None: + """Load tools from the MCP server.""" + if not self.session: + raise ToolExecutionException("MCP server not connected, please call connect() before using this method.") + try: + tool_list = await self.session.list_tools() + except Exception as exc: + logger.info( + "Tools could not be loaded, you can exclude trying to load, by setting: load_tools=False", + exc_info=exc, + ) + tool_list = None + for tool in tool_list.tools if tool_list else []: + local_name = _normalize_mcp_name(tool.name) + input_model = _get_input_model_from_mcp_tool(tool) + # Create AIFunctions out of each tool + func: AIFunction[BaseModel, list[AIContents]] = AIFunction( + func=partial(self.call_tool, tool.name), + name=local_name, + description=tool.description or "", + input_model=input_model, + ) + self.functions.append(func) + + async def close(self) -> None: + """Disconnect from the MCP server.""" + await self._exit_stack.aclose() + self.session = None + + @abstractmethod + def get_mcp_client(self) -> _AsyncGeneratorContextManager[Any, None]: + """Get an MCP client.""" + pass + + async def call_tool(self, tool_name: str, **kwargs: Any) -> list[AIContents]: + """Call a tool with the given arguments.""" + if not self.session: + raise ToolExecutionException("MCP server not connected, please call connect() before using this method.") + if not self.load_tools_flag: + raise ToolExecutionException( + "Tools are not loaded for this server, please set load_tools=True in the constructor." + ) + try: + return _mcp_call_tool_result_to_ai_contents(await self.session.call_tool(tool_name, arguments=kwargs)) + except McpError as mcp_exc: + raise ToolExecutionException(mcp_exc.error.message, inner_exception=mcp_exc) from mcp_exc + except Exception as ex: + raise ToolExecutionException(f"Failed to call tool '{tool_name}'.", inner_exception=ex) from ex + + async def get_prompt(self, prompt_name: str, **kwargs: Any) -> list[ChatMessage]: + """Call a prompt with the given arguments.""" + if not self.session: + raise ToolExecutionException("MCP server not connected, please call connect() before using this method.") + if not self.load_prompts_flag: + raise ToolExecutionException( + "Prompts are not loaded for this server, please set load_prompts=True in the constructor." + ) + try: + prompt_result = await self.session.get_prompt(prompt_name, arguments=kwargs) + return [_mcp_prompt_message_to_chat_message(message) for message in prompt_result.messages] + except McpError as mcp_exc: + raise ToolExecutionException(mcp_exc.error.message, inner_exception=mcp_exc) from mcp_exc + except Exception as ex: + raise ToolExecutionException(f"Failed to call prompt '{prompt_name}'.", inner_exception=ex) from ex + + async def __aenter__(self) -> Self: + """Enter the context manager.""" + try: + await self.connect() + return self + except ToolException: + raise + except Exception as ex: + await self._exit_stack.aclose() + raise ToolExecutionException("Failed to enter context manager.", inner_exception=ex) from ex + + async def __aexit__( + self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: Any + ) -> None: + """Exit the context manager.""" + await self.close() + + +# region: MCP Plugin Implementations + + +class McpStdioTool(McpTool): + """MCP stdio server configuration.""" + + def __init__( + self, + name: str, + command: str, + *, + load_tools: bool = True, + load_prompts: bool = True, + request_timeout: int | None = None, + session: ClientSession | None = None, + description: str | None = None, + additional_properties: dict[str, Any] | None = None, + args: list[str] | None = None, + env: dict[str, str] | None = None, + encoding: str | None = None, + chat_client: "ChatClient | None" = None, + **kwargs: Any, + ) -> None: + """Initialize the MCP stdio plugin. + + The arguments are used to create a StdioServerParameters object. + Which is then used to create a stdio client. + see mcp.client.stdio.stdio_client and mcp.client.stdio.stdio_server_parameters + for more details. + + Args: + name: The name of the plugin. + command: The command to run the MCP server. + load_tools: Whether to load tools from the MCP server. + load_prompts: Whether to load prompts from the MCP server. + request_timeout: The default timeout used for all requests. + session: The session to use for the MCP connection. + description: The description of the plugin. + additional_properties: Additional properties. + args: The arguments to pass to the command. + env: The environment variables to set for the command. + encoding: The encoding to use for the command output. + chat_client: The chat client to use for sampling. + kwargs: Any extra arguments to pass to the stdio client. + + """ + super().__init__( + name=name, + description=description, + additional_properties=additional_properties, + session=session, + chat_client=chat_client, + load_tools=load_tools, + load_prompts=load_prompts, + request_timeout=request_timeout, + ) + self.command = command + self.args = args or [] + self.env = env + self.encoding = encoding + self._client_kwargs = kwargs + + def get_mcp_client(self) -> _AsyncGeneratorContextManager[Any, None]: + """Get an MCP stdio client.""" + args: dict[str, Any] = { + "command": self.command, + "args": self.args, + "env": self.env, + } + if self.encoding: + args["encoding"] = self.encoding + if self._client_kwargs: + args.update(self._client_kwargs) + return stdio_client(server=StdioServerParameters(**args)) + + +class McpSseTools(McpTool): + """MCP sse server configuration.""" + + def __init__( + self, + name: str, + url: str, + *, + load_tools: bool = True, + load_prompts: bool = True, + request_timeout: int | None = None, + session: ClientSession | None = None, + description: str | None = None, + additional_properties: dict[str, Any] | None = None, + headers: dict[str, Any] | None = None, + timeout: float | None = None, + sse_read_timeout: float | None = None, + chat_client: "ChatClient | None" = None, + **kwargs: Any, + ) -> None: + """Initialize the MCP sse plugin. + + The arguments are used to create a sse client. + see mcp.client.sse.sse_client for more details. + + Any extra arguments passed to the constructor will be passed to the + sse client constructor. + + Args: + name: The name of the plugin. + url: The URL of the MCP server. + load_tools: Whether to load tools from the MCP server. + load_prompts: Whether to load prompts from the MCP server. + request_timeout: The default timeout used for all requests. + session: The session to use for the MCP connection. + description: The description of the plugin. + additional_properties: Additional properties. + headers: The headers to send with the request. + timeout: The timeout for the request. + sse_read_timeout: The timeout for reading from the SSE stream. + chat_client: The chat client to use for sampling. + kwargs: Any extra arguments to pass to the sse client. + + """ + super().__init__( + name=name, + description=description, + additional_properties=additional_properties, + session=session, + chat_client=chat_client, + load_tools=load_tools, + load_prompts=load_prompts, + request_timeout=request_timeout, + ) + self.url = url + self.headers = headers or {} + self.timeout = timeout + self.sse_read_timeout = sse_read_timeout + self._client_kwargs = kwargs + + def get_mcp_client(self) -> _AsyncGeneratorContextManager[Any, None]: + """Get an MCP SSE client.""" + args: dict[str, Any] = { + "url": self.url, + } + if self.headers: + args["headers"] = self.headers + if self.timeout is not None: + args["timeout"] = self.timeout + if self.sse_read_timeout is not None: + args["sse_read_timeout"] = self.sse_read_timeout + if self._client_kwargs: + args.update(self._client_kwargs) + return sse_client(**args) + + +class McpStreamableHttpTool(McpTool): + """MCP streamable http server configuration.""" + + def __init__( + self, + name: str, + url: str, + *, + load_tools: bool = True, + load_prompts: bool = True, + request_timeout: int | None = None, + session: ClientSession | None = None, + description: str | None = None, + additional_properties: dict[str, Any] | None = None, + headers: dict[str, Any] | None = None, + timeout: float | None = None, + sse_read_timeout: float | None = None, + terminate_on_close: bool | None = None, + chat_client: "ChatClient | None" = None, + **kwargs: Any, + ) -> None: + """Initialize the MCP streamable http plugin. + + The arguments are used to create a streamable http client. + see mcp.client.streamable_http.streamablehttp_client for more details. + + Any extra arguments passed to the constructor will be passed to the + streamable http client constructor. + + Args: + name: The name of the plugin. + url: The URL of the MCP server. + load_tools: Whether to load tools from the MCP server. + load_prompts: Whether to load prompts from the MCP server. + request_timeout: The default timeout used for all requests. + session: The session to use for the MCP connection. + description: The description of the plugin. + additional_properties: Additional properties. + headers: The headers to send with the request. + timeout: The timeout for the request. + sse_read_timeout: The timeout for reading from the SSE stream. + terminate_on_close: Close the transport when the MCP client is terminated. + chat_client: The chat client to use for sampling. + kwargs: Any extra arguments to pass to the sse client. + """ + super().__init__( + name=name, + description=description, + additional_properties=additional_properties, + session=session, + chat_client=chat_client, + load_tools=load_tools, + load_prompts=load_prompts, + request_timeout=request_timeout, + ) + self.url = url + self.headers = headers or {} + self.timeout = timeout + self.sse_read_timeout = sse_read_timeout + self.terminate_on_close = terminate_on_close + self._client_kwargs = kwargs + + def get_mcp_client(self) -> _AsyncGeneratorContextManager[Any, None]: + """Get an MCP streamable http client.""" + args: dict[str, Any] = { + "url": self.url, + } + if self.headers: + args["headers"] = self.headers + if self.timeout is not None: + args["timeout"] = self.timeout + if self.sse_read_timeout is not None: + args["sse_read_timeout"] = self.sse_read_timeout + if self.terminate_on_close is not None: + args["terminate_on_close"] = self.terminate_on_close + if self._client_kwargs: + args.update(self._client_kwargs) + return streamablehttp_client(**args) + + +class McpWebsocketTool(McpTool): + """MCP websocket server configuration.""" + + def __init__( + self, + name: str, + url: str, + *, + load_tools: bool = True, + load_prompts: bool = True, + request_timeout: int | None = None, + session: ClientSession | None = None, + description: str | None = None, + additional_properties: dict[str, Any] | None = None, + chat_client: "ChatClient | None" = None, + **kwargs: Any, + ) -> None: + """Initialize the MCP websocket plugin. + + The arguments are used to create a websocket client. + see mcp.client.websocket.websocket_client for more details. + + Any extra arguments passed to the constructor will be passed to the + websocket client constructor. + + Args: + name: The name of the plugin. + url: The URL of the MCP server. + load_tools: Whether to load tools from the MCP server. + load_prompts: Whether to load prompts from the MCP server. + request_timeout: The default timeout used for all requests. + session: The session to use for the MCP connection. + description: The description of the plugin. + additional_properties: Additional properties. + chat_client: The chat client to use for sampling. + kwargs: Any extra arguments to pass to the websocket client. + + """ + super().__init__( + name=name, + description=description, + additional_properties=additional_properties, + session=session, + chat_client=chat_client, + load_tools=load_tools, + load_prompts=load_prompts, + request_timeout=request_timeout, + ) + self.url = url + self._client_kwargs = kwargs + + def get_mcp_client(self) -> _AsyncGeneratorContextManager[Any, None]: + """Get an MCP websocket client.""" + args: dict[str, Any] = { + "url": self.url, + } + if self._client_kwargs: + args.update(self._client_kwargs) + return websocket_client(**args) diff --git a/python/packages/main/agent_framework/_types.py b/python/packages/main/agent_framework/_types.py index f9bf55790d..49dd146dff 100644 --- a/python/packages/main/agent_framework/_types.py +++ b/python/packages/main/agent_framework/_types.py @@ -26,6 +26,7 @@ from pydantic import ( model_serializer, ) +from ._logging import get_logger from ._pydantic import AFBaseModel from ._tools import AITool, ai_function from .exceptions import AgentFrameworkException @@ -35,9 +36,10 @@ if sys.version_info >= (3, 11): else: from typing_extensions import Self # pragma: no cover +logger = get_logger("agent_framework") + # region Constants and types _T = TypeVar("_T") -TValue = TypeVar("TValue") TEmbedding = TypeVar("TEmbedding") TChatResponse = TypeVar("TChatResponse", bound="ChatResponse") TChatToolMode = TypeVar("TChatToolMode", bound="ChatToolMode") @@ -99,7 +101,6 @@ __all__ = [ "HostedFileContent", "HostedVectorStoreContent", "SpeechToTextOptions", - "StructuredResponse", "TextContent", "TextReasoningContent", "TextSpanRegion", @@ -1317,10 +1318,9 @@ class ChatResponse(AFBaseModel): created_at: A timestamp for the chat response. finish_reason: The reason for the chat response. usage_details: The usage details for the chat response. + structured_output: The structured output of the chat response, if applicable. additional_properties: Any additional properties associated with the chat response. raw_representation: The raw representation of the chat response from an underlying implementation. - - """ messages: list[ChatMessage] @@ -1338,6 +1338,8 @@ class ChatResponse(AFBaseModel): """The reason for the chat response.""" usage_details: UsageDetails | None = None """The usage details for the chat response.""" + value: Any | None = None + """The structured output of the chat response, if applicable.""" additional_properties: dict[str, Any] | None = None """Any additional properties associated with the chat response.""" raw_representation: Any | None = None @@ -1354,6 +1356,8 @@ class ChatResponse(AFBaseModel): created_at: CreatedAtT | None = None, finish_reason: ChatFinishReason | None = None, usage_details: UsageDetails | None = None, + value: Any | None = None, + response_format: type[BaseModel] | None = None, additional_properties: dict[str, Any] | None = None, raw_representation: Any | None = None, **kwargs: Any, @@ -1368,6 +1372,8 @@ class ChatResponse(AFBaseModel): created_at: Optional timestamp for the chat response. finish_reason: Optional reason for the chat response. usage_details: Optional usage details for the chat response. + value: Optional value of the structured output. + response_format: Optional response format for the chat response. messages: List of ChatMessage objects to include in the response. additional_properties: Optional additional properties associated with the chat response. raw_representation: Optional raw representation of the chat response from an underlying implementation. @@ -1385,6 +1391,8 @@ class ChatResponse(AFBaseModel): created_at: CreatedAtT | None = None, finish_reason: ChatFinishReason | None = None, usage_details: UsageDetails | None = None, + value: Any | None = None, + response_format: type[BaseModel] | None = None, additional_properties: dict[str, Any] | None = None, raw_representation: Any | None = None, **kwargs: Any, @@ -1399,6 +1407,8 @@ class ChatResponse(AFBaseModel): created_at: Optional timestamp for the chat response. finish_reason: Optional reason for the chat response. usage_details: Optional usage details for the chat response. + value: Optional value of the structured output. + response_format: Optional response format for the chat response. additional_properties: Optional additional properties associated with the chat response. raw_representation: Optional raw representation of the chat response from an underlying implementation. **kwargs: Any additional keyword arguments. @@ -1416,6 +1426,8 @@ class ChatResponse(AFBaseModel): created_at: CreatedAtT | None = None, finish_reason: ChatFinishReason | None = None, usage_details: UsageDetails | None = None, + value: Any | None = None, + response_format: type[BaseModel] | None = None, additional_properties: dict[str, Any] | None = None, raw_representation: Any | None = None, **kwargs: Any, @@ -1438,29 +1450,44 @@ class ChatResponse(AFBaseModel): created_at=created_at, # type: ignore[reportCallIssue] finish_reason=finish_reason, # type: ignore[reportCallIssue] usage_details=usage_details, # type: ignore[reportCallIssue] + value=value, # type: ignore[reportCallIssue] additional_properties=additional_properties, # type: ignore[reportCallIssue] raw_representation=raw_representation, # type: ignore[reportCallIssue] **kwargs, ) + if response_format: + self.try_parse_value(output_format_type=response_format) @classmethod - def from_chat_response_updates(cls: type[TChatResponse], updates: Sequence["ChatResponseUpdate"]) -> TChatResponse: + def from_chat_response_updates( + cls: type[TChatResponse], + updates: Sequence["ChatResponseUpdate"], + *, + output_format_type: type[BaseModel] | None = None, + ) -> TChatResponse: """Joins multiple updates into a single ChatResponse.""" msg = cls(messages=[]) for update in updates: _process_update(msg, update) _finalize_response(msg) + if output_format_type: + msg.try_parse_value(output_format_type) return msg @classmethod async def from_chat_response_generator( - cls: type[TChatResponse], updates: AsyncIterable["ChatResponseUpdate"] + cls: type[TChatResponse], + updates: AsyncIterable["ChatResponseUpdate"], + *, + output_format_type: type[BaseModel] | None = None, ) -> TChatResponse: """Joins multiple updates into a single ChatResponse.""" msg = cls(messages=[]) async for update in updates: _process_update(msg, update) _finalize_response(msg) + if output_format_type: + msg.try_parse_value(output_format_type) return msg @property @@ -1471,97 +1498,13 @@ class ChatResponse(AFBaseModel): def __str__(self) -> str: return self.text - -class StructuredResponse(ChatResponse, Generic[TValue]): - """Represents a structured response to a chat request. - - Type Parameters: - TValue: The type of the value contained in the structured response. - """ - - value: TValue - """The result value of the chat response as an instance of `TValue`.""" - - @property - def text(self) -> str: - """Returns the concatenated text of all messages in the response.""" - return "\n".join(message.text for message in self.messages) - - @overload - def __init__( - self, - value: TValue, - *, - messages: ChatMessage | MutableSequence[ChatMessage], - response_id: str | None = None, - conversation_id: str | None = None, - model_id: str | None = None, - created_at: CreatedAtT | None = None, - finish_reason: ChatFinishReason | None = None, - usage_details: UsageDetails | None = None, - additional_properties: dict[str, Any] | None = None, - raw_representation: Any | None = None, - **kwargs: Any, - ) -> None: - """Initializes a StructuredResponse with the provided parameters.""" - - @overload - def __init__( - self, - value: TValue, - *, - text: TextContent | str, - response_id: str | None = None, - conversation_id: str | None = None, - model_id: str | None = None, - created_at: CreatedAtT | None = None, - finish_reason: ChatFinishReason | None = None, - usage_details: UsageDetails | None = None, - raw_representation: Any | None = None, - additional_properties: dict[str, Any] | None = None, - **kwargs: Any, - ) -> None: - """Initializes a StructuredResponse with the provided parameters.""" - - def __init__( - self, - value: TValue, - *, - messages: ChatMessage | MutableSequence[ChatMessage] | None = None, - text: TextContent | str | None = None, - response_id: str | None = None, - conversation_id: str | None = None, - model_id: str | None = None, - created_at: CreatedAtT | None = None, - finish_reason: ChatFinishReason | None = None, - usage_details: UsageDetails | None = None, - additional_properties: dict[str, Any] | None = None, - raw_representation: Any | None = None, - **kwargs: Any, - ) -> None: - """Initializes a StructuredResponse with the provided parameters.""" - if messages is None: - messages = [] - elif isinstance(messages, ChatMessage): - messages = [messages] - if text is not None: - if isinstance(text, str): - text = TextContent(text=text) - messages.append(ChatMessage(role=ChatRole.ASSISTANT, contents=[text])) - - super().__init__( - value=value, - messages=messages, - conversation_id=conversation_id, - created_at=created_at, - finish_reason=finish_reason, - model_id=model_id, - response_id=response_id, - usage_details=usage_details, - additional_properties=additional_properties, - raw_representation=raw_representation, - **kwargs, - ) + def try_parse_value(self, output_format_type: type[BaseModel]) -> None: + """If there is a value, does nothing, otherwise tries to parse the text into the value.""" + if self.value is None: + try: + self.value = output_format_type.model_validate_json(self.text) # type: ignore[reportUnknownMemberType] + except ValidationError as ex: + logger.debug("Failed to parse value from chat response text: %s", ex) # region ChatResponseUpdate diff --git a/python/packages/main/agent_framework/openai/_assistants_client.py b/python/packages/main/agent_framework/openai/_assistants_client.py index c332e4b97d..e26459d51e 100644 --- a/python/packages/main/agent_framework/openai/_assistants_client.py +++ b/python/packages/main/agent_framework/openai/_assistants_client.py @@ -144,7 +144,8 @@ class OpenAIAssistantsClient(OpenAIConfigBase, ChatClientBase): **kwargs: Any, ) -> ChatResponse: return await ChatResponse.from_chat_response_generator( - updates=self._inner_get_streaming_response(messages=messages, chat_options=chat_options, **kwargs) + updates=self._inner_get_streaming_response(messages=messages, chat_options=chat_options, **kwargs), + output_format_type=chat_options.response_format, ) async def _inner_get_streaming_response( diff --git a/python/packages/main/agent_framework/openai/_chat_client.py b/python/packages/main/agent_framework/openai/_chat_client.py index ab01e336e4..5e7d126182 100644 --- a/python/packages/main/agent_framework/openai/_chat_client.py +++ b/python/packages/main/agent_framework/openai/_chat_client.py @@ -61,7 +61,9 @@ class OpenAIChatClientBase(OpenAIHandler, ChatClientBase): ) -> ChatResponse: options_dict = self._prepare_options(messages, chat_options) try: - return self._create_chat_response(await self.client.chat.completions.create(stream=False, **options_dict)) + return self._create_chat_response( + await self.client.chat.completions.create(stream=False, **options_dict), chat_options + ) except BadRequestError as ex: if ex.code == "content_filter": raise OpenAIContentFilterException( @@ -143,7 +145,7 @@ class OpenAIChatClientBase(OpenAIHandler, ChatClientBase): options_dict["response_format"] = type_to_response_format_param(chat_options.response_format) return options_dict - def _create_chat_response(self, response: ChatCompletion) -> "ChatResponse": + def _create_chat_response(self, response: ChatCompletion, chat_options: ChatOptions) -> "ChatResponse": """Create a chat message content object from a choice.""" response_metadata = self._get_metadata_from_chat_response(response) messages: list[ChatMessage] = [] @@ -166,6 +168,7 @@ class OpenAIChatClientBase(OpenAIHandler, ChatClientBase): model_id=response.model, additional_properties=response_metadata, finish_reason=finish_reason, + response_format=chat_options.response_format, ) def _create_chat_response_update( diff --git a/python/packages/main/agent_framework/openai/_responses_client.py b/python/packages/main/agent_framework/openai/_responses_client.py index 6e3448f2e5..4ad6ad7dbf 100644 --- a/python/packages/main/agent_framework/openai/_responses_client.py +++ b/python/packages/main/agent_framework/openai/_responses_client.py @@ -44,7 +44,6 @@ from .._types import ( FunctionCallContent, FunctionResultContent, HostedFileContent, - StructuredResponse, TextContent, TextSpanRegion, UsageDetails, @@ -605,7 +604,8 @@ class OpenAIResponsesClientBase(OpenAIHandler, ChatClientBase): args["usage_details"] = usage_details if structured_response: args["value"] = structured_response - return StructuredResponse(**args) + elif chat_options.response_format: + args["response_format"] = chat_options.response_format return ChatResponse(**args) def _create_streaming_response_content( diff --git a/python/packages/main/pyproject.toml b/python/packages/main/pyproject.toml index 1cd1b5d97e..c73d6c528a 100644 --- a/python/packages/main/pyproject.toml +++ b/python/packages/main/pyproject.toml @@ -29,6 +29,7 @@ dependencies = [ "typing-extensions>=4.14.0", "opentelemetry-api ~= 1.24", "opentelemetry-sdk ~= 1.24", + "mcp>=1.12", ] [project.optional-dependencies] diff --git a/python/packages/main/tests/main/test_mcp.py b/python/packages/main/tests/main/test_mcp.py new file mode 100644 index 0000000000..6d6aa1ddba --- /dev/null +++ b/python/packages/main/tests/main/test_mcp.py @@ -0,0 +1,544 @@ +# Copyright (c) Microsoft. All rights reserved. +# type: ignore[reportPrivateUsage] +import os +from contextlib import _AsyncGeneratorContextManager # type: ignore +from typing import Any +from unittest.mock import AsyncMock, Mock + +import pytest +from mcp import types +from mcp.client.session import ClientSession +from mcp.shared.exceptions import McpError +from pydantic import AnyUrl, ValidationError + +from agent_framework import ( + AITool, + ChatMessage, + ChatRole, + DataContent, + McpSseTools, + McpStdioTool, + McpStreamableHttpTool, + McpWebsocketTool, + TextContent, + UriContent, +) +from agent_framework._mcp import ( + McpTool, + _ai_content_to_mcp_types, + _chat_message_to_mcp_types, + _get_input_model_from_mcp_prompt, + _get_input_model_from_mcp_tool, + _mcp_call_tool_result_to_ai_contents, + _mcp_prompt_message_to_chat_message, + _mcp_type_to_ai_content, + _normalize_mcp_name, +) +from agent_framework.exceptions import ToolExecutionException + +# Integration test skip condition +skip_if_mcp_integration_tests_disabled = pytest.mark.skipif( + os.getenv("RUN_INTEGRATION_TESTS", "false").lower() != "true" or os.getenv("LOCAL_MCP_URL", "") == "", + reason="No LOCAL_MCP_URL provided; skipping integration tests." + if os.getenv("RUN_INTEGRATION_TESTS", "false").lower() == "true" + else "Integration tests are disabled.", +) + + +# Helper function tests +def test_normalize_mcp_name(): + """Test MCP name normalization.""" + assert _normalize_mcp_name("valid_name") == "valid_name" + assert _normalize_mcp_name("name-with-dashes") == "name-with-dashes" + assert _normalize_mcp_name("name.with.dots") == "name.with.dots" + assert _normalize_mcp_name("name with spaces") == "name-with-spaces" + assert _normalize_mcp_name("name@with#special$chars") == "name-with-special-chars" + assert _normalize_mcp_name("name/with\\slashes") == "name-with-slashes" + + +def test_mcp_prompt_message_to_ai_content(): + """Test conversion from MCP prompt message to AI content.""" + mcp_message = types.PromptMessage(role="user", content=types.TextContent(type="text", text="Hello, world!")) + ai_content = _mcp_prompt_message_to_chat_message(mcp_message) + + assert isinstance(ai_content, ChatMessage) + assert ai_content.role.value == "user" + assert len(ai_content.contents) == 1 + assert isinstance(ai_content.contents[0], TextContent) + assert ai_content.contents[0].text == "Hello, world!" + assert ai_content.raw_representation == mcp_message + + +def test_mcp_call_tool_result_to_ai_contents(): + """Test conversion from MCP tool result to AI contents.""" + mcp_result = types.CallToolResult( + content=[ + types.TextContent(type="text", text="Result text"), + types.ImageContent(type="image", data="data:image/png;base64,xyz", mimeType="image/png"), + ] + ) + ai_contents = _mcp_call_tool_result_to_ai_contents(mcp_result) + + assert len(ai_contents) == 2 + assert isinstance(ai_contents[0], TextContent) + assert ai_contents[0].text == "Result text" + assert isinstance(ai_contents[1], DataContent) + assert ai_contents[1].uri == "data:image/png;base64,xyz" + assert ai_contents[1].media_type == "image/png" + + +def test_mcp_content_types_to_ai_content_text(): + """Test conversion of MCP text content to AI content.""" + mcp_content = types.TextContent(type="text", text="Sample text") + ai_content = _mcp_type_to_ai_content(mcp_content) + + assert isinstance(ai_content, TextContent) + assert ai_content.text == "Sample text" + assert ai_content.raw_representation == mcp_content + + +def test_mcp_content_types_to_ai_content_image(): + """Test conversion of MCP image content to AI content.""" + mcp_content = types.ImageContent(type="image", data="data:image/jpeg;base64,abc", mimeType="image/jpeg") + ai_content = _mcp_type_to_ai_content(mcp_content) + + assert isinstance(ai_content, DataContent) + assert ai_content.uri == "data:image/jpeg;base64,abc" + assert ai_content.media_type == "image/jpeg" + assert ai_content.raw_representation == mcp_content + + +def test_mcp_content_types_to_ai_content_audio(): + """Test conversion of MCP audio content to AI content.""" + mcp_content = types.AudioContent(type="audio", data="data:audio/wav;base64,def", mimeType="audio/wav") + ai_content = _mcp_type_to_ai_content(mcp_content) + + assert isinstance(ai_content, DataContent) + assert ai_content.uri == "data:audio/wav;base64,def" + assert ai_content.media_type == "audio/wav" + assert ai_content.raw_representation == mcp_content + + +def test_mcp_content_types_to_ai_content_resource_link(): + """Test conversion of MCP resource link to AI content.""" + mcp_content = types.ResourceLink( + type="resource_link", + uri=AnyUrl("https://example.com/resource"), + name="test_resource", + mimeType="application/json", + ) + ai_content = _mcp_type_to_ai_content(mcp_content) + + assert isinstance(ai_content, UriContent) + assert ai_content.uri == "https://example.com/resource" + assert ai_content.media_type == "application/json" + assert ai_content.raw_representation == mcp_content + + +def test_mcp_content_types_to_ai_content_embedded_resource_text(): + """Test conversion of MCP embedded text resource to AI content.""" + text_resource = types.TextResourceContents( + uri=AnyUrl("file://test.txt"), mimeType="text/plain", text="Embedded text content" + ) + mcp_content = types.EmbeddedResource(type="resource", resource=text_resource) + ai_content = _mcp_type_to_ai_content(mcp_content) + + assert isinstance(ai_content, TextContent) + assert ai_content.text == "Embedded text content" + assert ai_content.raw_representation == mcp_content + + +def test_mcp_content_types_to_ai_content_embedded_resource_blob(): + """Test conversion of MCP embedded blob resource to AI content.""" + # Use a proper data URI in the blob field since that's what the MCP implementation expects + blob_resource = types.BlobResourceContents( + uri=AnyUrl("file://test.bin"), + mimeType="application/octet-stream", + blob="data:application/octet-stream;base64,dGVzdCBkYXRh", + ) + mcp_content = types.EmbeddedResource(type="resource", resource=blob_resource) + ai_content = _mcp_type_to_ai_content(mcp_content) + + assert isinstance(ai_content, DataContent) + assert ai_content.uri == "data:application/octet-stream;base64,dGVzdCBkYXRh" + assert ai_content.media_type == "application/octet-stream" + assert ai_content.raw_representation == mcp_content + + +def test_ai_content_to_mcp_content_types_text(): + """Test conversion of AI text content to MCP content.""" + ai_content = TextContent(text="Sample text") + mcp_content = _ai_content_to_mcp_types(ai_content) + + assert isinstance(mcp_content, types.TextContent) + assert mcp_content.type == "text" + assert mcp_content.text == "Sample text" + + +def test_ai_content_to_mcp_content_types_data_image(): + """Test conversion of AI data content to MCP content.""" + ai_content = DataContent(uri="data:image/png;base64,xyz", media_type="image/png") + mcp_content = _ai_content_to_mcp_types(ai_content) + + assert isinstance(mcp_content, types.ImageContent) + assert mcp_content.type == "image" + assert mcp_content.data == "data:image/png;base64,xyz" + assert mcp_content.mimeType == "image/png" + + +def test_ai_content_to_mcp_content_types_data_audio(): + """Test conversion of AI data content to MCP content.""" + ai_content = DataContent(uri="data:audio/mpeg;base64,xyz", media_type="audio/mpeg") + mcp_content = _ai_content_to_mcp_types(ai_content) + + assert isinstance(mcp_content, types.AudioContent) + assert mcp_content.type == "audio" + assert mcp_content.data == "data:audio/mpeg;base64,xyz" + assert mcp_content.mimeType == "audio/mpeg" + + +def test_ai_content_to_mcp_content_types_data_binary(): + """Test conversion of AI data content to MCP content.""" + ai_content = DataContent(uri="data:application/octet-stream;base64,xyz", media_type="application/octet-stream") + mcp_content = _ai_content_to_mcp_types(ai_content) + + assert isinstance(mcp_content, types.EmbeddedResource) + assert mcp_content.type == "resource" + assert mcp_content.resource.blob == "data:application/octet-stream;base64,xyz" + assert mcp_content.resource.mimeType == "application/octet-stream" + + +def test_ai_content_to_mcp_content_types_uri(): + """Test conversion of AI URI content to MCP content.""" + ai_content = UriContent(uri="https://example.com/resource", media_type="application/json") + mcp_content = _ai_content_to_mcp_types(ai_content) + + assert isinstance(mcp_content, types.ResourceLink) + assert mcp_content.type == "resource_link" + assert str(mcp_content.uri) == "https://example.com/resource" + assert mcp_content.mimeType == "application/json" + + +def test_chat_message_to_mcp_types(): + message = ChatMessage( + role="user", + contents=[TextContent(text="test"), DataContent(uri="data:image/png;base64,xyz", media_type="image/png")], + ) + mcp_contents = _chat_message_to_mcp_types(message) + assert len(mcp_contents) == 2 + assert isinstance(mcp_contents[0], types.TextContent) + assert isinstance(mcp_contents[1], types.ImageContent) + + +def test_get_input_model_from_mcp_tool(): + """Test creation of input model from MCP tool.""" + tool = types.Tool( + name="test_tool", + description="A test tool", + inputSchema={ + "type": "object", + "properties": {"param1": {"type": "string"}, "param2": {"type": "number"}}, + "required": ["param1"], + }, + ) + model = _get_input_model_from_mcp_tool(tool) + + # Create an instance to verify the model works + instance = model(param1="test", param2=42) + assert instance.param1 == "test" + assert instance.param2 == 42 + + # Test validation + with pytest.raises(ValidationError): # Missing required param1 + model(param2=42) + + +def test_get_input_model_from_mcp_prompt(): + """Test creation of input model from MCP prompt.""" + prompt = types.Prompt( + name="test_prompt", + description="A test prompt", + arguments=[ + types.PromptArgument(name="arg1", description="First argument", required=True), + types.PromptArgument(name="arg2", description="Second argument", required=False), + ], + ) + model = _get_input_model_from_mcp_prompt(prompt) + + # Create an instance to verify the model works + instance = model(arg1="test", arg2="optional") + assert instance.arg1 == "test" + assert instance.arg2 == "optional" + + # Test validation + with pytest.raises(ValidationError): # Missing required arg1 + model(arg2="optional") + + +# McpTool tests +async def test_local_mcp_server_initialization(): + """Test McpTool initialization.""" + server = McpTool(name="test_server") + assert isinstance(server, AITool) + assert server.name == "test_server" + assert server.session is None + assert server.functions == [] + + +async def test_local_mcp_server_context_manager(): + """Test McpTool as context manager.""" + + class TestServer(McpTool): + async def connect(self): + # Mock connection + self.session = Mock(spec=ClientSession) + + def get_mcp_client(self) -> _AsyncGeneratorContextManager[Any, None]: + return None + + server = TestServer(name="test_server") + async with server: + assert server.session is not None + + assert server.session is None + + +async def test_local_mcp_server_load_functions(): + """Test loading functions from MCP server.""" + + class TestServer(McpTool): + async def connect(self): + self.session = Mock(spec=ClientSession) + # Mock tools list response + self.session.list_tools = AsyncMock( + return_value=types.ListToolsResult( + tools=[ + types.Tool( + name="test_tool", + description="Test tool", + inputSchema={ + "type": "object", + "properties": {"param": {"type": "string"}}, + "required": ["param"], + }, + ) + ] + ) + ) + + def get_mcp_client(self) -> _AsyncGeneratorContextManager[Any, None]: + return None + + server = TestServer(name="test_server") + assert isinstance(server, AITool) + async with server: + await server.load_tools() + assert len(server.functions) == 1 + assert server.functions[0].name == "test_tool" + + +async def test_local_mcp_server_load_prompts(): + """Test loading prompts from MCP server.""" + + class TestServer(McpTool): + async def connect(self): + self.session = Mock(spec=ClientSession) + # Mock prompts list response + self.session.list_prompts = AsyncMock( + return_value=types.ListPromptsResult( + prompts=[ + types.Prompt( + name="test_prompt", + description="Test prompt", + arguments=[types.PromptArgument(name="arg", description="Test arg", required=True)], + ) + ] + ) + ) + + def get_mcp_client(self) -> _AsyncGeneratorContextManager[Any, None]: + return None + + server = TestServer(name="test_server") + async with server: + await server.load_prompts() + assert len(server.functions) == 1 + assert server.functions[0].name == "test_prompt" + + +async def test_local_mcp_server_function_execution(): + """Test function execution through MCP server.""" + + class TestServer(McpTool): + async def connect(self): + self.session = Mock(spec=ClientSession) + self.session.list_tools = AsyncMock( + return_value=types.ListToolsResult( + tools=[ + types.Tool( + name="test_tool", + description="Test tool", + inputSchema={ + "type": "object", + "properties": {"param": {"type": "string"}}, + "required": ["param"], + }, + ) + ] + ) + ) + self.session.call_tool = AsyncMock( + return_value=types.CallToolResult( + content=[types.TextContent(type="text", text="Tool executed successfully")] + ) + ) + + def get_mcp_client(self) -> _AsyncGeneratorContextManager[Any, None]: + return None + + server = TestServer(name="test_server") + async with server: + await server.load_tools() + func = server.functions[0] + result = await func.invoke(param="test_value") + + assert len(result) == 1 + assert isinstance(result[0], TextContent) + assert result[0].text == "Tool executed successfully" + + +async def test_local_mcp_server_function_execution_error(): + """Test function execution error handling.""" + + class TestServer(McpTool): + async def connect(self): + self.session = Mock(spec=ClientSession) + self.session.list_tools = AsyncMock( + return_value=types.ListToolsResult( + tools=[ + types.Tool( + name="test_tool", + description="Test tool", + inputSchema={ + "type": "object", + "properties": {"param": {"type": "string"}}, + "required": ["param"], + }, + ) + ] + ) + ) + # Mock a tool call that raises an MCP error + self.session.call_tool = AsyncMock( + side_effect=McpError(types.ErrorData(code=-1, message="Tool execution failed")) + ) + + def get_mcp_client(self) -> _AsyncGeneratorContextManager[Any, None]: + return None + + server = TestServer(name="test_server") + async with server: + await server.load_tools() + func = server.functions[0] + + with pytest.raises(ToolExecutionException): + await func.invoke(param="test_value") + + +async def test_local_mcp_server_prompt_execution(): + """Test prompt execution through MCP server.""" + + class TestMcpTool(McpTool): + async def connect(self): + self.session = Mock(spec=ClientSession) + self.session.list_prompts = AsyncMock( + return_value=types.ListPromptsResult( + prompts=[ + types.Prompt( + name="test_prompt", + description="Test prompt", + arguments=[types.PromptArgument(name="arg", description="Test arg", required=True)], + ) + ] + ) + ) + self.session.get_prompt = AsyncMock( + return_value=types.GetPromptResult( + description="Generated prompt", + messages=[ + types.PromptMessage(role="user", content=types.TextContent(type="text", text="Test message")) + ], + ) + ) + + def get_mcp_client(self) -> _AsyncGeneratorContextManager[Any, None]: + return None + + server = TestMcpTool(name="test_server") + async with server: + await server.load_prompts() + prompt = server.functions[0] + result = await prompt.invoke(arg="test_value") + + assert len(result) == 1 + assert isinstance(result[0], ChatMessage) + assert result[0].role == ChatRole.USER + assert len(result[0].contents) == 1 + assert result[0].contents[0].text == "Test message" + + +# Server implementation tests +def test_local_mcp_stdio_tool_init(): + """Test McpStdioTool initialization.""" + tool = McpStdioTool(name="test", command="echo", args=["hello"]) + assert tool.name == "test" + assert tool.command == "echo" + assert tool.args == ["hello"] + + +def test_local_mcp_sse_tools_init(): + """Test McpSseTools initialization.""" + tool = McpSseTools(name="test", url="http://localhost:8080") + assert tool.name == "test" + assert tool.url == "http://localhost:8080" + + +def test_local_mcp_websocket_tool_init(): + """Test McpWebsocketTool initialization.""" + tool = McpWebsocketTool(name="test", url="ws://localhost:8080") + assert tool.name == "test" + assert tool.url == "ws://localhost:8080" + + +def test_local_mcp_streamable_http_tool_init(): + """Test McpStreamableHttpTool initialization.""" + tool = McpStreamableHttpTool(name="test", url="http://localhost:8080") + assert tool.name == "test" + assert tool.url == "http://localhost:8080" + + +# Integration test +@skip_if_mcp_integration_tests_disabled +async def test_streamable_http_integration(): + """Test MCP StreamableHTTP integration.""" + url = os.environ.get("LOCAL_MCP_URL", "") + if not url.startswith("http"): + pytest.skip("LOCAL_MCP_URL is not an HTTP URL") + + tool = McpStreamableHttpTool(name="integration_test", url=url) + + async with tool: + # Test that we can connect and load tools + assert tool.session is not None + assert isinstance(tool.functions, list) + + # If there are functions available, try to get information about one + assert tool.functions, "The MCP server should have at least one function." + + func = tool.functions[0] + + assert hasattr(func, "name") + assert hasattr(func, "description") + + result = await func.invoke(query="What is Agent Framework?") + assert result[0].text is not None diff --git a/python/packages/main/tests/main/test_types.py b/python/packages/main/tests/main/test_types.py index d92ecd3423..3e3ee74556 100644 --- a/python/packages/main/tests/main/test_types.py +++ b/python/packages/main/tests/main/test_types.py @@ -31,7 +31,6 @@ from agent_framework import ( HostedFileContent, HostedVectorStoreContent, SpeechToTextOptions, - StructuredResponse, TextContent, TextReasoningContent, TextSpanRegion, @@ -472,27 +471,44 @@ def test_chat_response(): assert str(response) == response.text -# region StructuredResponse +class OutputModel(BaseModel): + response: str -def test_structured_response(): - """Test the StructuredResponse class to ensure it initializes correctly with a value.""" +def test_chat_response_with_format(): + """Test the ChatResponse class to ensure it initializes correctly with a message.""" + # Create a ChatMessage + message = ChatMessage(role="assistant", text='{"response": "Hello"}') - class ResponseModel(BaseModel): - content: str - action: str - - # Create a StructuredResponse with a value - response = StructuredResponse[ResponseModel]( - value=ResponseModel(content="Hello, world!", action="test"), - text="{'content': 'Hello, world!', 'action': 'test'}", - ) + # Create a ChatResponse with the message + response = ChatResponse(messages=message) # Check the type and content - assert response.value == ResponseModel(content="Hello, world!", action="test") - assert isinstance(response, StructuredResponse) - # text property returns joined messages text (single message present) - assert isinstance(response.text, str) + assert response.messages[0].role == ChatRole.ASSISTANT + assert response.messages[0].text == '{"response": "Hello"}' + assert isinstance(response.messages[0], ChatMessage) + assert response.text == '{"response": "Hello"}' + assert response.value is None + response.try_parse_value(OutputModel) + assert response.value is not None + assert response.value.response == "Hello" + + +def test_chat_response_with_format_init(): + """Test the ChatResponse class to ensure it initializes correctly with a message.""" + # Create a ChatMessage + message = ChatMessage(role="assistant", text='{"response": "Hello"}') + + # Create a ChatResponse with the message + response = ChatResponse(messages=message, response_format=OutputModel) + + # Check the type and content + assert response.messages[0].role == ChatRole.ASSISTANT + assert response.messages[0].text == '{"response": "Hello"}' + assert isinstance(response.messages[0], ChatMessage) + assert response.text == '{"response": "Hello"}' + assert response.value is not None + assert response.value.response == "Hello" # region ChatResponseUpdate @@ -636,6 +652,32 @@ async def test_chat_response_from_async_generator(): assert resp.text == "Hello world" +@mark.asyncio +async def test_chat_response_from_async_generator_output_format(): + async def gen() -> AsyncIterable[ChatResponseUpdate]: + yield ChatResponseUpdate(text='{ "respon', message_id="1") + yield ChatResponseUpdate(text='se": "Hello" }', message_id="1") + + resp = await ChatResponse.from_chat_response_generator(gen()) + assert resp.text == '{ "response": "Hello" }' + assert resp.value is None + resp.try_parse_value(OutputModel) + assert resp.value is not None + assert resp.value.response == "Hello" + + +@mark.asyncio +async def test_chat_response_from_async_generator_output_format_in_method(): + async def gen() -> AsyncIterable[ChatResponseUpdate]: + yield ChatResponseUpdate(text='{ "respon', message_id="1") + yield ChatResponseUpdate(text='se": "Hello" }', message_id="1") + + resp = await ChatResponse.from_chat_response_generator(gen(), output_format_type=OutputModel) + assert resp.text == '{ "response": "Hello" }' + assert resp.value is not None + assert resp.value.response == "Hello" + + # region ChatToolMode diff --git a/python/pyproject.toml b/python/pyproject.toml index b42e390638..b63d471ac0 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -174,7 +174,7 @@ docs-serve = "sphinx-autobuild --watch docs/agent-framework docs/build --port 80 docs-check = "sphinx-build --fail-on-warning docs/agent-framework docs/build" docs-check-examples = "sphinx-build -b code_lint docs/agent-framework docs/build" pre-commit-install = "uv run pre-commit install --install-hooks --overwrite" -install = "uv sync --all-packages --dev -U --prerelease=if-necessary-or-explicit" +install = "uv sync --all-packages --all-extras --dev -U --prerelease=if-necessary-or-explicit" test = "python run_tasks_in_packages_if_exists.py test" fmt = "python run_tasks_in_packages_if_exists.py fmt" format.ref = "fmt" diff --git a/python/samples/getting_started/agents/openai_chat_client/openai_chat_client_with_local_mcp.py b/python/samples/getting_started/agents/openai_chat_client/openai_chat_client_with_local_mcp.py new file mode 100644 index 0000000000..e5f2db1589 --- /dev/null +++ b/python/samples/getting_started/agents/openai_chat_client/openai_chat_client_with_local_mcp.py @@ -0,0 +1,76 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio + +from agent_framework import ChatClientAgent, McpStreamableHttpTool +from agent_framework.openai import OpenAIChatClient + + +async def mcp_tools_on_run_level() -> None: + """Example showing MCP tools defined when running the agent.""" + print("=== Tools Defined on Run Level ===") + + # Tools are provided when running the agent + # This means we have to ensure we connect to the MCP server before running the agent + # and pass the tools to the run method. + async with ( + McpStreamableHttpTool( + name="Microsoft Learn MCP", + url="https://learn.microsoft.com/api/mcp", + ) as mcp_server, + ChatClientAgent( + chat_client=OpenAIChatClient(), + name="DocsAgent", + instructions="You are a helpful assistant that can help with microsoft documentation questions.", + ) as agent, + ): + # First query + query1 = "How to create an Azure storage account using az cli?" + print(f"User: {query1}") + result1 = await agent.run(query1, tools=mcp_server) + print(f"{agent.name}: {result1}\n") + print("\n=======================================\n") + # Second query + query2 = "What is Microsoft Semantic Kernel?" + print(f"User: {query2}") + result2 = await agent.run(query2, tools=mcp_server) + print(f"{agent.name}: {result2}\n") + + +async def mcp_tools_on_agent_level() -> None: + """Example showing tools defined when creating the agent.""" + print("=== Tools Defined on Agent Level ===") + + # Tools are provided when creating the agent + # The agent can use these tools for any query during its lifetime + # The agent will connect to the MCP server through its context manager. + async with OpenAIChatClient().create_agent( + name="DocsAgent", + instructions="You are a helpful assistant that can help with microsoft documentation questions.", + tools=McpStreamableHttpTool( # Tools defined at agent creation + name="Microsoft Learn MCP", + url="https://learn.microsoft.com/api/mcp", + ), + ) as agent: + # First query + query1 = "How to create an Azure storage account using az cli?" + print(f"User: {query1}") + result1 = await agent.run(query1) + print(f"{agent.name}: {result1}\n") + print("\n=======================================\n") + # Second query + query2 = "What is Microsoft Semantic Kernel?" + print(f"User: {query2}") + result2 = await agent.run(query2) + print(f"{agent.name}: {result2}\n") + + +async def main() -> None: + print("=== OpenAI Chat Client Agent with MCP Tools Examples ===\n") + + await mcp_tools_on_agent_level() + await mcp_tools_on_run_level() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/python/samples/getting_started/agents/openai_responses_client/openai_responses_client_with_local_mcp.py b/python/samples/getting_started/agents/openai_responses_client/openai_responses_client_with_local_mcp.py new file mode 100644 index 0000000000..c6a7ea1ce3 --- /dev/null +++ b/python/samples/getting_started/agents/openai_responses_client/openai_responses_client_with_local_mcp.py @@ -0,0 +1,87 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio + +from agent_framework import ChatClientAgent, McpStreamableHttpTool +from agent_framework.openai import OpenAIResponsesClient + + +async def streaming_with_mcp(show_raw_stream: bool = False) -> None: + """Example showing tools defined when creating the agent. + + If you want to access the full stream of events that has come from the model, you can access it, + through the raw_representation. You can view this, by setting the show_raw_stream parameter to True. + """ + print("=== Tools Defined on Agent Level ===") + + # Tools are provided when creating the agent + # The agent can use these tools for any query during its lifetime + async with ChatClientAgent( + chat_client=OpenAIResponsesClient(), + name="DocsAgent", + instructions="You are a helpful assistant that can help with microsoft documentation questions.", + tools=McpStreamableHttpTool( # Tools defined at agent creation + name="Microsoft Learn MCP", + url="https://learn.microsoft.com/api/mcp", + ), + ) as agent: + # First query + query1 = "How to create an Azure storage account using az cli?" + print(f"User: {query1}") + print(f"{agent.name}: ", end="") + async for chunk in agent.run_streaming(query1): + if show_raw_stream: + print("Streamed event: ", chunk.raw_representation.raw_representation) # type:ignore + elif chunk.text: + print(chunk.text, end="") + print("") + print("\n=======================================\n") + # Second query + query2 = "What is Microsoft Semantic Kernel?" + print(f"User: {query2}") + print(f"{agent.name}: ", end="") + async for chunk in agent.run_streaming(query2): + if show_raw_stream: + print("Streamed event: ", chunk.raw_representation.raw_representation) # type:ignore + elif chunk.text: + print(chunk.text, end="") + print("\n\n") + + +async def run_with_mcp() -> None: + """Example showing tools defined when creating the agent.""" + print("=== Tools Defined on Agent Level ===") + + # Tools are provided when creating the agent + # The agent can use these tools for any query during its lifetime + async with ChatClientAgent( + chat_client=OpenAIResponsesClient(), + name="DocsAgent", + instructions="You are a helpful assistant that can help with microsoft documentation questions.", + tools=McpStreamableHttpTool( # Tools defined at agent creation + name="Microsoft Learn MCP", + url="https://learn.microsoft.com/api/mcp", + ), + ) as agent: + # First query + query1 = "How to create an Azure storage account using az cli?" + print(f"User: {query1}") + result1 = await agent.run(query1) + print(f"{agent.name}: {result1}\n") + print("\n=======================================\n") + # Second query + query2 = "What is Microsoft Semantic Kernel?" + print(f"User: {query2}") + result2 = await agent.run(query2) + print(f"{agent.name}: {result2}\n") + + +async def main() -> None: + print("=== OpenAI Responses Client Agent with Function Tools Examples ===\n") + + await run_with_mcp() + await streaming_with_mcp() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/python/samples/getting_started/chat_client/azure_responses_client.py b/python/samples/getting_started/chat_client/azure_responses_client.py index e684fb1e03..08df71c48b 100644 --- a/python/samples/getting_started/chat_client/azure_responses_client.py +++ b/python/samples/getting_started/chat_client/azure_responses_client.py @@ -4,9 +4,10 @@ import asyncio from random import randint from typing import Annotated +from agent_framework import ChatResponse from agent_framework.azure import AzureResponsesClient from azure.identity import DefaultAzureCredential -from pydantic import Field +from pydantic import BaseModel, Field def get_weather( @@ -17,20 +18,28 @@ def get_weather( return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C." +class OutputStruct(BaseModel): + """Structured output for weather information.""" + + location: str + weather: str + + async def main() -> None: client = AzureResponsesClient(ad_credential=DefaultAzureCredential()) message = "What's the weather in Amsterdam and in Paris?" - stream = False + stream = True print(f"User: {message}") if stream: - print("Assistant: ", end="") - async for chunk in client.get_streaming_response(message, tools=get_weather): - if str(chunk): - print(str(chunk), end="") - print("") + response = await ChatResponse.from_chat_response_generator( + client.get_streaming_response(message, tools=get_weather, response_format=OutputStruct), + output_format_type=OutputStruct, + ) + print(f"Assistant: {response.value}") + else: - response = await client.get_response(message, tools=get_weather) - print(f"Assistant: {response}") + response = await client.get_response(message, tools=get_weather, response_format=OutputStruct) + print(f"Assistant: {response.value}") if __name__ == "__main__": diff --git a/python/uv.lock b/python/uv.lock index 5a8c96e4df..7473b38dab 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -41,6 +41,7 @@ name = "agent-framework" version = "0.1.0b1" source = { editable = "packages/main" } dependencies = [ + { name = "mcp", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "openai", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "opentelemetry-sdk", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -65,6 +66,7 @@ requires-dist = [ { name = "agent-framework-azure", marker = "extra == 'azure'", editable = "packages/azure" }, { name = "agent-framework-foundry", marker = "extra == 'foundry'", editable = "packages/foundry" }, { name = "agent-framework-workflow", marker = "extra == 'workflow'", editable = "packages/workflow" }, + { name = "mcp", specifier = ">=1.12" }, { name = "openai", specifier = ">=1.94.0" }, { name = "opentelemetry-api", specifier = "~=1.24" }, { name = "opentelemetry-sdk", specifier = "~=1.24" }, @@ -407,16 +409,16 @@ wheels = [ [[package]] name = "azure-ai-agents" -version = "1.2.0b1" +version = "1.2.0b2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "isodate", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ed/70/0aa275a7eecead1691bd86474514bc28787f815c37d1d79ac78be03a7612/azure_ai_agents-1.2.0b1.tar.gz", hash = "sha256:914e08e553ea4379d41ad60dbc8ea5468311d97f0ae1a362686229b8565ab8dd", size = 339933, upload-time = "2025-08-05T22:21:07.262Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/07/97eb5d1355abbd572c187789ae6c17d36dfcb3a9a1fae002e660d2663bf6/azure_ai_agents-1.2.0b2.tar.gz", hash = "sha256:4d9d220c12e2b7741f67bd7ef35e4faa60de7da32c0ab2526fa0ce1b978c2537", size = 353885, upload-time = "2025-08-12T21:35:46.264Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/40/c2/4824f3cd3980f976c4dace59cb25ab1891b22626be5c80c4a96f0b9c0ba5/azure_ai_agents-1.2.0b1-py3-none-any.whl", hash = "sha256:c6862f2e6655072ee3f1f1489be2dc2bf6c0ad636ec4e7f33a5fca9cb5c8eadb", size = 202032, upload-time = "2025-08-05T22:21:08.668Z" }, + { url = "https://files.pythonhosted.org/packages/73/9d/59688d265026e84dfff39b26d24cdbce0b2a2466a5bed06e0874a2a58e90/azure_ai_agents-1.2.0b2-py3-none-any.whl", hash = "sha256:f82117029fcc1dbed24d6b6c94d7e60e6b75276c333329fcfd9238853c82020b", size = 204422, upload-time = "2025-08-12T21:35:48.057Z" }, ] [[package]] @@ -1117,6 +1119,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] +[[package]] +name = "httpx-sse" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/fa/66bd985dd0b7c109a3bcb89272ee0bfb7e2b4d06309ad7b38ff866734b2a/httpx_sse-0.4.1.tar.gz", hash = "sha256:8f44d34414bc7b21bf3602713005c5df4917884f76072479b21f68befa4ea26e", size = 12998, upload-time = "2025-06-24T13:21:05.71Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/0a/6269e3473b09aed2dab8aa1a600c70f31f00ae1349bee30658f7e358a159/httpx_sse-0.4.1-py3-none-any.whl", hash = "sha256:cba42174344c3a5b06f255ce65b350880f962d99ead85e776f23c6618a377a37", size = 8054, upload-time = "2025-06-24T13:21:04.772Z" }, +] + [[package]] name = "identify" version = "2.6.13" @@ -1549,6 +1560,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899, upload-time = "2024-04-15T13:44:43.265Z" }, ] +[[package]] +name = "mcp" +version = "1.12.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "httpx", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "httpx-sse", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "jsonschema", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pydantic-settings", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "python-multipart", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "starlette", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "uvicorn", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/31/88/f6cb7e7c260cd4b4ce375f2b1614b33ce401f63af0f49f7141a2e9bf0a45/mcp-1.12.4.tar.gz", hash = "sha256:0765585e9a3a5916a3c3ab8659330e493adc7bd8b2ca6120c2d7a0c43e034ca5", size = 431148, upload-time = "2025-08-07T20:31:18.082Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/68/316cbc54b7163fa22571dcf42c9cc46562aae0a021b974e0a8141e897200/mcp-1.12.4-py3-none-any.whl", hash = "sha256:7aa884648969fab8e78b89399d59a683202972e12e6bc9a1c88ce7eda7743789", size = 160145, upload-time = "2025-08-07T20:31:15.69Z" }, +] + [[package]] name = "mdit-py-plugins" version = "0.5.0" @@ -1843,7 +1876,7 @@ wheels = [ [[package]] name = "openai" -version = "1.99.6" +version = "1.99.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -1855,9 +1888,9 @@ dependencies = [ { name = "tqdm", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/11/45/38a87bd6949236db5ae3132f41d5861824702b149f86d2627d6900919103/openai-1.99.6.tar.gz", hash = "sha256:f48f4239b938ef187062f3d5199a05b69711d8b600b9a9b6a3853cd271799183", size = 505364, upload-time = "2025-08-09T15:20:54.438Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/d2/ef89c6f3f36b13b06e271d3cc984ddd2f62508a0972c1cbcc8485a6644ff/openai-1.99.9.tar.gz", hash = "sha256:f2082d155b1ad22e83247c3de3958eb4255b20ccf4a1de2e6681b6957b554e92", size = 506992, upload-time = "2025-08-12T02:31:10.054Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/dd/9aa956485c2856346b3181542fbb0aea4e5b457fa7a523944726746da8da/openai-1.99.6-py3-none-any.whl", hash = "sha256:e40d44b2989588c45ce13819598788b77b8fb80ba2f7ae95ce90d14e46f1bd26", size = 786296, upload-time = "2025-08-09T15:20:51.95Z" }, + { 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]] @@ -1890,7 +1923,7 @@ wheels = [ [[package]] name = "opentelemetry-instrumentation-openai" -version = "0.44.1" +version = "0.45.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -1898,9 +1931,9 @@ dependencies = [ { name = "opentelemetry-semantic-conventions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "opentelemetry-semantic-conventions-ai", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6a/80/57f2626f192586befee609e72447e32d9b531ecd81dfc3a8437887caca2d/opentelemetry_instrumentation_openai-0.44.1.tar.gz", hash = "sha256:86209011adcfffad01315523462489fac43fd242ed9fb2c416a49c732b0efc00", size = 23942, upload-time = "2025-08-04T08:40:11.957Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/fc/98e6c4247b1ea5648bec0139dbe14b4fe5b5dcfc5a9437260ac175cf3440/opentelemetry_instrumentation_openai-0.45.0.tar.gz", hash = "sha256:f3ef05a21130054610cb374cdf4d87a9854ba57eb7bfe5e36cd0e2f863e330d3", size = 24576, upload-time = "2025-08-12T16:16:41.39Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/13/4c/a8d0f3bea91e15857a1c702fcfe5d92c5feea878328a0991a0abf36a1eb0/opentelemetry_instrumentation_openai-0.44.1-py3-none-any.whl", hash = "sha256:f3e3b0d197b76ae941e406c8be8b2f116bf806c4c535af78358e318e6ce2ecc0", size = 33747, upload-time = "2025-08-04T08:39:40.779Z" }, + { url = "https://files.pythonhosted.org/packages/aa/7a/1556fe9e80462fc042ecb9dcaefdc039f70b2cb45237b1e909a6fe1b2e31/opentelemetry_instrumentation_openai-0.45.0-py3-none-any.whl", hash = "sha256:6818a407b1ee735ec083333af7e39bb017a3a6ffdc1123dbaf3b8aef40cdc6df", size = 34371, upload-time = "2025-08-12T16:16:14.248Z" }, ] [[package]] @@ -2007,16 +2040,16 @@ wheels = [ [[package]] name = "poethepoet" -version = "0.36.0" +version = "0.37.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pastel", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "tomli", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cf/ac/311c8a492dc887f0b7a54d0ec3324cb2f9538b7b78ea06e5f7ae1f167e52/poethepoet-0.36.0.tar.gz", hash = "sha256:2217b49cb4e4c64af0b42ff8c4814b17f02e107d38bc461542517348ede25663", size = 66854, upload-time = "2025-06-29T19:54:50.444Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/f2/273fe54a78dc5c6c8dd63db71f5a6ceb95e4648516b5aeaeff4bde804e44/poethepoet-0.37.0.tar.gz", hash = "sha256:73edf458707c674a079baa46802e21455bda3a7f82a408e58c31b9f4fe8e933d", size = 68570, upload-time = "2025-08-11T18:00:29.103Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/03/29/dedb3a6b7e17ea723143b834a2da428a7d743c80d5cd4d22ed28b5e8c441/poethepoet-0.36.0-py3-none-any.whl", hash = "sha256:693e3c1eae9f6731d3613c3c0c40f747d3c5c68a375beda42e590a63c5623308", size = 88031, upload-time = "2025-06-29T19:54:48.884Z" }, + { url = "https://files.pythonhosted.org/packages/92/1b/5337af1a6a478d25a3e3c56b9b4b42b0a160314e02f4a0498d5322c8dac4/poethepoet-0.37.0-py3-none-any.whl", hash = "sha256:861790276315abcc8df1b4bd60e28c3d48a06db273edd3092f3c94e1a46e5e22", size = 90062, upload-time = "2025-08-11T18:00:27.595Z" }, ] [[package]] @@ -2445,6 +2478,15 @@ wheels = [ { 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]] +name = "python-multipart" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, +] + [[package]] name = "pywin32" version = "311" @@ -3021,47 +3063,59 @@ wheels = [ [[package]] name = "sqlalchemy" -version = "2.0.42" +version = "2.0.43" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64' and sys_platform == 'darwin') or (python_full_version < '3.14' and platform_machine == 'WIN32' and sys_platform == 'darwin') or (python_full_version < '3.14' and platform_machine == 'aarch64' and sys_platform == 'darwin') or (python_full_version < '3.14' and platform_machine == 'amd64' and sys_platform == 'darwin') or (python_full_version < '3.14' and platform_machine == 'ppc64le' and sys_platform == 'darwin') or (python_full_version < '3.14' and platform_machine == 'win32' and sys_platform == 'darwin') or (python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.14' and platform_machine == 'AMD64' and sys_platform == 'linux') or (python_full_version < '3.14' and platform_machine == 'WIN32' and sys_platform == 'linux') or (python_full_version < '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.14' and platform_machine == 'amd64' and sys_platform == 'linux') or (python_full_version < '3.14' and platform_machine == 'ppc64le' and sys_platform == 'linux') or (python_full_version < '3.14' and platform_machine == 'win32' and sys_platform == 'linux') or (python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.14' and platform_machine == 'AMD64' and sys_platform == 'win32') or (python_full_version < '3.14' and platform_machine == 'WIN32' and sys_platform == 'win32') or (python_full_version < '3.14' and platform_machine == 'aarch64' and sys_platform == 'win32') or (python_full_version < '3.14' and platform_machine == 'amd64' and sys_platform == 'win32') or (python_full_version < '3.14' and platform_machine == 'ppc64le' and sys_platform == 'win32') or (python_full_version < '3.14' and platform_machine == 'win32' and sys_platform == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'win32')" }, { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5a/03/a0af991e3a43174d6b83fca4fb399745abceddd1171bdabae48ce877ff47/sqlalchemy-2.0.42.tar.gz", hash = "sha256:160bedd8a5c28765bd5be4dec2d881e109e33b34922e50a3b881a7681773ac5f", size = 9749972, upload-time = "2025-07-29T12:48:09.323Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/bc/d59b5d97d27229b0e009bd9098cd81af71c2fa5549c580a0a67b9bed0496/sqlalchemy-2.0.43.tar.gz", hash = "sha256:788bfcef6787a7764169cfe9859fe425bf44559619e1d9f56f5bddf2ebf6f417", size = 9762949, upload-time = "2025-08-11T14:24:58.438Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/12/33ff43214c2c6cc87499b402fe419869d2980a08101c991daae31345e901/sqlalchemy-2.0.42-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:172b244753e034d91a826f80a9a70f4cbac690641207f2217f8404c261473efe", size = 2130469, upload-time = "2025-07-29T13:25:15.215Z" }, - { url = "https://files.pythonhosted.org/packages/63/c4/4d2f2c21ddde9a2c7f7b258b202d6af0bac9fc5abfca5de367461c86d766/sqlalchemy-2.0.42-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:be28f88abd74af8519a4542185ee80ca914933ca65cdfa99504d82af0e4210df", size = 2120393, upload-time = "2025-07-29T13:25:16.367Z" }, - { url = "https://files.pythonhosted.org/packages/a8/0d/5ff2f2dfbac10e4a9ade1942f8985ffc4bd8f157926b1f8aed553dfe3b88/sqlalchemy-2.0.42-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98b344859d282fde388047f1710860bb23f4098f705491e06b8ab52a48aafea9", size = 3206173, upload-time = "2025-07-29T13:29:00.623Z" }, - { url = "https://files.pythonhosted.org/packages/1f/59/71493fe74bd76a773ae8fa0c50bfc2ccac1cbf7cfa4f9843ad92897e6dcf/sqlalchemy-2.0.42-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97978d223b11f1d161390a96f28c49a13ce48fdd2fed7683167c39bdb1b8aa09", size = 3206910, upload-time = "2025-07-29T13:24:50.58Z" }, - { url = "https://files.pythonhosted.org/packages/a9/51/01b1d85bbb492a36b25df54a070a0f887052e9b190dff71263a09f48576b/sqlalchemy-2.0.42-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e35b9b000c59fcac2867ab3a79fc368a6caca8706741beab3b799d47005b3407", size = 3145479, upload-time = "2025-07-29T13:29:02.3Z" }, - { url = "https://files.pythonhosted.org/packages/fa/78/10834f010e2a3df689f6d1888ea6ea0074ff10184e6a550b8ed7f9189a89/sqlalchemy-2.0.42-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bc7347ad7a7b1c78b94177f2d57263113bb950e62c59b96ed839b131ea4234e1", size = 3169605, upload-time = "2025-07-29T13:24:52.135Z" }, - { url = "https://files.pythonhosted.org/packages/0c/75/e6fdd66d237582c8488dd1dfa90899f6502822fbd866363ab70e8ac4a2ce/sqlalchemy-2.0.42-cp310-cp310-win32.whl", hash = "sha256:739e58879b20a179156b63aa21f05ccacfd3e28e08e9c2b630ff55cd7177c4f1", size = 2098759, upload-time = "2025-07-29T13:23:55.809Z" }, - { url = "https://files.pythonhosted.org/packages/a5/a8/366db192641c2c2d1ea8977e7c77b65a0d16a7858907bb76ea68b9dd37af/sqlalchemy-2.0.42-cp310-cp310-win_amd64.whl", hash = "sha256:1aef304ada61b81f1955196f584b9e72b798ed525a7c0b46e09e98397393297b", size = 2122423, upload-time = "2025-07-29T13:23:56.968Z" }, - { url = "https://files.pythonhosted.org/packages/ea/3c/7bfd65f3c2046e2fb4475b21fa0b9d7995f8c08bfa0948df7a4d2d0de869/sqlalchemy-2.0.42-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c34100c0b7ea31fbc113c124bcf93a53094f8951c7bf39c45f39d327bad6d1e7", size = 2133779, upload-time = "2025-07-29T13:25:18.446Z" }, - { url = "https://files.pythonhosted.org/packages/66/17/19be542fe9dd64a766090e90e789e86bdaa608affda6b3c1e118a25a2509/sqlalchemy-2.0.42-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ad59dbe4d1252448c19d171dfba14c74e7950b46dc49d015722a4a06bfdab2b0", size = 2123843, upload-time = "2025-07-29T13:25:19.749Z" }, - { url = "https://files.pythonhosted.org/packages/14/fc/83e45fc25f0acf1c26962ebff45b4c77e5570abb7c1a425a54b00bcfa9c7/sqlalchemy-2.0.42-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9187498c2149919753a7fd51766ea9c8eecdec7da47c1b955fa8090bc642eaa", size = 3294824, upload-time = "2025-07-29T13:29:03.879Z" }, - { url = "https://files.pythonhosted.org/packages/b9/81/421efc09837104cd1a267d68b470e5b7b6792c2963b8096ca1e060ba0975/sqlalchemy-2.0.42-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f092cf83ebcafba23a247f5e03f99f5436e3ef026d01c8213b5eca48ad6efa9", size = 3294662, upload-time = "2025-07-29T13:24:53.715Z" }, - { url = "https://files.pythonhosted.org/packages/2f/ba/55406e09d32ed5e5f9e8aaec5ef70c4f20b4ae25b9fa9784f4afaa28e7c3/sqlalchemy-2.0.42-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc6afee7e66fdba4f5a68610b487c1f754fccdc53894a9567785932dbb6a265e", size = 3229413, upload-time = "2025-07-29T13:29:05.638Z" }, - { url = "https://files.pythonhosted.org/packages/d4/c4/df596777fce27bde2d1a4a2f5a7ddea997c0c6d4b5246aafba966b421cc0/sqlalchemy-2.0.42-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:260ca1d2e5910f1f1ad3fe0113f8fab28657cee2542cb48c2f342ed90046e8ec", size = 3255563, upload-time = "2025-07-29T13:24:55.17Z" }, - { url = "https://files.pythonhosted.org/packages/16/ed/b9c4a939b314400f43f972c9eb0091da59d8466ef9c51d0fd5b449edc495/sqlalchemy-2.0.42-cp311-cp311-win32.whl", hash = "sha256:2eb539fd83185a85e5fcd6b19214e1c734ab0351d81505b0f987705ba0a1e231", size = 2098513, upload-time = "2025-07-29T13:23:58.946Z" }, - { url = "https://files.pythonhosted.org/packages/91/72/55b0c34e39feb81991aa3c974d85074c356239ac1170dfb81a474b4c23b3/sqlalchemy-2.0.42-cp311-cp311-win_amd64.whl", hash = "sha256:9193fa484bf00dcc1804aecbb4f528f1123c04bad6a08d7710c909750fa76aeb", size = 2123380, upload-time = "2025-07-29T13:24:00.155Z" }, - { url = "https://files.pythonhosted.org/packages/61/66/ac31a9821fc70a7376321fb2c70fdd7eadbc06dadf66ee216a22a41d6058/sqlalchemy-2.0.42-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:09637a0872689d3eb71c41e249c6f422e3e18bbd05b4cd258193cfc7a9a50da2", size = 2132203, upload-time = "2025-07-29T13:29:19.291Z" }, - { url = "https://files.pythonhosted.org/packages/fc/ba/fd943172e017f955d7a8b3a94695265b7114efe4854feaa01f057e8f5293/sqlalchemy-2.0.42-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3cb3ec67cc08bea54e06b569398ae21623534a7b1b23c258883a7c696ae10df", size = 2120373, upload-time = "2025-07-29T13:29:21.049Z" }, - { url = "https://files.pythonhosted.org/packages/ea/a2/b5f7d233d063ffadf7e9fff3898b42657ba154a5bec95a96f44cba7f818b/sqlalchemy-2.0.42-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e87e6a5ef6f9d8daeb2ce5918bf5fddecc11cae6a7d7a671fcc4616c47635e01", size = 3317685, upload-time = "2025-07-29T13:26:40.837Z" }, - { url = "https://files.pythonhosted.org/packages/86/00/fcd8daab13a9119d41f3e485a101c29f5d2085bda459154ba354c616bf4e/sqlalchemy-2.0.42-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b718011a9d66c0d2f78e1997755cd965f3414563b31867475e9bc6efdc2281d", size = 3326967, upload-time = "2025-07-29T13:22:31.009Z" }, - { url = "https://files.pythonhosted.org/packages/a3/85/e622a273d648d39d6771157961956991a6d760e323e273d15e9704c30ccc/sqlalchemy-2.0.42-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:16d9b544873fe6486dddbb859501a07d89f77c61d29060bb87d0faf7519b6a4d", size = 3255331, upload-time = "2025-07-29T13:26:42.579Z" }, - { url = "https://files.pythonhosted.org/packages/3a/a0/2c2338b592c7b0a61feffd005378c084b4c01fabaf1ed5f655ab7bd446f0/sqlalchemy-2.0.42-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:21bfdf57abf72fa89b97dd74d3187caa3172a78c125f2144764a73970810c4ee", size = 3291791, upload-time = "2025-07-29T13:22:32.454Z" }, - { url = "https://files.pythonhosted.org/packages/41/19/b8a2907972a78285fdce4c880ecaab3c5067eb726882ca6347f7a4bf64f6/sqlalchemy-2.0.42-cp312-cp312-win32.whl", hash = "sha256:78b46555b730a24901ceb4cb901c6b45c9407f8875209ed3c5d6bcd0390a6ed1", size = 2096180, upload-time = "2025-07-29T13:16:08.952Z" }, - { url = "https://files.pythonhosted.org/packages/48/1f/67a78f3dfd08a2ed1c7be820fe7775944f5126080b5027cc859084f8e223/sqlalchemy-2.0.42-cp312-cp312-win_amd64.whl", hash = "sha256:4c94447a016f36c4da80072e6c6964713b0af3c8019e9c4daadf21f61b81ab53", size = 2123533, upload-time = "2025-07-29T13:16:11.705Z" }, - { url = "https://files.pythonhosted.org/packages/e9/7e/25d8c28b86730c9fb0e09156f601d7a96d1c634043bf8ba36513eb78887b/sqlalchemy-2.0.42-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:941804f55c7d507334da38133268e3f6e5b0340d584ba0f277dd884197f4ae8c", size = 2127905, upload-time = "2025-07-29T13:29:22.249Z" }, - { url = "https://files.pythonhosted.org/packages/e5/a1/9d8c93434d1d983880d976400fcb7895a79576bd94dca61c3b7b90b1ed0d/sqlalchemy-2.0.42-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:95d3d06a968a760ce2aa6a5889fefcbdd53ca935735e0768e1db046ec08cbf01", size = 2115726, upload-time = "2025-07-29T13:29:23.496Z" }, - { url = "https://files.pythonhosted.org/packages/a2/cc/d33646fcc24c87cc4e30a03556b611a4e7bcfa69a4c935bffb923e3c89f4/sqlalchemy-2.0.42-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cf10396a8a700a0f38ccd220d940be529c8f64435c5d5b29375acab9267a6c9", size = 3246007, upload-time = "2025-07-29T13:26:44.166Z" }, - { url = "https://files.pythonhosted.org/packages/67/08/4e6c533d4c7f5e7c4cbb6fe8a2c4e813202a40f05700d4009a44ec6e236d/sqlalchemy-2.0.42-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9cae6c2b05326d7c2c7c0519f323f90e0fb9e8afa783c6a05bb9ee92a90d0f04", size = 3250919, upload-time = "2025-07-29T13:22:33.74Z" }, - { url = "https://files.pythonhosted.org/packages/5c/82/f680e9a636d217aece1b9a8030d18ad2b59b5e216e0c94e03ad86b344af3/sqlalchemy-2.0.42-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f50f7b20677b23cfb35b6afcd8372b2feb348a38e3033f6447ee0704540be894", size = 3180546, upload-time = "2025-07-29T13:26:45.648Z" }, - { url = "https://files.pythonhosted.org/packages/7d/a2/8c8f6325f153894afa3775584c429cc936353fb1db26eddb60a549d0ff4b/sqlalchemy-2.0.42-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d88a1c0d66d24e229e3938e1ef16ebdbd2bf4ced93af6eff55225f7465cf350", size = 3216683, upload-time = "2025-07-29T13:22:34.977Z" }, - { url = "https://files.pythonhosted.org/packages/39/44/3a451d7fa4482a8ffdf364e803ddc2cfcafc1c4635fb366f169ecc2c3b11/sqlalchemy-2.0.42-cp313-cp313-win32.whl", hash = "sha256:45c842c94c9ad546c72225a0c0d1ae8ef3f7c212484be3d429715a062970e87f", size = 2093990, upload-time = "2025-07-29T13:16:13.036Z" }, - { url = "https://files.pythonhosted.org/packages/4b/9e/9bce34f67aea0251c8ac104f7bdb2229d58fb2e86a4ad8807999c4bee34b/sqlalchemy-2.0.42-cp313-cp313-win_amd64.whl", hash = "sha256:eb9905f7f1e49fd57a7ed6269bc567fcbbdac9feadff20ad6bd7707266a91577", size = 2120473, upload-time = "2025-07-29T13:16:14.502Z" }, - { url = "https://files.pythonhosted.org/packages/ee/55/ba2546ab09a6adebc521bf3974440dc1d8c06ed342cceb30ed62a8858835/sqlalchemy-2.0.42-py3-none-any.whl", hash = "sha256:defcdff7e661f0043daa381832af65d616e060ddb54d3fe4476f51df7eaa1835", size = 1922072, upload-time = "2025-07-29T13:09:17.061Z" }, + { url = "https://files.pythonhosted.org/packages/8f/4e/985f7da36f09592c5ade99321c72c15101d23c0bb7eecfd1daaca5714422/sqlalchemy-2.0.43-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:70322986c0c699dca241418fcf18e637a4369e0ec50540a2b907b184c8bca069", size = 2133162, upload-time = "2025-08-11T15:52:17.854Z" }, + { url = "https://files.pythonhosted.org/packages/37/34/798af8db3cae069461e3bc0898a1610dc469386a97048471d364dc8aae1c/sqlalchemy-2.0.43-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:87accdbba88f33efa7b592dc2e8b2a9c2cdbca73db2f9d5c510790428c09c154", size = 2123082, upload-time = "2025-08-11T15:52:19.181Z" }, + { url = "https://files.pythonhosted.org/packages/fb/0f/79cf4d9dad42f61ec5af1e022c92f66c2d110b93bb1dc9b033892971abfa/sqlalchemy-2.0.43-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c00e7845d2f692ebfc7d5e4ec1a3fd87698e4337d09e58d6749a16aedfdf8612", size = 3208871, upload-time = "2025-08-11T15:50:30.656Z" }, + { url = "https://files.pythonhosted.org/packages/56/b3/59befa58fb0e1a9802c87df02344548e6d007e77e87e6084e2131c29e033/sqlalchemy-2.0.43-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:022e436a1cb39b13756cf93b48ecce7aa95382b9cfacceb80a7d263129dfd019", size = 3209583, upload-time = "2025-08-11T15:57:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/29/d2/124b50c0eb8146e8f0fe16d01026c1a073844f0b454436d8544fe9b33bd7/sqlalchemy-2.0.43-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c5e73ba0d76eefc82ec0219d2301cb33bfe5205ed7a2602523111e2e56ccbd20", size = 3148177, upload-time = "2025-08-11T15:50:32.078Z" }, + { url = "https://files.pythonhosted.org/packages/83/f5/e369cd46aa84278107624617034a5825fedfc5c958b2836310ced4d2eadf/sqlalchemy-2.0.43-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9c2e02f06c68092b875d5cbe4824238ab93a7fa35d9c38052c033f7ca45daa18", size = 3172276, upload-time = "2025-08-11T15:57:49.477Z" }, + { url = "https://files.pythonhosted.org/packages/de/2b/4602bf4c3477fa4c837c9774e6dd22e0389fc52310c4c4dfb7e7ba05e90d/sqlalchemy-2.0.43-cp310-cp310-win32.whl", hash = "sha256:e7a903b5b45b0d9fa03ac6a331e1c1d6b7e0ab41c63b6217b3d10357b83c8b00", size = 2101491, upload-time = "2025-08-11T15:54:59.191Z" }, + { url = "https://files.pythonhosted.org/packages/38/2d/bfc6b6143adef553a08295490ddc52607ee435b9c751c714620c1b3dd44d/sqlalchemy-2.0.43-cp310-cp310-win_amd64.whl", hash = "sha256:4bf0edb24c128b7be0c61cd17eef432e4bef507013292415f3fb7023f02b7d4b", size = 2125148, upload-time = "2025-08-11T15:55:00.593Z" }, + { url = "https://files.pythonhosted.org/packages/9d/77/fa7189fe44114658002566c6fe443d3ed0ec1fa782feb72af6ef7fbe98e7/sqlalchemy-2.0.43-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:52d9b73b8fb3e9da34c2b31e6d99d60f5f99fd8c1225c9dad24aeb74a91e1d29", size = 2136472, upload-time = "2025-08-11T15:52:21.789Z" }, + { url = "https://files.pythonhosted.org/packages/99/ea/92ac27f2fbc2e6c1766bb807084ca455265707e041ba027c09c17d697867/sqlalchemy-2.0.43-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f42f23e152e4545157fa367b2435a1ace7571cab016ca26038867eb7df2c3631", size = 2126535, upload-time = "2025-08-11T15:52:23.109Z" }, + { url = "https://files.pythonhosted.org/packages/94/12/536ede80163e295dc57fff69724caf68f91bb40578b6ac6583a293534849/sqlalchemy-2.0.43-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fb1a8c5438e0c5ea51afe9c6564f951525795cf432bed0c028c1cb081276685", size = 3297521, upload-time = "2025-08-11T15:50:33.536Z" }, + { url = "https://files.pythonhosted.org/packages/03/b5/cacf432e6f1fc9d156eca0560ac61d4355d2181e751ba8c0cd9cb232c8c1/sqlalchemy-2.0.43-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db691fa174e8f7036afefe3061bc40ac2b770718be2862bfb03aabae09051aca", size = 3297343, upload-time = "2025-08-11T15:57:51.186Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ba/d4c9b526f18457667de4c024ffbc3a0920c34237b9e9dd298e44c7c00ee5/sqlalchemy-2.0.43-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fe2b3b4927d0bc03d02ad883f402d5de201dbc8894ac87d2e981e7d87430e60d", size = 3232113, upload-time = "2025-08-11T15:50:34.949Z" }, + { url = "https://files.pythonhosted.org/packages/aa/79/c0121b12b1b114e2c8a10ea297a8a6d5367bc59081b2be896815154b1163/sqlalchemy-2.0.43-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4d3d9b904ad4a6b175a2de0738248822f5ac410f52c2fd389ada0b5262d6a1e3", size = 3258240, upload-time = "2025-08-11T15:57:52.983Z" }, + { url = "https://files.pythonhosted.org/packages/79/99/a2f9be96fb382f3ba027ad42f00dbe30fdb6ba28cda5f11412eee346bec5/sqlalchemy-2.0.43-cp311-cp311-win32.whl", hash = "sha256:5cda6b51faff2639296e276591808c1726c4a77929cfaa0f514f30a5f6156921", size = 2101248, upload-time = "2025-08-11T15:55:01.855Z" }, + { url = "https://files.pythonhosted.org/packages/ee/13/744a32ebe3b4a7a9c7ea4e57babae7aa22070d47acf330d8e5a1359607f1/sqlalchemy-2.0.43-cp311-cp311-win_amd64.whl", hash = "sha256:c5d1730b25d9a07727d20ad74bc1039bbbb0a6ca24e6769861c1aa5bf2c4c4a8", size = 2126109, upload-time = "2025-08-11T15:55:04.092Z" }, + { url = "https://files.pythonhosted.org/packages/61/db/20c78f1081446095450bdc6ee6cc10045fce67a8e003a5876b6eaafc5cc4/sqlalchemy-2.0.43-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:20d81fc2736509d7a2bd33292e489b056cbae543661bb7de7ce9f1c0cd6e7f24", size = 2134891, upload-time = "2025-08-11T15:51:13.019Z" }, + { url = "https://files.pythonhosted.org/packages/45/0a/3d89034ae62b200b4396f0f95319f7d86e9945ee64d2343dcad857150fa2/sqlalchemy-2.0.43-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b9fc27650ff5a2c9d490c13c14906b918b0de1f8fcbb4c992712d8caf40e83", size = 2123061, upload-time = "2025-08-11T15:51:14.319Z" }, + { url = "https://files.pythonhosted.org/packages/cb/10/2711f7ff1805919221ad5bee205971254845c069ee2e7036847103ca1e4c/sqlalchemy-2.0.43-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6772e3ca8a43a65a37c88e2f3e2adfd511b0b1da37ef11ed78dea16aeae85bd9", size = 3320384, upload-time = "2025-08-11T15:52:35.088Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0e/3d155e264d2ed2778484006ef04647bc63f55b3e2d12e6a4f787747b5900/sqlalchemy-2.0.43-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a113da919c25f7f641ffbd07fbc9077abd4b3b75097c888ab818f962707eb48", size = 3329648, upload-time = "2025-08-11T15:56:34.153Z" }, + { url = "https://files.pythonhosted.org/packages/5b/81/635100fb19725c931622c673900da5efb1595c96ff5b441e07e3dd61f2be/sqlalchemy-2.0.43-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4286a1139f14b7d70141c67a8ae1582fc2b69105f1b09d9573494eb4bb4b2687", size = 3258030, upload-time = "2025-08-11T15:52:36.933Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ed/a99302716d62b4965fded12520c1cbb189f99b17a6d8cf77611d21442e47/sqlalchemy-2.0.43-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:529064085be2f4d8a6e5fab12d36ad44f1909a18848fcfbdb59cc6d4bbe48efe", size = 3294469, upload-time = "2025-08-11T15:56:35.553Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a2/3a11b06715149bf3310b55a98b5c1e84a42cfb949a7b800bc75cb4e33abc/sqlalchemy-2.0.43-cp312-cp312-win32.whl", hash = "sha256:b535d35dea8bbb8195e7e2b40059e2253acb2b7579b73c1b432a35363694641d", size = 2098906, upload-time = "2025-08-11T15:55:00.645Z" }, + { url = "https://files.pythonhosted.org/packages/bc/09/405c915a974814b90aa591280623adc6ad6b322f61fd5cff80aeaef216c9/sqlalchemy-2.0.43-cp312-cp312-win_amd64.whl", hash = "sha256:1c6d85327ca688dbae7e2b06d7d84cfe4f3fffa5b5f9e21bb6ce9d0e1a0e0e0a", size = 2126260, upload-time = "2025-08-11T15:55:02.965Z" }, + { url = "https://files.pythonhosted.org/packages/41/1c/a7260bd47a6fae7e03768bf66451437b36451143f36b285522b865987ced/sqlalchemy-2.0.43-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e7c08f57f75a2bb62d7ee80a89686a5e5669f199235c6d1dac75cd59374091c3", size = 2130598, upload-time = "2025-08-11T15:51:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/8e/84/8a337454e82388283830b3586ad7847aa9c76fdd4f1df09cdd1f94591873/sqlalchemy-2.0.43-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:14111d22c29efad445cd5021a70a8b42f7d9152d8ba7f73304c4d82460946aaa", size = 2118415, upload-time = "2025-08-11T15:51:17.256Z" }, + { url = "https://files.pythonhosted.org/packages/cf/ff/22ab2328148492c4d71899d62a0e65370ea66c877aea017a244a35733685/sqlalchemy-2.0.43-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21b27b56eb2f82653168cefe6cb8e970cdaf4f3a6cb2c5e3c3c1cf3158968ff9", size = 3248707, upload-time = "2025-08-11T15:52:38.444Z" }, + { url = "https://files.pythonhosted.org/packages/dc/29/11ae2c2b981de60187f7cbc84277d9d21f101093d1b2e945c63774477aba/sqlalchemy-2.0.43-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c5a9da957c56e43d72126a3f5845603da00e0293720b03bde0aacffcf2dc04f", size = 3253602, upload-time = "2025-08-11T15:56:37.348Z" }, + { url = "https://files.pythonhosted.org/packages/b8/61/987b6c23b12c56d2be451bc70900f67dd7d989d52b1ee64f239cf19aec69/sqlalchemy-2.0.43-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d79f9fdc9584ec83d1b3c75e9f4595c49017f5594fee1a2217117647225d738", size = 3183248, upload-time = "2025-08-11T15:52:39.865Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/29d216002d4593c2ce1c0ec2cec46dda77bfbcd221e24caa6e85eff53d89/sqlalchemy-2.0.43-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9df7126fd9db49e3a5a3999442cc67e9ee8971f3cb9644250107d7296cb2a164", size = 3219363, upload-time = "2025-08-11T15:56:39.11Z" }, + { url = "https://files.pythonhosted.org/packages/b6/e4/bd78b01919c524f190b4905d47e7630bf4130b9f48fd971ae1c6225b6f6a/sqlalchemy-2.0.43-cp313-cp313-win32.whl", hash = "sha256:7f1ac7828857fcedb0361b48b9ac4821469f7694089d15550bbcf9ab22564a1d", size = 2096718, upload-time = "2025-08-11T15:55:05.349Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a5/ca2f07a2a201f9497de1928f787926613db6307992fe5cda97624eb07c2f/sqlalchemy-2.0.43-cp313-cp313-win_amd64.whl", hash = "sha256:971ba928fcde01869361f504fcff3b7143b47d30de188b11c6357c0505824197", size = 2123200, upload-time = "2025-08-11T15:55:07.932Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d9/13bdde6521f322861fab67473cec4b1cc8999f3871953531cf61945fad92/sqlalchemy-2.0.43-py3-none-any.whl", hash = "sha256:1681c21dd2ccee222c2fe0bef671d1aef7c504087c9c4e800371cfcc8ac966fc", size = 1924759, upload-time = "2025-08-11T15:39:53.024Z" }, +] + +[[package]] +name = "sse-starlette" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/6f/22ed6e33f8a9e76ca0a412405f31abb844b779d52c5f96660766edcd737c/sse_starlette-3.0.2.tar.gz", hash = "sha256:ccd60b5765ebb3584d0de2d7a6e4f745672581de4f5005ab31c3a25d10b52b3a", size = 20985, upload-time = "2025-07-27T09:07:44.565Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/10/c78f463b4ef22eef8491f218f692be838282cd65480f6e423d7730dfd1fb/sse_starlette-3.0.2-py3-none-any.whl", hash = "sha256:16b7cbfddbcd4eaca11f7b586f3b8a080f1afe952c15813455b162edea619e5a", size = 11297, upload-time = "2025-07-27T09:07:43.268Z" }, ] [[package]] @@ -3229,28 +3283,28 @@ wheels = [ [[package]] name = "uv" -version = "0.8.8" +version = "0.8.9" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9c/d0/4cd8ac2c7938da78c8e9ca791205f80e74b0f5a680f2a2d50323d54961d0/uv-0.8.8.tar.gz", hash = "sha256:6880e96cd994e53445d364206ddb4b2fff89fd2fbc74a74bef4a6f86384b07d9", size = 3477036, upload-time = "2025-08-09T00:26:00.883Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/a1/4dea87c10875b441d906f82df42d725a4a04c2e8ae720d9fa01e1f75e3dc/uv-0.8.9.tar.gz", hash = "sha256:54d76faf5338d1e5643a32b048c600de0cdaa7084e5909106103df04f3306615", size = 3478291, upload-time = "2025-08-12T02:32:37.187Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/08/d5/49e188db80f3d8b1969bdbcb8a5468a3796827f15d773241204f206a9ff6/uv-0.8.8-py3-none-linux_armv6l.whl", hash = "sha256:fcdbee030de120478db1a4bb3e3bbf04eec572527ea9107ecf064a808259b6c9", size = 18470316, upload-time = "2025-08-09T00:25:11.956Z" }, - { url = "https://files.pythonhosted.org/packages/01/50/add1afadccd141d0d72b54e5146f8181fcc6efd1567a17c5b1edec444010/uv-0.8.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:461e8fb83931755cf0596bf1b8ccbfe02765e81a0d392c495c07685d6b6591f9", size = 18468770, upload-time = "2025-08-09T00:25:15.391Z" }, - { url = "https://files.pythonhosted.org/packages/8c/ac/3c6dc8781d37ef9854f412322caffac2978dd3fa1bf806f7daebcfebf2be/uv-0.8.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:58056e5ccebb0a1aad27bd89d0ccc5b65c086d5a7f6b0ac16a9dde030b63cf14", size = 17200419, upload-time = "2025-08-09T00:25:18.264Z" }, - { url = "https://files.pythonhosted.org/packages/a1/9e/c30ea1f634673d234999985984afbe96c3d2a4381986e36df0bb46c0f21b/uv-0.8.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:5b4c56a620137f562e1d7b09eac6c9d4adeb876aefc51be27973257fcb426c9d", size = 17779351, upload-time = "2025-08-09T00:25:20.891Z" }, - { url = "https://files.pythonhosted.org/packages/2f/89/f2885c6e97a265b4b18050df6285f56c81b603a867a63fcd8f2caa04d95c/uv-0.8.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5fc33adb91c4e3db550648aa30c2b97e8e4d8b8842ead7784a9e76dae3cb14dc", size = 18139292, upload-time = "2025-08-09T00:25:23.352Z" }, - { url = "https://files.pythonhosted.org/packages/38/5f/98dad16987919e7dc02f2566026a263ea6307bf57e8de0008dde4717d9cf/uv-0.8.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19a82d6738d3aa58e6646b9d6c343d103abf0c4caf97a68d16a8cab55282e4be", size = 18932468, upload-time = "2025-08-09T00:25:25.691Z" }, - { url = "https://files.pythonhosted.org/packages/56/99/52d0d9f53cc5df11b1a459e743bd7b2f4660d49f125a63640eb85ce993e0/uv-0.8.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:9dce4de70098cb5b98feea9ef0b8f7db5d6b9deea003a926bc044a793872d719", size = 20251614, upload-time = "2025-08-09T00:25:28.122Z" }, - { url = "https://files.pythonhosted.org/packages/9e/b1/0698099a905b4a07b8fa9d6838e0680de707216ccf003433ca1b4afff224/uv-0.8.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1038324c178d2d7407a4005c4c3294cbad6a02368ba5a85242308de62a6f4e12", size = 19916222, upload-time = "2025-08-09T00:25:30.732Z" }, - { url = "https://files.pythonhosted.org/packages/7f/29/8384e0f3f3536ef376d94b7ab177753179906a6c2f5bab893e3fb9525b45/uv-0.8.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3bd016beea3935f9148b3d2482e3d60dee36f0260f9e99d4f57acfd978c1142a", size = 19238516, upload-time = "2025-08-09T00:25:33.637Z" }, - { url = "https://files.pythonhosted.org/packages/0e/f1/6c107deccd6e66eb1c46776d8cef4ca9274aac73cec1b14453fe85e18a54/uv-0.8.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0a2b5ebc96aba2b0bf54283d2906b40f32949298cbc6ec48648097ddeac5c5d", size = 19232295, upload-time = "2025-08-09T00:25:37.154Z" }, - { url = "https://files.pythonhosted.org/packages/c5/96/9f5e935cd970102c67ce2a753ac721665fb4477c262e86afa0ab385cefff/uv-0.8.8-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:e529dc0a1be5e896d299e4eae4599fa68909f8cb3e6c5ee1a46f66c9048e3334", size = 18046917, upload-time = "2025-08-09T00:25:39.72Z" }, - { url = "https://files.pythonhosted.org/packages/32/75/97f371add0a02e5e37156ac0fea908ab4a1160fdf716d0e6c257b6767122/uv-0.8.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5d58d986c3b6a9ce0fb48cd48b3aee6cb1b1057f928d598432e75a4fcaa370f4", size = 18949133, upload-time = "2025-08-09T00:25:42.139Z" }, - { url = "https://files.pythonhosted.org/packages/1a/1b/ea988ae9d8c5531454ea6904290e229624c9ea830a5c37b91ec74ebde9a4/uv-0.8.8-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:e117e1230559058fd286292dd5839e8e82d1aaf05763bf4a496e91fe07b69fa1", size = 18080018, upload-time = "2025-08-09T00:25:44.645Z" }, - { url = "https://files.pythonhosted.org/packages/ff/14/3b16af331b79ae826d00a73e98f26f7f660dabedc0f82acb99069601b355/uv-0.8.8-py3-none-musllinux_1_1_i686.whl", hash = "sha256:372934fd94193c98dec59bd379cf39e73f906ae6162cbfb66686f32afd75fa0f", size = 18437896, upload-time = "2025-08-09T00:25:49.162Z" }, - { url = "https://files.pythonhosted.org/packages/1c/b6/c866684da5571dbf42e9a60b6587a62adc8a2eb592f07411d3b29cb09871/uv-0.8.8-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:9330c924faa9df00a5e78b54561ecf4e5eac1211066f027620dbe85bd6f479ce", size = 19341221, upload-time = "2025-08-09T00:25:51.444Z" }, - { url = "https://files.pythonhosted.org/packages/49/ea/55a0eff462b2ec5a6327dd87c401c53306406c830fa8f2cabd2af79dd97f/uv-0.8.8-py3-none-win32.whl", hash = "sha256:65113735aa3427d3897e2f537da1331d1391735c6eecb9b820da6a15fd2f6738", size = 18244601, upload-time = "2025-08-09T00:25:53.696Z" }, - { url = "https://files.pythonhosted.org/packages/bf/c0/f56ddb1b2276405618e3d2522018c962c010fc71f97f385d01b7e1dcd8df/uv-0.8.8-py3-none-win_amd64.whl", hash = "sha256:66189ca0b4051396aa19a6f036351477656073d0fd01618051faca699e1b3cdc", size = 20233481, upload-time = "2025-08-09T00:25:56.247Z" }, - { url = "https://files.pythonhosted.org/packages/ac/1a/70dc4c730c19f3af40be9450b98b801e03cd6d16609743013f7258f69a29/uv-0.8.8-py3-none-win_arm64.whl", hash = "sha256:1d829486e88ebbf7895306ff09a8b6014d3af7a18e27d751979ee37bf3a27832", size = 18786215, upload-time = "2025-08-09T00:25:58.941Z" }, + { url = "https://files.pythonhosted.org/packages/87/d8/a2a24d30660b5f05f86699f86b642b1193bea1017e77e5e5d3e1c64f7bcc/uv-0.8.9-py3-none-linux_armv6l.whl", hash = "sha256:4633c693c79c57a77c52608cbca8a6bb17801bfa223326fbc5c5142654c23cc3", size = 18477020, upload-time = "2025-08-12T02:31:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/4d/21/937e590fb08ce4c82503fddb08b54613c0d42dd06c660460f8f0552dd3a7/uv-0.8.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:1cdc11cbc81824e51ebb1bac35745a79048557e869ef9da458e99f1c3a96c7f9", size = 18486975, upload-time = "2025-08-12T02:31:54.804Z" }, + { url = "https://files.pythonhosted.org/packages/60/a8/e6fc3e204731aa26b09934bbdecc8d6baa58a2d9e55b59b13130bacf8e52/uv-0.8.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7b20ee83e3bf294e0b1347d0b27c56ea1a4fa7eeff4361fbf1f39587d4273059", size = 17178749, upload-time = "2025-08-12T02:31:57.251Z" }, + { url = "https://files.pythonhosted.org/packages/b2/3e/3104a054bb6e866503a13114ee969d4b66227ebab19a38e3468f36c03a87/uv-0.8.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:3418315e624f60a1c4ed37987b35d5ff0d03961d380e7e7946a3378499d5d779", size = 17790897, upload-time = "2025-08-12T02:31:59.451Z" }, + { url = "https://files.pythonhosted.org/packages/50/e6/ab64cca644f40bf85fb9b3a9050aad25af7882a1d774a384fc473ef9c697/uv-0.8.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7efe01b3ed9816e07e6cd4e088472a558a1d2946177f31002b4c42cd55cb4604", size = 18124831, upload-time = "2025-08-12T02:32:02.151Z" }, + { url = "https://files.pythonhosted.org/packages/08/d1/68a001e3ad5d0601ea9ff348b54a78c8ba87fd2a6b6b5e27b379f6f3dff0/uv-0.8.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e571132495d7ab24d2f0270c559d6facd4224745d9db7dff8c20ec0c71ae105a", size = 18924774, upload-time = "2025-08-12T02:32:04.479Z" }, + { url = "https://files.pythonhosted.org/packages/ed/71/1b252e523eb875aa4ac8d06d5f8df175fa2d29e13da347d5d4823bce6c47/uv-0.8.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:67507c66837d8465daaad9f2ccd7da7af981d8c94eb8e32798f62a98c28de82d", size = 20256335, upload-time = "2025-08-12T02:32:07.12Z" }, + { url = "https://files.pythonhosted.org/packages/30/fc/062a25088b30a0fd27e4cc46baa272dd816acdec252b120d05a16d63170a/uv-0.8.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3162f495805a26fba5aacbee49c8650e1e74313c7a2e6df6aec5de9d1299087", size = 19920018, upload-time = "2025-08-12T02:32:10.041Z" }, + { url = "https://files.pythonhosted.org/packages/d8/55/90a0dc35938e68509ff8e8a49ff45b0fd13f3a44752e37d8967cd9d19316/uv-0.8.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:60eb70afeb1c66180e12a15afd706bcc0968dbefccf7ef6e5d27a1aaa765419b", size = 19235553, upload-time = "2025-08-12T02:32:12.361Z" }, + { url = "https://files.pythonhosted.org/packages/ae/a4/2db5939a3a993a06bca0a42e2120b4385bf1a4ff54242780701759252052/uv-0.8.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:011d2b2d4781555f7f7d29d2f0d6b2638fc60eeff479406ed570052664589e6a", size = 19259174, upload-time = "2025-08-12T02:32:14.697Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c9/c52249b5f40f8eb2157587ae4b997942335e4df312dfb83b16b5ebdecc61/uv-0.8.9-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:97621843e087a68c0b4969676367d757e1de43c00a9f554eb7da35641bdff8a2", size = 18048069, upload-time = "2025-08-12T02:32:16.955Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ca/524137719fb09477e57c5983fa8864f824f5858b29fc679c0416634b79f0/uv-0.8.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:b1be6a7b49d23b75d598691cc5c065a9e3cdf5e6e75d7b7f42f24d758ceef3c4", size = 18943440, upload-time = "2025-08-12T02:32:19.212Z" }, + { url = "https://files.pythonhosted.org/packages/f0/b8/877bf9a52207023a8bf9b762bed3853697ed71c5c9911a4e31231de49a23/uv-0.8.9-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:91598361309c3601382c552dc22256f70b2491ad03357b66caa4be6fdf1111dd", size = 18075581, upload-time = "2025-08-12T02:32:21.732Z" }, + { url = "https://files.pythonhosted.org/packages/96/de/272d4111ff71765bcbfd3ecb4d4fff4073f08cc38b3ecdb7272518c3fe93/uv-0.8.9-py3-none-musllinux_1_1_i686.whl", hash = "sha256:dc81df9dd7571756e34255592caab92821652face35c3f52ad05efaa4bcc39d3", size = 18420275, upload-time = "2025-08-12T02:32:24.488Z" }, + { url = "https://files.pythonhosted.org/packages/90/15/fecfc6665d1bfc5c7dbd32ff1d63413ac43d7f6d16d76fdc4d2513cbe807/uv-0.8.9-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:9ef728e0a5caa2bb129c009a68b30819552e7addf934916a466116e302748bed", size = 19354288, upload-time = "2025-08-12T02:32:27.714Z" }, + { url = "https://files.pythonhosted.org/packages/52/b5/9fef88ac0cc3ca71ff718fa7d7e90c1b3a8639b041c674825aae00d24bf5/uv-0.8.9-py3-none-win32.whl", hash = "sha256:a347c2f2630a45a3b7ceae28a78a528137edfec4847bb29da1561bd8d1f7d254", size = 18197270, upload-time = "2025-08-12T02:32:30.288Z" }, + { url = "https://files.pythonhosted.org/packages/04/0a/dacd483c9726d2b74e42ee1f186aabab508222114f3099a7610ad0f78004/uv-0.8.9-py3-none-win_amd64.whl", hash = "sha256:dc12048cdb53210d0c7218bb403ad30118b1fe8eeff3fbcc184c13c26fcc47d4", size = 20221458, upload-time = "2025-08-12T02:32:32.706Z" }, + { url = "https://files.pythonhosted.org/packages/ac/7e/f2b35278304673dcf9e8fe84b6d15531d91c59530dcf7919111f39a8d28f/uv-0.8.9-py3-none-win_arm64.whl", hash = "sha256:53332de28e9ee00effb695a15cdc70b2455d6b5f6b596d556076b5dd1fd3aa26", size = 18805689, upload-time = "2025-08-12T02:32:35.036Z" }, ] [[package]] @@ -3451,66 +3505,71 @@ wheels = [ [[package]] name = "wrapt" -version = "1.17.2" +version = "1.17.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/fc/e91cc220803d7bc4db93fb02facd8461c37364151b8494762cc88b0fbcef/wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3", size = 55531, upload-time = "2025-01-14T10:35:45.465Z" } +sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/d1/1daec934997e8b160040c78d7b31789f19b122110a75eca3d4e8da0049e1/wrapt-1.17.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d57c572081fed831ad2d26fd430d565b76aa277ed1d30ff4d40670b1c0dd984", size = 53307, upload-time = "2025-01-14T10:33:13.616Z" }, - { url = "https://files.pythonhosted.org/packages/1b/7b/13369d42651b809389c1a7153baa01d9700430576c81a2f5c5e460df0ed9/wrapt-1.17.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5e251054542ae57ac7f3fba5d10bfff615b6c2fb09abeb37d2f1463f841ae22", size = 38486, upload-time = "2025-01-14T10:33:15.947Z" }, - { url = "https://files.pythonhosted.org/packages/62/bf/e0105016f907c30b4bd9e377867c48c34dc9c6c0c104556c9c9126bd89ed/wrapt-1.17.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80dd7db6a7cb57ffbc279c4394246414ec99537ae81ffd702443335a61dbf3a7", size = 38777, upload-time = "2025-01-14T10:33:17.462Z" }, - { url = "https://files.pythonhosted.org/packages/27/70/0f6e0679845cbf8b165e027d43402a55494779295c4b08414097b258ac87/wrapt-1.17.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a6e821770cf99cc586d33833b2ff32faebdbe886bd6322395606cf55153246c", size = 83314, upload-time = "2025-01-14T10:33:21.282Z" }, - { url = "https://files.pythonhosted.org/packages/0f/77/0576d841bf84af8579124a93d216f55d6f74374e4445264cb378a6ed33eb/wrapt-1.17.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b60fb58b90c6d63779cb0c0c54eeb38941bae3ecf7a73c764c52c88c2dcb9d72", size = 74947, upload-time = "2025-01-14T10:33:24.414Z" }, - { url = "https://files.pythonhosted.org/packages/90/ec/00759565518f268ed707dcc40f7eeec38637d46b098a1f5143bff488fe97/wrapt-1.17.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b870b5df5b71d8c3359d21be8f0d6c485fa0ebdb6477dda51a1ea54a9b558061", size = 82778, upload-time = "2025-01-14T10:33:26.152Z" }, - { url = "https://files.pythonhosted.org/packages/f8/5a/7cffd26b1c607b0b0c8a9ca9d75757ad7620c9c0a9b4a25d3f8a1480fafc/wrapt-1.17.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4011d137b9955791f9084749cba9a367c68d50ab8d11d64c50ba1688c9b457f2", size = 81716, upload-time = "2025-01-14T10:33:27.372Z" }, - { url = "https://files.pythonhosted.org/packages/7e/09/dccf68fa98e862df7e6a60a61d43d644b7d095a5fc36dbb591bbd4a1c7b2/wrapt-1.17.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1473400e5b2733e58b396a04eb7f35f541e1fb976d0c0724d0223dd607e0f74c", size = 74548, upload-time = "2025-01-14T10:33:28.52Z" }, - { url = "https://files.pythonhosted.org/packages/b7/8e/067021fa3c8814952c5e228d916963c1115b983e21393289de15128e867e/wrapt-1.17.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3cedbfa9c940fdad3e6e941db7138e26ce8aad38ab5fe9dcfadfed9db7a54e62", size = 81334, upload-time = "2025-01-14T10:33:29.643Z" }, - { url = "https://files.pythonhosted.org/packages/4b/0d/9d4b5219ae4393f718699ca1c05f5ebc0c40d076f7e65fd48f5f693294fb/wrapt-1.17.2-cp310-cp310-win32.whl", hash = "sha256:582530701bff1dec6779efa00c516496968edd851fba224fbd86e46cc6b73563", size = 36427, upload-time = "2025-01-14T10:33:30.832Z" }, - { url = "https://files.pythonhosted.org/packages/72/6a/c5a83e8f61aec1e1aeef939807602fb880e5872371e95df2137142f5c58e/wrapt-1.17.2-cp310-cp310-win_amd64.whl", hash = "sha256:58705da316756681ad3c9c73fd15499aa4d8c69f9fd38dc8a35e06c12468582f", size = 38774, upload-time = "2025-01-14T10:33:32.897Z" }, - { url = "https://files.pythonhosted.org/packages/cd/f7/a2aab2cbc7a665efab072344a8949a71081eed1d2f451f7f7d2b966594a2/wrapt-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58", size = 53308, upload-time = "2025-01-14T10:33:33.992Z" }, - { url = "https://files.pythonhosted.org/packages/50/ff/149aba8365fdacef52b31a258c4dc1c57c79759c335eff0b3316a2664a64/wrapt-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda", size = 38488, upload-time = "2025-01-14T10:33:35.264Z" }, - { url = "https://files.pythonhosted.org/packages/65/46/5a917ce85b5c3b490d35c02bf71aedaa9f2f63f2d15d9949cc4ba56e8ba9/wrapt-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438", size = 38776, upload-time = "2025-01-14T10:33:38.28Z" }, - { url = "https://files.pythonhosted.org/packages/ca/74/336c918d2915a4943501c77566db41d1bd6e9f4dbc317f356b9a244dfe83/wrapt-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a", size = 83776, upload-time = "2025-01-14T10:33:40.678Z" }, - { url = "https://files.pythonhosted.org/packages/09/99/c0c844a5ccde0fe5761d4305485297f91d67cf2a1a824c5f282e661ec7ff/wrapt-1.17.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000", size = 75420, upload-time = "2025-01-14T10:33:41.868Z" }, - { url = "https://files.pythonhosted.org/packages/b4/b0/9fc566b0fe08b282c850063591a756057c3247b2362b9286429ec5bf1721/wrapt-1.17.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6", size = 83199, upload-time = "2025-01-14T10:33:43.598Z" }, - { url = "https://files.pythonhosted.org/packages/9d/4b/71996e62d543b0a0bd95dda485219856def3347e3e9380cc0d6cf10cfb2f/wrapt-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b", size = 82307, upload-time = "2025-01-14T10:33:48.499Z" }, - { url = "https://files.pythonhosted.org/packages/39/35/0282c0d8789c0dc9bcc738911776c762a701f95cfe113fb8f0b40e45c2b9/wrapt-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662", size = 75025, upload-time = "2025-01-14T10:33:51.191Z" }, - { url = "https://files.pythonhosted.org/packages/4f/6d/90c9fd2c3c6fee181feecb620d95105370198b6b98a0770cba090441a828/wrapt-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72", size = 81879, upload-time = "2025-01-14T10:33:52.328Z" }, - { url = "https://files.pythonhosted.org/packages/8f/fa/9fb6e594f2ce03ef03eddbdb5f4f90acb1452221a5351116c7c4708ac865/wrapt-1.17.2-cp311-cp311-win32.whl", hash = "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317", size = 36419, upload-time = "2025-01-14T10:33:53.551Z" }, - { url = "https://files.pythonhosted.org/packages/47/f8/fb1773491a253cbc123c5d5dc15c86041f746ed30416535f2a8df1f4a392/wrapt-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3", size = 38773, upload-time = "2025-01-14T10:33:56.323Z" }, - { url = "https://files.pythonhosted.org/packages/a1/bd/ab55f849fd1f9a58ed7ea47f5559ff09741b25f00c191231f9f059c83949/wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925", size = 53799, upload-time = "2025-01-14T10:33:57.4Z" }, - { url = "https://files.pythonhosted.org/packages/53/18/75ddc64c3f63988f5a1d7e10fb204ffe5762bc663f8023f18ecaf31a332e/wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392", size = 38821, upload-time = "2025-01-14T10:33:59.334Z" }, - { url = "https://files.pythonhosted.org/packages/48/2a/97928387d6ed1c1ebbfd4efc4133a0633546bec8481a2dd5ec961313a1c7/wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40", size = 38919, upload-time = "2025-01-14T10:34:04.093Z" }, - { url = "https://files.pythonhosted.org/packages/73/54/3bfe5a1febbbccb7a2f77de47b989c0b85ed3a6a41614b104204a788c20e/wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d", size = 88721, upload-time = "2025-01-14T10:34:07.163Z" }, - { url = "https://files.pythonhosted.org/packages/25/cb/7262bc1b0300b4b64af50c2720ef958c2c1917525238d661c3e9a2b71b7b/wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b", size = 80899, upload-time = "2025-01-14T10:34:09.82Z" }, - { url = "https://files.pythonhosted.org/packages/2a/5a/04cde32b07a7431d4ed0553a76fdb7a61270e78c5fd5a603e190ac389f14/wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98", size = 89222, upload-time = "2025-01-14T10:34:11.258Z" }, - { url = "https://files.pythonhosted.org/packages/09/28/2e45a4f4771fcfb109e244d5dbe54259e970362a311b67a965555ba65026/wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82", size = 86707, upload-time = "2025-01-14T10:34:12.49Z" }, - { url = "https://files.pythonhosted.org/packages/c6/d2/dcb56bf5f32fcd4bd9aacc77b50a539abdd5b6536872413fd3f428b21bed/wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae", size = 79685, upload-time = "2025-01-14T10:34:15.043Z" }, - { url = "https://files.pythonhosted.org/packages/80/4e/eb8b353e36711347893f502ce91c770b0b0929f8f0bed2670a6856e667a9/wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9", size = 87567, upload-time = "2025-01-14T10:34:16.563Z" }, - { url = "https://files.pythonhosted.org/packages/17/27/4fe749a54e7fae6e7146f1c7d914d28ef599dacd4416566c055564080fe2/wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9", size = 36672, upload-time = "2025-01-14T10:34:17.727Z" }, - { url = "https://files.pythonhosted.org/packages/15/06/1dbf478ea45c03e78a6a8c4be4fdc3c3bddea5c8de8a93bc971415e47f0f/wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991", size = 38865, upload-time = "2025-01-14T10:34:19.577Z" }, - { url = "https://files.pythonhosted.org/packages/ce/b9/0ffd557a92f3b11d4c5d5e0c5e4ad057bd9eb8586615cdaf901409920b14/wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125", size = 53800, upload-time = "2025-01-14T10:34:21.571Z" }, - { url = "https://files.pythonhosted.org/packages/c0/ef/8be90a0b7e73c32e550c73cfb2fa09db62234227ece47b0e80a05073b375/wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998", size = 38824, upload-time = "2025-01-14T10:34:22.999Z" }, - { url = "https://files.pythonhosted.org/packages/36/89/0aae34c10fe524cce30fe5fc433210376bce94cf74d05b0d68344c8ba46e/wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5", size = 38920, upload-time = "2025-01-14T10:34:25.386Z" }, - { url = "https://files.pythonhosted.org/packages/3b/24/11c4510de906d77e0cfb5197f1b1445d4fec42c9a39ea853d482698ac681/wrapt-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8", size = 88690, upload-time = "2025-01-14T10:34:28.058Z" }, - { url = "https://files.pythonhosted.org/packages/71/d7/cfcf842291267bf455b3e266c0c29dcb675b5540ee8b50ba1699abf3af45/wrapt-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6", size = 80861, upload-time = "2025-01-14T10:34:29.167Z" }, - { url = "https://files.pythonhosted.org/packages/d5/66/5d973e9f3e7370fd686fb47a9af3319418ed925c27d72ce16b791231576d/wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc", size = 89174, upload-time = "2025-01-14T10:34:31.702Z" }, - { url = "https://files.pythonhosted.org/packages/a7/d3/8e17bb70f6ae25dabc1aaf990f86824e4fd98ee9cadf197054e068500d27/wrapt-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2", size = 86721, upload-time = "2025-01-14T10:34:32.91Z" }, - { url = "https://files.pythonhosted.org/packages/6f/54/f170dfb278fe1c30d0ff864513cff526d624ab8de3254b20abb9cffedc24/wrapt-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b", size = 79763, upload-time = "2025-01-14T10:34:34.903Z" }, - { url = "https://files.pythonhosted.org/packages/4a/98/de07243751f1c4a9b15c76019250210dd3486ce098c3d80d5f729cba029c/wrapt-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504", size = 87585, upload-time = "2025-01-14T10:34:36.13Z" }, - { url = "https://files.pythonhosted.org/packages/f9/f0/13925f4bd6548013038cdeb11ee2cbd4e37c30f8bfd5db9e5a2a370d6e20/wrapt-1.17.2-cp313-cp313-win32.whl", hash = "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a", size = 36676, upload-time = "2025-01-14T10:34:37.962Z" }, - { url = "https://files.pythonhosted.org/packages/bf/ae/743f16ef8c2e3628df3ddfd652b7d4c555d12c84b53f3d8218498f4ade9b/wrapt-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845", size = 38871, upload-time = "2025-01-14T10:34:39.13Z" }, - { url = "https://files.pythonhosted.org/packages/3d/bc/30f903f891a82d402ffb5fda27ec1d621cc97cb74c16fea0b6141f1d4e87/wrapt-1.17.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192", size = 56312, upload-time = "2025-01-14T10:34:40.604Z" }, - { url = "https://files.pythonhosted.org/packages/8a/04/c97273eb491b5f1c918857cd26f314b74fc9b29224521f5b83f872253725/wrapt-1.17.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b", size = 40062, upload-time = "2025-01-14T10:34:45.011Z" }, - { url = "https://files.pythonhosted.org/packages/4e/ca/3b7afa1eae3a9e7fefe499db9b96813f41828b9fdb016ee836c4c379dadb/wrapt-1.17.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0", size = 40155, upload-time = "2025-01-14T10:34:47.25Z" }, - { url = "https://files.pythonhosted.org/packages/89/be/7c1baed43290775cb9030c774bc53c860db140397047cc49aedaf0a15477/wrapt-1.17.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306", size = 113471, upload-time = "2025-01-14T10:34:50.934Z" }, - { url = "https://files.pythonhosted.org/packages/32/98/4ed894cf012b6d6aae5f5cc974006bdeb92f0241775addad3f8cd6ab71c8/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb", size = 101208, upload-time = "2025-01-14T10:34:52.297Z" }, - { url = "https://files.pythonhosted.org/packages/ea/fd/0c30f2301ca94e655e5e057012e83284ce8c545df7661a78d8bfca2fac7a/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681", size = 109339, upload-time = "2025-01-14T10:34:53.489Z" }, - { url = "https://files.pythonhosted.org/packages/75/56/05d000de894c4cfcb84bcd6b1df6214297b8089a7bd324c21a4765e49b14/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6", size = 110232, upload-time = "2025-01-14T10:34:55.327Z" }, - { url = "https://files.pythonhosted.org/packages/53/f8/c3f6b2cf9b9277fb0813418e1503e68414cd036b3b099c823379c9575e6d/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6", size = 100476, upload-time = "2025-01-14T10:34:58.055Z" }, - { url = "https://files.pythonhosted.org/packages/a7/b1/0bb11e29aa5139d90b770ebbfa167267b1fc548d2302c30c8f7572851738/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f", size = 106377, upload-time = "2025-01-14T10:34:59.3Z" }, - { url = "https://files.pythonhosted.org/packages/6a/e1/0122853035b40b3f333bbb25f1939fc1045e21dd518f7f0922b60c156f7c/wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555", size = 37986, upload-time = "2025-01-14T10:35:00.498Z" }, - { url = "https://files.pythonhosted.org/packages/09/5e/1655cf481e079c1f22d0cabdd4e51733679932718dc23bf2db175f329b76/wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c", size = 40750, upload-time = "2025-01-14T10:35:03.378Z" }, - { url = "https://files.pythonhosted.org/packages/2d/82/f56956041adef78f849db6b289b282e72b55ab8045a75abad81898c28d19/wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", size = 23594, upload-time = "2025-01-14T10:35:44.018Z" }, + { url = "https://files.pythonhosted.org/packages/3f/23/bb82321b86411eb51e5a5db3fb8f8032fd30bd7c2d74bfe936136b2fa1d6/wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04", size = 53482, upload-time = "2025-08-12T05:51:44.467Z" }, + { url = "https://files.pythonhosted.org/packages/45/69/f3c47642b79485a30a59c63f6d739ed779fb4cc8323205d047d741d55220/wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2", size = 38676, upload-time = "2025-08-12T05:51:32.636Z" }, + { url = "https://files.pythonhosted.org/packages/d1/71/e7e7f5670c1eafd9e990438e69d8fb46fa91a50785332e06b560c869454f/wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c", size = 38957, upload-time = "2025-08-12T05:51:54.655Z" }, + { url = "https://files.pythonhosted.org/packages/de/17/9f8f86755c191d6779d7ddead1a53c7a8aa18bccb7cea8e7e72dfa6a8a09/wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775", size = 81975, upload-time = "2025-08-12T05:52:30.109Z" }, + { url = "https://files.pythonhosted.org/packages/f2/15/dd576273491f9f43dd09fce517f6c2ce6eb4fe21681726068db0d0467096/wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd", size = 83149, upload-time = "2025-08-12T05:52:09.316Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c4/5eb4ce0d4814521fee7aa806264bf7a114e748ad05110441cd5b8a5c744b/wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05", size = 82209, upload-time = "2025-08-12T05:52:10.331Z" }, + { url = "https://files.pythonhosted.org/packages/31/4b/819e9e0eb5c8dc86f60dfc42aa4e2c0d6c3db8732bce93cc752e604bb5f5/wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418", size = 81551, upload-time = "2025-08-12T05:52:31.137Z" }, + { url = "https://files.pythonhosted.org/packages/f8/83/ed6baf89ba3a56694700139698cf703aac9f0f9eb03dab92f57551bd5385/wrapt-1.17.3-cp310-cp310-win32.whl", hash = "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390", size = 36464, upload-time = "2025-08-12T05:53:01.204Z" }, + { url = "https://files.pythonhosted.org/packages/2f/90/ee61d36862340ad7e9d15a02529df6b948676b9a5829fd5e16640156627d/wrapt-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6", size = 38748, upload-time = "2025-08-12T05:53:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c3/cefe0bd330d389c9983ced15d326f45373f4073c9f4a8c2f99b50bfea329/wrapt-1.17.3-cp310-cp310-win_arm64.whl", hash = "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18", size = 36810, upload-time = "2025-08-12T05:52:51.906Z" }, + { url = "https://files.pythonhosted.org/packages/52/db/00e2a219213856074a213503fdac0511203dceefff26e1daa15250cc01a0/wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7", size = 53482, upload-time = "2025-08-12T05:51:45.79Z" }, + { url = "https://files.pythonhosted.org/packages/5e/30/ca3c4a5eba478408572096fe9ce36e6e915994dd26a4e9e98b4f729c06d9/wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85", size = 38674, upload-time = "2025-08-12T05:51:34.629Z" }, + { url = "https://files.pythonhosted.org/packages/31/25/3e8cc2c46b5329c5957cec959cb76a10718e1a513309c31399a4dad07eb3/wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f", size = 38959, upload-time = "2025-08-12T05:51:56.074Z" }, + { url = "https://files.pythonhosted.org/packages/5d/8f/a32a99fc03e4b37e31b57cb9cefc65050ea08147a8ce12f288616b05ef54/wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311", size = 82376, upload-time = "2025-08-12T05:52:32.134Z" }, + { url = "https://files.pythonhosted.org/packages/31/57/4930cb8d9d70d59c27ee1332a318c20291749b4fba31f113c2f8ac49a72e/wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1", size = 83604, upload-time = "2025-08-12T05:52:11.663Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f3/1afd48de81d63dd66e01b263a6fbb86e1b5053b419b9b33d13e1f6d0f7d0/wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5", size = 82782, upload-time = "2025-08-12T05:52:12.626Z" }, + { url = "https://files.pythonhosted.org/packages/1e/d7/4ad5327612173b144998232f98a85bb24b60c352afb73bc48e3e0d2bdc4e/wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2", size = 82076, upload-time = "2025-08-12T05:52:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/59/e0adfc831674a65694f18ea6dc821f9fcb9ec82c2ce7e3d73a88ba2e8718/wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89", size = 36457, upload-time = "2025-08-12T05:53:03.936Z" }, + { url = "https://files.pythonhosted.org/packages/83/88/16b7231ba49861b6f75fc309b11012ede4d6b0a9c90969d9e0db8d991aeb/wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77", size = 38745, upload-time = "2025-08-12T05:53:02.885Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1e/c4d4f3398ec073012c51d1c8d87f715f56765444e1a4b11e5180577b7e6e/wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a", size = 36806, upload-time = "2025-08-12T05:52:53.368Z" }, + { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" }, + { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" }, + { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" }, + { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" }, + { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" }, + { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" }, + { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" }, + { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" }, + { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" }, + { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" }, + { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" }, + { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" }, + { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" }, + { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" }, + { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" }, + { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" }, + { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" }, + { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" }, + { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" }, + { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" }, + { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" }, + { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" }, + { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" }, + { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" }, + { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" }, + { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, ] [[package]]