From df9d85d1f0164951dc02789464394ccec4e98df3 Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Tue, 12 Aug 2025 08:14:22 +0200 Subject: [PATCH] Python: openai updates (#388) * openai updates * rebuild of openai structure * updated responses structure * renamed sample * added file id support to code interpreter * added hosted file ids to code interpretor * mypy fixes * removed default az cred from codebase * updated agent name setup * added kwargs to entra methods * and further kwargs * extra comment * updated all samples * readded custom get methods for responses * updated int tests with ad credential * missed one --- .../_assistants_client.py | 18 +- .../agent_framework_azure/_chat_client.py | 162 ++-- .../_entra_id_authentication.py | 54 +- .../_responses_client.py | 16 +- .../azure/agent_framework_azure/_shared.py | 58 +- .../tests/test_azure_assistants_client.py | 15 +- .../azure/tests/test_azure_chat_client.py | 115 +-- .../tests/test_azure_responses_client.py | 75 +- .../agent_framework_foundry/_chat_client.py | 63 +- .../foundry/tests/test_foundry_chat_client.py | 12 +- .../packages/main/agent_framework/_agents.py | 15 +- .../packages/main/agent_framework/_clients.py | 43 +- .../packages/main/agent_framework/_tools.py | 252 +++--- .../packages/main/agent_framework/_types.py | 51 +- .../main/agent_framework/exceptions.py | 30 +- .../openai/_assistants_client.py | 18 +- .../agent_framework/openai/_chat_client.py | 249 +++--- .../openai/_responses_client.py | 773 ++++++++++++------ .../main/agent_framework/openai/_shared.py | 199 +---- .../main/agent_framework/telemetry.py | 1 + .../packages/main/tests/main/test_clients.py | 45 - python/packages/main/tests/main/test_tools.py | 24 +- python/packages/main/tests/main/test_types.py | 8 +- .../openai/test_openai_chat_client_base.py | 13 +- .../openai/test_openai_responses_client.py | 66 +- .../azure_assistants_basic.py | 8 +- .../azure_assistants_with_code_interpreter.py | 3 +- .../azure_assistants_with_function_tools.py | 7 +- .../azure_assistants_with_thread.py | 9 +- .../azure_chat_client_basic.py | 8 +- .../azure_chat_client_with_function_tools.py | 7 +- .../azure_chat_client_with_thread.py | 9 +- .../azure_responses_client_basic.py | 8 +- ..._responses_client_with_code_interpreter.py | 3 +- ...re_responses_client_with_function_tools.py | 7 +- .../azure_responses_client_with_thread.py | 9 +- .../agents/foundry/foundry_basic.py | 5 +- .../foundry/foundry_with_code_interpreter.py | 3 +- .../foundry/foundry_with_explicit_settings.py | 2 +- .../foundry/foundry_with_function_tools.py | 7 +- .../agents/foundry/foundry_with_thread.py | 9 +- .../openai_assistants_basic.py | 7 +- .../openai_responses_client_reasoning.py | 46 ++ .../chat_client/azure_assistants_client.py | 3 +- .../chat_client/azure_chat_client.py | 3 +- .../chat_client/azure_responses_client.py | 3 +- .../chat_client/foundry_chat_client.py | 3 +- .../chat_client/openai_chat_client.py | 6 +- .../chat_client/openai_responses_client.py | 6 +- .../workflow/step_04_simple_group_chat.py | 11 +- .../step_05_simple_group_chat_with_hil.py | 10 +- .../workflow/step_06_map_reduce.py | 8 +- python/uv.lock | 553 ++++++------- 53 files changed, 1668 insertions(+), 1470 deletions(-) create mode 100644 python/samples/getting_started/agents/openai_responses_client/openai_responses_client_reasoning.py diff --git a/python/packages/azure/agent_framework_azure/_assistants_client.py b/python/packages/azure/agent_framework_azure/_assistants_client.py index 582b6e12da..b0d0c95bed 100644 --- a/python/packages/azure/agent_framework_azure/_assistants_client.py +++ b/python/packages/azure/agent_framework_azure/_assistants_client.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft. All rights reserved. from collections.abc import Mapping -from typing import Any, ClassVar +from typing import TYPE_CHECKING, Any, ClassVar from agent_framework.exceptions import ServiceInitializationError from agent_framework.openai import OpenAIAssistantsClient @@ -9,9 +9,10 @@ from openai.lib.azure import AsyncAzureADTokenProvider, AsyncAzureOpenAI from pydantic import SecretStr, ValidationError from pydantic.networks import AnyUrl -from ._shared import ( - AzureOpenAISettings, -) +from ._shared import AzureOpenAISettings + +if TYPE_CHECKING: + from azure.identity import ChainedTokenCredential __all__ = ["AzureAssistantsClient"] @@ -20,7 +21,6 @@ class AzureAssistantsClient(OpenAIAssistantsClient): """Azure OpenAI Assistants client.""" DEFAULT_AZURE_API_VERSION: ClassVar[str] = "2024-05-01-preview" - MODEL_PROVIDER_NAME: ClassVar[str] = "azure_openai" # type: ignore[reportIncompatibleVariableOverride, misc] def __init__( self, @@ -35,6 +35,7 @@ class AzureAssistantsClient(OpenAIAssistantsClient): ad_token: str | None = None, ad_token_provider: AsyncAzureADTokenProvider | None = None, token_endpoint: str | None = None, + ad_credential: "ChainedTokenCredential | None" = None, default_headers: Mapping[str, str] | None = None, async_client: AsyncAzureOpenAI | None = None, env_file_path: str | None = None, @@ -61,6 +62,7 @@ class AzureAssistantsClient(OpenAIAssistantsClient): ad_token: The Azure Active Directory token. (Optional) ad_token_provider: The Azure Active Directory token provider. (Optional) token_endpoint: The token endpoint to request an Azure token. (Optional) + ad_credential: The Azure AD credential to use for authentication. (Optional) default_headers: The default headers mapping of string keys to string values for HTTP requests. (Optional) async_client: An existing client to use. (Optional) @@ -93,11 +95,9 @@ class AzureAssistantsClient(OpenAIAssistantsClient): and not ad_token and not ad_token_provider and azure_openai_settings.token_endpoint + and ad_credential ): - # Try to get token using Entra ID if no other auth method is provided - from ._entra_id_authentication import get_entra_auth_token - - ad_token = get_entra_auth_token(azure_openai_settings.token_endpoint) + ad_token = azure_openai_settings.get_azure_auth_token(ad_credential) if not async_client and not azure_openai_settings.api_key and not ad_token and not ad_token_provider: raise ServiceInitializationError("The Azure OpenAI API key, ad_token, or ad_token_provider is required.") diff --git a/python/packages/azure/agent_framework_azure/_chat_client.py b/python/packages/azure/agent_framework_azure/_chat_client.py index f9ea9a5a99..3238fc7719 100644 --- a/python/packages/azure/agent_framework_azure/_chat_client.py +++ b/python/packages/azure/agent_framework_azure/_chat_client.py @@ -2,25 +2,21 @@ import json import logging +import sys from collections.abc import Mapping -from copy import deepcopy from typing import Any, TypeVar -from uuid import uuid4 from agent_framework import ( - ChatFinishReason, ChatResponse, ChatResponseUpdate, - FunctionCallContent, - FunctionResultContent, + CitationAnnotation, TextContent, ) from agent_framework.exceptions import ServiceInitializationError from agent_framework.openai._chat_client import OpenAIChatClientBase -from agent_framework.openai._shared import OpenAIModelTypes +from azure.identity import ChainedTokenCredential from openai.lib.azure import AsyncAzureADTokenProvider, AsyncAzureOpenAI -from openai.types.chat.chat_completion import ChatCompletion, Choice -from openai.types.chat.chat_completion_chunk import ChatCompletionChunk +from openai.types.chat.chat_completion import Choice from openai.types.chat.chat_completion_chunk import Choice as ChunkChoice from pydantic import SecretStr, ValidationError from pydantic.networks import AnyUrl @@ -30,9 +26,15 @@ from ._shared import ( AzureOpenAISettings, ) +if sys.version_info >= (3, 12): + from typing import override # type: ignore # pragma: no cover +else: + from typing_extensions import override # type: ignore[import] # pragma: no cover + logger: logging.Logger = logging.getLogger(__name__) TChatResponse = TypeVar("TChatResponse", ChatResponse, ChatResponseUpdate) +TAzureChatClient = TypeVar("TAzureChatClient", bound="AzureChatClient") class AzureChatClient(AzureOpenAIConfigBase, OpenAIChatClientBase): @@ -48,6 +50,7 @@ class AzureChatClient(AzureOpenAIConfigBase, OpenAIChatClientBase): ad_token: str | None = None, ad_token_provider: AsyncAzureADTokenProvider | None = None, token_endpoint: str | None = None, + ad_credential: ChainedTokenCredential | None = None, default_headers: Mapping[str, str] | None = None, async_client: AsyncAzureOpenAI | None = None, env_file_path: str | None = None, @@ -70,6 +73,7 @@ class AzureChatClient(AzureOpenAIConfigBase, OpenAIChatClientBase): ad_token: The Azure Active Directory token. (Optional) ad_token_provider: The Azure Active Directory token provider. (Optional) token_endpoint: The token endpoint to request an Azure token. (Optional) + ad_credential: The Azure Active Directory credential. (Optional) default_headers: The default headers mapping of string keys to string values for HTTP requests. (Optional) async_client: An existing client to use. (Optional) @@ -107,14 +111,14 @@ class AzureChatClient(AzureOpenAIConfigBase, OpenAIChatClientBase): ad_token=ad_token, ad_token_provider=ad_token_provider, token_endpoint=azure_openai_settings.token_endpoint, + ad_credential=ad_credential, default_headers=default_headers, - ai_model_type=OpenAIModelTypes.CHAT, client=async_client, instruction_role=instruction_role, ) @classmethod - def from_dict(cls, settings: dict[str, Any]) -> "AzureChatClient": + def from_dict(cls: type[TAzureChatClient], settings: dict[str, Any]) -> TAzureChatClient: """Initialize an Azure OpenAI service from a dictionary of settings. Args: @@ -122,7 +126,7 @@ class AzureChatClient(AzureOpenAIConfigBase, OpenAIChatClientBase): should contain keys: service_id, and optionally: ad_auth, ad_token_provider, default_headers """ - return AzureChatClient( + return cls( api_key=settings.get("api_key"), deployment_name=settings.get("deployment_name"), endpoint=settings.get("endpoint"), @@ -134,99 +138,49 @@ class AzureChatClient(AzureOpenAIConfigBase, OpenAIChatClientBase): env_file_path=settings.get("env_file_path"), ) - def _create_chat_message_content( - self, response: ChatCompletion, choice: Choice, response_metadata: dict[str, Any] - ) -> ChatResponse: - """Create an Azure chat message content object from a choice.""" - content = super()._create_chat_message_content(response, choice, response_metadata) - return self._add_tool_message_to_chat_message_content(content, choice) + @override + def _parse_text_from_choice(self, choice: Choice | ChunkChoice) -> TextContent | None: + """Parse the choice into a TextContent object. - def _create_streaming_chat_message_content( - self, - chunk: ChatCompletionChunk, - choice: ChunkChoice, - chunk_metadata: dict[str, Any], - ) -> ChatResponseUpdate: - """Create an Azure streaming chat message content object from a choice.""" - content = super()._create_streaming_chat_message_content(chunk, choice, chunk_metadata) - assert isinstance(content, ChatResponseUpdate) and isinstance(choice, ChunkChoice) # nosec # noqa: S101 - return self._add_tool_message_to_chat_message_content(content, choice) - - def _add_tool_message_to_chat_message_content( - self, - content: TChatResponse, - choice: Choice | ChunkChoice, - ) -> TChatResponse: - if tool_message := self._get_tool_message_from_chat_choice(choice=choice): - if not isinstance(tool_message, dict): - # try to json, to ensure it is a dictionary - try: - tool_message = json.loads(tool_message) - except json.JSONDecodeError: - logger.warning("Tool message is not a dictionary, ignore context.") - return content - function_call = FunctionCallContent( - call_id=str(uuid4()), - name="Azure-OnYourData", - arguments={"query": tool_message.get("intent", [])}, - ) - result = FunctionResultContent( - call_id=function_call.call_id, - result=tool_message["citations"], - exception=function_call.exception, - additional_properties=function_call.additional_properties, - ) - - inner_content = content.messages[0].contents if isinstance(content, ChatResponse) else content.contents - - inner_content.insert(0, function_call) - inner_content.insert(1, result) - return content - - def _get_tool_message_from_chat_choice(self, choice: Choice | ChunkChoice) -> dict[str, Any] | None: - """Get the tool message from a choice.""" - content = choice.message if isinstance(choice, Choice) else choice.delta - # When you enable asynchronous content filtering in Azure OpenAI, you may receive empty deltas - if content and content.model_extra is not None: - return content.model_extra.get("context", None) - # openai allows extra content, so model_extra will be a dict, but we need to check anyway, but no way to test. - return None # pragma: no cover - - @staticmethod - def _split_message(message: "ChatResponse") -> ChatResponse: - """Split an Azure On Your Data response into separate ChatMessages within the ChatResponse. - - If the message does not have three contents, and those three are one each of: - FunctionCallContent, FunctionResultContent, and TextContent, - it will not return three messages, potentially only one or two. - - The order of the returned messages is as expected by OpenAI. + Overwritten from OpenAIChatClientBase to deal with Azure On Your Data function. + For docs see: + https://learn.microsoft.com/en-us/azure/ai-foundry/openai/references/on-your-data?tabs=python#context """ - if len(message.messages) == 0: - return message - if len(message.messages[0].contents) != 3: - return message - messages = { - "tool_call": deepcopy(message.messages[0]), - "tool_result": deepcopy(message.messages[0]), - "assistant": deepcopy(message.messages[0]), - } - for key, msg in messages.items(): - if key == "tool_call": - msg.contents = [item for item in msg.contents if isinstance(item, FunctionCallContent)] - message.finish_reason = ChatFinishReason.TOOL_CALLS - if key == "tool_result": - msg.contents = [item for item in msg.contents if isinstance(item, FunctionResultContent)] - if key == "assistant": - msg.contents = [item for item in msg.contents if isinstance(item, TextContent)] + message = choice.message if isinstance(choice, Choice) else choice.delta + if hasattr(message, "refusal") and message.refusal: + return TextContent(text=message.refusal, raw_representation=choice) + if not message.content: + return None + text_content = TextContent(text=message.content, raw_representation=choice) + if not message.model_extra or "context" not in message.model_extra: + return text_content - return ChatResponse( - response_id=message.response_id, - conversation_id=message.conversation_id, - messages=[messages["tool_call"], messages["tool_result"], messages["assistant"]], - created_at=message.created_at, - model_id=message.ai_model_id, - usage_details=message.usage_details, - finish_reason=message.finish_reason, - additional_properties=message.additional_properties, - ) + context: dict[str, Any] | str = message.context # type: ignore[assignment, union-attr] + if isinstance(context, str): + try: + context = json.loads(context) + except json.JSONDecodeError: + logger.warning("Context is not a valid JSON string, ignoring context.") + return text_content + if not isinstance(context, dict): + logger.warning("Context is not a valid dictionary, ignoring context.") + return text_content + # `all_retrieved_documents` is currently not used, but can be retrieved + # through the raw_representation in the text content. + if intent := context.get("intent"): + text_content.additional_properties = {"intent": intent} + if citations := context.get("citations"): + text_content.annotations = [] + for citation in citations: + text_content.annotations.append( + CitationAnnotation( + title=citation.get("title", ""), + url=citation.get("url", ""), + snippet=citation.get("content", ""), + file_id=citation.get("filepath", ""), + tool_name="Azure-on-your-Data", + additional_properties={"chunk_id": citation.get("chunk_id", "")}, + raw_representation=citation, + ) + ) + return text_content diff --git a/python/packages/azure/agent_framework_azure/_entra_id_authentication.py b/python/packages/azure/agent_framework_azure/_entra_id_authentication.py index db094340a7..52423ed61c 100644 --- a/python/packages/azure/agent_framework_azure/_entra_id_authentication.py +++ b/python/packages/azure/agent_framework_azure/_entra_id_authentication.py @@ -1,22 +1,33 @@ # Copyright (c) Microsoft. All rights reserved. import logging +from typing import TYPE_CHECKING, Any from agent_framework.exceptions import ServiceInvalidAuthError from azure.core.exceptions import ClientAuthenticationError -from azure.identity import DefaultAzureCredential + +if TYPE_CHECKING: + from azure.identity import ChainedTokenCredential + from azure.identity.aio import ChainedTokenCredential as AsyncChainedTokenCredential logger: logging.Logger = logging.getLogger(__name__) -def get_entra_auth_token(token_endpoint: str) -> str | None: +def get_entra_auth_token( + credential: "ChainedTokenCredential", + token_endpoint: str, + **kwargs: Any, +) -> str | None: """Retrieve a Microsoft Entra Auth Token for a given token endpoint. The token endpoint may be specified as an environment variable, via the .env file or as an argument. If the token endpoint is not provided, the default is None. Args: + credential: The Azure credential to use for authentication. + for dev, you can use `DefaultAzureCredential`, but this is not recommended for production. token_endpoint: The token endpoint to use to retrieve the authentication token. + **kwargs: Additional keyword arguments to pass to the token retrieval method. Returns: The Azure token or None if the token could not be retrieved. @@ -26,12 +37,41 @@ def get_entra_auth_token(token_endpoint: str) -> str | None: "A token endpoint must be provided either in settings, as an environment variable, or as an argument." ) - credential = DefaultAzureCredential() - try: - auth_token = credential.get_token(token_endpoint) - except ClientAuthenticationError: - logger.error(f"Failed to retrieve Azure token for the specified endpoint: `{token_endpoint}`.") + auth_token = credential.get_token(token_endpoint, **kwargs) + except ClientAuthenticationError as ex: + logger.error(f"Failed to retrieve Azure token for the specified endpoint: `{token_endpoint}`, with error: {ex}") + return None + + return auth_token.token if auth_token else None + + +async def get_entra_auth_token_async( + credential: "AsyncChainedTokenCredential", token_endpoint: str, **kwargs: Any +) -> str | None: + """Retrieve a async Microsoft Entra Auth Token for a given token endpoint. + + The token endpoint may be specified as an environment variable, via the .env + file or as an argument. If the token endpoint is not provided, the default is None. + + Args: + credential: The async Azure credential to use for authentication. + for dev, you can use `DefaultAzureCredential`, but this is not recommended for production. + token_endpoint: The token endpoint to use to retrieve the authentication token. + **kwargs: Additional keyword arguments to pass to the token retrieval method. + + Returns: + The Azure token or None if the token could not be retrieved. + """ + if not token_endpoint: + raise ServiceInvalidAuthError( + "A token endpoint must be provided either in settings, as an environment variable, or as an argument." + ) + + try: + auth_token = await credential.get_token(token_endpoint, **kwargs) + except ClientAuthenticationError as ex: + logger.error(f"Failed to retrieve Azure token for the specified endpoint: `{token_endpoint}`, with error: {ex}") return None return auth_token.token if auth_token else None diff --git a/python/packages/azure/agent_framework_azure/_responses_client.py b/python/packages/azure/agent_framework_azure/_responses_client.py index 401ba1f7d6..0aec8fbc19 100644 --- a/python/packages/azure/agent_framework_azure/_responses_client.py +++ b/python/packages/azure/agent_framework_azure/_responses_client.py @@ -1,14 +1,14 @@ # Copyright (c) Microsoft. All rights reserved. from collections.abc import Mapping -from typing import Any, ClassVar +from typing import Any, TypeVar from urllib.parse import urljoin from agent_framework import use_tool_calling from agent_framework.exceptions import ServiceInitializationError from agent_framework.openai._responses_client import OpenAIResponsesClientBase -from agent_framework.openai._shared import OpenAIModelTypes from agent_framework.telemetry import use_telemetry +from azure.identity import ChainedTokenCredential from openai.lib.azure import AsyncAzureADTokenProvider, AsyncAzureOpenAI from pydantic import SecretStr, ValidationError from pydantic.networks import AnyUrl @@ -18,14 +18,14 @@ from ._shared import ( AzureOpenAISettings, ) +TAzureResponsesClient = TypeVar("TAzureResponsesClient", bound="AzureResponsesClient") + @use_telemetry @use_tool_calling class AzureResponsesClient(AzureOpenAIConfigBase, OpenAIResponsesClientBase): """Azure Responses completion class.""" - MODEL_PROVIDER_NAME: ClassVar[str] = "azure_openai" # type: ignore[reportIncompatibleVariableOverride, misc] - def __init__( self, api_key: str | None = None, @@ -36,6 +36,7 @@ class AzureResponsesClient(AzureOpenAIConfigBase, OpenAIResponsesClientBase): ad_token: str | None = None, ad_token_provider: AsyncAzureADTokenProvider | None = None, token_endpoint: str | None = None, + ad_credential: ChainedTokenCredential | None = None, default_headers: Mapping[str, str] | None = None, async_client: AsyncAzureOpenAI | None = None, env_file_path: str | None = None, @@ -58,6 +59,7 @@ class AzureResponsesClient(AzureOpenAIConfigBase, OpenAIResponsesClientBase): ad_token: The Azure Active Directory token. (Optional) ad_token_provider: The Azure Active Directory token provider. (Optional) token_endpoint: The token endpoint to request an Azure token. (Optional) + ad_credential: The Azure Active Directory credential. (Optional) default_headers: The default headers mapping of string keys to string values for HTTP requests. (Optional) async_client: An existing client to use. (Optional) @@ -105,20 +107,20 @@ class AzureResponsesClient(AzureOpenAIConfigBase, OpenAIResponsesClientBase): ad_token=ad_token, ad_token_provider=ad_token_provider, token_endpoint=azure_openai_settings.token_endpoint, + ad_credential=ad_credential, default_headers=default_headers, - ai_model_type=OpenAIModelTypes.RESPONSE, client=async_client, instruction_role=instruction_role, ) @classmethod - def from_dict(cls, settings: dict[str, Any]) -> "AzureResponsesClient": + def from_dict(cls: type[TAzureResponsesClient], settings: dict[str, Any]) -> TAzureResponsesClient: """Initialize an Open AI service from a dictionary of settings. Args: settings: A dictionary of settings for the service. """ - return AzureResponsesClient( + return cls( api_key=settings.get("api_key"), deployment_name=settings.get("deployment_name"), endpoint=settings.get("endpoint"), diff --git a/python/packages/azure/agent_framework_azure/_shared.py b/python/packages/azure/agent_framework_azure/_shared.py index e04a9a2404..05e9e01920 100644 --- a/python/packages/azure/agent_framework_azure/_shared.py +++ b/python/packages/azure/agent_framework_azure/_shared.py @@ -8,8 +8,9 @@ from typing import Any, ClassVar, Final from agent_framework._pydantic import AFBaseSettings, HttpsUrl from agent_framework.exceptions import ServiceInitializationError -from agent_framework.openai._shared import OpenAIHandler, OpenAIModelTypes +from agent_framework.openai._shared import OpenAIHandler from agent_framework.telemetry import USER_AGENT_KEY +from azure.identity import ChainedTokenCredential from openai.lib.azure import AsyncAzureOpenAI from pydantic import ConfigDict, SecretStr, model_validator, validate_call @@ -20,6 +21,7 @@ if sys.version_info >= (3, 11): else: from typing_extensions import Self # pragma: no cover + logger: logging.Logger = logging.getLogger(__name__) @@ -132,7 +134,9 @@ class AzureOpenAISettings(AFBaseSettings): default_api_version: str = DEFAULT_AZURE_API_VERSION default_token_endpoint: str = DEFAULT_AZURE_TOKEN_ENDPOINT - def get_azure_openai_auth_token(self, token_endpoint: str | None = None) -> str | None: + def get_azure_auth_token( + self, credential: "ChainedTokenCredential", token_endpoint: str | None = None, **kwargs: Any + ) -> str | None: """Retrieve a Microsoft Entra Auth Token for a given token endpoint for the use with Azure OpenAI. The required role for the token is `Cognitive Services OpenAI Contributor`. @@ -141,7 +145,9 @@ class AzureOpenAISettings(AFBaseSettings): The `token_endpoint` argument takes precedence over the `token_endpoint` attribute. Args: + credential: The Azure AD credential to use. token_endpoint: The token endpoint to use. Defaults to `https://cognitiveservices.azure.com/.default`. + **kwargs: Additional keyword arguments to pass to the token retrieval method. Returns: The Azure token or None if the token could not be retrieved. @@ -149,10 +155,8 @@ class AzureOpenAISettings(AFBaseSettings): Raises: ServiceInitializationError: If the token endpoint is not provided. """ - endpoint_to_use = token_endpoint or self.token_endpoint - if endpoint_to_use is None: # type: ignore - raise ServiceInitializationError("Please provide a token endpoint to retrieve the authentication token.") - return get_entra_auth_token(endpoint_to_use) + endpoint_to_use = token_endpoint or self.token_endpoint or self.default_token_endpoint + return get_entra_auth_token(credential, endpoint_to_use, **kwargs) @model_validator(mode="after") def _validate_fields(self) -> Self: @@ -164,11 +168,12 @@ class AzureOpenAISettings(AFBaseSettings): class AzureOpenAIConfigBase(OpenAIHandler): """Internal class for configuring a connection to an Azure OpenAI service.""" + MODEL_PROVIDER_NAME: ClassVar[str] = "azure_openai" # type: ignore[reportIncompatibleVariableOverride, misc] + @validate_call(config=ConfigDict(arbitrary_types_allowed=True)) def __init__( self, deployment_name: str, - ai_model_type: OpenAIModelTypes, endpoint: HttpsUrl | None = None, base_url: HttpsUrl | None = None, api_version: str = DEFAULT_AZURE_API_VERSION, @@ -176,6 +181,7 @@ class AzureOpenAIConfigBase(OpenAIHandler): ad_token: str | None = None, ad_token_provider: Callable[[], str | Awaitable[str]] | None = None, token_endpoint: str | None = None, + ad_credential: ChainedTokenCredential | None = None, default_headers: Mapping[str, str] | None = None, client: AsyncAzureOpenAI | None = None, instruction_role: str | None = None, @@ -187,20 +193,20 @@ class AzureOpenAIConfigBase(OpenAIHandler): This is necessary for types like `HttpsUrl` and `OpenAIModelTypes`. Args: - deployment_name (str): Name of the deployment. - ai_model_type (OpenAIModelTypes): The type of OpenAI model to deploy. - endpoint (HttpsUrl): The specific endpoint URL for the deployment. (Optional) - base_url (Url): The base URL for Azure services. (Optional) - api_version (str): Azure API version. Defaults to the defined DEFAULT_AZURE_API_VERSION. - api_key (str): API key for Azure services. (Optional) - ad_token (str): Azure AD token for authentication. (Optional) - ad_token_provider (Callable[[], Union[str, Awaitable[str]]]): A callable - or coroutine function providing Azure AD tokens. (Optional) - token_endpoint (str): Azure AD token endpoint use to get the token. (Optional) - default_headers (Union[Mapping[str, str], None]): Default headers for HTTP requests. (Optional) - client (AsyncAzureOpenAI): An existing client to use. (Optional) - instruction_role (str | None): The role to use for 'instruction' messages, for example, summarization - prompts could use `developer` or `system`. (Optional) + deployment_name: Name of the deployment. + ai_model_type: The type of OpenAI model to deploy. + endpoint: The specific endpoint URL for the deployment. + base_url: The base URL for Azure services. + api_version: Azure API version. Defaults to the defined DEFAULT_AZURE_API_VERSION. + api_key: API key for Azure services. + ad_token: Azure AD token for authentication. + ad_token_provider: A callable or coroutine function providing Azure AD tokens. + token_endpoint: Azure AD token endpoint use to get the token. + ad_credential: Azure AD credential for authentication. + default_headers: Default headers for HTTP requests. + client: An existing client to use. + instruction_role: The role to use for 'instruction' messages, for example, summarization + prompts could use `developer` or `system`. kwargs: Additional keyword arguments. """ @@ -211,8 +217,8 @@ class AzureOpenAIConfigBase(OpenAIHandler): # If the client is None, the api_key is none, the ad_token is none, and the ad_token_provider is none, # then we will attempt to get the ad_token using the default endpoint specified in the Azure OpenAI # settings. - if not api_key and not ad_token_provider and not ad_token and token_endpoint: - ad_token = get_entra_auth_token(token_endpoint) + if not api_key and not ad_token_provider and not ad_token and token_endpoint and ad_credential: + ad_token = get_entra_auth_token(ad_credential, token_endpoint) if not api_key and not ad_token and not ad_token_provider: raise ServiceInitializationError( @@ -237,10 +243,8 @@ class AzureOpenAIConfigBase(OpenAIHandler): args["base_url"] = str(base_url) if endpoint and not base_url: args["azure_endpoint"] = str(endpoint) - # TODO (eavanvalkenburg): Remove the check on model type when the package fixes: https://github.com/openai/openai-python/issues/2120 - if deployment_name and ai_model_type != OpenAIModelTypes.REALTIME: + if deployment_name: args["azure_deployment"] = deployment_name - if "websocket_base_url" in kwargs: args["websocket_base_url"] = kwargs.pop("websocket_base_url") @@ -248,7 +252,6 @@ class AzureOpenAIConfigBase(OpenAIHandler): args = { "ai_model_id": deployment_name, "client": client, - "ai_model_type": ai_model_type, } if instruction_role: args["instruction_role"] = instruction_role @@ -271,7 +274,6 @@ class AzureOpenAIConfigBase(OpenAIHandler): "total_tokens", "api_type", "org_id", - "ai_model_type", "service_id", "client", }, diff --git a/python/packages/azure/tests/test_azure_assistants_client.py b/python/packages/azure/tests/test_azure_assistants_client.py index 5ecaf88580..59bdae240e 100644 --- a/python/packages/azure/tests/test_azure_assistants_client.py +++ b/python/packages/azure/tests/test_azure_assistants_client.py @@ -13,6 +13,7 @@ from agent_framework import ( TextContent, ) from agent_framework.exceptions import ServiceInitializationError +from azure.identity import DefaultAzureCredential from pydantic import Field from agent_framework_azure import AzureAssistantsClient @@ -259,7 +260,7 @@ def get_weather( @skip_if_azure_integration_tests_disabled async def test_azure_assistants_client_get_response() -> None: """Test Azure Assistants Client response.""" - async with AzureAssistantsClient() as azure_assistants_client: + async with AzureAssistantsClient(ad_credential=DefaultAzureCredential()) as azure_assistants_client: assert isinstance(azure_assistants_client, ChatClient) messages: list[ChatMessage] = [] @@ -283,7 +284,7 @@ async def test_azure_assistants_client_get_response() -> None: @skip_if_azure_integration_tests_disabled async def test_azure_assistants_client_get_response_tools() -> None: """Test Azure Assistants Client response with tools.""" - async with AzureAssistantsClient() as azure_assistants_client: + async with AzureAssistantsClient(ad_credential=DefaultAzureCredential()) as azure_assistants_client: assert isinstance(azure_assistants_client, ChatClient) messages: list[ChatMessage] = [] @@ -304,7 +305,7 @@ async def test_azure_assistants_client_get_response_tools() -> None: @skip_if_azure_integration_tests_disabled async def test_azure_assistants_client_streaming() -> None: """Test Azure Assistants Client streaming response.""" - async with AzureAssistantsClient() as azure_assistants_client: + async with AzureAssistantsClient(ad_credential=DefaultAzureCredential()) as azure_assistants_client: assert isinstance(azure_assistants_client, ChatClient) messages: list[ChatMessage] = [] @@ -334,7 +335,7 @@ async def test_azure_assistants_client_streaming() -> None: @skip_if_azure_integration_tests_disabled async def test_azure_assistants_client_streaming_tools() -> None: """Test Azure Assistants Client streaming response with tools.""" - async with AzureAssistantsClient() as azure_assistants_client: + async with AzureAssistantsClient(ad_credential=DefaultAzureCredential()) as azure_assistants_client: assert isinstance(azure_assistants_client, ChatClient) messages: list[ChatMessage] = [] @@ -361,14 +362,16 @@ async def test_azure_assistants_client_streaming_tools() -> None: async def test_azure_assistants_client_with_existing_assistant() -> None: """Test Azure Assistants Client with existing assistant ID.""" # First create an assistant to use in the test - async with AzureAssistantsClient() as temp_client: + async with AzureAssistantsClient(ad_credential=DefaultAzureCredential()) as temp_client: # Get the assistant ID by triggering assistant creation messages = [ChatMessage(role="user", text="Hello")] await temp_client.get_response(messages=messages) assistant_id = temp_client.assistant_id # Now test using the existing assistant - async with AzureAssistantsClient(assistant_id=assistant_id) as azure_assistants_client: + async with AzureAssistantsClient( + assistant_id=assistant_id, ad_credential=DefaultAzureCredential() + ) as azure_assistants_client: assert isinstance(azure_assistants_client, ChatClient) assert azure_assistants_client.assistant_id == assistant_id diff --git a/python/packages/azure/tests/test_azure_chat_client.py b/python/packages/azure/tests/test_azure_chat_client.py index a36ecedcd9..f4d30972e1 100644 --- a/python/packages/azure/tests/test_azure_chat_client.py +++ b/python/packages/azure/tests/test_azure_chat_client.py @@ -12,8 +12,6 @@ from agent_framework import ( ChatMessage, ChatResponse, ChatResponseUpdate, - FunctionCallContent, - FunctionResultContent, TextContent, ai_function, ) @@ -23,6 +21,7 @@ from agent_framework.openai import ( OpenAIContentFilterException, ) from agent_framework.telemetry import USER_AGENT_KEY +from azure.identity import DefaultAzureCredential from httpx import Request, Response from openai import AsyncAzureOpenAI, AsyncStream from openai.resources.chat.completions import AsyncCompletions as AsyncChatCompletions @@ -123,6 +122,7 @@ def test_serialize(azure_openai_unit_test_env: dict[str, str]) -> None: "api_key": azure_openai_unit_test_env["AZURE_OPENAI_API_KEY"], "api_version": azure_openai_unit_test_env["AZURE_OPENAI_API_VERSION"], "default_headers": default_headers, + "env_file_path": "test.env", } azure_chat_client = AzureChatClient.from_dict(settings) @@ -258,13 +258,15 @@ async def test_azure_on_your_data( content="test", role="assistant", context={ # type: ignore - "citations": { - "content": "test content", - "title": "test title", - "url": "test url", - "filepath": "test filepath", - "chunk_id": "test chunk_id", - }, + "citations": [ + { + "content": "test content", + "title": "test title", + "url": "test url", + "filepath": "test filepath", + "chunk_id": "test chunk_id", + } + ], "intent": "query used", }, ), @@ -298,11 +300,11 @@ async def test_azure_on_your_data( additional_properties={"extra_body": expected_data_settings}, ) assert len(content.messages) == 1 - assert len(content.messages[0].contents) == 3 - assert isinstance(content.messages[0].contents[0], FunctionCallContent) - assert isinstance(content.messages[0].contents[1], FunctionResultContent) - assert isinstance(content.messages[0].contents[2], TextContent) - assert content.messages[0].contents[2].text == "test" + assert len(content.messages[0].contents) == 1 + assert isinstance(content.messages[0].contents[0], TextContent) + assert len(content.messages[0].contents[0].annotations) == 1 + assert content.messages[0].contents[0].annotations[0].title == "test title" + assert content.messages[0].contents[0].text == "test" mock_create.assert_awaited_once_with( model=azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"], @@ -326,13 +328,15 @@ async def test_azure_on_your_data_string( content="test", role="assistant", context=json.dumps({ # type: ignore - "citations": { - "content": "test content", - "title": "test title", - "url": "test url", - "filepath": "test filepath", - "chunk_id": "test chunk_id", - }, + "citations": [ + { + "content": "test content", + "title": "test title", + "url": "test url", + "filepath": "test filepath", + "chunk_id": "test chunk_id", + } + ], "intent": "query used", }), ), @@ -366,11 +370,11 @@ async def test_azure_on_your_data_string( additional_properties={"extra_body": expected_data_settings}, ) assert len(content.messages) == 1 - assert len(content.messages[0].contents) == 3 - assert isinstance(content.messages[0].contents[0], FunctionCallContent) - assert isinstance(content.messages[0].contents[1], FunctionResultContent) - assert isinstance(content.messages[0].contents[2], TextContent) - assert content.messages[0].contents[2].text == "test" + assert len(content.messages[0].contents) == 1 + assert isinstance(content.messages[0].contents[0], TextContent) + assert len(content.messages[0].contents[0].annotations) == 1 + assert content.messages[0].contents[0].annotations[0].title == "test title" + assert content.messages[0].contents[0].text == "test" mock_create.assert_awaited_once_with( model=azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"], @@ -437,55 +441,6 @@ async def test_azure_on_your_data_fail( ) -@patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock) -async def test_azure_on_your_data_split_messages( - mock_create: AsyncMock, - azure_openai_unit_test_env: dict[str, str], - chat_history: list[ChatMessage], - mock_chat_completion_response: ChatCompletion, -) -> None: - mock_chat_completion_response.choices = [ - Choice( - index=0, - message=ChatCompletionMessage( - content="test", - role="assistant", - context={ # type: ignore - "citations": { - "content": "test content", - "title": "test title", - "url": "test url", - "filepath": "test filepath", - "chunk_id": "test chunk_id", - }, - "intent": "query used", - }, - ), - finish_reason="stop", - ) - ] - mock_create.return_value = mock_chat_completion_response - prompt = "hello world" - messages_in = chat_history - messages_in.append(ChatMessage(text=prompt, role="user")) - messages_out: list[ChatMessage] = [] - messages_out.append(ChatMessage(text=prompt, role="user")) - - azure_chat_client = AzureChatClient() - - content = await azure_chat_client.get_response( - messages=messages_in, - ) - message = azure_chat_client._split_message(content) - assert len(content.messages) == 1 - assert len(content.messages[0].contents) == 3 - assert isinstance(content.messages[0].contents[0], FunctionCallContent) - assert isinstance(content.messages[0].contents[1], FunctionResultContent) - assert isinstance(content.messages[0].contents[2], TextContent) - assert content.messages[0].contents[2].text == "test" - assert message.messages[0].contents == [content.messages[0].contents[0]] - - CONTENT_FILTERED_ERROR_MESSAGE = ( "The response was filtered due to the prompt triggering Azure OpenAI's content management policy. Please " "modify your prompt and retry. To learn more about our content filtering policies please read our " @@ -607,7 +562,7 @@ async def test_bad_request_non_content_filter( @patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock) -async def test_cmc_streaming( +async def test_get_streaming( mock_create: AsyncMock, azure_openai_unit_test_env: dict[str, str], chat_history: list[ChatMessage], @@ -646,7 +601,7 @@ def get_story_text() -> str: @skip_if_azure_integration_tests_disabled async def test_azure_openai_chat_client_response() -> None: """Test Azure OpenAI chat completion responses.""" - azure_chat_client = AzureChatClient() + azure_chat_client = AzureChatClient(ad_credential=DefaultAzureCredential()) assert isinstance(azure_chat_client, ChatClient) messages: list[ChatMessage] = [] @@ -672,7 +627,7 @@ async def test_azure_openai_chat_client_response() -> None: @skip_if_azure_integration_tests_disabled async def test_azure_openai_chat_client_response_tools() -> None: """Test AzureOpenAI chat completion responses.""" - azure_chat_client = AzureChatClient() + azure_chat_client = AzureChatClient(ad_credential=DefaultAzureCredential()) assert isinstance(azure_chat_client, ChatClient) messages: list[ChatMessage] = [] @@ -693,7 +648,7 @@ async def test_azure_openai_chat_client_response_tools() -> None: @skip_if_azure_integration_tests_disabled async def test_azure_openai_chat_client_streaming() -> None: """Test Azure OpenAI chat completion responses.""" - azure_chat_client = AzureChatClient() + azure_chat_client = AzureChatClient(ad_credential=DefaultAzureCredential()) assert isinstance(azure_chat_client, ChatClient) messages: list[ChatMessage] = [] @@ -725,7 +680,7 @@ async def test_azure_openai_chat_client_streaming() -> None: @skip_if_azure_integration_tests_disabled async def test_azure_openai_chat_client_streaming_tools() -> None: """Test AzureOpenAI chat completion responses.""" - azure_chat_client = AzureChatClient() + azure_chat_client = AzureChatClient(ad_credential=DefaultAzureCredential()) assert isinstance(azure_chat_client, ChatClient) messages: list[ChatMessage] = [] diff --git a/python/packages/azure/tests/test_azure_responses_client.py b/python/packages/azure/tests/test_azure_responses_client.py index 10dc33842a..7380a18cf7 100644 --- a/python/packages/azure/tests/test_azure_responses_client.py +++ b/python/packages/azure/tests/test_azure_responses_client.py @@ -6,7 +6,8 @@ from typing import Annotated import pytest from agent_framework import ChatClient, ChatMessage, ChatResponse, ChatResponseUpdate, TextContent, ai_function from agent_framework.azure import AzureResponsesClient -from agent_framework.exceptions import ServiceInitializationError, ServiceResponseException +from agent_framework.exceptions import ServiceInitializationError +from azure.identity import DefaultAzureCredential from pydantic import BaseModel skip_if_azure_integration_tests_disabled = pytest.mark.skipif( @@ -104,7 +105,7 @@ def test_serialize(azure_openai_unit_test_env: dict[str, str]) -> None: @skip_if_azure_integration_tests_disabled async def test_azure_responses_client_response() -> None: """Test azure responses client responses.""" - azure_responses_client = AzureResponsesClient() + azure_responses_client = AzureResponsesClient(ad_credential=DefaultAzureCredential()) assert isinstance(azure_responses_client, ChatClient) @@ -147,7 +148,7 @@ async def test_azure_responses_client_response() -> None: @skip_if_azure_integration_tests_disabled async def test_azure_responses_client_response_tools() -> None: """Test azure responses client tools.""" - azure_responses_client = AzureResponsesClient() + azure_responses_client = AzureResponsesClient(ad_credential=DefaultAzureCredential()) assert isinstance(azure_responses_client, ChatClient) @@ -186,7 +187,7 @@ async def test_azure_responses_client_response_tools() -> None: @skip_if_azure_integration_tests_disabled async def test_azure_responses_client_streaming() -> None: """Test Azure azure responses client streaming responses.""" - azure_responses_client = AzureResponsesClient() + azure_responses_client = AzureResponsesClient(ad_credential=DefaultAzureCredential()) assert isinstance(azure_responses_client, ChatClient) @@ -219,29 +220,27 @@ 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?")) - # This is currently broken. See https://github.com/azure/azure-python/issues/2305 - with pytest.raises(ServiceResponseException): - response = azure_responses_client.get_streaming_response( - messages=messages, - response_format=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 + response = azure_responses_client.get_streaming_response( + messages=messages, + response_format=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() + output = OutputStruct.model_validate_json(full_message) + assert "Seattle" in output.location + assert "sunny" in output.weather.lower() @skip_if_azure_integration_tests_disabled async def test_azure_responses_client_streaming_tools() -> None: """Test azure responses client streaming tools.""" - azure_responses_client = AzureResponsesClient() + azure_responses_client = AzureResponsesClient(ad_credential=DefaultAzureCredential()) assert isinstance(azure_responses_client, ChatClient) @@ -266,22 +265,20 @@ async def test_azure_responses_client_streaming_tools() -> None: messages.clear() messages.append(ChatMessage(role="user", text="What is the weather in Seattle?")) - # This is currently broken. See https://github.com/azure/azure-python/issues/2305 - with pytest.raises(ServiceResponseException): - 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: - 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 + 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: + 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() + output = OutputStruct.model_validate_json(full_message) + assert "Seattle" in output.location + assert "sunny" in output.weather.lower() diff --git a/python/packages/foundry/agent_framework_foundry/_chat_client.py b/python/packages/foundry/agent_framework_foundry/_chat_client.py index d01c030b67..223de14c04 100644 --- a/python/packages/foundry/agent_framework_foundry/_chat_client.py +++ b/python/packages/foundry/agent_framework_foundry/_chat_client.py @@ -4,7 +4,7 @@ import contextlib import json import sys from collections.abc import AsyncIterable, MutableMapping, MutableSequence -from typing import Any, ClassVar +from typing import Any, ClassVar, TypeVar from agent_framework import ( AIContents, @@ -26,7 +26,6 @@ from agent_framework import ( UsageDetails, use_tool_calling, ) -from agent_framework._clients import ai_function_to_json_schema_spec from agent_framework._pydantic import AFBaseSettings from agent_framework.exceptions import ServiceInitializationError from agent_framework.telemetry import use_telemetry @@ -93,6 +92,9 @@ class FoundrySettings(AFBaseSettings): agent_name: str | None = "UnnamedAgent" +TFoundryChatClient = TypeVar("TFoundryChatClient", bound="FoundryChatClient") + + @use_telemetry @use_tool_calling class FoundryChatClient(ChatClientBase): @@ -102,21 +104,22 @@ class FoundryChatClient(ChatClientBase): client: AIProjectClient = Field(...) credential: AsyncTokenCredential | None = Field(...) agent_id: str | None = Field(default=None) + agent_name: str | None = Field(default=None) + ai_model_deployment_name: str | None = Field(default=None) thread_id: str | None = Field(default=None) _should_delete_agent: bool = PrivateAttr(default=False) # Track whether we should delete the agent _should_close_client: bool = PrivateAttr(default=False) # Track whether we should close client connection - _should_close_credential: bool = PrivateAttr(default=False) # Track whether we should close credential - _foundry_settings: FoundrySettings = PrivateAttr() def __init__( self, + *, client: AIProjectClient | None = None, agent_id: str | None = None, agent_name: str | None = None, thread_id: str | None = None, project_endpoint: str | None = None, model_deployment_name: str | None = None, - credential: AsyncTokenCredential | None = None, + async_ad_credential: AsyncTokenCredential | None = None, env_file_path: str | None = None, env_file_encoding: str | None = None, **kwargs: Any, @@ -133,8 +136,7 @@ class FoundryChatClient(ChatClientBase): conversation_id property, when making a request. project_endpoint: The Azure AI Foundry project endpoint URL. Used if client is not provided. model_deployment_name: The model deployment name to use for agent creation. - credential: Azure async credential to use for authentication. If not provided, - DefaultAzureCredential will be used. + async_ad_credential: Azure async credential to use for authentication. env_file_path: Path to environment file for loading settings. env_file_encoding: Encoding of the environment file. **kwargs: Additional keyword arguments passed to the parent class. @@ -152,7 +154,6 @@ class FoundryChatClient(ChatClientBase): # If no client is provided, create one should_close_client = False - should_close_credential = False if client is None: if not foundry_settings.project_endpoint: raise ServiceInitializationError("Project endpoint is required when client is not provided.") @@ -161,27 +162,20 @@ class FoundryChatClient(ChatClientBase): raise ServiceInitializationError("Model deployment name is required for agent creation.") # Use provided credential or fallback to DefaultAzureCredential - if credential is None: - from azure.identity.aio import DefaultAzureCredential - - credential = DefaultAzureCredential() - should_close_credential = True - - client = AIProjectClient(endpoint=foundry_settings.project_endpoint, credential=credential) - should_close_client = True + if not async_ad_credential: + raise ServiceInitializationError("Azure AD credential is required when client is not provided.") + client = AIProjectClient(endpoint=foundry_settings.project_endpoint, credential=async_ad_credential) super().__init__( client=client, # type: ignore[reportCallIssue] - credential=credential, # type: ignore[reportCallIssue] + credential=async_ad_credential, # type: ignore[reportCallIssue] agent_id=agent_id, # type: ignore[reportCallIssue] thread_id=thread_id, # type: ignore[reportCallIssue] + agent_name=foundry_settings.agent_name, # type: ignore[reportCallIssue] + ai_model_deployment_name=foundry_settings.model_deployment_name, # type: ignore[reportCallIssue] **kwargs, ) - - self._should_delete_agent = False self._should_close_client = should_close_client - self._should_close_credential = should_close_credential - self._foundry_settings = foundry_settings async def __aenter__(self) -> "Self": """Async context manager entry.""" @@ -195,10 +189,9 @@ class FoundryChatClient(ChatClientBase): """Close the client and clean up any agents we created.""" await self._cleanup_agent_if_needed() await self._close_client_if_needed() - await self._close_credential_if_needed() @classmethod - def from_dict(cls, settings: dict[str, Any]) -> "FoundryChatClient": + def from_dict(cls: type[TFoundryChatClient], settings: dict[str, Any]) -> TFoundryChatClient: """Initialize a FoundryChatClient from a dictionary of settings. Args: @@ -262,11 +255,11 @@ class FoundryChatClient(ChatClientBase): """ # If no agent_id is provided, create a temporary agent if self.agent_id is None: - if not self._foundry_settings.model_deployment_name: + if not self.ai_model_deployment_name: raise ServiceInitializationError("Model deployment name is required for agent creation.") - agent_name = self._foundry_settings.agent_name - args = {"model": self._foundry_settings.model_deployment_name, "name": agent_name} + agent_name = self.agent_name + args = {"model": self.ai_model_deployment_name, "name": agent_name} if run_options: if "tools" in run_options: args["tools"] = run_options["tools"] @@ -459,11 +452,6 @@ class FoundryChatClient(ChatClientBase): return contents - async def _close_credential_if_needed(self) -> None: - """Close credential if we created it.""" - if self._should_close_credential and self.credential is not None: - await self.credential.close() - async def _close_client_if_needed(self) -> None: """Close client session if we created it.""" if self._should_close_client: @@ -496,7 +484,7 @@ class FoundryChatClient(ChatClientBase): if chat_options.tool_choice != "none" and chat_options.tools is not None: for tool in chat_options.tools: if isinstance(tool, AIFunction): - tool_definitions.append(ai_function_to_json_schema_spec(tool)) # type: ignore[reportUnknownArgumentType] + tool_definitions.append(tool.to_json_schema_spec()) # type: ignore[reportUnknownArgumentType] elif isinstance(tool, HostedCodeInterpreterTool): tool_definitions.append(CodeInterpreterToolDefinition()) elif isinstance(tool, MutableMapping): @@ -605,3 +593,14 @@ class FoundryChatClient(ChatClientBase): tool_outputs.append(ToolOutput(tool_call_id=call_id, output=str(function_result_content.result))) return run_id, tool_outputs + + def _update_agent_name(self, agent_name: str | None) -> None: + """Update the agent name in the chat client. + + Args: + agent_name: The new name for the agent. + """ + # This is a no-op in the base class, but can be overridden by subclasses + # to update the agent name in the client. + if agent_name and not self.agent_name: + self.agent_name = agent_name diff --git a/python/packages/foundry/tests/test_foundry_chat_client.py b/python/packages/foundry/tests/test_foundry_chat_client.py index f1a32690b1..d0d71393be 100644 --- a/python/packages/foundry/tests/test_foundry_chat_client.py +++ b/python/packages/foundry/tests/test_foundry_chat_client.py @@ -15,6 +15,7 @@ from agent_framework import ( TextContent, ) from agent_framework.exceptions import ServiceInitializationError +from azure.identity.aio import DefaultAzureCredential from pydantic import Field from agent_framework_foundry import FoundryChatClient, FoundrySettings @@ -44,7 +45,8 @@ def create_test_foundry_chat_client( agent_id=agent_id, thread_id=thread_id, _should_delete_agent=should_delete_agent, - _foundry_settings=foundry_settings, + agent_name=foundry_settings.agent_name, # type: ignore[reportCallIssue] + ai_model_deployment_name=foundry_settings.model_deployment_name, # type: credential=None, ) @@ -304,7 +306,7 @@ def get_weather( @skip_if_foundry_integration_tests_disabled async def test_foundry_chat_client_get_response() -> None: """Test Foundry Chat Client response.""" - async with FoundryChatClient() as foundry_chat_client: + async with FoundryChatClient(async_ad_credential=DefaultAzureCredential()) as foundry_chat_client: assert isinstance(foundry_chat_client, ChatClient) messages: list[ChatMessage] = [] @@ -328,7 +330,7 @@ async def test_foundry_chat_client_get_response() -> None: @skip_if_foundry_integration_tests_disabled async def test_foundry_chat_client_get_response_tools() -> None: """Test Foundry Chat Client response with tools.""" - async with FoundryChatClient() as foundry_chat_client: + async with FoundryChatClient(async_ad_credential=DefaultAzureCredential()) as foundry_chat_client: assert isinstance(foundry_chat_client, ChatClient) messages: list[ChatMessage] = [] @@ -349,7 +351,7 @@ async def test_foundry_chat_client_get_response_tools() -> None: @skip_if_foundry_integration_tests_disabled async def test_foundry_chat_client_streaming() -> None: """Test Foundry Chat Client streaming response.""" - async with FoundryChatClient() as foundry_chat_client: + async with FoundryChatClient(async_ad_credential=DefaultAzureCredential()) as foundry_chat_client: assert isinstance(foundry_chat_client, ChatClient) messages: list[ChatMessage] = [] @@ -379,7 +381,7 @@ async def test_foundry_chat_client_streaming() -> None: @skip_if_foundry_integration_tests_disabled async def test_foundry_chat_client_streaming_tools() -> None: """Test Foundry Chat Client streaming response with tools.""" - async with FoundryChatClient() as foundry_chat_client: + async with FoundryChatClient(async_ad_credential=DefaultAzureCredential()) as foundry_chat_client: assert isinstance(foundry_chat_client, ChatClient) messages: list[ChatMessage] = [] diff --git a/python/packages/main/agent_framework/_agents.py b/python/packages/main/agent_framework/_agents.py index 7e3843a013..bb0add314b 100644 --- a/python/packages/main/agent_framework/_agents.py +++ b/python/packages/main/agent_framework/_agents.py @@ -381,6 +381,8 @@ class ChatClientAgent(AgentBase): kwargs: any additional keyword arguments. Unused, can be used by subclasses of this Agent. """ + kwargs.update(additional_properties or {}) + args: dict[str, Any] = { "chat_client": chat_client, "chat_options": ChatOptions( @@ -399,7 +401,7 @@ class ChatClientAgent(AgentBase): tools=tools, # type: ignore top_p=top_p, user=user, - additional_properties=additional_properties or {}, + additional_properties=kwargs, ), } if instructions is not None: @@ -412,6 +414,7 @@ class ChatClientAgent(AgentBase): args["id"] = id super().__init__(**args) + self._update_agent_name() async def __aenter__(self) -> "Self": """Async context manager entry. @@ -430,6 +433,16 @@ class ChatClientAgent(AgentBase): if isinstance(self.chat_client, AbstractAsyncContextManager): await self.chat_client.__aexit__(exc_type, exc_val, exc_tb) # type: ignore[reportUnknownMemberType] + def _update_agent_name(self) -> None: + """Update the agent name in a chat client. + + Checks if there is a agent name, the implementation + should check if there is already a agent name defined, and if not + set it to this value. + """ + if hasattr(self.chat_client, "_update_agent_name") and callable(self.chat_client._update_agent_name): # type: ignore[reportAttributeAccessIssue] + self.chat_client._update_agent_name(self.name) # type: ignore[reportAttributeAccessIssue] + async def run( self, messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None, diff --git a/python/packages/main/agent_framework/_clients.py b/python/packages/main/agent_framework/_clients.py index 2919262de2..031e23a9f6 100644 --- a/python/packages/main/agent_framework/_clients.py +++ b/python/packages/main/agent_framework/_clients.py @@ -73,18 +73,6 @@ async def _auto_invoke_function( ) -def ai_function_to_json_schema_spec(function: AIFunction[BaseModel, Any]) -> dict[str, Any]: - """Convert a AIFunction to the JSON Schema function specification format.""" - return { - "type": "function", - "function": { - "name": function.name, - "description": function.description, - "parameters": function.parameters(), - }, - } - - def _tool_call_non_streaming( func: Callable[..., Awaitable["ChatResponse"]], ) -> Callable[..., Awaitable["ChatResponse"]]: @@ -117,14 +105,15 @@ def _tool_call_non_streaming( _auto_invoke_function( function_call, custom_args=kwargs, - tool_map={t.name: t for t in chat_options._ai_tools or [] if isinstance(t, AIFunction)}, # type: ignore[reportPrivateUsage] + tool_map={t.name: t for t in chat_options.tools or [] if isinstance(t, AIFunction)}, # type: ignore[reportPrivateUsage] sequence_index=seq_idx, request_index=attempt_idx, ) for seq_idx, function_call in enumerate(function_calls) ]) # add a single ChatMessage to the response with the results - response.messages.append(ChatMessage(role="tool", contents=results)) + result_message = ChatMessage(role="tool", contents=results) + response.messages.append(result_message) # response should contain 2 messages after this, # one with function call contents # and one with function result contents @@ -133,7 +122,11 @@ def _tool_call_non_streaming( # we need to keep track of all function call messages fcc_messages.extend(response.messages) # and add them as additional context to the messages - messages.extend(response.messages) + if chat_options.store: + messages.clear() + messages.append(result_message) + else: + messages.extend(response.messages) continue # If we reach this point, it means there were no function calls to handle, # we'll add the previous function call and responses @@ -146,7 +139,7 @@ def _tool_call_non_streaming( # Failsafe: give up on tools, ask model for plain answer chat_options.tool_choice = "none" - self._prepare_tools_and_tool_choice(chat_options=chat_options) # type: ignore[reportPrivateUsage] + self._prepare_tool_choice(chat_options=chat_options) # type: ignore[reportPrivateUsage] response = await func(self, messages=messages, chat_options=chat_options) if fcc_messages: for msg in reversed(fcc_messages): @@ -202,7 +195,7 @@ def _tool_call_streaming( _auto_invoke_function( function_call, custom_args=kwargs, - tool_map={t.name: t for t in chat_options._ai_tools or [] if isinstance(t, AIFunction)}, # type: ignore[reportPrivateUsage] + tool_map={t.name: t for t in chat_options.tools or [] if isinstance(t, AIFunction)}, # type: ignore[reportPrivateUsage] sequence_index=seq_idx, request_index=attempt_idx, ) @@ -216,7 +209,7 @@ def _tool_call_streaming( # Failsafe: give up on tools, ask model for plain answer chat_options.tool_choice = "none" - self._prepare_tools_and_tool_choice(chat_options=chat_options) # type: ignore[reportPrivateUsage] + self._prepare_tool_choice(chat_options=chat_options) # type: ignore[reportPrivateUsage] async for update in func(self, messages=messages, chat_options=chat_options, **kwargs): yield update @@ -529,7 +522,7 @@ class ChatClientBase(AFBaseModel, ABC): additional_properties=additional_properties or {}, ) prepped_messages = self._prepare_messages(messages) - self._prepare_tools_and_tool_choice(chat_options=chat_options) + self._prepare_tool_choice(chat_options=chat_options) return await self._inner_get_response(messages=prepped_messages, chat_options=chat_options, **kwargs) async def get_streaming_response( @@ -610,13 +603,13 @@ class ChatClientBase(AFBaseModel, ABC): **kwargs, ) prepped_messages = self._prepare_messages(messages) - self._prepare_tools_and_tool_choice(chat_options=chat_options) + self._prepare_tool_choice(chat_options=chat_options) async for update in self._inner_get_streaming_response( messages=prepped_messages, chat_options=chat_options, **kwargs ): yield update - def _prepare_tools_and_tool_choice(self, chat_options: ChatOptions) -> None: + def _prepare_tool_choice(self, chat_options: ChatOptions) -> None: """Prepare the tools and tool choice for the chat options. This function should be overridden by subclasses to customize tool handling. @@ -627,10 +620,6 @@ class ChatClientBase(AFBaseModel, ABC): chat_options.tools = None chat_options.tool_choice = ChatToolMode.NONE.mode return - chat_options.tools = [ - (ai_function_to_json_schema_spec(t) if isinstance(t, AIFunction) else t) # type: ignore[reportUnknownArgumentType] - for t in chat_options._ai_tools or [] # type: ignore[reportPrivateUsage] - ] if not chat_options.tools: chat_options.tool_choice = ChatToolMode.NONE.mode else: @@ -647,8 +636,8 @@ class ChatClientBase(AFBaseModel, ABC): def create_agent( self, *, - name: str, - instructions: str, + name: str | None = None, + instructions: str | None = None, tools: AITool | list[AITool] | Callable[..., Any] diff --git a/python/packages/main/agent_framework/_tools.py b/python/packages/main/agent_framework/_tools.py index fd4c76a7bc..9059367471 100644 --- a/python/packages/main/agent_framework/_tools.py +++ b/python/packages/main/agent_framework/_tools.py @@ -7,9 +7,10 @@ from time import perf_counter from typing import TYPE_CHECKING, Annotated, Any, Generic, Protocol, TypeVar, get_args, get_origin, runtime_checkable from opentelemetry import metrics, trace -from pydantic import BaseModel, Field, create_model +from pydantic import BaseModel, Field, PrivateAttr, create_model from ._logging import get_logger +from ._pydantic import AFBaseModel from .telemetry import GenAIAttributes, start_as_current_span if TYPE_CHECKING: @@ -22,6 +23,48 @@ logger = get_logger() __all__ = ["AIFunction", "AITool", "HostedCodeInterpreterTool", "ai_function"] +def _parse_inputs( + inputs: "AIContents | dict[str, Any] | str | list[AIContents | dict[str, Any] | str] | None", +) -> list["AIContents"]: + """Parse the inputs for a tool, ensuring they are of type AIContents.""" + if inputs is None: + return [] + + from ._types import AIContent, DataContent, HostedFileContent, HostedVectorStoreContent, UriContent + + parsed_inputs: list["AIContents"] = [] + if not isinstance(inputs, list): + inputs = [inputs] + for input_item in inputs: + if isinstance(input_item, str): + # If it's a string, we assume it's a URI or similar identifier. + # Convert it to a UriContent or similar type as needed. + parsed_inputs.append(UriContent(uri=input_item, media_type="text/plain")) + elif isinstance(input_item, dict): + # If it's a dict, we assume it contains properties for a specific content type. + # we check if the required keys are present to determine the type. + # for instance, if it has "uri" and "media_type", we treat it as UriContent. + # if is only has uri, then we treat it as DataContent. + # etc. + if "uri" in input_item: + parsed_inputs.append( + UriContent(**input_item) if "media_type" in input_item else DataContent(**input_item) + ) + elif "file_id" in input_item: + parsed_inputs.append(HostedFileContent(**input_item)) + elif "vector_store_id" in input_item: + parsed_inputs.append(HostedVectorStoreContent(**input_item)) + elif "data" in input_item: + parsed_inputs.append(DataContent(**input_item)) + else: + raise ValueError(f"Unsupported input type: {input_item}") + elif isinstance(input_item, AIContent): + parsed_inputs.append(input_item) + else: + raise TypeError(f"Unsupported input type: {type(input_item).__name__}. Expected AIContents or dict.") + return parsed_inputs + + @runtime_checkable class AITool(Protocol): """Represents a generic tool that can be specified to an AI service. @@ -37,9 +80,9 @@ class AITool(Protocol): name: str """The name of the tool.""" - description: str | None = None + description: str """A description of the tool, suitable for use in describing the purpose to a model.""" - additional_properties: dict[str, Any] | None = None + additional_properties: dict[str, Any] | None """Additional properties associated with the tool.""" def __str__(self) -> str: @@ -51,48 +94,96 @@ ArgsT = TypeVar("ArgsT", bound=BaseModel) ReturnT = TypeVar("ReturnT") -class AIFunction(AITool, Generic[ArgsT, ReturnT]): - """A AITool that is callable as code.""" +class AIToolBase(AFBaseModel): + """Base class for AI tools, providing common attributes and methods. + + Args: + name: The name of the tool. + description: A description of the tool. + additional_properties: Additional properties associated with the tool. + """ + + name: str = Field(..., kw_only=False) + description: str = "" + additional_properties: dict[str, Any] | None = None + + def __str__(self) -> str: + """Return a string representation of the tool.""" + if self.description: + return f"{self.__class__.__name__}(name={self.name}, description={self.description})" + return f"{self.__class__.__name__}(name={self.name})" + + +class HostedCodeInterpreterTool(AIToolBase): + """Represents a hosted tool that can be specified to an AI service to enable it to execute generated code. + + This tool does not implement code interpretation itself. It serves as a marker to inform a service + that it is allowed to execute generated code if the service is capable of doing so. + """ + + inputs: list[Any] = Field(default_factory=list) def __init__( self, - func: Callable[..., Awaitable[ReturnT] | ReturnT], - name: str, - description: str, - input_model: type[ArgsT], + *, + inputs: "AIContents | dict[str, Any] | str | list[AIContents | dict[str, Any] | str] | None" = None, + description: str | None = None, + additional_properties: dict[str, Any] | None = None, **kwargs: Any, - ): - """Initialize a FunctionTool. + ) -> None: + """Initialize the HostedCodeInterpreterTool. Args: - func: The function to wrap. - name: The name of the tool. + inputs: A list of contents that the tool can accept as input. Defaults to None. + This should mostly be HostedFileContent or HostedVectorStoreContent. + Can also be DataContent, depending on the service used. + When supplying a list, it can contain: + - AIContents instances + - dicts with properties for AIContents (e.g., {"uri": "http://example.com", "media_type": "text/html"}) + - strings (which will be converted to UriContent with media_type "text/plain"). + If None, defaults to an empty list. description: A description of the tool. - input_model: A Pydantic model that defines the input parameters for the function. - **kwargs: Additional properties to set on the tool. - stored in additional_properties. + additional_properties: Additional properties associated with the tool. + **kwargs: Additional keyword arguments to pass to the base class. """ - self.name = name - self.description = description - self.input_model = input_model - self.additional_properties: dict[str, Any] | None = kwargs - self._func = func - self.invocation_duration_histogram = meter.create_histogram( - "agent_framework.function.invocation.duration", + args: dict[str, Any] = { + "name": "code_interpreter", + } + if inputs: + args["inputs"] = _parse_inputs(inputs) + if description is not None: + args["description"] = description + if additional_properties is not None: + args["additional_properties"] = additional_properties + if "name" in kwargs: + raise ValueError("The 'name' argument is reserved for the HostedCodeInterpreterTool and cannot be set.") + super().__init__(**args, **kwargs) + + +class AIFunction(AIToolBase, Generic[ArgsT, ReturnT]): + """A AITool that is callable as code. + + Args: + name: The name of the function. + description: A description of the function. + additional_properties: Additional properties to set on the function. + func: The function to wrap. If None, returns a decorator. + input_model: The Pydantic model that defines the input parameters for the function. + """ + + func: Callable[..., Awaitable[ReturnT] | ReturnT] + input_model: type[ArgsT] + _invocation_duration_histogram: metrics.Histogram = PrivateAttr( + default_factory=lambda: meter.create_histogram( + GenAIAttributes.MEASUREMENT_FUNCTION_INVOCATION_DURATION.value, unit="s", description="Measures the duration of a function's execution", ) - - def parameters(self) -> dict[str, Any]: - """Return the parameter json schemas of the input model.""" - return self.input_model.model_json_schema() + ) def __call__(self, *args: Any, **kwargs: Any) -> ReturnT | Awaitable[ReturnT]: """Call the wrapped function with the provided arguments.""" - return self._func(*args, **kwargs) - - def __str__(self) -> str: - return f"AIFunction(name={self.name}, description={self.description})" + return self.func(*args, **kwargs) async def invoke( self, @@ -136,9 +227,24 @@ class AIFunction(AITool, Generic[ArgsT, ReturnT]): raise finally: duration = perf_counter() - starting_time_stamp - self.invocation_duration_histogram.record(duration, attributes=attributes) + self._invocation_duration_histogram.record(duration, attributes=attributes) logger.info("Function completed. Duration: %fs", duration) + def parameters(self) -> dict[str, Any]: + """Create the json schema of the parameters.""" + return self.input_model.model_json_schema() + + def to_json_schema_spec(self) -> dict[str, Any]: + """Convert a AIFunction to the JSON Schema function specification format.""" + return { + "type": "function", + "function": { + "name": self.name, + "description": self.description, + "parameters": self.parameters(), + }, + } + def _parse_annotation(annotation: Any) -> Any: """Parse a type annotation and return the corresponding type. @@ -207,91 +313,13 @@ def ai_function( raise TypeError(f"Input model for {tool_name} must be a subclass of BaseModel, got {input_model}") return AIFunction[Any, ReturnT]( - func=f, name=tool_name, description=tool_desc, + additional_properties=additional_properties or {}, + func=f, input_model=input_model, - **(additional_properties if additional_properties is not None else {}), ) return wrapper(func) return decorator(func) if func else decorator # type: ignore[reportReturnType, return-value] - - -def _parse_inputs( - inputs: "AIContents | dict[str, Any] | str | list[AIContents | dict[str, Any] | str] | None", -) -> list["AIContents"]: - """Parse the inputs for a tool, ensuring they are of type AIContents.""" - if inputs is None: - return [] - - from ._types import AIContent, DataContent, HostedFileContent, HostedVectorStoreContent, UriContent - - parsed_inputs: list["AIContents"] = [] - if not isinstance(inputs, list): - inputs = [inputs] - for input_item in inputs: - if isinstance(input_item, str): - # If it's a string, we assume it's a URI or similar identifier. - # Convert it to a UriContent or similar type as needed. - parsed_inputs.append(UriContent(uri=input_item, media_type="text/plain")) - elif isinstance(input_item, dict): - # If it's a dict, we assume it contains properties for a specific content type. - # we check if the required keys are present to determine the type. - if "uri" in input_item: - parsed_inputs.append( - UriContent(**input_item) if "media_type" in input_item else DataContent(**input_item) - ) - elif "file_id" in input_item: - parsed_inputs.append(HostedFileContent(**input_item)) - elif "vector_store_id" in input_item: - parsed_inputs.append(HostedVectorStoreContent(**input_item)) - elif "data" in input_item: - parsed_inputs.append(DataContent(**input_item)) - else: - raise ValueError(f"Unsupported input type: {input_item}") - elif isinstance(input_item, AIContent): - parsed_inputs.append(input_item) - else: - raise TypeError(f"Unsupported input type: {type(input_item).__name__}. Expected AIContents or dict.") - return parsed_inputs - - -class HostedCodeInterpreterTool(AITool): - """Represents a hosted tool that can be specified to an AI service to enable it to execute generated code. - - This tool does not implement code interpretation itself. It serves as a marker to inform a service - that it is allowed to execute generated code if the service is capable of doing so. - """ - - def __init__( - self, - name: str = "code_interpreter", - inputs: "AIContents | dict[str, Any] | str | list[AIContents | dict[str, Any] | str] | None" = None, - description: str | None = None, - additional_properties: dict[str, Any] | None = None, - ): - """Initialize a HostedCodeInterpreterTool. - - Args: - name: The name of the tool. Defaults to "code_interpreter". - inputs: A list of contents that the tool can accept as input. Defaults to None. - This should mostly be HostedFileContent or HostedVectorStoreContent. - Can also be DataContent, depending on the service used. - When supplying a list, it can contain: - - AIContents instances - - dicts with properties for AIContents (e.g., {"uri": "http://example.com", "media_type": "text/html"}) - - strings (which will be converted to UriContent with media_type "text/plain"). - If None, defaults to an empty list. - description: A description of the tool. - additional_properties: Additional properties associated with the tool, specific to the service used. - """ - self.name = name - self.inputs = _parse_inputs(inputs) - self.description = description - self.additional_properties = additional_properties - - def __str__(self) -> str: - """Return a string representation of the tool.""" - return f"HostedCodeInterpreterTool(name={self.name})" diff --git a/python/packages/main/agent_framework/_types.py b/python/packages/main/agent_framework/_types.py index f20b17590a..f9bf55790d 100644 --- a/python/packages/main/agent_framework/_types.py +++ b/python/packages/main/agent_framework/_types.py @@ -21,11 +21,9 @@ from pydantic import ( BaseModel, ConfigDict, Field, - PrivateAttr, ValidationError, field_validator, model_serializer, - model_validator, ) from ._pydantic import AFBaseModel @@ -81,7 +79,6 @@ __all__ = [ "AIAnnotations", "AIContent", "AIContents", - "AITool", "AgentRunResponse", "AgentRunResponseUpdate", "AnnotatedRegion", @@ -159,6 +156,10 @@ class UsageDetails(AFBaseModel): **kwargs, ) + def __str__(self) -> str: + """Returns a string representation of the usage details.""" + return self.model_dump_json(indent=4, exclude_none=True) + @property def additional_counts(self) -> dict[str, int]: """Represents well-known additional counts for usage. This is not an exhaustive list. @@ -173,6 +174,14 @@ class UsageDetails(AFBaseModel): """ return self.model_extra or {} + def __setitem__(self, key: str, value: int) -> None: + """Sets an additional count for the usage details.""" + if not isinstance(value, int): + raise ValueError("Additional counts must be integers.") + if self.model_extra is None: + self.model_extra = {} # type: ignore[reportAttributeAccessIssue, misc] + self.model_extra[key] = value + def __add__(self, other: "UsageDetails | None") -> "UsageDetails": """Combines two `UsageDetails` instances.""" if not other: @@ -394,7 +403,7 @@ class AIContent(AFBaseModel): type: Literal["ai"] = "ai" annotations: list[AIAnnotations] | None = None additional_properties: dict[str, Any] | None = None - raw_representation: Any | None = Field(default=None, repr=False) + raw_representation: Any | None = Field(default=None, repr=False, exclude=True) class TextContent(AIContent): @@ -1130,7 +1139,7 @@ class ChatRole(AFBaseModel): TOOL: The role that provides additional information and references in response to tool use requests. """ - value: str + value: str = Field(..., kw_only=False) SYSTEM: ClassVar[Self] # type: ignore[assignment] """The role that instructs or sets the behaviour of the AI system.""" @@ -1211,7 +1220,7 @@ class ChatMessage(AFBaseModel): """The ID of the chat message.""" additional_properties: dict[str, Any] | None = None """Any additional properties associated with the chat message.""" - raw_representation: Any | None = None + raw_representation: Any | None = Field(default=None, exclude=True) """The raw representation of the chat message from an underlying implementation.""" @overload @@ -1760,13 +1769,6 @@ class ChatOptions(AFBaseModel): tools: list[AITool | MutableMapping[str, Any]] | None = None top_p: Annotated[float | None, Field(ge=0.0, le=1.0)] = None user: str | None = None - _ai_tools: list[AITool | MutableMapping[str, Any]] | None = PrivateAttr(default=None) - - @model_validator(mode="after") - def _copy_to_ai_tools(self) -> Self: - if self.tools and not self._ai_tools: - self._ai_tools = self.tools - return self @field_validator("tools", mode="before") @classmethod @@ -1782,10 +1784,7 @@ class ChatOptions(AFBaseModel): | None ), ) -> list[AITool | MutableMapping[str, Any]] | None: - """Parse the tools field. - - All tools are stored in both tools and _ai_tools. - """ + """Parse the tools field.""" if not tools: return None if not isinstance(tools, list): @@ -1849,22 +1848,22 @@ class ChatOptions(AFBaseModel): """ if not isinstance(other, ChatOptions): return self - ai_tools = other._ai_tools - updated_values = other.model_dump(exclude_none=True) - updated_values.pop("tools", []) + other_tools = other.tools + updated_values = other.model_dump(exclude_none=True, exclude={"tools"}) logit_bias = updated_values.pop("logit_bias", {}) metadata = updated_values.pop("metadata", {}) additional_properties = updated_values.pop("additional_properties", {}) combined = self.model_copy(update=updated_values) - if ai_tools: - if not combined._ai_tools: - combined._ai_tools = [] - for tool in ai_tools: - if tool not in combined._ai_tools: - combined._ai_tools.append(tool) combined.logit_bias = {**(combined.logit_bias or {}), **logit_bias} combined.metadata = {**(combined.metadata or {}), **metadata} combined.additional_properties = {**(combined.additional_properties or {}), **additional_properties} + if other_tools: + if not combined.tools: + combined.tools = other_tools + else: + for tool in other_tools: + if tool not in combined.tools: + combined.tools.append(tool) return combined diff --git a/python/packages/main/agent_framework/exceptions.py b/python/packages/main/agent_framework/exceptions.py index fd383b4b48..66a2c14033 100644 --- a/python/packages/main/agent_framework/exceptions.py +++ b/python/packages/main/agent_framework/exceptions.py @@ -1,10 +1,24 @@ # Copyright (c) Microsoft. All rights reserved. +import logging +from typing import Any + +logger = logging.getLogger("agent_framework") + class AgentFrameworkException(Exception): - """Base class for exceptions in the Agent Framework.""" + """Base exceptions for the Agent Framework. - pass + Automatically logs the message as debug. + """ + + def __init__(self, message: str, inner_exception: Exception | None = None, *args: Any, **kwargs: Any): + """Create an AgentFrameworkException. + + This emits a debug log, with the inner_exception if provided. + """ + logger.debug(message, exc_info=inner_exception) + super().__init__(message, *args, **kwargs) # type: ignore class AgentException(AgentFrameworkException): @@ -68,3 +82,15 @@ class ServiceInvalidResponseError(ServiceResponseException): """An error occurred while validating the response from the service.""" pass + + +class ToolException(AgentFrameworkException): + """An error occurred while executing a tool.""" + + pass + + +class ToolExecutionException(ToolException): + """An error occurred while executing a tool.""" + + pass diff --git a/python/packages/main/agent_framework/openai/_assistants_client.py b/python/packages/main/agent_framework/openai/_assistants_client.py index 16314da5c5..c332e4b97d 100644 --- a/python/packages/main/agent_framework/openai/_assistants_client.py +++ b/python/packages/main/agent_framework/openai/_assistants_client.py @@ -3,7 +3,7 @@ import json import sys from collections.abc import AsyncIterable, Mapping, MutableMapping, MutableSequence -from typing import Any, ClassVar +from typing import Any from openai import AsyncOpenAI from openai.types.beta.threads import ( @@ -20,7 +20,7 @@ from openai.types.beta.threads.run_submit_tool_outputs_params import ToolOutput from openai.types.beta.threads.runs import RunStep from pydantic import Field, PrivateAttr, SecretStr, ValidationError -from .._clients import ChatClientBase, ai_function_to_json_schema_spec, use_tool_calling +from .._clients import ChatClientBase, use_tool_calling from .._tools import AIFunction, HostedCodeInterpreterTool from .._types import ( AIContents, @@ -54,7 +54,6 @@ __all__ = ["OpenAIAssistantsClient"] class OpenAIAssistantsClient(OpenAIConfigBase, ChatClientBase): """OpenAI Assistants client.""" - MODEL_PROVIDER_NAME: ClassVar[str] = "openai" # type: ignore[reportIncompatibleVariableOverride, misc] assistant_id: str | None = Field(default=None) assistant_name: str | None = Field(default=None) thread_id: str | None = Field(default=None) @@ -362,7 +361,7 @@ class OpenAIAssistantsClient(OpenAIConfigBase, ChatClientBase): if chat_options.tool_choice != "none" and chat_options.tools is not None: for tool in chat_options.tools: if isinstance(tool, AIFunction): - tool_definitions.append(ai_function_to_json_schema_spec(tool)) # type: ignore[reportUnknownArgumentType] + tool_definitions.append(tool.to_json_schema_spec()) # type: ignore[reportUnknownArgumentType] elif isinstance(tool, HostedCodeInterpreterTool): tool_definitions.append({"type": "code_interpreter"}) elif isinstance(tool, MutableMapping): @@ -467,3 +466,14 @@ class OpenAIAssistantsClient(OpenAIConfigBase, ChatClientBase): tool_outputs.append(ToolOutput(tool_call_id=call_id, output=str(function_result_content.result))) return run_id, tool_outputs + + def _update_agent_name(self, agent_name: str | None) -> None: + """Update the agent name in the chat client. + + Args: + agent_name: The new name for the agent. + """ + # This is a no-op in the base class, but can be overridden by subclasses + # to update the agent name in the client. + if agent_name and not self.assistant_name: + self.assistant_name = agent_name diff --git a/python/packages/main/agent_framework/openai/_chat_client.py b/python/packages/main/agent_framework/openai/_chat_client.py index c73b067ea8..ab01e336e4 100644 --- a/python/packages/main/agent_framework/openai/_chat_client.py +++ b/python/packages/main/agent_framework/openai/_chat_client.py @@ -1,20 +1,24 @@ # Copyright (c) Microsoft. All rights reserved. import json -from collections.abc import AsyncIterable, Mapping, MutableSequence, Sequence +from collections.abc import AsyncIterable, Mapping, MutableMapping, MutableSequence, Sequence from datetime import datetime from itertools import chain -from typing import Any, ClassVar, cast +from typing import Any, TypeVar -from openai import AsyncOpenAI, AsyncStream +from openai import AsyncOpenAI, BadRequestError +from openai.lib._parsing._completions import type_to_response_format_param from openai.types import CompletionUsage from openai.types.chat.chat_completion import ChatCompletion, Choice -from openai.types.chat.chat_completion_chunk import ChatCompletionChunk, ChoiceDeltaToolCall +from openai.types.chat.chat_completion_chunk import ChatCompletionChunk from openai.types.chat.chat_completion_chunk import Choice as ChunkChoice -from openai.types.chat.chat_completion_message_tool_call import ChatCompletionMessageToolCall -from pydantic import SecretStr, ValidationError +from openai.types.chat.chat_completion_message_custom_tool_call import ChatCompletionMessageCustomToolCall +from pydantic import BaseModel, SecretStr, ValidationError + +from agent_framework import AIFunction, AITool, UsageContent from .._clients import ChatClientBase, use_tool_calling +from .._logging import get_logger from .._types import ( AIContents, ChatFinishReason, @@ -28,12 +32,19 @@ from .._types import ( TextContent, UsageDetails, ) -from ..exceptions import ServiceInitializationError, ServiceInvalidResponseError +from ..exceptions import ( + ServiceInitializationError, + ServiceInvalidRequestError, + ServiceResponseException, +) from ..telemetry import use_telemetry -from ._shared import OpenAIConfigBase, OpenAIHandler, OpenAIModelTypes, OpenAISettings +from ._exceptions import OpenAIContentFilterException +from ._shared import OpenAIConfigBase, OpenAIHandler, OpenAISettings, prepare_function_call_results __all__ = ["OpenAIChatClient"] +logger = get_logger("agent_framework.openai") + # region Base Client @use_telemetry @@ -41,8 +52,6 @@ __all__ = ["OpenAIChatClient"] class OpenAIChatClientBase(OpenAIHandler, ChatClientBase): """OpenAI Chat completion class.""" - MODEL_PROVIDER_NAME: ClassVar[str] = "openai" # type: ignore[reportIncompatibleVariableOverride, misc] - async def _inner_get_response( self, *, @@ -50,16 +59,24 @@ class OpenAIChatClientBase(OpenAIHandler, ChatClientBase): chat_options: ChatOptions, **kwargs: Any, ) -> ChatResponse: - chat_options.additional_properties["stream"] = False - if not chat_options.ai_model_id: - chat_options.ai_model_id = self.ai_model_id - - response = await self._send_request(chat_options, messages=self._prepare_chat_history_for_request(messages)) - assert isinstance(response, ChatCompletion) # nosec # noqa: S101 - response_metadata = self._get_metadata_from_chat_response(response) - return next( - self._create_chat_message_content(response, choice, response_metadata) for choice in response.choices - ) + options_dict = self._prepare_options(messages, chat_options) + try: + return self._create_chat_response(await self.client.chat.completions.create(stream=False, **options_dict)) + except BadRequestError as ex: + if ex.code == "content_filter": + raise OpenAIContentFilterException( + f"{type(self)} service encountered a content error", + ex, + ) from ex + raise ServiceResponseException( + f"{type(self)} service failed to complete the prompt", + ex, + ) from ex + except Exception as ex: + raise ServiceResponseException( + f"{type(self)} service failed to complete the prompt", + ex, + ) from ex async def _inner_get_streaming_response( self, @@ -68,91 +85,138 @@ class OpenAIChatClientBase(OpenAIHandler, ChatClientBase): chat_options: ChatOptions, **kwargs: Any, ) -> AsyncIterable[ChatResponseUpdate]: - chat_options.additional_properties["stream"] = True - chat_options.additional_properties["stream_options"] = {"include_usage": True} - chat_options.ai_model_id = chat_options.ai_model_id or self.ai_model_id - - response = await self._send_request(chat_options, messages=self._prepare_chat_history_for_request(messages)) - if not isinstance(response, AsyncStream): - raise ServiceInvalidResponseError("Expected an AsyncStream[ChatCompletionChunk] response.") - async for chunk in response: - assert isinstance(chunk, ChatCompletionChunk) # nosec # noqa: S101 - if len(chunk.choices) == 0 and chunk.usage is None: - continue - - assert isinstance(chunk, ChatCompletionChunk) # nosec # noqa: S101 - chunk_metadata = self._get_metadata_from_streaming_chat_response(chunk) - if chunk.usage is not None: - # Usage is contained in the last chunk where the choices are empty - # We are duplicating the usage metadata to all the choices in the response - yield ChatResponseUpdate( - role=ChatRole.ASSISTANT, - contents=[], - ai_model_id=chat_options.ai_model_id, - additional_properties=chunk_metadata, - ) - - else: - yield next( - self._create_streaming_chat_message_content(chunk, choice, chunk_metadata) - for choice in chunk.choices - ) + options_dict = self._prepare_options(messages, chat_options) + options_dict["stream_options"] = {"include_usage": True} + try: + async for chunk in await self.client.chat.completions.create(stream=True, **options_dict): + if len(chunk.choices) == 0 and chunk.usage is None: + continue + yield self._create_chat_response_update(chunk) + except BadRequestError as ex: + if ex.code == "content_filter": + raise OpenAIContentFilterException( + f"{type(self)} service encountered a content error", + ex, + ) from ex + raise ServiceResponseException( + f"{type(self)} service failed to complete the prompt", + ex, + ) from ex + except Exception as ex: + raise ServiceResponseException( + f"{type(self)} service failed to complete the prompt", + ex, + ) from ex # region content creation - def _create_chat_message_content( - self, response: ChatCompletion, choice: Choice, response_metadata: dict[str, Any] - ) -> "ChatResponse": - """Create a chat message content object from a choice.""" - metadata = self._get_metadata_from_chat_choice(choice) - metadata.update(response_metadata) - items: MutableSequence[ChatMessage] = [] - if parsed_tool_calls := [tool for tool in self._get_tool_calls_from_chat_choice(choice)]: - items.append(ChatMessage(role="assistant", contents=parsed_tool_calls)) - if choice.message.content: - items.append(ChatMessage(role="assistant", text=choice.message.content)) - elif hasattr(choice.message, "refusal") and choice.message.refusal: - items.append(ChatMessage(role="assistant", text=choice.message.refusal)) + def _chat_to_tool_spec(self, tools: list[AITool | MutableMapping[str, Any]]) -> list[dict[str, Any]]: + chat_tools: list[dict[str, Any]] = [] + for tool in tools: + if isinstance(tool, AITool): + match tool: + case AIFunction(): + chat_tools.append(tool.to_json_schema_spec()) + case _: + logger.debug("Unsupported tool passed (type: %s), ignoring", type(tool)) + else: + chat_tools.append(tool if isinstance(tool, dict) else dict(tool)) + return chat_tools + def _prepare_options(self, messages: MutableSequence[ChatMessage], chat_options: ChatOptions) -> dict[str, Any]: + options_dict = chat_options.to_provider_settings() + if messages and "messages" not in options_dict: + options_dict["messages"] = self._prepare_chat_history_for_request(messages) + if "messages" not in options_dict: + raise ServiceInvalidRequestError("Messages are required for chat completions") + if chat_options.tools is None: + options_dict.pop("parallel_tool_calls", None) + else: + options_dict["tools"] = self._chat_to_tool_spec(chat_options.tools) + if "model" not in options_dict: + options_dict["model"] = self.ai_model_id + if ( + chat_options.response_format + and isinstance(chat_options.response_format, type) + and issubclass(chat_options.response_format, BaseModel) + ): + options_dict["response_format"] = type_to_response_format_param(chat_options.response_format) + return options_dict + + def _create_chat_response(self, response: ChatCompletion) -> "ChatResponse": + """Create a chat message content object from a choice.""" + response_metadata = self._get_metadata_from_chat_response(response) + messages: list[ChatMessage] = [] + finish_reason: ChatFinishReason | None = None + for choice in response.choices: + response_metadata.update(self._get_metadata_from_chat_choice(choice)) + if choice.finish_reason: + finish_reason = ChatFinishReason(value=choice.finish_reason) + contents: list[AIContents] = [] + if parsed_tool_calls := [tool for tool in self._get_tool_calls_from_chat_choice(choice)]: + contents.extend(parsed_tool_calls) + if text_content := self._parse_text_from_choice(choice): + contents.append(text_content) + messages.append(ChatMessage(role="assistant", contents=contents)) return ChatResponse( response_id=response.id, created_at=datetime.fromtimestamp(response.created).strftime("%Y-%m-%dT%H:%M:%S.%fZ"), usage_details=self._usage_details_from_openai(response.usage) if response.usage else None, - messages=items, - model_id=self.ai_model_id, - additional_properties=metadata, - finish_reason=(ChatFinishReason(value=choice.finish_reason) if choice.finish_reason else None), + messages=messages, + model_id=response.model, + additional_properties=response_metadata, + finish_reason=finish_reason, ) - def _create_streaming_chat_message_content( + def _create_chat_response_update( self, chunk: ChatCompletionChunk, - choice: ChunkChoice, - chunk_metadata: dict[str, Any], ) -> ChatResponseUpdate: """Create a streaming chat message content object from a choice.""" - metadata = self._get_metadata_from_chat_choice(choice) - metadata.update(chunk_metadata) + chunk_metadata = self._get_metadata_from_streaming_chat_response(chunk) + if chunk.usage: + return ChatResponseUpdate( + role=ChatRole.ASSISTANT, + contents=[UsageContent(details=self._usage_details_from_openai(chunk.usage), raw_representation=chunk)], + ai_model_id=chunk.model, + additional_properties=chunk_metadata, + ) + contents: list[AIContents] = [] + finish_reason: ChatFinishReason | None = None + for choice in chunk.choices: + chunk_metadata.update(self._get_metadata_from_chat_choice(choice)) + contents.extend(self._get_tool_calls_from_chat_choice(choice)) + if choice.finish_reason: + finish_reason = ChatFinishReason(value=choice.finish_reason) - items: list[Any] = self._get_tool_calls_from_chat_choice(choice) - if choice.delta and choice.delta.content is not None: - items.append(TextContent(text=choice.delta.content)) + if text_content := self._parse_text_from_choice(choice): + contents.append(text_content) return ChatResponseUpdate( created_at=datetime.fromtimestamp(chunk.created).strftime("%Y-%m-%dT%H:%M:%S.%fZ"), - contents=items, + contents=contents, role=ChatRole.ASSISTANT, - ai_model_id=self.ai_model_id, - additional_properties=metadata, - finish_reason=(ChatFinishReason(value=choice.finish_reason) if choice.finish_reason else None), + ai_model_id=chunk.model, + additional_properties=chunk_metadata, + finish_reason=finish_reason, + raw_representation=chunk, ) - def _usage_details_from_openai(self, usage: CompletionUsage) -> UsageDetails | None: + def _usage_details_from_openai(self, usage: CompletionUsage) -> UsageDetails: return UsageDetails( prompt_tokens=usage.prompt_tokens, completion_tokens=usage.completion_tokens, total_tokens=usage.total_tokens, ) + def _parse_text_from_choice(self, choice: Choice | ChunkChoice) -> TextContent | None: + """Parse the choice into a TextContent object.""" + message = choice.message if isinstance(choice, Choice) else choice.delta + if message.content: + return TextContent(text=message.content, raw_representation=choice) + if hasattr(message, "refusal") and message.refusal: + return TextContent(text=message.refusal, raw_representation=choice) + return None + def _get_metadata_from_chat_response(self, response: ChatCompletion) -> dict[str, Any]: """Get metadata from a chat response.""" return { @@ -175,13 +239,15 @@ class OpenAIChatClientBase(OpenAIHandler, ChatClientBase): """Get tool calls from a chat choice.""" resp: list[AIContents] = [] content = choice.message if isinstance(choice, Choice) else choice.delta - if content and (tool_calls := getattr(content, "tool_calls", None)) is not None: - for tool in cast(list[ChatCompletionMessageToolCall] | list[ChoiceDeltaToolCall], tool_calls): - if tool.function: # type: ignore[reportAttributeAccessIssue, union-attr] + if content and content.tool_calls: + for tool in content.tool_calls: + if not isinstance(tool, ChatCompletionMessageCustomToolCall) and tool.function: + # ignoring tool.custom fcc = FunctionCallContent( call_id=tool.id if tool.id else "", - name=tool.function.name if tool.function.name else "", # type: ignore - arguments=tool.function.arguments if tool.function.arguments else "", # type: ignore + name=tool.function.name if tool.function.name else "", + arguments=tool.function.arguments if tool.function.arguments else "", + raw_representation=tool.function, ) resp.append(fcc) @@ -236,7 +302,8 @@ class OpenAIChatClientBase(OpenAIHandler, ChatClientBase): args["tool_calls"] = [self._openai_content_parser(content)] # type: ignore case FunctionResultContent(): args["tool_call_id"] = content.call_id - args["content"] = content.result + if content.result: + args["content"] = prepare_function_call_results(content.result) case _: if "content" not in args: args["content"] = [] @@ -275,6 +342,8 @@ class OpenAIChatClientBase(OpenAIHandler, ChatClientBase): # region Public client +TOpenAIChatClient = TypeVar("TOpenAIChatClient", bound="OpenAIChatClient") + class OpenAIChatClient(OpenAIConfigBase, OpenAIChatClientBase): """OpenAI Chat completion class.""" @@ -328,23 +397,19 @@ class OpenAIChatClient(OpenAIConfigBase, OpenAIChatClientBase): ai_model_id=openai_settings.chat_model_id, api_key=openai_settings.api_key.get_secret_value() if openai_settings.api_key else None, org_id=openai_settings.org_id, - ai_model_type=OpenAIModelTypes.CHAT, default_headers=default_headers, client=async_client, instruction_role=instruction_role, ) @classmethod - def from_dict(cls, settings: dict[str, Any]) -> "OpenAIChatClient": - """Initialize an Open AI service from a dictionary of settings. + def from_dict(cls: type[TOpenAIChatClient], settings: dict[str, Any]) -> TOpenAIChatClient: + """Initialize an Open AI Chat Client from a dictionary of settings. Args: settings: A dictionary of settings for the service. """ - return OpenAIChatClient( - ai_model_id=settings["ai_model_id"], - default_headers=settings.get("default_headers"), - ) + return cls(**settings) # endregion diff --git a/python/packages/main/agent_framework/openai/_responses_client.py b/python/packages/main/agent_framework/openai/_responses_client.py index a387a1281c..6e3448f2e5 100644 --- a/python/packages/main/agent_framework/openai/_responses_client.py +++ b/python/packages/main/agent_framework/openai/_responses_client.py @@ -4,70 +4,87 @@ import sys from collections.abc import AsyncIterable, Callable, Mapping, MutableMapping, MutableSequence, Sequence from datetime import datetime from itertools import chain -from typing import Any, ClassVar, Literal +from typing import TYPE_CHECKING, Any, Literal, TypeVar -from openai import AsyncOpenAI, AsyncStream +from openai import AsyncOpenAI, BadRequestError +from openai.types.responses.function_tool_param import FunctionToolParam +from openai.types.responses.parsed_response import ( + ParsedResponse, +) from openai.types.responses.response import Response as OpenAIResponse -from openai.types.responses.response_code_interpreter_tool_call import ResponseCodeInterpreterToolCall from openai.types.responses.response_completed_event import ResponseCompletedEvent from openai.types.responses.response_content_part_added_event import ResponseContentPartAddedEvent -from openai.types.responses.response_function_tool_call import ResponseFunctionToolCall -from openai.types.responses.response_includable import ResponseIncludable -from openai.types.responses.response_output_item import ResponseOutputItem -from openai.types.responses.response_output_message import ResponseOutputMessage +from openai.types.responses.response_function_call_arguments_delta_event import ResponseFunctionCallArgumentsDeltaEvent +from openai.types.responses.response_output_item_added_event import ResponseOutputItemAddedEvent from openai.types.responses.response_output_refusal import ResponseOutputRefusal from openai.types.responses.response_output_text import ResponseOutputText from openai.types.responses.response_stream_event import ResponseStreamEvent as OpenAIResponseStreamEvent from openai.types.responses.response_text_delta_event import ResponseTextDeltaEvent from openai.types.responses.response_usage import ResponseUsage +from openai.types.responses.tool_param import ( + CodeInterpreter, + CodeInterpreterContainerCodeInterpreterToolAuto, + ToolParam, +) from pydantic import BaseModel, SecretStr, ValidationError +from agent_framework import DataContent, TextReasoningContent, UriContent, UsageContent + from .._clients import ChatClientBase, use_tool_calling -from .._tools import HostedCodeInterpreterTool +from .._logging import get_logger +from .._tools import AIFunction, AITool, HostedCodeInterpreterTool from .._types import ( AIContents, - AITool, ChatMessage, ChatOptions, ChatResponse, ChatResponseUpdate, ChatRole, - ChatToolMode, + CitationAnnotation, FunctionCallContent, FunctionResultContent, + HostedFileContent, + StructuredResponse, TextContent, + TextSpanRegion, UsageDetails, ) -from ..exceptions import ServiceInitializationError, ServiceInvalidResponseError +from ..exceptions import ( + ServiceInitializationError, + ServiceInvalidRequestError, + ServiceResponseException, +) from ..telemetry import use_telemetry -from ._shared import OpenAIConfigBase, OpenAIHandler, OpenAIModelTypes, OpenAISettings +from ._exceptions import OpenAIContentFilterException +from ._shared import OpenAIConfigBase, OpenAIHandler, OpenAISettings, prepare_function_call_results if sys.version_info >= (3, 12): from typing import override # type: ignore # pragma: no cover else: from typing_extensions import override # type: ignore[import] # pragma: no cover +if TYPE_CHECKING: + from openai.types.responses.response_includable import ResponseIncludable + + from .._types import ChatToolMode + + +logger = get_logger("agent_framework.openai") + __all__ = ["OpenAIResponsesClient"] # region ResponsesClient class OpenAIResponsesClientBase(OpenAIHandler, ChatClientBase): - def _filter_options(self, **kwargs: Any) -> dict[str, Any]: - """Filter options for the responses call.""" - # The responses call does not support all the options that the chat completion call does. - # We filter out the unsupported options. - return {key: value for key, value in kwargs.items() if value is not None} + """Base class for all OpenAI Responses based API's.""" - # The responses create call takes very different parameters than the chat completion call, - # so we override the get_response method to handle the specific parameters for responses. @override async def get_response( self, messages: str | ChatMessage | list[str] | list[ChatMessage], *, - # TODO(peterychang): enable this option. background: bool | None = None, - include: list[ResponseIncludable] | None = None, + include: list["ResponseIncludable"] | None = None, instruction: str | None = None, max_tokens: int | None = None, parallel_tool_calls: bool | None = None, @@ -79,7 +96,7 @@ class OpenAIResponsesClientBase(OpenAIHandler, ChatClientBase): seed: int | None = None, store: bool | None = None, temperature: float | None = None, - tool_choice: ChatToolMode | Literal["auto", "required", "none"] | dict[str, Any] | None = "auto", + tool_choice: "ChatToolMode" | Literal["auto", "required", "none"] | dict[str, Any] | None = "auto", tools: AITool | list[AITool] | Callable[..., Any] @@ -123,31 +140,27 @@ class OpenAIResponsesClientBase(OpenAIHandler, ChatClientBase): Returns: A chat response from the model. """ - filtered_options = self._filter_options( - background=False, + return await super().get_response( + messages=messages, include=include, instruction=instruction, + max_tokens=max_tokens, parallel_tool_calls=parallel_tool_calls, + model=model, previous_response_id=previous_response_id, reasoning=reasoning, service_tier=service_tier, - truncation=truncation, - timeout=timeout, - ) - filtered_options.update(additional_properties or {}) - return await super().get_response( - messages=messages, - model=model, - max_tokens=max_tokens, response_format=response_format, seed=seed, store=store, temperature=temperature, - top_p=top_p, tool_choice=tool_choice, tools=tools, + top_p=top_p, user=user, - additional_properties=filtered_options, + truncation=truncation, + timeout=timeout, + additional_properties=additional_properties, **kwargs, ) @@ -157,7 +170,7 @@ class OpenAIResponsesClientBase(OpenAIHandler, ChatClientBase): messages: str | ChatMessage | list[str] | list[ChatMessage], *, # TODO(peterychang): enable this option. background: bool | None = None, - include: list[ResponseIncludable] | None = None, + include: list["ResponseIncludable"] | None = None, instruction: str | None = None, max_tokens: int | None = None, parallel_tool_calls: bool | None = None, @@ -169,7 +182,7 @@ class OpenAIResponsesClientBase(OpenAIHandler, ChatClientBase): seed: int | None = None, store: bool | None = None, temperature: float | None = None, - tool_choice: ChatToolMode | Literal["auto", "required", "none"] | dict[str, Any] | None = "auto", + tool_choice: "ChatToolMode" | Literal["auto", "required", "none"] | dict[str, Any] | None = "auto", tools: AITool | list[AITool] | Callable[..., Any] @@ -213,55 +226,32 @@ class OpenAIResponsesClientBase(OpenAIHandler, ChatClientBase): Returns: A stream representing the response(s) from the LLM. """ - filtered_options = self._filter_options( - background=False, + async for update in super().get_streaming_response( + messages=messages, include=include, instruction=instruction, + max_tokens=max_tokens, parallel_tool_calls=parallel_tool_calls, + model=model, previous_response_id=previous_response_id, reasoning=reasoning, service_tier=service_tier, - truncation=truncation, - timeout=timeout, - ) - filtered_options.update(additional_properties or {}) - async for update in super().get_streaming_response( - messages=messages, - model=model, - max_tokens=max_tokens, response_format=response_format, seed=seed, store=store, temperature=temperature, - top_p=top_p, tool_choice=tool_choice, tools=tools, + top_p=top_p, user=user, - additional_properties=filtered_options, + truncation=truncation, + timeout=timeout, + additional_properties=additional_properties, **kwargs, ): yield update - def _chat_to_response_tool_spec(self, tools: list[AITool | MutableMapping[str, Any]]) -> list[dict[str, Any]]: - response_tools: list[dict[str, Any]] = [] - for tool in tools: - if isinstance(tool, AITool): - # TODO(peterychang): Support AITools - if isinstance(tool, HostedCodeInterpreterTool): - response_tools.append({"type": "code_interpreter", "container": {"type": "auto"}}) - continue - if "function" not in tool: - response_tools.append(tool if isinstance(tool, dict) else dict(tool)) - parameters = {"additionalProperties": False} - parameters.update(tool.get("function", {}).get("parameters", {})) - response_tools.append({ - "type": "function", - "name": tool.get("function", {}).get("name", ""), - "strict": True, - "description": tool.get("function", {}).get("description", None), - "parameters": parameters, - }) - return response_tools + # region Inner Methods async def _inner_get_response( self, @@ -270,16 +260,39 @@ class OpenAIResponsesClientBase(OpenAIHandler, ChatClientBase): chat_options: ChatOptions, **kwargs: Any, ) -> ChatResponse: - chat_options.additional_properties["stream"] = False - if not chat_options.ai_model_id: - chat_options.ai_model_id = self.ai_model_id - if chat_options.tools: - chat_options.additional_properties.update({ - "response_tools": self._chat_to_response_tool_spec(chat_options.tools) - }) - response = await self._send_request(chat_options, messages=self._prepare_chat_history_for_request(messages)) - assert isinstance(response, OpenAIResponse) # nosec # noqa: S101 - return next(self._create_response_content(response, item, store=chat_options.store) for item in response.output) + options_dict = self._prepare_options(messages, chat_options) + try: + if not chat_options.response_format: + response = await self.client.responses.create( + stream=False, + **options_dict, + ) + chat_options.conversation_id = response.id if chat_options.store is True else None + return self._create_response_content(response, chat_options=chat_options) + # create call does not support response_format, so we need to handle it via parse call + resp_format = chat_options.response_format + parsed_response: ParsedResponse[BaseModel] = await self.client.responses.parse( + text_format=resp_format, + stream=False, + **options_dict, + ) + chat_options.conversation_id = parsed_response.id if chat_options.store is True else None + return self._create_response_content(parsed_response, chat_options=chat_options) + except BadRequestError as ex: + if ex.code == "content_filter": + raise OpenAIContentFilterException( + f"{type(self)} service encountered a content error", + inner_exception=ex, + ) from ex + raise ServiceResponseException( + f"{type(self)} service failed to complete the prompt", + inner_exception=ex, + ) from ex + except Exception as ex: + raise ServiceResponseException( + f"{type(self)} service failed to complete the prompt, with exception: {ex}", + inner_exception=ex, + ) from ex async def _inner_get_streaming_response( self, @@ -288,169 +301,113 @@ class OpenAIResponsesClientBase(OpenAIHandler, ChatClientBase): chat_options: ChatOptions, **kwargs: Any, ) -> AsyncIterable[ChatResponseUpdate]: - chat_options.additional_properties["stream"] = True - chat_options.ai_model_id = chat_options.ai_model_id or self.ai_model_id + options_dict = self._prepare_options(messages, chat_options) + function_call_ids: dict[int, tuple[str, str]] = {} # output_index: (call_id, name) + try: + if not chat_options.response_format: + response = await self.client.responses.create( + stream=True, + **options_dict, + ) + async for chunk in response: + update = self._create_streaming_response_content( + chunk, chat_options=chat_options, function_call_ids=function_call_ids + ) + yield update + return + # create call does not support response_format, so we need to handle it via stream call + async with self.client.responses.stream( + text_format=chat_options.response_format, + **options_dict, + ) as response: + async for chunk in response: + update = self._create_streaming_response_content( + chunk, chat_options=chat_options, function_call_ids=function_call_ids + ) + yield update + except BadRequestError as ex: + if ex.code == "content_filter": + raise OpenAIContentFilterException( + f"{type(self)} service encountered a content error", + inner_exception=ex, + ) from ex + raise ServiceResponseException( + f"{type(self)} service failed to complete the prompt", + inner_exception=ex, + ) from ex + except Exception as ex: + raise ServiceResponseException( + f"{type(self)} service failed to complete the prompt", + inner_exception=ex, + ) from ex - if chat_options.tools: - chat_options.additional_properties.update({ - "response_tools": self._chat_to_response_tool_spec(chat_options.tools) - }) - response = await self._send_request(chat_options, messages=self._prepare_chat_history_for_request(messages)) - if not isinstance(response, AsyncStream): - raise ServiceInvalidResponseError("Expected an AsyncStream[ResponseStreamEvent] response.") - async for chunk in response: - update = self._create_streaming_response_content(chunk, store=chat_options.store) # type: ignore - if not update: - continue - yield update + # region Prep methods - def _create_response_content( - self, response: OpenAIResponse, item: ResponseOutputItem, store: bool | None - ) -> "ChatResponse": - """Create a chat message content object from a choice.""" - items: MutableSequence[ChatMessage] = [] - metadata: dict[str, Any] = response.metadata or {} - # TODO(peterychang): Add support for other content types - if parsed_tool_calls := [tool for tool in self._get_tool_calls_from_response(response)]: - items.append(ChatMessage(role="assistant", contents=parsed_tool_calls)) - if isinstance(item, ResponseOutputMessage): - for content in item.content: - # TODO(peterychang): Add annotations when available - if isinstance(content, ResponseOutputText): - items.append(ChatMessage(role=item.role, text=content.text)) - metadata.update(self._get_metadata_from_response(content)) - elif isinstance(content, ResponseOutputRefusal): - items.append(ChatMessage(role=item.role, text=content.refusal)) - if isinstance(item, ResponseCodeInterpreterToolCall): - items.append(ChatMessage(role=ChatRole.ASSISTANT, text=response.output_text)) - return ChatResponse( - response_id=response.id, - conversation_id=response.id if store is True else None, - created_at=datetime.fromtimestamp(response.created_at).strftime("%Y-%m-%dT%H:%M:%S.%fZ"), - usage_details=self._usage_details_from_openai(response.usage) if response.usage else None, - messages=items, - model_id=self.ai_model_id, - additional_properties=metadata, - raw_representation=response, - ) + def _chat_to_response_tool_spec( + self, tools: list[AITool | MutableMapping[str, Any]] + ) -> list[ToolParam | dict[str, Any]]: + response_tools: list[ToolParam | dict[str, Any]] = [] + for tool in tools: + if isinstance(tool, AITool): + match tool: + case HostedCodeInterpreterTool(): + tool_args: dict[str, Any] = {"type": "auto"} + if tool.inputs: + tool_args["file_ids"] = [] + for tool_input in tool.inputs: + if isinstance(tool_input, HostedFileContent): + tool_args["file_ids"].append(tool_input.file_id) + if not tool_args["file_ids"]: + tool_args.pop("file_ids") + response_tools.append( + CodeInterpreter( + type="code_interpreter", + container=CodeInterpreterContainerCodeInterpreterToolAuto(**tool_args), # type: ignore[typeddict-item] + ) + ) + case AIFunction(): + params = tool.parameters() + params["additionalProperties"] = False + response_tools.append( + FunctionToolParam( + name=tool.name, + parameters=params, + strict=False, + type="function", + description=tool.description, + ) + ) + case _: + logger.debug("Unsupported tool passed (type: %s)", type(tool)) + else: + response_tools.append(tool if isinstance(tool, dict) else dict(tool)) + return response_tools - def _create_streaming_response_content( - self, event: OpenAIResponseStreamEvent, store: bool | None - ) -> ChatResponseUpdate | None: - """Create a streaming chat message content object from a choice.""" - metadata: dict[str, Any] = {} - items: list[AIContents] = [] - conversation_id: str | None = None - # TODO(peterychang): Add support for other content types - if isinstance(event, ResponseContentPartAddedEvent): - if isinstance(event.part, ResponseOutputText): - items.append(TextContent(text=event.part.text)) - metadata.update(self._get_metadata_from_response(event.part)) - elif isinstance(event.part, ResponseOutputRefusal): - items.append(TextContent(text=event.part.refusal)) - elif isinstance(event, ResponseTextDeltaEvent): - items.append(TextContent(text=event.delta)) - metadata.update(self._get_metadata_from_response(event)) - elif isinstance(event, ResponseCompletedEvent): - conversation_id = event.response.id if store is True else None - # Tool calls are available in the completed event - if parsed_tool_calls := [tool for tool in self._get_tool_calls_from_response(event.response)]: - items.extend(parsed_tool_calls) + def _prepare_options(self, messages: MutableSequence[ChatMessage], chat_options: ChatOptions) -> dict[str, Any]: + """Take ChatOptions and create the specific options for Responses.""" + options_dict = chat_options.to_provider_settings(exclude={"response_format"}) + # messages + request_input = self._prepare_chat_messages_for_request(messages) + if not request_input: + raise ServiceInvalidRequestError("Messages are required for chat completions") + options_dict["input"] = request_input + # tools + if chat_options.tools is None: + options_dict.pop("parallel_tool_calls", None) else: - return None - return ChatResponseUpdate( - contents=items, - conversation_id=conversation_id, - role=ChatRole.ASSISTANT, - ai_model_id=self.ai_model_id, - additional_properties=metadata, - raw_representation=event, - ) + options_dict["tools"] = self._chat_to_response_tool_spec(chat_options.tools) + # other settings + if "store" not in options_dict: + options_dict["store"] = False + if "conversation_id" in options_dict: + options_dict["previous_response_id"] = options_dict["conversation_id"] + options_dict.pop("conversation_id") + if "model" not in options_dict: + options_dict["model"] = self.ai_model_id + return options_dict - def _get_tool_calls_from_response(self, response: OpenAIResponse) -> list[AIContents]: - resp: list[AIContents] = [] - # TODO(peterychang): Support the other tool calls - for item in (i for i in response.output if isinstance(i, ResponseFunctionToolCall)): - fcc = FunctionCallContent( - call_id=item.id if item.id else "", - name=item.name, - arguments=item.arguments, - additional_properties={"call_id": item.call_id}, - ) - resp.append(fcc) - - return resp - - def _usage_details_from_openai(self, usage: ResponseUsage) -> UsageDetails | None: - return UsageDetails( - prompt_tokens=usage.input_tokens, - completion_tokens=usage.output_tokens, - total_tokens=usage.total_tokens, - ) - - def _openai_chat_message_parser( - self, - message: ChatMessage, - tool_id_to_call_id: dict[str, str], - ) -> list[dict[str, Any]]: - """Parse a chat message into the openai format.""" - all_messages: list[dict[str, Any]] = [] - args: dict[str, Any] = { - "role": message.role.value if isinstance(message.role, ChatRole) else message.role, - } - if message.additional_properties: - args["metadata"] = message.additional_properties - for content in message.contents: - match content: - case FunctionResultContent(): - new_args: dict[str, Any] = {} - new_args.update(self._openai_content_parser(message.role, content, tool_id_to_call_id)) - all_messages.append(new_args) - case FunctionCallContent(): - function_call = self._openai_content_parser(message.role, content, tool_id_to_call_id) - all_messages.append(function_call) # type: ignore - case _: - if "content" not in args: - args["content"] = [] - args["content"].append(self._openai_content_parser(message.role, content, tool_id_to_call_id)) # type: ignore - if "content" in args or "tool_calls" in args: - all_messages.append(args) - return all_messages - - def _openai_content_parser( - self, - role: ChatRole, - content: AIContents, - tool_id_to_call_id: dict[str, str], - ) -> dict[str, Any]: - """Parse contents into the openai format.""" - match content: - case FunctionCallContent(): - return { - "id": content.call_id, - "call_id": tool_id_to_call_id[content.call_id], - "type": "function_call", - "name": content.name, - "arguments": content.arguments, - } - case FunctionResultContent(): - # call_id for the result needs to be the same as the call_id for the function call - return { - "call_id": tool_id_to_call_id[content.call_id], - "type": "function_call_output", - "output": content.result, - } - case TextContent(): - return { - "type": "output_text" if role == ChatRole.ASSISTANT else "input_text", - "text": content.text, - } - # TODO(peterychang): We'll probably need to specialize the other content types as well - case _: - return content.model_dump(exclude_none=True) - - def _prepare_chat_history_for_request(self, chat_messages: Sequence[ChatMessage]) -> list[dict[str, Any]]: - """Prepare the chat history for a request. + def _prepare_chat_messages_for_request(self, chat_messages: Sequence[ChatMessage]) -> list[dict[str, Any]]: + """Prepare the chat messages for a request. Allowing customization of the key names for role/author, and optionally overriding the role. @@ -464,24 +421,336 @@ class OpenAIResponsesClientBase(OpenAIHandler, ChatClientBase): chat_messages: The chat history to prepare. Returns: - prepared_chat_history (Any): The prepared chat history for a request. + The prepared chat messages for a request. """ - tool_id_to_call_id: dict[str, str] = {} + call_id_to_id: dict[str, str] = {} for message in chat_messages: for content in message.contents: - if isinstance(content, FunctionCallContent): - assert content.additional_properties and "call_id" in content.additional_properties # nosec # noqa: S101 - call_id = content.additional_properties["call_id"] - tool_id_to_call_id[content.call_id] = call_id - list_of_list = [self._openai_chat_message_parser(message, tool_id_to_call_id) for message in chat_messages] + if ( + isinstance(content, FunctionCallContent) + and content.additional_properties + and "fc_id" in content.additional_properties + ): + call_id_to_id[content.call_id] = content.additional_properties["fc_id"] + list_of_list = [self._openai_chat_message_parser(message, call_id_to_id) for message in chat_messages] # Flatten the list of lists into a single list return list(chain.from_iterable(list_of_list)) + # region Response creation methods + + def _create_response_content( + self, + response: OpenAIResponse | ParsedResponse[BaseModel], + chat_options: ChatOptions, + ) -> "ChatResponse": + """Create a chat message content object from a choice.""" + structured_response: BaseModel | None = response.output_parsed if isinstance(response, ParsedResponse) else None # type: ignore[reportUnknownMemberType] + + metadata: dict[str, Any] = response.metadata or {} + contents: list[AIContents] = [] + for item in response.output: # type: ignore[reportUnknownMemberType] + match item.type: + # types: + # ParsedResponseOutputMessage[Unknown] | + # ParsedResponseFunctionToolCall | + # ResponseFileSearchToolCall | + # ResponseFunctionWebSearch | + # ResponseComputerToolCall | + # ResponseReasoningItem | + # McpCall | + # McpApprovalRequest | + # ImageGenerationCall | + # LocalShellCall | + # LocalShellCallAction | + # McpListTools | + # ResponseCodeInterpreterToolCall | + # ResponseCustomToolCall | + # ParsedResponseOutputMessage[BaseModel] | + # ResponseOutputMessage | + # ResponseFunctionToolCall + case "message": # ResponseOutputMessage + for message_content in item.content: # type: ignore[reportMissingTypeArgument] + match message_content.type: + case "output_text": + text_content = TextContent( + text=message_content.text, raw_representation=message_content + ) + metadata.update(self._get_metadata_from_response(message_content)) + if message_content.annotations: + text_content.annotations = [] + for annotation in message_content.annotations: + match annotation.type: + case "file_path": + text_content.annotations.append( + CitationAnnotation( + file_id=annotation.file_id, + additional_properties={ + "index": annotation.index, + }, + raw_representation=annotation, + ) + ) + case "file_citation": + text_content.annotations.append( + CitationAnnotation( + url=annotation.filename, + file_id=annotation.file_id, + raw_representation=annotation, + additional_properties={ + "index": annotation.index, + }, + ) + ) + case "url_citation": + text_content.annotations.append( + CitationAnnotation( + title=annotation.title, + url=annotation.url, + annotated_regions=[ + TextSpanRegion( + start_index=annotation.start_index, + end_index=annotation.end_index, + ) + ], + raw_representation=annotation, + ) + ) + case "container_file_citation": + text_content.annotations.append( + CitationAnnotation( + file_id=annotation.file_id, + url=annotation.filename, + additional_properties={ + "container_id": annotation.container_id, + }, + annotated_regions=[ + TextSpanRegion( + start_index=annotation.start_index, + end_index=annotation.end_index, + ) + ], + raw_representation=annotation, + ) + ) + case _: + logger.debug("Unparsed annotation type: %s", annotation.type) + contents.append(text_content) + case "refusal": + contents.append( + TextContent(text=message_content.refusal, raw_representation=message_content) + ) + case "reasoning": # ResponseOutputReasoning + if item.content: + for index, reasoning_content in enumerate(item.content): + additional_properties = None + if item.summary and index < len(item.summary): + additional_properties = {"summary": item.summary[index]} + contents.append( + TextReasoningContent( + text=reasoning_content.text, + raw_representation=reasoning_content, + additional_properties=additional_properties, + ) + ) + case "code_interpreter_call": # ResponseOutputCodeInterpreterCall + if item.outputs: + for code_output in item.outputs: + if code_output.type == "logs": + contents.append(TextContent(text=code_output.logs, raw_representation=item)) + if code_output.type == "image": + contents.append( + UriContent( + uri=code_output.url, + raw_representation=item, + # no more specific media type then this can be inferred + media_type="image", + ) + ) + elif item.code: + # fallback if no output was returned is the code: + contents.append(TextContent(text=item.code, raw_representation=item)) + case "function_call": # ResponseOutputFunctionCall + contents.append( + FunctionCallContent( + call_id=item.call_id if item.call_id else "", + name=item.name, + arguments=item.arguments, + additional_properties={"fc_id": item.id}, + raw_representation=item, + ) + ) + case "image_generation_call": # ResponseOutputImageGenerationCall + if item.result: + contents.append( + DataContent( + uri=item.result, + raw_representation=item, + ) + ) + # TODO(peterychang): Add support for other content types + case _: + logger.debug("Unparsed content of type: %s: %s", item.type, item) + response_message = ChatMessage(role="assistant", contents=contents) + args: dict[str, Any] = { + "response_id": response.id, + "created_at": datetime.fromtimestamp(response.created_at).strftime("%Y-%m-%dT%H:%M:%S.%fZ"), + "messages": response_message, + "model_id": response.model, + "additional_properties": metadata, + "raw_representation": response, + } + if chat_options.store: + args["conversation_id"] = response.id + if response.usage and (usage_details := self._usage_details_from_openai(response.usage)): + args["usage_details"] = usage_details + if structured_response: + args["value"] = structured_response + return StructuredResponse(**args) + return ChatResponse(**args) + + def _create_streaming_response_content( + self, + event: OpenAIResponseStreamEvent, + chat_options: ChatOptions, + function_call_ids: dict[int, tuple[str, str]], + ) -> ChatResponseUpdate: + """Create a streaming chat message content object from a choice.""" + metadata: dict[str, Any] = {} + items: list[AIContents] = [] + conversation_id: str | None = None + model = self.ai_model_id + # TODO(peterychang): Add support for other content types + match event: + case ResponseContentPartAddedEvent(): + match event.part: + case ResponseOutputText(): + items.append(TextContent(text=event.part.text, raw_representation=event)) + metadata.update(self._get_metadata_from_response(event.part)) + case ResponseOutputRefusal(): + items.append(TextContent(text=event.part.refusal, raw_representation=event)) + case ResponseTextDeltaEvent(): + items.append(TextContent(text=event.delta, raw_representation=event)) + metadata.update(self._get_metadata_from_response(event)) + case ResponseCompletedEvent(): + conversation_id = event.response.id if chat_options.store is True else None + model = event.response.model + if event.response.usage: + usage = self._usage_details_from_openai(event.response.usage) + if usage: + items.append(UsageContent(details=usage, raw_representation=event)) + case ResponseOutputItemAddedEvent(): + if event.item.type == "function_call": + function_call_ids[event.output_index] = (event.item.call_id, event.item.name) + case ResponseFunctionCallArgumentsDeltaEvent(): + call_id, name = function_call_ids.get(event.output_index, (None, None)) + if call_id and name: + items.append( + FunctionCallContent( + call_id=call_id, + name=name, + arguments=event.delta, + additional_properties={"output_index": event.output_index, "fc_id": event.item_id}, + raw_representation=event, + ) + ) + case _: + logger.debug("Unparsed event: %s", event) + + return ChatResponseUpdate( + contents=items, + conversation_id=conversation_id, + role=ChatRole.ASSISTANT, + ai_model_id=model, + additional_properties=metadata, + raw_representation=event, + ) + + def _usage_details_from_openai(self, usage: ResponseUsage) -> UsageDetails | None: + details = UsageDetails( + input_token_count=usage.input_tokens, + output_token_count=usage.output_tokens, + total_token_count=usage.total_tokens, + ) + if usage.input_tokens_details and usage.input_tokens_details.cached_tokens: + details["openai.cached_input_tokens"] = usage.input_tokens_details.cached_tokens + if usage.output_tokens_details and usage.output_tokens_details.reasoning_tokens: + details["openai.reasoning_tokens"] = usage.output_tokens_details.reasoning_tokens + return details + + def _openai_chat_message_parser( + self, + message: ChatMessage, + call_id_to_id: dict[str, str], + ) -> list[dict[str, Any]]: + """Parse a chat message into the openai format.""" + all_messages: list[dict[str, Any]] = [] + args: dict[str, Any] = { + "role": message.role.value if isinstance(message.role, ChatRole) else message.role, + } + if message.additional_properties: + args["metadata"] = message.additional_properties + for content in message.contents: + match content: + case FunctionResultContent(): + new_args: dict[str, Any] = {} + new_args.update(self._openai_content_parser(message.role, content, call_id_to_id)) + all_messages.append(new_args) + case FunctionCallContent(): + function_call = self._openai_content_parser(message.role, content, call_id_to_id) + all_messages.append(function_call) # type: ignore + case _: + if "content" not in args: + args["content"] = [] + args["content"].append(self._openai_content_parser(message.role, content, call_id_to_id)) # type: ignore + if "content" in args or "tool_calls" in args: + all_messages.append(args) + return all_messages + + def _openai_content_parser( + self, + role: ChatRole, + content: AIContents, + call_id_to_id: dict[str, str], + ) -> dict[str, Any]: + """Parse contents into the openai format.""" + match content: + case FunctionCallContent(): + return { + "call_id": content.call_id, + "id": call_id_to_id[content.call_id], + "type": "function_call", + "name": content.name, + "arguments": content.arguments, + } + case FunctionResultContent(): + # call_id for the result needs to be the same as the call_id for the function call + args: dict[str, Any] = { + "call_id": content.call_id, + "id": call_id_to_id.get(content.call_id), + "type": "function_call_output", + } + if content.result: + args["output"] = prepare_function_call_results(content.result) + return args + case TextContent(): + return { + "type": "output_text" if role == ChatRole.ASSISTANT else "input_text", + "text": content.text, + } + # TODO(peterychang): We'll probably need to specialize the other content types as well + case _: + return content.model_dump(exclude_none=True) + def _get_metadata_from_response(self, output: Any) -> dict[str, Any]: """Get metadata from a chat choice.""" - return { - "logprobs": getattr(output, "logprobs", None), - } + if logprobs := getattr(output, "logprobs", None): + return { + "logprobs": logprobs, + } + return {} + + +TOpenAIResponsesClient = TypeVar("TOpenAIResponsesClient", bound="OpenAIResponsesClient") @use_telemetry @@ -489,8 +758,6 @@ class OpenAIResponsesClientBase(OpenAIHandler, ChatClientBase): class OpenAIResponsesClient(OpenAIConfigBase, OpenAIResponsesClientBase): """OpenAI Responses client class.""" - MODEL_PROVIDER_NAME: ClassVar[str] = "openai" # type: ignore[reportIncompatibleVariableOverride, misc] - def __init__( self, ai_model_id: str | None = None, @@ -540,25 +807,19 @@ class OpenAIResponsesClient(OpenAIConfigBase, OpenAIResponsesClientBase): ai_model_id=openai_settings.responses_model_id, api_key=openai_settings.api_key.get_secret_value() if openai_settings.api_key else None, org_id=openai_settings.org_id, - ai_model_type=OpenAIModelTypes.RESPONSE, default_headers=default_headers, client=async_client, instruction_role=instruction_role, ) @classmethod - def from_dict(cls, settings: dict[str, Any]) -> "OpenAIResponsesClient": + def from_dict(cls: type[TOpenAIResponsesClient], settings: dict[str, Any]) -> TOpenAIResponsesClient: """Initialize an Open AI service from a dictionary of settings. Args: settings: A dictionary of settings for the service. """ - return OpenAIResponsesClient( - ai_model_id=settings["ai_model_id"], - default_headers=settings.get("default_headers"), - api_key=settings.get("api_key"), - org_id=settings.get("org_id"), - ) + return cls(**settings) # endregion diff --git a/python/packages/main/agent_framework/openai/_shared.py b/python/packages/main/agent_framework/openai/_shared.py index 5e922d69d7..9332f0ecf4 100644 --- a/python/packages/main/agent_framework/openai/_shared.py +++ b/python/packages/main/agent_framework/openai/_shared.py @@ -1,19 +1,16 @@ # Copyright (c) Microsoft. All rights reserved. +import json import logging -from abc import ABC from collections.abc import Mapping from copy import copy -from enum import Enum from typing import Annotated, Any, ClassVar, Union from openai import ( AsyncOpenAI, AsyncStream, - BadRequestError, _legacy_response, # type: ignore ) -from openai.lib._parsing._completions import type_to_response_format_param from openai.types import Completion from openai.types.audio import Transcription from openai.types.chat import ChatCompletion, ChatCompletionChunk @@ -25,10 +22,9 @@ from pydantic.types import StringConstraints from .._logging import get_logger from .._pydantic import AFBaseModel, AFBaseSettings -from .._types import ChatOptions, SpeechToTextOptions, TextToSpeechOptions -from ..exceptions import ServiceInitializationError, ServiceInvalidRequestError, ServiceResponseException +from .._types import AIContents, ChatOptions, SpeechToTextOptions, TextToSpeechOptions +from ..exceptions import ServiceInitializationError from ..telemetry import APP_INFO, USER_AGENT_KEY, prepend_agent_framework_to_user_agent -from ._exceptions import OpenAIContentFilterException logger: logging.Logger = get_logger("agent_framework.openai") @@ -46,11 +42,7 @@ RESPONSE_TYPE = Union[ _legacy_response.HttpxBinaryResponseContent, ] -OPTION_TYPE = Union[ - ChatOptions, - SpeechToTextOptions, - TextToSpeechOptions, -] +OPTION_TYPE = Union[ChatOptions, SpeechToTextOptions, TextToSpeechOptions, dict[str, Any]] __all__ = [ @@ -58,6 +50,23 @@ __all__ = [ ] +def prepare_function_call_results(content: AIContents | Any | list[AIContents | Any]) -> str | list[str]: + """Prepare the values of the function call results.""" + if isinstance(content, list): + results: list[str] = [] + for item in content: + res = prepare_function_call_results(item) + if isinstance(res, list): + results.extend(res) + else: + results.append(res) + return results[0] if len(results) == 1 else results + if isinstance(content, BaseModel): + return content.model_dump_json(exclude_none=True, exclude={"raw_representation", "additional_properties"}) + # fallback + return json.dumps(content) + + class OpenAISettings(AFBaseSettings): """OpenAI environment settings. @@ -108,177 +117,23 @@ class OpenAISettings(AFBaseSettings): realtime_model_id: str | None = None -class OpenAIModelTypes(Enum): - """OpenAI model types, can be text, chat or embedding.""" - - CHAT = "chat" - EMBEDDING = "embedding" - TEXT_TO_IMAGE = "text-to-image" - SPEECH_TO_TEXT = "speech-to-text" - TEXT_TO_SPEECH = "text-to-speech" - REALTIME = "realtime" - RESPONSE = "response" - - -class OpenAIHandler(AFBaseModel, ABC): - """Internal class for calls to OpenAI API's.""" +class OpenAIHandler(AFBaseModel): + """Base class for OpenAI Clients.""" client: AsyncOpenAI ai_model_id: Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)] - ai_model_type: OpenAIModelTypes = OpenAIModelTypes.CHAT - - async def _send_request(self, options: OPTION_TYPE, messages: list[dict[str, Any]] | None = None) -> RESPONSE_TYPE: - """Send a request to the OpenAI API.""" - if self.ai_model_type == OpenAIModelTypes.CHAT: - assert isinstance(options, ChatOptions) # nosec # noqa: S101 - return await self._send_completion_request(options, messages) - # TODO(evmattso): move other PromptExecutionSettings to a common options class - if self.ai_model_type == OpenAIModelTypes.EMBEDDING: - raise NotImplementedError("Embedding generation is not yet implemented in OpenAIHandler") - if self.ai_model_type == OpenAIModelTypes.TEXT_TO_IMAGE: - raise NotImplementedError("Text to image generation is not yet implemented in OpenAIHandler") - if self.ai_model_type == OpenAIModelTypes.SPEECH_TO_TEXT: - assert isinstance(options, SpeechToTextOptions) # nosec # noqa: S101 - return await self._send_audio_to_text_request(options) - if self.ai_model_type == OpenAIModelTypes.TEXT_TO_SPEECH: - assert isinstance(options, TextToSpeechOptions) # nosec # noqa: S101 - return await self._send_text_to_audio_request(options) - if self.ai_model_type == OpenAIModelTypes.RESPONSE: - assert isinstance(options, ChatOptions) # nosec # noqa: S101 - return await self._send_response_request(options, messages) - - raise NotImplementedError(f"Model type {self.ai_model_type} is not supported") - - async def _send_completion_request( - self, - chat_options: "ChatOptions", - messages: list[dict[str, Any]] | None = None, - ) -> ChatCompletion | AsyncStream[ChatCompletionChunk]: - """Execute the appropriate call to OpenAI models.""" - try: - options_dict = chat_options.to_provider_settings() - if messages and "messages" not in options_dict: - options_dict["messages"] = messages - if "messages" not in options_dict: - raise ServiceInvalidRequestError("Messages are required for chat completions") - self._handle_structured_outputs(chat_options, options_dict) - if chat_options.tools is None: - options_dict.pop("parallel_tool_calls", None) - return await self.client.chat.completions.create(**options_dict) # type: ignore - except BadRequestError as ex: - if ex.code == "content_filter": - raise OpenAIContentFilterException( - f"{type(self)} service encountered a content error", - ex, - ) from ex - raise ServiceResponseException( - f"{type(self)} service failed to complete the prompt", - ex, - ) from ex - except Exception as ex: - raise ServiceResponseException( - f"{type(self)} service failed to complete the prompt", - ex, - ) from ex - - async def _send_audio_to_text_request(self, options: SpeechToTextOptions) -> Transcription: - """Send a request to the OpenAI audio to text endpoint.""" - if not options.additional_properties["filename"]: - raise ServiceInvalidRequestError("Audio file is required for audio to text service") - - try: - # TODO(peterychang): open isn't async safe - with open(options.additional_properties["filename"], "rb") as audio_file: # noqa: ASYNC230 - return await self.client.audio.transcriptions.create( # type: ignore - file=audio_file, - **options.to_provider_settings(exclude={"filename"}), - ) - except Exception as ex: - raise ServiceResponseException( - f"{type(self)} service failed to transcribe audio", - ex, - ) from ex - - async def _send_text_to_audio_request( - self, options: TextToSpeechOptions - ) -> _legacy_response.HttpxBinaryResponseContent: - """Send a request to the OpenAI text to audio endpoint. - - The OpenAI API returns the content of the generated audio file. - """ - try: - return await self.client.audio.speech.create( - **options.to_provider_settings(), - ) - except Exception as ex: - raise ServiceResponseException( - f"{type(self)} service failed to generate audio", - ex, - ) from ex - - async def _send_response_request( - self, - chat_options: "ChatOptions", - messages: list[dict[str, Any]] | None = None, - ) -> Response | AsyncStream[ResponseStreamEvent]: - try: - options_dict = chat_options.to_provider_settings() - if messages and "input" not in options_dict: - options_dict["input"] = messages - if "input" not in options_dict: - raise ServiceInvalidRequestError("Messages are required for chat completions") - if chat_options.tools is None: - options_dict.pop("parallel_tool_calls", None) - else: - options_dict["tools"] = options_dict["response_tools"] - options_dict.pop("response_tools", None) - if chat_options.response_format: - # create call does not support response_format, so we need to handle it via parse call - resp_format = options_dict.pop("response_format", None) - return await self.client.responses.parse( - **options_dict, - text_format=resp_format, - ) - if "store" not in options_dict: - options_dict["store"] = False - if "conversation_id" in options_dict: - options_dict["previous_response_id"] = options_dict["conversation_id"] - options_dict.pop("conversation_id") - return await self.client.responses.create(**options_dict) # type: ignore - except BadRequestError as ex: - if ex.code == "content_filter": - raise OpenAIContentFilterException( - f"{type(self)} service encountered a content error", - ex, - ) from ex - raise ServiceResponseException( - f"{type(self)} service failed to complete the prompt", - ex, - ) from ex - except Exception as ex: - raise ServiceResponseException( - f"{type(self)} service failed to complete the prompt", - ex, - ) from ex - - def _handle_structured_outputs(self, chat_options: "ChatOptions", options_dict: dict[str, Any]) -> None: - if ( - chat_options.response_format - and isinstance(chat_options.response_format, type) - and issubclass(chat_options.response_format, BaseModel) - ): - options_dict["response_format"] = type_to_response_format_param(chat_options.response_format) class OpenAIConfigBase(OpenAIHandler): """Internal class for configuring a connection to an OpenAI service.""" + MODEL_PROVIDER_NAME: ClassVar[str] = "openai" # type: ignore[reportIncompatibleVariableOverride, misc] + @validate_call(config=ConfigDict(arbitrary_types_allowed=True)) def __init__( self, ai_model_id: str = Field(min_length=1), api_key: str | None = Field(min_length=1), - ai_model_type: OpenAIModelTypes | None = OpenAIModelTypes.CHAT, org_id: str | None = None, default_headers: Mapping[str, str] | None = None, client: AsyncOpenAI | None = None, @@ -295,8 +150,6 @@ class OpenAIConfigBase(OpenAIHandler): Default to a preset value. api_key (str): OpenAI API key for authentication. Must be non-empty. (Optional) - ai_model_type (OpenAIModelTypes): The type of OpenAI - model to interact with. Defaults to CHAT. org_id (str): OpenAI organization ID. This is optional unless the account belongs to multiple organizations. default_headers (Mapping[str, str]): Default headers @@ -324,7 +177,6 @@ class OpenAIConfigBase(OpenAIHandler): args = { "ai_model_id": ai_model_id, "client": client, - "ai_model_type": ai_model_type, } if instruction_role: args["instruction_role"] = instruction_role @@ -344,7 +196,6 @@ class OpenAIConfigBase(OpenAIHandler): "completion_tokens", "total_tokens", "api_type", - "ai_model_type", "client", }, by_alias=True, diff --git a/python/packages/main/agent_framework/telemetry.py b/python/packages/main/agent_framework/telemetry.py index f4b6ecf581..76c643205a 100644 --- a/python/packages/main/agent_framework/telemetry.py +++ b/python/packages/main/agent_framework/telemetry.py @@ -138,6 +138,7 @@ class GenAIAttributes(str, Enum): # Agent Framework specific attributes MEASUREMENT_FUNCTION_TAG_NAME = "agent_framework.function.name" + MEASUREMENT_FUNCTION_INVOCATION_DURATION = "agent_framework.function.invocation.duration" AGENT_FRAMEWORK_GEN_AI_SYSTEM = "microsoft.agent_framework" diff --git a/python/packages/main/tests/main/test_clients.py b/python/packages/main/tests/main/test_clients.py index cea5b8b2a1..36238a44b5 100644 --- a/python/packages/main/tests/main/test_clients.py +++ b/python/packages/main/tests/main/test_clients.py @@ -305,48 +305,3 @@ async def test_base_client_with_streaming_function_calling_disabled(chat_client_ updates.append(update) assert len(updates) == 1 assert exec_counter == 0 - - -def test_chat_options_parsing_tools(chat_client_base, ai_function_tool) -> None: - """Test that chat options can parse tools correctly.""" - - def echo() -> str: - """Echo the input.""" - return "Echo" - - dict_function = { - "type": "function", - "function": { - "name": "get_weather", - "description": "Retrieves current weather for the given location.", - "parameters": { - "type": "object", - "properties": { - "location": {"type": "string", "description": "City and country e.g. Bogotá, Colombia"}, - "units": { - "type": "string", - "enum": ["celsius", "fahrenheit"], - "description": "Units the temperature will be returned in.", - }, - }, - "required": ["location", "units"], - "additionalProperties": False, - }, - "strict": True, - }, - } - - options = ChatOptions(tools=[ai_function_tool, echo, dict_function], tool_choice="auto") - assert len(options.tools) == 3 - assert options.tools[0] == ai_function_tool - assert options.tools[1] != echo - assert options.tools[2] == dict_function - # after prepare, the tools should be represented as dicts - # while ai_tools is still the same. - chat_client_base._prepare_tools_and_tool_choice(chat_options=options) - assert options._ai_tools[0] == ai_function_tool - assert options._ai_tools[2] == dict_function - assert len(options.tools) == 3 - assert options.tools[0]["function"]["name"] == "simple_function" - assert options.tools[1]["function"]["name"] == "echo" - assert options.tools[2]["function"]["name"] == "get_weather" diff --git a/python/packages/main/tests/main/test_tools.py b/python/packages/main/tests/main/test_tools.py index 38918cf814..aee2c5c0bb 100644 --- a/python/packages/main/tests/main/test_tools.py +++ b/python/packages/main/tests/main/test_tools.py @@ -95,7 +95,7 @@ async def test_ai_function_invoke_telemetry_enabled(): # Mock the histogram mock_histogram = Mock() - telemetry_test_tool.invocation_duration_histogram = mock_histogram + telemetry_test_tool._invocation_duration_histogram = mock_histogram # Call invoke result = await telemetry_test_tool.invoke(x=1, y=2, tool_call_id="test_call_id") @@ -139,7 +139,7 @@ async def test_ai_function_invoke_telemetry_with_pydantic_args(): mock_start_span.return_value = mock_context_manager mock_histogram = Mock() - pydantic_test_tool.invocation_duration_histogram = mock_histogram + pydantic_test_tool._invocation_duration_histogram = mock_histogram # Call invoke with Pydantic model result = await pydantic_test_tool.invoke(arguments=args_model, tool_call_id="pydantic_call") @@ -172,7 +172,7 @@ async def test_ai_function_invoke_telemetry_with_exception(): mock_start_span.return_value = mock_context_manager mock_histogram = Mock() - exception_test_tool.invocation_duration_histogram = mock_histogram + exception_test_tool._invocation_duration_histogram = mock_histogram # Call invoke and expect exception with pytest.raises(ValueError, match="Test exception for telemetry"): @@ -212,7 +212,7 @@ async def test_ai_function_invoke_telemetry_async_function(): mock_start_span.return_value = mock_context_manager mock_histogram = Mock() - async_telemetry_test.invocation_duration_histogram = mock_histogram + async_telemetry_test._invocation_duration_histogram = mock_histogram # Call invoke result = await async_telemetry_test.invoke(x=3, y=4, tool_call_id="async_call") @@ -251,7 +251,7 @@ async def test_ai_function_invoke_telemetry_no_tool_call_id(): mock_start_span.return_value = mock_context_manager mock_histogram = Mock() - no_id_test_tool.invocation_duration_histogram = mock_histogram + no_id_test_tool._invocation_duration_histogram = mock_histogram # Call invoke without tool_call_id result = await no_id_test_tool.invoke(x=5) @@ -300,29 +300,19 @@ def test_hosted_code_interpreter_tool_default(): assert tool.name == "code_interpreter" assert tool.inputs == [] - assert tool.description is None + assert tool.description == "" assert tool.additional_properties is None assert str(tool) == "HostedCodeInterpreterTool(name=code_interpreter)" -def test_hosted_code_interpreter_tool_custom_name(): - """Test HostedCodeInterpreterTool with custom name.""" - tool = HostedCodeInterpreterTool(name="custom_interpreter") - - assert tool.name == "custom_interpreter" - assert tool.inputs == [] - assert str(tool) == "HostedCodeInterpreterTool(name=custom_interpreter)" - - def test_hosted_code_interpreter_tool_with_description(): """Test HostedCodeInterpreterTool with description and additional properties.""" tool = HostedCodeInterpreterTool( - name="test_interpreter", description="A test code interpreter", additional_properties={"version": "1.0", "language": "python"}, ) - assert tool.name == "test_interpreter" + assert tool.name == "code_interpreter" assert tool.description == "A test code interpreter" assert tool.additional_properties == {"version": "1.0", "language": "python"} diff --git a/python/packages/main/tests/main/test_types.py b/python/packages/main/tests/main/test_types.py index 0f4ed6fd0e..d92ecd3423 100644 --- a/python/packages/main/tests/main/test_types.py +++ b/python/packages/main/tests/main/test_types.py @@ -12,6 +12,7 @@ from agent_framework import ( AIAnnotation, AIContent, AIContents, + AIFunction, AITool, AnnotatedRegion, ChatFinishReason, @@ -723,11 +724,12 @@ def test_chat_options_init_with_args(ai_function_tool, ai_tool) -> None: assert options.presence_penalty == 0.0 assert options.frequency_penalty == 0.0 assert options.user == "user-123" - for tool in options._ai_tools: + for tool in options.tools: assert isinstance(tool, AITool) assert tool.name is not None assert tool.description is not None - assert tool.parameters() is not None + if isinstance(tool, AIFunction): + assert tool.parameters() is not None settings = options.to_provider_settings() assert settings["model"] == "gpt-4" # uses alias @@ -754,8 +756,6 @@ def test_chat_options_and(ai_function_tool, ai_tool) -> None: options3 = options1 & options2 assert options3.ai_model_id == "gpt-4.1" - assert len(options3._ai_tools) == 2 - assert options3._ai_tools == [ai_function_tool, ai_tool] assert options3.tools == [ai_function_tool, ai_tool] assert options3.logit_bias == {"x": 1} assert options3.metadata == {"a": "b"} diff --git a/python/packages/main/tests/openai/test_openai_chat_client_base.py b/python/packages/main/tests/openai/test_openai_chat_client_base.py index 894b4fd7e5..d1c01e21f5 100644 --- a/python/packages/main/tests/openai/test_openai_chat_client_base.py +++ b/python/packages/main/tests/openai/test_openai_chat_client_base.py @@ -15,7 +15,6 @@ from pydantic import BaseModel from agent_framework import ChatMessage, ChatResponseUpdate from agent_framework.exceptions import ( - ServiceInvalidResponseError, ServiceResponseException, ) from agent_framework.openai import OpenAIChatClient @@ -192,7 +191,7 @@ async def test_cmc_general_exception( @patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock) -async def test_scmc( +async def test_get_streaming( mock_create: AsyncMock, chat_history: list[ChatMessage], openai_unit_test_env: dict[str, str], @@ -231,7 +230,7 @@ async def test_scmc( @patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock) -async def test_scmc_singular( +async def test_get_streaming_singular( mock_create: AsyncMock, chat_history: list[ChatMessage], openai_unit_test_env: dict[str, str], @@ -270,7 +269,7 @@ async def test_scmc_singular( @patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock) -async def test_scmc_structured_output_no_fcc( +async def test_get_streaming_structured_output_no_fcc( mock_create: AsyncMock, chat_history: list[ChatMessage], openai_unit_test_env: dict[str, str], @@ -308,7 +307,7 @@ async def test_scmc_structured_output_no_fcc( @patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock) -async def test_scmc_no_fcc_in_response( +async def test_get_streaming_no_fcc_in_response( mock_create: AsyncMock, chat_history: list[ChatMessage], mock_streaming_chat_completion_response: ChatCompletion, @@ -334,7 +333,7 @@ async def test_scmc_no_fcc_in_response( @patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock) -async def test_scmc_no_stream( +async def test_get_streaming_no_stream( mock_create: AsyncMock, chat_history: list[ChatMessage], openai_unit_test_env: dict[str, str], @@ -344,7 +343,7 @@ async def test_scmc_no_stream( chat_history.append(ChatMessage(role="user", text="hello world")) openai_chat_completion = OpenAIChatClient() - with pytest.raises(ServiceInvalidResponseError): + with pytest.raises(ServiceResponseException): [ msg async for msg in openai_chat_completion.get_streaming_response( diff --git a/python/packages/main/tests/openai/test_openai_responses_client.py b/python/packages/main/tests/openai/test_openai_responses_client.py index 8a083140e1..e610963966 100644 --- a/python/packages/main/tests/openai/test_openai_responses_client.py +++ b/python/packages/main/tests/openai/test_openai_responses_client.py @@ -7,7 +7,7 @@ import pytest from pydantic import BaseModel from agent_framework import ChatClient, ChatMessage, ChatResponse, ChatResponseUpdate, TextContent, ai_function -from agent_framework.exceptions import ServiceInitializationError, ServiceResponseException +from agent_framework.exceptions import ServiceInitializationError from agent_framework.openai import OpenAIResponsesClient skip_if_openai_integration_tests_disabled = pytest.mark.skipif( @@ -247,23 +247,21 @@ async def test_openai_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?")) - # This is currently broken. See https://github.com/openai/openai-python/issues/2305 - with pytest.raises(ServiceResponseException): - response = openai_responses_client.get_streaming_response( - messages=messages, - response_format=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 + response = openai_responses_client.get_streaming_response( + messages=messages, + response_format=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 + output = OutputStruct.model_validate_json(full_message) + assert "Seattle" in output.location + assert "sunny" in output.weather @skip_if_openai_integration_tests_disabled @@ -294,22 +292,20 @@ async def test_openai_responses_client_streaming_tools() -> None: messages.clear() messages.append(ChatMessage(role="user", text="What is the weather in Seattle?")) - # This is currently broken. See https://github.com/openai/openai-python/issues/2305 - with pytest.raises(ServiceResponseException): - response = openai_responses_client.get_streaming_response( - messages=messages, - tools=[get_weather], - tool_choice="auto", - response_format=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 + response = openai_responses_client.get_streaming_response( + messages=messages, + tools=[get_weather], + tool_choice="auto", + response_format=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 + output = OutputStruct.model_validate_json(full_message) + assert "Seattle" in output.location + assert "sunny" in output.weather diff --git a/python/samples/getting_started/agents/azure_assistants_client/azure_assistants_basic.py b/python/samples/getting_started/agents/azure_assistants_client/azure_assistants_basic.py index 66b2cc4590..1c5cc77630 100644 --- a/python/samples/getting_started/agents/azure_assistants_client/azure_assistants_basic.py +++ b/python/samples/getting_started/agents/azure_assistants_client/azure_assistants_basic.py @@ -4,8 +4,8 @@ import asyncio from random import randint from typing import Annotated -from agent_framework import ChatClientAgent from agent_framework.azure import AzureAssistantsClient +from azure.identity import DefaultAzureCredential from pydantic import Field @@ -23,8 +23,7 @@ async def non_streaming_example() -> None: # Since no assistant ID is provided, the assistant will be automatically created # and deleted after getting a response - async with ChatClientAgent( - chat_client=AzureAssistantsClient(), + async with AzureAssistantsClient(ad_credential=DefaultAzureCredential()).create_agent( instructions="You are a helpful weather agent.", tools=get_weather, ) as agent: @@ -40,8 +39,7 @@ async def streaming_example() -> None: # Since no assistant ID is provided, the assistant will be automatically created # and deleted after getting a response - async with ChatClientAgent( - chat_client=AzureAssistantsClient(), + async with AzureAssistantsClient(ad_credential=DefaultAzureCredential()).create_agent( instructions="You are a helpful weather agent.", tools=get_weather, ) as agent: diff --git a/python/samples/getting_started/agents/azure_assistants_client/azure_assistants_with_code_interpreter.py b/python/samples/getting_started/agents/azure_assistants_client/azure_assistants_with_code_interpreter.py index 5a6114226d..6f75436f5c 100644 --- a/python/samples/getting_started/agents/azure_assistants_client/azure_assistants_with_code_interpreter.py +++ b/python/samples/getting_started/agents/azure_assistants_client/azure_assistants_with_code_interpreter.py @@ -4,6 +4,7 @@ import asyncio from agent_framework import AgentRunResponseUpdate, ChatClientAgent, ChatResponseUpdate, HostedCodeInterpreterTool from agent_framework.azure import AzureAssistantsClient +from azure.identity import DefaultAzureCredential from openai.types.beta.threads.runs import ( CodeInterpreterToolCallDelta, RunStepDelta, @@ -37,7 +38,7 @@ async def main() -> None: print("=== Azure OpenAI Assistants Agent with Code Interpreter Example ===") async with ChatClientAgent( - chat_client=AzureAssistantsClient(), + chat_client=AzureAssistantsClient(ad_credential=DefaultAzureCredential()), instructions="You are a helpful assistant that can write and execute Python code to solve problems.", tools=HostedCodeInterpreterTool(), ) as agent: diff --git a/python/samples/getting_started/agents/azure_assistants_client/azure_assistants_with_function_tools.py b/python/samples/getting_started/agents/azure_assistants_client/azure_assistants_with_function_tools.py index ed697af840..25c4308b16 100644 --- a/python/samples/getting_started/agents/azure_assistants_client/azure_assistants_with_function_tools.py +++ b/python/samples/getting_started/agents/azure_assistants_client/azure_assistants_with_function_tools.py @@ -7,6 +7,7 @@ from typing import Annotated from agent_framework import ChatClientAgent from agent_framework.azure import AzureAssistantsClient +from azure.identity import DefaultAzureCredential from pydantic import Field @@ -31,7 +32,7 @@ async def tools_on_agent_level() -> None: # Tools are provided when creating the agent # The agent can use these tools for any query during its lifetime async with ChatClientAgent( - chat_client=AzureAssistantsClient(), + chat_client=AzureAssistantsClient(ad_credential=DefaultAzureCredential()), instructions="You are a helpful assistant that can provide weather and time information.", tools=[get_weather, get_time], # Tools defined at agent creation ) as agent: @@ -60,7 +61,7 @@ async def tools_on_run_level() -> None: # Agent created without tools async with ChatClientAgent( - chat_client=AzureAssistantsClient(), + chat_client=AzureAssistantsClient(ad_credential=DefaultAzureCredential()), instructions="You are a helpful assistant.", # No tools defined here ) as agent: @@ -89,7 +90,7 @@ async def mixed_tools_example() -> None: # Agent created with some base tools async with ChatClientAgent( - chat_client=AzureAssistantsClient(), + chat_client=AzureAssistantsClient(ad_credential=DefaultAzureCredential()), instructions="You are a comprehensive assistant that can help with various information requests.", tools=[get_weather], # Base tool available for all queries ) as agent: diff --git a/python/samples/getting_started/agents/azure_assistants_client/azure_assistants_with_thread.py b/python/samples/getting_started/agents/azure_assistants_client/azure_assistants_with_thread.py index 48523b6ec9..749eee597e 100644 --- a/python/samples/getting_started/agents/azure_assistants_client/azure_assistants_with_thread.py +++ b/python/samples/getting_started/agents/azure_assistants_client/azure_assistants_with_thread.py @@ -6,6 +6,7 @@ from typing import Annotated from agent_framework import ChatClientAgent, ChatClientAgentThread from agent_framework.azure import AzureAssistantsClient +from azure.identity import DefaultAzureCredential from pydantic import Field @@ -22,7 +23,7 @@ async def example_with_automatic_thread_creation() -> None: print("=== Automatic Thread Creation Example ===") async with ChatClientAgent( - chat_client=AzureAssistantsClient(), + chat_client=AzureAssistantsClient(ad_credential=DefaultAzureCredential()), instructions="You are a helpful weather agent.", tools=get_weather, ) as agent: @@ -46,7 +47,7 @@ async def example_with_thread_persistence() -> None: print("Using the same thread across multiple conversations to maintain context.\n") async with ChatClientAgent( - chat_client=AzureAssistantsClient(), + chat_client=AzureAssistantsClient(ad_credential=DefaultAzureCredential()), instructions="You are a helpful weather agent.", tools=get_weather, ) as agent: @@ -82,7 +83,7 @@ async def example_with_existing_thread_id() -> None: existing_thread_id = None async with ChatClientAgent( - chat_client=AzureAssistantsClient(), + chat_client=AzureAssistantsClient(ad_credential=DefaultAzureCredential()), instructions="You are a helpful weather agent.", tools=get_weather, ) as agent: @@ -102,7 +103,7 @@ async def example_with_existing_thread_id() -> None: # Create a new agent instance but use the existing thread ID async with ChatClientAgent( - chat_client=AzureAssistantsClient(thread_id=existing_thread_id), + chat_client=AzureAssistantsClient(thread_id=existing_thread_id, ad_credential=DefaultAzureCredential()), instructions="You are a helpful weather agent.", tools=get_weather, ) as agent: diff --git a/python/samples/getting_started/agents/azure_chat_client/azure_chat_client_basic.py b/python/samples/getting_started/agents/azure_chat_client/azure_chat_client_basic.py index 80564a8785..e607bb5c35 100644 --- a/python/samples/getting_started/agents/azure_chat_client/azure_chat_client_basic.py +++ b/python/samples/getting_started/agents/azure_chat_client/azure_chat_client_basic.py @@ -4,8 +4,8 @@ import asyncio from random import randint from typing import Annotated -from agent_framework import ChatClientAgent from agent_framework.azure import AzureChatClient +from azure.identity import DefaultAzureCredential from pydantic import Field @@ -22,8 +22,7 @@ async def non_streaming_example() -> None: print("=== Non-streaming Response Example ===") # Create agent with Azure Chat Client - agent = ChatClientAgent( - chat_client=AzureChatClient(), + agent = AzureChatClient(ad_credential=DefaultAzureCredential()).create_agent( instructions="You are a helpful weather agent.", tools=get_weather, ) @@ -39,8 +38,7 @@ async def streaming_example() -> None: print("=== Streaming Response Example ===") # Create agent with Azure Chat Client - agent = ChatClientAgent( - chat_client=AzureChatClient(), + agent = AzureChatClient(ad_credential=DefaultAzureCredential()).create_agent( instructions="You are a helpful weather agent.", tools=get_weather, ) diff --git a/python/samples/getting_started/agents/azure_chat_client/azure_chat_client_with_function_tools.py b/python/samples/getting_started/agents/azure_chat_client/azure_chat_client_with_function_tools.py index 7050c75f4e..a5fcdbb852 100644 --- a/python/samples/getting_started/agents/azure_chat_client/azure_chat_client_with_function_tools.py +++ b/python/samples/getting_started/agents/azure_chat_client/azure_chat_client_with_function_tools.py @@ -7,6 +7,7 @@ from typing import Annotated from agent_framework import ChatClientAgent from agent_framework.azure import AzureChatClient +from azure.identity import DefaultAzureCredential from pydantic import Field @@ -31,7 +32,7 @@ async def tools_on_agent_level() -> None: # Tools are provided when creating the agent # The agent can use these tools for any query during its lifetime agent = ChatClientAgent( - chat_client=AzureChatClient(), + chat_client=AzureChatClient(ad_credential=DefaultAzureCredential()), instructions="You are a helpful assistant that can provide weather and time information.", tools=[get_weather, get_time], # Tools defined at agent creation ) @@ -61,7 +62,7 @@ async def tools_on_run_level() -> None: # Agent created without tools agent = ChatClientAgent( - chat_client=AzureChatClient(), + chat_client=AzureChatClient(ad_credential=DefaultAzureCredential()), instructions="You are a helpful assistant.", # No tools defined here ) @@ -91,7 +92,7 @@ async def mixed_tools_example() -> None: # Agent created with some base tools agent = ChatClientAgent( - chat_client=AzureChatClient(), + chat_client=AzureChatClient(ad_credential=DefaultAzureCredential()), instructions="You are a comprehensive assistant that can help with various information requests.", tools=[get_weather], # Base tool available for all queries ) diff --git a/python/samples/getting_started/agents/azure_chat_client/azure_chat_client_with_thread.py b/python/samples/getting_started/agents/azure_chat_client/azure_chat_client_with_thread.py index 623efce0e3..7a256286ab 100644 --- a/python/samples/getting_started/agents/azure_chat_client/azure_chat_client_with_thread.py +++ b/python/samples/getting_started/agents/azure_chat_client/azure_chat_client_with_thread.py @@ -6,6 +6,7 @@ from typing import Annotated from agent_framework import ChatClientAgent, ChatClientAgentThread from agent_framework.azure import AzureChatClient +from azure.identity import DefaultAzureCredential from pydantic import Field @@ -22,7 +23,7 @@ async def example_with_automatic_thread_creation() -> None: print("=== Automatic Thread Creation Example ===") agent = ChatClientAgent( - chat_client=AzureChatClient(), + chat_client=AzureChatClient(ad_credential=DefaultAzureCredential()), instructions="You are a helpful weather agent.", tools=get_weather, ) @@ -47,7 +48,7 @@ async def example_with_thread_persistence() -> None: print("Using the same thread across multiple conversations to maintain context.\n") agent = ChatClientAgent( - chat_client=AzureChatClient(), + chat_client=AzureChatClient(ad_credential=DefaultAzureCredential()), instructions="You are a helpful weather agent.", tools=get_weather, ) @@ -80,7 +81,7 @@ async def example_with_existing_thread_messages() -> None: print("=== Existing Thread Messages Example ===") agent = ChatClientAgent( - chat_client=AzureChatClient(), + chat_client=AzureChatClient(ad_credential=DefaultAzureCredential()), instructions="You are a helpful weather agent.", tools=get_weather, ) @@ -102,7 +103,7 @@ async def example_with_existing_thread_messages() -> None: # Create a new agent instance but use the existing thread with its message history new_agent = ChatClientAgent( - chat_client=AzureChatClient(), + chat_client=AzureChatClient(ad_credential=DefaultAzureCredential()), instructions="You are a helpful weather agent.", tools=get_weather, ) diff --git a/python/samples/getting_started/agents/azure_responses_client/azure_responses_client_basic.py b/python/samples/getting_started/agents/azure_responses_client/azure_responses_client_basic.py index 1d717fff00..9520e56498 100644 --- a/python/samples/getting_started/agents/azure_responses_client/azure_responses_client_basic.py +++ b/python/samples/getting_started/agents/azure_responses_client/azure_responses_client_basic.py @@ -4,8 +4,8 @@ import asyncio from random import randint from typing import Annotated -from agent_framework import ChatClientAgent from agent_framework.azure import AzureResponsesClient +from azure.identity import DefaultAzureCredential from pydantic import Field @@ -21,8 +21,7 @@ async def non_streaming_example() -> None: """Example of non-streaming response (get the complete result at once).""" print("=== Non-streaming Response Example ===") - agent = ChatClientAgent( - chat_client=AzureResponsesClient(), + agent = AzureResponsesClient(ad_credential=DefaultAzureCredential()).create_agent( instructions="You are a helpful weather agent.", tools=get_weather, ) @@ -37,8 +36,7 @@ async def streaming_example() -> None: """Example of streaming response (get results as they are generated).""" print("=== Streaming Response Example ===") - agent = ChatClientAgent( - chat_client=AzureResponsesClient(), + agent = AzureResponsesClient(ad_credential=DefaultAzureCredential()).create_agent( instructions="You are a helpful weather agent.", tools=get_weather, ) diff --git a/python/samples/getting_started/agents/azure_responses_client/azure_responses_client_with_code_interpreter.py b/python/samples/getting_started/agents/azure_responses_client/azure_responses_client_with_code_interpreter.py index 2fa186ca11..6ea16bbb71 100644 --- a/python/samples/getting_started/agents/azure_responses_client/azure_responses_client_with_code_interpreter.py +++ b/python/samples/getting_started/agents/azure_responses_client/azure_responses_client_with_code_interpreter.py @@ -4,6 +4,7 @@ import asyncio from agent_framework import ChatClientAgent, ChatResponse, HostedCodeInterpreterTool from agent_framework.azure import AzureResponsesClient +from azure.identity import DefaultAzureCredential from openai.types.responses.response import Response as OpenAIResponse from openai.types.responses.response_code_interpreter_tool_call import ResponseCodeInterpreterToolCall @@ -13,7 +14,7 @@ async def main() -> None: print("=== Azure OpenAI Responses Agent with Code Interpreter Example ===") agent = ChatClientAgent( - chat_client=AzureResponsesClient(), + chat_client=AzureResponsesClient(ad_credential=DefaultAzureCredential()), instructions="You are a helpful assistant that can write and execute Python code to solve problems.", tools=HostedCodeInterpreterTool(), ) diff --git a/python/samples/getting_started/agents/azure_responses_client/azure_responses_client_with_function_tools.py b/python/samples/getting_started/agents/azure_responses_client/azure_responses_client_with_function_tools.py index 96ec5df5c5..d0739dbfae 100644 --- a/python/samples/getting_started/agents/azure_responses_client/azure_responses_client_with_function_tools.py +++ b/python/samples/getting_started/agents/azure_responses_client/azure_responses_client_with_function_tools.py @@ -7,6 +7,7 @@ from typing import Annotated from agent_framework import ChatClientAgent from agent_framework.azure import AzureResponsesClient +from azure.identity import DefaultAzureCredential from pydantic import Field @@ -31,7 +32,7 @@ async def tools_on_agent_level() -> None: # Tools are provided when creating the agent # The agent can use these tools for any query during its lifetime agent = ChatClientAgent( - chat_client=AzureResponsesClient(), + chat_client=AzureResponsesClient(ad_credential=DefaultAzureCredential()), instructions="You are a helpful assistant that can provide weather and time information.", tools=[get_weather, get_time], # Tools defined at agent creation ) @@ -61,7 +62,7 @@ async def tools_on_run_level() -> None: # Agent created without tools agent = ChatClientAgent( - chat_client=AzureResponsesClient(), + chat_client=AzureResponsesClient(ad_credential=DefaultAzureCredential()), instructions="You are a helpful assistant.", # No tools defined here ) @@ -91,7 +92,7 @@ async def mixed_tools_example() -> None: # Agent created with some base tools agent = ChatClientAgent( - chat_client=AzureResponsesClient(), + chat_client=AzureResponsesClient(ad_credential=DefaultAzureCredential()), instructions="You are a comprehensive assistant that can help with various information requests.", tools=[get_weather], # Base tool available for all queries ) diff --git a/python/samples/getting_started/agents/azure_responses_client/azure_responses_client_with_thread.py b/python/samples/getting_started/agents/azure_responses_client/azure_responses_client_with_thread.py index bd22c3a49c..0fd3284738 100644 --- a/python/samples/getting_started/agents/azure_responses_client/azure_responses_client_with_thread.py +++ b/python/samples/getting_started/agents/azure_responses_client/azure_responses_client_with_thread.py @@ -6,6 +6,7 @@ from typing import Annotated from agent_framework import ChatClientAgent, ChatClientAgentThread from agent_framework.azure import AzureResponsesClient +from azure.identity import DefaultAzureCredential from pydantic import Field @@ -22,7 +23,7 @@ async def example_with_automatic_thread_creation() -> None: print("=== Automatic Thread Creation Example ===") agent = ChatClientAgent( - chat_client=AzureResponsesClient(), + chat_client=AzureResponsesClient(ad_credential=DefaultAzureCredential()), instructions="You are a helpful weather agent.", tools=get_weather, ) @@ -49,7 +50,7 @@ async def example_with_thread_persistence_in_memory() -> None: print("=== Thread Persistence Example (In-Memory) ===") agent = ChatClientAgent( - chat_client=AzureResponsesClient(), + chat_client=AzureResponsesClient(ad_credential=DefaultAzureCredential()), instructions="You are a helpful weather agent.", tools=get_weather, ) @@ -92,7 +93,7 @@ async def example_with_existing_thread_id() -> None: existing_thread_id = None agent = ChatClientAgent( - chat_client=AzureResponsesClient(), + chat_client=AzureResponsesClient(ad_credential=DefaultAzureCredential()), instructions="You are a helpful weather agent.", tools=get_weather, ) @@ -116,7 +117,7 @@ async def example_with_existing_thread_id() -> None: print("\n--- Continuing with the same thread ID in a new agent instance ---") agent = ChatClientAgent( - chat_client=AzureResponsesClient(), + chat_client=AzureResponsesClient(ad_credential=DefaultAzureCredential()), instructions="You are a helpful weather agent.", tools=get_weather, ) diff --git a/python/samples/getting_started/agents/foundry/foundry_basic.py b/python/samples/getting_started/agents/foundry/foundry_basic.py index dd91b736c5..ab377b72a3 100644 --- a/python/samples/getting_started/agents/foundry/foundry_basic.py +++ b/python/samples/getting_started/agents/foundry/foundry_basic.py @@ -5,6 +5,7 @@ from random import randint from typing import Annotated from agent_framework.foundry import FoundryChatClient +from azure.identity.aio import DefaultAzureCredential from pydantic import Field @@ -22,7 +23,7 @@ async def non_streaming_example() -> None: # Since no Agent ID is provided, the agent will be automatically created # and deleted after getting a response - async with FoundryChatClient().create_agent( + async with FoundryChatClient(async_ad_credential=DefaultAzureCredential()).create_agent( name="WeatherAgent", instructions="You are a helpful weather agent.", tools=get_weather, @@ -39,7 +40,7 @@ async def streaming_example() -> None: # Since no Agent ID is provided, the agent will be automatically created # and deleted after getting a response - async with FoundryChatClient().create_agent( + async with FoundryChatClient(async_ad_credential=DefaultAzureCredential()).create_agent( name="WeatherAgent", instructions="You are a helpful weather agent.", tools=get_weather, diff --git a/python/samples/getting_started/agents/foundry/foundry_with_code_interpreter.py b/python/samples/getting_started/agents/foundry/foundry_with_code_interpreter.py index fd8e2e0a96..2a70aa3f46 100644 --- a/python/samples/getting_started/agents/foundry/foundry_with_code_interpreter.py +++ b/python/samples/getting_started/agents/foundry/foundry_with_code_interpreter.py @@ -11,6 +11,7 @@ from azure.ai.agents.models import ( RunStepDeltaCodeInterpreterToolCall, RunStepDeltaToolCallObject, ) +from azure.identity.aio import DefaultAzureCredential def get_code_interpreter_chunk(chunk: AgentRunResponseUpdate) -> str | None: @@ -37,7 +38,7 @@ async def main() -> None: print("=== Foundry Agent with Code Interpreter Example ===") async with ChatClientAgent( - chat_client=FoundryChatClient(), + chat_client=FoundryChatClient(async_ad_credential=DefaultAzureCredential()), instructions="You are a helpful assistant that can write and execute Python code to solve problems.", tools=HostedCodeInterpreterTool(), ) as agent: diff --git a/python/samples/getting_started/agents/foundry/foundry_with_explicit_settings.py b/python/samples/getting_started/agents/foundry/foundry_with_explicit_settings.py index a9d451e701..b78f45ee6b 100644 --- a/python/samples/getting_started/agents/foundry/foundry_with_explicit_settings.py +++ b/python/samples/getting_started/agents/foundry/foundry_with_explicit_settings.py @@ -28,7 +28,7 @@ async def main() -> None: chat_client=FoundryChatClient( project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], model_deployment_name=os.environ["FOUNDRY_MODEL_DEPLOYMENT_NAME"], - credential=AzureCliCredential(), + async_ad_credential=AzureCliCredential(), agent_name="WeatherAgent", ), instructions="You are a helpful weather agent.", diff --git a/python/samples/getting_started/agents/foundry/foundry_with_function_tools.py b/python/samples/getting_started/agents/foundry/foundry_with_function_tools.py index 5d3fc42c2e..d6dfba396b 100644 --- a/python/samples/getting_started/agents/foundry/foundry_with_function_tools.py +++ b/python/samples/getting_started/agents/foundry/foundry_with_function_tools.py @@ -7,6 +7,7 @@ from typing import Annotated from agent_framework import ChatClientAgent from agent_framework.foundry import FoundryChatClient +from azure.identity.aio import DefaultAzureCredential from pydantic import Field @@ -31,7 +32,7 @@ async def tools_on_agent_level() -> None: # Tools are provided when creating the agent # The agent can use these tools for any query during its lifetime async with ChatClientAgent( - chat_client=FoundryChatClient(), + chat_client=FoundryChatClient(async_ad_credential=DefaultAzureCredential()), instructions="You are a helpful assistant that can provide weather and time information.", tools=[get_weather, get_time], # Tools defined at agent creation ) as agent: @@ -60,7 +61,7 @@ async def tools_on_run_level() -> None: # Agent created without tools async with ChatClientAgent( - chat_client=FoundryChatClient(), + chat_client=FoundryChatClient(async_ad_credential=DefaultAzureCredential()), instructions="You are a helpful assistant.", # No tools defined here ) as agent: @@ -89,7 +90,7 @@ async def mixed_tools_example() -> None: # Agent created with some base tools async with ChatClientAgent( - chat_client=FoundryChatClient(), + chat_client=FoundryChatClient(async_ad_credential=DefaultAzureCredential()), instructions="You are a comprehensive assistant that can help with various information requests.", tools=[get_weather], # Base tool available for all queries ) as agent: diff --git a/python/samples/getting_started/agents/foundry/foundry_with_thread.py b/python/samples/getting_started/agents/foundry/foundry_with_thread.py index ae596ad510..6a2a4ebfeb 100644 --- a/python/samples/getting_started/agents/foundry/foundry_with_thread.py +++ b/python/samples/getting_started/agents/foundry/foundry_with_thread.py @@ -6,6 +6,7 @@ from typing import Annotated from agent_framework import ChatClientAgent, ChatClientAgentThread from agent_framework.foundry import FoundryChatClient +from azure.identity.aio import DefaultAzureCredential from pydantic import Field @@ -22,7 +23,7 @@ async def example_with_automatic_thread_creation() -> None: print("=== Automatic Thread Creation Example ===") async with ChatClientAgent( - chat_client=FoundryChatClient(), + chat_client=FoundryChatClient(async_ad_credential=DefaultAzureCredential()), instructions="You are a helpful weather agent.", tools=get_weather, ) as agent: @@ -46,7 +47,7 @@ async def example_with_thread_persistence() -> None: print("Using the same thread across multiple conversations to maintain context.\n") async with ChatClientAgent( - chat_client=FoundryChatClient(), + chat_client=FoundryChatClient(async_ad_credential=DefaultAzureCredential()), instructions="You are a helpful weather agent.", tools=get_weather, ) as agent: @@ -82,7 +83,7 @@ async def example_with_existing_thread_id() -> None: existing_thread_id = None async with ChatClientAgent( - chat_client=FoundryChatClient(), + chat_client=FoundryChatClient(async_ad_credential=DefaultAzureCredential()), instructions="You are a helpful weather agent.", tools=get_weather, ) as agent: @@ -102,7 +103,7 @@ async def example_with_existing_thread_id() -> None: # Create a new agent instance but use the existing thread ID async with ChatClientAgent( - chat_client=FoundryChatClient(thread_id=existing_thread_id), + chat_client=FoundryChatClient(thread_id=existing_thread_id, async_ad_credential=DefaultAzureCredential()), instructions="You are a helpful weather agent.", tools=get_weather, ) as agent: diff --git a/python/samples/getting_started/agents/openai_assistants_client/openai_assistants_basic.py b/python/samples/getting_started/agents/openai_assistants_client/openai_assistants_basic.py index 363a9996e8..efa5678bba 100644 --- a/python/samples/getting_started/agents/openai_assistants_client/openai_assistants_basic.py +++ b/python/samples/getting_started/agents/openai_assistants_client/openai_assistants_basic.py @@ -4,7 +4,6 @@ import asyncio from random import randint from typing import Annotated -from agent_framework import ChatClientAgent from agent_framework.openai import OpenAIAssistantsClient from pydantic import Field @@ -23,8 +22,7 @@ async def non_streaming_example() -> None: # Since no assistant ID is provided, the assistant will be automatically created # and deleted after getting a response - async with ChatClientAgent( - chat_client=OpenAIAssistantsClient(), + async with OpenAIAssistantsClient().create_agent( instructions="You are a helpful weather agent.", tools=get_weather, ) as agent: @@ -40,8 +38,7 @@ async def streaming_example() -> None: # Since no assistant ID is provided, the assistant will be automatically created # and deleted after getting a response - async with ChatClientAgent( - chat_client=OpenAIAssistantsClient(), + async with OpenAIAssistantsClient().create_agent( instructions="You are a helpful weather agent.", tools=get_weather, ) as agent: diff --git a/python/samples/getting_started/agents/openai_responses_client/openai_responses_client_reasoning.py b/python/samples/getting_started/agents/openai_responses_client/openai_responses_client_reasoning.py new file mode 100644 index 0000000000..2b2d7eba26 --- /dev/null +++ b/python/samples/getting_started/agents/openai_responses_client/openai_responses_client_reasoning.py @@ -0,0 +1,46 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio + +from agent_framework import HostedCodeInterpreterTool, TextContent, TextReasoningContent, UsageContent +from agent_framework.openai import OpenAIResponsesClient + + +async def reasoning_example() -> None: + """Example of reasoning response (get results as they are generated).""" + print("=== Reasoning Example ===") + + agent = OpenAIResponsesClient(ai_model_id="o4-mini").create_agent( + name="MathHelper", + instructions="You are a personal math tutor. When asked a math question, " + "write and run code using the python tool to answer the question.", + tools=HostedCodeInterpreterTool(), + reasoning={"effort": "medium"}, + ) + + query = "I need to solve the equation 3x + 11 = 14. Can you help me?" + print(f"User: {query}") + print(f"{agent.name}: ", end="", flush=True) + usage = None + async for chunk in agent.run_streaming(query): + if chunk.contents: + for content in chunk.contents: + if isinstance(content, TextReasoningContent): + print(f"\033[97m{content.text}\033[0m", end="", flush=True) + if isinstance(content, TextContent): + print(content.text, end="", flush=True) + if isinstance(content, UsageContent): + usage = content + print("\n") + if usage: + print(f"Usage: {usage.details}") + + +async def main() -> None: + print("=== Basic OpenAI Responses Reasoning Agent Example ===") + + await reasoning_example() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/python/samples/getting_started/chat_client/azure_assistants_client.py b/python/samples/getting_started/chat_client/azure_assistants_client.py index d011da3a77..59f9e7d4de 100644 --- a/python/samples/getting_started/chat_client/azure_assistants_client.py +++ b/python/samples/getting_started/chat_client/azure_assistants_client.py @@ -5,6 +5,7 @@ from random import randint from typing import Annotated from agent_framework.azure import AzureAssistantsClient +from azure.identity import DefaultAzureCredential from pydantic import Field @@ -17,7 +18,7 @@ def get_weather( async def main() -> None: - async with AzureAssistantsClient() as client: + async with AzureAssistantsClient(ad_credential=DefaultAzureCredential()) as client: message = "What's the weather in Amsterdam and in Paris?" stream = False print(f"User: {message}") diff --git a/python/samples/getting_started/chat_client/azure_chat_client.py b/python/samples/getting_started/chat_client/azure_chat_client.py index dc7ea4069c..3f63677bd7 100644 --- a/python/samples/getting_started/chat_client/azure_chat_client.py +++ b/python/samples/getting_started/chat_client/azure_chat_client.py @@ -5,6 +5,7 @@ from random import randint from typing import Annotated from agent_framework.azure import AzureChatClient +from azure.identity import DefaultAzureCredential from pydantic import Field @@ -17,7 +18,7 @@ def get_weather( async def main() -> None: - client = AzureChatClient() + client = AzureChatClient(ad_credential=DefaultAzureCredential()) message = "What's the weather in Amsterdam and in Paris?" stream = False print(f"User: {message}") 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 24dfce5328..e684fb1e03 100644 --- a/python/samples/getting_started/chat_client/azure_responses_client.py +++ b/python/samples/getting_started/chat_client/azure_responses_client.py @@ -5,6 +5,7 @@ from random import randint from typing import Annotated from agent_framework.azure import AzureResponsesClient +from azure.identity import DefaultAzureCredential from pydantic import Field @@ -17,7 +18,7 @@ def get_weather( async def main() -> None: - client = AzureResponsesClient() + client = AzureResponsesClient(ad_credential=DefaultAzureCredential()) message = "What's the weather in Amsterdam and in Paris?" stream = False print(f"User: {message}") diff --git a/python/samples/getting_started/chat_client/foundry_chat_client.py b/python/samples/getting_started/chat_client/foundry_chat_client.py index 13fc4a7612..a26d91f998 100644 --- a/python/samples/getting_started/chat_client/foundry_chat_client.py +++ b/python/samples/getting_started/chat_client/foundry_chat_client.py @@ -5,6 +5,7 @@ from random import randint from typing import Annotated from agent_framework.foundry import FoundryChatClient +from azure.identity.aio import DefaultAzureCredential from pydantic import Field @@ -17,7 +18,7 @@ def get_weather( async def main() -> None: - async with FoundryChatClient() as client: + async with FoundryChatClient(async_ad_credential=DefaultAzureCredential()) as client: message = "What's the weather in Amsterdam and in Paris?" stream = False print(f"User: {message}") diff --git a/python/samples/getting_started/chat_client/openai_chat_client.py b/python/samples/getting_started/chat_client/openai_chat_client.py index 183a1f61d4..fce79f3135 100644 --- a/python/samples/getting_started/chat_client/openai_chat_client.py +++ b/python/samples/getting_started/chat_client/openai_chat_client.py @@ -19,13 +19,13 @@ def get_weather( async def main() -> None: client = OpenAIChatClient() 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="") + if chunk.text: + print(chunk.text, end="") print("") else: response = await client.get_response(message, tools=get_weather) diff --git a/python/samples/getting_started/chat_client/openai_responses_client.py b/python/samples/getting_started/chat_client/openai_responses_client.py index cd8bc231c4..a7ebd3d976 100644 --- a/python/samples/getting_started/chat_client/openai_responses_client.py +++ b/python/samples/getting_started/chat_client/openai_responses_client.py @@ -17,15 +17,15 @@ def get_weather( async def main() -> None: - client = OpenAIResponsesClient(ai_model_id="gpt-4o-mini") + client = OpenAIResponsesClient() message = "What's the weather in Amsterdam and in Paris?" stream = False 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="") + if chunk.text: + print(chunk.text, end="") print("") else: response = await client.get_response(message, tools=get_weather) diff --git a/python/samples/getting_started/workflow/step_04_simple_group_chat.py b/python/samples/getting_started/workflow/step_04_simple_group_chat.py index dd5ae88e3f..0e0d6eda8d 100644 --- a/python/samples/getting_started/workflow/step_04_simple_group_chat.py +++ b/python/samples/getting_started/workflow/step_04_simple_group_chat.py @@ -2,7 +2,7 @@ import asyncio -from agent_framework import ChatClientAgent, ChatMessage, ChatRole +from agent_framework import ChatMessage, ChatRole from agent_framework.azure import AzureChatClient from agent_framework.workflow import ( AgentExecutor, @@ -15,6 +15,7 @@ from agent_framework.workflow import ( WorkflowContext, handler, ) +from azure.identity import DefaultAzureCredential """ The following sample demonstrates a basic workflow that simulates @@ -90,10 +91,9 @@ async def main(): """Main function to run the group chat workflow.""" # Step 1: Create the executors. - chat_client = AzureChatClient() + chat_client = AzureChatClient(ad_credential=DefaultAzureCredential()) writer = AgentExecutor( - ChatClientAgent( - chat_client, + chat_client.create_agent( instructions=( "You are an excellent content writer. You create new content and edit contents based on the feedback." ), @@ -101,8 +101,7 @@ async def main(): id="writer", ) reviewer = AgentExecutor( - ChatClientAgent( - chat_client, + chat_client.create_agent( instructions=( "You are an excellent content reviewer. You review the content and provide feedback to the writer." ), diff --git a/python/samples/getting_started/workflow/step_05_simple_group_chat_with_hil.py b/python/samples/getting_started/workflow/step_05_simple_group_chat_with_hil.py index 4f0040a249..1c30404426 100644 --- a/python/samples/getting_started/workflow/step_05_simple_group_chat_with_hil.py +++ b/python/samples/getting_started/workflow/step_05_simple_group_chat_with_hil.py @@ -2,7 +2,7 @@ import asyncio -from agent_framework import ChatClientAgent, ChatMessage, ChatRole +from agent_framework import ChatMessage, ChatRole from agent_framework.azure import AzureChatClient from agent_framework.workflow import ( AgentExecutor, @@ -17,6 +17,7 @@ from agent_framework.workflow import ( WorkflowContext, handler, ) +from azure.identity import DefaultAzureCredential """ The following sample demonstrates a basic workflow that simulates @@ -135,9 +136,9 @@ class CriticGroupChatManager(Executor): async def main(): """Main function to run the group chat workflow.""" # Step 1: Create the executors. + chat_client = AzureChatClient(ad_credential=DefaultAzureCredential()) writer = AgentExecutor( - ChatClientAgent( - AzureChatClient(), + chat_client.create_agent( instructions=( "You are an excellent content writer. You create new content and edit contents based on the feedback." ), @@ -146,8 +147,7 @@ async def main(): ), ) reviewer = AgentExecutor( - ChatClientAgent( - AzureChatClient(), + chat_client.create_agent( instructions=( "You are an excellent content reviewer. You review the content and provide feedback to the writer. " "You do not address user requests. Only provide feedback to the writer." diff --git a/python/samples/getting_started/workflow/step_06_map_reduce.py b/python/samples/getting_started/workflow/step_06_map_reduce.py index 2e8c5ca995..e7665f67ed 100644 --- a/python/samples/getting_started/workflow/step_06_map_reduce.py +++ b/python/samples/getting_started/workflow/step_06_map_reduce.py @@ -7,7 +7,13 @@ from collections import defaultdict from dataclasses import dataclass import aiofiles -from agent_framework.workflow import Executor, WorkflowBuilder, WorkflowCompletedEvent, WorkflowContext, handler +from agent_framework.workflow import ( + Executor, + WorkflowBuilder, + WorkflowCompletedEvent, + WorkflowContext, + handler, +) """ The following sample demonstrates a basic map reduce workflow that diff --git a/python/uv.lock b/python/uv.lock index c8e0b3ff32..5a8c96e4df 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -588,63 +588,66 @@ wheels = [ [[package]] name = "charset-normalizer" -version = "3.4.2" +version = "3.4.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } +sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818, upload-time = "2025-05-02T08:31:46.725Z" }, - { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649, upload-time = "2025-05-02T08:31:48.889Z" }, - { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045, upload-time = "2025-05-02T08:31:50.757Z" }, - { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356, upload-time = "2025-05-02T08:31:52.634Z" }, - { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471, upload-time = "2025-05-02T08:31:56.207Z" }, - { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317, upload-time = "2025-05-02T08:31:57.613Z" }, - { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368, upload-time = "2025-05-02T08:31:59.468Z" }, - { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491, upload-time = "2025-05-02T08:32:01.219Z" }, - { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695, upload-time = "2025-05-02T08:32:03.045Z" }, - { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849, upload-time = "2025-05-02T08:32:04.651Z" }, - { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091, upload-time = "2025-05-02T08:32:06.719Z" }, - { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445, upload-time = "2025-05-02T08:32:08.66Z" }, - { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782, upload-time = "2025-05-02T08:32:10.46Z" }, - { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794, upload-time = "2025-05-02T08:32:11.945Z" }, - { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846, upload-time = "2025-05-02T08:32:13.946Z" }, - { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350, upload-time = "2025-05-02T08:32:15.873Z" }, - { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657, upload-time = "2025-05-02T08:32:17.283Z" }, - { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260, upload-time = "2025-05-02T08:32:18.807Z" }, - { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164, upload-time = "2025-05-02T08:32:20.333Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571, upload-time = "2025-05-02T08:32:21.86Z" }, - { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952, upload-time = "2025-05-02T08:32:23.434Z" }, - { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959, upload-time = "2025-05-02T08:32:24.993Z" }, - { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030, upload-time = "2025-05-02T08:32:26.435Z" }, - { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015, upload-time = "2025-05-02T08:32:28.376Z" }, - { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106, upload-time = "2025-05-02T08:32:30.281Z" }, - { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402, upload-time = "2025-05-02T08:32:32.191Z" }, - { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" }, - { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" }, - { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" }, - { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" }, - { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" }, - { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" }, - { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" }, - { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" }, - { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" }, - { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" }, - { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, - { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, - { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, - { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, - { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, - { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, - { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, - { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, - { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, - { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, - { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, - { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, - { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, - { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, - { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, - { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, - { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/d6/98/f3b8013223728a99b908c9344da3aa04ee6e3fa235f19409033eda92fb78/charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72", size = 207695, upload-time = "2025-08-09T07:55:36.452Z" }, + { url = "https://files.pythonhosted.org/packages/21/40/5188be1e3118c82dcb7c2a5ba101b783822cfb413a0268ed3be0468532de/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe", size = 147153, upload-time = "2025-08-09T07:55:38.467Z" }, + { url = "https://files.pythonhosted.org/packages/37/60/5d0d74bc1e1380f0b72c327948d9c2aca14b46a9efd87604e724260f384c/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601", size = 160428, upload-time = "2025-08-09T07:55:40.072Z" }, + { url = "https://files.pythonhosted.org/packages/85/9a/d891f63722d9158688de58d050c59dc3da560ea7f04f4c53e769de5140f5/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c", size = 157627, upload-time = "2025-08-09T07:55:41.706Z" }, + { url = "https://files.pythonhosted.org/packages/65/1a/7425c952944a6521a9cfa7e675343f83fd82085b8af2b1373a2409c683dc/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2", size = 152388, upload-time = "2025-08-09T07:55:43.262Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c9/a2c9c2a355a8594ce2446085e2ec97fd44d323c684ff32042e2a6b718e1d/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0", size = 150077, upload-time = "2025-08-09T07:55:44.903Z" }, + { url = "https://files.pythonhosted.org/packages/3b/38/20a1f44e4851aa1c9105d6e7110c9d020e093dfa5836d712a5f074a12bf7/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0", size = 161631, upload-time = "2025-08-09T07:55:46.346Z" }, + { url = "https://files.pythonhosted.org/packages/a4/fa/384d2c0f57edad03d7bec3ebefb462090d8905b4ff5a2d2525f3bb711fac/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0", size = 159210, upload-time = "2025-08-09T07:55:47.539Z" }, + { url = "https://files.pythonhosted.org/packages/33/9e/eca49d35867ca2db336b6ca27617deed4653b97ebf45dfc21311ce473c37/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a", size = 153739, upload-time = "2025-08-09T07:55:48.744Z" }, + { url = "https://files.pythonhosted.org/packages/2a/91/26c3036e62dfe8de8061182d33be5025e2424002125c9500faff74a6735e/charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f", size = 99825, upload-time = "2025-08-09T07:55:50.305Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c6/f05db471f81af1fa01839d44ae2a8bfeec8d2a8b4590f16c4e7393afd323/charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669", size = 107452, upload-time = "2025-08-09T07:55:51.461Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", size = 204483, upload-time = "2025-08-09T07:55:53.12Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", size = 145520, upload-time = "2025-08-09T07:55:54.712Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", size = 158876, upload-time = "2025-08-09T07:55:56.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e6/63bb0e10f90a8243c5def74b5b105b3bbbfb3e7bb753915fe333fb0c11ea/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", size = 156083, upload-time = "2025-08-09T07:55:57.582Z" }, + { url = "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", size = 150295, upload-time = "2025-08-09T07:55:59.147Z" }, + { url = "https://files.pythonhosted.org/packages/61/f1/190d9977e0084d3f1dc169acd060d479bbbc71b90bf3e7bf7b9927dec3eb/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", size = 148379, upload-time = "2025-08-09T07:56:00.364Z" }, + { url = "https://files.pythonhosted.org/packages/4c/92/27dbe365d34c68cfe0ca76f1edd70e8705d82b378cb54ebbaeabc2e3029d/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", size = 160018, upload-time = "2025-08-09T07:56:01.678Z" }, + { url = "https://files.pythonhosted.org/packages/99/04/baae2a1ea1893a01635d475b9261c889a18fd48393634b6270827869fa34/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", size = 157430, upload-time = "2025-08-09T07:56:02.87Z" }, + { url = "https://files.pythonhosted.org/packages/2f/36/77da9c6a328c54d17b960c89eccacfab8271fdaaa228305330915b88afa9/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", size = 151600, upload-time = "2025-08-09T07:56:04.089Z" }, + { url = "https://files.pythonhosted.org/packages/64/d4/9eb4ff2c167edbbf08cdd28e19078bf195762e9bd63371689cab5ecd3d0d/charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", size = 99616, upload-time = "2025-08-09T07:56:05.658Z" }, + { url = "https://files.pythonhosted.org/packages/f4/9c/996a4a028222e7761a96634d1820de8a744ff4327a00ada9c8942033089b/charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", size = 107108, upload-time = "2025-08-09T07:56:07.176Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" }, + { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" }, + { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" }, + { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" }, + { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" }, + { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" }, + { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" }, + { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" }, + { url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload-time = "2025-08-09T07:56:21.551Z" }, + { url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload-time = "2025-08-09T07:56:23.115Z" }, + { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" }, + { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" }, + { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" }, + { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" }, + { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" }, + { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" }, + { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" }, + { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" }, + { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" }, + { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" }, + { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" }, + { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, ] [[package]] @@ -679,87 +682,87 @@ wheels = [ [[package]] name = "coverage" -version = "7.10.2" +version = "7.10.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/76/17780846fc7aade1e66712e1e27dd28faa0a5d987a1f433610974959eaa8/coverage-7.10.2.tar.gz", hash = "sha256:5d6e6d84e6dd31a8ded64759626627247d676a23c1b892e1326f7c55c8d61055", size = 820754, upload-time = "2025-08-04T00:35:17.511Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/2c/253cc41cd0f40b84c1c34c5363e0407d73d4a1cae005fed6db3b823175bd/coverage-7.10.3.tar.gz", hash = "sha256:812ba9250532e4a823b070b0420a36499859542335af3dca8f47fc6aa1a05619", size = 822936, upload-time = "2025-08-10T21:27:39.968Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/5f/5ce748ab3f142593698aff5f8a0cf020775aa4e24b9d8748b5a56b64d3f8/coverage-7.10.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:79f0283ab5e6499fd5fe382ca3d62afa40fb50ff227676a3125d18af70eabf65", size = 215003, upload-time = "2025-08-04T00:33:02.977Z" }, - { url = "https://files.pythonhosted.org/packages/f4/ed/507088561217b000109552139802fa99c33c16ad19999c687b601b3790d0/coverage-7.10.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4545e906f595ee8ab8e03e21be20d899bfc06647925bc5b224ad7e8c40e08b8", size = 215391, upload-time = "2025-08-04T00:33:05.645Z" }, - { url = "https://files.pythonhosted.org/packages/79/1b/0f496259fe137c4c5e1e8eaff496fb95af88b71700f5e57725a4ddbe742b/coverage-7.10.2-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ae385e1d58fbc6a9b1c315e5510ac52281e271478b45f92ca9b5ad42cf39643f", size = 242367, upload-time = "2025-08-04T00:33:07.189Z" }, - { url = "https://files.pythonhosted.org/packages/b9/8e/5a8835fb0122a2e2a108bf3527931693c4625fdc4d953950a480b9625852/coverage-7.10.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6f0cbe5f7dd19f3a32bac2251b95d51c3b89621ac88a2648096ce40f9a5aa1e7", size = 243627, upload-time = "2025-08-04T00:33:08.809Z" }, - { url = "https://files.pythonhosted.org/packages/c3/96/6a528429c2e0e8d85261764d0cd42e51a429510509bcc14676ee5d1bb212/coverage-7.10.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd17f427f041f6b116dc90b4049c6f3e1230524407d00daa2d8c7915037b5947", size = 245485, upload-time = "2025-08-04T00:33:10.29Z" }, - { url = "https://files.pythonhosted.org/packages/bf/82/1fba935c4d02c33275aca319deabf1f22c0f95f2c0000bf7c5f276d6f7b4/coverage-7.10.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7f10ca4cde7b466405cce0a0e9971a13eb22e57a5ecc8b5f93a81090cc9c7eb9", size = 243429, upload-time = "2025-08-04T00:33:11.909Z" }, - { url = "https://files.pythonhosted.org/packages/fc/a8/c8dc0a57a729fc93be33ab78f187a8f52d455fa8f79bfb379fe23b45868d/coverage-7.10.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3b990df23dd51dccce26d18fb09fd85a77ebe46368f387b0ffba7a74e470b31b", size = 242104, upload-time = "2025-08-04T00:33:13.467Z" }, - { url = "https://files.pythonhosted.org/packages/b9/6f/0b7da1682e2557caeed299a00897b42afde99a241a01eba0197eb982b90f/coverage-7.10.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc3902584d25c7eef57fb38f440aa849a26a3a9f761a029a72b69acfca4e31f8", size = 242397, upload-time = "2025-08-04T00:33:14.682Z" }, - { url = "https://files.pythonhosted.org/packages/2d/e4/54dc833dadccd519c04a28852f39a37e522bad35d70cfe038817cdb8f168/coverage-7.10.2-cp310-cp310-win32.whl", hash = "sha256:9dd37e9ac00d5eb72f38ed93e3cdf2280b1dbda3bb9b48c6941805f265ad8d87", size = 217502, upload-time = "2025-08-04T00:33:16.254Z" }, - { url = "https://files.pythonhosted.org/packages/c3/e7/2f78159c4c127549172f427dff15b02176329327bf6a6a1fcf1f603b5456/coverage-7.10.2-cp310-cp310-win_amd64.whl", hash = "sha256:99d16f15cb5baf0729354c5bd3080ae53847a4072b9ba1e10957522fb290417f", size = 218388, upload-time = "2025-08-04T00:33:17.4Z" }, - { url = "https://files.pythonhosted.org/packages/6e/53/0125a6fc0af4f2687b4e08b0fb332cd0d5e60f3ca849e7456f995d022656/coverage-7.10.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c3b210d79925a476dfc8d74c7d53224888421edebf3a611f3adae923e212b27", size = 215119, upload-time = "2025-08-04T00:33:19.101Z" }, - { url = "https://files.pythonhosted.org/packages/0e/2e/960d9871de9152dbc9ff950913c6a6e9cf2eb4cc80d5bc8f93029f9f2f9f/coverage-7.10.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf67d1787cd317c3f8b2e4c6ed1ae93497be7e30605a0d32237ac37a37a8a322", size = 215511, upload-time = "2025-08-04T00:33:20.32Z" }, - { url = "https://files.pythonhosted.org/packages/3f/34/68509e44995b9cad806d81b76c22bc5181f3535bca7cd9c15791bfd8951e/coverage-7.10.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:069b779d03d458602bc0e27189876e7d8bdf6b24ac0f12900de22dd2154e6ad7", size = 245513, upload-time = "2025-08-04T00:33:21.896Z" }, - { url = "https://files.pythonhosted.org/packages/ef/d4/9b12f357413248ce40804b0f58030b55a25b28a5c02db95fb0aa50c5d62c/coverage-7.10.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4c2de4cb80b9990e71c62c2d3e9f3ec71b804b1f9ca4784ec7e74127e0f42468", size = 247350, upload-time = "2025-08-04T00:33:23.917Z" }, - { url = "https://files.pythonhosted.org/packages/b6/40/257945eda1f72098e4a3c350b1d68fdc5d7d032684a0aeb6c2391153ecf4/coverage-7.10.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75bf7ab2374a7eb107602f1e07310cda164016cd60968abf817b7a0b5703e288", size = 249516, upload-time = "2025-08-04T00:33:25.5Z" }, - { url = "https://files.pythonhosted.org/packages/ff/55/8987f852ece378cecbf39a367f3f7ec53351e39a9151b130af3a3045b83f/coverage-7.10.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3f37516458ec1550815134937f73d6d15b434059cd10f64678a2068f65c62406", size = 247241, upload-time = "2025-08-04T00:33:26.767Z" }, - { url = "https://files.pythonhosted.org/packages/df/ae/da397de7a42a18cea6062ed9c3b72c50b39e0b9e7b2893d7172d3333a9a1/coverage-7.10.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:de3c6271c482c250d3303fb5c6bdb8ca025fff20a67245e1425df04dc990ece9", size = 245274, upload-time = "2025-08-04T00:33:28.494Z" }, - { url = "https://files.pythonhosted.org/packages/4e/64/7baa895eb55ec0e1ec35b988687ecd5d4475ababb0d7ae5ca3874dd90ee7/coverage-7.10.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:98a838101321ac3089c9bb1d4bfa967e8afed58021fda72d7880dc1997f20ae1", size = 245882, upload-time = "2025-08-04T00:33:30.048Z" }, - { url = "https://files.pythonhosted.org/packages/24/6c/1fd76a0bd09ae75220ae9775a8290416d726f0e5ba26ea72346747161240/coverage-7.10.2-cp311-cp311-win32.whl", hash = "sha256:f2a79145a531a0e42df32d37be5af069b4a914845b6f686590739b786f2f7bce", size = 217541, upload-time = "2025-08-04T00:33:31.376Z" }, - { url = "https://files.pythonhosted.org/packages/5f/2d/8c18fb7a6e74c79fd4661e82535bc8c68aee12f46c204eabf910b097ccc9/coverage-7.10.2-cp311-cp311-win_amd64.whl", hash = "sha256:e4f5f1320f8ee0d7cfa421ceb257bef9d39fd614dd3ddcfcacd284d4824ed2c2", size = 218426, upload-time = "2025-08-04T00:33:32.976Z" }, - { url = "https://files.pythonhosted.org/packages/da/40/425bb35e4ff7c7af177edf5dffd4154bc2a677b27696afe6526d75c77fec/coverage-7.10.2-cp311-cp311-win_arm64.whl", hash = "sha256:d8f2d83118f25328552c728b8e91babf93217db259ca5c2cd4dd4220b8926293", size = 217116, upload-time = "2025-08-04T00:33:34.302Z" }, - { url = "https://files.pythonhosted.org/packages/4e/1e/2c752bdbbf6f1199c59b1a10557fbb6fb3dc96b3c0077b30bd41a5922c1f/coverage-7.10.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:890ad3a26da9ec7bf69255b9371800e2a8da9bc223ae5d86daeb940b42247c83", size = 215311, upload-time = "2025-08-04T00:33:35.524Z" }, - { url = "https://files.pythonhosted.org/packages/68/6a/84277d73a2cafb96e24be81b7169372ba7ff28768ebbf98e55c85a491b0f/coverage-7.10.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38fd1ccfca7838c031d7a7874d4353e2f1b98eb5d2a80a2fe5732d542ae25e9c", size = 215550, upload-time = "2025-08-04T00:33:37.109Z" }, - { url = "https://files.pythonhosted.org/packages/b5/e7/5358b73b46ac76f56cc2de921eeabd44fabd0b7ff82ea4f6b8c159c4d5dc/coverage-7.10.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:76c1ffaaf4f6f0f6e8e9ca06f24bb6454a7a5d4ced97a1bc466f0d6baf4bd518", size = 246564, upload-time = "2025-08-04T00:33:38.33Z" }, - { url = "https://files.pythonhosted.org/packages/7c/0e/b0c901dd411cb7fc0cfcb28ef0dc6f3049030f616bfe9fc4143aecd95901/coverage-7.10.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:86da8a3a84b79ead5c7d0e960c34f580bc3b231bb546627773a3f53c532c2f21", size = 248993, upload-time = "2025-08-04T00:33:39.555Z" }, - { url = "https://files.pythonhosted.org/packages/0e/4e/a876db272072a9e0df93f311e187ccdd5f39a190c6d1c1f0b6e255a0d08e/coverage-7.10.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99cef9731c8a39801830a604cc53c93c9e57ea8b44953d26589499eded9576e0", size = 250454, upload-time = "2025-08-04T00:33:41.023Z" }, - { url = "https://files.pythonhosted.org/packages/64/d6/1222dc69f8dd1be208d55708a9f4a450ad582bf4fa05320617fea1eaa6d8/coverage-7.10.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ea58b112f2966a8b91eb13f5d3b1f8bb43c180d624cd3283fb33b1cedcc2dd75", size = 248365, upload-time = "2025-08-04T00:33:42.376Z" }, - { url = "https://files.pythonhosted.org/packages/62/e3/40fd71151064fc315c922dd9a35e15b30616f00146db1d6a0b590553a75a/coverage-7.10.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:20f405188d28da9522b7232e51154e1b884fc18d0b3a10f382d54784715bbe01", size = 246562, upload-time = "2025-08-04T00:33:43.663Z" }, - { url = "https://files.pythonhosted.org/packages/fc/14/8aa93ddcd6623ddaef5d8966268ac9545b145bce4fe7b1738fd1c3f0d957/coverage-7.10.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:64586ce42bbe0da4d9f76f97235c545d1abb9b25985a8791857690f96e23dc3b", size = 247772, upload-time = "2025-08-04T00:33:45.068Z" }, - { url = "https://files.pythonhosted.org/packages/07/4e/dcb1c01490623c61e2f2ea85cb185fa6a524265bb70eeb897d3c193efeb9/coverage-7.10.2-cp312-cp312-win32.whl", hash = "sha256:bc2e69b795d97ee6d126e7e22e78a509438b46be6ff44f4dccbb5230f550d340", size = 217710, upload-time = "2025-08-04T00:33:46.378Z" }, - { url = "https://files.pythonhosted.org/packages/79/16/e8aab4162b5f80ad2e5e1f54b1826e2053aa2f4db508b864af647f00c239/coverage-7.10.2-cp312-cp312-win_amd64.whl", hash = "sha256:adda2268b8cf0d11f160fad3743b4dfe9813cd6ecf02c1d6397eceaa5b45b388", size = 218499, upload-time = "2025-08-04T00:33:48.048Z" }, - { url = "https://files.pythonhosted.org/packages/06/7f/c112ec766e8f1131ce8ce26254be028772757b2d1e63e4f6a4b0ad9a526c/coverage-7.10.2-cp312-cp312-win_arm64.whl", hash = "sha256:164429decd0d6b39a0582eaa30c67bf482612c0330572343042d0ed9e7f15c20", size = 217154, upload-time = "2025-08-04T00:33:49.299Z" }, - { url = "https://files.pythonhosted.org/packages/8d/04/9b7a741557f93c0ed791b854d27aa8d9fe0b0ce7bb7c52ca1b0f2619cb74/coverage-7.10.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:aca7b5645afa688de6d4f8e89d30c577f62956fefb1bad021490d63173874186", size = 215337, upload-time = "2025-08-04T00:33:50.61Z" }, - { url = "https://files.pythonhosted.org/packages/02/a4/8d1088cd644750c94bc305d3cf56082b4cdf7fb854a25abb23359e74892f/coverage-7.10.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:96e5921342574a14303dfdb73de0019e1ac041c863743c8fe1aa6c2b4a257226", size = 215596, upload-time = "2025-08-04T00:33:52.33Z" }, - { url = "https://files.pythonhosted.org/packages/01/2f/643a8d73343f70e162d8177a3972b76e306b96239026bc0c12cfde4f7c7a/coverage-7.10.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:11333094c1bff621aa811b67ed794865cbcaa99984dedea4bd9cf780ad64ecba", size = 246145, upload-time = "2025-08-04T00:33:53.641Z" }, - { url = "https://files.pythonhosted.org/packages/1f/4a/722098d1848db4072cda71b69ede1e55730d9063bf868375264d0d302bc9/coverage-7.10.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6eb586fa7d2aee8d65d5ae1dd71414020b2f447435c57ee8de8abea0a77d5074", size = 248492, upload-time = "2025-08-04T00:33:55.366Z" }, - { url = "https://files.pythonhosted.org/packages/3f/b0/8a6d7f326f6e3e6ed398cde27f9055e860a1e858317001835c521673fb60/coverage-7.10.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d358f259d8019d4ef25d8c5b78aca4c7af25e28bd4231312911c22a0e824a57", size = 249927, upload-time = "2025-08-04T00:33:57.042Z" }, - { url = "https://files.pythonhosted.org/packages/bb/21/1aaadd3197b54d1e61794475379ecd0f68d8fc5c2ebd352964dc6f698a3d/coverage-7.10.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5250bda76e30382e0a2dcd68d961afcab92c3a7613606e6269855c6979a1b0bb", size = 248138, upload-time = "2025-08-04T00:33:58.329Z" }, - { url = "https://files.pythonhosted.org/packages/48/65/be75bafb2bdd22fd8bf9bf63cd5873b91bb26ec0d68f02d4b8b09c02decb/coverage-7.10.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a91e027d66eff214d88d9afbe528e21c9ef1ecdf4956c46e366c50f3094696d0", size = 246111, upload-time = "2025-08-04T00:33:59.899Z" }, - { url = "https://files.pythonhosted.org/packages/5e/30/a4f0c5e249c3cc60e6c6f30d8368e372f2d380eda40e0434c192ac27ccf5/coverage-7.10.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:228946da741558904e2c03ce870ba5efd9cd6e48cbc004d9a27abee08100a15a", size = 247493, upload-time = "2025-08-04T00:34:01.619Z" }, - { url = "https://files.pythonhosted.org/packages/85/99/f09b9493e44a75cf99ca834394c12f8cb70da6c1711ee296534f97b52729/coverage-7.10.2-cp313-cp313-win32.whl", hash = "sha256:95e23987b52d02e7c413bf2d6dc6288bd5721beb518052109a13bfdc62c8033b", size = 217756, upload-time = "2025-08-04T00:34:03.277Z" }, - { url = "https://files.pythonhosted.org/packages/2d/bb/cbcb09103be330c7d26ff0ab05c4a8861dd2e254656fdbd3eb7600af4336/coverage-7.10.2-cp313-cp313-win_amd64.whl", hash = "sha256:f35481d42c6d146d48ec92d4e239c23f97b53a3f1fbd2302e7c64336f28641fe", size = 218526, upload-time = "2025-08-04T00:34:04.635Z" }, - { url = "https://files.pythonhosted.org/packages/37/8f/8bfb4e0bca52c00ab680767c0dd8cfd928a2a72d69897d9b2d5d8b5f63f5/coverage-7.10.2-cp313-cp313-win_arm64.whl", hash = "sha256:65b451949cb789c346f9f9002441fc934d8ccedcc9ec09daabc2139ad13853f7", size = 217176, upload-time = "2025-08-04T00:34:05.973Z" }, - { url = "https://files.pythonhosted.org/packages/1e/25/d458ba0bf16a8204a88d74dbb7ec5520f29937ffcbbc12371f931c11efd2/coverage-7.10.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e8415918856a3e7d57a4e0ad94651b761317de459eb74d34cc1bb51aad80f07e", size = 216058, upload-time = "2025-08-04T00:34:07.368Z" }, - { url = "https://files.pythonhosted.org/packages/0b/1c/af4dfd2d7244dc7610fed6d59d57a23ea165681cd764445dc58d71ed01a6/coverage-7.10.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f287a25a8ca53901c613498e4a40885b19361a2fe8fbfdbb7f8ef2cad2a23f03", size = 216273, upload-time = "2025-08-04T00:34:09.073Z" }, - { url = "https://files.pythonhosted.org/packages/8e/67/ec5095d4035c6e16368226fa9cb15f77f891194c7e3725aeefd08e7a3e5a/coverage-7.10.2-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:75cc1a3f8c88c69bf16a871dab1fe5a7303fdb1e9f285f204b60f1ee539b8fc0", size = 257513, upload-time = "2025-08-04T00:34:10.403Z" }, - { url = "https://files.pythonhosted.org/packages/1c/47/be5550b57a3a8ba797de4236b0fd31031f88397b2afc84ab3c2d4cf265f6/coverage-7.10.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca07fa78cc9d26bc8c4740de1abd3489cf9c47cc06d9a8ab3d552ff5101af4c0", size = 259377, upload-time = "2025-08-04T00:34:12.138Z" }, - { url = "https://files.pythonhosted.org/packages/37/50/b12a4da1382e672305c2d17cd3029dc16b8a0470de2191dbf26b91431378/coverage-7.10.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2e117e64c26300032755d4520cd769f2623cde1a1d1c3515b05a3b8add0ade1", size = 261516, upload-time = "2025-08-04T00:34:13.608Z" }, - { url = "https://files.pythonhosted.org/packages/db/41/4d3296dbd33dd8da178171540ca3391af7c0184c0870fd4d4574ac290290/coverage-7.10.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:daaf98009977f577b71f8800208f4d40d4dcf5c2db53d4d822787cdc198d76e1", size = 259110, upload-time = "2025-08-04T00:34:15.089Z" }, - { url = "https://files.pythonhosted.org/packages/ea/f1/b409959ecbc0cec0e61e65683b22bacaa4a3b11512f834e16dd8ffbc37db/coverage-7.10.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ea8d8fe546c528535c761ba424410bbeb36ba8a0f24be653e94b70c93fd8a8ca", size = 257248, upload-time = "2025-08-04T00:34:16.501Z" }, - { url = "https://files.pythonhosted.org/packages/48/ab/7076dc1c240412e9267d36ec93e9e299d7659f6a5c1e958f87e998b0fb6d/coverage-7.10.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:fe024d40ac31eb8d5aae70215b41dafa264676caa4404ae155f77d2fa95c37bb", size = 258063, upload-time = "2025-08-04T00:34:18.338Z" }, - { url = "https://files.pythonhosted.org/packages/1e/77/f6b51a0288f8f5f7dcc7c89abdd22cf514f3bc5151284f5cd628917f8e10/coverage-7.10.2-cp313-cp313t-win32.whl", hash = "sha256:8f34b09f68bdadec122ffad312154eda965ade433559cc1eadd96cca3de5c824", size = 218433, upload-time = "2025-08-04T00:34:19.71Z" }, - { url = "https://files.pythonhosted.org/packages/7b/6d/547a86493e25270ce8481543e77f3a0aa3aa872c1374246b7b76273d66eb/coverage-7.10.2-cp313-cp313t-win_amd64.whl", hash = "sha256:71d40b3ac0f26fa9ffa6ee16219a714fed5c6ec197cdcd2018904ab5e75bcfa3", size = 219523, upload-time = "2025-08-04T00:34:21.171Z" }, - { url = "https://files.pythonhosted.org/packages/ff/d5/3c711e38eaf9ab587edc9bed232c0298aed84e751a9f54aaa556ceaf7da6/coverage-7.10.2-cp313-cp313t-win_arm64.whl", hash = "sha256:abb57fdd38bf6f7dcc66b38dafb7af7c5fdc31ac6029ce373a6f7f5331d6f60f", size = 217739, upload-time = "2025-08-04T00:34:22.514Z" }, - { url = "https://files.pythonhosted.org/packages/71/53/83bafa669bb9d06d4c8c6a055d8d05677216f9480c4698fb183ba7ec5e47/coverage-7.10.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a3e853cc04987c85ec410905667eed4bf08b1d84d80dfab2684bb250ac8da4f6", size = 215328, upload-time = "2025-08-04T00:34:23.991Z" }, - { url = "https://files.pythonhosted.org/packages/1d/6c/30827a9c5a48a813e865fbaf91e2db25cce990bd223a022650ef2293fe11/coverage-7.10.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0100b19f230df72c90fdb36db59d3f39232391e8d89616a7de30f677da4f532b", size = 215608, upload-time = "2025-08-04T00:34:25.437Z" }, - { url = "https://files.pythonhosted.org/packages/bb/a0/c92d85948056ddc397b72a3d79d36d9579c53cb25393ed3c40db7d33b193/coverage-7.10.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9c1cd71483ea78331bdfadb8dcec4f4edfb73c7002c1206d8e0af6797853f5be", size = 246111, upload-time = "2025-08-04T00:34:26.857Z" }, - { url = "https://files.pythonhosted.org/packages/c2/cf/d695cf86b2559aadd072c91720a7844be4fb82cb4a3b642a2c6ce075692d/coverage-7.10.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9f75dbf4899e29a37d74f48342f29279391668ef625fdac6d2f67363518056a1", size = 248419, upload-time = "2025-08-04T00:34:28.726Z" }, - { url = "https://files.pythonhosted.org/packages/ce/0a/03206aec4a05986e039418c038470d874045f6e00426b0c3879adc1f9251/coverage-7.10.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7df481e7508de1c38b9b8043da48d94931aefa3e32b47dd20277e4978ed5b95", size = 250038, upload-time = "2025-08-04T00:34:30.061Z" }, - { url = "https://files.pythonhosted.org/packages/ab/9b/b3bd6bd52118c12bc4cf319f5baba65009c9beea84e665b6b9f03fa3f180/coverage-7.10.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:835f39e618099325e7612b3406f57af30ab0a0af350490eff6421e2e5f608e46", size = 248066, upload-time = "2025-08-04T00:34:31.53Z" }, - { url = "https://files.pythonhosted.org/packages/80/cc/bfa92e261d3e055c851a073e87ba6a3bff12a1f7134233e48a8f7d855875/coverage-7.10.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:12e52b5aa00aa720097d6947d2eb9e404e7c1101ad775f9661ba165ed0a28303", size = 245909, upload-time = "2025-08-04T00:34:32.943Z" }, - { url = "https://files.pythonhosted.org/packages/12/80/c8df15db4847710c72084164f615ae900af1ec380dce7f74a5678ccdf5e1/coverage-7.10.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:718044729bf1fe3e9eb9f31b52e44ddae07e434ec050c8c628bf5adc56fe4bdd", size = 247329, upload-time = "2025-08-04T00:34:34.388Z" }, - { url = "https://files.pythonhosted.org/packages/04/6f/cb66e1f7124d5dd9ced69f889f02931419cb448125e44a89a13f4e036124/coverage-7.10.2-cp314-cp314-win32.whl", hash = "sha256:f256173b48cc68486299d510a3e729a96e62c889703807482dbf56946befb5c8", size = 218007, upload-time = "2025-08-04T00:34:35.846Z" }, - { url = "https://files.pythonhosted.org/packages/8c/e1/3d4be307278ce32c1b9d95cc02ee60d54ddab784036101d053ec9e4fe7f5/coverage-7.10.2-cp314-cp314-win_amd64.whl", hash = "sha256:2e980e4179f33d9b65ac4acb86c9c0dde904098853f27f289766657ed16e07b3", size = 218802, upload-time = "2025-08-04T00:34:37.35Z" }, - { url = "https://files.pythonhosted.org/packages/ec/66/1e43bbeb66c55a5a5efec70f1c153cf90cfc7f1662ab4ebe2d844de9122c/coverage-7.10.2-cp314-cp314-win_arm64.whl", hash = "sha256:14fb5b6641ab5b3c4161572579f0f2ea8834f9d3af2f7dd8fbaecd58ef9175cc", size = 217397, upload-time = "2025-08-04T00:34:39.15Z" }, - { url = "https://files.pythonhosted.org/packages/81/01/ae29c129217f6110dc694a217475b8aecbb1b075d8073401f868c825fa99/coverage-7.10.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:e96649ac34a3d0e6491e82a2af71098e43be2874b619547c3282fc11d3840a4b", size = 216068, upload-time = "2025-08-04T00:34:40.648Z" }, - { url = "https://files.pythonhosted.org/packages/a2/50/6e9221d4139f357258f36dfa1d8cac4ec56d9d5acf5fdcc909bb016954d7/coverage-7.10.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1a2e934e9da26341d342d30bfe91422bbfdb3f1f069ec87f19b2909d10d8dcc4", size = 216285, upload-time = "2025-08-04T00:34:42.441Z" }, - { url = "https://files.pythonhosted.org/packages/eb/ec/89d1d0c0ece0d296b4588e0ef4df185200456d42a47f1141335f482c2fc5/coverage-7.10.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:651015dcd5fd9b5a51ca79ece60d353cacc5beaf304db750407b29c89f72fe2b", size = 257603, upload-time = "2025-08-04T00:34:43.899Z" }, - { url = "https://files.pythonhosted.org/packages/82/06/c830af66734671c778fc49d35b58339e8f0687fbd2ae285c3f96c94da092/coverage-7.10.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:81bf6a32212f9f66da03d63ecb9cd9bd48e662050a937db7199dbf47d19831de", size = 259568, upload-time = "2025-08-04T00:34:45.519Z" }, - { url = "https://files.pythonhosted.org/packages/60/57/f280dd6f1c556ecc744fbf39e835c33d3ae987d040d64d61c6f821e87829/coverage-7.10.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d800705f6951f75a905ea6feb03fff8f3ea3468b81e7563373ddc29aa3e5d1ca", size = 261691, upload-time = "2025-08-04T00:34:47.019Z" }, - { url = "https://files.pythonhosted.org/packages/54/2b/c63a0acbd19d99ec32326164c23df3a4e18984fb86e902afdd66ff7b3d83/coverage-7.10.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:248b5394718e10d067354448dc406d651709c6765669679311170da18e0e9af8", size = 259166, upload-time = "2025-08-04T00:34:48.792Z" }, - { url = "https://files.pythonhosted.org/packages/fd/c5/cd2997dcfcbf0683634da9df52d3967bc1f1741c1475dd0e4722012ba9ef/coverage-7.10.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5c61675a922b569137cf943770d7ad3edd0202d992ce53ac328c5ff68213ccf4", size = 257241, upload-time = "2025-08-04T00:34:51.038Z" }, - { url = "https://files.pythonhosted.org/packages/16/26/c9e30f82fdad8d47aee90af4978b18c88fa74369ae0f0ba0dbf08cee3a80/coverage-7.10.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:52d708b5fd65589461381fa442d9905f5903d76c086c6a4108e8e9efdca7a7ed", size = 258139, upload-time = "2025-08-04T00:34:52.533Z" }, - { url = "https://files.pythonhosted.org/packages/c9/99/bdb7bd00bebcd3dedfb895fa9af8e46b91422993e4a37ac634a5f1113790/coverage-7.10.2-cp314-cp314t-win32.whl", hash = "sha256:916369b3b914186b2c5e5ad2f7264b02cff5df96cdd7cdad65dccd39aa5fd9f0", size = 218809, upload-time = "2025-08-04T00:34:54.075Z" }, - { url = "https://files.pythonhosted.org/packages/eb/5e/56a7852e38a04d1520dda4dfbfbf74a3d6dec932c20526968f7444763567/coverage-7.10.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5b9d538e8e04916a5df63052d698b30c74eb0174f2ca9cd942c981f274a18eaf", size = 219926, upload-time = "2025-08-04T00:34:55.643Z" }, - { url = "https://files.pythonhosted.org/packages/e0/12/7fbe6b9c52bb9d627e9556f9f2edfdbe88b315e084cdecc9afead0c3b36a/coverage-7.10.2-cp314-cp314t-win_arm64.whl", hash = "sha256:04c74f9ef1f925456a9fd23a7eef1103126186d0500ef9a0acb0bd2514bdc7cc", size = 217925, upload-time = "2025-08-04T00:34:57.564Z" }, - { url = "https://files.pythonhosted.org/packages/18/d8/9b768ac73a8ac2d10c080af23937212434a958c8d2a1c84e89b450237942/coverage-7.10.2-py3-none-any.whl", hash = "sha256:95db3750dd2e6e93d99fa2498f3a1580581e49c494bddccc6f85c5c21604921f", size = 206973, upload-time = "2025-08-04T00:35:15.918Z" }, + { url = "https://files.pythonhosted.org/packages/2f/44/e14576c34b37764c821866909788ff7463228907ab82bae188dab2b421f1/coverage-7.10.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:53808194afdf948c462215e9403cca27a81cf150d2f9b386aee4dab614ae2ffe", size = 215964, upload-time = "2025-08-10T21:25:22.828Z" }, + { url = "https://files.pythonhosted.org/packages/e6/15/f4f92d9b83100903efe06c9396ee8d8bdba133399d37c186fc5b16d03a87/coverage-7.10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f4d1b837d1abf72187a61645dbf799e0d7705aa9232924946e1f57eb09a3bf00", size = 216361, upload-time = "2025-08-10T21:25:25.603Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3a/c92e8cd5e89acc41cfc026dfb7acedf89661ce2ea1ee0ee13aacb6b2c20c/coverage-7.10.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2a90dd4505d3cc68b847ab10c5ee81822a968b5191664e8a0801778fa60459fa", size = 243115, upload-time = "2025-08-10T21:25:27.09Z" }, + { url = "https://files.pythonhosted.org/packages/23/53/c1d8c2778823b1d95ca81701bb8f42c87dc341a2f170acdf716567523490/coverage-7.10.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d52989685ff5bf909c430e6d7f6550937bc6d6f3e6ecb303c97a86100efd4596", size = 244927, upload-time = "2025-08-10T21:25:28.77Z" }, + { url = "https://files.pythonhosted.org/packages/79/41/1e115fd809031f432b4ff8e2ca19999fb6196ab95c35ae7ad5e07c001130/coverage-7.10.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdb558a1d97345bde3a9f4d3e8d11c9e5611f748646e9bb61d7d612a796671b5", size = 246784, upload-time = "2025-08-10T21:25:30.195Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b2/0eba9bdf8f1b327ae2713c74d4b7aa85451bb70622ab4e7b8c000936677c/coverage-7.10.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c9e6331a8f09cb1fc8bda032752af03c366870b48cce908875ba2620d20d0ad4", size = 244828, upload-time = "2025-08-10T21:25:31.785Z" }, + { url = "https://files.pythonhosted.org/packages/1f/cc/74c56b6bf71f2a53b9aa3df8bc27163994e0861c065b4fe3a8ac290bed35/coverage-7.10.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:992f48bf35b720e174e7fae916d943599f1a66501a2710d06c5f8104e0756ee1", size = 242844, upload-time = "2025-08-10T21:25:33.37Z" }, + { url = "https://files.pythonhosted.org/packages/b6/7b/ac183fbe19ac5596c223cb47af5737f4437e7566100b7e46cc29b66695a5/coverage-7.10.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c5595fc4ad6a39312c786ec3326d7322d0cf10e3ac6a6df70809910026d67cfb", size = 243721, upload-time = "2025-08-10T21:25:34.939Z" }, + { url = "https://files.pythonhosted.org/packages/57/96/cb90da3b5a885af48f531905234a1e7376acfc1334242183d23154a1c285/coverage-7.10.3-cp310-cp310-win32.whl", hash = "sha256:9e92fa1f2bd5a57df9d00cf9ce1eb4ef6fccca4ceabec1c984837de55329db34", size = 218481, upload-time = "2025-08-10T21:25:36.935Z" }, + { url = "https://files.pythonhosted.org/packages/15/67/1ba4c7d75745c4819c54a85766e0a88cc2bff79e1760c8a2debc34106dc2/coverage-7.10.3-cp310-cp310-win_amd64.whl", hash = "sha256:b96524d6e4a3ce6a75c56bb15dbd08023b0ae2289c254e15b9fbdddf0c577416", size = 219382, upload-time = "2025-08-10T21:25:38.267Z" }, + { url = "https://files.pythonhosted.org/packages/87/04/810e506d7a19889c244d35199cbf3239a2f952b55580aa42ca4287409424/coverage-7.10.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f2ff2e2afdf0d51b9b8301e542d9c21a8d084fd23d4c8ea2b3a1b3c96f5f7397", size = 216075, upload-time = "2025-08-10T21:25:39.891Z" }, + { url = "https://files.pythonhosted.org/packages/2e/50/6b3fbab034717b4af3060bdaea6b13dfdc6b1fad44b5082e2a95cd378a9a/coverage-7.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:18ecc5d1b9a8c570f6c9b808fa9a2b16836b3dd5414a6d467ae942208b095f85", size = 216476, upload-time = "2025-08-10T21:25:41.137Z" }, + { url = "https://files.pythonhosted.org/packages/c7/96/4368c624c1ed92659812b63afc76c492be7867ac8e64b7190b88bb26d43c/coverage-7.10.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1af4461b25fe92889590d438905e1fc79a95680ec2a1ff69a591bb3fdb6c7157", size = 246865, upload-time = "2025-08-10T21:25:42.408Z" }, + { url = "https://files.pythonhosted.org/packages/34/12/5608f76070939395c17053bf16e81fd6c06cf362a537ea9d07e281013a27/coverage-7.10.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3966bc9a76b09a40dc6063c8b10375e827ea5dfcaffae402dd65953bef4cba54", size = 248800, upload-time = "2025-08-10T21:25:44.098Z" }, + { url = "https://files.pythonhosted.org/packages/ce/52/7cc90c448a0ad724283cbcdfd66b8d23a598861a6a22ac2b7b8696491798/coverage-7.10.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:205a95b87ef4eb303b7bc5118b47b6b6604a644bcbdb33c336a41cfc0a08c06a", size = 250904, upload-time = "2025-08-10T21:25:45.384Z" }, + { url = "https://files.pythonhosted.org/packages/e6/70/9967b847063c1c393b4f4d6daab1131558ebb6b51f01e7df7150aa99f11d/coverage-7.10.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b3801b79fb2ad61e3c7e2554bab754fc5f105626056980a2b9cf3aef4f13f84", size = 248597, upload-time = "2025-08-10T21:25:47.059Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fe/263307ce6878b9ed4865af42e784b42bb82d066bcf10f68defa42931c2c7/coverage-7.10.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b0dc69c60224cda33d384572da945759756e3f06b9cdac27f302f53961e63160", size = 246647, upload-time = "2025-08-10T21:25:48.334Z" }, + { url = "https://files.pythonhosted.org/packages/8e/27/d27af83ad162eba62c4eb7844a1de6cf7d9f6b185df50b0a3514a6f80ddd/coverage-7.10.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a83d4f134bab2c7ff758e6bb1541dd72b54ba295ced6a63d93efc2e20cb9b124", size = 247290, upload-time = "2025-08-10T21:25:49.945Z" }, + { url = "https://files.pythonhosted.org/packages/28/83/904ff27e15467a5622dbe9ad2ed5831b4a616a62570ec5924d06477dff5a/coverage-7.10.3-cp311-cp311-win32.whl", hash = "sha256:54e409dd64e5302b2a8fdf44ec1c26f47abd1f45a2dcf67bd161873ee05a59b8", size = 218521, upload-time = "2025-08-10T21:25:51.208Z" }, + { url = "https://files.pythonhosted.org/packages/b8/29/bc717b8902faaccf0ca486185f0dcab4778561a529dde51cb157acaafa16/coverage-7.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:30c601610a9b23807c5e9e2e442054b795953ab85d525c3de1b1b27cebeb2117", size = 219412, upload-time = "2025-08-10T21:25:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7a/5a1a7028c11bb589268c656c6b3f2bbf06e0aced31bbdf7a4e94e8442cc0/coverage-7.10.3-cp311-cp311-win_arm64.whl", hash = "sha256:dabe662312a97958e932dee056f2659051d822552c0b866823e8ba1c2fe64770", size = 218091, upload-time = "2025-08-10T21:25:54.102Z" }, + { url = "https://files.pythonhosted.org/packages/b8/62/13c0b66e966c43d7aa64dadc8cd2afa1f5a2bf9bb863bdabc21fb94e8b63/coverage-7.10.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:449c1e2d3a84d18bd204258a897a87bc57380072eb2aded6a5b5226046207b42", size = 216262, upload-time = "2025-08-10T21:25:55.367Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f0/59fdf79be7ac2f0206fc739032f482cfd3f66b18f5248108ff192741beae/coverage-7.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1d4f9ce50b9261ad196dc2b2e9f1fbbee21651b54c3097a25ad783679fd18294", size = 216496, upload-time = "2025-08-10T21:25:56.759Z" }, + { url = "https://files.pythonhosted.org/packages/34/b1/bc83788ba31bde6a0c02eb96bbc14b2d1eb083ee073beda18753fa2c4c66/coverage-7.10.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4dd4564207b160d0d45c36a10bc0a3d12563028e8b48cd6459ea322302a156d7", size = 247989, upload-time = "2025-08-10T21:25:58.067Z" }, + { url = "https://files.pythonhosted.org/packages/0c/29/f8bdf88357956c844bd872e87cb16748a37234f7f48c721dc7e981145eb7/coverage-7.10.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5ca3c9530ee072b7cb6a6ea7b640bcdff0ad3b334ae9687e521e59f79b1d0437", size = 250738, upload-time = "2025-08-10T21:25:59.406Z" }, + { url = "https://files.pythonhosted.org/packages/ae/df/6396301d332b71e42bbe624670af9376f63f73a455cc24723656afa95796/coverage-7.10.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b6df359e59fa243c9925ae6507e27f29c46698359f45e568fd51b9315dbbe587", size = 251868, upload-time = "2025-08-10T21:26:00.65Z" }, + { url = "https://files.pythonhosted.org/packages/91/21/d760b2df6139b6ef62c9cc03afb9bcdf7d6e36ed4d078baacffa618b4c1c/coverage-7.10.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a181e4c2c896c2ff64c6312db3bda38e9ade2e1aa67f86a5628ae85873786cea", size = 249790, upload-time = "2025-08-10T21:26:02.009Z" }, + { url = "https://files.pythonhosted.org/packages/69/91/5dcaa134568202397fa4023d7066d4318dc852b53b428052cd914faa05e1/coverage-7.10.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a374d4e923814e8b72b205ef6b3d3a647bb50e66f3558582eda074c976923613", size = 247907, upload-time = "2025-08-10T21:26:03.757Z" }, + { url = "https://files.pythonhosted.org/packages/38/ed/70c0e871cdfef75f27faceada461206c1cc2510c151e1ef8d60a6fedda39/coverage-7.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:daeefff05993e5e8c6e7499a8508e7bd94502b6b9a9159c84fd1fe6bce3151cb", size = 249344, upload-time = "2025-08-10T21:26:05.11Z" }, + { url = "https://files.pythonhosted.org/packages/5f/55/c8a273ed503cedc07f8a00dcd843daf28e849f0972e4c6be4c027f418ad6/coverage-7.10.3-cp312-cp312-win32.whl", hash = "sha256:187ecdcac21f9636d570e419773df7bd2fda2e7fa040f812e7f95d0bddf5f79a", size = 218693, upload-time = "2025-08-10T21:26:06.534Z" }, + { url = "https://files.pythonhosted.org/packages/94/58/dd3cfb2473b85be0b6eb8c5b6d80b6fc3f8f23611e69ef745cef8cf8bad5/coverage-7.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:4a50ad2524ee7e4c2a95e60d2b0b83283bdfc745fe82359d567e4f15d3823eb5", size = 219501, upload-time = "2025-08-10T21:26:08.195Z" }, + { url = "https://files.pythonhosted.org/packages/56/af/7cbcbf23d46de6f24246e3f76b30df099d05636b30c53c158a196f7da3ad/coverage-7.10.3-cp312-cp312-win_arm64.whl", hash = "sha256:c112f04e075d3495fa3ed2200f71317da99608cbb2e9345bdb6de8819fc30571", size = 218135, upload-time = "2025-08-10T21:26:09.584Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ff/239e4de9cc149c80e9cc359fab60592365b8c4cbfcad58b8a939d18c6898/coverage-7.10.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b99e87304ffe0eb97c5308447328a584258951853807afdc58b16143a530518a", size = 216298, upload-time = "2025-08-10T21:26:10.973Z" }, + { url = "https://files.pythonhosted.org/packages/56/da/28717da68f8ba68f14b9f558aaa8f3e39ada8b9a1ae4f4977c8f98b286d5/coverage-7.10.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4af09c7574d09afbc1ea7da9dcea23665c01f3bc1b1feb061dac135f98ffc53a", size = 216546, upload-time = "2025-08-10T21:26:12.616Z" }, + { url = "https://files.pythonhosted.org/packages/de/bb/e1ade16b9e3f2d6c323faeb6bee8e6c23f3a72760a5d9af102ef56a656cb/coverage-7.10.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:488e9b50dc5d2aa9521053cfa706209e5acf5289e81edc28291a24f4e4488f46", size = 247538, upload-time = "2025-08-10T21:26:14.455Z" }, + { url = "https://files.pythonhosted.org/packages/ea/2f/6ae1db51dc34db499bfe340e89f79a63bd115fc32513a7bacdf17d33cd86/coverage-7.10.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:913ceddb4289cbba3a310704a424e3fb7aac2bc0c3a23ea473193cb290cf17d4", size = 250141, upload-time = "2025-08-10T21:26:15.787Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ed/33efd8819895b10c66348bf26f011dd621e804866c996ea6893d682218df/coverage-7.10.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b1f91cbc78c7112ab84ed2a8defbccd90f888fcae40a97ddd6466b0bec6ae8a", size = 251415, upload-time = "2025-08-10T21:26:17.535Z" }, + { url = "https://files.pythonhosted.org/packages/26/04/cb83826f313d07dc743359c9914d9bc460e0798da9a0e38b4f4fabc207ed/coverage-7.10.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0bac054d45af7cd938834b43a9878b36ea92781bcb009eab040a5b09e9927e3", size = 249575, upload-time = "2025-08-10T21:26:18.921Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fd/ae963c7a8e9581c20fa4355ab8940ca272554d8102e872dbb932a644e410/coverage-7.10.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fe72cbdd12d9e0f4aca873fa6d755e103888a7f9085e4a62d282d9d5b9f7928c", size = 247466, upload-time = "2025-08-10T21:26:20.263Z" }, + { url = "https://files.pythonhosted.org/packages/99/e8/b68d1487c6af370b8d5ef223c6d7e250d952c3acfbfcdbf1a773aa0da9d2/coverage-7.10.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c1e2e927ab3eadd7c244023927d646e4c15c65bb2ac7ae3c3e9537c013700d21", size = 249084, upload-time = "2025-08-10T21:26:21.638Z" }, + { url = "https://files.pythonhosted.org/packages/66/4d/a0bcb561645c2c1e21758d8200443669d6560d2a2fb03955291110212ec4/coverage-7.10.3-cp313-cp313-win32.whl", hash = "sha256:24d0c13de473b04920ddd6e5da3c08831b1170b8f3b17461d7429b61cad59ae0", size = 218735, upload-time = "2025-08-10T21:26:23.009Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c3/78b4adddbc0feb3b223f62761e5f9b4c5a758037aaf76e0a5845e9e35e48/coverage-7.10.3-cp313-cp313-win_amd64.whl", hash = "sha256:3564aae76bce4b96e2345cf53b4c87e938c4985424a9be6a66ee902626edec4c", size = 219531, upload-time = "2025-08-10T21:26:24.474Z" }, + { url = "https://files.pythonhosted.org/packages/70/1b/1229c0b2a527fa5390db58d164aa896d513a1fbb85a1b6b6676846f00552/coverage-7.10.3-cp313-cp313-win_arm64.whl", hash = "sha256:f35580f19f297455f44afcd773c9c7a058e52eb6eb170aa31222e635f2e38b87", size = 218162, upload-time = "2025-08-10T21:26:25.847Z" }, + { url = "https://files.pythonhosted.org/packages/fc/26/1c1f450e15a3bf3eaecf053ff64538a2612a23f05b21d79ce03be9ff5903/coverage-7.10.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07009152f497a0464ffdf2634586787aea0e69ddd023eafb23fc38267db94b84", size = 217003, upload-time = "2025-08-10T21:26:27.231Z" }, + { url = "https://files.pythonhosted.org/packages/29/96/4b40036181d8c2948454b458750960956a3c4785f26a3c29418bbbee1666/coverage-7.10.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8dd2ba5f0c7e7e8cc418be2f0c14c4d9e3f08b8fb8e4c0f83c2fe87d03eb655e", size = 217238, upload-time = "2025-08-10T21:26:28.83Z" }, + { url = "https://files.pythonhosted.org/packages/62/23/8dfc52e95da20957293fb94d97397a100e63095ec1e0ef5c09dd8c6f591a/coverage-7.10.3-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1ae22b97003c74186e034a93e4f946c75fad8c0ce8d92fbbc168b5e15ee2841f", size = 258561, upload-time = "2025-08-10T21:26:30.475Z" }, + { url = "https://files.pythonhosted.org/packages/59/95/00e7fcbeda3f632232f4c07dde226afe3511a7781a000aa67798feadc535/coverage-7.10.3-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb329f1046888a36b1dc35504d3029e1dd5afe2196d94315d18c45ee380f67d5", size = 260735, upload-time = "2025-08-10T21:26:32.333Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4c/f4666cbc4571804ba2a65b078ff0de600b0b577dc245389e0bc9b69ae7ca/coverage-7.10.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce01048199a91f07f96ca3074b0c14021f4fe7ffd29a3e6a188ac60a5c3a4af8", size = 262960, upload-time = "2025-08-10T21:26:33.701Z" }, + { url = "https://files.pythonhosted.org/packages/c1/a5/8a9e8a7b12a290ed98b60f73d1d3e5e9ced75a4c94a0d1a671ce3ddfff2a/coverage-7.10.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:08b989a06eb9dfacf96d42b7fb4c9a22bafa370d245dc22fa839f2168c6f9fa1", size = 260515, upload-time = "2025-08-10T21:26:35.16Z" }, + { url = "https://files.pythonhosted.org/packages/86/11/bb59f7f33b2cac0c5b17db0d9d0abba9c90d9eda51a6e727b43bd5fce4ae/coverage-7.10.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:669fe0d4e69c575c52148511029b722ba8d26e8a3129840c2ce0522e1452b256", size = 258278, upload-time = "2025-08-10T21:26:36.539Z" }, + { url = "https://files.pythonhosted.org/packages/cc/22/3646f8903743c07b3e53fded0700fed06c580a980482f04bf9536657ac17/coverage-7.10.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3262d19092771c83f3413831d9904b1ccc5f98da5de4ffa4ad67f5b20c7aaf7b", size = 259408, upload-time = "2025-08-10T21:26:37.954Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/6375e9d905da22ddea41cd85c30994b8b6f6c02e44e4c5744b76d16b026f/coverage-7.10.3-cp313-cp313t-win32.whl", hash = "sha256:cc0ee4b2ccd42cab7ee6be46d8a67d230cb33a0a7cd47a58b587a7063b6c6b0e", size = 219396, upload-time = "2025-08-10T21:26:39.426Z" }, + { url = "https://files.pythonhosted.org/packages/33/3b/7da37fd14412b8c8b6e73c3e7458fef6b1b05a37f990a9776f88e7740c89/coverage-7.10.3-cp313-cp313t-win_amd64.whl", hash = "sha256:03db599f213341e2960430984e04cf35fb179724e052a3ee627a068653cf4a7c", size = 220458, upload-time = "2025-08-10T21:26:40.905Z" }, + { url = "https://files.pythonhosted.org/packages/28/cc/59a9a70f17edab513c844ee7a5c63cf1057041a84cc725b46a51c6f8301b/coverage-7.10.3-cp313-cp313t-win_arm64.whl", hash = "sha256:46eae7893ba65f53c71284585a262f083ef71594f05ec5c85baf79c402369098", size = 218722, upload-time = "2025-08-10T21:26:42.362Z" }, + { url = "https://files.pythonhosted.org/packages/2d/84/bb773b51a06edbf1231b47dc810a23851f2796e913b335a0fa364773b842/coverage-7.10.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:bce8b8180912914032785850d8f3aacb25ec1810f5f54afc4a8b114e7a9b55de", size = 216280, upload-time = "2025-08-10T21:26:44.132Z" }, + { url = "https://files.pythonhosted.org/packages/92/a8/4d8ca9c111d09865f18d56facff64d5fa076a5593c290bd1cfc5dceb8dba/coverage-7.10.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07790b4b37d56608536f7c1079bd1aa511567ac2966d33d5cec9cf520c50a7c8", size = 216557, upload-time = "2025-08-10T21:26:45.598Z" }, + { url = "https://files.pythonhosted.org/packages/fe/b2/eb668bfc5060194bc5e1ccd6f664e8e045881cfee66c42a2aa6e6c5b26e8/coverage-7.10.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e79367ef2cd9166acedcbf136a458dfe9a4a2dd4d1ee95738fb2ee581c56f667", size = 247598, upload-time = "2025-08-10T21:26:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/fd/b0/9faa4ac62c8822219dd83e5d0e73876398af17d7305968aed8d1606d1830/coverage-7.10.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:419d2a0f769f26cb1d05e9ccbc5eab4cb5d70231604d47150867c07822acbdf4", size = 250131, upload-time = "2025-08-10T21:26:48.65Z" }, + { url = "https://files.pythonhosted.org/packages/4e/90/203537e310844d4bf1bdcfab89c1e05c25025c06d8489b9e6f937ad1a9e2/coverage-7.10.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee221cf244757cdc2ac882e3062ab414b8464ad9c884c21e878517ea64b3fa26", size = 251485, upload-time = "2025-08-10T21:26:50.368Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b2/9d894b26bc53c70a1fe503d62240ce6564256d6d35600bdb86b80e516e7d/coverage-7.10.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c2079d8cdd6f7373d628e14b3357f24d1db02c9dc22e6a007418ca7a2be0435a", size = 249488, upload-time = "2025-08-10T21:26:52.045Z" }, + { url = "https://files.pythonhosted.org/packages/b4/28/af167dbac5281ba6c55c933a0ca6675d68347d5aee39cacc14d44150b922/coverage-7.10.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:bd8df1f83c0703fa3ca781b02d36f9ec67ad9cb725b18d486405924f5e4270bd", size = 247419, upload-time = "2025-08-10T21:26:53.533Z" }, + { url = "https://files.pythonhosted.org/packages/f4/1c/9a4ddc9f0dcb150d4cd619e1c4bb39bcf694c6129220bdd1e5895d694dda/coverage-7.10.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6b4e25e0fa335c8aa26e42a52053f3786a61cc7622b4d54ae2dad994aa754fec", size = 248917, upload-time = "2025-08-10T21:26:55.11Z" }, + { url = "https://files.pythonhosted.org/packages/92/27/c6a60c7cbe10dbcdcd7fc9ee89d531dc04ea4c073800279bb269954c5a9f/coverage-7.10.3-cp314-cp314-win32.whl", hash = "sha256:d7c3d02c2866deb217dce664c71787f4b25420ea3eaf87056f44fb364a3528f5", size = 218999, upload-time = "2025-08-10T21:26:56.637Z" }, + { url = "https://files.pythonhosted.org/packages/36/09/a94c1369964ab31273576615d55e7d14619a1c47a662ed3e2a2fe4dee7d4/coverage-7.10.3-cp314-cp314-win_amd64.whl", hash = "sha256:9c8916d44d9e0fe6cdb2227dc6b0edd8bc6c8ef13438bbbf69af7482d9bb9833", size = 219801, upload-time = "2025-08-10T21:26:58.207Z" }, + { url = "https://files.pythonhosted.org/packages/23/59/f5cd2a80f401c01cf0f3add64a7b791b7d53fd6090a4e3e9ea52691cf3c4/coverage-7.10.3-cp314-cp314-win_arm64.whl", hash = "sha256:1007d6a2b3cf197c57105cc1ba390d9ff7f0bee215ced4dea530181e49c65ab4", size = 218381, upload-time = "2025-08-10T21:26:59.707Z" }, + { url = "https://files.pythonhosted.org/packages/73/3d/89d65baf1ea39e148ee989de6da601469ba93c1d905b17dfb0b83bd39c96/coverage-7.10.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ebc8791d346410d096818788877d675ca55c91db87d60e8f477bd41c6970ffc6", size = 217019, upload-time = "2025-08-10T21:27:01.242Z" }, + { url = "https://files.pythonhosted.org/packages/7d/7d/d9850230cd9c999ce3a1e600f85c2fff61a81c301334d7a1faa1a5ba19c8/coverage-7.10.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f4e4d8e75f6fd3c6940ebeed29e3d9d632e1f18f6fb65d33086d99d4d073241", size = 217237, upload-time = "2025-08-10T21:27:03.442Z" }, + { url = "https://files.pythonhosted.org/packages/36/51/b87002d417202ab27f4a1cd6bd34ee3b78f51b3ddbef51639099661da991/coverage-7.10.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:24581ed69f132b6225a31b0228ae4885731cddc966f8a33fe5987288bdbbbd5e", size = 258735, upload-time = "2025-08-10T21:27:05.124Z" }, + { url = "https://files.pythonhosted.org/packages/1c/02/1f8612bfcb46fc7ca64a353fff1cd4ed932bb6e0b4e0bb88b699c16794b8/coverage-7.10.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec151569ddfccbf71bac8c422dce15e176167385a00cd86e887f9a80035ce8a5", size = 260901, upload-time = "2025-08-10T21:27:06.68Z" }, + { url = "https://files.pythonhosted.org/packages/aa/3a/fe39e624ddcb2373908bd922756384bb70ac1c5009b0d1674eb326a3e428/coverage-7.10.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2ae8e7c56290b908ee817200c0b65929b8050bc28530b131fe7c6dfee3e7d86b", size = 263157, upload-time = "2025-08-10T21:27:08.398Z" }, + { url = "https://files.pythonhosted.org/packages/5e/89/496b6d5a10fa0d0691a633bb2b2bcf4f38f0bdfcbde21ad9e32d1af328ed/coverage-7.10.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fb742309766d7e48e9eb4dc34bc95a424707bc6140c0e7d9726e794f11b92a0", size = 260597, upload-time = "2025-08-10T21:27:10.237Z" }, + { url = "https://files.pythonhosted.org/packages/b6/a6/8b5bf6a9e8c6aaeb47d5fe9687014148efc05c3588110246d5fdeef9b492/coverage-7.10.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:c65e2a5b32fbe1e499f1036efa6eb9cb4ea2bf6f7168d0e7a5852f3024f471b1", size = 258353, upload-time = "2025-08-10T21:27:11.773Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6d/ad131be74f8afd28150a07565dfbdc86592fd61d97e2dc83383d9af219f0/coverage-7.10.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d48d2cb07d50f12f4f18d2bb75d9d19e3506c26d96fffabf56d22936e5ed8f7c", size = 259504, upload-time = "2025-08-10T21:27:13.254Z" }, + { url = "https://files.pythonhosted.org/packages/ec/30/fc9b5097092758cba3375a8cc4ff61774f8cd733bcfb6c9d21a60077a8d8/coverage-7.10.3-cp314-cp314t-win32.whl", hash = "sha256:dec0d9bc15ee305e09fe2cd1911d3f0371262d3cfdae05d79515d8cb712b4869", size = 219782, upload-time = "2025-08-10T21:27:14.736Z" }, + { url = "https://files.pythonhosted.org/packages/72/9b/27fbf79451b1fac15c4bda6ec6e9deae27cf7c0648c1305aa21a3454f5c4/coverage-7.10.3-cp314-cp314t-win_amd64.whl", hash = "sha256:424ea93a323aa0f7f01174308ea78bde885c3089ec1bef7143a6d93c3e24ef64", size = 220898, upload-time = "2025-08-10T21:27:16.297Z" }, + { url = "https://files.pythonhosted.org/packages/d1/cf/a32bbf92869cbf0b7c8b84325327bfc718ad4b6d2c63374fef3d58e39306/coverage-7.10.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f5983c132a62d93d71c9ef896a0b9bf6e6828d8d2ea32611f58684fba60bba35", size = 218922, upload-time = "2025-08-10T21:27:18.22Z" }, + { url = "https://files.pythonhosted.org/packages/84/19/e67f4ae24e232c7f713337f3f4f7c9c58afd0c02866fb07c7b9255a19ed7/coverage-7.10.3-py3-none-any.whl", hash = "sha256:416a8d74dc0adfd33944ba2f405897bab87b7e9e84a391e09d241956bd953ce1", size = 207921, upload-time = "2025-08-10T21:27:38.254Z" }, ] [package.optional-dependencies] @@ -1116,11 +1119,11 @@ wheels = [ [[package]] name = "identify" -version = "2.6.12" +version = "2.6.13" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/88/d193a27416618628a5eea64e3223acd800b40749a96ffb322a9b55a49ed1/identify-2.6.12.tar.gz", hash = "sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6", size = 99254, upload-time = "2025-05-23T20:37:53.3Z" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ca/ffbabe3635bb839aa36b3a893c91a9b0d368cb4d8073e03a12896970af82/identify-2.6.13.tar.gz", hash = "sha256:da8d6c828e773620e13bfa86ea601c5a5310ba4bcd65edf378198b56a1f9fb32", size = 99243, upload-time = "2025-08-09T19:35:00.6Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/cd/18f8da995b658420625f7ef13f037be53ae04ec5ad33f9b718240dcfd48c/identify-2.6.12-py2.py3-none-any.whl", hash = "sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2", size = 99145, upload-time = "2025-05-23T20:37:51.495Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ce/461b60a3ee109518c055953729bf9ed089a04db895d47e95444071dcdef2/identify-2.6.13-py2.py3-none-any.whl", hash = "sha256:60381139b3ae39447482ecc406944190f690d4a2997f2584062089848361b33b", size = 99153, upload-time = "2025-08-09T19:34:59.1Z" }, ] [[package]] @@ -1465,15 +1468,15 @@ linkify = [ [[package]] name = "markdownify" -version = "1.1.0" +version = "1.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "beautifulsoup4", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "six", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2f/78/c48fed23c7aebc2c16049062e72de1da3220c274de59d28c942acdc9ffb2/markdownify-1.1.0.tar.gz", hash = "sha256:449c0bbbf1401c5112379619524f33b63490a8fa479456d41de9dc9e37560ebd", size = 17127, upload-time = "2025-03-05T11:54:40.574Z" } +sdist = { url = "https://files.pythonhosted.org/packages/83/1b/6f2697b51eaca81f08852fd2734745af15718fea10222a1d40f8a239c4ea/markdownify-1.2.0.tar.gz", hash = "sha256:f6c367c54eb24ee953921804dfe6d6575c5e5b42c643955e7242034435de634c", size = 18771, upload-time = "2025-08-09T17:44:15.302Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/64/11/b751af7ad41b254a802cf52f7bc1fca7cabe2388132f2ce60a1a6b9b9622/markdownify-1.1.0-py3-none-any.whl", hash = "sha256:32a5a08e9af02c8a6528942224c91b933b4bd2c7d078f9012943776fc313eeef", size = 13901, upload-time = "2025-03-05T11:54:39.454Z" }, + { url = "https://files.pythonhosted.org/packages/6a/e2/7af643acb4cae0741dffffaa7f3f7c9e7ab4046724543ba1777c401d821c/markdownify-1.2.0-py3-none-any.whl", hash = "sha256:48e150a1c4993d4d50f282f725c0111bd9eb25645d41fa2f543708fd44161351", size = 15561, upload-time = "2025-08-09T17:44:14.074Z" }, ] [[package]] @@ -1548,14 +1551,14 @@ wheels = [ [[package]] name = "mdit-py-plugins" -version = "0.4.2" +version = "0.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/03/a2ecab526543b152300717cf232bb4bb8605b6edb946c845016fa9c9c9fd/mdit_py_plugins-0.4.2.tar.gz", hash = "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5", size = 43542, upload-time = "2024-09-09T20:27:49.564Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655, upload-time = "2025-08-11T07:25:49.083Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/f7/7782a043553ee469c1ff49cfa1cdace2d6bf99a1f333cf38676b3ddf30da/mdit_py_plugins-0.4.2-py3-none-any.whl", hash = "sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636", size = 55316, upload-time = "2024-09-09T20:27:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f", size = 57205, upload-time = "2025-08-11T07:25:47.597Z" }, ] [[package]] @@ -1595,104 +1598,104 @@ wheels = [ [[package]] name = "multidict" -version = "6.6.3" +version = "6.6.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", 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/3d/2c/5dad12e82fbdf7470f29bff2171484bf07cb3b16ada60a6589af8f376440/multidict-6.6.3.tar.gz", hash = "sha256:798a9eb12dab0a6c2e29c1de6f3468af5cb2da6053a20dfa3344907eed0937cc", size = 101006, upload-time = "2025-06-30T15:53:46.929Z" } +sdist = { url = "https://files.pythonhosted.org/packages/69/7f/0652e6ed47ab288e3756ea9c0df8b14950781184d4bd7883f4d87dd41245/multidict-6.6.4.tar.gz", hash = "sha256:d2d4e4787672911b48350df02ed3fa3fffdc2f2e8ca06dd6afdf34189b76a9dd", size = 101843, upload-time = "2025-08-11T12:08:48.217Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/67/414933982bce2efce7cbcb3169eaaf901e0f25baec69432b4874dfb1f297/multidict-6.6.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a2be5b7b35271f7fff1397204ba6708365e3d773579fe2a30625e16c4b4ce817", size = 77017, upload-time = "2025-06-30T15:50:58.931Z" }, - { url = "https://files.pythonhosted.org/packages/8a/fe/d8a3ee1fad37dc2ef4f75488b0d9d4f25bf204aad8306cbab63d97bff64a/multidict-6.6.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:12f4581d2930840295c461764b9a65732ec01250b46c6b2c510d7ee68872b140", size = 44897, upload-time = "2025-06-30T15:51:00.999Z" }, - { url = "https://files.pythonhosted.org/packages/1f/e0/265d89af8c98240265d82b8cbcf35897f83b76cd59ee3ab3879050fd8c45/multidict-6.6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dd7793bab517e706c9ed9d7310b06c8672fd0aeee5781bfad612f56b8e0f7d14", size = 44574, upload-time = "2025-06-30T15:51:02.449Z" }, - { url = "https://files.pythonhosted.org/packages/e6/05/6b759379f7e8e04ccc97cfb2a5dcc5cdbd44a97f072b2272dc51281e6a40/multidict-6.6.3-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:72d8815f2cd3cf3df0f83cac3f3ef801d908b2d90409ae28102e0553af85545a", size = 225729, upload-time = "2025-06-30T15:51:03.794Z" }, - { url = "https://files.pythonhosted.org/packages/4e/f5/8d5a15488edd9a91fa4aad97228d785df208ed6298580883aa3d9def1959/multidict-6.6.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:531e331a2ee53543ab32b16334e2deb26f4e6b9b28e41f8e0c87e99a6c8e2d69", size = 242515, upload-time = "2025-06-30T15:51:05.002Z" }, - { url = "https://files.pythonhosted.org/packages/6e/b5/a8f317d47d0ac5bb746d6d8325885c8967c2a8ce0bb57be5399e3642cccb/multidict-6.6.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:42ca5aa9329a63be8dc49040f63817d1ac980e02eeddba763a9ae5b4027b9c9c", size = 222224, upload-time = "2025-06-30T15:51:06.148Z" }, - { url = "https://files.pythonhosted.org/packages/76/88/18b2a0d5e80515fa22716556061189c2853ecf2aa2133081ebbe85ebea38/multidict-6.6.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:208b9b9757060b9faa6f11ab4bc52846e4f3c2fb8b14d5680c8aac80af3dc751", size = 253124, upload-time = "2025-06-30T15:51:07.375Z" }, - { url = "https://files.pythonhosted.org/packages/62/bf/ebfcfd6b55a1b05ef16d0775ae34c0fe15e8dab570d69ca9941073b969e7/multidict-6.6.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:acf6b97bd0884891af6a8b43d0f586ab2fcf8e717cbd47ab4bdddc09e20652d8", size = 251529, upload-time = "2025-06-30T15:51:08.691Z" }, - { url = "https://files.pythonhosted.org/packages/44/11/780615a98fd3775fc309d0234d563941af69ade2df0bb82c91dda6ddaea1/multidict-6.6.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:68e9e12ed00e2089725669bdc88602b0b6f8d23c0c95e52b95f0bc69f7fe9b55", size = 241627, upload-time = "2025-06-30T15:51:10.605Z" }, - { url = "https://files.pythonhosted.org/packages/28/3d/35f33045e21034b388686213752cabc3a1b9d03e20969e6fa8f1b1d82db1/multidict-6.6.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:05db2f66c9addb10cfa226e1acb363450fab2ff8a6df73c622fefe2f5af6d4e7", size = 239351, upload-time = "2025-06-30T15:51:12.18Z" }, - { url = "https://files.pythonhosted.org/packages/6e/cc/ff84c03b95b430015d2166d9aae775a3985d757b94f6635010d0038d9241/multidict-6.6.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:0db58da8eafb514db832a1b44f8fa7906fdd102f7d982025f816a93ba45e3dcb", size = 233429, upload-time = "2025-06-30T15:51:13.533Z" }, - { url = "https://files.pythonhosted.org/packages/2e/f0/8cd49a0b37bdea673a4b793c2093f2f4ba8e7c9d6d7c9bd672fd6d38cd11/multidict-6.6.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:14117a41c8fdb3ee19c743b1c027da0736fdb79584d61a766da53d399b71176c", size = 243094, upload-time = "2025-06-30T15:51:14.815Z" }, - { url = "https://files.pythonhosted.org/packages/96/19/5d9a0cfdafe65d82b616a45ae950975820289069f885328e8185e64283c2/multidict-6.6.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:877443eaaabcd0b74ff32ebeed6f6176c71850feb7d6a1d2db65945256ea535c", size = 248957, upload-time = "2025-06-30T15:51:16.076Z" }, - { url = "https://files.pythonhosted.org/packages/e6/dc/c90066151da87d1e489f147b9b4327927241e65f1876702fafec6729c014/multidict-6.6.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:70b72e749a4f6e7ed8fb334fa8d8496384840319512746a5f42fa0aec79f4d61", size = 243590, upload-time = "2025-06-30T15:51:17.413Z" }, - { url = "https://files.pythonhosted.org/packages/ec/39/458afb0cccbb0ee9164365273be3e039efddcfcb94ef35924b7dbdb05db0/multidict-6.6.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:43571f785b86afd02b3855c5ac8e86ec921b760298d6f82ff2a61daf5a35330b", size = 237487, upload-time = "2025-06-30T15:51:19.039Z" }, - { url = "https://files.pythonhosted.org/packages/35/38/0016adac3990426610a081787011177e661875546b434f50a26319dc8372/multidict-6.6.3-cp310-cp310-win32.whl", hash = "sha256:20c5a0c3c13a15fd5ea86c42311859f970070e4e24de5a550e99d7c271d76318", size = 41390, upload-time = "2025-06-30T15:51:20.362Z" }, - { url = "https://files.pythonhosted.org/packages/f3/d2/17897a8f3f2c5363d969b4c635aa40375fe1f09168dc09a7826780bfb2a4/multidict-6.6.3-cp310-cp310-win_amd64.whl", hash = "sha256:ab0a34a007704c625e25a9116c6770b4d3617a071c8a7c30cd338dfbadfe6485", size = 45954, upload-time = "2025-06-30T15:51:21.383Z" }, - { url = "https://files.pythonhosted.org/packages/2d/5f/d4a717c1e457fe44072e33fa400d2b93eb0f2819c4d669381f925b7cba1f/multidict-6.6.3-cp310-cp310-win_arm64.whl", hash = "sha256:769841d70ca8bdd140a715746199fc6473414bd02efd678d75681d2d6a8986c5", size = 42981, upload-time = "2025-06-30T15:51:22.809Z" }, - { url = "https://files.pythonhosted.org/packages/08/f0/1a39863ced51f639c81a5463fbfa9eb4df59c20d1a8769ab9ef4ca57ae04/multidict-6.6.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:18f4eba0cbac3546b8ae31e0bbc55b02c801ae3cbaf80c247fcdd89b456ff58c", size = 76445, upload-time = "2025-06-30T15:51:24.01Z" }, - { url = "https://files.pythonhosted.org/packages/c9/0e/a7cfa451c7b0365cd844e90b41e21fab32edaa1e42fc0c9f68461ce44ed7/multidict-6.6.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef43b5dd842382329e4797c46f10748d8c2b6e0614f46b4afe4aee9ac33159df", size = 44610, upload-time = "2025-06-30T15:51:25.158Z" }, - { url = "https://files.pythonhosted.org/packages/c6/bb/a14a4efc5ee748cc1904b0748be278c31b9295ce5f4d2ef66526f410b94d/multidict-6.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf9bd1fd5eec01494e0f2e8e446a74a85d5e49afb63d75a9934e4a5423dba21d", size = 44267, upload-time = "2025-06-30T15:51:26.326Z" }, - { url = "https://files.pythonhosted.org/packages/c2/f8/410677d563c2d55e063ef74fe578f9d53fe6b0a51649597a5861f83ffa15/multidict-6.6.3-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5bd8d6f793a787153956cd35e24f60485bf0651c238e207b9a54f7458b16d539", size = 230004, upload-time = "2025-06-30T15:51:27.491Z" }, - { url = "https://files.pythonhosted.org/packages/fd/df/2b787f80059314a98e1ec6a4cc7576244986df3e56b3c755e6fc7c99e038/multidict-6.6.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bf99b4daf908c73856bd87ee0a2499c3c9a3d19bb04b9c6025e66af3fd07462", size = 247196, upload-time = "2025-06-30T15:51:28.762Z" }, - { url = "https://files.pythonhosted.org/packages/05/f2/f9117089151b9a8ab39f9019620d10d9718eec2ac89e7ca9d30f3ec78e96/multidict-6.6.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b9e59946b49dafaf990fd9c17ceafa62976e8471a14952163d10a7a630413a9", size = 225337, upload-time = "2025-06-30T15:51:30.025Z" }, - { url = "https://files.pythonhosted.org/packages/93/2d/7115300ec5b699faa152c56799b089a53ed69e399c3c2d528251f0aeda1a/multidict-6.6.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e2db616467070d0533832d204c54eea6836a5e628f2cb1e6dfd8cd6ba7277cb7", size = 257079, upload-time = "2025-06-30T15:51:31.716Z" }, - { url = "https://files.pythonhosted.org/packages/15/ea/ff4bab367623e39c20d3b07637225c7688d79e4f3cc1f3b9f89867677f9a/multidict-6.6.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7394888236621f61dcdd25189b2768ae5cc280f041029a5bcf1122ac63df79f9", size = 255461, upload-time = "2025-06-30T15:51:33.029Z" }, - { url = "https://files.pythonhosted.org/packages/74/07/2c9246cda322dfe08be85f1b8739646f2c4c5113a1422d7a407763422ec4/multidict-6.6.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f114d8478733ca7388e7c7e0ab34b72547476b97009d643644ac33d4d3fe1821", size = 246611, upload-time = "2025-06-30T15:51:34.47Z" }, - { url = "https://files.pythonhosted.org/packages/a8/62/279c13d584207d5697a752a66ffc9bb19355a95f7659140cb1b3cf82180e/multidict-6.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cdf22e4db76d323bcdc733514bf732e9fb349707c98d341d40ebcc6e9318ef3d", size = 243102, upload-time = "2025-06-30T15:51:36.525Z" }, - { url = "https://files.pythonhosted.org/packages/69/cc/e06636f48c6d51e724a8bc8d9e1db5f136fe1df066d7cafe37ef4000f86a/multidict-6.6.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e995a34c3d44ab511bfc11aa26869b9d66c2d8c799fa0e74b28a473a692532d6", size = 238693, upload-time = "2025-06-30T15:51:38.278Z" }, - { url = "https://files.pythonhosted.org/packages/89/a4/66c9d8fb9acf3b226cdd468ed009537ac65b520aebdc1703dd6908b19d33/multidict-6.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:766a4a5996f54361d8d5a9050140aa5362fe48ce51c755a50c0bc3706460c430", size = 246582, upload-time = "2025-06-30T15:51:39.709Z" }, - { url = "https://files.pythonhosted.org/packages/cf/01/c69e0317be556e46257826d5449feb4e6aa0d18573e567a48a2c14156f1f/multidict-6.6.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:3893a0d7d28a7fe6ca7a1f760593bc13038d1d35daf52199d431b61d2660602b", size = 253355, upload-time = "2025-06-30T15:51:41.013Z" }, - { url = "https://files.pythonhosted.org/packages/c0/da/9cc1da0299762d20e626fe0042e71b5694f9f72d7d3f9678397cbaa71b2b/multidict-6.6.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:934796c81ea996e61914ba58064920d6cad5d99140ac3167901eb932150e2e56", size = 247774, upload-time = "2025-06-30T15:51:42.291Z" }, - { url = "https://files.pythonhosted.org/packages/e6/91/b22756afec99cc31105ddd4a52f95ab32b1a4a58f4d417979c570c4a922e/multidict-6.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9ed948328aec2072bc00f05d961ceadfd3e9bfc2966c1319aeaf7b7c21219183", size = 242275, upload-time = "2025-06-30T15:51:43.642Z" }, - { url = "https://files.pythonhosted.org/packages/be/f1/adcc185b878036a20399d5be5228f3cbe7f823d78985d101d425af35c800/multidict-6.6.3-cp311-cp311-win32.whl", hash = "sha256:9f5b28c074c76afc3e4c610c488e3493976fe0e596dd3db6c8ddfbb0134dcac5", size = 41290, upload-time = "2025-06-30T15:51:45.264Z" }, - { url = "https://files.pythonhosted.org/packages/e0/d4/27652c1c6526ea6b4f5ddd397e93f4232ff5de42bea71d339bc6a6cc497f/multidict-6.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:bc7f6fbc61b1c16050a389c630da0b32fc6d4a3d191394ab78972bf5edc568c2", size = 45942, upload-time = "2025-06-30T15:51:46.377Z" }, - { url = "https://files.pythonhosted.org/packages/16/18/23f4932019804e56d3c2413e237f866444b774b0263bcb81df2fdecaf593/multidict-6.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:d4e47d8faffaae822fb5cba20937c048d4f734f43572e7079298a6c39fb172cb", size = 42880, upload-time = "2025-06-30T15:51:47.561Z" }, - { url = "https://files.pythonhosted.org/packages/0e/a0/6b57988ea102da0623ea814160ed78d45a2645e4bbb499c2896d12833a70/multidict-6.6.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:056bebbeda16b2e38642d75e9e5310c484b7c24e3841dc0fb943206a72ec89d6", size = 76514, upload-time = "2025-06-30T15:51:48.728Z" }, - { url = "https://files.pythonhosted.org/packages/07/7a/d1e92665b0850c6c0508f101f9cf0410c1afa24973e1115fe9c6a185ebf7/multidict-6.6.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e5f481cccb3c5c5e5de5d00b5141dc589c1047e60d07e85bbd7dea3d4580d63f", size = 45394, upload-time = "2025-06-30T15:51:49.986Z" }, - { url = "https://files.pythonhosted.org/packages/52/6f/dd104490e01be6ef8bf9573705d8572f8c2d2c561f06e3826b081d9e6591/multidict-6.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:10bea2ee839a759ee368b5a6e47787f399b41e70cf0c20d90dfaf4158dfb4e55", size = 43590, upload-time = "2025-06-30T15:51:51.331Z" }, - { url = "https://files.pythonhosted.org/packages/44/fe/06e0e01b1b0611e6581b7fd5a85b43dacc08b6cea3034f902f383b0873e5/multidict-6.6.3-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:2334cfb0fa9549d6ce2c21af2bfbcd3ac4ec3646b1b1581c88e3e2b1779ec92b", size = 237292, upload-time = "2025-06-30T15:51:52.584Z" }, - { url = "https://files.pythonhosted.org/packages/ce/71/4f0e558fb77696b89c233c1ee2d92f3e1d5459070a0e89153c9e9e804186/multidict-6.6.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8fee016722550a2276ca2cb5bb624480e0ed2bd49125b2b73b7010b9090e888", size = 258385, upload-time = "2025-06-30T15:51:53.913Z" }, - { url = "https://files.pythonhosted.org/packages/e3/25/cca0e68228addad24903801ed1ab42e21307a1b4b6dd2cf63da5d3ae082a/multidict-6.6.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5511cb35f5c50a2db21047c875eb42f308c5583edf96bd8ebf7d770a9d68f6d", size = 242328, upload-time = "2025-06-30T15:51:55.672Z" }, - { url = "https://files.pythonhosted.org/packages/6e/a3/46f2d420d86bbcb8fe660b26a10a219871a0fbf4d43cb846a4031533f3e0/multidict-6.6.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:712b348f7f449948e0a6c4564a21c7db965af900973a67db432d724619b3c680", size = 268057, upload-time = "2025-06-30T15:51:57.037Z" }, - { url = "https://files.pythonhosted.org/packages/9e/73/1c743542fe00794a2ec7466abd3f312ccb8fad8dff9f36d42e18fb1ec33e/multidict-6.6.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e4e15d2138ee2694e038e33b7c3da70e6b0ad8868b9f8094a72e1414aeda9c1a", size = 269341, upload-time = "2025-06-30T15:51:59.111Z" }, - { url = "https://files.pythonhosted.org/packages/a4/11/6ec9dcbe2264b92778eeb85407d1df18812248bf3506a5a1754bc035db0c/multidict-6.6.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8df25594989aebff8a130f7899fa03cbfcc5d2b5f4a461cf2518236fe6f15961", size = 256081, upload-time = "2025-06-30T15:52:00.533Z" }, - { url = "https://files.pythonhosted.org/packages/9b/2b/631b1e2afeb5f1696846d747d36cda075bfdc0bc7245d6ba5c319278d6c4/multidict-6.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:159ca68bfd284a8860f8d8112cf0521113bffd9c17568579e4d13d1f1dc76b65", size = 253581, upload-time = "2025-06-30T15:52:02.43Z" }, - { url = "https://files.pythonhosted.org/packages/bf/0e/7e3b93f79efeb6111d3bf9a1a69e555ba1d07ad1c11bceb56b7310d0d7ee/multidict-6.6.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e098c17856a8c9ade81b4810888c5ad1914099657226283cab3062c0540b0643", size = 250750, upload-time = "2025-06-30T15:52:04.26Z" }, - { url = "https://files.pythonhosted.org/packages/ad/9e/086846c1d6601948e7de556ee464a2d4c85e33883e749f46b9547d7b0704/multidict-6.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:67c92ed673049dec52d7ed39f8cf9ebbadf5032c774058b4406d18c8f8fe7063", size = 251548, upload-time = "2025-06-30T15:52:06.002Z" }, - { url = "https://files.pythonhosted.org/packages/8c/7b/86ec260118e522f1a31550e87b23542294880c97cfbf6fb18cc67b044c66/multidict-6.6.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:bd0578596e3a835ef451784053cfd327d607fc39ea1a14812139339a18a0dbc3", size = 262718, upload-time = "2025-06-30T15:52:07.707Z" }, - { url = "https://files.pythonhosted.org/packages/8c/bd/22ce8f47abb0be04692c9fc4638508b8340987b18691aa7775d927b73f72/multidict-6.6.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:346055630a2df2115cd23ae271910b4cae40f4e336773550dca4889b12916e75", size = 259603, upload-time = "2025-06-30T15:52:09.58Z" }, - { url = "https://files.pythonhosted.org/packages/07/9c/91b7ac1691be95cd1f4a26e36a74b97cda6aa9820632d31aab4410f46ebd/multidict-6.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:555ff55a359302b79de97e0468e9ee80637b0de1fce77721639f7cd9440b3a10", size = 251351, upload-time = "2025-06-30T15:52:10.947Z" }, - { url = "https://files.pythonhosted.org/packages/6f/5c/4d7adc739884f7a9fbe00d1eac8c034023ef8bad71f2ebe12823ca2e3649/multidict-6.6.3-cp312-cp312-win32.whl", hash = "sha256:73ab034fb8d58ff85c2bcbadc470efc3fafeea8affcf8722855fb94557f14cc5", size = 41860, upload-time = "2025-06-30T15:52:12.334Z" }, - { url = "https://files.pythonhosted.org/packages/6a/a3/0fbc7afdf7cb1aa12a086b02959307848eb6bcc8f66fcb66c0cb57e2a2c1/multidict-6.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:04cbcce84f63b9af41bad04a54d4cc4e60e90c35b9e6ccb130be2d75b71f8c17", size = 45982, upload-time = "2025-06-30T15:52:13.6Z" }, - { url = "https://files.pythonhosted.org/packages/b8/95/8c825bd70ff9b02462dc18d1295dd08d3e9e4eb66856d292ffa62cfe1920/multidict-6.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:0f1130b896ecb52d2a1e615260f3ea2af55fa7dc3d7c3003ba0c3121a759b18b", size = 43210, upload-time = "2025-06-30T15:52:14.893Z" }, - { url = "https://files.pythonhosted.org/packages/52/1d/0bebcbbb4f000751fbd09957257903d6e002943fc668d841a4cf2fb7f872/multidict-6.6.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:540d3c06d48507357a7d57721e5094b4f7093399a0106c211f33540fdc374d55", size = 75843, upload-time = "2025-06-30T15:52:16.155Z" }, - { url = "https://files.pythonhosted.org/packages/07/8f/cbe241b0434cfe257f65c2b1bcf9e8d5fb52bc708c5061fb29b0fed22bdf/multidict-6.6.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9c19cea2a690f04247d43f366d03e4eb110a0dc4cd1bbeee4d445435428ed35b", size = 45053, upload-time = "2025-06-30T15:52:17.429Z" }, - { url = "https://files.pythonhosted.org/packages/32/d2/0b3b23f9dbad5b270b22a3ac3ea73ed0a50ef2d9a390447061178ed6bdb8/multidict-6.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7af039820cfd00effec86bda5d8debef711a3e86a1d3772e85bea0f243a4bd65", size = 43273, upload-time = "2025-06-30T15:52:19.346Z" }, - { url = "https://files.pythonhosted.org/packages/fd/fe/6eb68927e823999e3683bc49678eb20374ba9615097d085298fd5b386564/multidict-6.6.3-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:500b84f51654fdc3944e936f2922114349bf8fdcac77c3092b03449f0e5bc2b3", size = 237124, upload-time = "2025-06-30T15:52:20.773Z" }, - { url = "https://files.pythonhosted.org/packages/e7/ab/320d8507e7726c460cb77117848b3834ea0d59e769f36fdae495f7669929/multidict-6.6.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3fc723ab8a5c5ed6c50418e9bfcd8e6dceba6c271cee6728a10a4ed8561520c", size = 256892, upload-time = "2025-06-30T15:52:22.242Z" }, - { url = "https://files.pythonhosted.org/packages/76/60/38ee422db515ac69834e60142a1a69111ac96026e76e8e9aa347fd2e4591/multidict-6.6.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:94c47ea3ade005b5976789baaed66d4de4480d0a0bf31cef6edaa41c1e7b56a6", size = 240547, upload-time = "2025-06-30T15:52:23.736Z" }, - { url = "https://files.pythonhosted.org/packages/27/fb/905224fde2dff042b030c27ad95a7ae744325cf54b890b443d30a789b80e/multidict-6.6.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dbc7cf464cc6d67e83e136c9f55726da3a30176f020a36ead246eceed87f1cd8", size = 266223, upload-time = "2025-06-30T15:52:25.185Z" }, - { url = "https://files.pythonhosted.org/packages/76/35/dc38ab361051beae08d1a53965e3e1a418752fc5be4d3fb983c5582d8784/multidict-6.6.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:900eb9f9da25ada070f8ee4a23f884e0ee66fe4e1a38c3af644256a508ad81ca", size = 267262, upload-time = "2025-06-30T15:52:26.969Z" }, - { url = "https://files.pythonhosted.org/packages/1f/a3/0a485b7f36e422421b17e2bbb5a81c1af10eac1d4476f2ff92927c730479/multidict-6.6.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c6df517cf177da5d47ab15407143a89cd1a23f8b335f3a28d57e8b0a3dbb884", size = 254345, upload-time = "2025-06-30T15:52:28.467Z" }, - { url = "https://files.pythonhosted.org/packages/b4/59/bcdd52c1dab7c0e0d75ff19cac751fbd5f850d1fc39172ce809a74aa9ea4/multidict-6.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ef421045f13879e21c994b36e728d8e7d126c91a64b9185810ab51d474f27e7", size = 252248, upload-time = "2025-06-30T15:52:29.938Z" }, - { url = "https://files.pythonhosted.org/packages/bb/a4/2d96aaa6eae8067ce108d4acee6f45ced5728beda55c0f02ae1072c730d1/multidict-6.6.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6c1e61bb4f80895c081790b6b09fa49e13566df8fbff817da3f85b3a8192e36b", size = 250115, upload-time = "2025-06-30T15:52:31.416Z" }, - { url = "https://files.pythonhosted.org/packages/25/d2/ed9f847fa5c7d0677d4f02ea2c163d5e48573de3f57bacf5670e43a5ffaa/multidict-6.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e5e8523bb12d7623cd8300dbd91b9e439a46a028cd078ca695eb66ba31adee3c", size = 249649, upload-time = "2025-06-30T15:52:32.996Z" }, - { url = "https://files.pythonhosted.org/packages/1f/af/9155850372563fc550803d3f25373308aa70f59b52cff25854086ecb4a79/multidict-6.6.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ef58340cc896219e4e653dade08fea5c55c6df41bcc68122e3be3e9d873d9a7b", size = 261203, upload-time = "2025-06-30T15:52:34.521Z" }, - { url = "https://files.pythonhosted.org/packages/36/2f/c6a728f699896252cf309769089568a33c6439626648843f78743660709d/multidict-6.6.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc9dc435ec8699e7b602b94fe0cd4703e69273a01cbc34409af29e7820f777f1", size = 258051, upload-time = "2025-06-30T15:52:35.999Z" }, - { url = "https://files.pythonhosted.org/packages/d0/60/689880776d6b18fa2b70f6cc74ff87dd6c6b9b47bd9cf74c16fecfaa6ad9/multidict-6.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9e864486ef4ab07db5e9cb997bad2b681514158d6954dd1958dfb163b83d53e6", size = 249601, upload-time = "2025-06-30T15:52:37.473Z" }, - { url = "https://files.pythonhosted.org/packages/75/5e/325b11f2222a549019cf2ef879c1f81f94a0d40ace3ef55cf529915ba6cc/multidict-6.6.3-cp313-cp313-win32.whl", hash = "sha256:5633a82fba8e841bc5c5c06b16e21529573cd654f67fd833650a215520a6210e", size = 41683, upload-time = "2025-06-30T15:52:38.927Z" }, - { url = "https://files.pythonhosted.org/packages/b1/ad/cf46e73f5d6e3c775cabd2a05976547f3f18b39bee06260369a42501f053/multidict-6.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:e93089c1570a4ad54c3714a12c2cef549dc9d58e97bcded193d928649cab78e9", size = 45811, upload-time = "2025-06-30T15:52:40.207Z" }, - { url = "https://files.pythonhosted.org/packages/c5/c9/2e3fe950db28fb7c62e1a5f46e1e38759b072e2089209bc033c2798bb5ec/multidict-6.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:c60b401f192e79caec61f166da9c924e9f8bc65548d4246842df91651e83d600", size = 43056, upload-time = "2025-06-30T15:52:41.575Z" }, - { url = "https://files.pythonhosted.org/packages/3a/58/aaf8114cf34966e084a8cc9517771288adb53465188843d5a19862cb6dc3/multidict-6.6.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:02fd8f32d403a6ff13864b0851f1f523d4c988051eea0471d4f1fd8010f11134", size = 82811, upload-time = "2025-06-30T15:52:43.281Z" }, - { url = "https://files.pythonhosted.org/packages/71/af/5402e7b58a1f5b987a07ad98f2501fdba2a4f4b4c30cf114e3ce8db64c87/multidict-6.6.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f3aa090106b1543f3f87b2041eef3c156c8da2aed90c63a2fbed62d875c49c37", size = 48304, upload-time = "2025-06-30T15:52:45.026Z" }, - { url = "https://files.pythonhosted.org/packages/39/65/ab3c8cafe21adb45b24a50266fd747147dec7847425bc2a0f6934b3ae9ce/multidict-6.6.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e924fb978615a5e33ff644cc42e6aa241effcf4f3322c09d4f8cebde95aff5f8", size = 46775, upload-time = "2025-06-30T15:52:46.459Z" }, - { url = "https://files.pythonhosted.org/packages/49/ba/9fcc1b332f67cc0c0c8079e263bfab6660f87fe4e28a35921771ff3eea0d/multidict-6.6.3-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:b9fe5a0e57c6dbd0e2ce81ca66272282c32cd11d31658ee9553849d91289e1c1", size = 229773, upload-time = "2025-06-30T15:52:47.88Z" }, - { url = "https://files.pythonhosted.org/packages/a4/14/0145a251f555f7c754ce2dcbcd012939bbd1f34f066fa5d28a50e722a054/multidict-6.6.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b24576f208793ebae00280c59927c3b7c2a3b1655e443a25f753c4611bc1c373", size = 250083, upload-time = "2025-06-30T15:52:49.366Z" }, - { url = "https://files.pythonhosted.org/packages/9e/d4/d5c0bd2bbb173b586c249a151a26d2fb3ec7d53c96e42091c9fef4e1f10c/multidict-6.6.3-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:135631cb6c58eac37d7ac0df380294fecdc026b28837fa07c02e459c7fb9c54e", size = 228980, upload-time = "2025-06-30T15:52:50.903Z" }, - { url = "https://files.pythonhosted.org/packages/21/32/c9a2d8444a50ec48c4733ccc67254100c10e1c8ae8e40c7a2d2183b59b97/multidict-6.6.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:274d416b0df887aef98f19f21578653982cfb8a05b4e187d4a17103322eeaf8f", size = 257776, upload-time = "2025-06-30T15:52:52.764Z" }, - { url = "https://files.pythonhosted.org/packages/68/d0/14fa1699f4ef629eae08ad6201c6b476098f5efb051b296f4c26be7a9fdf/multidict-6.6.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e252017a817fad7ce05cafbe5711ed40faeb580e63b16755a3a24e66fa1d87c0", size = 256882, upload-time = "2025-06-30T15:52:54.596Z" }, - { url = "https://files.pythonhosted.org/packages/da/88/84a27570fbe303c65607d517a5f147cd2fc046c2d1da02b84b17b9bdc2aa/multidict-6.6.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e4cc8d848cd4fe1cdee28c13ea79ab0ed37fc2e89dd77bac86a2e7959a8c3bc", size = 247816, upload-time = "2025-06-30T15:52:56.175Z" }, - { url = "https://files.pythonhosted.org/packages/1c/60/dca352a0c999ce96a5d8b8ee0b2b9f729dcad2e0b0c195f8286269a2074c/multidict-6.6.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9e236a7094b9c4c1b7585f6b9cca34b9d833cf079f7e4c49e6a4a6ec9bfdc68f", size = 245341, upload-time = "2025-06-30T15:52:57.752Z" }, - { url = "https://files.pythonhosted.org/packages/50/ef/433fa3ed06028f03946f3993223dada70fb700f763f70c00079533c34578/multidict-6.6.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:e0cb0ab69915c55627c933f0b555a943d98ba71b4d1c57bc0d0a66e2567c7471", size = 235854, upload-time = "2025-06-30T15:52:59.74Z" }, - { url = "https://files.pythonhosted.org/packages/1b/1f/487612ab56fbe35715320905215a57fede20de7db40a261759690dc80471/multidict-6.6.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:81ef2f64593aba09c5212a3d0f8c906a0d38d710a011f2f42759704d4557d3f2", size = 243432, upload-time = "2025-06-30T15:53:01.602Z" }, - { url = "https://files.pythonhosted.org/packages/da/6f/ce8b79de16cd885c6f9052c96a3671373d00c59b3ee635ea93e6e81b8ccf/multidict-6.6.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:b9cbc60010de3562545fa198bfc6d3825df430ea96d2cc509c39bd71e2e7d648", size = 252731, upload-time = "2025-06-30T15:53:03.517Z" }, - { url = "https://files.pythonhosted.org/packages/bb/fe/a2514a6aba78e5abefa1624ca85ae18f542d95ac5cde2e3815a9fbf369aa/multidict-6.6.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:70d974eaaa37211390cd02ef93b7e938de564bbffa866f0b08d07e5e65da783d", size = 247086, upload-time = "2025-06-30T15:53:05.48Z" }, - { url = "https://files.pythonhosted.org/packages/8c/22/b788718d63bb3cce752d107a57c85fcd1a212c6c778628567c9713f9345a/multidict-6.6.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3713303e4a6663c6d01d648a68f2848701001f3390a030edaaf3fc949c90bf7c", size = 243338, upload-time = "2025-06-30T15:53:07.522Z" }, - { url = "https://files.pythonhosted.org/packages/22/d6/fdb3d0670819f2228f3f7d9af613d5e652c15d170c83e5f1c94fbc55a25b/multidict-6.6.3-cp313-cp313t-win32.whl", hash = "sha256:639ecc9fe7cd73f2495f62c213e964843826f44505a3e5d82805aa85cac6f89e", size = 47812, upload-time = "2025-06-30T15:53:09.263Z" }, - { url = "https://files.pythonhosted.org/packages/b6/d6/a9d2c808f2c489ad199723197419207ecbfbc1776f6e155e1ecea9c883aa/multidict-6.6.3-cp313-cp313t-win_amd64.whl", hash = "sha256:9f97e181f344a0ef3881b573d31de8542cc0dbc559ec68c8f8b5ce2c2e91646d", size = 53011, upload-time = "2025-06-30T15:53:11.038Z" }, - { url = "https://files.pythonhosted.org/packages/f2/40/b68001cba8188dd267590a111f9661b6256debc327137667e832bf5d66e8/multidict-6.6.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ce8b7693da41a3c4fde5871c738a81490cea5496c671d74374c8ab889e1834fb", size = 45254, upload-time = "2025-06-30T15:53:12.421Z" }, - { url = "https://files.pythonhosted.org/packages/d8/30/9aec301e9772b098c1f5c0ca0279237c9766d94b97802e9888010c64b0ed/multidict-6.6.3-py3-none-any.whl", hash = "sha256:8db10f29c7541fc5da4defd8cd697e1ca429db743fa716325f236079b96f775a", size = 12313, upload-time = "2025-06-30T15:53:45.437Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6b/86f353088c1358e76fd30b0146947fddecee812703b604ee901e85cd2a80/multidict-6.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b8aa6f0bd8125ddd04a6593437bad6a7e70f300ff4180a531654aa2ab3f6d58f", size = 77054, upload-time = "2025-08-11T12:06:02.99Z" }, + { url = "https://files.pythonhosted.org/packages/19/5d/c01dc3d3788bb877bd7f5753ea6eb23c1beeca8044902a8f5bfb54430f63/multidict-6.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b9e5853bbd7264baca42ffc53391b490d65fe62849bf2c690fa3f6273dbcd0cb", size = 44914, upload-time = "2025-08-11T12:06:05.264Z" }, + { url = "https://files.pythonhosted.org/packages/46/44/964dae19ea42f7d3e166474d8205f14bb811020e28bc423d46123ddda763/multidict-6.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0af5f9dee472371e36d6ae38bde009bd8ce65ac7335f55dcc240379d7bed1495", size = 44601, upload-time = "2025-08-11T12:06:06.627Z" }, + { url = "https://files.pythonhosted.org/packages/31/20/0616348a1dfb36cb2ab33fc9521de1f27235a397bf3f59338e583afadd17/multidict-6.6.4-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:d24f351e4d759f5054b641c81e8291e5d122af0fca5c72454ff77f7cbe492de8", size = 224821, upload-time = "2025-08-11T12:06:08.06Z" }, + { url = "https://files.pythonhosted.org/packages/14/26/5d8923c69c110ff51861af05bd27ca6783011b96725d59ccae6d9daeb627/multidict-6.6.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db6a3810eec08280a172a6cd541ff4a5f6a97b161d93ec94e6c4018917deb6b7", size = 242608, upload-time = "2025-08-11T12:06:09.697Z" }, + { url = "https://files.pythonhosted.org/packages/5c/cc/e2ad3ba9459aa34fa65cf1f82a5c4a820a2ce615aacfb5143b8817f76504/multidict-6.6.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a1b20a9d56b2d81e2ff52ecc0670d583eaabaa55f402e8d16dd062373dbbe796", size = 222324, upload-time = "2025-08-11T12:06:10.905Z" }, + { url = "https://files.pythonhosted.org/packages/19/db/4ed0f65701afbc2cb0c140d2d02928bb0fe38dd044af76e58ad7c54fd21f/multidict-6.6.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8c9854df0eaa610a23494c32a6f44a3a550fb398b6b51a56e8c6b9b3689578db", size = 253234, upload-time = "2025-08-11T12:06:12.658Z" }, + { url = "https://files.pythonhosted.org/packages/94/c1/5160c9813269e39ae14b73debb907bfaaa1beee1762da8c4fb95df4764ed/multidict-6.6.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4bb7627fd7a968f41905a4d6343b0d63244a0623f006e9ed989fa2b78f4438a0", size = 251613, upload-time = "2025-08-11T12:06:13.97Z" }, + { url = "https://files.pythonhosted.org/packages/05/a9/48d1bd111fc2f8fb98b2ed7f9a115c55a9355358432a19f53c0b74d8425d/multidict-6.6.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caebafea30ed049c57c673d0b36238b1748683be2593965614d7b0e99125c877", size = 241649, upload-time = "2025-08-11T12:06:15.204Z" }, + { url = "https://files.pythonhosted.org/packages/85/2a/f7d743df0019408768af8a70d2037546a2be7b81fbb65f040d76caafd4c5/multidict-6.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ad887a8250eb47d3ab083d2f98db7f48098d13d42eb7a3b67d8a5c795f224ace", size = 239238, upload-time = "2025-08-11T12:06:16.467Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b8/4f4bb13323c2d647323f7919201493cf48ebe7ded971717bfb0f1a79b6bf/multidict-6.6.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:ed8358ae7d94ffb7c397cecb62cbac9578a83ecefc1eba27b9090ee910e2efb6", size = 233517, upload-time = "2025-08-11T12:06:18.107Z" }, + { url = "https://files.pythonhosted.org/packages/33/29/4293c26029ebfbba4f574febd2ed01b6f619cfa0d2e344217d53eef34192/multidict-6.6.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ecab51ad2462197a4c000b6d5701fc8585b80eecb90583635d7e327b7b6923eb", size = 243122, upload-time = "2025-08-11T12:06:19.361Z" }, + { url = "https://files.pythonhosted.org/packages/20/60/a1c53628168aa22447bfde3a8730096ac28086704a0d8c590f3b63388d0c/multidict-6.6.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c5c97aa666cf70e667dfa5af945424ba1329af5dd988a437efeb3a09430389fb", size = 248992, upload-time = "2025-08-11T12:06:20.661Z" }, + { url = "https://files.pythonhosted.org/packages/a3/3b/55443a0c372f33cae5d9ec37a6a973802884fa0ab3586659b197cf8cc5e9/multidict-6.6.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:9a950b7cf54099c1209f455ac5970b1ea81410f2af60ed9eb3c3f14f0bfcf987", size = 243708, upload-time = "2025-08-11T12:06:21.891Z" }, + { url = "https://files.pythonhosted.org/packages/7c/60/a18c6900086769312560b2626b18e8cca22d9e85b1186ba77f4755b11266/multidict-6.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:163c7ea522ea9365a8a57832dea7618e6cbdc3cd75f8c627663587459a4e328f", size = 237498, upload-time = "2025-08-11T12:06:23.206Z" }, + { url = "https://files.pythonhosted.org/packages/11/3d/8bdd8bcaff2951ce2affccca107a404925a2beafedd5aef0b5e4a71120a6/multidict-6.6.4-cp310-cp310-win32.whl", hash = "sha256:17d2cbbfa6ff20821396b25890f155f40c986f9cfbce5667759696d83504954f", size = 41415, upload-time = "2025-08-11T12:06:24.77Z" }, + { url = "https://files.pythonhosted.org/packages/c0/53/cab1ad80356a4cd1b685a254b680167059b433b573e53872fab245e9fc95/multidict-6.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:ce9a40fbe52e57e7edf20113a4eaddfacac0561a0879734e636aa6d4bb5e3fb0", size = 46046, upload-time = "2025-08-11T12:06:25.893Z" }, + { url = "https://files.pythonhosted.org/packages/cf/9a/874212b6f5c1c2d870d0a7adc5bb4cfe9b0624fa15cdf5cf757c0f5087ae/multidict-6.6.4-cp310-cp310-win_arm64.whl", hash = "sha256:01d0959807a451fe9fdd4da3e139cb5b77f7328baf2140feeaf233e1d777b729", size = 43147, upload-time = "2025-08-11T12:06:27.534Z" }, + { url = "https://files.pythonhosted.org/packages/6b/7f/90a7f01e2d005d6653c689039977f6856718c75c5579445effb7e60923d1/multidict-6.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c7a0e9b561e6460484318a7612e725df1145d46b0ef57c6b9866441bf6e27e0c", size = 76472, upload-time = "2025-08-11T12:06:29.006Z" }, + { url = "https://files.pythonhosted.org/packages/54/a3/bed07bc9e2bb302ce752f1dabc69e884cd6a676da44fb0e501b246031fdd/multidict-6.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6bf2f10f70acc7a2446965ffbc726e5fc0b272c97a90b485857e5c70022213eb", size = 44634, upload-time = "2025-08-11T12:06:30.374Z" }, + { url = "https://files.pythonhosted.org/packages/a7/4b/ceeb4f8f33cf81277da464307afeaf164fb0297947642585884f5cad4f28/multidict-6.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66247d72ed62d5dd29752ffc1d3b88f135c6a8de8b5f63b7c14e973ef5bda19e", size = 44282, upload-time = "2025-08-11T12:06:31.958Z" }, + { url = "https://files.pythonhosted.org/packages/03/35/436a5da8702b06866189b69f655ffdb8f70796252a8772a77815f1812679/multidict-6.6.4-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:105245cc6b76f51e408451a844a54e6823bbd5a490ebfe5bdfc79798511ceded", size = 229696, upload-time = "2025-08-11T12:06:33.087Z" }, + { url = "https://files.pythonhosted.org/packages/b6/0e/915160be8fecf1fca35f790c08fb74ca684d752fcba62c11daaf3d92c216/multidict-6.6.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cbbc54e58b34c3bae389ef00046be0961f30fef7cb0dd9c7756aee376a4f7683", size = 246665, upload-time = "2025-08-11T12:06:34.448Z" }, + { url = "https://files.pythonhosted.org/packages/08/ee/2f464330acd83f77dcc346f0b1a0eaae10230291450887f96b204b8ac4d3/multidict-6.6.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:56c6b3652f945c9bc3ac6c8178cd93132b8d82dd581fcbc3a00676c51302bc1a", size = 225485, upload-time = "2025-08-11T12:06:35.672Z" }, + { url = "https://files.pythonhosted.org/packages/71/cc/9a117f828b4d7fbaec6adeed2204f211e9caf0a012692a1ee32169f846ae/multidict-6.6.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b95494daf857602eccf4c18ca33337dd2be705bccdb6dddbfc9d513e6addb9d9", size = 257318, upload-time = "2025-08-11T12:06:36.98Z" }, + { url = "https://files.pythonhosted.org/packages/25/77/62752d3dbd70e27fdd68e86626c1ae6bccfebe2bb1f84ae226363e112f5a/multidict-6.6.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e5b1413361cef15340ab9dc61523e653d25723e82d488ef7d60a12878227ed50", size = 254689, upload-time = "2025-08-11T12:06:38.233Z" }, + { url = "https://files.pythonhosted.org/packages/00/6e/fac58b1072a6fc59af5e7acb245e8754d3e1f97f4f808a6559951f72a0d4/multidict-6.6.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e167bf899c3d724f9662ef00b4f7fef87a19c22b2fead198a6f68b263618df52", size = 246709, upload-time = "2025-08-11T12:06:39.517Z" }, + { url = "https://files.pythonhosted.org/packages/01/ef/4698d6842ef5e797c6db7744b0081e36fb5de3d00002cc4c58071097fac3/multidict-6.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:aaea28ba20a9026dfa77f4b80369e51cb767c61e33a2d4043399c67bd95fb7c6", size = 243185, upload-time = "2025-08-11T12:06:40.796Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c9/d82e95ae1d6e4ef396934e9b0e942dfc428775f9554acf04393cce66b157/multidict-6.6.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8c91cdb30809a96d9ecf442ec9bc45e8cfaa0f7f8bdf534e082c2443a196727e", size = 237838, upload-time = "2025-08-11T12:06:42.595Z" }, + { url = "https://files.pythonhosted.org/packages/57/cf/f94af5c36baaa75d44fab9f02e2a6bcfa0cd90acb44d4976a80960759dbc/multidict-6.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1a0ccbfe93ca114c5d65a2471d52d8829e56d467c97b0e341cf5ee45410033b3", size = 246368, upload-time = "2025-08-11T12:06:44.304Z" }, + { url = "https://files.pythonhosted.org/packages/4a/fe/29f23460c3d995f6a4b678cb2e9730e7277231b981f0b234702f0177818a/multidict-6.6.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:55624b3f321d84c403cb7d8e6e982f41ae233d85f85db54ba6286f7295dc8a9c", size = 253339, upload-time = "2025-08-11T12:06:45.597Z" }, + { url = "https://files.pythonhosted.org/packages/29/b6/fd59449204426187b82bf8a75f629310f68c6adc9559dc922d5abe34797b/multidict-6.6.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:4a1fb393a2c9d202cb766c76208bd7945bc194eba8ac920ce98c6e458f0b524b", size = 246933, upload-time = "2025-08-11T12:06:46.841Z" }, + { url = "https://files.pythonhosted.org/packages/19/52/d5d6b344f176a5ac3606f7a61fb44dc746e04550e1a13834dff722b8d7d6/multidict-6.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:43868297a5759a845fa3a483fb4392973a95fb1de891605a3728130c52b8f40f", size = 242225, upload-time = "2025-08-11T12:06:48.588Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d3/5b2281ed89ff4d5318d82478a2a2450fcdfc3300da48ff15c1778280ad26/multidict-6.6.4-cp311-cp311-win32.whl", hash = "sha256:ed3b94c5e362a8a84d69642dbeac615452e8af9b8eb825b7bc9f31a53a1051e2", size = 41306, upload-time = "2025-08-11T12:06:49.95Z" }, + { url = "https://files.pythonhosted.org/packages/74/7d/36b045c23a1ab98507aefd44fd8b264ee1dd5e5010543c6fccf82141ccef/multidict-6.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:d8c112f7a90d8ca5d20213aa41eac690bb50a76da153e3afb3886418e61cb22e", size = 46029, upload-time = "2025-08-11T12:06:51.082Z" }, + { url = "https://files.pythonhosted.org/packages/0f/5e/553d67d24432c5cd52b49047f2d248821843743ee6d29a704594f656d182/multidict-6.6.4-cp311-cp311-win_arm64.whl", hash = "sha256:3bb0eae408fa1996d87247ca0d6a57b7fc1dcf83e8a5c47ab82c558c250d4adf", size = 43017, upload-time = "2025-08-11T12:06:52.243Z" }, + { url = "https://files.pythonhosted.org/packages/05/f6/512ffd8fd8b37fb2680e5ac35d788f1d71bbaf37789d21a820bdc441e565/multidict-6.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0ffb87be160942d56d7b87b0fdf098e81ed565add09eaa1294268c7f3caac4c8", size = 76516, upload-time = "2025-08-11T12:06:53.393Z" }, + { url = "https://files.pythonhosted.org/packages/99/58/45c3e75deb8855c36bd66cc1658007589662ba584dbf423d01df478dd1c5/multidict-6.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d191de6cbab2aff5de6c5723101705fd044b3e4c7cfd587a1929b5028b9714b3", size = 45394, upload-time = "2025-08-11T12:06:54.555Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ca/e8c4472a93a26e4507c0b8e1f0762c0d8a32de1328ef72fd704ef9cc5447/multidict-6.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38a0956dd92d918ad5feff3db8fcb4a5eb7dba114da917e1a88475619781b57b", size = 43591, upload-time = "2025-08-11T12:06:55.672Z" }, + { url = "https://files.pythonhosted.org/packages/05/51/edf414f4df058574a7265034d04c935aa84a89e79ce90fcf4df211f47b16/multidict-6.6.4-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6865f6d3b7900ae020b495d599fcf3765653bc927951c1abb959017f81ae8287", size = 237215, upload-time = "2025-08-11T12:06:57.213Z" }, + { url = "https://files.pythonhosted.org/packages/c8/45/8b3d6dbad8cf3252553cc41abea09ad527b33ce47a5e199072620b296902/multidict-6.6.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a2088c126b6f72db6c9212ad827d0ba088c01d951cee25e758c450da732c138", size = 258299, upload-time = "2025-08-11T12:06:58.946Z" }, + { url = "https://files.pythonhosted.org/packages/3c/e8/8ca2e9a9f5a435fc6db40438a55730a4bf4956b554e487fa1b9ae920f825/multidict-6.6.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0f37bed7319b848097085d7d48116f545985db988e2256b2e6f00563a3416ee6", size = 242357, upload-time = "2025-08-11T12:07:00.301Z" }, + { url = "https://files.pythonhosted.org/packages/0f/84/80c77c99df05a75c28490b2af8f7cba2a12621186e0a8b0865d8e745c104/multidict-6.6.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:01368e3c94032ba6ca0b78e7ccb099643466cf24f8dc8eefcfdc0571d56e58f9", size = 268369, upload-time = "2025-08-11T12:07:01.638Z" }, + { url = "https://files.pythonhosted.org/packages/0d/e9/920bfa46c27b05fb3e1ad85121fd49f441492dca2449c5bcfe42e4565d8a/multidict-6.6.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe323540c255db0bffee79ad7f048c909f2ab0edb87a597e1c17da6a54e493c", size = 269341, upload-time = "2025-08-11T12:07:02.943Z" }, + { url = "https://files.pythonhosted.org/packages/af/65/753a2d8b05daf496f4a9c367fe844e90a1b2cac78e2be2c844200d10cc4c/multidict-6.6.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8eb3025f17b0a4c3cd08cda49acf312a19ad6e8a4edd9dbd591e6506d999402", size = 256100, upload-time = "2025-08-11T12:07:04.564Z" }, + { url = "https://files.pythonhosted.org/packages/09/54/655be13ae324212bf0bc15d665a4e34844f34c206f78801be42f7a0a8aaa/multidict-6.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bbc14f0365534d35a06970d6a83478b249752e922d662dc24d489af1aa0d1be7", size = 253584, upload-time = "2025-08-11T12:07:05.914Z" }, + { url = "https://files.pythonhosted.org/packages/5c/74/ab2039ecc05264b5cec73eb018ce417af3ebb384ae9c0e9ed42cb33f8151/multidict-6.6.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:75aa52fba2d96bf972e85451b99d8e19cc37ce26fd016f6d4aa60da9ab2b005f", size = 251018, upload-time = "2025-08-11T12:07:08.301Z" }, + { url = "https://files.pythonhosted.org/packages/af/0a/ccbb244ac848e56c6427f2392741c06302bbfba49c0042f1eb3c5b606497/multidict-6.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4fefd4a815e362d4f011919d97d7b4a1e566f1dde83dc4ad8cfb5b41de1df68d", size = 251477, upload-time = "2025-08-11T12:07:10.248Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b0/0ed49bba775b135937f52fe13922bc64a7eaf0a3ead84a36e8e4e446e096/multidict-6.6.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:db9801fe021f59a5b375ab778973127ca0ac52429a26e2fd86aa9508f4d26eb7", size = 263575, upload-time = "2025-08-11T12:07:11.928Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d9/7fb85a85e14de2e44dfb6a24f03c41e2af8697a6df83daddb0e9b7569f73/multidict-6.6.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a650629970fa21ac1fb06ba25dabfc5b8a2054fcbf6ae97c758aa956b8dba802", size = 259649, upload-time = "2025-08-11T12:07:13.244Z" }, + { url = "https://files.pythonhosted.org/packages/03/9e/b3a459bcf9b6e74fa461a5222a10ff9b544cb1cd52fd482fb1b75ecda2a2/multidict-6.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:452ff5da78d4720d7516a3a2abd804957532dd69296cb77319c193e3ffb87e24", size = 251505, upload-time = "2025-08-11T12:07:14.57Z" }, + { url = "https://files.pythonhosted.org/packages/86/a2/8022f78f041dfe6d71e364001a5cf987c30edfc83c8a5fb7a3f0974cff39/multidict-6.6.4-cp312-cp312-win32.whl", hash = "sha256:8c2fcb12136530ed19572bbba61b407f655e3953ba669b96a35036a11a485793", size = 41888, upload-time = "2025-08-11T12:07:15.904Z" }, + { url = "https://files.pythonhosted.org/packages/c7/eb/d88b1780d43a56db2cba24289fa744a9d216c1a8546a0dc3956563fd53ea/multidict-6.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:047d9425860a8c9544fed1b9584f0c8bcd31bcde9568b047c5e567a1025ecd6e", size = 46072, upload-time = "2025-08-11T12:07:17.045Z" }, + { url = "https://files.pythonhosted.org/packages/9f/16/b929320bf5750e2d9d4931835a4c638a19d2494a5b519caaaa7492ebe105/multidict-6.6.4-cp312-cp312-win_arm64.whl", hash = "sha256:14754eb72feaa1e8ae528468f24250dd997b8e2188c3d2f593f9eba259e4b364", size = 43222, upload-time = "2025-08-11T12:07:18.328Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5d/e1db626f64f60008320aab00fbe4f23fc3300d75892a3381275b3d284580/multidict-6.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f46a6e8597f9bd71b31cc708195d42b634c8527fecbcf93febf1052cacc1f16e", size = 75848, upload-time = "2025-08-11T12:07:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/4c/aa/8b6f548d839b6c13887253af4e29c939af22a18591bfb5d0ee6f1931dae8/multidict-6.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:22e38b2bc176c5eb9c0a0e379f9d188ae4cd8b28c0f53b52bce7ab0a9e534657", size = 45060, upload-time = "2025-08-11T12:07:21.163Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c6/f5e97e5d99a729bc2aa58eb3ebfa9f1e56a9b517cc38c60537c81834a73f/multidict-6.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5df8afd26f162da59e218ac0eefaa01b01b2e6cd606cffa46608f699539246da", size = 43269, upload-time = "2025-08-11T12:07:22.392Z" }, + { url = "https://files.pythonhosted.org/packages/dc/31/d54eb0c62516776f36fe67f84a732f97e0b0e12f98d5685bebcc6d396910/multidict-6.6.4-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:49517449b58d043023720aa58e62b2f74ce9b28f740a0b5d33971149553d72aa", size = 237158, upload-time = "2025-08-11T12:07:23.636Z" }, + { url = "https://files.pythonhosted.org/packages/c4/1c/8a10c1c25b23156e63b12165a929d8eb49a6ed769fdbefb06e6f07c1e50d/multidict-6.6.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae9408439537c5afdca05edd128a63f56a62680f4b3c234301055d7a2000220f", size = 257076, upload-time = "2025-08-11T12:07:25.049Z" }, + { url = "https://files.pythonhosted.org/packages/ad/86/90e20b5771d6805a119e483fd3d1e8393e745a11511aebca41f0da38c3e2/multidict-6.6.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:87a32d20759dc52a9e850fe1061b6e41ab28e2998d44168a8a341b99ded1dba0", size = 240694, upload-time = "2025-08-11T12:07:26.458Z" }, + { url = "https://files.pythonhosted.org/packages/e7/49/484d3e6b535bc0555b52a0a26ba86e4d8d03fd5587d4936dc59ba7583221/multidict-6.6.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:52e3c8d43cdfff587ceedce9deb25e6ae77daba560b626e97a56ddcad3756879", size = 266350, upload-time = "2025-08-11T12:07:27.94Z" }, + { url = "https://files.pythonhosted.org/packages/bf/b4/aa4c5c379b11895083d50021e229e90c408d7d875471cb3abf721e4670d6/multidict-6.6.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ad8850921d3a8d8ff6fbef790e773cecfc260bbfa0566998980d3fa8f520bc4a", size = 267250, upload-time = "2025-08-11T12:07:29.303Z" }, + { url = "https://files.pythonhosted.org/packages/80/e5/5e22c5bf96a64bdd43518b1834c6d95a4922cc2066b7d8e467dae9b6cee6/multidict-6.6.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:497a2954adc25c08daff36f795077f63ad33e13f19bfff7736e72c785391534f", size = 254900, upload-time = "2025-08-11T12:07:30.764Z" }, + { url = "https://files.pythonhosted.org/packages/17/38/58b27fed927c07035abc02befacab42491e7388ca105e087e6e0215ead64/multidict-6.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:024ce601f92d780ca1617ad4be5ac15b501cc2414970ffa2bb2bbc2bd5a68fa5", size = 252355, upload-time = "2025-08-11T12:07:32.205Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a1/dad75d23a90c29c02b5d6f3d7c10ab36c3197613be5d07ec49c7791e186c/multidict-6.6.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a693fc5ed9bdd1c9e898013e0da4dcc640de7963a371c0bd458e50e046bf6438", size = 250061, upload-time = "2025-08-11T12:07:33.623Z" }, + { url = "https://files.pythonhosted.org/packages/b8/1a/ac2216b61c7f116edab6dc3378cca6c70dc019c9a457ff0d754067c58b20/multidict-6.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:190766dac95aab54cae5b152a56520fd99298f32a1266d66d27fdd1b5ac00f4e", size = 249675, upload-time = "2025-08-11T12:07:34.958Z" }, + { url = "https://files.pythonhosted.org/packages/d4/79/1916af833b800d13883e452e8e0977c065c4ee3ab7a26941fbfdebc11895/multidict-6.6.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:34d8f2a5ffdceab9dcd97c7a016deb2308531d5f0fced2bb0c9e1df45b3363d7", size = 261247, upload-time = "2025-08-11T12:07:36.588Z" }, + { url = "https://files.pythonhosted.org/packages/c5/65/d1f84fe08ac44a5fc7391cbc20a7cedc433ea616b266284413fd86062f8c/multidict-6.6.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:59e8d40ab1f5a8597abcef00d04845155a5693b5da00d2c93dbe88f2050f2812", size = 257960, upload-time = "2025-08-11T12:07:39.735Z" }, + { url = "https://files.pythonhosted.org/packages/13/b5/29ec78057d377b195ac2c5248c773703a6b602e132a763e20ec0457e7440/multidict-6.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:467fe64138cfac771f0e949b938c2e1ada2b5af22f39692aa9258715e9ea613a", size = 250078, upload-time = "2025-08-11T12:07:41.525Z" }, + { url = "https://files.pythonhosted.org/packages/c4/0e/7e79d38f70a872cae32e29b0d77024bef7834b0afb406ddae6558d9e2414/multidict-6.6.4-cp313-cp313-win32.whl", hash = "sha256:14616a30fe6d0a48d0a48d1a633ab3b8bec4cf293aac65f32ed116f620adfd69", size = 41708, upload-time = "2025-08-11T12:07:43.405Z" }, + { url = "https://files.pythonhosted.org/packages/9d/34/746696dffff742e97cd6a23da953e55d0ea51fa601fa2ff387b3edcfaa2c/multidict-6.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:40cd05eaeb39e2bc8939451f033e57feaa2ac99e07dbca8afe2be450a4a3b6cf", size = 45912, upload-time = "2025-08-11T12:07:45.082Z" }, + { url = "https://files.pythonhosted.org/packages/c7/87/3bac136181e271e29170d8d71929cdeddeb77f3e8b6a0c08da3a8e9da114/multidict-6.6.4-cp313-cp313-win_arm64.whl", hash = "sha256:f6eb37d511bfae9e13e82cb4d1af36b91150466f24d9b2b8a9785816deb16605", size = 43076, upload-time = "2025-08-11T12:07:46.746Z" }, + { url = "https://files.pythonhosted.org/packages/64/94/0a8e63e36c049b571c9ae41ee301ada29c3fee9643d9c2548d7d558a1d99/multidict-6.6.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:6c84378acd4f37d1b507dfa0d459b449e2321b3ba5f2338f9b085cf7a7ba95eb", size = 82812, upload-time = "2025-08-11T12:07:48.402Z" }, + { url = "https://files.pythonhosted.org/packages/25/1a/be8e369dfcd260d2070a67e65dd3990dd635cbd735b98da31e00ea84cd4e/multidict-6.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0e0558693063c75f3d952abf645c78f3c5dfdd825a41d8c4d8156fc0b0da6e7e", size = 48313, upload-time = "2025-08-11T12:07:49.679Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/dd4ade298674b2f9a7b06a32c94ffbc0497354df8285f27317c66433ce3b/multidict-6.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3f8e2384cb83ebd23fd07e9eada8ba64afc4c759cd94817433ab8c81ee4b403f", size = 46777, upload-time = "2025-08-11T12:07:51.318Z" }, + { url = "https://files.pythonhosted.org/packages/89/db/98aa28bc7e071bfba611ac2ae803c24e96dd3a452b4118c587d3d872c64c/multidict-6.6.4-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f996b87b420995a9174b2a7c1a8daf7db4750be6848b03eb5e639674f7963773", size = 229321, upload-time = "2025-08-11T12:07:52.965Z" }, + { url = "https://files.pythonhosted.org/packages/c7/bc/01ddda2a73dd9d167bd85d0e8ef4293836a8f82b786c63fb1a429bc3e678/multidict-6.6.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc356250cffd6e78416cf5b40dc6a74f1edf3be8e834cf8862d9ed5265cf9b0e", size = 249954, upload-time = "2025-08-11T12:07:54.423Z" }, + { url = "https://files.pythonhosted.org/packages/06/78/6b7c0f020f9aa0acf66d0ab4eb9f08375bac9a50ff5e3edb1c4ccd59eafc/multidict-6.6.4-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:dadf95aa862714ea468a49ad1e09fe00fcc9ec67d122f6596a8d40caf6cec7d0", size = 228612, upload-time = "2025-08-11T12:07:55.914Z" }, + { url = "https://files.pythonhosted.org/packages/00/44/3faa416f89b2d5d76e9d447296a81521e1c832ad6e40b92f990697b43192/multidict-6.6.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7dd57515bebffd8ebd714d101d4c434063322e4fe24042e90ced41f18b6d3395", size = 257528, upload-time = "2025-08-11T12:07:57.371Z" }, + { url = "https://files.pythonhosted.org/packages/05/5f/77c03b89af0fcb16f018f668207768191fb9dcfb5e3361a5e706a11db2c9/multidict-6.6.4-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:967af5f238ebc2eb1da4e77af5492219fbd9b4b812347da39a7b5f5c72c0fa45", size = 256329, upload-time = "2025-08-11T12:07:58.844Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e9/ed750a2a9afb4f8dc6f13dc5b67b514832101b95714f1211cd42e0aafc26/multidict-6.6.4-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a4c6875c37aae9794308ec43e3530e4aa0d36579ce38d89979bbf89582002bb", size = 247928, upload-time = "2025-08-11T12:08:01.037Z" }, + { url = "https://files.pythonhosted.org/packages/1f/b5/e0571bc13cda277db7e6e8a532791d4403dacc9850006cb66d2556e649c0/multidict-6.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7f683a551e92bdb7fac545b9c6f9fa2aebdeefa61d607510b3533286fcab67f5", size = 245228, upload-time = "2025-08-11T12:08:02.96Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a3/69a84b0eccb9824491f06368f5b86e72e4af54c3067c37c39099b6687109/multidict-6.6.4-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:3ba5aaf600edaf2a868a391779f7a85d93bed147854925f34edd24cc70a3e141", size = 235869, upload-time = "2025-08-11T12:08:04.746Z" }, + { url = "https://files.pythonhosted.org/packages/a9/9d/28802e8f9121a6a0804fa009debf4e753d0a59969ea9f70be5f5fdfcb18f/multidict-6.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:580b643b7fd2c295d83cad90d78419081f53fd532d1f1eb67ceb7060f61cff0d", size = 243446, upload-time = "2025-08-11T12:08:06.332Z" }, + { url = "https://files.pythonhosted.org/packages/38/ea/6c98add069b4878c1d66428a5f5149ddb6d32b1f9836a826ac764b9940be/multidict-6.6.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:37b7187197da6af3ee0b044dbc9625afd0c885f2800815b228a0e70f9a7f473d", size = 252299, upload-time = "2025-08-11T12:08:07.931Z" }, + { url = "https://files.pythonhosted.org/packages/3a/09/8fe02d204473e14c0af3affd50af9078839dfca1742f025cca765435d6b4/multidict-6.6.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e1b93790ed0bc26feb72e2f08299691ceb6da5e9e14a0d13cc74f1869af327a0", size = 246926, upload-time = "2025-08-11T12:08:09.467Z" }, + { url = "https://files.pythonhosted.org/packages/37/3d/7b1e10d774a6df5175ecd3c92bff069e77bed9ec2a927fdd4ff5fe182f67/multidict-6.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a506a77ddee1efcca81ecbeae27ade3e09cdf21a8ae854d766c2bb4f14053f92", size = 243383, upload-time = "2025-08-11T12:08:10.981Z" }, + { url = "https://files.pythonhosted.org/packages/50/b0/a6fae46071b645ae98786ab738447de1ef53742eaad949f27e960864bb49/multidict-6.6.4-cp313-cp313t-win32.whl", hash = "sha256:f93b2b2279883d1d0a9e1bd01f312d6fc315c5e4c1f09e112e4736e2f650bc4e", size = 47775, upload-time = "2025-08-11T12:08:12.439Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0a/2436550b1520091af0600dff547913cb2d66fbac27a8c33bc1b1bccd8d98/multidict-6.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:6d46a180acdf6e87cc41dc15d8f5c2986e1e8739dc25dbb7dac826731ef381a4", size = 53100, upload-time = "2025-08-11T12:08:13.823Z" }, + { url = "https://files.pythonhosted.org/packages/97/ea/43ac51faff934086db9c072a94d327d71b7d8b40cd5dcb47311330929ef0/multidict-6.6.4-cp313-cp313t-win_arm64.whl", hash = "sha256:756989334015e3335d087a27331659820d53ba432befdef6a718398b0a8493ad", size = 45501, upload-time = "2025-08-11T12:08:15.173Z" }, + { url = "https://files.pythonhosted.org/packages/fd/69/b547032297c7e63ba2af494edba695d781af8a0c6e89e4d06cf848b21d80/multidict-6.6.4-py3-none-any.whl", hash = "sha256:27d8f8e125c07cb954e54d75d04905a9bba8a439c1d84aca94949d4d03d8601c", size = 12313, upload-time = "2025-08-11T12:08:46.891Z" }, ] [[package]] @@ -1840,7 +1843,7 @@ wheels = [ [[package]] name = "openai" -version = "1.99.3" +version = "1.99.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -1852,9 +1855,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/72/d3/c372420c8ca1c60e785fd8c19e536cea8f16b0cfdcdad6458e1d8884f2ea/openai-1.99.3.tar.gz", hash = "sha256:1a0e2910e4545d828c14218f2ac3276827c94a043f5353e43b9413b38b497897", size = 504932, upload-time = "2025-08-07T20:35:15.893Z" } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/92/bc/e52f49940b4e320629da7db09c90a2407a48c612cff397b4b41b7e58cdf9/openai-1.99.3-py3-none-any.whl", hash = "sha256:c786a03f6cddadb5ee42c6d749aa4f6134fe14fdd7d69a667e5e7ce7fd29a719", size = 785776, upload-time = "2025-08-07T20:35:13.653Z" }, + { 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" }, ] [[package]] @@ -2018,7 +2021,7 @@ wheels = [ [[package]] name = "pre-commit" -version = "4.2.0" +version = "4.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cfgv", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -2027,9 +2030,9 @@ dependencies = [ { name = "pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "virtualenv", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424, upload-time = "2025-03-18T21:35:20.987Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/29/7cf5bbc236333876e4b41f56e06857a87937ce4bf91e117a6991a2dbb02a/pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16", size = 193792, upload-time = "2025-08-09T18:56:14.651Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707, upload-time = "2025-03-18T21:35:19.343Z" }, + { url = "https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8", size = 220965, upload-time = "2025-08-09T18:56:13.192Z" }, ] [[package]] @@ -3147,21 +3150,21 @@ wheels = [ [[package]] name = "tornado" -version = "6.5.1" +version = "6.5.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/51/89/c72771c81d25d53fe33e3dca61c233b665b2780f21820ba6fd2c6793c12b/tornado-6.5.1.tar.gz", hash = "sha256:84ceece391e8eb9b2b95578db65e920d2a61070260594819589609ba9bc6308c", size = 509934, upload-time = "2025-05-22T18:15:38.788Z" } +sdist = { url = "https://files.pythonhosted.org/packages/09/ce/1eb500eae19f4648281bb2186927bb062d2438c2e5093d1360391afd2f90/tornado-6.5.2.tar.gz", hash = "sha256:ab53c8f9a0fa351e2c0741284e06c7a45da86afb544133201c5cc8578eb076a0", size = 510821, upload-time = "2025-08-08T18:27:00.78Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/89/f4532dee6843c9e0ebc4e28d4be04c67f54f60813e4bf73d595fe7567452/tornado-6.5.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d50065ba7fd11d3bd41bcad0825227cc9a95154bad83239357094c36708001f7", size = 441948, upload-time = "2025-05-22T18:15:20.862Z" }, - { url = "https://files.pythonhosted.org/packages/15/9a/557406b62cffa395d18772e0cdcf03bed2fff03b374677348eef9f6a3792/tornado-6.5.1-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9e9ca370f717997cb85606d074b0e5b247282cf5e2e1611568b8821afe0342d6", size = 440112, upload-time = "2025-05-22T18:15:22.591Z" }, - { url = "https://files.pythonhosted.org/packages/55/82/7721b7319013a3cf881f4dffa4f60ceff07b31b394e459984e7a36dc99ec/tornado-6.5.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b77e9dfa7ed69754a54c89d82ef746398be82f749df69c4d3abe75c4d1ff4888", size = 443672, upload-time = "2025-05-22T18:15:24.027Z" }, - { url = "https://files.pythonhosted.org/packages/7d/42/d11c4376e7d101171b94e03cef0cbce43e823ed6567ceda571f54cf6e3ce/tornado-6.5.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:253b76040ee3bab8bcf7ba9feb136436a3787208717a1fb9f2c16b744fba7331", size = 443019, upload-time = "2025-05-22T18:15:25.735Z" }, - { url = "https://files.pythonhosted.org/packages/7d/f7/0c48ba992d875521ac761e6e04b0a1750f8150ae42ea26df1852d6a98942/tornado-6.5.1-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:308473f4cc5a76227157cdf904de33ac268af770b2c5f05ca6c1161d82fdd95e", size = 443252, upload-time = "2025-05-22T18:15:27.499Z" }, - { url = "https://files.pythonhosted.org/packages/89/46/d8d7413d11987e316df4ad42e16023cd62666a3c0dfa1518ffa30b8df06c/tornado-6.5.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:caec6314ce8a81cf69bd89909f4b633b9f523834dc1a352021775d45e51d9401", size = 443930, upload-time = "2025-05-22T18:15:29.299Z" }, - { url = "https://files.pythonhosted.org/packages/78/b2/f8049221c96a06df89bed68260e8ca94beca5ea532ffc63b1175ad31f9cc/tornado-6.5.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:13ce6e3396c24e2808774741331638ee6c2f50b114b97a55c5b442df65fd9692", size = 443351, upload-time = "2025-05-22T18:15:31.038Z" }, - { url = "https://files.pythonhosted.org/packages/76/ff/6a0079e65b326cc222a54720a748e04a4db246870c4da54ece4577bfa702/tornado-6.5.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5cae6145f4cdf5ab24744526cc0f55a17d76f02c98f4cff9daa08ae9a217448a", size = 443328, upload-time = "2025-05-22T18:15:32.426Z" }, - { url = "https://files.pythonhosted.org/packages/49/18/e3f902a1d21f14035b5bc6246a8c0f51e0eef562ace3a2cea403c1fb7021/tornado-6.5.1-cp39-abi3-win32.whl", hash = "sha256:e0a36e1bc684dca10b1aa75a31df8bdfed656831489bc1e6a6ebed05dc1ec365", size = 444396, upload-time = "2025-05-22T18:15:34.205Z" }, - { url = "https://files.pythonhosted.org/packages/7b/09/6526e32bf1049ee7de3bebba81572673b19a2a8541f795d887e92af1a8bc/tornado-6.5.1-cp39-abi3-win_amd64.whl", hash = "sha256:908e7d64567cecd4c2b458075589a775063453aeb1d2a1853eedb806922f568b", size = 444840, upload-time = "2025-05-22T18:15:36.1Z" }, - { url = "https://files.pythonhosted.org/packages/55/a7/535c44c7bea4578e48281d83c615219f3ab19e6abc67625ef637c73987be/tornado-6.5.1-cp39-abi3-win_arm64.whl", hash = "sha256:02420a0eb7bf617257b9935e2b754d1b63897525d8a289c9d65690d580b4dcf7", size = 443596, upload-time = "2025-05-22T18:15:37.433Z" }, + { url = "https://files.pythonhosted.org/packages/f6/48/6a7529df2c9cc12efd2e8f5dd219516184d703b34c06786809670df5b3bd/tornado-6.5.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:2436822940d37cde62771cff8774f4f00b3c8024fe482e16ca8387b8a2724db6", size = 442563, upload-time = "2025-08-08T18:26:42.945Z" }, + { url = "https://files.pythonhosted.org/packages/f2/b5/9b575a0ed3e50b00c40b08cbce82eb618229091d09f6d14bce80fc01cb0b/tornado-6.5.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:583a52c7aa94ee046854ba81d9ebb6c81ec0fd30386d96f7640c96dad45a03ef", size = 440729, upload-time = "2025-08-08T18:26:44.473Z" }, + { url = "https://files.pythonhosted.org/packages/1b/4e/619174f52b120efcf23633c817fd3fed867c30bff785e2cd5a53a70e483c/tornado-6.5.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0fe179f28d597deab2842b86ed4060deec7388f1fd9c1b4a41adf8af058907e", size = 444295, upload-time = "2025-08-08T18:26:46.021Z" }, + { url = "https://files.pythonhosted.org/packages/95/fa/87b41709552bbd393c85dd18e4e3499dcd8983f66e7972926db8d96aa065/tornado-6.5.2-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b186e85d1e3536d69583d2298423744740986018e393d0321df7340e71898882", size = 443644, upload-time = "2025-08-08T18:26:47.625Z" }, + { url = "https://files.pythonhosted.org/packages/f9/41/fb15f06e33d7430ca89420283a8762a4e6b8025b800ea51796ab5e6d9559/tornado-6.5.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e792706668c87709709c18b353da1f7662317b563ff69f00bab83595940c7108", size = 443878, upload-time = "2025-08-08T18:26:50.599Z" }, + { url = "https://files.pythonhosted.org/packages/11/92/fe6d57da897776ad2e01e279170ea8ae726755b045fe5ac73b75357a5a3f/tornado-6.5.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:06ceb1300fd70cb20e43b1ad8aaee0266e69e7ced38fa910ad2e03285009ce7c", size = 444549, upload-time = "2025-08-08T18:26:51.864Z" }, + { url = "https://files.pythonhosted.org/packages/9b/02/c8f4f6c9204526daf3d760f4aa555a7a33ad0e60843eac025ccfd6ff4a93/tornado-6.5.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:74db443e0f5251be86cbf37929f84d8c20c27a355dd452a5cfa2aada0d001ec4", size = 443973, upload-time = "2025-08-08T18:26:53.625Z" }, + { url = "https://files.pythonhosted.org/packages/ae/2d/f5f5707b655ce2317190183868cd0f6822a1121b4baeae509ceb9590d0bd/tornado-6.5.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b5e735ab2889d7ed33b32a459cac490eda71a1ba6857b0118de476ab6c366c04", size = 443954, upload-time = "2025-08-08T18:26:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/e8/59/593bd0f40f7355806bf6573b47b8c22f8e1374c9b6fd03114bd6b7a3dcfd/tornado-6.5.2-cp39-abi3-win32.whl", hash = "sha256:c6f29e94d9b37a95013bb669616352ddb82e3bfe8326fccee50583caebc8a5f0", size = 445023, upload-time = "2025-08-08T18:26:56.677Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2a/f609b420c2f564a748a2d80ebfb2ee02a73ca80223af712fca591386cafb/tornado-6.5.2-cp39-abi3-win_amd64.whl", hash = "sha256:e56a5af51cc30dd2cae649429af65ca2f6571da29504a07995175df14c18f35f", size = 445427, upload-time = "2025-08-08T18:26:57.91Z" }, + { url = "https://files.pythonhosted.org/packages/5e/4f/e1f65e8f8c76d73658b33d33b81eed4322fb5085350e4328d5c956f0c8f9/tornado-6.5.2-cp39-abi3-win_arm64.whl", hash = "sha256:d6c33dc3672e3a1f3618eb63b7ef4683a7688e7b9e6e8f0d9aa5726360a004af", size = 444456, upload-time = "2025-08-08T18:26:59.207Z" }, ] [[package]] @@ -3226,28 +3229,28 @@ wheels = [ [[package]] name = "uv" -version = "0.8.6" +version = "0.8.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b5/3b/1140dbbca9fb3ca32be38e01c670a5980a4ee4874366d70438317876d40a/uv-0.8.6.tar.gz", hash = "sha256:4d4e042f6bd9f143094051a05de758684028f451e563846cbc0c6f505b530cca", size = 3463644, upload-time = "2025-08-07T15:43:34.206Z" } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/71/64/a96f40f95626c6e353e66f6bc5a5ca7c1399e95caf0dcb56cae38754e073/uv-0.8.6-py3-none-linux_armv6l.whl", hash = "sha256:d96ff3a1d06a6a00ed94dfb2996228153b3b5bfc892174b7556216ab872a91b1", size = 18437310, upload-time = "2025-08-07T15:42:49.611Z" }, - { url = "https://files.pythonhosted.org/packages/41/30/b2fed99d5a6b16410669f223767f6d65bc6595858622f5f36386892ed963/uv-0.8.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fdceb1ef554df0ddc620bfe83fdcf740829e489c62f78ba1f089abd62c71c63e", size = 18615884, upload-time = "2025-08-07T15:42:53.452Z" }, - { url = "https://files.pythonhosted.org/packages/d7/82/a53684eadb9cb169eab32ab71f2bdaf7c382819d6de44d4e8df91ca14a00/uv-0.8.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7c1f48279ff61940143c78b969094e13324988eabcfcd4799f4350d9d36c1d48", size = 17173005, upload-time = "2025-08-07T15:42:55.571Z" }, - { url = "https://files.pythonhosted.org/packages/e7/4a/2890d9ccaf4b383fea43ae6362252870dcd97dda7412f34f20d80ccf7a39/uv-0.8.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:1913f5627c57076c88dd38b0173bdb006ae9b8dbd92b1798a1acc9d744c1a7cc", size = 17813305, upload-time = "2025-08-07T15:42:57.998Z" }, - { url = "https://files.pythonhosted.org/packages/9b/c3/33a10049728ffbcde673b75b9a73cd61bfab5e1598d935d1f1b2556b07a4/uv-0.8.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7796acc3c5b84d5ee5e10cc6cf92eb61c19f6551855d0aa89ef5925e4a371fbf", size = 18159834, upload-time = "2025-08-07T15:43:00.207Z" }, - { url = "https://files.pythonhosted.org/packages/81/28/ff884f7007a6b9d0e3368dbe4ae7d28acacbaaf1b3a583640e5af6dc5360/uv-0.8.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57a98367bfad38e870e1a8a6626464796ffcee6e937d429fbd7b25ddf46bb36f", size = 18954223, upload-time = "2025-08-07T15:43:03.577Z" }, - { url = "https://files.pythonhosted.org/packages/78/1d/a4ed2da913ecacc1c976e97dff905979c13359834eeeac8bbaf5ed0b2fca/uv-0.8.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2ac28509db2e52613a59264bdb150d13274ed13e5b305f7e274da8cd83033985", size = 20215802, upload-time = "2025-08-07T15:43:06.181Z" }, - { url = "https://files.pythonhosted.org/packages/2c/12/c9ca1cc8bdbecd54db4a7c1a44808f15271da60838dfa9f180ce8171407a/uv-0.8.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:deab2ce32d2dd7a1c0de459aa23470c60feb0ea24e67c9c5c5988d8bf4eb4a09", size = 19898210, upload-time = "2025-08-07T15:43:09.008Z" }, - { url = "https://files.pythonhosted.org/packages/c0/15/e10347768b2929ae9c65abbfd0867a736e6227f6d63da1f86fe6bdcbcdca/uv-0.8.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b201ebc1c5c76c3a415fa4edcb25a0e06263d2255319d6d52275c775e926e23", size = 19247208, upload-time = "2025-08-07T15:43:11.578Z" }, - { url = "https://files.pythonhosted.org/packages/62/8d/dc290df05d1820d003f30e2fb7853496eec43bcb986c5e35aaea2f5343d3/uv-0.8.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6acdc77099906ba64bc1b725bef973c10905d7e9596d1b25f271db772bc9e8e4", size = 19261881, upload-time = "2025-08-07T15:43:13.815Z" }, - { url = "https://files.pythonhosted.org/packages/20/bd/6c3b9c87e4ed323f72de6ece7d51a6179091f0ff6e0c9c6ed29e28efe17c/uv-0.8.6-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:4e81380549151e34ae96d56499438444ba58591ca9f2fc6ba0a867152601849e", size = 18037135, upload-time = "2025-08-07T15:43:15.941Z" }, - { url = "https://files.pythonhosted.org/packages/7d/e1/b3e825ad9cc3f03f0f3e232286f91aef985d8029db69fd7091c2f332212b/uv-0.8.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:c9de4adac36a62e4bddd959ce65fb4bb09b0cbfd95946d50390f2a9c186ecb9c", size = 19040739, upload-time = "2025-08-07T15:43:18.092Z" }, - { url = "https://files.pythonhosted.org/packages/c5/14/921e2e7b2a4be0bac17f9d04a126546b89828bb33aa56368af7f00538fe3/uv-0.8.6-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:993af2c295856c5ca053678a8dadc11ce2f85485513ed1568c16e98d5dfa88bf", size = 18060742, upload-time = "2025-08-07T15:43:20.39Z" }, - { url = "https://files.pythonhosted.org/packages/81/54/0b1ecc64353725b62f02d3739a67a567faa70c76c4ea19a21253df1c4d99/uv-0.8.6-py3-none-musllinux_1_1_i686.whl", hash = "sha256:132e73f1e9fe05edc6c06c00416f7c721c48298786fd7293be6c584793170bbc", size = 18430300, upload-time = "2025-08-07T15:43:22.797Z" }, - { url = "https://files.pythonhosted.org/packages/da/be/a1a249eacb9b1e397292106250490ec1546a90c0e19de19f0b36f52aecea/uv-0.8.6-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:ee67acf1b211be2cfbeaec16cde13c8325810d32ff85963a9dedd1f9d7c61ef7", size = 19407124, upload-time = "2025-08-07T15:43:25.915Z" }, - { url = "https://files.pythonhosted.org/packages/11/18/552bb94bb931ea9d09a0e98e5c3d8cefc8c8db25549af88d1484e52d6cdd/uv-0.8.6-py3-none-win32.whl", hash = "sha256:e35cc1ef79d3dce2b6aeffbfb280d02d5ad741d4ca07874bdf0a4d85c841d9de", size = 18324229, upload-time = "2025-08-07T15:43:28.029Z" }, - { url = "https://files.pythonhosted.org/packages/fd/df/b7d1171579e2cc821aafc38a86393104e5426ac1ebc4e95be79ac705a11f/uv-0.8.6-py3-none-win_amd64.whl", hash = "sha256:37227aaf1e41c7eda3d7f0028e747a2a2eed3f3506b0adc121a4366e8281115b", size = 20279856, upload-time = "2025-08-07T15:43:30.07Z" }, - { url = "https://files.pythonhosted.org/packages/09/1b/2629d605e101db6a52397e6ea8859a51af0207cf254051b2a621c683ee07/uv-0.8.6-py3-none-win_arm64.whl", hash = "sha256:0b524de39f317bd8733c38cf100b6f8091d44e06b23f7752523ad1ad1454ede3", size = 18839643, upload-time = "2025-08-07T15:43:32.332Z" }, + { 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" }, ] [[package]]