chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from semantic_kernel.utils.feature_stage_decorator import experimental
|
||||
|
||||
|
||||
@experimental
|
||||
class RestApiExpectedResponse:
|
||||
"""RestApiExpectedResponse."""
|
||||
|
||||
def __init__(self, description: str, media_type: str, schema: dict[str, str] | None = None):
|
||||
"""Initialize the RestApiExpectedResponse."""
|
||||
self.description = description
|
||||
self.media_type = media_type
|
||||
self.schema = schema
|
||||
@@ -0,0 +1,16 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from semantic_kernel.utils.feature_stage_decorator import experimental
|
||||
|
||||
|
||||
@experimental
|
||||
@dataclass
|
||||
class RestApiOAuthFlow:
|
||||
"""Represents the OAuth flow used by the REST API."""
|
||||
|
||||
authorization_url: str
|
||||
token_url: str
|
||||
scopes: dict[str, str]
|
||||
refresh_url: str | None = None
|
||||
@@ -0,0 +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.feature_stage_decorator import experimental
|
||||
|
||||
|
||||
@experimental
|
||||
@dataclass
|
||||
class RestApiOAuthFlows:
|
||||
"""Represents the OAuth flows used by the REST API."""
|
||||
|
||||
implicit: RestApiOAuthFlow | None = None
|
||||
password: RestApiOAuthFlow | None = None
|
||||
client_credentials: RestApiOAuthFlow | None = None
|
||||
authorization_code: RestApiOAuthFlow | None = None
|
||||
@@ -0,0 +1,552 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import re
|
||||
from typing import Any, Final
|
||||
from urllib.parse import ParseResult, ParseResultBytes, quote, unquote, urlencode, urljoin, urlparse, urlunparse
|
||||
|
||||
from semantic_kernel.connectors.openapi_plugin.models.rest_api_expected_response import (
|
||||
RestApiExpectedResponse,
|
||||
)
|
||||
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_parameter_style import (
|
||||
RestApiParameterStyle,
|
||||
)
|
||||
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
|
||||
from semantic_kernel.functions.kernel_parameter_metadata import KernelParameterMetadata
|
||||
from semantic_kernel.utils.feature_stage_decorator import experimental
|
||||
|
||||
|
||||
@experimental
|
||||
class RestApiOperation:
|
||||
"""RestApiOperation."""
|
||||
|
||||
MEDIA_TYPE_TEXT_PLAIN = "text/plain"
|
||||
PAYLOAD_ARGUMENT_NAME = "payload"
|
||||
CONTENT_TYPE_ARGUMENT_NAME = "content-type"
|
||||
INVALID_SYMBOLS_REGEX = re.compile(r"[^0-9A-Za-z_]+")
|
||||
|
||||
_preferred_responses: Final[list[str]] = [
|
||||
"200",
|
||||
"201",
|
||||
"202",
|
||||
"203",
|
||||
"204",
|
||||
"205",
|
||||
"206",
|
||||
"207",
|
||||
"208",
|
||||
"226",
|
||||
"2XX",
|
||||
"default",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
id: str,
|
||||
method: str,
|
||||
servers: list[dict[str, Any]],
|
||||
path: str,
|
||||
summary: str | None = None,
|
||||
description: str | 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 = 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[dict[str, Any]]):
|
||||
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."""
|
||||
parsed_base = urlparse(base_url)
|
||||
base_path = parsed_base.path + "/" if not parsed_base.path.endswith("/") else parsed_base.path
|
||||
full_path = urljoin(base_path, path.lstrip("/"))
|
||||
return urlunparse(parsed_base._replace(path=full_path))
|
||||
|
||||
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 == RestApiParameterLocation.HEADER]
|
||||
|
||||
for parameter in parameters:
|
||||
argument = arguments.get(parameter.name)
|
||||
|
||||
if argument is None:
|
||||
if parameter.is_required:
|
||||
raise FunctionExecutionException(
|
||||
f"No argument is provided for the `{parameter.name}` "
|
||||
f"required parameter of the operation - `{self.id}`."
|
||||
)
|
||||
continue
|
||||
|
||||
headers[parameter.name] = str(argument)
|
||||
|
||||
return headers
|
||||
|
||||
def build_operation_url(self, arguments, server_url_override=None, api_host_url=None):
|
||||
"""Build the URL for the operation."""
|
||||
server_url = self.get_server_url(server_url_override, api_host_url, arguments)
|
||||
path = self.build_path(self.path, arguments)
|
||||
try:
|
||||
request_url = 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
|
||||
self._ensure_request_target_matches_server(server_url, request_url)
|
||||
return request_url
|
||||
|
||||
@staticmethod
|
||||
def _ensure_request_target_matches_server(server_url: str, request_url: str) -> None:
|
||||
"""Verify URL construction did not move the request off the configured server.
|
||||
|
||||
A selected operation path must resolve to a request on the same scheme, host, and port and
|
||||
within the server's base path. Otherwise an absolute or authority-changing operation path
|
||||
(for example "https://another-host/admin") could redirect a credential-bearing request to an
|
||||
unintended target even though it carries no dot-segment. This complements
|
||||
`_validate_path_segments` so operation selection, path validation, and request construction
|
||||
share one canonical target.
|
||||
"""
|
||||
server = urlparse(server_url)
|
||||
request = urlparse(request_url)
|
||||
|
||||
if (request.scheme, request.username, request.password, request.hostname, request.port) != (
|
||||
server.scheme,
|
||||
server.username,
|
||||
server.password,
|
||||
server.hostname,
|
||||
server.port,
|
||||
):
|
||||
raise FunctionExecutionException(
|
||||
f"The operation path resolves to '{request.scheme}://{request.netloc}', which does not match "
|
||||
f"the configured server '{server.scheme}://{server.netloc}'."
|
||||
)
|
||||
|
||||
# get_server_url guarantees a trailing slash, so the server base path always ends with "/".
|
||||
base_path = server.path
|
||||
if request.path != base_path.rstrip("/") and not request.path.startswith(base_path):
|
||||
raise FunctionExecutionException(
|
||||
f"The operation path resolves to '{request.path}', which is outside the configured server "
|
||||
f"base path '{base_path}'."
|
||||
)
|
||||
|
||||
def get_server_url(self, server_url_override=None, api_host_url=None, arguments=None):
|
||||
"""Get the server URL for the operation."""
|
||||
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)
|
||||
allowed_values = variable_def.get("enum")
|
||||
if argument_name in arguments:
|
||||
value = str(arguments[argument_name])
|
||||
elif "default" in variable_def and variable_def["default"] is not None:
|
||||
# Use the default value if no argument is provided
|
||||
value = str(variable_def["default"])
|
||||
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}'."
|
||||
)
|
||||
if allowed_values is not None and value not in allowed_values:
|
||||
raise FunctionExecutionException(
|
||||
f"Value '{value}' for server variable '{variable_name}' is not one of the allowed values."
|
||||
)
|
||||
server_url_string = server_url_string.replace(f"{{{variable_name}}}", quote(value, safe=""))
|
||||
elif self.server_url:
|
||||
server_url_string = self.server_url
|
||||
elif api_host_url is not None:
|
||||
server_url_string = api_host_url
|
||||
else:
|
||||
raise FunctionExecutionException(f"No valid server URL for operation {self.id}")
|
||||
|
||||
# Ensure the base URL ends with a trailing slash
|
||||
if not server_url_string.endswith("/"):
|
||||
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."""
|
||||
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:
|
||||
if parameter.is_required:
|
||||
raise FunctionExecutionException(
|
||||
f"No argument is provided for the `{parameter.name}` "
|
||||
f"required parameter of the operation - `{self.id}`."
|
||||
)
|
||||
continue
|
||||
path_template = path_template.replace(f"{{{parameter.name}}}", quote(str(argument), safe=""))
|
||||
self._validate_path_segments(path_template)
|
||||
return path_template
|
||||
|
||||
@staticmethod
|
||||
def _validate_path_segments(path: str) -> None:
|
||||
"""Reject dot-segments (. or ..), including percent-encoded forms, that enable path traversal.
|
||||
|
||||
The operation is selected using the raw path but the request URL is built from a canonicalized
|
||||
path, so encoded dot-segments such as "%2e%2e" must be rejected before the URL is constructed.
|
||||
"""
|
||||
if RestApiOperation._contains_dot_segment(path):
|
||||
raise FunctionExecutionException(
|
||||
f"Path '{path}' contains a dot-segment, which could lead to path traversal."
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _contains_dot_segment(path: str) -> bool:
|
||||
"""Return True if the path contains a dot-segment (. or ..), including percent-encoded forms.
|
||||
|
||||
Used both to reject such paths when building a request URL and to exclude them during operation
|
||||
selection so an encoded dot-segment cannot bypass an include/exclude operation-selection filter.
|
||||
"""
|
||||
if not path:
|
||||
return False
|
||||
for segment in path.split("/"):
|
||||
decoded = segment
|
||||
for _ in range(5):
|
||||
unescaped = unquote(decoded)
|
||||
if unescaped == decoded:
|
||||
break
|
||||
decoded = unescaped
|
||||
# A decoded segment may contain encoded separators ("%2f"/"%5c"), so re-split on
|
||||
# both "/" and "\" and reject any resulting dot-segment.
|
||||
for part in decoded.replace("\\", "/").split("/"):
|
||||
if part in (".", ".."):
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _is_non_relative_path(path: str) -> bool:
|
||||
"""Return True if the path is non-relative (absolute or authority-changing).
|
||||
|
||||
A conformant OpenAPI operation path is relative to the server URL. An absolute URI, a
|
||||
protocol-relative reference ("//host"), or a backslash-authority reference changes the request
|
||||
target once combined with the server URL, so such a path is excluded during operation selection
|
||||
to keep selection and request construction on one canonical target.
|
||||
"""
|
||||
if not path:
|
||||
return False
|
||||
if path.startswith(("//", "\\\\", "/\\", "\\/")):
|
||||
return True
|
||||
parsed = urlparse(path)
|
||||
return bool(parsed.scheme or parsed.netloc)
|
||||
|
||||
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 == RestApiParameterLocation.QUERY]
|
||||
for parameter in parameters:
|
||||
argument = arguments.get(parameter.name)
|
||||
if argument is None:
|
||||
if parameter.is_required:
|
||||
raise FunctionExecutionException(
|
||||
f"No argument or value is provided for the `{parameter.name}` "
|
||||
f"required parameter of the operation - `{self.id}`."
|
||||
)
|
||||
continue
|
||||
segments.append((parameter.name, argument))
|
||||
return urlencode(segments)
|
||||
|
||||
def replace_invalid_symbols(self, parameter_name):
|
||||
"""Replace invalid symbols in the parameter name with underscores."""
|
||||
return RestApiOperation.INVALID_SYMBOLS_REGEX.sub("_", parameter_name)
|
||||
|
||||
def get_parameters(
|
||||
self,
|
||||
operation: "RestApiOperation",
|
||||
add_payload_params_from_metadata: bool = True,
|
||||
enable_payload_spacing: bool = False,
|
||||
) -> 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:
|
||||
params.extend(
|
||||
self.get_payload_parameters(
|
||||
operation=operation,
|
||||
use_parameters_from_metadata=add_payload_params_from_metadata,
|
||||
enable_namespacing=enable_payload_spacing,
|
||||
)
|
||||
)
|
||||
|
||||
for parameter in params:
|
||||
parameter.alternative_name = self.replace_invalid_symbols(parameter.name)
|
||||
|
||||
return params
|
||||
|
||||
def create_payload_artificial_parameter(self, operation: "RestApiOperation") -> "RestApiParameter":
|
||||
"""Create an artificial parameter for the REST API request body."""
|
||||
return RestApiParameter(
|
||||
name=self.PAYLOAD_ARGUMENT_NAME,
|
||||
type=(
|
||||
"string"
|
||||
if operation.request_body
|
||||
and operation.request_body.media_type == RestApiOperation.MEDIA_TYPE_TEXT_PLAIN
|
||||
else "object"
|
||||
),
|
||||
is_required=True,
|
||||
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) -> "RestApiParameter":
|
||||
"""Create an artificial parameter for the content type of the REST API request body."""
|
||||
return RestApiParameter(
|
||||
name=self.CONTENT_TYPE_ARGUMENT_NAME,
|
||||
type="string",
|
||||
is_required=False,
|
||||
location=RestApiParameterLocation.BODY,
|
||||
style=RestApiParameterStyle.SIMPLE,
|
||||
description="Content type of REST API request body.",
|
||||
)
|
||||
|
||||
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["RestApiPayloadProperty"],
|
||||
enable_namespacing: bool = False,
|
||||
root_property_name: bool | None = None,
|
||||
) -> 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(
|
||||
RestApiParameter(
|
||||
name=parameter_name,
|
||||
type=property.type,
|
||||
is_required=property.is_required,
|
||||
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, RestApiPayloadProperty):
|
||||
nested_properties = [property.properties]
|
||||
else:
|
||||
nested_properties = property.properties
|
||||
|
||||
parameters.extend(
|
||||
self._get_parameters_from_payload_metadata(nested_properties, enable_namespacing, parameter_name)
|
||||
)
|
||||
return parameters
|
||||
|
||||
def get_payload_parameters(
|
||||
self, operation: "RestApiOperation", use_parameters_from_metadata: bool, enable_namespacing: bool
|
||||
):
|
||||
"""Get the payload parameters for the operation."""
|
||||
if use_parameters_from_metadata:
|
||||
if operation.request_body is None:
|
||||
raise Exception(
|
||||
f"Payload parameters cannot be retrieved from the `{operation.id}` "
|
||||
f"operation payload metadata because it is missing."
|
||||
)
|
||||
if operation.request_body.media_type == RestApiOperation.MEDIA_TYPE_TEXT_PLAIN:
|
||||
return [self.create_payload_artificial_parameter(operation)]
|
||||
|
||||
return self._get_parameters_from_payload_metadata(operation.request_body.properties, enable_namespacing)
|
||||
|
||||
return [
|
||||
self.create_payload_artificial_parameter(operation),
|
||||
self.create_content_type_artificial_parameter(),
|
||||
]
|
||||
|
||||
def get_default_response(
|
||||
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.
|
||||
"""
|
||||
for code in preferred_responses:
|
||||
if code in responses:
|
||||
return responses[code]
|
||||
return None
|
||||
|
||||
def get_default_return_parameter(self, preferred_responses: list[str] | None = None) -> KernelParameterMetadata:
|
||||
"""Get the default return parameter for the operation."""
|
||||
if preferred_responses is None:
|
||||
preferred_responses = self._preferred_responses
|
||||
|
||||
responses = self.responses if self.responses is not None else {}
|
||||
|
||||
rest_operation_response = self.get_default_response(responses, preferred_responses)
|
||||
|
||||
schema_type = None
|
||||
if rest_operation_response is not None and rest_operation_response.schema is not None:
|
||||
schema_type = rest_operation_response.schema.get("type")
|
||||
|
||||
if rest_operation_response:
|
||||
return KernelParameterMetadata(
|
||||
name="return",
|
||||
description=rest_operation_response.description,
|
||||
type_=schema_type,
|
||||
schema_data=rest_operation_response.schema,
|
||||
)
|
||||
|
||||
return KernelParameterMetadata(
|
||||
name="return",
|
||||
description="Default return parameter",
|
||||
type_="string",
|
||||
schema_data={"type": "string"},
|
||||
)
|
||||
@@ -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.feature_stage_decorator import experimental
|
||||
|
||||
|
||||
@experimental
|
||||
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: RestApiParameterLocation):
|
||||
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: RestApiParameterStyle | 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
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from enum import Enum
|
||||
|
||||
from semantic_kernel.utils.feature_stage_decorator import experimental
|
||||
|
||||
|
||||
@experimental
|
||||
class RestApiParameterLocation(Enum):
|
||||
"""The location of the REST API parameter."""
|
||||
|
||||
PATH = "path"
|
||||
QUERY = "query"
|
||||
HEADER = "header"
|
||||
COOKIE = "cookie"
|
||||
BODY = "body"
|
||||
@@ -0,0 +1,12 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from enum import Enum
|
||||
|
||||
from semantic_kernel.utils.feature_stage_decorator import experimental
|
||||
|
||||
|
||||
@experimental
|
||||
class RestApiParameterStyle(Enum):
|
||||
"""RestApiParameterStyle."""
|
||||
|
||||
SIMPLE = "simple"
|
||||
@@ -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.feature_stage_decorator import experimental
|
||||
|
||||
|
||||
@experimental
|
||||
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
|
||||
@@ -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.feature_stage_decorator import experimental
|
||||
|
||||
|
||||
@experimental
|
||||
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
|
||||
@@ -0,0 +1,19 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
|
||||
class RestApiRunOptions:
|
||||
"""The options for running the REST API operation."""
|
||||
|
||||
def __init__(
|
||||
self, server_url_override: str | None = None, api_host_url: str | None = None, timeout: float | None = None
|
||||
) -> None:
|
||||
"""Initialize the REST API operation run options.
|
||||
|
||||
Args:
|
||||
server_url_override: The server URL override, if any.
|
||||
api_host_url: The API host URL, if any.
|
||||
timeout: The timeout for the operation, if any.
|
||||
"""
|
||||
self.server_url_override: str | None = server_url_override
|
||||
self.api_host_url: str | None = api_host_url
|
||||
self.timeout: float | None = timeout
|
||||
+13
@@ -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.feature_stage_decorator import experimental
|
||||
|
||||
|
||||
@experimental
|
||||
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)
|
||||
@@ -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_parameter_location import (
|
||||
RestApiParameterLocation,
|
||||
)
|
||||
from semantic_kernel.utils.feature_stage_decorator import experimental
|
||||
|
||||
|
||||
@experimental
|
||||
class RestApiSecurityScheme:
|
||||
"""Represents the security scheme used by the REST API."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
security_scheme_type: str,
|
||||
name: str,
|
||||
in_: RestApiParameterLocation,
|
||||
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
|
||||
@@ -0,0 +1,19 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from semantic_kernel.utils.feature_stage_decorator import experimental
|
||||
|
||||
|
||||
@experimental
|
||||
class Uri:
|
||||
"""The Uri class that represents the URI."""
|
||||
|
||||
def __init__(self, uri):
|
||||
"""Initialize the Uri."""
|
||||
self.uri = uri
|
||||
|
||||
def get_left_part(self):
|
||||
"""Get the left part of the URI."""
|
||||
parsed_uri = urlparse(self.uri)
|
||||
return f"{parsed_uri.scheme}://{parsed_uri.netloc}"
|
||||
Reference in New Issue
Block a user