From c251ef95be9df0d68020f809415e4a6ec15cfefb Mon Sep 17 00:00:00 2001 From: Evan Mattson Date: Mon, 11 Nov 2024 13:57:35 -0500 Subject: [PATCH 1/9] Allow RestApiOperation to use multiple defined server uris. --- .../openai_authentication_config.py | 4 ++ .../openai_function_execution_parameters.py | 3 + .../connectors/openai_plugin/openai_utils.py | 3 + .../models/rest_api_operation.py | 22 +++--- .../openapi_plugin/openapi_parser.py | 8 +-- .../functions/kernel_function_extension.py | 5 ++ .../connectors/openapi_plugin/openapi.yaml | 1 + .../openapi_plugin/test_sk_openapi.py | 72 +++++++++---------- 8 files changed, 66 insertions(+), 52 deletions(-) diff --git a/python/semantic_kernel/connectors/openai_plugin/openai_authentication_config.py b/python/semantic_kernel/connectors/openai_plugin/openai_authentication_config.py index fddb9a722a1a..ce1fbfac87ae 100644 --- a/python/semantic_kernel/connectors/openai_plugin/openai_authentication_config.py +++ b/python/semantic_kernel/connectors/openai_plugin/openai_authentication_config.py @@ -4,10 +4,12 @@ from enum import Enum from pydantic import HttpUrl +from typing_extensions import deprecated from semantic_kernel.kernel_pydantic import KernelBaseModel +@deprecated("The `OpenAIAuthenticationType` class is deprecated; use OpenAPI Plugins instead.", category=None) class OpenAIAuthenticationType(str, Enum): """OpenAI authentication types.""" @@ -15,6 +17,7 @@ class OpenAIAuthenticationType(str, Enum): NoneType = "none" +@deprecated("The `OpenAIAuthenticationType` class is deprecated; use OpenAPI Plugins instead.", category=None) class OpenAIAuthorizationType(str, Enum): """OpenAI authorization types.""" @@ -22,6 +25,7 @@ class OpenAIAuthorizationType(str, Enum): Basic = "Basic" +@deprecated("The `OpenAIAuthenticationConfig` class is deprecated; use OpenAPI Plugins instead.", category=None) class OpenAIAuthenticationConfig(KernelBaseModel): """OpenAI authentication configuration.""" diff --git a/python/semantic_kernel/connectors/openai_plugin/openai_function_execution_parameters.py b/python/semantic_kernel/connectors/openai_plugin/openai_function_execution_parameters.py index 0638e820fbaf..93b647b3295d 100644 --- a/python/semantic_kernel/connectors/openai_plugin/openai_function_execution_parameters.py +++ b/python/semantic_kernel/connectors/openai_plugin/openai_function_execution_parameters.py @@ -4,6 +4,8 @@ from collections.abc import Awaitable, Callable from typing import Any +from typing_extensions import deprecated + from semantic_kernel.connectors.openapi_plugin.openapi_function_execution_parameters import ( OpenAPIFunctionExecutionParameters, ) @@ -11,6 +13,7 @@ OpenAIAuthCallbackType = Callable[..., Awaitable[Any]] +@deprecated("The `OpenAIFunctionExecutionParameters` class is deprecated; use OpenAPI Plugins instead.", category=None) class OpenAIFunctionExecutionParameters(OpenAPIFunctionExecutionParameters): """OpenAI function execution parameters.""" diff --git a/python/semantic_kernel/connectors/openai_plugin/openai_utils.py b/python/semantic_kernel/connectors/openai_plugin/openai_utils.py index 44ce20f127ce..fa3617c46a8b 100644 --- a/python/semantic_kernel/connectors/openai_plugin/openai_utils.py +++ b/python/semantic_kernel/connectors/openai_plugin/openai_utils.py @@ -4,11 +4,14 @@ import logging from typing import Any +from typing_extensions import deprecated + from semantic_kernel.exceptions.function_exceptions import PluginInitializationError logger: logging.Logger = logging.getLogger(__name__) +@deprecated("The `OpenAIUtils` class is deprecated; use OpenAPI Plugins instead.", category=None) class OpenAIUtils: """Utility functions for OpenAI plugins.""" diff --git a/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation.py b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation.py index 7ab80e300405..0fffe6b2be73 100644 --- a/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation.py +++ b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation.py @@ -51,7 +51,7 @@ def __init__( self, id: str, method: str, - server_url: str | ParseResult, + servers: list[str | ParseResult], path: str, summary: str | None = None, description: str | None = None, @@ -62,7 +62,7 @@ def __init__( """Initialize the RestApiOperation.""" self.id = id self.method = method.upper() - self.server_url = urlparse(server_url) if isinstance(server_url, str) else server_url + self.servers = [urlparse(s) if isinstance(s, str) else s for s in servers] self.path = path self.summary = summary self.description = description @@ -106,16 +106,16 @@ def build_operation_url(self, arguments, server_url_override=None, api_host_url= def get_server_url(self, server_url_override=None, api_host_url=None): """Get the server URL for the operation.""" - if server_url_override is not None and server_url_override.geturl() != b"": - server_url_string = server_url_override.geturl() + if server_url_override is not None and server_url_override.geturl() != "": + server_url = server_url_override + elif self.servers and self.servers[0].geturl() != "": + server_url = self.servers[0] + elif api_host_url is not None: + server_url = api_host_url else: - server_url_string = ( - self.server_url.geturl() - if self.server_url - else api_host_url.geturl() - if api_host_url - else self._raise_invalid_operation_exception() - ) + raise FunctionExecutionException(f"No valid server URL for operation {self.id}") + + server_url_string = server_url.geturl() # make sure the base URL ends with a trailing slash if not server_url_string.endswith("/"): diff --git a/python/semantic_kernel/connectors/openapi_plugin/openapi_parser.py b/python/semantic_kernel/connectors/openapi_plugin/openapi_parser.py index 984f120e837b..ea8ebc40624e 100644 --- a/python/semantic_kernel/connectors/openapi_plugin/openapi_parser.py +++ b/python/semantic_kernel/connectors/openapi_plugin/openapi_parser.py @@ -4,7 +4,6 @@ from collections import OrderedDict from collections.abc import Generator from typing import TYPE_CHECKING, Any, Final -from urllib.parse import urlparse from prance import ResolvingParser @@ -178,12 +177,11 @@ def create_rest_api_operations( paths = parsed_document.get("paths", {}) request_objects = {} - base_url = "/" servers = parsed_document.get("servers", []) - base_url = servers[0].get("url") if servers else "/" + server_urls = [server.get("url") for server in servers] if servers else ["/"] if execution_settings and execution_settings.server_url_override: - base_url = execution_settings.server_url_override + server_urls = [execution_settings.server_url_override] for path, methods in paths.items(): for method, details in methods.items(): @@ -201,7 +199,7 @@ def create_rest_api_operations( rest_api_operation = RestApiOperation( id=operationId, method=request_method, - server_url=urlparse(base_url), + servers=server_urls, path=path, params=parsed_params, request_body=request_body, diff --git a/python/semantic_kernel/functions/kernel_function_extension.py b/python/semantic_kernel/functions/kernel_function_extension.py index 361e821ac0c7..9571085c1ee9 100644 --- a/python/semantic_kernel/functions/kernel_function_extension.py +++ b/python/semantic_kernel/functions/kernel_function_extension.py @@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Any, Literal from pydantic import Field, field_validator +from typing_extensions import deprecated from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings from semantic_kernel.exceptions import KernelFunctionNotFoundError, KernelPluginNotFoundError @@ -233,6 +234,10 @@ def add_plugin_from_openapi( ) ) + @deprecated( + "The `add_plugin_from_openai` method is deprecated; use the `add_plugin_from_openapi` method instead.", + category=None, + ) async def add_plugin_from_openai( self, plugin_name: str, diff --git a/python/tests/unit/connectors/openapi_plugin/openapi.yaml b/python/tests/unit/connectors/openapi_plugin/openapi.yaml index c2487b4d29d6..30746c8e3dec 100644 --- a/python/tests/unit/connectors/openapi_plugin/openapi.yaml +++ b/python/tests/unit/connectors/openapi_plugin/openapi.yaml @@ -4,6 +4,7 @@ info: version: 1.0.0 servers: - url: http://example.com + - url: https://example-two.com paths: /todos: get: diff --git a/python/tests/unit/connectors/openapi_plugin/test_sk_openapi.py b/python/tests/unit/connectors/openapi_plugin/test_sk_openapi.py index 45229b6f1630..9d759d25347e 100644 --- a/python/tests/unit/connectors/openapi_plugin/test_sk_openapi.py +++ b/python/tests/unit/connectors/openapi_plugin/test_sk_openapi.py @@ -46,7 +46,7 @@ put_operation = RestApiOperation( id="updateTodoById", method="PUT", - server_url="http://example.com", + servers="http://example.com", path="/todos/{id}", summary="Update a todo by ID", params=[ @@ -119,7 +119,7 @@ def test_parse_invalid_format(): def test_url_join_with_trailing_slash(): - operation = RestApiOperation(id="test", method="GET", server_url="https://example.com/", path="test/path") + operation = RestApiOperation(id="test", method="GET", servers=["https://example.com/"], path="test/path") base_url = "https://example.com/" path = "test/path" expected_url = "https://example.com/test/path" @@ -127,7 +127,7 @@ def test_url_join_with_trailing_slash(): def test_url_join_without_trailing_slash(): - operation = RestApiOperation(id="test", method="GET", server_url="https://example.com", path="test/path") + operation = RestApiOperation(id="test", method="GET", servers=["https://example.com"], path="test/path") base_url = "https://example.com" path = "test/path" expected_url = "https://example.com/test/path" @@ -135,7 +135,7 @@ def test_url_join_without_trailing_slash(): def test_url_join_base_path_with_path(): - operation = RestApiOperation(id="test", method="GET", server_url="https://example.com/base/", path="test/path") + operation = RestApiOperation(id="test", method="GET", servers="https://example.com/base/", path="test/path") base_url = "https://example.com/base/" path = "test/path" expected_url = "https://example.com/base/test/path" @@ -143,7 +143,7 @@ def test_url_join_base_path_with_path(): def test_url_join_with_leading_slash_in_path(): - operation = RestApiOperation(id="test", method="GET", server_url="https://example.com/", path="/test/path") + operation = RestApiOperation(id="test", method="GET", servers=["https://example.com/"], path="/test/path") base_url = "https://example.com/" path = "/test/path" expected_url = "https://example.com/test/path" @@ -151,7 +151,7 @@ def test_url_join_with_leading_slash_in_path(): def test_url_join_base_path_without_trailing_slash(): - operation = RestApiOperation(id="test", method="GET", server_url="https://example.com/base", path="test/path") + operation = RestApiOperation(id="test", method="GET", servers="https://example.com/base", path="test/path") base_url = "https://example.com/base" path = "test/path" expected_url = "https://example.com/base/test/path" @@ -165,7 +165,7 @@ def test_build_headers_with_required_parameter(): ) ] operation = RestApiOperation( - id="test", method="GET", server_url="https://example.com", path="test/path", params=parameters + id="test", method="GET", servers=["https://example.com"], path="test/path", params=parameters ) arguments = {"Authorization": "Bearer token"} expected_headers = {"Authorization": "Bearer token"} @@ -179,7 +179,7 @@ def test_build_headers_missing_required_parameter(): ) ] operation = RestApiOperation( - id="test", method="GET", server_url="https://example.com", path="test/path", params=parameters + id="test", method="GET", servers=["https://example.com"], path="test/path", params=parameters ) arguments = {} with pytest.raises( @@ -196,7 +196,7 @@ def test_build_headers_with_optional_parameter(): ) ] operation = RestApiOperation( - id="test", method="GET", server_url="https://example.com", path="test/path", params=parameters + id="test", method="GET", servers=["https://example.com"], path="test/path", params=parameters ) arguments = {"Authorization": "Bearer token"} expected_headers = {"Authorization": "Bearer token"} @@ -210,7 +210,7 @@ def test_build_headers_missing_optional_parameter(): ) ] operation = RestApiOperation( - id="test", method="GET", server_url="https://example.com", path="test/path", params=parameters + id="test", method="GET", servers=["https://example.com"], path="test/path", params=parameters ) arguments = {} expected_headers = {} @@ -227,7 +227,7 @@ def test_build_headers_multiple_parameters(): ), ] operation = RestApiOperation( - id="test", method="GET", server_url="https://example.com", path="test/path", params=parameters + id="test", method="GET", servers=["https://example.com"], path="test/path", params=parameters ) arguments = {"Authorization": "Bearer token", "Content-Type": "application/json"} expected_headers = {"Authorization": "Bearer token", "Content-Type": "application/json"} @@ -241,7 +241,7 @@ def test_build_operation_url_with_override(): ) ] operation = RestApiOperation( - id="test", method="GET", server_url="https://example.com/", path="/resource/{id}", params=parameters + id="test", method="GET", servers=["https://example.com/"], path="/resource/{id}", params=parameters ) arguments = {"id": "123"} server_url_override = urlparse("https://override.com") @@ -256,7 +256,7 @@ def test_build_operation_url_without_override(): ) ] operation = RestApiOperation( - id="test", method="GET", server_url="https://example.com/", path="/resource/{id}", params=parameters + id="test", method="GET", servers=["https://example.com/"], path="/resource/{id}", params=parameters ) arguments = {"id": "123"} expected_url = "https://example.com/resource/123" @@ -264,14 +264,14 @@ def test_build_operation_url_without_override(): def test_get_server_url_with_override(): - operation = RestApiOperation(id="test", method="GET", server_url="https://example.com", path="/resource/{id}") + operation = RestApiOperation(id="test", method="GET", servers=["https://example.com"], path="/resource/{id}") server_url_override = urlparse("https://override.com") expected_url = "https://override.com/" assert operation.get_server_url(server_url_override=server_url_override).geturl() == expected_url def test_get_server_url_without_override(): - operation = RestApiOperation(id="test", method="GET", server_url="https://example.com", path="/resource/{id}") + operation = RestApiOperation(id="test", method="GET", servers=["https://example.com"], path="/resource/{id}") expected_url = "https://example.com/" assert operation.get_server_url().geturl() == expected_url @@ -283,7 +283,7 @@ def test_build_path_with_required_parameter(): ) ] operation = RestApiOperation( - id="test", method="GET", server_url="https://example.com/", path="/resource/{id}", params=parameters + id="test", method="GET", servers=["https://example.com/"], path="/resource/{id}", params=parameters ) arguments = {"id": "123"} expected_path = "/resource/123" @@ -297,7 +297,7 @@ def test_build_path_missing_required_parameter(): ) ] operation = RestApiOperation( - id="test", method="GET", server_url="https://example.com/", path="/resource/{id}", params=parameters + id="test", method="GET", servers=["https://example.com/"], path="/resource/{id}", params=parameters ) arguments = {} with pytest.raises( @@ -317,7 +317,7 @@ def test_build_path_with_optional_and_required_parameters(): ), ] operation = RestApiOperation( - id="test", method="GET", server_url="https://example.com/", path="/resource/{id}/{optional}", params=parameters + id="test", method="GET", servers=["https://example.com/"], path="/resource/{id}/{optional}", params=parameters ) arguments = {"id": "123"} expected_path = "/resource/123/{optional}" @@ -331,7 +331,7 @@ def test_build_query_string_with_required_parameter(): ) ] operation = RestApiOperation( - id="test", method="GET", server_url="https://example.com/", path="/resource", params=parameters + id="test", method="GET", servers=["https://example.com/"], path="/resource", params=parameters ) arguments = {"query": "value"} expected_query_string = "query=value" @@ -345,7 +345,7 @@ def test_build_query_string_missing_required_parameter(): ) ] operation = RestApiOperation( - id="test", method="GET", server_url="https://example.com/", path="/resource", params=parameters + id="test", method="GET", servers=["https://example.com/"], path="/resource", params=parameters ) arguments = {} with pytest.raises( @@ -365,7 +365,7 @@ def test_build_query_string_with_optional_and_required_parameters(): ), ] operation = RestApiOperation( - id="test", method="GET", server_url="https://example.com/", path="/resource", params=parameters + id="test", method="GET", servers=["https://example.com/"], path="/resource", params=parameters ) arguments = {"required_param": "required_value"} expected_query_string = "required_param=required_value" @@ -391,7 +391,7 @@ def test_create_payload_artificial_parameter_with_text_plain(): schema="Test schema", ) operation = RestApiOperation( - id="test", method="POST", server_url="https://example.com/", path="/resource", request_body=request_body + id="test", method="POST", servers=["https://example.com/"], path="/resource", request_body=request_body ) expected_parameter = RestApiOperationParameter( name=operation.PAYLOAD_ARGUMENT_NAME, @@ -428,7 +428,7 @@ def test_create_payload_artificial_parameter_with_object(): media_type="application/json", properties=properties, description="Test description", schema="Test schema" ) operation = RestApiOperation( - id="test", method="POST", server_url="https://example.com/", path="/resource", request_body=request_body + id="test", method="POST", servers=["https://example.com/"], path="/resource", request_body=request_body ) expected_parameter = RestApiOperationParameter( name=operation.PAYLOAD_ARGUMENT_NAME, @@ -450,7 +450,7 @@ def test_create_payload_artificial_parameter_with_object(): def test_create_payload_artificial_parameter_without_request_body(): - operation = RestApiOperation(id="test", method="POST", server_url="https://example.com/", path="/resource") + operation = RestApiOperation(id="test", method="POST", servers=["https://example.com/"], path="/resource") expected_parameter = RestApiOperationParameter( name=operation.PAYLOAD_ARGUMENT_NAME, type="object", @@ -471,7 +471,7 @@ def test_create_payload_artificial_parameter_without_request_body(): def test_create_content_type_artificial_parameter(): - operation = RestApiOperation(id="test", method="POST", server_url="https://example.com/", path="/resource") + operation = RestApiOperation(id="test", method="POST", servers=["https://example.com/"], path="/resource") expected_parameter = RestApiOperationParameter( name=operation.CONTENT_TYPE_ARGUMENT_NAME, type="string", @@ -490,7 +490,7 @@ def test_create_content_type_artificial_parameter(): def test_get_property_name_with_namespacing_and_root_property(): - operation = RestApiOperation(id="test", method="POST", server_url="https://example.com/", path="/resource") + operation = RestApiOperation(id="test", method="POST", servers=["https://example.com/"], path="/resource") property = RestApiOperationPayloadProperty( name="child", type="string", properties=[], description="Property description" ) @@ -499,7 +499,7 @@ def test_get_property_name_with_namespacing_and_root_property(): def test_get_property_name_without_namespacing(): - operation = RestApiOperation(id="test", method="POST", server_url="https://example.com/", path="/resource") + operation = RestApiOperation(id="test", method="POST", servers=["https://example.com/"], path="/resource") property = RestApiOperationPayloadProperty( name="child", type="string", properties=[], description="Property description" ) @@ -515,7 +515,7 @@ def test_get_payload_parameters_with_metadata_and_text_plain(): media_type=RestApiOperation.MEDIA_TYPE_TEXT_PLAIN, properties=properties, description="Test description" ) operation = RestApiOperation( - id="test", method="POST", server_url="https://example.com/", path="/resource", request_body=request_body + id="test", method="POST", servers=["https://example.com/"], path="/resource", request_body=request_body ) result = operation.get_payload_parameters(operation, use_parameters_from_metadata=True, enable_namespacing=True) assert len(result) == 1 @@ -530,7 +530,7 @@ def test_get_payload_parameters_with_metadata_and_json(): media_type="application/json", properties=properties, description="Test description" ) operation = RestApiOperation( - id="test", method="POST", server_url="https://example.com/", path="/resource", request_body=request_body + id="test", method="POST", servers=["https://example.com/"], path="/resource", request_body=request_body ) result = operation.get_payload_parameters(operation, use_parameters_from_metadata=True, enable_namespacing=True) assert len(result) == len(properties) @@ -538,7 +538,7 @@ def test_get_payload_parameters_with_metadata_and_json(): def test_get_payload_parameters_without_metadata(): - operation = RestApiOperation(id="test", method="POST", server_url="https://example.com/", path="/resource") + operation = RestApiOperation(id="test", method="POST", servers=["https://example.com/"], path="/resource") result = operation.get_payload_parameters(operation, use_parameters_from_metadata=False, enable_namespacing=False) assert len(result) == 2 assert result[0].name == operation.PAYLOAD_ARGUMENT_NAME @@ -549,7 +549,7 @@ def test_get_payload_parameters_raises_exception(): operation = RestApiOperation( id="test", method="POST", - server_url="https://example.com/", + servers=["https://example.com/"], path="/resource", request_body=None, ) @@ -561,7 +561,7 @@ def test_get_payload_parameters_raises_exception(): def test_get_default_response(): - operation = RestApiOperation(id="test", method="GET", server_url="https://example.com/", path="/resource") + operation = RestApiOperation(id="test", method="GET", servers=["https://example.com/"], path="/resource") responses = { "200": RestApiOperationExpectedResponse( description="Success", media_type="application/json", schema={"type": "object"} @@ -576,7 +576,7 @@ def test_get_default_response(): def test_get_default_response_with_default(): - operation = RestApiOperation(id="test", method="GET", server_url="https://example.com/", path="/resource") + operation = RestApiOperation(id="test", method="GET", servers=["https://example.com/"], path="/resource") responses = { "default": RestApiOperationExpectedResponse( description="Default response", media_type="application/json", schema={"type": "object"} @@ -588,7 +588,7 @@ def test_get_default_response_with_default(): def test_get_default_response_none(): - operation = RestApiOperation(id="test", method="GET", server_url="https://example.com/", path="/resource") + operation = RestApiOperation(id="test", method="GET", servers=["https://example.com/"], path="/resource") responses = {} preferred_responses = ["200", "default"] result = operation.get_default_response(responses, preferred_responses) @@ -596,7 +596,7 @@ def test_get_default_response_none(): def test_get_default_return_parameter_with_response(): - operation = RestApiOperation(id="test", method="GET", server_url="https://example.com/", path="/resource") + operation = RestApiOperation(id="test", method="GET", servers=["https://example.com/"], path="/resource") responses = { "200": RestApiOperationExpectedResponse( description="Success", media_type="application/json", schema={"type": "object"} @@ -614,7 +614,7 @@ def test_get_default_return_parameter_with_response(): def test_get_default_return_parameter_none(): - operation = RestApiOperation(id="test", method="GET", server_url="https://example.com/", path="/resource") + operation = RestApiOperation(id="test", method="GET", servers=["https://example.com/"], path="/resource") responses = {} operation.responses = responses result = operation.get_default_return_parameter(preferred_responses=["200", "default"]) From bafd0f9b0c6532ec2137e369ee489386032d24d6 Mon Sep 17 00:00:00 2001 From: Evan Mattson Date: Tue, 12 Nov 2024 15:39:27 -0500 Subject: [PATCH 2/9] Support multiple OpenAPI servers in a RestApiOperation. Allow for OpenAPI security configuration. --- .../models/rest_api_oauth_flow.py | 15 ++++ .../models/rest_api_oauth_flows.py | 22 +++++ .../models/rest_api_operation.py | 3 + .../models/rest_api_security_requirement.py | 13 +++ .../models/rest_api_security_scheme.py | 33 +++++++ .../openapi_function_execution_parameters.py | 2 +- .../openapi_plugin/openapi_manager.py | 40 ++++++++- .../openapi_plugin/openapi_parser.py | 61 +++++++++++-- .../openapi_plugin/apikey-securityV3_0.json | 79 ++++++++++++++++ .../openapi_plugin/no-securityV3_0.json | 74 +++++++++++++++ .../openapi_plugin/oauth-securityV3_0.json | 89 +++++++++++++++++++ .../openapi_plugin/test_openapi_parser.py | 69 ++++++++++++++ 12 files changed, 490 insertions(+), 10 deletions(-) create mode 100644 python/semantic_kernel/connectors/openapi_plugin/models/rest_api_oauth_flow.py create mode 100644 python/semantic_kernel/connectors/openapi_plugin/models/rest_api_oauth_flows.py create mode 100644 python/semantic_kernel/connectors/openapi_plugin/models/rest_api_security_requirement.py create mode 100644 python/semantic_kernel/connectors/openapi_plugin/models/rest_api_security_scheme.py create mode 100644 python/tests/unit/connectors/openapi_plugin/apikey-securityV3_0.json create mode 100644 python/tests/unit/connectors/openapi_plugin/no-securityV3_0.json create mode 100644 python/tests/unit/connectors/openapi_plugin/oauth-securityV3_0.json diff --git a/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_oauth_flow.py b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_oauth_flow.py new file mode 100644 index 000000000000..f47ec59801b6 --- /dev/null +++ b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_oauth_flow.py @@ -0,0 +1,15 @@ +# Copyright (c) Microsoft. All rights reserved. + +from semantic_kernel.utils.experimental_decorator import experimental_class + + +@experimental_class +class RestApiOAuthFlow: + """Represents the OAuth flow used by the REST API.""" + + def __init__(self, authorization_url: str, token_url: str, scopes: dict[str, str], refresh_url: str | None = None): + """Initializes a new instance of the RestApiOAuthFlow class.""" + self.authorization_url = authorization_url + self.token_url = token_url + self.refresh_url = refresh_url + self.scopes = scopes diff --git a/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_oauth_flows.py b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_oauth_flows.py new file mode 100644 index 000000000000..4da0d4698ed1 --- /dev/null +++ b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_oauth_flows.py @@ -0,0 +1,22 @@ +# Copyright (c) Microsoft. All rights reserved. + +from semantic_kernel.connectors.openapi_plugin.models.rest_api_oauth_flow import RestApiOAuthFlow +from semantic_kernel.utils.experimental_decorator import experimental_class + + +@experimental_class +class RestApiOAuthFlows: + """Represents the OAuth flows used by the REST API.""" + + def __init__( + self, + implicit: RestApiOAuthFlow | None = None, + password: RestApiOAuthFlow | None = None, + client_credentials: RestApiOAuthFlow | None = None, + authorization_code: RestApiOAuthFlow | None = None, + ): + """Initializes a new instance of the RestApiOAuthFlows class.""" + self.implicit = implicit + self.password = password + self.client_credentials = client_credentials + self.authorization_code = authorization_code diff --git a/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation.py b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation.py index 0fffe6b2be73..a621c1fb75ba 100644 --- a/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation.py +++ b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation.py @@ -18,6 +18,7 @@ from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation_payload_property import ( RestApiOperationPayloadProperty, ) +from semantic_kernel.connectors.openapi_plugin.models.rest_api_security_requirement import RestApiSecurityRequirement from semantic_kernel.exceptions.function_exceptions import FunctionExecutionException from semantic_kernel.functions.kernel_parameter_metadata import KernelParameterMetadata from semantic_kernel.utils.experimental_decorator import experimental_class @@ -58,6 +59,7 @@ def __init__( params: list["RestApiOperationParameter"] | None = None, request_body: "RestApiOperationPayload | None" = None, responses: dict[str, "RestApiOperationExpectedResponse"] | None = None, + security_requirements: list[RestApiSecurityRequirement] | None = None, ): """Initialize the RestApiOperation.""" self.id = id @@ -69,6 +71,7 @@ def __init__( self.parameters = params if params else [] self.request_body = request_body self.responses = responses + self.security_requirements = security_requirements def url_join(self, base_url: str, path: str): """Join a base URL and a path, correcting for any missing slashes.""" diff --git a/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_security_requirement.py b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_security_requirement.py new file mode 100644 index 000000000000..78a07ace5da6 --- /dev/null +++ b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_security_requirement.py @@ -0,0 +1,13 @@ +# Copyright (c) Microsoft. All rights reserved. + +from semantic_kernel.connectors.openapi_plugin.models.rest_api_security_scheme import RestApiSecurityScheme +from semantic_kernel.utils.experimental_decorator import experimental_class + + +@experimental_class +class RestApiSecurityRequirement(dict[RestApiSecurityScheme, list[str]]): + """Represents the security requirements used by the REST API.""" + + def __init__(self, dictionary: dict[RestApiSecurityScheme, list[str]]): + """Initializes a new instance of the RestApiSecurityRequirement class.""" + super().__init__(dictionary) diff --git a/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_security_scheme.py b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_security_scheme.py new file mode 100644 index 000000000000..ccb9a39773b5 --- /dev/null +++ b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_security_scheme.py @@ -0,0 +1,33 @@ +# Copyright (c) Microsoft. All rights reserved. + +from semantic_kernel.connectors.openapi_plugin.models.rest_api_oauth_flows import RestApiOAuthFlows +from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation_parameter_location import ( + RestApiOperationParameterLocation, +) +from semantic_kernel.utils.experimental_decorator import experimental_class + + +@experimental_class +class RestApiSecurityScheme: + """Represents the security scheme used by the REST API.""" + + def __init__( + self, + security_scheme_type: str, + name: str, + in_: RestApiOperationParameterLocation, + scheme: str, + open_id_connect_url: str, + description: str | None = None, + bearer_format: str | None = None, + flows: RestApiOAuthFlows | None = None, + ): + """Initializes a new instance of the RestApiSecurityScheme class.""" + self.security_scheme_type = security_scheme_type + self.description = description + self.name = name + self.in_ = in_ + self.scheme = scheme + self.bearer_format = bearer_format + self.flows = flows + self.open_id_connect_url = open_id_connect_url diff --git a/python/semantic_kernel/connectors/openapi_plugin/openapi_function_execution_parameters.py b/python/semantic_kernel/connectors/openapi_plugin/openapi_function_execution_parameters.py index dd014a97e55c..0f1463ebbf0d 100644 --- a/python/semantic_kernel/connectors/openapi_plugin/openapi_function_execution_parameters.py +++ b/python/semantic_kernel/connectors/openapi_plugin/openapi_function_execution_parameters.py @@ -24,7 +24,7 @@ class OpenAPIFunctionExecutionParameters(KernelBaseModel): user_agent: str | None = None enable_dynamic_payload: bool = True enable_payload_namespacing: bool = False - operations_to_exclude: list[str] = Field(default_factory=list) + operations_to_exclude: list[str] = Field(default_factory=list, description="The operationId(s) to exclude") def model_post_init(self, __context: Any) -> None: """Post initialization method for the model.""" diff --git a/python/semantic_kernel/connectors/openapi_plugin/openapi_manager.py b/python/semantic_kernel/connectors/openapi_plugin/openapi_manager.py index bc195dec1bef..13b30256018f 100644 --- a/python/semantic_kernel/connectors/openapi_plugin/openapi_manager.py +++ b/python/semantic_kernel/connectors/openapi_plugin/openapi_manager.py @@ -1,12 +1,14 @@ # Copyright (c) Microsoft. All rights reserved. import logging +from enum import Enum from typing import TYPE_CHECKING, Any from urllib.parse import urlparse from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation import RestApiOperation from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation_parameter import RestApiOperationParameter from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation_run_options import RestApiOperationRunOptions +from semantic_kernel.connectors.openapi_plugin.models.rest_api_security_requirement import RestApiSecurityRequirement from semantic_kernel.connectors.openapi_plugin.models.rest_api_uri import Uri from semantic_kernel.connectors.openapi_plugin.openapi_parser import OpenApiParser from semantic_kernel.connectors.openapi_plugin.openapi_runner import OpenApiRunner @@ -16,7 +18,7 @@ from semantic_kernel.functions.kernel_function_from_method import KernelFunctionFromMethod from semantic_kernel.functions.kernel_parameter_metadata import KernelParameterMetadata from semantic_kernel.schema.kernel_json_schema_builder import TYPE_MAPPING -from semantic_kernel.utils.experimental_decorator import experimental_function +from semantic_kernel.utils.experimental_decorator import experimental_class, experimental_function if TYPE_CHECKING: from semantic_kernel.connectors.openai_plugin.openai_function_execution_parameters import ( @@ -29,6 +31,18 @@ logger: logging.Logger = logging.getLogger(__name__) +@experimental_class +class OperationExtensions(Enum): + """The operation extensions.""" + + METHOD_KEY = "method" + OPERATION_KEY = "operation" + INFO_KEY = "info" + SECURITY_KEY = "security" + SERVER_URLS_KEY = "server-urls" + METADATA_KEY = "operation-extensions" + + @experimental_function def create_functions_from_openapi( plugin_name: str, @@ -50,6 +64,8 @@ def create_functions_from_openapi( raise FunctionExecutionException(f"Error parsing OpenAPI document: {openapi_document_path}") operations = parser.create_rest_api_operations(parsed_doc, execution_settings=execution_settings) + global_security_requirements = parsed_doc.get("security", []) + auth_callback = None if execution_settings and execution_settings.auth_callback: auth_callback = execution_settings.auth_callback @@ -63,7 +79,13 @@ def create_functions_from_openapi( ) return [ - _create_function_from_operation(openapi_runner, operation, plugin_name, execution_parameters=execution_settings) + _create_function_from_operation( + openapi_runner, + operation, + plugin_name, + execution_parameters=execution_settings, + security=global_security_requirements, + ) for operation in operations.values() ] @@ -75,6 +97,7 @@ def _create_function_from_operation( plugin_name: str | None = None, execution_parameters: "OpenAIFunctionExecutionParameters | OpenAPIFunctionExecutionParameters | None" = None, document_uri: str | None = None, + security: list[RestApiSecurityRequirement] | None = None, ) -> KernelFunctionFromMethod: logger.info(f"Registering OpenAPI operation: {plugin_name}.{operation.id}") @@ -145,7 +168,18 @@ async def run_openapi_operation( return_parameter = operation.get_default_return_parameter() - additional_metadata = {"method": operation.method.upper()} + additional_metadata = { + OperationExtensions.METHOD_KEY.value: operation.method.upper(), + OperationExtensions.OPERATION_KEY.value: operation, + OperationExtensions.SERVER_URLS_KEY.value: ( + [operation.servers[0].geturl()] + if operation.servers and len(operation.servers) > 0 and operation.servers[0].geturl() + else [] + ), + } + + if security is not None: + additional_metadata[OperationExtensions.SECURITY_KEY.value] = security return KernelFunctionFromMethod( method=run_openapi_operation, diff --git a/python/semantic_kernel/connectors/openapi_plugin/openapi_parser.py b/python/semantic_kernel/connectors/openapi_plugin/openapi_parser.py index ea8ebc40624e..ace0fe12021b 100644 --- a/python/semantic_kernel/connectors/openapi_plugin/openapi_parser.py +++ b/python/semantic_kernel/connectors/openapi_plugin/openapi_parser.py @@ -19,6 +19,8 @@ from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation_payload_property import ( RestApiOperationPayloadProperty, ) +from semantic_kernel.connectors.openapi_plugin.models.rest_api_security_requirement import RestApiSecurityRequirement +from semantic_kernel.connectors.openapi_plugin.models.rest_api_security_scheme import RestApiSecurityScheme from semantic_kernel.exceptions.function_exceptions import PluginInitializationError from semantic_kernel.utils.experimental_decorator import experimental_class @@ -160,20 +162,59 @@ def _create_response( ), ) + def _parse_security_schemes(self, components: dict) -> dict[str, dict]: + security_schemes = {} + schemes = components.get("securitySchemes", {}) + for scheme_name, scheme_data in schemes.items(): + security_schemes[scheme_name] = scheme_data + return security_schemes + + def _create_rest_api_security_scheme(self, security_scheme_data: dict) -> RestApiSecurityScheme: + return RestApiSecurityScheme( + security_scheme_type=security_scheme_data.get("type", ""), + description=security_scheme_data.get("description"), + name=security_scheme_data.get("name", ""), + in_=security_scheme_data.get("in", ""), + scheme=security_scheme_data.get("scheme", ""), + bearer_format=security_scheme_data.get("bearerFormat"), + flows=security_scheme_data.get("flows"), + open_id_connect_url=security_scheme_data.get("openIdConnectUrl", ""), + ) + + def _create_security_requirements( + self, + security: list[dict[str, list[str]]], + security_schemes: dict[str, dict], + ) -> list[RestApiSecurityRequirement]: + security_requirements: list[RestApiSecurityRequirement] = [] + + for requirement in security: + for scheme_name, scopes in requirement.items(): + scheme_data = security_schemes.get(scheme_name) + if not scheme_data: + raise PluginInitializationError(f"Security scheme '{scheme_name}' is not defined in components.") + scheme = self._create_rest_api_security_scheme(scheme_data) + security_requirements.append(RestApiSecurityRequirement({scheme: scopes})) + + return security_requirements + def create_rest_api_operations( self, parsed_document: Any, execution_settings: "OpenAIFunctionExecutionParameters | OpenAPIFunctionExecutionParameters | None" = None, ) -> dict[str, RestApiOperation]: - """Create the REST API Operations from the parsed OpenAPI document. + """Create REST API operations from the parsed OpenAPI document. Args: - parsed_document: The parsed OpenAPI document - execution_settings: The execution settings + parsed_document: The parsed OpenAPI document. + execution_settings: The execution settings. Returns: - A dictionary of RestApiOperation objects keyed by operationId + A dictionary of RestApiOperation instances. """ + components = parsed_document.get("components", {}) + security_schemes = self._parse_security_schemes(components) + paths = parsed_document.get("paths", {}) request_objects = {} @@ -186,16 +227,23 @@ def create_rest_api_operations( for path, methods in paths.items(): for method, details in methods.items(): request_method = method.lower() - - parameters = details.get("parameters", []) operationId = details.get("operationId", path + "_" + request_method) + + if execution_settings and operationId in execution_settings.operations_to_exclude: + logger.info(f"Skipping operation {operationId} as it is excluded.") + continue + summary = details.get("summary", None) description = details.get("description", None) + parameters = details.get("parameters", []) parsed_params = self._parse_parameters(parameters) request_body = self._create_rest_api_operation_payload(operationId, details.get("requestBody", None)) responses = dict(self._create_response(details.get("responses", {}))) + operation_security = details.get("security", []) + security_requirements = self._create_security_requirements(operation_security, security_schemes) + rest_api_operation = RestApiOperation( id=operationId, method=request_method, @@ -206,6 +254,7 @@ def create_rest_api_operations( summary=summary, description=description, responses=OrderedDict(responses), + security_requirements=security_requirements, ) request_objects[operationId] = rest_api_operation diff --git a/python/tests/unit/connectors/openapi_plugin/apikey-securityV3_0.json b/python/tests/unit/connectors/openapi_plugin/apikey-securityV3_0.json new file mode 100644 index 000000000000..089c3493dc99 --- /dev/null +++ b/python/tests/unit/connectors/openapi_plugin/apikey-securityV3_0.json @@ -0,0 +1,79 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "Semantic Kernel Open API Sample", + "description": "A sample Open API schema with endpoints which have security requirements defined.", + "version": "1.0" + }, + "servers": [ + { + "url": "https://example.org" + } + ], + "paths": { + "/use_global_security": { + "get": { + "summary": "No security defined on operation", + "description": "", + "operationId": "NoSecurity", + "parameters": [], + "responses": { + "200": { + "description": "default" + } + } + }, + "post": { + "summary": "Security defined on operation", + "description": "", + "operationId": "Security", + "parameters": [], + "responses": { + "200": { + "description": "default" + } + }, + "security": [ + { + "ApiKeyAuthQuery": [] + } + ] + }, + "put": { + "summary": "Security defined on operation with new scope", + "description": "", + "operationId": "SecurityAndScope", + "parameters": [], + "responses": { + "200": { + "description": "default" + } + }, + "security": [ + { + "ApiKeyAuthQuery": [] + } + ] + } + } + }, + "components": { + "securitySchemes": { + "ApiKeyAuthHeader": { + "type": "apiKey", + "in": "header", + "name": "X-API-KEY" + }, + "ApiKeyAuthQuery": { + "type": "apiKey", + "in": "query", + "name": "apiKey" + } + } + }, + "security": [ + { + "ApiKeyAuthHeader": [] + } + ] +} \ No newline at end of file diff --git a/python/tests/unit/connectors/openapi_plugin/no-securityV3_0.json b/python/tests/unit/connectors/openapi_plugin/no-securityV3_0.json new file mode 100644 index 000000000000..f5d5ea2566ad --- /dev/null +++ b/python/tests/unit/connectors/openapi_plugin/no-securityV3_0.json @@ -0,0 +1,74 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "Semantic Kernel Open API Sample", + "description": "A sample Open API schema with endpoints which have security requirements defined.", + "version": "1.0" + }, + "servers": [ + { + "url": "https://example.org" + } + ], + "paths": { + "/use_global_security": { + "get": { + "summary": "No security defined on operation", + "description": "", + "operationId": "NoSecurity", + "parameters": [], + "responses": { + "200": { + "description": "default" + } + } + }, + "post": { + "summary": "Security defined on operation", + "description": "", + "operationId": "Security", + "parameters": [], + "responses": { + "200": { + "description": "default" + } + }, + "security": [ + { + "ApiKeyAuthQuery": [] + } + ] + }, + "put": { + "summary": "Security defined on operation with new scope", + "description": "", + "operationId": "SecurityAndScope", + "parameters": [], + "responses": { + "200": { + "description": "default" + } + }, + "security": [ + { + "ApiKeyAuthQuery": ["new_scope"] + } + ] + } + } + }, + "components": { + "securitySchemes": { + "ApiKeyAuthHeader": { + "type": "apiKey", + "in": "header", + "name": "X-API-KEY" + }, + "ApiKeyAuthQuery": { + "type": "apiKey", + "in": "query", + "name": "apiKey" + } + } + } +} \ No newline at end of file diff --git a/python/tests/unit/connectors/openapi_plugin/oauth-securityV3_0.json b/python/tests/unit/connectors/openapi_plugin/oauth-securityV3_0.json new file mode 100644 index 000000000000..cd5655c8025c --- /dev/null +++ b/python/tests/unit/connectors/openapi_plugin/oauth-securityV3_0.json @@ -0,0 +1,89 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "Semantic Kernel Open API Sample", + "description": "A sample Open API schema with endpoints which have security requirements defined.", + "version": "1.0" + }, + "servers": [ + { + "url": "https://example.org" + } + ], + "paths": { + "/use_global_security": { + "get": { + "summary": "No security defined on operation", + "description": "", + "operationId": "NoSecurity", + "parameters": [], + "responses": { + "200": { + "description": "default" + } + } + }, + "post": { + "summary": "Security defined on operation", + "description": "", + "operationId": "Security", + "parameters": [], + "responses": { + "200": { + "description": "default" + } + }, + "security": [ + { + "OAuth2Auth": [] + } + ] + }, + "put": { + "summary": "Security defined on operation with new scope", + "description": "", + "operationId": "SecurityAndScope", + "parameters": [], + "responses": { + "200": { + "description": "default" + } + }, + "security": [ + { + "OAuth2Auth": [ "new_scope" ] + } + ] + } + } + }, + "components": { + "securitySchemes": { + "OAuth2Auth": { + "type": "oauth2", + "flows": { + "authorizationCode": { + "authorizationUrl": "https://login.windows.net/common/oauth2/authorize", + "tokenUrl": "https://login.windows.net/common/oauth2/authorize", + "scopes": {} + } + } + }, + "ApiKeyAuthHeader": { + "type": "apiKey", + "in": "header", + "name": "X-API-KEY" + }, + "ApiKeyAuthQuery": { + "type": "apiKey", + "in": "query", + "name": "apiKey" + } + } + }, + "security": [ + { + "OAuth2Auth": [] + } + ] +} \ No newline at end of file diff --git a/python/tests/unit/connectors/openapi_plugin/test_openapi_parser.py b/python/tests/unit/connectors/openapi_plugin/test_openapi_parser.py index 0e4d278a1667..5d99cc5a079f 100644 --- a/python/tests/unit/connectors/openapi_plugin/test_openapi_parser.py +++ b/python/tests/unit/connectors/openapi_plugin/test_openapi_parser.py @@ -4,6 +4,7 @@ import pytest +from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation import RestApiOperation from semantic_kernel.connectors.openapi_plugin.openapi_manager import OpenApiParser, create_functions_from_openapi from semantic_kernel.exceptions.function_exceptions import PluginInitializationError from semantic_kernel.functions import KernelFunctionFromMethod, KernelFunctionMetadata, KernelParameterMetadata @@ -109,3 +110,71 @@ def test_create_rest_api_operation_payload_media_type_none(): request_body = {"content": {"application/xml": {"schema": {"type": "object"}}}} with pytest.raises(Exception, match="Neither of the media types of operation_id is supported."): parser._create_rest_api_operation_payload("operation_id", request_body) + + +def generate_security_member_data(): + return [ + ( + "no-securityV3_0.json", + { + "NoSecurity": [], + "Security": ["apiKey"], + "SecurityAndScope": ["apiKey"], + }, + ), + ( + "apikey-securityV3_0.json", + { + "NoSecurity": [], + "Security": ["apiKey"], + "SecurityAndScope": ["apiKey"], + }, + ), + ( + "oauth-securityV3_0.json", + { + "NoSecurity": [], + "Security": ["oauth2"], + "SecurityAndScope": ["oauth2"], + }, + ), + ] + + +@pytest.mark.parametrize("document_file_name, security_type_map", generate_security_member_data()) +def test_it_adds_security_metadata_to_operation(document_file_name, security_type_map): + # Arrange + current_dir = os.path.dirname(__file__) + openapi_document_path = os.path.join(current_dir, document_file_name) + + # Act + plugin_functions = create_functions_from_openapi( + plugin_name="fakePlugin", + openapi_document_path=openapi_document_path, + execution_settings=None, + ) + + # Assert + for function in plugin_functions: + additional_properties = function.metadata.additional_properties + assert "operation" in additional_properties + + function_name = function.metadata.name + security_types = security_type_map.get(function_name, []) + + operation = additional_properties["operation"] + assert isinstance(operation, RestApiOperation) + assert operation is not None + assert operation.security_requirements is not None + assert len(operation.security_requirements) == len(security_types) + + for security_type in security_types: + found = False + for security_requirement in operation.security_requirements: + for scheme in security_requirement: + if scheme.security_scheme_type.lower() == security_type.lower(): + found = True + break + if found: + break + assert found, f"Security type '{security_type}' not found in operation '{operation.id}'" From 2c3cfae98f9bfe4725ca600deb669b200a876654 Mon Sep 17 00:00:00 2001 From: Evan Mattson Date: Tue, 12 Nov 2024 15:59:31 -0500 Subject: [PATCH 3/9] Remove unused classes. --- .../models/rest_api_oauth_flow.py | 15 - .../models/rest_api_oauth_flows.py | 22 -- python/uv.lock | 289 +++++++++++++++++- 3 files changed, 284 insertions(+), 42 deletions(-) delete mode 100644 python/semantic_kernel/connectors/openapi_plugin/models/rest_api_oauth_flow.py delete mode 100644 python/semantic_kernel/connectors/openapi_plugin/models/rest_api_oauth_flows.py diff --git a/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_oauth_flow.py b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_oauth_flow.py deleted file mode 100644 index f47ec59801b6..000000000000 --- a/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_oauth_flow.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -from semantic_kernel.utils.experimental_decorator import experimental_class - - -@experimental_class -class RestApiOAuthFlow: - """Represents the OAuth flow used by the REST API.""" - - def __init__(self, authorization_url: str, token_url: str, scopes: dict[str, str], refresh_url: str | None = None): - """Initializes a new instance of the RestApiOAuthFlow class.""" - self.authorization_url = authorization_url - self.token_url = token_url - self.refresh_url = refresh_url - self.scopes = scopes diff --git a/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_oauth_flows.py b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_oauth_flows.py deleted file mode 100644 index 4da0d4698ed1..000000000000 --- a/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_oauth_flows.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -from semantic_kernel.connectors.openapi_plugin.models.rest_api_oauth_flow import RestApiOAuthFlow -from semantic_kernel.utils.experimental_decorator import experimental_class - - -@experimental_class -class RestApiOAuthFlows: - """Represents the OAuth flows used by the REST API.""" - - def __init__( - self, - implicit: RestApiOAuthFlow | None = None, - password: RestApiOAuthFlow | None = None, - client_credentials: RestApiOAuthFlow | None = None, - authorization_code: RestApiOAuthFlow | None = None, - ): - """Initializes a new instance of the RestApiOAuthFlows class.""" - self.implicit = implicit - self.password = password - self.client_credentials = client_credentials - self.authorization_code = authorization_code diff --git a/python/uv.lock b/python/uv.lock index ff0b4ae00c8d..7f5ac60551d2 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -5,17 +5,20 @@ resolution-markers = [ "python_full_version == '3.11.*' and sys_platform == 'darwin'", "python_full_version < '3.11' and sys_platform == 'darwin'", "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", "python_full_version < '3.11' and sys_platform == 'linux'", "python_full_version == '3.11.*' and sys_platform == 'linux'", "python_full_version < '3.11' and sys_platform == 'linux'", "python_full_version == '3.11.*' and sys_platform == 'linux'", - "python_full_version >= '3.12' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'linux'", "python_full_version < '3.11' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version < '3.11' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version >= '3.13' and sys_platform == 'win32'", ] supported-markers = [ "sys_platform == 'darwin'", @@ -110,6 +113,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/05/be/6a403b464dcab3631fe8e27b0f1d906d9e45c5e92aca97ee007e5a895560/aiohttp-3.10.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c1277cd707c465cd09572a774559a3cc7c7a28802eb3a2a9472588f062097205", size = 1306186 }, { url = "https://files.pythonhosted.org/packages/8e/fd/bb50fe781068a736a02bf5c7ad5f3ab53e39f1d1e63110da6d30f7605edc/aiohttp-3.10.10-cp312-cp312-win32.whl", hash = "sha256:59bb3c54aa420521dc4ce3cc2c3fe2ad82adf7b09403fa1f48ae45c0cbde6628", size = 359289 }, { url = "https://files.pythonhosted.org/packages/70/9e/5add7e240f77ef67c275c82cc1d08afbca57b77593118c1f6e920ae8ad3f/aiohttp-3.10.10-cp312-cp312-win_amd64.whl", hash = "sha256:0e1b370d8007c4ae31ee6db7f9a2fe801a42b146cec80a86766e7ad5c4a259cf", size = 379313 }, + { url = "https://files.pythonhosted.org/packages/b1/eb/618b1b76c7fe8082a71c9d62e3fe84c5b9af6703078caa9ec57850a12080/aiohttp-3.10.10-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ad7593bb24b2ab09e65e8a1d385606f0f47c65b5a2ae6c551db67d6653e78c28", size = 576114 }, + { url = "https://files.pythonhosted.org/packages/aa/37/3126995d7869f8b30d05381b81a2d4fb4ec6ad313db788e009bc6d39c211/aiohttp-3.10.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1eb89d3d29adaf533588f209768a9c02e44e4baf832b08118749c5fad191781d", size = 391901 }, + { url = "https://files.pythonhosted.org/packages/3e/f2/8fdfc845be1f811c31ceb797968523813f8e1263ee3e9120d61253f6848f/aiohttp-3.10.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3fe407bf93533a6fa82dece0e74dbcaaf5d684e5a51862887f9eaebe6372cd79", size = 387418 }, + { url = "https://files.pythonhosted.org/packages/60/d5/33d2061d36bf07e80286e04b7e0a4de37ce04b5ebfed72dba67659a05250/aiohttp-3.10.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50aed5155f819873d23520919e16703fc8925e509abbb1a1491b0087d1cd969e", size = 1287073 }, + { url = "https://files.pythonhosted.org/packages/00/52/affb55be16a4747740bd630b4c002dac6c5eac42f9bb64202fc3cf3f1930/aiohttp-3.10.10-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4f05e9727ce409358baa615dbeb9b969db94324a79b5a5cea45d39bdb01d82e6", size = 1323612 }, + { url = "https://files.pythonhosted.org/packages/94/f2/cddb69b975387daa2182a8442566971d6410b8a0179bb4540d81c97b1611/aiohttp-3.10.10-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dffb610a30d643983aeb185ce134f97f290f8935f0abccdd32c77bed9388b42", size = 1368406 }, + { url = "https://files.pythonhosted.org/packages/c1/e4/afba7327da4d932da8c6e29aecaf855f9d52dace53ac15bfc8030a246f1b/aiohttp-3.10.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa6658732517ddabe22c9036479eabce6036655ba87a0224c612e1ae6af2087e", size = 1282761 }, + { url = "https://files.pythonhosted.org/packages/9f/6b/364856faa0c9031ea76e24ef0f7fef79cddd9fa8e7dba9a1771c6acc56b5/aiohttp-3.10.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:741a46d58677d8c733175d7e5aa618d277cd9d880301a380fd296975a9cdd7bc", size = 1236518 }, + { url = "https://files.pythonhosted.org/packages/46/af/c382846f8356fe64a7b5908bb9b477457aa23b71be7ed551013b7b7d4d87/aiohttp-3.10.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e00e3505cd80440f6c98c6d69269dcc2a119f86ad0a9fd70bccc59504bebd68a", size = 1250344 }, + { url = "https://files.pythonhosted.org/packages/87/53/294f87fc086fd0772d0ab82497beb9df67f0f27a8b3dd5742a2656db2bc6/aiohttp-3.10.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ffe595f10566f8276b76dc3a11ae4bb7eba1aac8ddd75811736a15b0d5311414", size = 1248956 }, + { url = "https://files.pythonhosted.org/packages/86/30/7d746717fe11bdfefb88bb6c09c5fc985d85c4632da8bb6018e273899254/aiohttp-3.10.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdfcf6443637c148c4e1a20c48c566aa694fa5e288d34b20fcdc58507882fed3", size = 1293379 }, + { url = "https://files.pythonhosted.org/packages/48/b9/45d670a834458db67a24258e9139ba61fa3bd7d69b98ecf3650c22806f8f/aiohttp-3.10.10-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d183cf9c797a5291e8301790ed6d053480ed94070637bfaad914dd38b0981f67", size = 1320108 }, + { url = "https://files.pythonhosted.org/packages/72/8c/804bb2e837a175635d2000a0659eafc15b2e9d92d3d81c8f69e141ecd0b0/aiohttp-3.10.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:77abf6665ae54000b98b3c742bc6ea1d1fb31c394bcabf8b5d2c1ac3ebfe7f3b", size = 1281546 }, + { url = "https://files.pythonhosted.org/packages/89/c0/862e6a9de3d6eeb126cd9d9ea388243b70df9b871ce1a42b193b7a4a77fc/aiohttp-3.10.10-cp313-cp313-win32.whl", hash = "sha256:4470c73c12cd9109db8277287d11f9dd98f77fc54155fc71a7738a83ffcc8ea8", size = 357516 }, + { url = "https://files.pythonhosted.org/packages/ae/63/3e1aee3e554263f3f1011cca50d78a4894ae16ce99bf78101ac3a2f0ef74/aiohttp-3.10.10-cp313-cp313-win_amd64.whl", hash = "sha256:486f7aabfa292719a2753c016cc3a8f8172965cabb3ea2e7f7436c7f5a22a151", size = 376785 }, ] [[package]] @@ -564,6 +582,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f7/fa/d3fc622de05a86f30beea5fc4e9ac46aead4731e73fd9055496732bcc0a4/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", size = 144800 }, { url = "https://files.pythonhosted.org/packages/9a/65/bdb9bc496d7d190d725e96816e20e2ae3a6fa42a5cac99c3c3d6ff884118/charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", size = 94836 }, { url = "https://files.pythonhosted.org/packages/3e/67/7b72b69d25b89c0b3cea583ee372c43aa24df15f0e0f8d3982c57804984b/charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", size = 102187 }, + { url = "https://files.pythonhosted.org/packages/f3/89/68a4c86f1a0002810a27f12e9a7b22feb198c59b2f05231349fbce5c06f4/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", size = 194617 }, + { url = "https://files.pythonhosted.org/packages/4f/cd/8947fe425e2ab0aa57aceb7807af13a0e4162cd21eee42ef5b053447edf5/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", size = 125310 }, + { url = "https://files.pythonhosted.org/packages/5b/f0/b5263e8668a4ee9becc2b451ed909e9c27058337fda5b8c49588183c267a/charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", size = 119126 }, + { url = "https://files.pythonhosted.org/packages/ff/6e/e445afe4f7fda27a533f3234b627b3e515a1b9429bc981c9a5e2aa5d97b6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", size = 139342 }, + { url = "https://files.pythonhosted.org/packages/a1/b2/4af9993b532d93270538ad4926c8e37dc29f2111c36f9c629840c57cd9b3/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", size = 149383 }, + { url = "https://files.pythonhosted.org/packages/fb/6f/4e78c3b97686b871db9be6f31d64e9264e889f8c9d7ab33c771f847f79b7/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", size = 142214 }, + { url = "https://files.pythonhosted.org/packages/2b/c9/1c8fe3ce05d30c87eff498592c89015b19fade13df42850aafae09e94f35/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", size = 144104 }, + { url = "https://files.pythonhosted.org/packages/ee/68/efad5dcb306bf37db7db338338e7bb8ebd8cf38ee5bbd5ceaaaa46f257e6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", size = 146255 }, + { url = "https://files.pythonhosted.org/packages/0c/75/1ed813c3ffd200b1f3e71121c95da3f79e6d2a96120163443b3ad1057505/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", size = 140251 }, + { url = "https://files.pythonhosted.org/packages/7d/0d/6f32255c1979653b448d3c709583557a4d24ff97ac4f3a5be156b2e6a210/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", size = 148474 }, + { url = "https://files.pythonhosted.org/packages/ac/a0/c1b5298de4670d997101fef95b97ac440e8c8d8b4efa5a4d1ef44af82f0d/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", size = 151849 }, + { url = "https://files.pythonhosted.org/packages/04/4f/b3961ba0c664989ba63e30595a3ed0875d6790ff26671e2aae2fdc28a399/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", size = 149781 }, + { url = "https://files.pythonhosted.org/packages/d8/90/6af4cd042066a4adad58ae25648a12c09c879efa4849c705719ba1b23d8c/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482", size = 144970 }, + { url = "https://files.pythonhosted.org/packages/cc/67/e5e7e0cbfefc4ca79025238b43cdf8a2037854195b37d6417f3d0895c4c2/charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", size = 94973 }, + { url = "https://files.pythonhosted.org/packages/65/97/fc9bbc54ee13d33dc54a7fcf17b26368b18505500fc01e228c27b5222d80/charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", size = 102308 }, { url = "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", size = 49446 }, ] @@ -733,6 +766,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ce/9c/4337f468ef0ab7a2e0887a9c9da0e58e2eada6fc6cbee637a4acd5dfd8a9/coverage-7.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cf717ee42012be8c0cb205dbbf18ffa9003c4cbf4ad078db47b95e10748eec5", size = 238824 }, { url = "https://files.pythonhosted.org/packages/5e/09/3e94912b8dd37251377bb02727a33a67ee96b84bbbe092f132b401ca5dd9/coverage-7.6.4-cp312-cp312-win32.whl", hash = "sha256:7bb92c539a624cf86296dd0c68cd5cc286c9eef2d0c3b8b192b604ce9de20a17", size = 209639 }, { url = "https://files.pythonhosted.org/packages/01/69/d4f3a4101171f32bc5b3caec8ff94c2c60f700107a6aaef7244b2c166793/coverage-7.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:1032e178b76a4e2b5b32e19d0fd0abbce4b58e77a1ca695820d10e491fa32b08", size = 210428 }, + { url = "https://files.pythonhosted.org/packages/c2/4d/2dede4f7cb5a70fb0bb40a57627fddf1dbdc6b9c1db81f7c4dcdcb19e2f4/coverage-7.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:023bf8ee3ec6d35af9c1c6ccc1d18fa69afa1cb29eaac57cb064dbb262a517f9", size = 207039 }, + { url = "https://files.pythonhosted.org/packages/3f/f9/d86368ae8c79e28f1fb458ebc76ae9ff3e8bd8069adc24e8f2fed03c58b7/coverage-7.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0ac3d42cb51c4b12df9c5f0dd2f13a4f24f01943627120ec4d293c9181219ba", size = 207298 }, + { url = "https://files.pythonhosted.org/packages/64/c5/b4cc3c3f64622c58fbfd4d8b9a7a8ce9d355f172f91fcabbba1f026852f6/coverage-7.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8fe4984b431f8621ca53d9380901f62bfb54ff759a1348cd140490ada7b693c", size = 239813 }, + { url = "https://files.pythonhosted.org/packages/8a/86/14c42e60b70a79b26099e4d289ccdfefbc68624d096f4481163085aa614c/coverage-7.6.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5fbd612f8a091954a0c8dd4c0b571b973487277d26476f8480bfa4b2a65b5d06", size = 236959 }, + { url = "https://files.pythonhosted.org/packages/7f/f8/4436a643631a2fbab4b44d54f515028f6099bfb1cd95b13cfbf701e7f2f2/coverage-7.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dacbc52de979f2823a819571f2e3a350a7e36b8cb7484cdb1e289bceaf35305f", size = 238950 }, + { url = "https://files.pythonhosted.org/packages/49/50/1571810ddd01f99a0a8be464a4ac8b147f322cd1e8e296a1528984fc560b/coverage-7.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dab4d16dfef34b185032580e2f2f89253d302facba093d5fa9dbe04f569c4f4b", size = 238610 }, + { url = "https://files.pythonhosted.org/packages/f3/8c/6312d241fe7cbd1f0cade34a62fea6f333d1a261255d76b9a87074d8703c/coverage-7.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:862264b12ebb65ad8d863d51f17758b1684560b66ab02770d4f0baf2ff75da21", size = 236697 }, + { url = "https://files.pythonhosted.org/packages/ce/5f/fef33dfd05d87ee9030f614c857deb6df6556b8f6a1c51bbbb41e24ee5ac/coverage-7.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5beb1ee382ad32afe424097de57134175fea3faf847b9af002cc7895be4e2a5a", size = 238541 }, + { url = "https://files.pythonhosted.org/packages/a9/64/6a984b6e92e1ea1353b7ffa08e27f707a5e29b044622445859200f541e8c/coverage-7.6.4-cp313-cp313-win32.whl", hash = "sha256:bf20494da9653f6410213424f5f8ad0ed885e01f7e8e59811f572bdb20b8972e", size = 209707 }, + { url = "https://files.pythonhosted.org/packages/5c/60/ce5a9e942e9543783b3db5d942e0578b391c25cdd5e7f342d854ea83d6b7/coverage-7.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:182e6cd5c040cec0a1c8d415a87b67ed01193ed9ad458ee427741c7d8513d963", size = 210439 }, + { url = "https://files.pythonhosted.org/packages/78/53/6719677e92c308207e7f10561a1b16ab8b5c00e9328efc9af7cfd6fb703e/coverage-7.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a181e99301a0ae128493a24cfe5cfb5b488c4e0bf2f8702091473d033494d04f", size = 207784 }, + { url = "https://files.pythonhosted.org/packages/fa/dd/7054928930671fcb39ae6a83bb71d9ab5f0afb733172543ced4b09a115ca/coverage-7.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:df57bdbeffe694e7842092c5e2e0bc80fff7f43379d465f932ef36f027179806", size = 208058 }, + { url = "https://files.pythonhosted.org/packages/b5/7d/fd656ddc2b38301927b9eb3aae3fe827e7aa82e691923ed43721fd9423c9/coverage-7.6.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bcd1069e710600e8e4cf27f65c90c7843fa8edfb4520fb0ccb88894cad08b11", size = 250772 }, + { url = "https://files.pythonhosted.org/packages/90/d0/eb9a3cc2100b83064bb086f18aedde3afffd7de6ead28f69736c00b7f302/coverage-7.6.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99b41d18e6b2a48ba949418db48159d7a2e81c5cc290fc934b7d2380515bd0e3", size = 246490 }, + { url = "https://files.pythonhosted.org/packages/45/44/3f64f38f6faab8a0cfd2c6bc6eb4c6daead246b97cf5f8fc23bf3788f841/coverage-7.6.4-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1e54712ba3474f34b7ef7a41e65bd9037ad47916ccb1cc78769bae324c01a", size = 248848 }, + { url = "https://files.pythonhosted.org/packages/5d/11/4c465a5f98656821e499f4b4619929bd5a34639c466021740ecdca42aa30/coverage-7.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:53d202fd109416ce011578f321460795abfe10bb901b883cafd9b3ef851bacfc", size = 248340 }, + { url = "https://files.pythonhosted.org/packages/f1/96/ebecda2d016cce9da812f404f720ca5df83c6b29f65dc80d2000d0078741/coverage-7.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:c48167910a8f644671de9f2083a23630fbf7a1cb70ce939440cd3328e0919f70", size = 246229 }, + { url = "https://files.pythonhosted.org/packages/16/d9/3d820c00066ae55d69e6d0eae11d6149a5ca7546de469ba9d597f01bf2d7/coverage-7.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc8ff50b50ce532de2fa7a7daae9dd12f0a699bfcd47f20945364e5c31799fef", size = 247510 }, + { url = "https://files.pythonhosted.org/packages/8f/c3/4fa1eb412bb288ff6bfcc163c11700ff06e02c5fad8513817186e460ed43/coverage-7.6.4-cp313-cp313t-win32.whl", hash = "sha256:b8d3a03d9bfcaf5b0141d07a88456bb6a4c3ce55c080712fec8418ef3610230e", size = 210353 }, + { url = "https://files.pythonhosted.org/packages/7e/77/03fc2979d1538884d921c2013075917fc927f41cd8526909852fe4494112/coverage-7.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:f3ddf056d3ebcf6ce47bdaf56142af51bb7fad09e4af310241e9db7a3a8022e1", size = 211502 }, { url = "https://files.pythonhosted.org/packages/cc/56/e1d75e8981a2a92c2a777e67c26efa96c66da59d645423146eb9ff3a851b/coverage-7.6.4-pp39.pp310-none-any.whl", hash = "sha256:3c65d37f3a9ebb703e710befdc489a38683a5b152242664b973a7b7b22348a4e", size = 198954 }, ] @@ -792,6 +845,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/80/79/8bba39190d2ea17840925d287f1c6c3a7c60b58f5090444e9ecf176c540f/debugpy-1.8.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:703c1fd62ae0356e194f3e7b7a92acd931f71fe81c4b3be2c17a7b8a4b546ec2", size = 4170911 }, { url = "https://files.pythonhosted.org/packages/3b/19/5b3d312936db8eb281310fa27903459328ed722d845d594ba5feaeb2f0b3/debugpy-1.8.7-cp312-cp312-win32.whl", hash = "sha256:2f729228430ef191c1e4df72a75ac94e9bf77413ce5f3f900018712c9da0aaca", size = 5195476 }, { url = "https://files.pythonhosted.org/packages/9f/49/ad20b29f8c921fd5124530d3d39b8f2077efd51b71339a2eff02bba693e9/debugpy-1.8.7-cp312-cp312-win_amd64.whl", hash = "sha256:45c30aaefb3e1975e8a0258f5bbd26cd40cde9bfe71e9e5a7ac82e79bad64e39", size = 5235031 }, + { url = "https://files.pythonhosted.org/packages/41/95/29b247518d0a6afdb5249f5d05743c9c5bfaf4bd13a85b81cb5e1dc65837/debugpy-1.8.7-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:d050a1ec7e925f514f0f6594a1e522580317da31fbda1af71d1530d6ea1f2b40", size = 2517557 }, + { url = "https://files.pythonhosted.org/packages/4d/93/026e2000a0740e2f54b198f8dc317accf3a70b6524b2b15fa8e6eca74414/debugpy-1.8.7-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2f4349a28e3228a42958f8ddaa6333d6f8282d5edaea456070e48609c5983b7", size = 4162703 }, + { url = "https://files.pythonhosted.org/packages/c3/92/a48e653b19a171434290ecdc5935b7a292a65488139c5271d6d0eceeb0f1/debugpy-1.8.7-cp313-cp313-win32.whl", hash = "sha256:11ad72eb9ddb436afb8337891a986302e14944f0f755fd94e90d0d71e9100bba", size = 5195220 }, + { url = "https://files.pythonhosted.org/packages/4e/b3/dc3c5527edafcd1a6d0f8c4ecc6c5c9bc431f77340cf4193328e98f0ac38/debugpy-1.8.7-cp313-cp313-win_amd64.whl", hash = "sha256:2efb84d6789352d7950b03d7f866e6d180284bc02c7e12cb37b489b7083d81aa", size = 5235333 }, { url = "https://files.pythonhosted.org/packages/51/b1/a0866521c71a6ae3d3ca320e74835163a4671b1367ba360a55a0a51e5a91/debugpy-1.8.7-py2.py3-none-any.whl", hash = "sha256:57b00de1c8d2c84a61b90880f7e5b6deaf4c312ecbde3a0e8912f2a56c4ac9ae", size = 5210683 }, ] @@ -1014,6 +1071,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/37/e0/47f87544055b3349b633a03c4d94b405956cf2437f4ab46d0928b74b7526/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f", size = 280569 }, { url = "https://files.pythonhosted.org/packages/f9/7c/490133c160fb6b84ed374c266f42800e33b50c3bbab1652764e6e1fc498a/frozenlist-1.5.0-cp312-cp312-win32.whl", hash = "sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8", size = 44721 }, { url = "https://files.pythonhosted.org/packages/b1/56/4e45136ffc6bdbfa68c29ca56ef53783ef4c2fd395f7cbf99a2624aa9aaa/frozenlist-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f", size = 51329 }, + { url = "https://files.pythonhosted.org/packages/da/3b/915f0bca8a7ea04483622e84a9bd90033bab54bdf485479556c74fd5eaf5/frozenlist-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953", size = 91538 }, + { url = "https://files.pythonhosted.org/packages/c7/d1/a7c98aad7e44afe5306a2b068434a5830f1470675f0e715abb86eb15f15b/frozenlist-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0", size = 52849 }, + { url = "https://files.pythonhosted.org/packages/3a/c8/76f23bf9ab15d5f760eb48701909645f686f9c64fbb8982674c241fbef14/frozenlist-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2", size = 50583 }, + { url = "https://files.pythonhosted.org/packages/1f/22/462a3dd093d11df623179d7754a3b3269de3b42de2808cddef50ee0f4f48/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f", size = 265636 }, + { url = "https://files.pythonhosted.org/packages/80/cf/e075e407fc2ae7328155a1cd7e22f932773c8073c1fc78016607d19cc3e5/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608", size = 270214 }, + { url = "https://files.pythonhosted.org/packages/a1/58/0642d061d5de779f39c50cbb00df49682832923f3d2ebfb0fedf02d05f7f/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b", size = 273905 }, + { url = "https://files.pythonhosted.org/packages/ab/66/3fe0f5f8f2add5b4ab7aa4e199f767fd3b55da26e3ca4ce2cc36698e50c4/frozenlist-1.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840", size = 250542 }, + { url = "https://files.pythonhosted.org/packages/f6/b8/260791bde9198c87a465224e0e2bb62c4e716f5d198fc3a1dacc4895dbd1/frozenlist-1.5.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439", size = 267026 }, + { url = "https://files.pythonhosted.org/packages/2e/a4/3d24f88c527f08f8d44ade24eaee83b2627793fa62fa07cbb7ff7a2f7d42/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de", size = 257690 }, + { url = "https://files.pythonhosted.org/packages/de/9a/d311d660420b2beeff3459b6626f2ab4fb236d07afbdac034a4371fe696e/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641", size = 253893 }, + { url = "https://files.pythonhosted.org/packages/c6/23/e491aadc25b56eabd0f18c53bb19f3cdc6de30b2129ee0bc39cd387cd560/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e", size = 267006 }, + { url = "https://files.pythonhosted.org/packages/08/c4/ab918ce636a35fb974d13d666dcbe03969592aeca6c3ab3835acff01f79c/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9", size = 276157 }, + { url = "https://files.pythonhosted.org/packages/c0/29/3b7a0bbbbe5a34833ba26f686aabfe982924adbdcafdc294a7a129c31688/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03", size = 264642 }, + { url = "https://files.pythonhosted.org/packages/ab/42/0595b3dbffc2e82d7fe658c12d5a5bafcd7516c6bf2d1d1feb5387caa9c1/frozenlist-1.5.0-cp313-cp313-win32.whl", hash = "sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c", size = 44914 }, + { url = "https://files.pythonhosted.org/packages/17/c4/b7db1206a3fea44bf3b838ca61deb6f74424a8a5db1dd53ecb21da669be6/frozenlist-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28", size = 51167 }, { url = "https://files.pythonhosted.org/packages/c6/c8/a5be5b7550c10858fcf9b0ea054baccab474da77d37f1e828ce043a3a5d4/frozenlist-1.5.0-py3-none-any.whl", hash = "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3", size = 11901 }, ] @@ -1312,6 +1384,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/94/16550ad6b3f13b96f0856ee5dfc2554efac28539ee84a51d7b14526da985/grpcio-1.67.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:699e964923b70f3101393710793289e42845791ea07565654ada0969522d0a38", size = 6149369 }, { url = "https://files.pythonhosted.org/packages/33/0d/4c3b2587e8ad7f121b597329e6c2620374fccbc2e4e1aa3c73ccc670fde4/grpcio-1.67.1-cp312-cp312-win32.whl", hash = "sha256:4e7b904484a634a0fff132958dabdb10d63e0927398273917da3ee103e8d1f78", size = 3599176 }, { url = "https://files.pythonhosted.org/packages/7d/36/0c03e2d80db69e2472cf81c6123aa7d14741de7cf790117291a703ae6ae1/grpcio-1.67.1-cp312-cp312-win_amd64.whl", hash = "sha256:5721e66a594a6c4204458004852719b38f3d5522082be9061d6510b455c90afc", size = 4346574 }, + { url = "https://files.pythonhosted.org/packages/12/d2/2f032b7a153c7723ea3dea08bffa4bcaca9e0e5bdf643ce565b76da87461/grpcio-1.67.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:aa0162e56fd10a5547fac8774c4899fc3e18c1aa4a4759d0ce2cd00d3696ea6b", size = 5091487 }, + { url = "https://files.pythonhosted.org/packages/d0/ae/ea2ff6bd2475a082eb97db1104a903cf5fc57c88c87c10b3c3f41a184fc0/grpcio-1.67.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:beee96c8c0b1a75d556fe57b92b58b4347c77a65781ee2ac749d550f2a365dc1", size = 10943530 }, + { url = "https://files.pythonhosted.org/packages/07/62/646be83d1a78edf8d69b56647327c9afc223e3140a744c59b25fbb279c3b/grpcio-1.67.1-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:a93deda571a1bf94ec1f6fcda2872dad3ae538700d94dc283c672a3b508ba3af", size = 5589079 }, + { url = "https://files.pythonhosted.org/packages/d0/25/71513d0a1b2072ce80d7f5909a93596b7ed10348b2ea4fdcbad23f6017bf/grpcio-1.67.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e6f255980afef598a9e64a24efce87b625e3e3c80a45162d111a461a9f92955", size = 6213542 }, + { url = "https://files.pythonhosted.org/packages/76/9a/d21236297111052dcb5dc85cd77dc7bf25ba67a0f55ae028b2af19a704bc/grpcio-1.67.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e838cad2176ebd5d4a8bb03955138d6589ce9e2ce5d51c3ada34396dbd2dba8", size = 5850211 }, + { url = "https://files.pythonhosted.org/packages/2d/fe/70b1da9037f5055be14f359026c238821b9bcf6ca38a8d760f59a589aacd/grpcio-1.67.1-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:a6703916c43b1d468d0756c8077b12017a9fcb6a1ef13faf49e67d20d7ebda62", size = 6572129 }, + { url = "https://files.pythonhosted.org/packages/74/0d/7df509a2cd2a54814598caf2fb759f3e0b93764431ff410f2175a6efb9e4/grpcio-1.67.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:917e8d8994eed1d86b907ba2a61b9f0aef27a2155bca6cbb322430fc7135b7bb", size = 6149819 }, + { url = "https://files.pythonhosted.org/packages/0a/08/bc3b0155600898fd10f16b79054e1cca6cb644fa3c250c0fe59385df5e6f/grpcio-1.67.1-cp313-cp313-win32.whl", hash = "sha256:e279330bef1744040db8fc432becc8a727b84f456ab62b744d3fdb83f327e121", size = 3596561 }, + { url = "https://files.pythonhosted.org/packages/5a/96/44759eca966720d0f3e1b105c43f8ad4590c97bf8eb3cd489656e9590baa/grpcio-1.67.1-cp313-cp313-win_amd64.whl", hash = "sha256:fa0c739ad8b1996bd24823950e3cb5152ae91fca1c09cc791190bf1627ffefba", size = 4346042 }, ] [[package]] @@ -1379,6 +1460,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/0d/a5d703214fe49d261b4b8f0a64140a4dc1f88560724a38ad937120b899ad/grpcio_tools-1.67.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:db9e87f6ea4b0ce99b2651203480585fd9e8dd0dd122a19e46836e93e3a1b749", size = 2870421 }, { url = "https://files.pythonhosted.org/packages/ac/af/41d79cb87eae99c0348e8f1fb3dbed9e40a6f63548b216e99f4d1165fa5c/grpcio_tools-1.67.1-cp312-cp312-win32.whl", hash = "sha256:6a595a872fb720dde924c4e8200f41d5418dd6baab8cc1a3c1e540f8f4596351", size = 940542 }, { url = "https://files.pythonhosted.org/packages/66/e5/096e12f5319835aa2bcb746d49ae62220bb48313ca649e89bdbef605c11d/grpcio_tools-1.67.1-cp312-cp312-win_amd64.whl", hash = "sha256:92eebb9b31031604ae97ea7657ae2e43149b0394af7117ad7e15894b6cc136dc", size = 1090425 }, + { url = "https://files.pythonhosted.org/packages/62/b3/91c88440c978740752d39f1abae83f21408048b98b93652ebd84f974ad3d/grpcio_tools-1.67.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:9a3b9510cc87b6458b05ad49a6dee38df6af37f9ee6aa027aa086537798c3d4a", size = 2307453 }, + { url = "https://files.pythonhosted.org/packages/05/33/faf3330825463c0409fa3891bc1459bf86a00055b19790211365279538d7/grpcio_tools-1.67.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9e4c9b9fa9b905f15d414cb7bd007ba7499f8907bdd21231ab287a86b27da81a", size = 5517975 }, + { url = "https://files.pythonhosted.org/packages/bd/78/461ab34cadbd0b5b9a0b6efedda96b58e0de471e3fa91d8e4a4e31924e1b/grpcio_tools-1.67.1-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:e11a98b41af4bc88b7a738232b8fa0306ad82c79fa5d7090bb607f183a57856f", size = 2281081 }, + { url = "https://files.pythonhosted.org/packages/5f/0c/b30bdbcab1795b12e05adf30c20981c14f66198e22044edb15b3c1d9f0bc/grpcio_tools-1.67.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de0fcfe61c26679d64b1710746f2891f359593f76894fcf492c37148d5694f00", size = 2616929 }, + { url = "https://files.pythonhosted.org/packages/d3/c2/a77ca68ae768f8d5f1d070ea4afc42fda40401083e7c4f5c08211e84de38/grpcio_tools-1.67.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ae3b3e2ee5aad59dece65a613624c46a84c9582fc3642686537c6dfae8e47dc", size = 2414633 }, + { url = "https://files.pythonhosted.org/packages/39/70/8d7131dccfe4d7b739c96ada7ea9acde631f58f013eae773791fb490a3eb/grpcio_tools-1.67.1-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:9a630f83505b6471a3094a7a372a1240de18d0cd3e64f4fbf46b361bac2be65b", size = 3224328 }, + { url = "https://files.pythonhosted.org/packages/2a/28/2d24b933ccf0d6877035aa3d5f8b64aad18c953657dd43c682b5701dc127/grpcio_tools-1.67.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d85a1fcbacd3e08dc2b3d1d46b749351a9a50899fa35cf2ff040e1faf7d405ad", size = 2869640 }, + { url = "https://files.pythonhosted.org/packages/37/77/ddd2b4cc896639fb0f85fc21d5684f25080ee28845c5a4031e3dd65fdc92/grpcio_tools-1.67.1-cp313-cp313-win32.whl", hash = "sha256:778470f025f25a1fca5a48c93c0a18af395b46b12dd8df7fca63736b85181f41", size = 939997 }, + { url = "https://files.pythonhosted.org/packages/96/d0/f0855a0ccb26ffeb41e6db68b5cbb25d7e9ba1f8f19151eef36210e64efc/grpcio_tools-1.67.1-cp313-cp313-win_amd64.whl", hash = "sha256:6961da86e9856b4ddee0bf51ef6636b4bf9c29c0715aa71f3c8f027c45d42654", size = 1089819 }, ] [[package]] @@ -1523,6 +1613,13 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/52/d8/254d16a31d543073a0e57f1c329ca7378d8924e7e292eda72d0064987486/httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81", size = 485289 }, { url = "https://files.pythonhosted.org/packages/5f/3c/4aee161b4b7a971660b8be71a92c24d6c64372c1ab3ae7f366b3680df20f/httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f", size = 489779 }, { url = "https://files.pythonhosted.org/packages/12/b7/5cae71a8868e555f3f67a50ee7f673ce36eac970f029c0c5e9d584352961/httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970", size = 88634 }, + { url = "https://files.pythonhosted.org/packages/94/a3/9fe9ad23fd35f7de6b91eeb60848986058bd8b5a5c1e256f5860a160cc3e/httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660", size = 197214 }, + { url = "https://files.pythonhosted.org/packages/ea/d9/82d5e68bab783b632023f2fa31db20bebb4e89dfc4d2293945fd68484ee4/httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083", size = 102431 }, + { url = "https://files.pythonhosted.org/packages/96/c1/cb499655cbdbfb57b577734fde02f6fa0bbc3fe9fb4d87b742b512908dff/httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3", size = 473121 }, + { url = "https://files.pythonhosted.org/packages/af/71/ee32fd358f8a3bb199b03261f10921716990808a675d8160b5383487a317/httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071", size = 473805 }, + { url = "https://files.pythonhosted.org/packages/8a/0a/0d4df132bfca1507114198b766f1737d57580c9ad1cf93c1ff673e3387be/httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5", size = 448858 }, + { url = "https://files.pythonhosted.org/packages/1e/6a/787004fdef2cabea27bad1073bf6a33f2437b4dbd3b6fb4a9d71172b1c7c/httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0", size = 452042 }, + { url = "https://files.pythonhosted.org/packages/4d/dc/7decab5c404d1d2cdc1bb330b1bf70e83d6af0396fd4fc76fc60c0d522bf/httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8", size = 87682 }, ] [[package]] @@ -1754,6 +1851,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/af/9d/816d2d7f19070b72cf0133437cbacf99a9202f6fbbc2cfa2111fb686b0e0/jiter-0.7.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:15cf691ebd8693b70c94627d6b748f01e6d697d9a6e9f2bc310934fcfb7cf25e", size = 498050 }, { url = "https://files.pythonhosted.org/packages/3e/a5/d0afb758c02d2d3c8ac3214a5be26579594d790944eaee7a47af06915e0e/jiter-0.7.0-cp312-none-win32.whl", hash = "sha256:9dcd54fa422fb66ca398bec296fed5f58e756aa0589496011cfea2abb5be38a5", size = 198912 }, { url = "https://files.pythonhosted.org/packages/d0/8e/80b2afd0391a3530966d8fc2f9c104955ba41093b3c319ae40b25e68e323/jiter-0.7.0-cp312-none-win_amd64.whl", hash = "sha256:cc989951f73f9375b8eacd571baaa057f3d7d11b7ce6f67b9d54642e7475bfad", size = 199942 }, + { url = "https://files.pythonhosted.org/packages/50/bb/82c7180dc126687ddcc25386727b3a1688ab8eff496afe7838b69886fcc7/jiter-0.7.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:24cecd18df540963cd27c08ca5ce1d0179f229ff78066d9eecbe5add29361340", size = 292624 }, + { url = "https://files.pythonhosted.org/packages/11/c2/3b6d4596eab2ff81ebfe5bab779f457433cc2ffb8a2d1d6ab5ac187f26f6/jiter-0.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d41b46236b90b043cca73785674c23d2a67d16f226394079d0953f94e765ed76", size = 304723 }, + { url = "https://files.pythonhosted.org/packages/49/65/56f78dfccfb22e43815cad4a468b4360f8cfebecc024edb5e2a625b83a04/jiter-0.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b160db0987171365c153e406a45dcab0ee613ae3508a77bfff42515cb4ce4d6e", size = 328319 }, + { url = "https://files.pythonhosted.org/packages/fd/f2/9e3ed9ac0b122dd65250fc83cd0f0979da82f055ef6041411191f6301284/jiter-0.7.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d1c8d91e0f0bd78602eaa081332e8ee4f512c000716f5bc54e9a037306d693a7", size = 347323 }, + { url = "https://files.pythonhosted.org/packages/42/18/24517f9f8575daf36fdac9dd53fcecde3d4c5bdd9f7b97a55e26ed2555b5/jiter-0.7.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:997706c683195eeff192d2e5285ce64d2a610414f37da3a3f2625dcf8517cf90", size = 374073 }, + { url = "https://files.pythonhosted.org/packages/a1/b1/b368ccdeff3eabb4b293a21a94317a6f717ecc5bfbfca4eecd12ff39da3f/jiter-0.7.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7ea52a8a0ff0229ab2920284079becd2bae0688d432fca94857ece83bb49c541", size = 388224 }, + { url = "https://files.pythonhosted.org/packages/92/1e/cc3d0655bcbc026e4b7746cb1ccab10d6eb2c29ffa64e574072db4d55f73/jiter-0.7.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d77449d2738cf74752bb35d75ee431af457e741124d1db5e112890023572c7c", size = 326145 }, + { url = "https://files.pythonhosted.org/packages/bb/24/d410c732326738d4f392689621ff14e10d3717efe7de9ecb97c44d8765a3/jiter-0.7.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8203519907a1d81d6cb00902c98e27c2d0bf25ce0323c50ca594d30f5f1fbcf", size = 366857 }, + { url = "https://files.pythonhosted.org/packages/14/a1/53df95b8248968936e7ba9eb5839918e3cfd183e56356d2961b9b29a49fc/jiter-0.7.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41d15ccc53931c822dd7f1aebf09faa3cda2d7b48a76ef304c7dbc19d1302e51", size = 514972 }, + { url = "https://files.pythonhosted.org/packages/97/c8/1876add533606ff1204450dd2564638cac7f164ff90844cb921cdf25cf68/jiter-0.7.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:febf3179b2fabf71fbd2fd52acb8594163bb173348b388649567a548f356dbf6", size = 497728 }, + { url = "https://files.pythonhosted.org/packages/94/31/1e59f246e264414b004864b63783e54aa3397be88f53dda3b01db3ae4251/jiter-0.7.0-cp313-none-win32.whl", hash = "sha256:4a8e2d866e7eda19f012444e01b55079d8e1c4c30346aaac4b97e80c54e2d6d3", size = 198660 }, + { url = "https://files.pythonhosted.org/packages/ca/96/58b3d260e212add0087563672931b1176e70bef1225839a4470ec66157a5/jiter-0.7.0-cp313-none-win_amd64.whl", hash = "sha256:7417c2b928062c496f381fb0cb50412eee5ad1d8b53dbc0e011ce45bb2de522c", size = 199305 }, ] [[package]] @@ -1955,6 +2064,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, ] [[package]] @@ -2091,6 +2220,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9b/fd/eb1a3573cda74d4c2381d10ded62c128e869954ced1881c15e2bcd97a48f/mmh3-5.0.1-cp312-cp312-win32.whl", hash = "sha256:842516acf04da546f94fad52db125ee619ccbdcada179da51c326a22c4578cb9", size = 39206 }, { url = "https://files.pythonhosted.org/packages/66/e8/542ed252924002b84c43a68a080cfd4facbea0d5df361e4f59637638d3c7/mmh3-5.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:d963be0dbfd9fca209c17172f6110787ebf78934af25e3694fe2ba40e55c1e2b", size = 39799 }, { url = "https://files.pythonhosted.org/packages/bd/25/ff2cd36c82a23afa57a05cdb52ab467a911fb12c055c8a8238c0d426cbf0/mmh3-5.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:a5da292ceeed8ce8e32b68847261a462d30fd7b478c3f55daae841404f433c15", size = 36537 }, + { url = "https://files.pythonhosted.org/packages/09/e0/fb19c46265c18311b422ba5ce3e18046ad45c48cfb213fd6dbec23ae6b51/mmh3-5.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:673e3f1c8d4231d6fb0271484ee34cb7146a6499fc0df80788adb56fd76842da", size = 52909 }, + { url = "https://files.pythonhosted.org/packages/c3/94/54fc591e7a24c7ce2c531ecfc5715cff932f9d320c2936550cc33d67304d/mmh3-5.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f795a306bd16a52ad578b663462cc8e95500b3925d64118ae63453485d67282b", size = 38396 }, + { url = "https://files.pythonhosted.org/packages/1f/9a/142bcc9d0d28fc8ae45bbfb83926adc069f984cdf3495a71534cc22b8e27/mmh3-5.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5ed57a5e28e502a1d60436cc25c76c3a5ba57545f250f2969af231dc1221e0a5", size = 38207 }, + { url = "https://files.pythonhosted.org/packages/f8/5b/f1c9110aa70321bb1ee713f17851b9534586c63bc25e0110e4fc03ae2450/mmh3-5.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:632c28e7612e909dbb6cbe2fe496201ada4695b7715584005689c5dc038e59ad", size = 94988 }, + { url = "https://files.pythonhosted.org/packages/87/e5/4dc67e7e0e716c641ab0a5875a659e37258417439590feff5c3bd3ff4538/mmh3-5.0.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53fd6bd525a5985e391c43384672d9d6b317fcb36726447347c7fc75bfed34ec", size = 99969 }, + { url = "https://files.pythonhosted.org/packages/ac/68/d148327337687c53f04ad9ceaedfa9ad155ee0111d0cb06220f044d66720/mmh3-5.0.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dceacf6b0b961a0e499836af3aa62d60633265607aef551b2a3e3c48cdaa5edd", size = 99662 }, + { url = "https://files.pythonhosted.org/packages/13/79/782adb6df6397947c1097b1e94b7f8d95629a4a73df05cf7207bd5148c1f/mmh3-5.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8f0738d478fdfb5d920f6aff5452c78f2c35b0eff72caa2a97dfe38e82f93da2", size = 87606 }, + { url = "https://files.pythonhosted.org/packages/f2/c2/0404383281df049d0e4ccf07fabd659fc1f3da834df6708d934116cbf45d/mmh3-5.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e70285e7391ab88b872e5bef632bad16b9d99a6d3ca0590656a4753d55988af", size = 94836 }, + { url = "https://files.pythonhosted.org/packages/c8/33/fda67c5f28e4c2131891cf8cbc3513cfc55881e3cfe26e49328e38ffacb3/mmh3-5.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:27e5fc6360aa6b828546a4318da1a7da6bf6e5474ccb053c3a6aa8ef19ff97bd", size = 90492 }, + { url = "https://files.pythonhosted.org/packages/64/2f/0ed38aefe2a87f30bb1b12e5b75dc69fcffdc16def40d1752d6fc7cbbf96/mmh3-5.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7989530c3c1e2c17bf5a0ec2bba09fd19819078ba90beedabb1c3885f5040b0d", size = 89594 }, + { url = "https://files.pythonhosted.org/packages/95/ab/6e7a5e765fc78e3dbd0a04a04cfdf72e91eb8e31976228e69d82c741a5b4/mmh3-5.0.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:cdad7bee649950da7ecd3cbbbd12fb81f1161072ecbdb5acfa0018338c5cb9cf", size = 94929 }, + { url = "https://files.pythonhosted.org/packages/74/51/f748f00c072006f4a093d9b08853a0e2e3cd5aeaa91343d4e2d942851978/mmh3-5.0.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e143b8f184c1bb58cecd85ab4a4fd6dc65a2d71aee74157392c3fddac2a4a331", size = 91317 }, + { url = "https://files.pythonhosted.org/packages/df/a1/21ee8017a7feb0270c49f756ff56da9f99bd150dcfe3b3f6f0d4b243423d/mmh3-5.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5eb12e886f3646dd636f16b76eb23fc0c27e8ff3c1ae73d4391e50ef60b40f6", size = 89861 }, + { url = "https://files.pythonhosted.org/packages/c2/d2/46a6d070de4659bdf91cd6a62d659f8cc547dadee52b6d02bcbacb3262ed/mmh3-5.0.1-cp313-cp313-win32.whl", hash = "sha256:16e6dddfa98e1c2d021268e72c78951234186deb4df6630e984ac82df63d0a5d", size = 39201 }, + { url = "https://files.pythonhosted.org/packages/ed/07/316c062f09019b99b248a4183c5333f8eeebe638345484774908a8f2c9c0/mmh3-5.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:d3ffb792d70b8c4a2382af3598dad6ae0c5bd9cee5b7ffcc99aa2f5fd2c1bf70", size = 39807 }, + { url = "https://files.pythonhosted.org/packages/9d/d3/f7e6d7d062b8d7072c3989a528d9d47486ee5d5ae75250f6e26b4976d098/mmh3-5.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:122fa9ec148383f9124292962bda745f192b47bfd470b2af5fe7bb3982b17896", size = 36539 }, ] [[package]] @@ -2257,6 +2402,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ba/07/37d67048786ae84e6612575e173d713c9a05d0ae495dde1e68d972207d98/mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2", size = 12589275 }, { url = "https://files.pythonhosted.org/packages/1f/17/b1018c6bb3e9f1ce3956722b3bf91bff86c1cefccca71cec05eae49d6d41/mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0", size = 13037783 }, { url = "https://files.pythonhosted.org/packages/cb/32/cd540755579e54a88099aee0287086d996f5a24281a673f78a0e14dba150/mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2", size = 9726197 }, + { url = "https://files.pythonhosted.org/packages/11/bb/ab4cfdc562cad80418f077d8be9b4491ee4fb257440da951b85cbb0a639e/mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7", size = 11069721 }, + { url = "https://files.pythonhosted.org/packages/59/3b/a393b1607cb749ea2c621def5ba8c58308ff05e30d9dbdc7c15028bca111/mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62", size = 10063996 }, + { url = "https://files.pythonhosted.org/packages/d1/1f/6b76be289a5a521bb1caedc1f08e76ff17ab59061007f201a8a18cc514d1/mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8", size = 12584043 }, + { url = "https://files.pythonhosted.org/packages/a6/83/5a85c9a5976c6f96e3a5a7591aa28b4a6ca3a07e9e5ba0cec090c8b596d6/mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7", size = 13036996 }, + { url = "https://files.pythonhosted.org/packages/b4/59/c39a6f752f1f893fccbcf1bdd2aca67c79c842402b5283563d006a67cf76/mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc", size = 9737709 }, { url = "https://files.pythonhosted.org/packages/3b/86/72ce7f57431d87a7ff17d442f521146a6585019eb8f4f31b7c02801f78ad/mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a", size = 2647043 }, ] @@ -2388,6 +2538,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bf/72/66af7916d9c3c6dbfbc8acdd4930c65461e1953374a2bc43d00f948f004a/numpy-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e585c8ae871fd38ac50598f4763d73ec5497b0de9a0ab4ef5b69f01c6a046142", size = 14081134 }, { url = "https://files.pythonhosted.org/packages/dc/5a/59a67d84f33fe00ae74f0b5b69dd4f93a586a4aba7f7e19b54b2133db038/numpy-2.1.2-cp312-cp312-win32.whl", hash = "sha256:9c6c754df29ce6a89ed23afb25550d1c2d5fdb9901d9c67a16e0b16eaf7e2550", size = 6237784 }, { url = "https://files.pythonhosted.org/packages/4c/79/73735a6a5dad6059c085f240a4e74c9270feccd2bc66e4d31b5ca01d329c/numpy-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:456e3b11cb79ac9946c822a56346ec80275eaf2950314b249b512896c0d2505e", size = 12568254 }, + { url = "https://files.pythonhosted.org/packages/16/72/716fa1dbe92395a9a623d5049203ff8ddb0cfce65b9df9117c3696ccc011/numpy-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a84498e0d0a1174f2b3ed769b67b656aa5460c92c9554039e11f20a05650f00d", size = 20834690 }, + { url = "https://files.pythonhosted.org/packages/1e/fb/3e85a39511586053b5c6a59a643879e376fae22230ebfef9cfabb0e032e2/numpy-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4d6ec0d4222e8ffdab1744da2560f07856421b367928026fb540e1945f2eeeaf", size = 13507474 }, + { url = "https://files.pythonhosted.org/packages/35/eb/5677556d9ba13436dab51e129f98d4829d95cd1b6bd0e199c14485a4bdb9/numpy-2.1.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:259ec80d54999cc34cd1eb8ded513cb053c3bf4829152a2e00de2371bd406f5e", size = 5074742 }, + { url = "https://files.pythonhosted.org/packages/3e/c5/6c5ef5ba41b65a7e51bed50dbf3e1483eb578055633dd013e811a28e96a1/numpy-2.1.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:675c741d4739af2dc20cd6c6a5c4b7355c728167845e3c6b0e824e4e5d36a6c3", size = 6606787 }, + { url = "https://files.pythonhosted.org/packages/08/ac/f2f29dd4fd325b379c7dc932a0ebab22f0e031dbe80b2f6019b291a3a544/numpy-2.1.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05b2d4e667895cc55e3ff2b56077e4c8a5604361fc21a042845ea3ad67465aa8", size = 13601333 }, + { url = "https://files.pythonhosted.org/packages/44/26/63f5f4e5089654dfb858f4892215ed968cd1a68e6f4a83f9961f84f855cb/numpy-2.1.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43cca367bf94a14aca50b89e9bc2061683116cfe864e56740e083392f533ce7a", size = 16038090 }, + { url = "https://files.pythonhosted.org/packages/1d/21/015e0594de9c3a8d5edd24943d2bd23f102ec71aec026083f822f86497e2/numpy-2.1.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:76322dcdb16fccf2ac56f99048af32259dcc488d9b7e25b51e5eca5147a3fb98", size = 16410865 }, + { url = "https://files.pythonhosted.org/packages/df/01/c1bcf9e6025d79077fbf3f3ee503b50aa7bfabfcd8f4b54f5829f4c00f3f/numpy-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:32e16a03138cabe0cb28e1007ee82264296ac0983714094380b408097a418cfe", size = 14078077 }, + { url = "https://files.pythonhosted.org/packages/ba/06/db9d127d63bd11591770ba9f3d960f8041e0f895184b9351d4b1b5b56983/numpy-2.1.2-cp313-cp313-win32.whl", hash = "sha256:242b39d00e4944431a3cd2db2f5377e15b5785920421993770cddb89992c3f3a", size = 6234904 }, + { url = "https://files.pythonhosted.org/packages/a9/96/9f61f8f95b6e0ea0aa08633b704c75d1882bdcb331bdf8bfd63263b25b00/numpy-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:f2ded8d9b6f68cc26f8425eda5d3877b47343e68ca23d0d0846f4d312ecaa445", size = 12561910 }, + { url = "https://files.pythonhosted.org/packages/36/b8/033f627821784a48e8f75c218033471eebbaacdd933f8979c79637a1b44b/numpy-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ffef621c14ebb0188a8633348504a35c13680d6da93ab5cb86f4e54b7e922b5", size = 20857719 }, + { url = "https://files.pythonhosted.org/packages/96/46/af5726fde5b74ed83f2f17a73386d399319b7ed4d51279fb23b721d0816d/numpy-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ad369ed238b1959dfbade9018a740fb9392c5ac4f9b5173f420bd4f37ba1f7a0", size = 13518826 }, + { url = "https://files.pythonhosted.org/packages/db/6e/8ce677edf36da1c4dae80afe5529f47690697eb55b4864673af260ccea7b/numpy-2.1.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d82075752f40c0ddf57e6e02673a17f6cb0f8eb3f587f63ca1eaab5594da5b17", size = 5115036 }, + { url = "https://files.pythonhosted.org/packages/6a/ba/3cce44fb1b8438042c11847048812a776f75ee0e7070179c22e4cfbf420c/numpy-2.1.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:1600068c262af1ca9580a527d43dc9d959b0b1d8e56f8a05d830eea39b7c8af6", size = 6628641 }, + { url = "https://files.pythonhosted.org/packages/59/c8/e722998720ccbd35ffbcf1d1b8ed0aa2304af88d3f1c38e06ebf983599b3/numpy-2.1.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a26ae94658d3ba3781d5e103ac07a876b3e9b29db53f68ed7df432fd033358a8", size = 13574803 }, + { url = "https://files.pythonhosted.org/packages/7c/8e/fc1fdd83a55476765329ac2913321c4aed5b082a7915095628c4ca30ea72/numpy-2.1.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13311c2db4c5f7609b462bc0f43d3c465424d25c626d95040f073e30f7570e35", size = 16021174 }, + { url = "https://files.pythonhosted.org/packages/2a/b6/a790742aa88067adb4bd6c89a946778c1417d4deaeafce3ca928f26d4c52/numpy-2.1.2-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:2abbf905a0b568706391ec6fa15161fad0fb5d8b68d73c461b3c1bab6064dd62", size = 16400117 }, + { url = "https://files.pythonhosted.org/packages/48/6f/129e3c17e3befe7fefdeaa6890f4c4df3f3cf0831aa053802c3862da67aa/numpy-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ef444c57d664d35cac4e18c298c47d7b504c66b17c2ea91312e979fcfbdfb08a", size = 14066202 }, { url = "https://files.pythonhosted.org/packages/73/c9/3e1d6bbe6d3d2e2c5a9483b24b2f29a229b323f62054278a3bba7fee11e5/numpy-2.1.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:bdd407c40483463898b84490770199d5714dcc9dd9b792f6c6caccc523c00952", size = 20981945 }, { url = "https://files.pythonhosted.org/packages/6e/62/989c4988bde1a8e08117fccc3bab73d2886421fb98cde597168714f3c54e/numpy-2.1.2-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:da65fb46d4cbb75cb417cddf6ba5e7582eb7bb0b47db4b99c9fe5787ce5d91f5", size = 6750558 }, { url = "https://files.pythonhosted.org/packages/53/b1/00ef9f30975f1312a53257f68e57b4513d14d537e03d507e2773a684b1e8/numpy-2.1.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c193d0b0238638e6fc5f10f1b074a6993cb13b0b431f64079a509d63d3aa8b7", size = 16141552 }, @@ -2811,6 +2979,13 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ad/9b/be8b3d3aec42aa47f6058482ace0d2ca3023477a46643d766e96281d5d31/orjson-3.10.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:730ed5350147db7beb23ddaf072f490329e90a1d059711d364b49fe352ec987b", size = 170424 }, { url = "https://files.pythonhosted.org/packages/1b/15/a4cc61e23c39b9dec4620cb95817c83c84078be1771d602f6d03f0e5c696/orjson-3.10.10-cp312-none-win32.whl", hash = "sha256:a8f4bf5f1c85bea2170800020d53a8877812892697f9c2de73d576c9307a8a5f", size = 145132 }, { url = "https://files.pythonhosted.org/packages/9f/8a/ce7c28e4ea337f6d95261345d7c61322f8561c52f57b263a3ad7025984f4/orjson-3.10.10-cp312-none-win_amd64.whl", hash = "sha256:384cd13579a1b4cd689d218e329f459eb9ddc504fa48c5a83ef4889db7fd7a4f", size = 139389 }, + { url = "https://files.pythonhosted.org/packages/0c/69/f1c4382cd44bdaf10006c4e82cb85d2bcae735369f84031e203c4e5d87de/orjson-3.10.10-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:44bffae68c291f94ff5a9b4149fe9d1bdd4cd0ff0fb575bcea8351d48db629a1", size = 270695 }, + { url = "https://files.pythonhosted.org/packages/61/29/aeb5153271d4953872b06ed239eb54993a5f344353727c42d3aabb2046f6/orjson-3.10.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e27b4c6437315df3024f0835887127dac2a0a3ff643500ec27088d2588fa5ae1", size = 141632 }, + { url = "https://files.pythonhosted.org/packages/bc/a2/c8ac38d8fb461a9b717c766fbe1f7d3acf9bde2f12488eb13194960782e4/orjson-3.10.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bca84df16d6b49325a4084fd8b2fe2229cb415e15c46c529f868c3387bb1339d", size = 144854 }, + { url = "https://files.pythonhosted.org/packages/79/51/e7698fdb28bdec633888cc667edc29fd5376fce9ade0a5b3e22f5ebe0343/orjson-3.10.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c14ce70e8f39bd71f9f80423801b5d10bf93d1dceffdecd04df0f64d2c69bc01", size = 172023 }, + { url = "https://files.pythonhosted.org/packages/02/2d/0d99c20878658c7e33b90e6a4bb75cf2924d6ff29c2365262cff3c26589a/orjson-3.10.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:24ac62336da9bda1bd93c0491eff0613003b48d3cb5d01470842e7b52a40d5b4", size = 170429 }, + { url = "https://files.pythonhosted.org/packages/cd/45/6a4a446f4fb29bb4703c3537d5c6a2bf7fed768cb4d7b7dce9d71b72fc93/orjson-3.10.10-cp313-none-win32.whl", hash = "sha256:eb0a42831372ec2b05acc9ee45af77bcaccbd91257345f93780a8e654efc75db", size = 145099 }, + { url = "https://files.pythonhosted.org/packages/72/6e/4631fe219a4203aa111e9bb763ad2e2e0cdd1a03805029e4da124d96863f/orjson-3.10.10-cp313-none-win_amd64.whl", hash = "sha256:f0c4f37f8bf3f1075c6cc8dd8a9f843689a4b618628f8812d0a71e6968b95ffd", size = 139176 }, ] [[package]] @@ -2966,6 +3141,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/af/3a/da80224a6eb15bba7a0dcb2346e2b686bb9bf98378c0b4353cd88e62b171/pillow-11.0.0-cp312-cp312-win32.whl", hash = "sha256:86510e3f5eca0ab87429dd77fafc04693195eec7fd6a137c389c3eeb4cfb77c6", size = 2249631 }, { url = "https://files.pythonhosted.org/packages/57/97/73f756c338c1d86bb802ee88c3cab015ad7ce4b838f8a24f16b676b1ac7c/pillow-11.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:8ec4a89295cd6cd4d1058a5e6aec6bf51e0eaaf9714774e1bfac7cfc9051db47", size = 2567533 }, { url = "https://files.pythonhosted.org/packages/0b/30/2b61876e2722374558b871dfbfcbe4e406626d63f4f6ed92e9c8e24cac37/pillow-11.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:27a7860107500d813fcd203b4ea19b04babe79448268403172782754870dac25", size = 2254890 }, + { url = "https://files.pythonhosted.org/packages/63/24/e2e15e392d00fcf4215907465d8ec2a2f23bcec1481a8ebe4ae760459995/pillow-11.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bcd1fb5bb7b07f64c15618c89efcc2cfa3e95f0e3bcdbaf4642509de1942a699", size = 3147300 }, + { url = "https://files.pythonhosted.org/packages/43/72/92ad4afaa2afc233dc44184adff289c2e77e8cd916b3ddb72ac69495bda3/pillow-11.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0e038b0745997c7dcaae350d35859c9715c71e92ffb7e0f4a8e8a16732150f38", size = 2978742 }, + { url = "https://files.pythonhosted.org/packages/9e/da/c8d69c5bc85d72a8523fe862f05ababdc52c0a755cfe3d362656bb86552b/pillow-11.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ae08bd8ffc41aebf578c2af2f9d8749d91f448b3bfd41d7d9ff573d74f2a6b2", size = 4194349 }, + { url = "https://files.pythonhosted.org/packages/cd/e8/686d0caeed6b998351d57796496a70185376ed9c8ec7d99e1d19ad591fc6/pillow-11.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d69bfd8ec3219ae71bcde1f942b728903cad25fafe3100ba2258b973bd2bc1b2", size = 4298714 }, + { url = "https://files.pythonhosted.org/packages/ec/da/430015cec620d622f06854be67fd2f6721f52fc17fca8ac34b32e2d60739/pillow-11.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:61b887f9ddba63ddf62fd02a3ba7add935d053b6dd7d58998c630e6dbade8527", size = 4208514 }, + { url = "https://files.pythonhosted.org/packages/44/ae/7e4f6662a9b1cb5f92b9cc9cab8321c381ffbee309210940e57432a4063a/pillow-11.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:c6a660307ca9d4867caa8d9ca2c2658ab685de83792d1876274991adec7b93fa", size = 4380055 }, + { url = "https://files.pythonhosted.org/packages/74/d5/1a807779ac8a0eeed57f2b92a3c32ea1b696e6140c15bd42eaf908a261cd/pillow-11.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:73e3a0200cdda995c7e43dd47436c1548f87a30bb27fb871f352a22ab8dcf45f", size = 4296751 }, + { url = "https://files.pythonhosted.org/packages/38/8c/5fa3385163ee7080bc13026d59656267daaaaf3c728c233d530e2c2757c8/pillow-11.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fba162b8872d30fea8c52b258a542c5dfd7b235fb5cb352240c8d63b414013eb", size = 4430378 }, + { url = "https://files.pythonhosted.org/packages/ca/1d/ad9c14811133977ff87035bf426875b93097fb50af747793f013979facdb/pillow-11.0.0-cp313-cp313-win32.whl", hash = "sha256:f1b82c27e89fffc6da125d5eb0ca6e68017faf5efc078128cfaa42cf5cb38798", size = 2249588 }, + { url = "https://files.pythonhosted.org/packages/fb/01/3755ba287dac715e6afdb333cb1f6d69740a7475220b4637b5ce3d78cec2/pillow-11.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ba470552b48e5835f1d23ecb936bb7f71d206f9dfeee64245f30c3270b994de", size = 2567509 }, + { url = "https://files.pythonhosted.org/packages/c0/98/2c7d727079b6be1aba82d195767d35fcc2d32204c7a5820f822df5330152/pillow-11.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:846e193e103b41e984ac921b335df59195356ce3f71dcfd155aa79c603873b84", size = 2254791 }, + { url = "https://files.pythonhosted.org/packages/eb/38/998b04cc6f474e78b563716b20eecf42a2fa16a84589d23c8898e64b0ffd/pillow-11.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4ad70c4214f67d7466bea6a08061eba35c01b1b89eaa098040a35272a8efb22b", size = 3150854 }, + { url = "https://files.pythonhosted.org/packages/13/8e/be23a96292113c6cb26b2aa3c8b3681ec62b44ed5c2bd0b258bd59503d3c/pillow-11.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6ec0d5af64f2e3d64a165f490d96368bb5dea8b8f9ad04487f9ab60dc4bb6003", size = 2982369 }, + { url = "https://files.pythonhosted.org/packages/97/8a/3db4eaabb7a2ae8203cd3a332a005e4aba00067fc514aaaf3e9721be31f1/pillow-11.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c809a70e43c7977c4a42aefd62f0131823ebf7dd73556fa5d5950f5b354087e2", size = 4333703 }, + { url = "https://files.pythonhosted.org/packages/28/ac/629ffc84ff67b9228fe87a97272ab125bbd4dc462745f35f192d37b822f1/pillow-11.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:4b60c9520f7207aaf2e1d94de026682fc227806c6e1f55bba7606d1c94dd623a", size = 4412550 }, + { url = "https://files.pythonhosted.org/packages/d6/07/a505921d36bb2df6868806eaf56ef58699c16c388e378b0dcdb6e5b2fb36/pillow-11.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1e2688958a840c822279fda0086fec1fdab2f95bf2b717b66871c4ad9859d7e8", size = 4461038 }, + { url = "https://files.pythonhosted.org/packages/d6/b9/fb620dd47fc7cc9678af8f8bd8c772034ca4977237049287e99dda360b66/pillow-11.0.0-cp313-cp313t-win32.whl", hash = "sha256:607bbe123c74e272e381a8d1957083a9463401f7bd01287f50521ecb05a313f8", size = 2253197 }, + { url = "https://files.pythonhosted.org/packages/df/86/25dde85c06c89d7fc5db17940f07aae0a56ac69aa9ccb5eb0f09798862a8/pillow-11.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c39ed17edea3bc69c743a8dd3e9853b7509625c2462532e62baa0732163a904", size = 2572169 }, + { url = "https://files.pythonhosted.org/packages/51/85/9c33f2517add612e17f3381aee7c4072779130c634921a756c97bc29fb49/pillow-11.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:75acbbeb05b86bc53cbe7b7e6fe00fbcf82ad7c684b3ad82e3d711da9ba287d3", size = 2256828 }, { url = "https://files.pythonhosted.org/packages/36/57/42a4dd825eab762ba9e690d696d894ba366e06791936056e26e099398cda/pillow-11.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1187739620f2b365de756ce086fdb3604573337cc28a0d3ac4a01ab6b2d2a6d2", size = 3119239 }, { url = "https://files.pythonhosted.org/packages/98/f7/25f9f9e368226a1d6cf3507081a1a7944eddd3ca7821023377043f5a83c8/pillow-11.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fbbcb7b57dc9c794843e3d1258c0fbf0f48656d46ffe9e09b63bbd6e8cd5d0a2", size = 2950803 }, { url = "https://files.pythonhosted.org/packages/59/01/98ead48a6c2e31e6185d4c16c978a67fe3ccb5da5c2ff2ba8475379bb693/pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d203af30149ae339ad1b4f710d9844ed8796e97fda23ffbc4cc472968a47d0b", size = 3281098 }, @@ -3157,6 +3351,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fd/bd/8657918a35d50b18a9e4d78a5df7b6c82a637a311ab20851eef4326305c1/propcache-0.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4a9d9b4d0a9b38d1c391bb4ad24aa65f306c6f01b512e10a8a34a2dc5675d348", size = 235922 }, { url = "https://files.pythonhosted.org/packages/a8/6f/ec0095e1647b4727db945213a9f395b1103c442ef65e54c62e92a72a3f75/propcache-0.2.0-cp312-cp312-win32.whl", hash = "sha256:69d3a98eebae99a420d4b28756c8ce6ea5a29291baf2dc9ff9414b42676f61d5", size = 40177 }, { url = "https://files.pythonhosted.org/packages/20/a2/bd0896fdc4f4c1db46d9bc361c8c79a9bf08ccc08ba054a98e38e7ba1557/propcache-0.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ad9c9b99b05f163109466638bd30ada1722abb01bbb85c739c50b6dc11f92dc3", size = 44446 }, + { url = "https://files.pythonhosted.org/packages/a8/a7/5f37b69197d4f558bfef5b4bceaff7c43cc9b51adf5bd75e9081d7ea80e4/propcache-0.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ecddc221a077a8132cf7c747d5352a15ed763b674c0448d811f408bf803d9ad7", size = 78120 }, + { url = "https://files.pythonhosted.org/packages/c8/cd/48ab2b30a6b353ecb95a244915f85756d74f815862eb2ecc7a518d565b48/propcache-0.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0e53cb83fdd61cbd67202735e6a6687a7b491c8742dfc39c9e01e80354956763", size = 45127 }, + { url = "https://files.pythonhosted.org/packages/a5/ba/0a1ef94a3412aab057bd996ed5f0ac7458be5bf469e85c70fa9ceb43290b/propcache-0.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92fe151145a990c22cbccf9ae15cae8ae9eddabfc949a219c9f667877e40853d", size = 44419 }, + { url = "https://files.pythonhosted.org/packages/b4/6c/ca70bee4f22fa99eacd04f4d2f1699be9d13538ccf22b3169a61c60a27fa/propcache-0.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6a21ef516d36909931a2967621eecb256018aeb11fc48656e3257e73e2e247a", size = 229611 }, + { url = "https://files.pythonhosted.org/packages/19/70/47b872a263e8511ca33718d96a10c17d3c853aefadeb86dc26e8421184b9/propcache-0.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f88a4095e913f98988f5b338c1d4d5d07dbb0b6bad19892fd447484e483ba6b", size = 234005 }, + { url = "https://files.pythonhosted.org/packages/4f/be/3b0ab8c84a22e4a3224719099c1229ddfdd8a6a1558cf75cb55ee1e35c25/propcache-0.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a5b3bb545ead161be780ee85a2b54fdf7092815995661947812dde94a40f6fb", size = 237270 }, + { url = "https://files.pythonhosted.org/packages/04/d8/f071bb000d4b8f851d312c3c75701e586b3f643fe14a2e3409b1b9ab3936/propcache-0.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67aeb72e0f482709991aa91345a831d0b707d16b0257e8ef88a2ad246a7280bf", size = 231877 }, + { url = "https://files.pythonhosted.org/packages/93/e7/57a035a1359e542bbb0a7df95aad6b9871ebee6dce2840cb157a415bd1f3/propcache-0.2.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c997f8c44ec9b9b0bcbf2d422cc00a1d9b9c681f56efa6ca149a941e5560da2", size = 217848 }, + { url = "https://files.pythonhosted.org/packages/f0/93/d1dea40f112ec183398fb6c42fde340edd7bab202411c4aa1a8289f461b6/propcache-0.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2a66df3d4992bc1d725b9aa803e8c5a66c010c65c741ad901e260ece77f58d2f", size = 216987 }, + { url = "https://files.pythonhosted.org/packages/62/4c/877340871251145d3522c2b5d25c16a1690ad655fbab7bb9ece6b117e39f/propcache-0.2.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3ebbcf2a07621f29638799828b8d8668c421bfb94c6cb04269130d8de4fb7136", size = 212451 }, + { url = "https://files.pythonhosted.org/packages/7c/bb/a91b72efeeb42906ef58ccf0cdb87947b54d7475fee3c93425d732f16a61/propcache-0.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1235c01ddaa80da8235741e80815ce381c5267f96cc49b1477fdcf8c047ef325", size = 212879 }, + { url = "https://files.pythonhosted.org/packages/9b/7f/ee7fea8faac57b3ec5d91ff47470c6c5d40d7f15d0b1fccac806348fa59e/propcache-0.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3947483a381259c06921612550867b37d22e1df6d6d7e8361264b6d037595f44", size = 222288 }, + { url = "https://files.pythonhosted.org/packages/ff/d7/acd67901c43d2e6b20a7a973d9d5fd543c6e277af29b1eb0e1f7bd7ca7d2/propcache-0.2.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d5bed7f9805cc29c780f3aee05de3262ee7ce1f47083cfe9f77471e9d6777e83", size = 228257 }, + { url = "https://files.pythonhosted.org/packages/8d/6f/6272ecc7a8daad1d0754cfc6c8846076a8cb13f810005c79b15ce0ef0cf2/propcache-0.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4a91d44379f45f5e540971d41e4626dacd7f01004826a18cb048e7da7e96544", size = 221075 }, + { url = "https://files.pythonhosted.org/packages/7c/bd/c7a6a719a6b3dd8b3aeadb3675b5783983529e4a3185946aa444d3e078f6/propcache-0.2.0-cp313-cp313-win32.whl", hash = "sha256:f902804113e032e2cdf8c71015651c97af6418363bea8d78dc0911d56c335032", size = 39654 }, + { url = "https://files.pythonhosted.org/packages/88/e7/0eef39eff84fa3e001b44de0bd41c7c0e3432e7648ffd3d64955910f002d/propcache-0.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:8f188cfcc64fb1266f4684206c9de0e80f54622c3f22a910cbd200478aeae61e", size = 43705 }, { url = "https://files.pythonhosted.org/packages/3d/b6/e6d98278f2d49b22b4d033c9f792eda783b9ab2094b041f013fc69bcde87/propcache-0.2.0-py3-none-any.whl", hash = "sha256:2ccc28197af5313706511fab3a8b66dcd6da067a1331372c82ea1cb74285e036", size = 11603 }, ] @@ -3260,6 +3470,17 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/11/68/eaf85b3421b3f01b638dd6b16f4e9bc8de42eb1d000da62964fb29f8c823/psycopg_binary-3.2.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:709447bd7203b0b2debab1acec23123eb80b386f6c29e7604a5d4326a11e5bd6", size = 3189977 }, { url = "https://files.pythonhosted.org/packages/83/5a/cf94c3ba87ea6c8331aa0aba36a18a837a3231764457780661968804673e/psycopg_binary-3.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5e37d5027e297a627da3551a1e962316d0f88ee4ada74c768f6c9234e26346d9", size = 3232263 }, { url = "https://files.pythonhosted.org/packages/0e/3a/9d912b16059e87b04e3eb4fca457f079d78d6468f627d5622fbda80e9378/psycopg_binary-3.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:261f0031ee6074765096a19b27ed0f75498a8338c3dcd7f4f0d831e38adf12d1", size = 2912530 }, + { url = "https://files.pythonhosted.org/packages/c6/bf/717c5e51c68e2498b60a6e9f1476cc47953013275a54bf8e23fd5082a72d/psycopg_binary-3.2.3-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:41fdec0182efac66b27478ac15ef54c9ebcecf0e26ed467eb7d6f262a913318b", size = 3360874 }, + { url = "https://files.pythonhosted.org/packages/31/d5/6f9ad6fe5ef80ca9172bc3d028ebae8e9a1ee8aebd917c95c747a5efd85f/psycopg_binary-3.2.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:07d019a786eb020c0f984691aa1b994cb79430061065a694cf6f94056c603d26", size = 3502320 }, + { url = "https://files.pythonhosted.org/packages/fb/7b/c58dd26c27fe7a491141ca765c103e702872ff1c174ebd669d73d7fb0b5d/psycopg_binary-3.2.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c57615791a337378fe5381143259a6c432cdcbb1d3e6428bfb7ce59fff3fb5c", size = 4446950 }, + { url = "https://files.pythonhosted.org/packages/ed/75/acf6a81c788007b7bc0a43b02c22eff7cb19a6ace9e84c32838e86083a3f/psycopg_binary-3.2.3-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8eb9a4e394926b93ad919cad1b0a918e9b4c846609e8c1cfb6b743683f64da0", size = 4252409 }, + { url = "https://files.pythonhosted.org/packages/83/a5/8a01b923fe42acd185d53f24fb98ead717725ede76a4cd183ff293daf1f1/psycopg_binary-3.2.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5905729668ef1418bd36fbe876322dcb0f90b46811bba96d505af89e6fbdce2f", size = 4488121 }, + { url = "https://files.pythonhosted.org/packages/14/8f/b00e65e204340ab1259ecc8d4cc4c1f72c386be5ca7bfb90ae898a058d68/psycopg_binary-3.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd65774ed7d65101b314808b6893e1a75b7664f680c3ef18d2e5c84d570fa393", size = 4190653 }, + { url = "https://files.pythonhosted.org/packages/ce/fc/ba830fc6c9b02b66d1e2fb420736df4d78369760144169a9046f04d72ac6/psycopg_binary-3.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:700679c02f9348a0d0a2adcd33a0275717cd0d0aee9d4482b47d935023629505", size = 3118074 }, + { url = "https://files.pythonhosted.org/packages/b8/75/b62d06930a615435e909e05de126aa3d49f6ec2993d1aa6a99e7faab5570/psycopg_binary-3.2.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:96334bb64d054e36fed346c50c4190bad9d7c586376204f50bede21a913bf942", size = 3100457 }, + { url = "https://files.pythonhosted.org/packages/57/e5/32dc7518325d0010813853a87b19c784d8b11fdb17f5c0e0c148c5ac77af/psycopg_binary-3.2.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9099e443d4cc24ac6872e6a05f93205ba1a231b1a8917317b07c9ef2b955f1f4", size = 3192788 }, + { url = "https://files.pythonhosted.org/packages/23/a3/d1aa04329253c024a2323051774446770d47b43073874a3de8cca797ed8e/psycopg_binary-3.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1985ab05e9abebfbdf3163a16ebb37fbc5d49aff2bf5b3d7375ff0920bbb54cd", size = 3234247 }, + { url = "https://files.pythonhosted.org/packages/03/20/b675af723b9a61d48abd6a3d64cbb9797697d330255d1f8105713d54ed8e/psycopg_binary-3.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:e90352d7b610b4693fad0feea48549d4315d10f1eba5605421c92bb834e90170", size = 2913413 }, ] [[package]] @@ -3679,6 +3900,9 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/00/7c/d00d6bdd96de4344e06c4afbf218bc86b54436a94c01c71a8701f613aa56/pywin32-308-cp312-cp312-win32.whl", hash = "sha256:587f3e19696f4bf96fde9d8a57cec74a57021ad5f204c9e627e15c33ff568897", size = 5939729 }, { url = "https://files.pythonhosted.org/packages/21/27/0c8811fbc3ca188f93b5354e7c286eb91f80a53afa4e11007ef661afa746/pywin32-308-cp312-cp312-win_amd64.whl", hash = "sha256:00b3e11ef09ede56c6a43c71f2d31857cf7c54b0ab6e78ac659497abd2834f47", size = 6543015 }, { url = "https://files.pythonhosted.org/packages/9d/0f/d40f8373608caed2255781a3ad9a51d03a594a1248cd632d6a298daca693/pywin32-308-cp312-cp312-win_arm64.whl", hash = "sha256:9b4de86c8d909aed15b7011182c8cab38c8850de36e6afb1f0db22b8959e3091", size = 7976033 }, + { url = "https://files.pythonhosted.org/packages/a9/a4/aa562d8935e3df5e49c161b427a3a2efad2ed4e9cf81c3de636f1fdddfd0/pywin32-308-cp313-cp313-win32.whl", hash = "sha256:1c44539a37a5b7b21d02ab34e6a4d314e0788f1690d65b48e9b0b89f31abbbed", size = 5938579 }, + { url = "https://files.pythonhosted.org/packages/c7/50/b0efb8bb66210da67a53ab95fd7a98826a97ee21f1d22949863e6d588b22/pywin32-308-cp313-cp313-win_amd64.whl", hash = "sha256:fd380990e792eaf6827fcb7e187b2b4b1cede0585e3d0c9e84201ec27b9905e4", size = 6542056 }, + { url = "https://files.pythonhosted.org/packages/26/df/2b63e3e4f2df0224f8aaf6d131f54fe4e8c96400eb9df563e2aae2e1a1f9/pywin32-308-cp313-cp313-win_arm64.whl", hash = "sha256:ef313c46d4c18dfb82a2431e3051ac8f112ccee1a34f29c263c583c568db63cd", size = 7974986 }, ] [[package]] @@ -4014,6 +4238,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5e/0e/b1bdc7ea0db0946d640ab8965146099093391bb5d265832994c47461e3c5/rpds_py-0.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3c6afcf2338e7f374e8edc765c79fbcb4061d02b15dd5f8f314a4af2bdc7feb5", size = 530940 }, { url = "https://files.pythonhosted.org/packages/ae/d3/ffe907084299484fab60a7955f7c0e8a295c04249090218c59437010f9f4/rpds_py-0.20.1-cp312-none-win32.whl", hash = "sha256:dc73505153798c6f74854aba69cc75953888cf9866465196889c7cdd351e720c", size = 203164 }, { url = "https://files.pythonhosted.org/packages/1f/ba/9cbb57423c4bfbd81c473913bebaed151ad4158ee2590a4e4b3e70238b48/rpds_py-0.20.1-cp312-none-win_amd64.whl", hash = "sha256:8bbe951244a838a51289ee53a6bae3a07f26d4e179b96fc7ddd3301caf0518eb", size = 220750 }, + { url = "https://files.pythonhosted.org/packages/b5/01/fee2e1d1274c92fff04aa47d805a28d62c2aa971d1f49f5baea1c6e670d9/rpds_py-0.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6ca91093a4a8da4afae7fe6a222c3b53ee4eef433ebfee4d54978a103435159e", size = 329359 }, + { url = "https://files.pythonhosted.org/packages/b0/cf/4aeffb02b7090029d7aeecbffb9a10e1c80f6f56d7e9a30e15481dc4099c/rpds_py-0.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b9c2fe36d1f758b28121bef29ed1dee9b7a2453e997528e7d1ac99b94892527c", size = 320543 }, + { url = "https://files.pythonhosted.org/packages/17/69/85cf3429e9ccda684ba63ff36b5866d5f9451e921cc99819341e19880334/rpds_py-0.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f009c69bc8c53db5dfab72ac760895dc1f2bc1b62ab7408b253c8d1ec52459fc", size = 363107 }, + { url = "https://files.pythonhosted.org/packages/ef/de/7df88dea9c3eeb832196d23b41f0f6fc5f9a2ee9b2080bbb1db8731ead9c/rpds_py-0.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6740a3e8d43a32629bb9b009017ea5b9e713b7210ba48ac8d4cb6d99d86c8ee8", size = 372027 }, + { url = "https://files.pythonhosted.org/packages/d1/b8/88675399d2038580743c570a809c43a900e7090edc6553f8ffb66b23c965/rpds_py-0.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:32b922e13d4c0080d03e7b62991ad7f5007d9cd74e239c4b16bc85ae8b70252d", size = 405031 }, + { url = "https://files.pythonhosted.org/packages/e1/aa/cca639f6d17caf00bab51bdc70fcc0bdda3063e5662665c4fdf60443c474/rpds_py-0.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe00a9057d100e69b4ae4a094203a708d65b0f345ed546fdef86498bf5390982", size = 422271 }, + { url = "https://files.pythonhosted.org/packages/c4/07/bf8a949d2ec4626c285579c9d6b356c692325f1a4126e947736b416e1fc4/rpds_py-0.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49fe9b04b6fa685bd39237d45fad89ba19e9163a1ccaa16611a812e682913496", size = 363625 }, + { url = "https://files.pythonhosted.org/packages/11/f0/06675c6a58d6ce34547879138810eb9aab0c10e5607ea6c2e4dc56b703c8/rpds_py-0.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aa7ac11e294304e615b43f8c441fee5d40094275ed7311f3420d805fde9b07b4", size = 385906 }, + { url = "https://files.pythonhosted.org/packages/bf/ac/2d1f50374eb8e41030fad4e87f81751e1c39e3b5d4bee8c5618830d8a6ac/rpds_py-0.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aa97af1558a9bef4025f8f5d8c60d712e0a3b13a2fe875511defc6ee77a1ab7", size = 549021 }, + { url = "https://files.pythonhosted.org/packages/f7/d4/a7d70a7cc71df772eeadf4bce05e32e780a9fe44a511a5b091c7a85cb767/rpds_py-0.20.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:483b29f6f7ffa6af845107d4efe2e3fa8fb2693de8657bc1849f674296ff6a5a", size = 553800 }, + { url = "https://files.pythonhosted.org/packages/87/81/dc30bc449ccba63ad23a0f6633486d4e0e6955f45f3715a130dacabd6ad0/rpds_py-0.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:37fe0f12aebb6a0e3e17bb4cd356b1286d2d18d2e93b2d39fe647138458b4bcb", size = 531076 }, + { url = "https://files.pythonhosted.org/packages/50/80/fb62ab48f3b5cfe704ead6ad372da1922ddaa76397055e02eb507054c979/rpds_py-0.20.1-cp313-none-win32.whl", hash = "sha256:a624cc00ef2158e04188df5e3016385b9353638139a06fb77057b3498f794782", size = 202804 }, + { url = "https://files.pythonhosted.org/packages/d9/30/a3391e76d0b3313f33bdedd394a519decae3a953d2943e3dabf80ae32447/rpds_py-0.20.1-cp313-none-win_amd64.whl", hash = "sha256:b71b8666eeea69d6363248822078c075bac6ed135faa9216aa85f295ff009b1e", size = 220502 }, { url = "https://files.pythonhosted.org/packages/b6/fa/7959429e69569d0f6e7d27f80451402da0409349dd2b07f6bcbdd5fad2d3/rpds_py-0.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7a07ced2b22f0cf0b55a6a510078174c31b6d8544f3bc00c2bcee52b3d613f74", size = 328209 }, { url = "https://files.pythonhosted.org/packages/25/97/5dfdb091c30267ff404d2fd9e70c7a6d6ffc65ca77fffe9456e13b719066/rpds_py-0.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:68cb0a499f2c4a088fd2f521453e22ed3527154136a855c62e148b7883b99f9a", size = 319499 }, { url = "https://files.pythonhosted.org/packages/7c/98/cf2608722400f5f9bb4c82aa5ac09026f3ac2ebea9d4059d3533589ed0b6/rpds_py-0.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa3060d885657abc549b2a0f8e1b79699290e5d83845141717c6c90c2df38311", size = 361795 }, @@ -4082,6 +4319,14 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/d5/a659ca6f503b9379b930f13bc6b130c9f176469b73b9834296822a83a132/ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680", size = 745831 }, { url = "https://files.pythonhosted.org/packages/b1/82/85cb92f15a4231c89b95dfe08b09eb6adca929ef7df7e17ab59902b6f589/ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl", hash = "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5", size = 98777 }, { url = "https://files.pythonhosted.org/packages/d7/8f/c3654f6f1ddb75daf3922c3d8fc6005b1ab56671ad56ffb874d908bfa668/ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4", size = 115523 }, + { url = "https://files.pythonhosted.org/packages/29/00/4864119668d71a5fa45678f380b5923ff410701565821925c69780356ffa/ruamel.yaml.clib-0.2.12-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a", size = 132011 }, + { url = "https://files.pythonhosted.org/packages/7f/5e/212f473a93ae78c669ffa0cb051e3fee1139cb2d385d2ae1653d64281507/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:e7e3736715fbf53e9be2a79eb4db68e4ed857017344d697e8b9749444ae57475", size = 642488 }, + { url = "https://files.pythonhosted.org/packages/1f/8f/ecfbe2123ade605c49ef769788f79c38ddb1c8fa81e01f4dbf5cf1a44b16/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b7e75b4965e1d4690e93021adfcecccbca7d61c7bddd8e22406ef2ff20d74ef", size = 745066 }, + { url = "https://files.pythonhosted.org/packages/e2/a9/28f60726d29dfc01b8decdb385de4ced2ced9faeb37a847bd5cf26836815/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6", size = 701785 }, + { url = "https://files.pythonhosted.org/packages/84/7e/8e7ec45920daa7f76046578e4f677a3215fe8f18ee30a9cb7627a19d9b4c/ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf", size = 693017 }, + { url = "https://files.pythonhosted.org/packages/c5/b3/d650eaade4ca225f02a648321e1ab835b9d361c60d51150bac49063b83fa/ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1", size = 741270 }, + { url = "https://files.pythonhosted.org/packages/30/8c/ed73f047a73638257aa9377ad356bea4d96125b305c34a28766f4445cc0f/ruamel.yaml.clib-0.2.12-cp313-cp313-win32.whl", hash = "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6", size = 98583 }, + { url = "https://files.pythonhosted.org/packages/b0/85/e8e751d8791564dd333d5d9a4eab0a7a115f7e349595417fd50ecae3395c/ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3", size = 115190 }, ] [[package]] @@ -4261,7 +4506,7 @@ wheels = [ [[package]] name = "semantic-kernel" -version = "1.13.0" +version = "1.14.0" source = { editable = "." } dependencies = [ { name = "aiohttp", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -4403,7 +4648,7 @@ requires-dist = [ { name = "pydantic", specifier = "~=2.0" }, { name = "pydantic-settings", specifier = "~=2.0" }, { name = "pymilvus", marker = "extra == 'milvus'", specifier = ">=2.3,<2.5" }, - { name = "pymongo", marker = "extra == 'mongo'", specifier = ">=4.8.0,<4.9" }, + { name = "pymongo", marker = "extra == 'mongo'", specifier = ">=4.8.0,<4.11" }, { name = "qdrant-client", marker = "extra == 'qdrant'", specifier = "~=1.9" }, { name = "redis", extras = ["hiredis"], marker = "extra == 'redis'", specifier = "~=5.0" }, { name = "sentence-transformers", marker = "extra == 'hugging-face'", specifier = ">=2.2,<4.0" }, @@ -4771,6 +5016,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6d/69/d8ada8b6e0a4257556d5b4ddeb4345ea8eeaaef3c98b60d1cca197c7ad8e/torch-2.5.1-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:3f4b7f10a247e0dcd7ea97dc2d3bfbfc90302ed36d7f3952b0008d0df264e697", size = 91811673 }, { url = "https://files.pythonhosted.org/packages/5f/ba/607d013b55b9fd805db2a5c2662ec7551f1910b4eef39653eeaba182c5b2/torch-2.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:73e58e78f7d220917c5dbfad1a40e09df9929d3b95d25e57d9f8558f84c9a11c", size = 203046841 }, { url = "https://files.pythonhosted.org/packages/57/6c/bf52ff061da33deb9f94f4121fde7ff3058812cb7d2036c97bc167793bd1/torch-2.5.1-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:8c712df61101964eb11910a846514011f0b6f5920c55dbf567bff8a34163d5b1", size = 63858109 }, + { url = "https://files.pythonhosted.org/packages/69/72/20cb30f3b39a9face296491a86adb6ff8f1a47a897e4d14667e6cf89d5c3/torch-2.5.1-cp313-cp313-manylinux1_x86_64.whl", hash = "sha256:9b61edf3b4f6e3b0e0adda8b3960266b9009d02b37555971f4d1c8f7a05afed7", size = 906393265 }, ] [[package]] @@ -5101,6 +5347,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/06/a7/b4e6a19925c900be9f98bec0a75e6e8f79bb53bdeb891916609ab3958967/uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc", size = 4693770 }, { url = "https://files.pythonhosted.org/packages/ce/0c/f07435a18a4b94ce6bd0677d8319cd3de61f3a9eeb1e5f8ab4e8b5edfcb3/uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb", size = 4451321 }, { url = "https://files.pythonhosted.org/packages/8f/eb/f7032be105877bcf924709c97b1bf3b90255b4ec251f9340cef912559f28/uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f", size = 4659022 }, + { url = "https://files.pythonhosted.org/packages/3f/8d/2cbef610ca21539f0f36e2b34da49302029e7c9f09acef0b1c3b5839412b/uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281", size = 1468123 }, + { url = "https://files.pythonhosted.org/packages/93/0d/b0038d5a469f94ed8f2b2fce2434a18396d8fbfb5da85a0a9781ebbdec14/uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af", size = 819325 }, + { url = "https://files.pythonhosted.org/packages/50/94/0a687f39e78c4c1e02e3272c6b2ccdb4e0085fda3b8352fecd0410ccf915/uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6", size = 4582806 }, + { url = "https://files.pythonhosted.org/packages/d2/19/f5b78616566ea68edd42aacaf645adbf71fbd83fc52281fba555dc27e3f1/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816", size = 4701068 }, + { url = "https://files.pythonhosted.org/packages/47/57/66f061ee118f413cd22a656de622925097170b9380b30091b78ea0c6ea75/uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc", size = 4454428 }, + { url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018 }, ] [[package]] @@ -5276,6 +5528,17 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5c/f1/a29dd6046d3a722d26f182b783a7997d25298873a14028c4760347974ea3/websockets-13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a9dcaf8b0cc72a392760bb8755922c03e17a5a54e08cca58e8b74f6902b433cf", size = 164686 }, { url = "https://files.pythonhosted.org/packages/0f/99/ab1cdb282f7e595391226f03f9b498f52109d25a2ba03832e21614967dfa/websockets-13.1-cp312-cp312-win32.whl", hash = "sha256:2f85cf4f2a1ba8f602298a853cec8526c2ca42a9a4b947ec236eaedb8f2dc80c", size = 158712 }, { url = "https://files.pythonhosted.org/packages/46/93/e19160db48b5581feac8468330aa11b7292880a94a37d7030478596cc14e/websockets-13.1-cp312-cp312-win_amd64.whl", hash = "sha256:38377f8b0cdeee97c552d20cf1865695fcd56aba155ad1b4ca8779a5b6ef4ac3", size = 159145 }, + { url = "https://files.pythonhosted.org/packages/51/20/2b99ca918e1cbd33c53db2cace5f0c0cd8296fc77558e1908799c712e1cd/websockets-13.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a9ab1e71d3d2e54a0aa646ab6d4eebfaa5f416fe78dfe4da2839525dc5d765c6", size = 157828 }, + { url = "https://files.pythonhosted.org/packages/b8/47/0932a71d3d9c0e9483174f60713c84cee58d62839a143f21a2bcdbd2d205/websockets-13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b9d7439d7fab4dce00570bb906875734df13d9faa4b48e261c440a5fec6d9708", size = 155487 }, + { url = "https://files.pythonhosted.org/packages/a9/60/f1711eb59ac7a6c5e98e5637fef5302f45b6f76a2c9d64fd83bbb341377a/websockets-13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:327b74e915cf13c5931334c61e1a41040e365d380f812513a255aa804b183418", size = 155721 }, + { url = "https://files.pythonhosted.org/packages/6a/e6/ba9a8db7f9d9b0e5f829cf626ff32677f39824968317223605a6b419d445/websockets-13.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:325b1ccdbf5e5725fdcb1b0e9ad4d2545056479d0eee392c291c1bf76206435a", size = 165609 }, + { url = "https://files.pythonhosted.org/packages/c1/22/4ec80f1b9c27a0aebd84ccd857252eda8418ab9681eb571b37ca4c5e1305/websockets-13.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:346bee67a65f189e0e33f520f253d5147ab76ae42493804319b5716e46dddf0f", size = 164556 }, + { url = "https://files.pythonhosted.org/packages/27/ac/35f423cb6bb15600438db80755609d27eda36d4c0b3c9d745ea12766c45e/websockets-13.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91a0fa841646320ec0d3accdff5b757b06e2e5c86ba32af2e0815c96c7a603c5", size = 164993 }, + { url = "https://files.pythonhosted.org/packages/31/4e/98db4fd267f8be9e52e86b6ee4e9aa7c42b83452ea0ea0672f176224b977/websockets-13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:18503d2c5f3943e93819238bf20df71982d193f73dcecd26c94514f417f6b135", size = 165360 }, + { url = "https://files.pythonhosted.org/packages/3f/15/3f0de7cda70ffc94b7e7024544072bc5b26e2c1eb36545291abb755d8cdb/websockets-13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9cd1af7e18e5221d2878378fbc287a14cd527fdd5939ed56a18df8a31136bb2", size = 164745 }, + { url = "https://files.pythonhosted.org/packages/a1/6e/66b6b756aebbd680b934c8bdbb6dcb9ce45aad72cde5f8a7208dbb00dd36/websockets-13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:70c5be9f416aa72aab7a2a76c90ae0a4fe2755c1816c153c1a2bcc3333ce4ce6", size = 164732 }, + { url = "https://files.pythonhosted.org/packages/35/c6/12e3aab52c11aeb289e3dbbc05929e7a9d90d7a9173958477d3ef4f8ce2d/websockets-13.1-cp313-cp313-win32.whl", hash = "sha256:624459daabeb310d3815b276c1adef475b3e6804abaf2d9d2c061c319f7f187d", size = 158709 }, + { url = "https://files.pythonhosted.org/packages/41/d8/63d6194aae711d7263df4498200c690a9c39fb437ede10f3e157a6343e0d/websockets-13.1-cp313-cp313-win_amd64.whl", hash = "sha256:c518e84bb59c2baae725accd355c8dc517b4a3ed8db88b4bc93c78dae2974bf2", size = 159144 }, { url = "https://files.pythonhosted.org/packages/2d/75/6da22cb3ad5b8c606963f9a5f9f88656256fecc29d420b4b2bf9e0c7d56f/websockets-13.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5dd6da9bec02735931fccec99d97c29f47cc61f644264eb995ad6c0c27667238", size = 155499 }, { url = "https://files.pythonhosted.org/packages/c0/ba/22833d58629088fcb2ccccedfae725ac0bbcd713319629e97125b52ac681/websockets-13.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:2510c09d8e8df777177ee3d40cd35450dc169a81e747455cc4197e63f7e7bfe5", size = 155737 }, { url = "https://files.pythonhosted.org/packages/95/54/61684fe22bdb831e9e1843d972adadf359cf04ab8613285282baea6a24bb/websockets-13.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1c3cf67185543730888b20682fb186fc8d0fa6f07ccc3ef4390831ab4b388d9", size = 157095 }, @@ -5395,6 +5658,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/89/bf/f6b75b4c2fcf0e7bb56edc0ed74e33f37fac45dc40e5a52a3be66b02587a/yarl-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d0eea830b591dbc68e030c86a9569826145df485b2b4554874b07fea1275a199", size = 356355 }, { url = "https://files.pythonhosted.org/packages/45/1f/50a0257cd07eef65c8c65ad6a21f5fb230012d659e021aeb6ac8a7897bf6/yarl-1.17.1-cp312-cp312-win32.whl", hash = "sha256:46ddf6e0b975cd680eb83318aa1d321cb2bf8d288d50f1754526230fcf59ba96", size = 83279 }, { url = "https://files.pythonhosted.org/packages/bc/82/fafb2c1268d63d54ec08b3a254fbe51f4ef098211501df646026717abee3/yarl-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:117ed8b3732528a1e41af3aa6d4e08483c2f0f2e3d3d7dca7cf538b3516d93df", size = 89590 }, + { url = "https://files.pythonhosted.org/packages/06/1e/5a93e3743c20eefbc68bd89334d9c9f04f3f2334380f7bbf5e950f29511b/yarl-1.17.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5d1d42556b063d579cae59e37a38c61f4402b47d70c29f0ef15cee1acaa64488", size = 139974 }, + { url = "https://files.pythonhosted.org/packages/a1/be/4e0f6919013c7c5eaea5c31811c551ccd599d2fc80aa3dd6962f1bbdcddd/yarl-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0167540094838ee9093ef6cc2c69d0074bbf84a432b4995835e8e5a0d984374", size = 93364 }, + { url = "https://files.pythonhosted.org/packages/73/f0/650f994bc491d0cb85df8bb45392780b90eab1e175f103a5edc61445ff67/yarl-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2f0a6423295a0d282d00e8701fe763eeefba8037e984ad5de44aa349002562ac", size = 91177 }, + { url = "https://files.pythonhosted.org/packages/f3/e8/9945ed555d14b43ede3ae8b1bd73e31068a694cad2b9d3cad0a28486c2eb/yarl-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5b078134f48552c4d9527db2f7da0b5359abd49393cdf9794017baec7506170", size = 333086 }, + { url = "https://files.pythonhosted.org/packages/a6/c0/7d167e48e14d26639ca066825af8da7df1d2fcdba827e3fd6341aaf22a3b/yarl-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d401f07261dc5aa36c2e4efc308548f6ae943bfff20fcadb0a07517a26b196d8", size = 343661 }, + { url = "https://files.pythonhosted.org/packages/fa/81/80a266517531d4e3553aecd141800dbf48d02e23ebd52909e63598a80134/yarl-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b5f1ac7359e17efe0b6e5fec21de34145caef22b260e978336f325d5c84e6938", size = 345196 }, + { url = "https://files.pythonhosted.org/packages/b0/77/6adc482ba7f2dc6c0d9b3b492e7cd100edfac4cfc3849c7ffa26fd7beb1a/yarl-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f63d176a81555984e91f2c84c2a574a61cab7111cc907e176f0f01538e9ff6e", size = 338743 }, + { url = "https://files.pythonhosted.org/packages/6d/cc/f0c4c0b92ff3ada517ffde2b127406c001504b225692216d969879ada89a/yarl-1.17.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e275792097c9f7e80741c36de3b61917aebecc08a67ae62899b074566ff8556", size = 326719 }, + { url = "https://files.pythonhosted.org/packages/18/3b/7bfc80d3376b5fa162189993a87a5a6a58057f88315bd0ea00610055b57a/yarl-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:81713b70bea5c1386dc2f32a8f0dab4148a2928c7495c808c541ee0aae614d67", size = 345826 }, + { url = "https://files.pythonhosted.org/packages/2e/66/cf0b0338107a5c370205c1a572432af08f36ca12ecce127f5b558398b4fd/yarl-1.17.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:aa46dce75078fceaf7cecac5817422febb4355fbdda440db55206e3bd288cfb8", size = 340335 }, + { url = "https://files.pythonhosted.org/packages/2f/52/b084b0eec0fd4d2490e1d33ace3320fad704c5f1f3deaa709f929d2d87fc/yarl-1.17.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1ce36ded585f45b1e9bb36d0ae94765c6608b43bd2e7f5f88079f7a85c61a4d3", size = 345301 }, + { url = "https://files.pythonhosted.org/packages/ef/38/9e2036d948efd3bafcdb4976cb212166fded76615f0dfc6c1492c4ce4784/yarl-1.17.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:2d374d70fdc36f5863b84e54775452f68639bc862918602d028f89310a034ab0", size = 354205 }, + { url = "https://files.pythonhosted.org/packages/81/c1/13dfe1e70b86811733316221c696580725ceb1c46d4e4db852807e134310/yarl-1.17.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2d9f0606baaec5dd54cb99667fcf85183a7477f3766fbddbe3f385e7fc253299", size = 360501 }, + { url = "https://files.pythonhosted.org/packages/91/87/756e05c74cd8bf9e71537df4a2cae7e8211a9ebe0d2350a3e26949e1e41c/yarl-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b0341e6d9a0c0e3cdc65857ef518bb05b410dbd70d749a0d33ac0f39e81a4258", size = 359452 }, + { url = "https://files.pythonhosted.org/packages/06/b2/b2bb09c1e6d59e1c9b1b36a86caa473e22c3dbf26d1032c030e9bfb554dc/yarl-1.17.1-cp313-cp313-win32.whl", hash = "sha256:2e7ba4c9377e48fb7b20dedbd473cbcbc13e72e1826917c185157a137dac9df2", size = 308904 }, + { url = "https://files.pythonhosted.org/packages/f3/27/f084d9a5668853c1f3b246620269b14ee871ef3c3cc4f3a1dd53645b68ec/yarl-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:949681f68e0e3c25377462be4b658500e85ca24323d9619fdc41f68d46a1ffda", size = 314637 }, { url = "https://files.pythonhosted.org/packages/52/ad/1fe7ff5f3e8869d4c5070f47b96bac2b4d15e67c100a8278d8e7876329fc/yarl-1.17.1-py3-none-any.whl", hash = "sha256:f1790a4b1e8e8e028c391175433b9c8122c39b46e1663228158e61e6f915bf06", size = 44352 }, ] From e89be1a2466d9b0a96830959a1bc6a958fa7fe21 Mon Sep 17 00:00:00 2001 From: Evan Mattson Date: Tue, 12 Nov 2024 16:40:46 -0500 Subject: [PATCH 4/9] Add the classes back --- .../models/rest_api_oauth_flow.py | 15 +++++++++++++ .../models/rest_api_oauth_flows.py | 22 +++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 python/semantic_kernel/connectors/openapi_plugin/models/rest_api_oauth_flow.py create mode 100644 python/semantic_kernel/connectors/openapi_plugin/models/rest_api_oauth_flows.py diff --git a/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_oauth_flow.py b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_oauth_flow.py new file mode 100644 index 000000000000..f47ec59801b6 --- /dev/null +++ b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_oauth_flow.py @@ -0,0 +1,15 @@ +# Copyright (c) Microsoft. All rights reserved. + +from semantic_kernel.utils.experimental_decorator import experimental_class + + +@experimental_class +class RestApiOAuthFlow: + """Represents the OAuth flow used by the REST API.""" + + def __init__(self, authorization_url: str, token_url: str, scopes: dict[str, str], refresh_url: str | None = None): + """Initializes a new instance of the RestApiOAuthFlow class.""" + self.authorization_url = authorization_url + self.token_url = token_url + self.refresh_url = refresh_url + self.scopes = scopes diff --git a/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_oauth_flows.py b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_oauth_flows.py new file mode 100644 index 000000000000..4da0d4698ed1 --- /dev/null +++ b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_oauth_flows.py @@ -0,0 +1,22 @@ +# Copyright (c) Microsoft. All rights reserved. + +from semantic_kernel.connectors.openapi_plugin.models.rest_api_oauth_flow import RestApiOAuthFlow +from semantic_kernel.utils.experimental_decorator import experimental_class + + +@experimental_class +class RestApiOAuthFlows: + """Represents the OAuth flows used by the REST API.""" + + def __init__( + self, + implicit: RestApiOAuthFlow | None = None, + password: RestApiOAuthFlow | None = None, + client_credentials: RestApiOAuthFlow | None = None, + authorization_code: RestApiOAuthFlow | None = None, + ): + """Initializes a new instance of the RestApiOAuthFlows class.""" + self.implicit = implicit + self.password = password + self.client_credentials = client_credentials + self.authorization_code = authorization_code From f6ac46e06858968ddb886a3d19c9e9eb6206ed77 Mon Sep 17 00:00:00 2001 From: Evan Mattson Date: Fri, 15 Nov 2024 12:04:16 -0500 Subject: [PATCH 5/9] Rename openapi classes to remove operation. Add ability to set operation skip predicate. Allow for customer spec parsing. --- .../openai_authentication_config.py | 6 +- .../openai_function_execution_parameters.py | 40 ++- .../connectors/openai_plugin/openai_utils.py | 2 +- .../connectors/openapi_plugin/__init__.py | 6 +- ...ponse.py => rest_api_expected_response.py} | 6 +- .../models/rest_api_oauth_flow.py | 13 +- .../models/rest_api_oauth_flows.py | 19 +- .../models/rest_api_operation.py | 210 +++++++++++---- .../models/rest_api_operation_parameter.py | 44 ---- .../models/rest_api_operation_payload.py | 24 -- .../rest_api_operation_payload_property.py | 29 --- .../models/rest_api_parameter.py | 155 +++++++++++ ...tion.py => rest_api_parameter_location.py} | 4 +- ...r_style.py => rest_api_parameter_style.py} | 4 +- .../openapi_plugin/models/rest_api_payload.py | 77 ++++++ .../models/rest_api_payload_property.py | 112 ++++++++ ...run_options.py => rest_api_run_options.py} | 2 +- .../models/rest_api_security_scheme.py | 6 +- .../openapi_function_execution_parameters.py | 8 +- .../openapi_plugin/openapi_manager.py | 58 +++-- .../openapi_plugin/openapi_parser.py | 55 ++-- .../openapi_plugin/openapi_runner.py | 20 +- .../operation_selection_predicate_context.py | 12 + .../functions/kernel_plugin.py | 22 +- .../cross_language/test_cross_language.py | 3 +- .../openapi_plugin/test_openapi_manager.py | 22 +- .../openapi_plugin/test_openapi_runner.py | 27 +- .../test_rest_api_operation_run_options.py | 6 +- .../openapi_plugin/test_sk_openapi.py | 242 +++++++++++------- .../unit/functions/test_kernel_plugins.py | 21 +- 30 files changed, 891 insertions(+), 364 deletions(-) rename python/semantic_kernel/connectors/openapi_plugin/models/{rest_api_operation_expected_response.py => rest_api_expected_response.py} (70%) delete mode 100644 python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation_parameter.py delete mode 100644 python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation_payload.py delete mode 100644 python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation_payload_property.py create mode 100644 python/semantic_kernel/connectors/openapi_plugin/models/rest_api_parameter.py rename python/semantic_kernel/connectors/openapi_plugin/models/{rest_api_operation_parameter_location.py => rest_api_parameter_location.py} (71%) rename python/semantic_kernel/connectors/openapi_plugin/models/{rest_api_operation_parameter_style.py => rest_api_parameter_style.py} (69%) create mode 100644 python/semantic_kernel/connectors/openapi_plugin/models/rest_api_payload.py create mode 100644 python/semantic_kernel/connectors/openapi_plugin/models/rest_api_payload_property.py rename python/semantic_kernel/connectors/openapi_plugin/models/{rest_api_operation_run_options.py => rest_api_run_options.py} (92%) create mode 100644 python/semantic_kernel/connectors/openapi_plugin/operation_selection_predicate_context.py diff --git a/python/semantic_kernel/connectors/openai_plugin/openai_authentication_config.py b/python/semantic_kernel/connectors/openai_plugin/openai_authentication_config.py index ce1fbfac87ae..e9025334d159 100644 --- a/python/semantic_kernel/connectors/openai_plugin/openai_authentication_config.py +++ b/python/semantic_kernel/connectors/openai_plugin/openai_authentication_config.py @@ -9,7 +9,7 @@ from semantic_kernel.kernel_pydantic import KernelBaseModel -@deprecated("The `OpenAIAuthenticationType` class is deprecated; use OpenAPI Plugins instead.", category=None) +@deprecated("The `OpenAIAuthenticationType` class is deprecated; use the `OpenAPI` plugin instead.", category=None) class OpenAIAuthenticationType(str, Enum): """OpenAI authentication types.""" @@ -17,7 +17,7 @@ class OpenAIAuthenticationType(str, Enum): NoneType = "none" -@deprecated("The `OpenAIAuthenticationType` class is deprecated; use OpenAPI Plugins instead.", category=None) +@deprecated("The `OpenAIAuthenticationType` class is deprecated; use the `OpenAPI` plugin instead.", category=None) class OpenAIAuthorizationType(str, Enum): """OpenAI authorization types.""" @@ -25,7 +25,7 @@ class OpenAIAuthorizationType(str, Enum): Basic = "Basic" -@deprecated("The `OpenAIAuthenticationConfig` class is deprecated; use OpenAPI Plugins instead.", category=None) +@deprecated("The `OpenAIAuthenticationConfig` class is deprecated; use the `OpenAPI` plugin instead.", category=None) class OpenAIAuthenticationConfig(KernelBaseModel): """OpenAI authentication configuration.""" diff --git a/python/semantic_kernel/connectors/openai_plugin/openai_function_execution_parameters.py b/python/semantic_kernel/connectors/openai_plugin/openai_function_execution_parameters.py index 93b647b3295d..b590f3f28898 100644 --- a/python/semantic_kernel/connectors/openai_plugin/openai_function_execution_parameters.py +++ b/python/semantic_kernel/connectors/openai_plugin/openai_function_execution_parameters.py @@ -2,19 +2,47 @@ from collections.abc import Awaitable, Callable -from typing import Any +from typing import TYPE_CHECKING, Any +from urllib.parse import urlparse +import httpx +from pydantic import Field from typing_extensions import deprecated -from semantic_kernel.connectors.openapi_plugin.openapi_function_execution_parameters import ( - OpenAPIFunctionExecutionParameters, -) +from semantic_kernel.kernel_pydantic import KernelBaseModel + +if TYPE_CHECKING: + from semantic_kernel.connectors.openapi_plugin import ( + OperationSelectionPredicateContext, + ) OpenAIAuthCallbackType = Callable[..., Awaitable[Any]] -@deprecated("The `OpenAIFunctionExecutionParameters` class is deprecated; use OpenAPI Plugins instead.", category=None) -class OpenAIFunctionExecutionParameters(OpenAPIFunctionExecutionParameters): +@deprecated( + "The `OpenAIFunctionExecutionParameters` class is deprecated; use the `OpenAPI` plugin instead.", category=None +) +class OpenAIFunctionExecutionParameters(KernelBaseModel): """OpenAI function execution parameters.""" auth_callback: OpenAIAuthCallbackType | None = None + http_client: httpx.AsyncClient | None = None + server_url_override: str | None = None + ignore_non_compliant_errors: bool = False + user_agent: str | None = None + enable_dynamic_payload: bool = True + enable_payload_namespacing: bool = False + operations_to_exclude: list[str] = Field(default_factory=list, description="The operationId(s) to exclude") + operation_selection_predicate: Callable[["OperationSelectionPredicateContext"], bool] | None = None + + def model_post_init(self, __context: Any) -> None: + """Post initialization method for the model.""" + from semantic_kernel.utils.telemetry.user_agent import HTTP_USER_AGENT + + if self.server_url_override: + parsed_url = urlparse(self.server_url_override) + if not parsed_url.scheme or not parsed_url.netloc: + raise ValueError(f"Invalid server_url_override: {self.server_url_override}") + + if not self.user_agent: + self.user_agent = HTTP_USER_AGENT diff --git a/python/semantic_kernel/connectors/openai_plugin/openai_utils.py b/python/semantic_kernel/connectors/openai_plugin/openai_utils.py index fa3617c46a8b..9e95374f7f5a 100644 --- a/python/semantic_kernel/connectors/openai_plugin/openai_utils.py +++ b/python/semantic_kernel/connectors/openai_plugin/openai_utils.py @@ -11,7 +11,7 @@ logger: logging.Logger = logging.getLogger(__name__) -@deprecated("The `OpenAIUtils` class is deprecated; use OpenAPI Plugins instead.", category=None) +@deprecated("The `OpenAIUtils` class is deprecated; use the `OpenAPI` plugin instead.", category=None) class OpenAIUtils: """Utility functions for OpenAI plugins.""" diff --git a/python/semantic_kernel/connectors/openapi_plugin/__init__.py b/python/semantic_kernel/connectors/openapi_plugin/__init__.py index 8ad89fbd5635..875c5155d301 100644 --- a/python/semantic_kernel/connectors/openapi_plugin/__init__.py +++ b/python/semantic_kernel/connectors/openapi_plugin/__init__.py @@ -3,5 +3,9 @@ from semantic_kernel.connectors.openapi_plugin.openapi_function_execution_parameters import ( OpenAPIFunctionExecutionParameters, ) +from semantic_kernel.connectors.openapi_plugin.openapi_parser import OpenApiParser +from semantic_kernel.connectors.openapi_plugin.operation_selection_predicate_context import ( + OperationSelectionPredicateContext, +) -__all__ = ["OpenAPIFunctionExecutionParameters"] +__all__ = ["OpenAPIFunctionExecutionParameters", "OpenApiParser", "OperationSelectionPredicateContext"] diff --git a/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation_expected_response.py b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_expected_response.py similarity index 70% rename from python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation_expected_response.py rename to python/semantic_kernel/connectors/openapi_plugin/models/rest_api_expected_response.py index f5669ecb081d..5fee34f9e2c0 100644 --- a/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation_expected_response.py +++ b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_expected_response.py @@ -5,11 +5,11 @@ @experimental_class -class RestApiOperationExpectedResponse: - """RestApiOperationExpectedResponse.""" +class RestApiExpectedResponse: + """RestApiExpectedResponse.""" def __init__(self, description: str, media_type: str, schema: dict[str, str] | None = None): - """Initialize the RestApiOperationExpectedResponse.""" + """Initialize the RestApiExpectedResponse.""" self.description = description self.media_type = media_type self.schema = schema diff --git a/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_oauth_flow.py b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_oauth_flow.py index f47ec59801b6..2de8cc4162ec 100644 --- a/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_oauth_flow.py +++ b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_oauth_flow.py @@ -1,15 +1,16 @@ # Copyright (c) Microsoft. All rights reserved. +from dataclasses import dataclass + from semantic_kernel.utils.experimental_decorator import experimental_class @experimental_class +@dataclass class RestApiOAuthFlow: """Represents the OAuth flow used by the REST API.""" - def __init__(self, authorization_url: str, token_url: str, scopes: dict[str, str], refresh_url: str | None = None): - """Initializes a new instance of the RestApiOAuthFlow class.""" - self.authorization_url = authorization_url - self.token_url = token_url - self.refresh_url = refresh_url - self.scopes = scopes + authorization_url: str + token_url: str + scopes: dict[str, str] + refresh_url: str | None = None diff --git a/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_oauth_flows.py b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_oauth_flows.py index 4da0d4698ed1..f739b757cb95 100644 --- a/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_oauth_flows.py +++ b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_oauth_flows.py @@ -1,22 +1,17 @@ # Copyright (c) Microsoft. All rights reserved. +from dataclasses import dataclass + from semantic_kernel.connectors.openapi_plugin.models.rest_api_oauth_flow import RestApiOAuthFlow from semantic_kernel.utils.experimental_decorator import experimental_class @experimental_class +@dataclass class RestApiOAuthFlows: """Represents the OAuth flows used by the REST API.""" - def __init__( - self, - implicit: RestApiOAuthFlow | None = None, - password: RestApiOAuthFlow | None = None, - client_credentials: RestApiOAuthFlow | None = None, - authorization_code: RestApiOAuthFlow | None = None, - ): - """Initializes a new instance of the RestApiOAuthFlows class.""" - self.implicit = implicit - self.password = password - self.client_credentials = client_credentials - self.authorization_code = authorization_code + implicit: RestApiOAuthFlow | None = None + password: RestApiOAuthFlow | None = None + client_credentials: RestApiOAuthFlow | None = None + authorization_code: RestApiOAuthFlow | None = None diff --git a/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation.py b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation.py index a621c1fb75ba..733c6d12101c 100644 --- a/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation.py +++ b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation.py @@ -4,19 +4,19 @@ from typing import Any, Final from urllib.parse import ParseResult, urlencode, urljoin, urlparse, urlunparse -from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation_expected_response import ( - RestApiOperationExpectedResponse, +from semantic_kernel.connectors.openapi_plugin.models.rest_api_expected_response import ( + RestApiExpectedResponse, ) -from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation_parameter import RestApiOperationParameter -from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation_parameter_location import ( - RestApiOperationParameterLocation, +from semantic_kernel.connectors.openapi_plugin.models.rest_api_parameter import RestApiParameter +from semantic_kernel.connectors.openapi_plugin.models.rest_api_parameter_location import ( + RestApiParameterLocation, ) -from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation_parameter_style import ( - RestApiOperationParameterStyle, +from semantic_kernel.connectors.openapi_plugin.models.rest_api_parameter_style import ( + RestApiParameterStyle, ) -from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation_payload import RestApiOperationPayload -from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation_payload_property import ( - RestApiOperationPayloadProperty, +from semantic_kernel.connectors.openapi_plugin.models.rest_api_payload import RestApiPayload +from semantic_kernel.connectors.openapi_plugin.models.rest_api_payload_property import ( + RestApiPayloadProperty, ) from semantic_kernel.connectors.openapi_plugin.models.rest_api_security_requirement import RestApiSecurityRequirement from semantic_kernel.exceptions.function_exceptions import FunctionExecutionException @@ -56,22 +56,140 @@ def __init__( path: str, summary: str | None = None, description: str | None = None, - params: list["RestApiOperationParameter"] | None = None, - request_body: "RestApiOperationPayload | None" = None, - responses: dict[str, "RestApiOperationExpectedResponse"] | None = None, + params: list["RestApiParameter"] | None = None, + request_body: "RestApiPayload | None" = None, + responses: dict[str, "RestApiExpectedResponse"] | None = None, security_requirements: list[RestApiSecurityRequirement] | None = None, ): """Initialize the RestApiOperation.""" - self.id = id - self.method = method.upper() - self.servers = [urlparse(s) if isinstance(s, str) else s for s in servers] - self.path = path - self.summary = summary - self.description = description - self.parameters = params if params else [] - self.request_body = request_body - self.responses = responses - self.security_requirements = security_requirements + self._id = id + self._method = method.upper() + self._servers = [urlparse(s) if isinstance(s, str) else s for s in servers] + self._path = path + self._summary = summary + self._description = description + self._parameters = params if params else [] + self._request_body = request_body + self._responses = responses + self._security_requirements = security_requirements + self._is_frozen = False + + def freeze(self): + """Make the instance and its components immutable.""" + self._is_frozen = True + + if self.request_body: + self.request_body.freeze() + + for param in self.parameters: + param.freeze() + + def _throw_if_frozen(self): + """Raise an exception if the object is frozen.""" + if self._is_frozen: + raise FunctionExecutionException( + f"The `RestApiOperation` instance with id {self.id} is frozen and cannot be modified." + ) + + @property + def id(self): + """Get the ID of the operation.""" + return self._id + + @id.setter + def id(self, value: str): + self._throw_if_frozen() + self._id = value + + @property + def method(self): + """Get the method of the operation.""" + return self._method + + @method.setter + def method(self, value: str): + self._throw_if_frozen() + self._method = value + + @property + def servers(self): + """Get the servers of the operation.""" + return self._servers + + @servers.setter + def servers(self, value: list[str | ParseResult]): + self._throw_if_frozen() + self._servers = value + + @property + def path(self): + """Get the path of the operation.""" + return self._path + + @path.setter + def path(self, value: str): + self._throw_if_frozen() + self._path = value + + @property + def summary(self): + """Get the summary of the operation.""" + return self._summary + + @summary.setter + def summary(self, value: str | None): + self._throw_if_frozen() + self._summary = value + + @property + def description(self): + """Get the description of the operation.""" + return self._description + + @description.setter + def description(self, value: str | None): + self._throw_if_frozen() + self._description = value + + @property + def parameters(self): + """Get the parameters of the operation.""" + return self._parameters + + @parameters.setter + def parameters(self, value: list["RestApiParameter"]): + self._throw_if_frozen() + self._parameters = value + + @property + def request_body(self): + """Get the request body of the operation.""" + return self._request_body + + @request_body.setter + def request_body(self, value: "RestApiPayload | None"): + self._throw_if_frozen() + self._request_body = value + + @property + def responses(self): + """Get the responses of the operation.""" + return self._responses + + @responses.setter + def responses(self, value: dict[str, "RestApiExpectedResponse"] | None): + self._throw_if_frozen() + self._responses = value + + @property + def security_requirements(self): + """Get the security requirements of the operation.""" + return self._security_requirements + + @security_requirements.setter + def security_requirements(self, value: list[RestApiSecurityRequirement] | None): + self._throw_if_frozen() + self._security_requirements = value def url_join(self, base_url: str, path: str): """Join a base URL and a path, correcting for any missing slashes.""" @@ -84,7 +202,7 @@ def build_headers(self, arguments: dict[str, Any]) -> dict[str, str]: """Build the headers for the operation.""" headers = {} - parameters = [p for p in self.parameters if p.location == RestApiOperationParameterLocation.HEADER] + parameters = [p for p in self.parameters if p.location == RestApiParameterLocation.HEADER] for parameter in parameters: argument = arguments.get(parameter.name) @@ -128,7 +246,7 @@ def get_server_url(self, server_url_override=None, api_host_url=None): def build_path(self, path_template: str, arguments: dict[str, Any]) -> str: """Build the path for the operation.""" - parameters = [p for p in self.parameters if p.location == RestApiOperationParameterLocation.PATH] + parameters = [p for p in self.parameters if p.location == RestApiParameterLocation.PATH] for parameter in parameters: argument = arguments.get(parameter.name) if argument is None: @@ -144,7 +262,7 @@ def build_path(self, path_template: str, arguments: dict[str, Any]) -> str: def build_query_string(self, arguments: dict[str, Any]) -> str: """Build the query string for the operation.""" segments = [] - parameters = [p for p in self.parameters if p.location == RestApiOperationParameterLocation.QUERY] + parameters = [p for p in self.parameters if p.location == RestApiParameterLocation.QUERY] for parameter in parameters: argument = arguments.get(parameter.name) if argument is None: @@ -166,7 +284,7 @@ def get_parameters( operation: "RestApiOperation", add_payload_params_from_metadata: bool = True, enable_payload_spacing: bool = False, - ) -> list["RestApiOperationParameter"]: + ) -> list["RestApiParameter"]: """Get the parameters for the operation.""" params = list(operation.parameters) if operation.parameters is not None else [] if operation.request_body is not None: @@ -183,9 +301,9 @@ def get_parameters( return params - def create_payload_artificial_parameter(self, operation: "RestApiOperation") -> "RestApiOperationParameter": + def create_payload_artificial_parameter(self, operation: "RestApiOperation") -> "RestApiParameter": """Create an artificial parameter for the REST API request body.""" - return RestApiOperationParameter( + return RestApiParameter( name=self.PAYLOAD_ARGUMENT_NAME, type=( "string" @@ -194,54 +312,52 @@ def create_payload_artificial_parameter(self, operation: "RestApiOperation") -> else "object" ), is_required=True, - location=RestApiOperationParameterLocation.BODY, - style=RestApiOperationParameterStyle.SIMPLE, + location=RestApiParameterLocation.BODY, + style=RestApiParameterStyle.SIMPLE, description=operation.request_body.description if operation.request_body else "REST API request body.", schema=operation.request_body.schema if operation.request_body else None, ) - def create_content_type_artificial_parameter(self) -> "RestApiOperationParameter": + def create_content_type_artificial_parameter(self) -> "RestApiParameter": """Create an artificial parameter for the content type of the REST API request body.""" - return RestApiOperationParameter( + return RestApiParameter( name=self.CONTENT_TYPE_ARGUMENT_NAME, type="string", is_required=False, - location=RestApiOperationParameterLocation.BODY, - style=RestApiOperationParameterStyle.SIMPLE, + location=RestApiParameterLocation.BODY, + style=RestApiParameterStyle.SIMPLE, description="Content type of REST API request body.", ) - def _get_property_name( - self, property: RestApiOperationPayloadProperty, root_property_name: bool, enable_namespacing: bool - ): + def _get_property_name(self, property: RestApiPayloadProperty, root_property_name: bool, enable_namespacing: bool): if enable_namespacing and root_property_name: return f"{root_property_name}.{property.name}" return property.name def _get_parameters_from_payload_metadata( self, - properties: list["RestApiOperationPayloadProperty"], + properties: list["RestApiPayloadProperty"], enable_namespacing: bool = False, root_property_name: bool | None = None, - ) -> list["RestApiOperationParameter"]: - parameters: list[RestApiOperationParameter] = [] + ) -> list["RestApiParameter"]: + parameters: list[RestApiParameter] = [] for property in properties: parameter_name = self._get_property_name(property, root_property_name or False, enable_namespacing) if not hasattr(property, "properties") or not property.properties: parameters.append( - RestApiOperationParameter( + RestApiParameter( name=parameter_name, type=property.type, is_required=property.is_required, - location=RestApiOperationParameterLocation.BODY, - style=RestApiOperationParameterStyle.SIMPLE, + location=RestApiParameterLocation.BODY, + style=RestApiParameterStyle.SIMPLE, description=property.description, schema=property.schema, ) ) else: # Handle property.properties as a single instance or a list - if isinstance(property.properties, RestApiOperationPayloadProperty): + if isinstance(property.properties, RestApiPayloadProperty): nested_properties = [property.properties] else: nested_properties = property.properties @@ -272,8 +388,8 @@ def get_payload_parameters( ] def get_default_response( - self, responses: dict[str, RestApiOperationExpectedResponse], preferred_responses: list[str] - ) -> RestApiOperationExpectedResponse | None: + self, responses: dict[str, RestApiExpectedResponse], preferred_responses: list[str] + ) -> RestApiExpectedResponse | None: """Get the default response for the operation. If no appropriate response is found, returns None. diff --git a/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation_parameter.py b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation_parameter.py deleted file mode 100644 index 0f8745e08f2e..000000000000 --- a/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation_parameter.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -from typing import Any - -from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation_expected_response import ( - RestApiOperationExpectedResponse, -) -from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation_parameter_location import ( - RestApiOperationParameterLocation, -) -from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation_parameter_style import ( - RestApiOperationParameterStyle, -) -from semantic_kernel.utils.experimental_decorator import experimental_class - - -@experimental_class -class RestApiOperationParameter: - """RestApiOperationParameter.""" - - def __init__( - self, - name: str, - type: str, - location: RestApiOperationParameterLocation, - style: RestApiOperationParameterStyle | None = None, - alternative_name: str | None = None, - description: str | None = None, - is_required: bool = False, - default_value: Any | None = None, - schema: str | dict | None = None, - response: RestApiOperationExpectedResponse | None = None, - ): - """Initialize the RestApiOperationParameter.""" - self.name = name - self.type = type - self.location = location - self.style = style - self.alternative_name = alternative_name - self.description = description - self.is_required = is_required - self.default_value = default_value - self.schema = schema - self.response = response diff --git a/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation_payload.py b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation_payload.py deleted file mode 100644 index 6734114f28a2..000000000000 --- a/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation_payload.py +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation_payload_property import ( - RestApiOperationPayloadProperty, -) -from semantic_kernel.utils.experimental_decorator import experimental_class - - -@experimental_class -class RestApiOperationPayload: - """RestApiOperationPayload.""" - - def __init__( - self, - media_type: str, - properties: list["RestApiOperationPayloadProperty"], - description: str | None = None, - schema: str | None = None, - ): - """Initialize the RestApiOperationPayload.""" - self.media_type = media_type - self.properties = properties - self.description = description - self.schema = schema diff --git a/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation_payload_property.py b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation_payload_property.py deleted file mode 100644 index ab0ee15f3e9d..000000000000 --- a/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation_payload_property.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -from typing import Any - -from semantic_kernel.utils.experimental_decorator import experimental_class - - -@experimental_class -class RestApiOperationPayloadProperty: - """RestApiOperationPayloadProperty.""" - - def __init__( - self, - name: str, - type: str, - properties: "RestApiOperationPayloadProperty", - description: str | None = None, - is_required: bool = False, - default_value: Any | None = None, - schema: str | None = None, - ): - """Initialize the RestApiOperationPayloadProperty.""" - self.name = name - self.type = type - self.properties = properties - self.description = description - self.is_required = is_required - self.default_value = default_value - self.schema = schema diff --git a/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_parameter.py b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_parameter.py new file mode 100644 index 000000000000..8ae4ad0db471 --- /dev/null +++ b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_parameter.py @@ -0,0 +1,155 @@ +# Copyright (c) Microsoft. All rights reserved. + +from typing import Any + +from semantic_kernel.connectors.openapi_plugin.models.rest_api_expected_response import ( + RestApiExpectedResponse, +) +from semantic_kernel.connectors.openapi_plugin.models.rest_api_parameter_location import ( + RestApiParameterLocation, +) +from semantic_kernel.connectors.openapi_plugin.models.rest_api_parameter_style import ( + RestApiParameterStyle, +) +from semantic_kernel.exceptions.function_exceptions import FunctionExecutionException +from semantic_kernel.utils.experimental_decorator import experimental_class + + +@experimental_class +class RestApiParameter: + """RestApiParameter.""" + + def __init__( + self, + name: str, + type: str, + location: RestApiParameterLocation, + style: RestApiParameterStyle | None = None, + alternative_name: str | None = None, + description: str | None = None, + is_required: bool = False, + default_value: Any | None = None, + schema: str | dict | None = None, + response: RestApiExpectedResponse | None = None, + ): + """Initialize the RestApiParameter.""" + self._name = name + self._type = type + self._location = location + self._style = style + self._alternative_name = alternative_name + self._description = description + self._is_required = is_required + self._default_value = default_value + self._schema = schema + self._response = response + self._is_frozen = False + + def freeze(self): + """Make the instance immutable.""" + self._is_frozen = True + + def _throw_if_frozen(self): + """Raise an exception if the object is frozen.""" + if self._is_frozen: + raise FunctionExecutionException("This `RestApiParameter` instance is frozen and cannot be modified.") + + @property + def name(self): + """Get the name of the parameter.""" + return self._name + + @name.setter + def name(self, value: str): + self._throw_if_frozen() + self._name = value + + @property + def type(self): + """Get the type of the parameter.""" + return self._type + + @type.setter + def type(self, value: str): + self._throw_if_frozen() + self._type = value + + @property + def location(self): + """Get the location of the parameter.""" + return self._location + + @location.setter + def location(self, value: str): + self._throw_if_frozen() + self._location = value + + @property + def style(self): + """Get the style of the parameter.""" + return self._style + + @style.setter + def style(self, value: str | None): + self._throw_if_frozen() + self._style = value + + @property + def alternative_name(self): + """Get the alternative name of the parameter.""" + return self._alternative_name + + @alternative_name.setter + def alternative_name(self, value: str | None): + self._throw_if_frozen() + self._alternative_name = value + + @property + def description(self): + """Get the description of the parameter.""" + return self._description + + @description.setter + def description(self, value: str | None): + self._throw_if_frozen() + self._description = value + + @property + def is_required(self): + """Get whether the parameter is required.""" + return self._is_required + + @is_required.setter + def is_required(self, value: bool): + self._throw_if_frozen() + self._is_required = value + + @property + def default_value(self): + """Get the default value of the parameter.""" + return self._default_value + + @default_value.setter + def default_value(self, value: Any | None): + self._throw_if_frozen() + self._default_value = value + + @property + def schema(self): + """Get the schema of the parameter.""" + return self._schema + + @schema.setter + def schema(self, value: str | dict | None): + self._throw_if_frozen() + self._schema = value + + @property + def response(self): + """Get the response of the parameter.""" + return self._response + + @response.setter + def response(self, value: Any | None): + self._throw_if_frozen() + self._response = value diff --git a/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation_parameter_location.py b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_parameter_location.py similarity index 71% rename from python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation_parameter_location.py rename to python/semantic_kernel/connectors/openapi_plugin/models/rest_api_parameter_location.py index f1d7b68e2f0a..25da836bd3ce 100644 --- a/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation_parameter_location.py +++ b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_parameter_location.py @@ -6,8 +6,8 @@ @experimental_class -class RestApiOperationParameterLocation(Enum): - """The location of the REST API operation parameter.""" +class RestApiParameterLocation(Enum): + """The location of the REST API parameter.""" PATH = "path" QUERY = "query" diff --git a/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation_parameter_style.py b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_parameter_style.py similarity index 69% rename from python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation_parameter_style.py rename to python/semantic_kernel/connectors/openapi_plugin/models/rest_api_parameter_style.py index c76f9e3a8847..a5db1b921f6f 100644 --- a/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation_parameter_style.py +++ b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_parameter_style.py @@ -6,7 +6,7 @@ @experimental_class -class RestApiOperationParameterStyle(Enum): - """RestApiOperationParameterStyle.""" +class RestApiParameterStyle(Enum): + """RestApiParameterStyle.""" SIMPLE = "simple" diff --git a/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_payload.py b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_payload.py new file mode 100644 index 000000000000..21c7cb288500 --- /dev/null +++ b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_payload.py @@ -0,0 +1,77 @@ +# Copyright (c) Microsoft. All rights reserved. + +from semantic_kernel.connectors.openapi_plugin.models.rest_api_payload_property import ( + RestApiPayloadProperty, +) +from semantic_kernel.exceptions.function_exceptions import FunctionExecutionException +from semantic_kernel.utils.experimental_decorator import experimental_class + + +@experimental_class +class RestApiPayload: + """RestApiPayload.""" + + def __init__( + self, + media_type: str, + properties: list[RestApiPayloadProperty], + description: str | None = None, + schema: str | None = None, + ): + """Initialize the RestApiPayload.""" + self._media_type = media_type + self._properties = properties + self._description = description + self._schema = schema + self._is_frozen = False + + def freeze(self): + """Make the instance immutable and freeze properties.""" + self._is_frozen = True + for property in self._properties: + property.freeze() + + def _throw_if_frozen(self): + """Raise an exception if the object is frozen.""" + if self._is_frozen: + raise FunctionExecutionException("This `RestApiPayload` instance is frozen and cannot be modified.") + + @property + def media_type(self): + """Get the media type of the payload.""" + return self._media_type + + @media_type.setter + def media_type(self, value: str): + self._throw_if_frozen() + self._media_type = value + + @property + def description(self): + """Get the description of the payload.""" + return self._description + + @description.setter + def description(self, value: str | None): + self._throw_if_frozen() + self._description = value + + @property + def properties(self): + """Get the properties of the payload.""" + return self._properties + + @properties.setter + def properties(self, value: list[RestApiPayloadProperty]): + self._throw_if_frozen() + self._properties = value + + @property + def schema(self): + """Get the schema of the payload.""" + return self._schema + + @schema.setter + def schema(self, value: str | None): + self._throw_if_frozen() + self._schema = value diff --git a/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_payload_property.py b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_payload_property.py new file mode 100644 index 000000000000..455609fdf927 --- /dev/null +++ b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_payload_property.py @@ -0,0 +1,112 @@ +# Copyright (c) Microsoft. All rights reserved. + +from typing import Any + +from semantic_kernel.exceptions.function_exceptions import FunctionExecutionException +from semantic_kernel.utils.experimental_decorator import experimental_class + + +@experimental_class +class RestApiPayloadProperty: + """RestApiPayloadProperty.""" + + def __init__( + self, + name: str, + type: str, + properties: list["RestApiPayloadProperty"] | None = None, + description: str | None = None, + is_required: bool = False, + default_value: Any | None = None, + schema: str | None = None, + ): + """Initialize the RestApiPayloadProperty.""" + self._name = name + self._type = type + self._properties = properties or [] + self._description = description + self._is_required = is_required + self._default_value = default_value + self._schema = schema + self._is_frozen = False + + def freeze(self): + """Make the instance immutable, and freeze nested properties.""" + self._is_frozen = True + for prop in self._properties: + prop.freeze() + + def _throw_if_frozen(self): + """Raise an exception if the object is frozen.""" + if self._is_frozen: + raise FunctionExecutionException("This instance is frozen and cannot be modified.") + + @property + def name(self): + """Get the name of the property.""" + return self._name + + @name.setter + def name(self, value: str): + self._throw_if_frozen() + self._name = value + + @property + def type(self): + """Get the type of the property.""" + return self._type + + @type.setter + def type(self, value: str): + self._throw_if_frozen() + self._type = value + + @property + def properties(self): + """Get the properties of the property.""" + return self._properties + + @properties.setter + def properties(self, value: list["RestApiPayloadProperty"]): + self._throw_if_frozen() + self._properties = value + + @property + def description(self): + """Get the description of the property.""" + return self._description + + @description.setter + def description(self, value: str | None): + self._throw_if_frozen() + self._description = value + + @property + def is_required(self): + """Get whether the property is required.""" + return self._is_required + + @is_required.setter + def is_required(self, value: bool): + self._throw_if_frozen() + self._is_required = value + + @property + def default_value(self): + """Get the default value of the property.""" + return self._default_value + + @default_value.setter + def default_value(self, value: Any | None): + self._throw_if_frozen() + self._default_value = value + + @property + def schema(self): + """Get the schema of the property.""" + return self._schema + + @schema.setter + def schema(self, value: str | None): + self._throw_if_frozen() + self._schema = value diff --git a/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation_run_options.py b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_run_options.py similarity index 92% rename from python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation_run_options.py rename to python/semantic_kernel/connectors/openapi_plugin/models/rest_api_run_options.py index 332a446bf609..78ce7a760ca7 100644 --- a/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation_run_options.py +++ b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_run_options.py @@ -4,7 +4,7 @@ @experimental_class -class RestApiOperationRunOptions: +class RestApiRunOptions: """The options for running the REST API operation.""" def __init__(self, server_url_override=None, api_host_url=None) -> None: diff --git a/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_security_scheme.py b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_security_scheme.py index ccb9a39773b5..c57669b7f121 100644 --- a/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_security_scheme.py +++ b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_security_scheme.py @@ -1,8 +1,8 @@ # Copyright (c) Microsoft. All rights reserved. from semantic_kernel.connectors.openapi_plugin.models.rest_api_oauth_flows import RestApiOAuthFlows -from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation_parameter_location import ( - RestApiOperationParameterLocation, +from semantic_kernel.connectors.openapi_plugin.models.rest_api_parameter_location import ( + RestApiParameterLocation, ) from semantic_kernel.utils.experimental_decorator import experimental_class @@ -15,7 +15,7 @@ def __init__( self, security_scheme_type: str, name: str, - in_: RestApiOperationParameterLocation, + in_: RestApiParameterLocation, scheme: str, open_id_connect_url: str, description: str | None = None, diff --git a/python/semantic_kernel/connectors/openapi_plugin/openapi_function_execution_parameters.py b/python/semantic_kernel/connectors/openapi_plugin/openapi_function_execution_parameters.py index 0f1463ebbf0d..b5ff0ec2ae4e 100644 --- a/python/semantic_kernel/connectors/openapi_plugin/openapi_function_execution_parameters.py +++ b/python/semantic_kernel/connectors/openapi_plugin/openapi_function_execution_parameters.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft. All rights reserved. from collections.abc import Awaitable, Callable -from typing import Any +from typing import TYPE_CHECKING, Any from urllib.parse import urlparse import httpx @@ -10,6 +10,11 @@ from semantic_kernel.kernel_pydantic import KernelBaseModel from semantic_kernel.utils.experimental_decorator import experimental_class +if TYPE_CHECKING: + from semantic_kernel.connectors.openapi_plugin import ( + OperationSelectionPredicateContext, + ) + AuthCallbackType = Callable[..., Awaitable[Any]] @@ -25,6 +30,7 @@ class OpenAPIFunctionExecutionParameters(KernelBaseModel): enable_dynamic_payload: bool = True enable_payload_namespacing: bool = False operations_to_exclude: list[str] = Field(default_factory=list, description="The operationId(s) to exclude") + operation_selection_predicate: Callable[["OperationSelectionPredicateContext"], bool] | None = None def model_post_init(self, __context: Any) -> None: """Post initialization method for the model.""" diff --git a/python/semantic_kernel/connectors/openapi_plugin/openapi_manager.py b/python/semantic_kernel/connectors/openapi_plugin/openapi_manager.py index 13b30256018f..ef87717a47b3 100644 --- a/python/semantic_kernel/connectors/openapi_plugin/openapi_manager.py +++ b/python/semantic_kernel/connectors/openapi_plugin/openapi_manager.py @@ -6,8 +6,8 @@ from urllib.parse import urlparse from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation import RestApiOperation -from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation_parameter import RestApiOperationParameter -from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation_run_options import RestApiOperationRunOptions +from semantic_kernel.connectors.openapi_plugin.models.rest_api_parameter import RestApiParameter +from semantic_kernel.connectors.openapi_plugin.models.rest_api_run_options import RestApiRunOptions from semantic_kernel.connectors.openapi_plugin.models.rest_api_security_requirement import RestApiSecurityRequirement from semantic_kernel.connectors.openapi_plugin.models.rest_api_uri import Uri from semantic_kernel.connectors.openapi_plugin.openapi_parser import OpenApiParser @@ -46,22 +46,36 @@ class OperationExtensions(Enum): @experimental_function def create_functions_from_openapi( plugin_name: str, - openapi_document_path: str, + openapi_document_path: str | None = None, + openapi_parsed_spec: dict[str, Any] | None = None, execution_settings: "OpenAIFunctionExecutionParameters | OpenAPIFunctionExecutionParameters | None" = None, ) -> list[KernelFunctionFromMethod]: """Creates the functions from OpenAPI document. Args: plugin_name: The name of the plugin - openapi_document_path: The OpenAPI document path, it must be a file path to the spec. + openapi_document_path: The OpenAPI document path, it must be a file path to the spec (optional) + openapi_parsed_spec: The parsed OpenAPI spec (optional) execution_settings: The execution settings Returns: list[KernelFunctionFromMethod]: the operations as functions """ + if openapi_parsed_spec is not None: + parsed_doc = openapi_parsed_spec + else: + if openapi_document_path is None: + raise FunctionExecutionException( + "Either `openapi_document_path` or `openapi_parsed_spec` must be provided." + ) + + # Parse the document from the given path + parser = OpenApiParser() + parsed_doc = parser.parse(openapi_document_path) + if parsed_doc is None: + raise FunctionExecutionException(f"Error parsing OpenAPI document: {openapi_document_path}") + parser = OpenApiParser() - if (parsed_doc := parser.parse(openapi_document_path)) is None: - raise FunctionExecutionException(f"Error parsing OpenAPI document: {openapi_document_path}") operations = parser.create_rest_api_operations(parsed_doc, execution_settings=execution_settings) global_security_requirements = parsed_doc.get("security", []) @@ -78,16 +92,24 @@ def create_functions_from_openapi( enable_payload_namespacing=execution_settings.enable_payload_namespacing if execution_settings else False, ) - return [ - _create_function_from_operation( - openapi_runner, - operation, - plugin_name, - execution_parameters=execution_settings, - security=global_security_requirements, - ) - for operation in operations.values() - ] + functions = [] + for operation in operations.values(): + try: + kernel_function = _create_function_from_operation( + openapi_runner, + operation, + plugin_name, + execution_parameters=execution_settings, + security=global_security_requirements, + ) + functions.append(kernel_function) + operation.freeze() + except Exception as ex: + error_msg = f"Error while registering Rest function {plugin_name}.{operation.id}: {ex}" + logger.error(error_msg) + raise FunctionExecutionException(error_msg) from ex + + return functions @experimental_function @@ -101,7 +123,7 @@ def _create_function_from_operation( ) -> KernelFunctionFromMethod: logger.info(f"Registering OpenAPI operation: {plugin_name}.{operation.id}") - rest_operation_params: list[RestApiOperationParameter] = operation.get_parameters( + rest_operation_params: list[RestApiParameter] = operation.get_parameters( operation=operation, add_payload_params_from_metadata=getattr(execution_parameters, "enable_dynamic_payload", True), enable_payload_spacing=getattr(execution_parameters, "enable_payload_namespacing", False), @@ -136,7 +158,7 @@ async def run_openapi_operation( f"`{parameter.name}` parameter of the `{plugin_name}.{operation.id}` REST function." ) - options = RestApiOperationRunOptions( + options = RestApiRunOptions( server_url_override=( urlparse(execution_parameters.server_url_override) if execution_parameters else None ), diff --git a/python/semantic_kernel/connectors/openapi_plugin/openapi_parser.py b/python/semantic_kernel/connectors/openapi_plugin/openapi_parser.py index ace0fe12021b..583681fc3807 100644 --- a/python/semantic_kernel/connectors/openapi_plugin/openapi_parser.py +++ b/python/semantic_kernel/connectors/openapi_plugin/openapi_parser.py @@ -7,22 +7,21 @@ from prance import ResolvingParser -from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation import RestApiOperation -from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation_expected_response import ( - RestApiOperationExpectedResponse, +from semantic_kernel.connectors.openapi_plugin.models.rest_api_expected_response import ( + RestApiExpectedResponse, ) -from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation_parameter import RestApiOperationParameter -from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation_parameter_location import ( - RestApiOperationParameterLocation, +from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation import RestApiOperation +from semantic_kernel.connectors.openapi_plugin.models.rest_api_parameter import RestApiParameter +from semantic_kernel.connectors.openapi_plugin.models.rest_api_parameter_location import ( + RestApiParameterLocation, ) -from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation_payload import RestApiOperationPayload -from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation_payload_property import ( - RestApiOperationPayloadProperty, +from semantic_kernel.connectors.openapi_plugin.models.rest_api_payload import RestApiPayload +from semantic_kernel.connectors.openapi_plugin.models.rest_api_payload_property import ( + RestApiPayloadProperty, ) from semantic_kernel.connectors.openapi_plugin.models.rest_api_security_requirement import RestApiSecurityRequirement from semantic_kernel.connectors.openapi_plugin.models.rest_api_security_scheme import RestApiSecurityScheme from semantic_kernel.exceptions.function_exceptions import PluginInitializationError -from semantic_kernel.utils.experimental_decorator import experimental_class if TYPE_CHECKING: from semantic_kernel.connectors.openai_plugin.openai_function_execution_parameters import ( @@ -35,7 +34,6 @@ logger: logging.Logger = logging.getLogger(__name__) -@experimental_class class OpenApiParser: """NOTE: SK Python only supports the OpenAPI Spec >=3.0. @@ -62,7 +60,7 @@ def parse(self, openapi_document: str) -> Any | dict[str, Any] | None: def _parse_parameters(self, parameters: list[dict[str, Any]]): """Parse the parameters from the OpenAPI document.""" - result: list[RestApiOperationParameter] = [] + result: list[RestApiParameter] = [] for param in parameters: name: str = param["name"] if not param.get("in"): @@ -70,14 +68,14 @@ def _parse_parameters(self, parameters: list[dict[str, Any]]): if param.get("content", None) is not None: # The schema and content fields are mutually exclusive. raise PluginInitializationError(f"Parameter {name} cannot have a 'content' field. Expected: schema.") - location = RestApiOperationParameterLocation(param["in"]) + location = RestApiParameterLocation(param["in"]) description: str = param.get("description", None) is_required: bool = param.get("required", False) default_value = param.get("default", None) schema: dict[str, Any] | None = param.get("schema", None) result.append( - RestApiOperationParameter( + RestApiParameter( name=name, type=schema.get("type", "string") if schema else "string", location=location, @@ -104,7 +102,7 @@ def _get_payload_properties(self, operation_id, schema, required_properties, lev for property_name, property_schema in schema.get("properties", {}).items(): default_value = property_schema.get("default", None) - property = RestApiOperationPayloadProperty( + property = RestApiPayloadProperty( name=property_name, type=property_schema.get("type", None), is_required=property_name in required_properties, @@ -120,7 +118,7 @@ def _get_payload_properties(self, operation_id, schema, required_properties, lev def _create_rest_api_operation_payload( self, operation_id: str, request_body: dict[str, Any] - ) -> RestApiOperationPayload | None: + ) -> RestApiPayload | None: if request_body is None or request_body.get("content") is None: return None @@ -136,16 +134,14 @@ def _create_rest_api_operation_payload( payload_properties = self._get_payload_properties( operation_id, media_type_metadata["schema"], media_type_metadata["schema"].get("required", set()) ) - return RestApiOperationPayload( + return RestApiPayload( media_type, payload_properties, request_body.get("description"), schema=media_type_metadata.get("schema", None), ) - def _create_response( - self, responses: dict[str, Any] - ) -> Generator[tuple[str, RestApiOperationExpectedResponse], None, None]: + def _create_response(self, responses: dict[str, Any]) -> Generator[tuple[str, RestApiExpectedResponse], None, None]: for response_key, response_value in responses.items(): media_type = next( (mt for mt in OpenApiParser.SUPPORTED_MEDIA_TYPES if mt in response_value.get("content", {})), None @@ -155,7 +151,7 @@ def _create_response( description = response_value.get("description") or matching_schema.get("description", "") yield ( response_key, - RestApiOperationExpectedResponse( + RestApiExpectedResponse( description=description, media_type=media_type, schema=matching_schema if matching_schema else None, @@ -212,6 +208,8 @@ def create_rest_api_operations( Returns: A dictionary of RestApiOperation instances. """ + from semantic_kernel.connectors.openapi_plugin import OperationSelectionPredicateContext + components = parsed_document.get("components", {}) security_schemes = self._parse_security_schemes(components) @@ -229,13 +227,22 @@ def create_rest_api_operations( request_method = method.lower() operationId = details.get("operationId", path + "_" + request_method) + summary = details.get("summary", None) + description = details.get("description", None) + + context = OperationSelectionPredicateContext(operationId, path, method, description) + if ( + execution_settings + and execution_settings.operation_selection_predicate + and not execution_settings.operation_selection_predicate(context) + ): + logger.info(f"Skipping operation {operationId} based on custom predicate.") + continue + if execution_settings and operationId in execution_settings.operations_to_exclude: logger.info(f"Skipping operation {operationId} as it is excluded.") continue - summary = details.get("summary", None) - description = details.get("description", None) - parameters = details.get("parameters", []) parsed_params = self._parse_parameters(parameters) request_body = self._create_rest_api_operation_payload(operationId, details.get("requestBody", None)) diff --git a/python/semantic_kernel/connectors/openapi_plugin/openapi_runner.py b/python/semantic_kernel/connectors/openapi_plugin/openapi_runner.py index 09869445dc05..4998052f0ae2 100644 --- a/python/semantic_kernel/connectors/openapi_plugin/openapi_runner.py +++ b/python/semantic_kernel/connectors/openapi_plugin/openapi_runner.py @@ -11,12 +11,12 @@ import httpx from openapi_core import Spec -from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation import RestApiOperation -from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation_expected_response import ( - RestApiOperationExpectedResponse, +from semantic_kernel.connectors.openapi_plugin.models.rest_api_expected_response import ( + RestApiExpectedResponse, ) -from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation_payload import RestApiOperationPayload -from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation_run_options import RestApiOperationRunOptions +from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation import RestApiOperation +from semantic_kernel.connectors.openapi_plugin.models.rest_api_payload import RestApiPayload +from semantic_kernel.connectors.openapi_plugin.models.rest_api_run_options import RestApiRunOptions from semantic_kernel.exceptions.function_exceptions import FunctionExecutionException from semantic_kernel.functions.kernel_arguments import KernelArguments from semantic_kernel.utils.experimental_decorator import experimental_class @@ -60,9 +60,7 @@ def build_operation_url( url = operation.build_operation_url(arguments, server_url_override, api_host_url) return self.build_full_url(url, operation.build_query_string(arguments)) - def build_json_payload( - self, payload_metadata: RestApiOperationPayload, arguments: dict[str, Any] - ) -> tuple[str, str]: + def build_json_payload(self, payload_metadata: RestApiPayload, arguments: dict[str, Any]) -> tuple[str, str]: """Build the JSON payload.""" if self.enable_dynamic_payload: if payload_metadata is None: @@ -118,9 +116,7 @@ def get_argument_name_for_payload(self, property_name, property_namespace=None): return property_name return f"{property_namespace}.{property_name}" if property_namespace else property_name - def _get_first_response_media_type( - self, responses: OrderedDict[str, RestApiOperationExpectedResponse] | None - ) -> str: + def _get_first_response_media_type(self, responses: OrderedDict[str, RestApiExpectedResponse] | None) -> str: if responses: first_response = next(iter(responses.values())) return first_response.media_type if first_response.media_type else self.media_type_application_json @@ -130,7 +126,7 @@ async def run_operation( self, operation: RestApiOperation, arguments: KernelArguments | None = None, - options: RestApiOperationRunOptions | None = None, + options: RestApiRunOptions | None = None, ) -> str: """Runs the operation defined in the OpenAPI manifest.""" if not arguments: diff --git a/python/semantic_kernel/connectors/openapi_plugin/operation_selection_predicate_context.py b/python/semantic_kernel/connectors/openapi_plugin/operation_selection_predicate_context.py new file mode 100644 index 000000000000..8d8081668eee --- /dev/null +++ b/python/semantic_kernel/connectors/openapi_plugin/operation_selection_predicate_context.py @@ -0,0 +1,12 @@ +# Copyright (c) Microsoft. All rights reserved. + + +class OperationSelectionPredicateContext: + """The context for the operation selection predicate.""" + + def __init__(self, operation_id: str, path: str, method: str, description: str | None = None): + """Initialize the operation selection predicate context.""" + self.operation_id = operation_id + self.path = path + self.method = method + self.description = description diff --git a/python/semantic_kernel/functions/kernel_plugin.py b/python/semantic_kernel/functions/kernel_plugin.py index 14469ac86760..8d3799cc9139 100644 --- a/python/semantic_kernel/functions/kernel_plugin.py +++ b/python/semantic_kernel/functions/kernel_plugin.py @@ -13,6 +13,7 @@ import httpx from pydantic import Field, StringConstraints +from typing_extensions import deprecated from semantic_kernel.connectors.openai_plugin.openai_authentication_config import OpenAIAuthenticationConfig from semantic_kernel.connectors.openai_plugin.openai_function_execution_parameters import ( @@ -357,17 +358,19 @@ def from_directory( def from_openapi( cls: type[_T], plugin_name: str, - openapi_document_path: str, + openapi_document_path: str | None = None, + openapi_parsed_spec: dict[str, Any] | None = None, execution_settings: "OpenAPIFunctionExecutionParameters | None" = None, description: str | None = None, ) -> _T: """Create a plugin from an OpenAPI document. Args: - plugin_name (str): The name of the plugin - openapi_document_path (str): The path to the OpenAPI document - execution_settings (OpenAPIFunctionExecutionParameters | None): The execution parameters - description (str | None): The description of the plugin + plugin_name: The name of the plugin + openapi_document_path: The path to the OpenAPI document (optional) + openapi_parsed_spec: The parsed OpenAPI spec (optional) + execution_settings: The execution parameters + description: The description of the plugin Returns: KernelPlugin: The created plugin @@ -375,8 +378,8 @@ def from_openapi( Raises: PluginInitializationError: if the plugin URL or plugin JSON/YAML is not provided """ - if not openapi_document_path: - raise PluginInitializationError("OpenAPI document path is required.") + if not openapi_document_path and not openapi_parsed_spec: + raise PluginInitializationError("Either the OpenAPI document path or a parsed OpenAPI spec is required.") return cls( # type: ignore name=plugin_name, @@ -384,10 +387,15 @@ def from_openapi( functions=create_functions_from_openapi( # type: ignore plugin_name=plugin_name, openapi_document_path=openapi_document_path, + openapi_parsed_spec=openapi_parsed_spec, execution_settings=execution_settings, ), ) + @deprecated( + "The `OpenAI` plugin is deprecated; use the `from_openapi` method to add an `OpenAPI` plugin instead.", + category=None, + ) @classmethod async def from_openai( cls: type[_T], diff --git a/python/tests/integration/cross_language/test_cross_language.py b/python/tests/integration/cross_language/test_cross_language.py index 456874a95a76..a4e9f3827bec 100644 --- a/python/tests/integration/cross_language/test_cross_language.py +++ b/python/tests/integration/cross_language/test_cross_language.py @@ -12,7 +12,6 @@ from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion from semantic_kernel.connectors.ai.open_ai.settings.open_ai_settings import OpenAISettings -from semantic_kernel.connectors.openapi_plugin import OpenAPIFunctionExecutionParameters from semantic_kernel.functions.kernel_arguments import KernelArguments from semantic_kernel.functions.kernel_function import KernelFunction from semantic_kernel.functions.kernel_function_decorator import kernel_function @@ -539,6 +538,8 @@ async def test_yaml_prompt(is_streaming, prompt_path, expected_result_path, kern async def setup_openapi_function_call(kernel, function_name, arguments): + from semantic_kernel.connectors.openapi_plugin import OpenAPIFunctionExecutionParameters + openapi_spec_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "data", "light_bulb_api.json") request_details = None diff --git a/python/tests/unit/connectors/openapi_plugin/test_openapi_manager.py b/python/tests/unit/connectors/openapi_plugin/test_openapi_manager.py index de5d834c1361..33823aff4006 100644 --- a/python/tests/unit/connectors/openapi_plugin/test_openapi_manager.py +++ b/python/tests/unit/connectors/openapi_plugin/test_openapi_manager.py @@ -4,9 +4,9 @@ import pytest -from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation_parameter import ( - RestApiOperationParameter, - RestApiOperationParameterLocation, +from semantic_kernel.connectors.openapi_plugin.models.rest_api_parameter import ( + RestApiParameter, + RestApiParameterLocation, ) from semantic_kernel.connectors.openapi_plugin.openapi_manager import ( _create_function_from_operation, @@ -26,9 +26,7 @@ async def test_run_openapi_operation_success(kernel: Kernel): operation.summary = "Test Summary" operation.description = "Test Description" operation.get_parameters.return_value = [ - RestApiOperationParameter( - name="param1", type="string", location=RestApiOperationParameterLocation.QUERY, is_required=True - ) + RestApiParameter(name="param1", type="string", location=RestApiParameterLocation.QUERY, is_required=True) ] execution_parameters = MagicMock() @@ -77,9 +75,7 @@ async def test_run_openapi_operation_missing_required_param(kernel: Kernel): operation.summary = "Test Summary" operation.description = "Test Description" operation.get_parameters.return_value = [ - RestApiOperationParameter( - name="param1", type="string", location=RestApiOperationParameterLocation.QUERY, is_required=True - ) + RestApiParameter(name="param1", type="string", location=RestApiParameterLocation.QUERY, is_required=True) ] execution_parameters = MagicMock() @@ -127,9 +123,7 @@ async def test_run_openapi_operation_runner_exception(kernel: Kernel): operation.summary = "Test Summary" operation.description = "Test Description" operation.get_parameters.return_value = [ - RestApiOperationParameter( - name="param1", type="string", location=RestApiOperationParameterLocation.QUERY, is_required=True - ) + RestApiParameter(name="param1", type="string", location=RestApiParameterLocation.QUERY, is_required=True) ] execution_parameters = MagicMock() @@ -177,10 +171,10 @@ async def test_run_openapi_operation_alternative_name(kernel: Kernel): operation.summary = "Test Summary" operation.description = "Test Description" operation.get_parameters.return_value = [ - RestApiOperationParameter( + RestApiParameter( name="param1", type="string", - location=RestApiOperationParameterLocation.QUERY, + location=RestApiParameterLocation.QUERY, is_required=True, alternative_name="alt_param1", ) diff --git a/python/tests/unit/connectors/openapi_plugin/test_openapi_runner.py b/python/tests/unit/connectors/openapi_plugin/test_openapi_runner.py index 43955661d6d2..935ee40df4dc 100644 --- a/python/tests/unit/connectors/openapi_plugin/test_openapi_runner.py +++ b/python/tests/unit/connectors/openapi_plugin/test_openapi_runner.py @@ -6,7 +6,7 @@ import pytest from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation import RestApiOperation -from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation_payload import RestApiOperationPayload +from semantic_kernel.connectors.openapi_plugin.models.rest_api_payload import RestApiPayload from semantic_kernel.connectors.openapi_plugin.openapi_manager import OpenApiRunner from semantic_kernel.exceptions import FunctionExecutionException @@ -31,9 +31,15 @@ def test_build_operation_url(): def test_build_json_payload_dynamic_payload(): runner = OpenApiRunner({}, enable_dynamic_payload=True) - payload_metadata = RestApiOperationPayload( + + mock_property1 = Mock() + mock_property2 = Mock() + mock_property1.freeze = MagicMock() + mock_property2.freeze = MagicMock() + + payload_metadata = RestApiPayload( media_type="application/json", - properties=["property1", "property2"], + properties=[mock_property1, mock_property2], description=None, schema=None, ) @@ -41,6 +47,19 @@ def test_build_json_payload_dynamic_payload(): runner.build_json_object = MagicMock(return_value={"property1": "value1", "property2": "value2"}) + payload_metadata.description = "A dynamic payload" + assert payload_metadata.description == "A dynamic payload" + + payload_metadata.freeze() + + mock_property1.freeze.assert_called_once() + mock_property2.freeze.assert_called_once() + + with pytest.raises( + FunctionExecutionException, match="This `RestApiPayload` instance is frozen and cannot be modified." + ): + payload_metadata.description = "Should raise error" + content, media_type = runner.build_json_payload(payload_metadata, arguments) runner.build_json_object.assert_called_once_with(payload_metadata.properties, arguments) @@ -206,7 +225,7 @@ def test_get_argument_name_for_payload_with_namespacing(): def test_build_operation_payload_with_request_body(): runner = OpenApiRunner({}) - request_body = RestApiOperationPayload( + request_body = RestApiPayload( media_type="application/json", properties=["property1", "property2"], description=None, diff --git a/python/tests/unit/connectors/openapi_plugin/test_rest_api_operation_run_options.py b/python/tests/unit/connectors/openapi_plugin/test_rest_api_operation_run_options.py index 29df73cc7040..bca9e0ca97ec 100644 --- a/python/tests/unit/connectors/openapi_plugin/test_rest_api_operation_run_options.py +++ b/python/tests/unit/connectors/openapi_plugin/test_rest_api_operation_run_options.py @@ -1,20 +1,20 @@ # Copyright (c) Microsoft. All rights reserved. -from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation_run_options import RestApiOperationRunOptions +from semantic_kernel.connectors.openapi_plugin.models.rest_api_run_options import RestApiRunOptions def test_initialization(): server_url_override = "http://example.com" api_host_url = "http://example.com" - rest_api_operation_run_options = RestApiOperationRunOptions(server_url_override, api_host_url) + rest_api_operation_run_options = RestApiRunOptions(server_url_override, api_host_url) assert rest_api_operation_run_options.server_url_override == server_url_override assert rest_api_operation_run_options.api_host_url == api_host_url def test_initialization_no_params(): - rest_api_operation_run_options = RestApiOperationRunOptions() + rest_api_operation_run_options = RestApiRunOptions() assert rest_api_operation_run_options.server_url_override is None assert rest_api_operation_run_options.api_host_url is None diff --git a/python/tests/unit/connectors/openapi_plugin/test_sk_openapi.py b/python/tests/unit/connectors/openapi_plugin/test_sk_openapi.py index 9d759d25347e..4cfd6d03fd4a 100644 --- a/python/tests/unit/connectors/openapi_plugin/test_sk_openapi.py +++ b/python/tests/unit/connectors/openapi_plugin/test_sk_openapi.py @@ -8,19 +8,20 @@ import yaml from openapi_core import Spec -from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation_expected_response import ( - RestApiOperationExpectedResponse, +from semantic_kernel.connectors.openapi_plugin import OperationSelectionPredicateContext +from semantic_kernel.connectors.openapi_plugin.models.rest_api_expected_response import ( + RestApiExpectedResponse, ) -from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation_parameter import ( - RestApiOperationParameter, - RestApiOperationParameterLocation, +from semantic_kernel.connectors.openapi_plugin.models.rest_api_parameter import ( + RestApiParameter, + RestApiParameterLocation, ) -from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation_parameter_style import ( - RestApiOperationParameterStyle, +from semantic_kernel.connectors.openapi_plugin.models.rest_api_parameter_style import ( + RestApiParameterStyle, ) -from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation_payload import RestApiOperationPayload -from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation_payload_property import ( - RestApiOperationPayloadProperty, +from semantic_kernel.connectors.openapi_plugin.models.rest_api_payload import RestApiPayload +from semantic_kernel.connectors.openapi_plugin.models.rest_api_payload_property import ( + RestApiPayloadProperty, ) from semantic_kernel.connectors.openapi_plugin.openapi_function_execution_parameters import ( OpenAPIFunctionExecutionParameters, @@ -160,8 +161,8 @@ def test_url_join_base_path_without_trailing_slash(): def test_build_headers_with_required_parameter(): parameters = [ - RestApiOperationParameter( - name="Authorization", type="string", location=RestApiOperationParameterLocation.HEADER, is_required=True + RestApiParameter( + name="Authorization", type="string", location=RestApiParameterLocation.HEADER, is_required=True ) ] operation = RestApiOperation( @@ -172,10 +173,40 @@ def test_build_headers_with_required_parameter(): assert operation.build_headers(arguments) == expected_headers +def test_rest_api_operation_freeze(): + operation = RestApiOperation( + id="test", + method="GET", + servers=["https://example.com/"], + path="test/path", + summary="A test summary", + description="A test description", + params=[], + request_body=None, + responses={}, + security_requirements=[], + ) + + operation.description = "Modified description" + assert operation.description == "Modified description" + + operation.freeze() + + with pytest.raises(FunctionExecutionException, match="is frozen and cannot be modified"): + operation.description = "Another modification" + + with pytest.raises(FunctionExecutionException, match="is frozen and cannot be modified"): + operation.path = "new/test/path" + + if operation.request_body: + with pytest.raises(FunctionExecutionException): + operation.request_body.description = "New request body description" + + def test_build_headers_missing_required_parameter(): parameters = [ - RestApiOperationParameter( - name="Authorization", type="string", location=RestApiOperationParameterLocation.HEADER, is_required=True + RestApiParameter( + name="Authorization", type="string", location=RestApiParameterLocation.HEADER, is_required=True ) ] operation = RestApiOperation( @@ -191,8 +222,8 @@ def test_build_headers_missing_required_parameter(): def test_build_headers_with_optional_parameter(): parameters = [ - RestApiOperationParameter( - name="Authorization", type="string", location=RestApiOperationParameterLocation.HEADER, is_required=False + RestApiParameter( + name="Authorization", type="string", location=RestApiParameterLocation.HEADER, is_required=False ) ] operation = RestApiOperation( @@ -205,8 +236,8 @@ def test_build_headers_with_optional_parameter(): def test_build_headers_missing_optional_parameter(): parameters = [ - RestApiOperationParameter( - name="Authorization", type="string", location=RestApiOperationParameterLocation.HEADER, is_required=False + RestApiParameter( + name="Authorization", type="string", location=RestApiParameterLocation.HEADER, is_required=False ) ] operation = RestApiOperation( @@ -219,11 +250,11 @@ def test_build_headers_missing_optional_parameter(): def test_build_headers_multiple_parameters(): parameters = [ - RestApiOperationParameter( - name="Authorization", type="string", location=RestApiOperationParameterLocation.HEADER, is_required=True + RestApiParameter( + name="Authorization", type="string", location=RestApiParameterLocation.HEADER, is_required=True ), - RestApiOperationParameter( - name="Content-Type", type="string", location=RestApiOperationParameterLocation.HEADER, is_required=False + RestApiParameter( + name="Content-Type", type="string", location=RestApiParameterLocation.HEADER, is_required=False ), ] operation = RestApiOperation( @@ -235,11 +266,7 @@ def test_build_headers_multiple_parameters(): def test_build_operation_url_with_override(): - parameters = [ - RestApiOperationParameter( - name="id", type="string", location=RestApiOperationParameterLocation.PATH, is_required=True - ) - ] + parameters = [RestApiParameter(name="id", type="string", location=RestApiParameterLocation.PATH, is_required=True)] operation = RestApiOperation( id="test", method="GET", servers=["https://example.com/"], path="/resource/{id}", params=parameters ) @@ -250,11 +277,7 @@ def test_build_operation_url_with_override(): def test_build_operation_url_without_override(): - parameters = [ - RestApiOperationParameter( - name="id", type="string", location=RestApiOperationParameterLocation.PATH, is_required=True - ) - ] + parameters = [RestApiParameter(name="id", type="string", location=RestApiParameterLocation.PATH, is_required=True)] operation = RestApiOperation( id="test", method="GET", servers=["https://example.com/"], path="/resource/{id}", params=parameters ) @@ -277,11 +300,7 @@ def test_get_server_url_without_override(): def test_build_path_with_required_parameter(): - parameters = [ - RestApiOperationParameter( - name="id", type="string", location=RestApiOperationParameterLocation.PATH, is_required=True - ) - ] + parameters = [RestApiParameter(name="id", type="string", location=RestApiParameterLocation.PATH, is_required=True)] operation = RestApiOperation( id="test", method="GET", servers=["https://example.com/"], path="/resource/{id}", params=parameters ) @@ -291,11 +310,7 @@ def test_build_path_with_required_parameter(): def test_build_path_missing_required_parameter(): - parameters = [ - RestApiOperationParameter( - name="id", type="string", location=RestApiOperationParameterLocation.PATH, is_required=True - ) - ] + parameters = [RestApiParameter(name="id", type="string", location=RestApiParameterLocation.PATH, is_required=True)] operation = RestApiOperation( id="test", method="GET", servers=["https://example.com/"], path="/resource/{id}", params=parameters ) @@ -309,12 +324,8 @@ def test_build_path_missing_required_parameter(): def test_build_path_with_optional_and_required_parameters(): parameters = [ - RestApiOperationParameter( - name="id", type="string", location=RestApiOperationParameterLocation.PATH, is_required=True - ), - RestApiOperationParameter( - name="optional", type="string", location=RestApiOperationParameterLocation.PATH, is_required=False - ), + RestApiParameter(name="id", type="string", location=RestApiParameterLocation.PATH, is_required=True), + RestApiParameter(name="optional", type="string", location=RestApiParameterLocation.PATH, is_required=False), ] operation = RestApiOperation( id="test", method="GET", servers=["https://example.com/"], path="/resource/{id}/{optional}", params=parameters @@ -326,9 +337,7 @@ def test_build_path_with_optional_and_required_parameters(): def test_build_query_string_with_required_parameter(): parameters = [ - RestApiOperationParameter( - name="query", type="string", location=RestApiOperationParameterLocation.QUERY, is_required=True - ) + RestApiParameter(name="query", type="string", location=RestApiParameterLocation.QUERY, is_required=True) ] operation = RestApiOperation( id="test", method="GET", servers=["https://example.com/"], path="/resource", params=parameters @@ -340,9 +349,7 @@ def test_build_query_string_with_required_parameter(): def test_build_query_string_missing_required_parameter(): parameters = [ - RestApiOperationParameter( - name="query", type="string", location=RestApiOperationParameterLocation.QUERY, is_required=True - ) + RestApiParameter(name="query", type="string", location=RestApiParameterLocation.QUERY, is_required=True) ] operation = RestApiOperation( id="test", method="GET", servers=["https://example.com/"], path="/resource", params=parameters @@ -357,11 +364,11 @@ def test_build_query_string_missing_required_parameter(): def test_build_query_string_with_optional_and_required_parameters(): parameters = [ - RestApiOperationParameter( - name="required_param", type="string", location=RestApiOperationParameterLocation.QUERY, is_required=True + RestApiParameter( + name="required_param", type="string", location=RestApiParameterLocation.QUERY, is_required=True ), - RestApiOperationParameter( - name="optional_param", type="string", location=RestApiOperationParameterLocation.QUERY, is_required=False + RestApiParameter( + name="optional_param", type="string", location=RestApiParameterLocation.QUERY, is_required=False ), ] operation = RestApiOperation( @@ -374,7 +381,7 @@ def test_build_query_string_with_optional_and_required_parameters(): def test_create_payload_artificial_parameter_with_text_plain(): properties = [ - RestApiOperationPayloadProperty( + RestApiPayloadProperty( name="prop1", type="string", properties=[], @@ -384,7 +391,7 @@ def test_create_payload_artificial_parameter_with_text_plain(): schema=None, ) ] - request_body = RestApiOperationPayload( + request_body = RestApiPayload( media_type=RestApiOperation.MEDIA_TYPE_TEXT_PLAIN, properties=properties, description="Test description", @@ -393,12 +400,12 @@ def test_create_payload_artificial_parameter_with_text_plain(): operation = RestApiOperation( id="test", method="POST", servers=["https://example.com/"], path="/resource", request_body=request_body ) - expected_parameter = RestApiOperationParameter( + expected_parameter = RestApiParameter( name=operation.PAYLOAD_ARGUMENT_NAME, type="string", is_required=True, - location=RestApiOperationParameterLocation.BODY, - style=RestApiOperationParameterStyle.SIMPLE, + location=RestApiParameterLocation.BODY, + style=RestApiParameterStyle.SIMPLE, description="Test description", schema="Test schema", ) @@ -414,7 +421,7 @@ def test_create_payload_artificial_parameter_with_text_plain(): def test_create_payload_artificial_parameter_with_object(): properties = [ - RestApiOperationPayloadProperty( + RestApiPayloadProperty( name="prop1", type="string", properties=[], @@ -424,18 +431,18 @@ def test_create_payload_artificial_parameter_with_object(): schema=None, ) ] - request_body = RestApiOperationPayload( + request_body = RestApiPayload( media_type="application/json", properties=properties, description="Test description", schema="Test schema" ) operation = RestApiOperation( id="test", method="POST", servers=["https://example.com/"], path="/resource", request_body=request_body ) - expected_parameter = RestApiOperationParameter( + expected_parameter = RestApiParameter( name=operation.PAYLOAD_ARGUMENT_NAME, type="object", is_required=True, - location=RestApiOperationParameterLocation.BODY, - style=RestApiOperationParameterStyle.SIMPLE, + location=RestApiParameterLocation.BODY, + style=RestApiParameterStyle.SIMPLE, description="Test description", schema="Test schema", ) @@ -451,12 +458,12 @@ def test_create_payload_artificial_parameter_with_object(): def test_create_payload_artificial_parameter_without_request_body(): operation = RestApiOperation(id="test", method="POST", servers=["https://example.com/"], path="/resource") - expected_parameter = RestApiOperationParameter( + expected_parameter = RestApiParameter( name=operation.PAYLOAD_ARGUMENT_NAME, type="object", is_required=True, - location=RestApiOperationParameterLocation.BODY, - style=RestApiOperationParameterStyle.SIMPLE, + location=RestApiParameterLocation.BODY, + style=RestApiParameterStyle.SIMPLE, description="REST API request body.", schema=None, ) @@ -472,12 +479,12 @@ def test_create_payload_artificial_parameter_without_request_body(): def test_create_content_type_artificial_parameter(): operation = RestApiOperation(id="test", method="POST", servers=["https://example.com/"], path="/resource") - expected_parameter = RestApiOperationParameter( + expected_parameter = RestApiParameter( name=operation.CONTENT_TYPE_ARGUMENT_NAME, type="string", is_required=False, - location=RestApiOperationParameterLocation.BODY, - style=RestApiOperationParameterStyle.SIMPLE, + location=RestApiParameterLocation.BODY, + style=RestApiParameterStyle.SIMPLE, description="Content type of REST API request body.", ) parameter = operation.create_content_type_artificial_parameter() @@ -491,27 +498,23 @@ def test_create_content_type_artificial_parameter(): def test_get_property_name_with_namespacing_and_root_property(): operation = RestApiOperation(id="test", method="POST", servers=["https://example.com/"], path="/resource") - property = RestApiOperationPayloadProperty( - name="child", type="string", properties=[], description="Property description" - ) + property = RestApiPayloadProperty(name="child", type="string", properties=[], description="Property description") result = operation._get_property_name(property, root_property_name="root", enable_namespacing=True) assert result == "root.child" def test_get_property_name_without_namespacing(): operation = RestApiOperation(id="test", method="POST", servers=["https://example.com/"], path="/resource") - property = RestApiOperationPayloadProperty( - name="child", type="string", properties=[], description="Property description" - ) + property = RestApiPayloadProperty(name="child", type="string", properties=[], description="Property description") result = operation._get_property_name(property, root_property_name="root", enable_namespacing=False) assert result == "child" def test_get_payload_parameters_with_metadata_and_text_plain(): properties = [ - RestApiOperationPayloadProperty(name="prop1", type="string", properties=[], description="Property description") + RestApiPayloadProperty(name="prop1", type="string", properties=[], description="Property description") ] - request_body = RestApiOperationPayload( + request_body = RestApiPayload( media_type=RestApiOperation.MEDIA_TYPE_TEXT_PLAIN, properties=properties, description="Test description" ) operation = RestApiOperation( @@ -524,11 +527,9 @@ def test_get_payload_parameters_with_metadata_and_text_plain(): def test_get_payload_parameters_with_metadata_and_json(): properties = [ - RestApiOperationPayloadProperty(name="prop1", type="string", properties=[], description="Property description") + RestApiPayloadProperty(name="prop1", type="string", properties=[], description="Property description") ] - request_body = RestApiOperationPayload( - media_type="application/json", properties=properties, description="Test description" - ) + request_body = RestApiPayload(media_type="application/json", properties=properties, description="Test description") operation = RestApiOperation( id="test", method="POST", servers=["https://example.com/"], path="/resource", request_body=request_body ) @@ -563,10 +564,8 @@ def test_get_payload_parameters_raises_exception(): def test_get_default_response(): operation = RestApiOperation(id="test", method="GET", servers=["https://example.com/"], path="/resource") responses = { - "200": RestApiOperationExpectedResponse( - description="Success", media_type="application/json", schema={"type": "object"} - ), - "default": RestApiOperationExpectedResponse( + "200": RestApiExpectedResponse(description="Success", media_type="application/json", schema={"type": "object"}), + "default": RestApiExpectedResponse( description="Default response", media_type="application/json", schema={"type": "object"} ), } @@ -578,7 +577,7 @@ def test_get_default_response(): def test_get_default_response_with_default(): operation = RestApiOperation(id="test", method="GET", servers=["https://example.com/"], path="/resource") responses = { - "default": RestApiOperationExpectedResponse( + "default": RestApiExpectedResponse( description="Default response", media_type="application/json", schema={"type": "object"} ) } @@ -598,10 +597,8 @@ def test_get_default_response_none(): def test_get_default_return_parameter_with_response(): operation = RestApiOperation(id="test", method="GET", servers=["https://example.com/"], path="/resource") responses = { - "200": RestApiOperationExpectedResponse( - description="Success", media_type="application/json", schema={"type": "object"} - ), - "default": RestApiOperationExpectedResponse( + "200": RestApiExpectedResponse(description="Success", media_type="application/json", schema={"type": "object"}), + "default": RestApiExpectedResponse( description="Default response", media_type="application/json", schema={"type": "object"} ), } @@ -657,6 +654,61 @@ async def dummy_auth_callback(**kwargs): return runner, operations +@pytest.fixture +def openapi_runner_with_predicate_callback(): + # Define a dummy predicate callback + def predicate_callback(context): + # Skip operations with DELETE method or containing 'internal' in the path + return context.method != "DELETE" and "internal" not in context.path + + parser = OpenApiParser() + parsed_doc = parser.parse(openapi_document) + exec_settings = OpenAPIFunctionExecutionParameters( + server_url_override="http://urloverride.com", + operation_selection_predicate=predicate_callback, + ) + operations = parser.create_rest_api_operations(parsed_doc, execution_settings=exec_settings) + runner = OpenApiRunner(parsed_openapi_document=parsed_doc) + return runner, operations, exec_settings + + +def test_predicate_callback_applied(openapi_runner_with_predicate_callback): + _, operations, exec_settings = openapi_runner_with_predicate_callback + + skipped_operations = [] + executed_operations = [] + + # Iterate over the operation objects instead of just the keys + for operation_id, operation in operations.items(): + context = OperationSelectionPredicateContext( + operation_id=operation_id, + path=operation.path, + method=operation.method, + description=operation.description, + ) + if not exec_settings.operation_selection_predicate(context): + skipped_operations.append(operation_id) + else: + executed_operations.append(operation_id) + + # Assertions to verify the callback's behavior + assert len(skipped_operations) > 0, "No operations were skipped, predicate not applied correctly" + assert len(executed_operations) > 0, "All operations were skipped, predicate not applied correctly" + + # Example specific checks based on the callback logic + for operation_id in skipped_operations: + operation = operations[operation_id] + assert operation.method == "DELETE" or "internal" in operation.path, ( + f"Predicate incorrectly skipped operation {operation_id}" + ) + + for operation_id in executed_operations: + operation = operations[operation_id] + assert operation.method != "DELETE" and "internal" not in operation.path, ( + f"Predicate incorrectly executed operation {operation_id}" + ) + + @patch("aiohttp.ClientSession.request") @pytest.mark.asyncio async def test_run_operation_with_invalid_request(mock_request, openapi_runner): diff --git a/python/tests/unit/functions/test_kernel_plugins.py b/python/tests/unit/functions/test_kernel_plugins.py index a30f16f6b2c4..986bbee99aea 100644 --- a/python/tests/unit/functions/test_kernel_plugins.py +++ b/python/tests/unit/functions/test_kernel_plugins.py @@ -13,6 +13,7 @@ from semantic_kernel.connectors.openai_plugin.openai_function_execution_parameters import ( OpenAIFunctionExecutionParameters, ) +from semantic_kernel.connectors.openapi_plugin.openapi_parser import OpenApiParser from semantic_kernel.exceptions.function_exceptions import PluginInitializationError from semantic_kernel.functions import kernel_function from semantic_kernel.functions.kernel_function import KernelFunction @@ -589,7 +590,25 @@ def test_from_openapi(): assert plugin.functions.get("SetSecret") is not None -def test_from_openapi_missing_document_throws(): +def test_custom_spec_from_openapi(): + openapi_spec_file = os.path.join( + os.path.dirname(__file__), "../../assets/test_plugins", "TestOpenAPIPlugin", "akv-openapi.yaml" + ) + + parser = OpenApiParser() + openapi_spec = parser.parse(openapi_spec_file) + + plugin = KernelPlugin.from_openapi( + plugin_name="TestOpenAPIPlugin", + openapi_parsed_spec=openapi_spec, + ) + assert plugin is not None + assert plugin.name == "TestOpenAPIPlugin" + assert plugin.functions.get("GetSecret") is not None + assert plugin.functions.get("SetSecret") is not None + + +def test_from_openapi_missing_document_and_parsed_spec_throws(): with raises(PluginInitializationError): KernelPlugin.from_openapi( plugin_name="TestOpenAPIPlugin", From e5c5c19ee0fe16dd13c2bfadbaeb2d74bd720925 Mon Sep 17 00:00:00 2001 From: Evan Mattson Date: Fri, 15 Nov 2024 12:32:23 -0500 Subject: [PATCH 6/9] Fix mypy errors --- .../connectors/openapi_plugin/models/rest_api_operation.py | 2 +- .../connectors/openapi_plugin/models/rest_api_parameter.py | 4 ++-- .../connectors/openapi_plugin/openapi_manager.py | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation.py b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation.py index 733c6d12101c..60a3f24ca924 100644 --- a/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation.py +++ b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation.py @@ -119,7 +119,7 @@ def servers(self): @servers.setter def servers(self, value: list[str | ParseResult]): self._throw_if_frozen() - self._servers = value + self._servers = [urlparse(s) if isinstance(s, str) else s for s in value] @property def path(self): diff --git a/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_parameter.py b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_parameter.py index 8ae4ad0db471..def469cbeadf 100644 --- a/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_parameter.py +++ b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_parameter.py @@ -80,7 +80,7 @@ def location(self): return self._location @location.setter - def location(self, value: str): + def location(self, value: RestApiParameterLocation): self._throw_if_frozen() self._location = value @@ -90,7 +90,7 @@ def style(self): return self._style @style.setter - def style(self, value: str | None): + def style(self, value: RestApiParameterStyle | None): self._throw_if_frozen() self._style = value diff --git a/python/semantic_kernel/connectors/openapi_plugin/openapi_manager.py b/python/semantic_kernel/connectors/openapi_plugin/openapi_manager.py index ef87717a47b3..ab17fe0657e4 100644 --- a/python/semantic_kernel/connectors/openapi_plugin/openapi_manager.py +++ b/python/semantic_kernel/connectors/openapi_plugin/openapi_manager.py @@ -61,6 +61,7 @@ def create_functions_from_openapi( Returns: list[KernelFunctionFromMethod]: the operations as functions """ + parsed_doc: dict[str, Any] | Any = None if openapi_parsed_spec is not None: parsed_doc = openapi_parsed_spec else: From 65959c739e7b45c7cc29b3c8a52d3764d254ce53 Mon Sep 17 00:00:00 2001 From: Evan Mattson Date: Fri, 15 Nov 2024 12:36:12 -0500 Subject: [PATCH 7/9] Fix error message in dapr sample apps --- python/samples/demos/process_with_dapr/fastapi_app.py | 4 ++-- python/samples/demos/process_with_dapr/flask_app.py | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/python/samples/demos/process_with_dapr/fastapi_app.py b/python/samples/demos/process_with_dapr/fastapi_app.py index d34f96503acb..263356a8bcea 100644 --- a/python/samples/demos/process_with_dapr/fastapi_app.py +++ b/python/samples/demos/process_with_dapr/fastapi_app.py @@ -65,8 +65,8 @@ async def start_process(process_id: str): process_id=process_id, ) return JSONResponse(content={"processId": process_id}, status_code=200) - except Exception as e: - return JSONResponse(content={"error": str(e)}, status_code=500) + except Exception: + return JSONResponse(content={"error": "Error starting process"}, status_code=500) if __name__ == "__main__": diff --git a/python/samples/demos/process_with_dapr/flask_app.py b/python/samples/demos/process_with_dapr/flask_app.py index bd7483e1854f..3e86510f4a2b 100644 --- a/python/samples/demos/process_with_dapr/flask_app.py +++ b/python/samples/demos/process_with_dapr/flask_app.py @@ -55,9 +55,8 @@ def start_process(process_id): ) return jsonify({"processId": process_id}), 200 - except Exception as e: - logging.exception("Error starting process") - return jsonify({"error": str(e)}), 500 + except Exception: + return jsonify({"error": "Error starting process"}), 500 # Run application From 85056109d8b1ff05221e3fe9b2b15dc833b40850 Mon Sep 17 00:00:00 2001 From: Evan Mattson Date: Fri, 15 Nov 2024 16:24:28 -0500 Subject: [PATCH 8/9] Support passing in defined openapi spec to plugin creation method via kernel. --- .../connectors/openapi_plugin/const.py | 18 ++++++++++++++++++ .../openapi_plugin/openapi_manager.py | 16 ++-------------- .../functions/kernel_function_extension.py | 13 ++++++++----- 3 files changed, 28 insertions(+), 19 deletions(-) create mode 100644 python/semantic_kernel/connectors/openapi_plugin/const.py diff --git a/python/semantic_kernel/connectors/openapi_plugin/const.py b/python/semantic_kernel/connectors/openapi_plugin/const.py new file mode 100644 index 000000000000..ac4cebb1aeab --- /dev/null +++ b/python/semantic_kernel/connectors/openapi_plugin/const.py @@ -0,0 +1,18 @@ +# Copyright (c) Microsoft. All rights reserved. + + +from enum import Enum + +from semantic_kernel.utils.experimental_decorator import experimental_class + + +@experimental_class +class OperationExtensions(Enum): + """The operation extensions.""" + + METHOD_KEY = "method" + OPERATION_KEY = "operation" + INFO_KEY = "info" + SECURITY_KEY = "security" + SERVER_URLS_KEY = "server-urls" + METADATA_KEY = "operation-extensions" diff --git a/python/semantic_kernel/connectors/openapi_plugin/openapi_manager.py b/python/semantic_kernel/connectors/openapi_plugin/openapi_manager.py index ab17fe0657e4..1acc49372597 100644 --- a/python/semantic_kernel/connectors/openapi_plugin/openapi_manager.py +++ b/python/semantic_kernel/connectors/openapi_plugin/openapi_manager.py @@ -1,10 +1,10 @@ # Copyright (c) Microsoft. All rights reserved. import logging -from enum import Enum from typing import TYPE_CHECKING, Any from urllib.parse import urlparse +from semantic_kernel.connectors.openapi_plugin.const import OperationExtensions from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation import RestApiOperation from semantic_kernel.connectors.openapi_plugin.models.rest_api_parameter import RestApiParameter from semantic_kernel.connectors.openapi_plugin.models.rest_api_run_options import RestApiRunOptions @@ -18,7 +18,7 @@ from semantic_kernel.functions.kernel_function_from_method import KernelFunctionFromMethod from semantic_kernel.functions.kernel_parameter_metadata import KernelParameterMetadata from semantic_kernel.schema.kernel_json_schema_builder import TYPE_MAPPING -from semantic_kernel.utils.experimental_decorator import experimental_class, experimental_function +from semantic_kernel.utils.experimental_decorator import experimental_function if TYPE_CHECKING: from semantic_kernel.connectors.openai_plugin.openai_function_execution_parameters import ( @@ -31,18 +31,6 @@ logger: logging.Logger = logging.getLogger(__name__) -@experimental_class -class OperationExtensions(Enum): - """The operation extensions.""" - - METHOD_KEY = "method" - OPERATION_KEY = "operation" - INFO_KEY = "info" - SECURITY_KEY = "security" - SERVER_URLS_KEY = "server-urls" - METADATA_KEY = "operation-extensions" - - @experimental_function def create_functions_from_openapi( plugin_name: str, diff --git a/python/semantic_kernel/functions/kernel_function_extension.py b/python/semantic_kernel/functions/kernel_function_extension.py index 9571085c1ee9..97f5cf527fce 100644 --- a/python/semantic_kernel/functions/kernel_function_extension.py +++ b/python/semantic_kernel/functions/kernel_function_extension.py @@ -207,17 +207,19 @@ def add_functions( def add_plugin_from_openapi( self, plugin_name: str, - openapi_document_path: str, + openapi_document_path: str | None = None, + openapi_parsed_spec: dict[str, Any] | None = None, execution_settings: "OpenAPIFunctionExecutionParameters | None" = None, description: str | None = None, ) -> KernelPlugin: """Add a plugin from the OpenAPI manifest. Args: - plugin_name (str): The name of the plugin - openapi_document_path (str): The path to the OpenAPI document - execution_settings (OpenAPIFunctionExecutionParameters | None): The execution parameters - description (str | None): The description of the plugin + plugin_name: The name of the plugin + openapi_document_path: The path to the OpenAPI document + openapi_parsed_spec: The parsed OpenAPI spec + execution_settings: The execution parameters + description: The description of the plugin Returns: KernelPlugin: The imported plugin @@ -229,6 +231,7 @@ def add_plugin_from_openapi( KernelPlugin.from_openapi( plugin_name=plugin_name, openapi_document_path=openapi_document_path, + openapi_parsed_spec=openapi_parsed_spec, execution_settings=execution_settings, description=description, ) From 5400c7f05917b8659fc9aef8ca326fe4e66ad071 Mon Sep 17 00:00:00 2001 From: Evan Mattson Date: Thu, 21 Nov 2024 07:43:47 -0600 Subject: [PATCH 9/9] Improve support for handling multiple OpenAPI servers. --- .../services/azure_ai_inference_tracing.py | 2 +- .../models/rest_api_operation.py | 64 +++++++++---- .../openapi_plugin/openapi_manager.py | 4 +- .../openapi_plugin/openapi_parser.py | 17 +++- .../openapi_plugin/test_sk_openapi.py | 89 +++++++++++++++++-- 5 files changed, 149 insertions(+), 27 deletions(-) diff --git a/python/semantic_kernel/connectors/ai/azure_ai_inference/services/azure_ai_inference_tracing.py b/python/semantic_kernel/connectors/ai/azure_ai_inference/services/azure_ai_inference_tracing.py index 03d0c6c4f53a..e326ed1d9b6b 100644 --- a/python/semantic_kernel/connectors/ai/azure_ai_inference/services/azure_ai_inference_tracing.py +++ b/python/semantic_kernel/connectors/ai/azure_ai_inference/services/azure_ai_inference_tracing.py @@ -35,7 +35,7 @@ def __enter__(self) -> None: self.diagnostics_settings.enable_otel_diagnostics or self.diagnostics_settings.enable_otel_diagnostics_sensitive ): - AIInferenceInstrumentor().instrument( + AIInferenceInstrumentor().instrument( # type: ignore enable_content_recording=self.diagnostics_settings.enable_otel_diagnostics_sensitive ) diff --git a/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation.py b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation.py index 60a3f24ca924..f6150e70a0a7 100644 --- a/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation.py +++ b/python/semantic_kernel/connectors/openapi_plugin/models/rest_api_operation.py @@ -2,7 +2,7 @@ import re from typing import Any, Final -from urllib.parse import ParseResult, urlencode, urljoin, urlparse, urlunparse +from urllib.parse import ParseResult, ParseResultBytes, urlencode, urljoin, urlparse, urlunparse from semantic_kernel.connectors.openapi_plugin.models.rest_api_expected_response import ( RestApiExpectedResponse, @@ -52,7 +52,7 @@ def __init__( self, id: str, method: str, - servers: list[str | ParseResult], + servers: list[dict[str, Any]], path: str, summary: str | None = None, description: str | None = None, @@ -64,7 +64,7 @@ def __init__( """Initialize the RestApiOperation.""" self._id = id self._method = method.upper() - self._servers = [urlparse(s) if isinstance(s, str) else s for s in servers] + self._servers = servers self._path = path self._summary = summary self._description = description @@ -117,9 +117,9 @@ def servers(self): return self._servers @servers.setter - def servers(self, value: list[str | ParseResult]): + def servers(self, value: list[dict[str, Any]]): self._throw_if_frozen() - self._servers = [urlparse(s) if isinstance(s, str) else s for s in value] + self._servers = value @property def path(self): @@ -223,26 +223,58 @@ def build_operation_url(self, arguments, server_url_override=None, api_host_url= """Build the URL for the operation.""" server_url = self.get_server_url(server_url_override, api_host_url) path = self.build_path(self.path, arguments) - return urljoin(server_url.geturl(), path.lstrip("/")) + try: + return urljoin(server_url, path.lstrip("/")) + except Exception as e: + raise FunctionExecutionException(f"Error building the URL for the operation {self.id}: {e!s}") from e - def get_server_url(self, server_url_override=None, api_host_url=None): + def get_server_url(self, server_url_override=None, api_host_url=None, arguments=None): """Get the server URL for the operation.""" - if server_url_override is not None and server_url_override.geturl() != "": - server_url = server_url_override - elif self.servers and self.servers[0].geturl() != "": - server_url = self.servers[0] + if arguments is None: + arguments = {} + + # Prioritize server_url_override + if ( + server_url_override is not None + and isinstance(server_url_override, (ParseResult, ParseResultBytes)) + and server_url_override.geturl() != b"" + ): + server_url_string = server_url_override.geturl() + elif server_url_override is not None and isinstance(server_url_override, str) and server_url_override != "": + server_url_string = server_url_override + elif self.servers and len(self.servers) > 0: + # Use the first server by default + server = self.servers[0] + server_url_string = server["url"] if isinstance(server, dict) else server + server_variables = server.get("variables", {}) if isinstance(server, dict) else {} + + # Substitute server variables if available + for variable_name, variable_def in server_variables.items(): + argument_name = variable_def.get("argument_name", variable_name) + if argument_name in arguments: + value = arguments[argument_name] + server_url_string = server_url_string.replace(f"{{{variable_name}}}", value) + elif "default" in variable_def and variable_def["default"] is not None: + # Use the default value if no argument is provided + value = variable_def["default"] + server_url_string = server_url_string.replace(f"{{{variable_name}}}", value) + else: + # Raise an exception if no value is available + raise FunctionExecutionException( + f"No argument provided for the '{variable_name}' server variable of the operation '{self.id}'." + ) + elif self.server_url: + server_url_string = self.server_url elif api_host_url is not None: - server_url = api_host_url + server_url_string = api_host_url else: raise FunctionExecutionException(f"No valid server URL for operation {self.id}") - server_url_string = server_url.geturl() - - # make sure the base URL ends with a trailing slash + # Ensure the base URL ends with a trailing slash if not server_url_string.endswith("/"): server_url_string += "/" - return urlparse(server_url_string) + return server_url_string # Return the URL string directly def build_path(self, path_template: str, arguments: dict[str, Any]) -> str: """Build the path for the operation.""" diff --git a/python/semantic_kernel/connectors/openapi_plugin/openapi_manager.py b/python/semantic_kernel/connectors/openapi_plugin/openapi_manager.py index 1acc49372597..c5823c7d559f 100644 --- a/python/semantic_kernel/connectors/openapi_plugin/openapi_manager.py +++ b/python/semantic_kernel/connectors/openapi_plugin/openapi_manager.py @@ -183,8 +183,8 @@ async def run_openapi_operation( OperationExtensions.METHOD_KEY.value: operation.method.upper(), OperationExtensions.OPERATION_KEY.value: operation, OperationExtensions.SERVER_URLS_KEY.value: ( - [operation.servers[0].geturl()] - if operation.servers and len(operation.servers) > 0 and operation.servers[0].geturl() + [operation.servers[0]["url"]] + if operation.servers and len(operation.servers) > 0 and operation.servers[0]["url"] else [] ), } diff --git a/python/semantic_kernel/connectors/openapi_plugin/openapi_parser.py b/python/semantic_kernel/connectors/openapi_plugin/openapi_parser.py index 583681fc3807..a08276de387d 100644 --- a/python/semantic_kernel/connectors/openapi_plugin/openapi_parser.py +++ b/python/semantic_kernel/connectors/openapi_plugin/openapi_parser.py @@ -217,10 +217,23 @@ def create_rest_api_operations( request_objects = {} servers = parsed_document.get("servers", []) - server_urls = [server.get("url") for server in servers] if servers else ["/"] if execution_settings and execution_settings.server_url_override: - server_urls = [execution_settings.server_url_override] + # Override the servers with the provided URL + server_urls = [{"url": execution_settings.server_url_override, "variables": {}}] + elif servers: + # Process servers, ensuring we capture their variables + server_urls = [] + for server in servers: + server_entry = { + "url": server.get("url", "/"), + "variables": server.get("variables", {}), + "description": server.get("description", ""), + } + server_urls.append(server_entry) + else: + # Default server if none specified + server_urls = [{"url": "/", "variables": {}, "description": ""}] for path, methods in paths.items(): for method, details in methods.items(): diff --git a/python/tests/unit/connectors/openapi_plugin/test_sk_openapi.py b/python/tests/unit/connectors/openapi_plugin/test_sk_openapi.py index 4cfd6d03fd4a..4dbab11ad34a 100644 --- a/python/tests/unit/connectors/openapi_plugin/test_sk_openapi.py +++ b/python/tests/unit/connectors/openapi_plugin/test_sk_openapi.py @@ -279,24 +279,101 @@ def test_build_operation_url_with_override(): def test_build_operation_url_without_override(): parameters = [RestApiParameter(name="id", type="string", location=RestApiParameterLocation.PATH, is_required=True)] operation = RestApiOperation( - id="test", method="GET", servers=["https://example.com/"], path="/resource/{id}", params=parameters + id="test", + method="GET", + servers=[{"url": "https://example.com/"}], + path="/resource/{id}", + params=parameters, ) arguments = {"id": "123"} expected_url = "https://example.com/resource/123" assert operation.build_operation_url(arguments) == expected_url -def test_get_server_url_with_override(): - operation = RestApiOperation(id="test", method="GET", servers=["https://example.com"], path="/resource/{id}") +def test_get_server_url_with_parse_result_override(): + operation = RestApiOperation( + id="test", + method="GET", + servers=[{"url": "https://example.com"}], + path="/resource/{id}", + ) server_url_override = urlparse("https://override.com") expected_url = "https://override.com/" - assert operation.get_server_url(server_url_override=server_url_override).geturl() == expected_url + assert operation.get_server_url(server_url_override=server_url_override) == expected_url + + +def test_get_server_url_with_string_override(): + operation = RestApiOperation( + id="test", + method="GET", + servers=[{"url": "https://example.com"}], + path="/resource/{id}", + ) + server_url_override = "https://override.com" + expected_url = "https://override.com/" + assert operation.get_server_url(server_url_override=server_url_override) == expected_url + + +def test_get_server_url_with_servers_no_variables(): + operation = RestApiOperation( + id="test", + method="GET", + servers=[{"url": "https://example.com"}], + path="/resource/{id}", + ) + expected_url = "https://example.com/" + assert operation.get_server_url() == expected_url + + +def test_get_server_url_with_servers_and_variables(): + operation = RestApiOperation( + id="test", + method="GET", + servers=[ + { + "url": "https://example.com/{version}", + "variables": {"version": {"default": "v1", "argument_name": "api_version"}}, + } + ], + path="/resource/{id}", + ) + arguments = {"api_version": "v2"} + expected_url = "https://example.com/v2/" + assert operation.get_server_url(arguments=arguments) == expected_url + + +def test_get_server_url_with_servers_and_default_variable(): + operation = RestApiOperation( + id="test", + method="GET", + servers=[{"url": "https://example.com/{version}", "variables": {"version": {"default": "v1"}}}], + path="/resource/{id}", + ) + expected_url = "https://example.com/v1/" + assert operation.get_server_url() == expected_url + + +def test_get_server_url_with_override(): + operation = RestApiOperation( + id="test", + method="GET", + servers=[{"url": "https://example.com"}], + path="/resource/{id}", + ) + server_url_override = "https://override.com" + expected_url = "https://override.com/" + assert operation.get_server_url(server_url_override=server_url_override) == expected_url def test_get_server_url_without_override(): - operation = RestApiOperation(id="test", method="GET", servers=["https://example.com"], path="/resource/{id}") + operation = RestApiOperation( + id="test", + method="GET", + servers=[{"url": "https://example.com"}], + path="/resource/{id}", + ) expected_url = "https://example.com/" - assert operation.get_server_url().geturl() == expected_url + assert operation.get_server_url() == expected_url def test_build_path_with_required_parameter():