diff --git a/.gitignore b/.gitignore index 93e515667..009679906 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,6 @@ htmlcov *.egg-info dist .venv + +# make gen +/workspace/ diff --git a/Makefile b/Makefile index 46793806b..95f1623e6 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .DEFAULT_GOAL:=help -.PHONY: help install format-java check-java build-java generate-protocol-tests generate-codegen-output-py api-snapshot aio-check ci lint-py check-py lint-rust check-rust third-party-licenses test-py test-protocols typecheck-protocols static-analysis test-unit test-functional test-integration benchmark build-py clean +.PHONY: help install format-java check-java build-java generate-protocol-tests generate-codegen-output-py api-snapshot aio-check gen ci lint-py check-py lint-rust check-rust third-party-licenses test-py test-protocols typecheck-protocols static-analysis test-unit test-functional test-integration benchmark build-py clean help: ## Show this help. @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n\nTargets:\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-25s\033[0m %s\n", $$1, $$2 }' $(MAKEFILE_LIST) @@ -106,6 +106,18 @@ aio-check: ## Enforces aio naming and separation in package sources. uv run python scripts/aio-naming/aio_naming.py +# `make gen dynamodb`: the second goal is the client name, so swallow it as a no-op target. +ifeq (gen,$(firstword $(MAKECMDGOALS))) +CLIENT ?= $(word 2,$(MAKECMDGOALS)) +ifneq (,$(word 2,$(MAKECMDGOALS))) +$(eval $(word 2,$(MAKECMDGOALS)):;@:) +endif +endif + +gen: ## Generates an AWS SDK client from a sibling aws-sdk-python model into ./workspace/clients (make gen dynamodb). + uv run python scripts/gen-client/gen_client.py "$(CLIENT)" + + lint-py: ## Runs linters and formatters on the python packages. uv run ruff check packages --fix --config pyproject.toml uv run ruff format packages --config pyproject.toml diff --git a/codegen-output/aws-json-1-0/pyproject.toml b/codegen-output/aws-json-1-0/pyproject.toml index 6be5d91e9..40ab4f579 100644 --- a/codegen-output/aws-json-1-0/pyproject.toml +++ b/codegen-output/aws-json-1-0/pyproject.toml @@ -26,7 +26,7 @@ classifiers = [ dependencies = [ "smithy_aws_core[json]~=0.12.0", "smithy_core~=0.9.0", - "smithy_http[aiohttp]~=0.6.0" + "smithy_http[aiohttp, urllib3]~=0.6.0" ] [project.optional-dependencies] diff --git a/codegen-output/aws-json-1-0/src/awsjson10/__init__.py b/codegen-output/aws-json-1-0/src/awsjson10/__init__.py index 30f6d44e9..00abe99f2 100644 --- a/codegen-output/aws-json-1-0/src/awsjson10/__init__.py +++ b/codegen-output/aws-json-1-0/src/awsjson10/__init__.py @@ -1,3 +1,20 @@ # Code generated by smithy-python-codegen DO NOT EDIT. +from typing import Any, TYPE_CHECKING + + +if TYPE_CHECKING: + from .sync import JSONRPC10Client, JSONRPC10Config + +__all__ = ["JSONRPC10Client", "JSONRPC10Config"] + + +def __getattr__(name: str) -> Any: + if name in __all__: + from . import sync + + return getattr(sync, name) + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + __version__: str = "0.0.1" diff --git a/codegen-output/aws-json-1-0/src/awsjson10/sync/__init__.py b/codegen-output/aws-json-1-0/src/awsjson10/sync/__init__.py new file mode 100644 index 000000000..2c8a03587 --- /dev/null +++ b/codegen-output/aws-json-1-0/src/awsjson10/sync/__init__.py @@ -0,0 +1,6 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from .client import JSONRPC10Client +from .config import JSONRPC10Config, Plugin + +__all__ = ["JSONRPC10Client", "JSONRPC10Config", "Plugin"] diff --git a/codegen-output/aws-json-1-0/src/awsjson10/sync/client.py b/codegen-output/aws-json-1-0/src/awsjson10/sync/client.py new file mode 100644 index 000000000..c16f22215 --- /dev/null +++ b/codegen-output/aws-json-1-0/src/awsjson10/sync/client.py @@ -0,0 +1,614 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from __future__ import annotations + +from copy import deepcopy +import logging +from typing import Any, cast + +from smithy_aws_core.config import ConfigSource +from smithy_aws_core.identity import AWSCredentialsIdentity +from smithy_aws_core.identity.chain import IdentityChain +from smithy_core.client import Client, ClientCall, RequestPipeline +from smithy_core.deserializers import DeserializeableShape +from smithy_core.exceptions import ExpectationNotMetError +from smithy_core.schemas import APIOperation +from smithy_core.serializers import SerializeableShape +from smithy_http.interfaces import HTTPClient +from smithy_http.plugins import user_agent_plugin + +from ..models import ( + CONTENT_TYPE_PARAMETERS, + EMPTY_INPUT_AND_EMPTY_OUTPUT, + ENDPOINT_OPERATION, + ENDPOINT_WITH_HOST_LABEL_OPERATION, + GREETING_WITH_ERRORS, + HOST_WITH_PATH_OPERATION, + JSON_UNIONS, + NO_INPUT_AND_NO_OUTPUT, + NO_INPUT_AND_OUTPUT, + OPERATION_WITH_DEFAULTS, + OPERATION_WITH_NESTED_STRUCTURE, + OPERATION_WITH_REQUIRED_MEMBERS, + OPERATION_WITH_REQUIRED_MEMBERS_WITH_DEFAULTS, + PUT_WITH_CONTENT_ENCODING, + QUERY_INCOMPATIBLE_OPERATION, + SIMPLE_SCALAR_PROPERTIES, +) +from .config import JSONRPC10Config, Plugin +from .user_agent import aws_user_agent_plugin + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from ..models import ( + ContentTypeParametersInput, + ContentTypeParametersOutput, + EmptyInputAndEmptyOutputInput, + EmptyInputAndEmptyOutputOutput, + EndpointOperationInput, + EndpointOperationOutput, + EndpointWithHostLabelOperationInput, + EndpointWithHostLabelOperationOutput, + GreetingWithErrorsInput, + GreetingWithErrorsOutput, + HostWithPathOperationInput, + HostWithPathOperationOutput, + JsonUnionsInput, + JsonUnionsOutput, + NoInputAndNoOutputInput, + NoInputAndNoOutputOutput, + NoInputAndOutputInput, + NoInputAndOutputOutput, + OperationWithDefaultsInput, + OperationWithDefaultsOutput, + OperationWithNestedStructureInput, + OperationWithNestedStructureOutput, + OperationWithRequiredMembersInput, + OperationWithRequiredMembersOutput, + OperationWithRequiredMembersWithDefaultsInput, + OperationWithRequiredMembersWithDefaultsOutput, + PutWithContentEncodingInput, + PutWithContentEncodingOutput, + QueryIncompatibleOperationInput, + QueryIncompatibleOperationOutput, + SimpleScalarPropertiesInput, + SimpleScalarPropertiesOutput, + ) + + +logger = logging.getLogger(__name__) + + +class JSONRPC10Client(Client): + """Client for JsonRpc10""" + + def __init__( + self, config: JSONRPC10Config | None = None, plugins: list[Plugin] | None = None + ): + """ + Constructor for `JSONRPC10Client`. + + Args: + config: + Optional configuration for the client. Here you can set things like + the endpoint for HTTP services or auth credentials. + plugins: + A list of callables applied once to the client's base configuration. + Their changes are inherited by every operation invocation. + """ + super().__init__() + self._config = config + self._plugins = plugins + self._client_plugins: list[Plugin] = [aws_user_agent_plugin, user_agent_plugin] + + def _ensure_setup(self) -> JSONRPC10Config: + if not self._setup_done: + with self._derive_lock: + if self._closed: + raise RuntimeError( + "Cannot invoke an operation on a client that has been closed." + ) + if not self._setup_done: + if self._config is None: + config = JSONRPC10Config.resolve() + else: + # Copy so plugins don't mutate the caller's config. + config = deepcopy(self._config) + for plugin in self._client_plugins: + plugin(config) + if self._plugins: + for plugin in self._plugins: + plugin(config) + # Publish state only after setup fully succeeds, so a + # failed _post_setup leaves the caller's config untouched + # (no re-applied plugins) and the transport closeable. + self._post_setup(config) + self._transport = config.transport + self._config = config + self._setup_done = True + assert self._config is not None + return self._config + + def _post_setup(self, config: JSONRPC10Config) -> None: + if config.aws_credentials_identity_resolver is None: + config_context = config.resolution_context() + config_file = None + profile_name = None + if config_context is not None: + config_file = config_context.parsed_profiles() + if config_context.profile_source is ConfigSource.OVERRIDE: + profile_name = config_context.profile_name + config.aws_credentials_identity_resolver = IdentityChain.create( + AWSCredentialsIdentity, + config_file=config_file, + profile_name=profile_name, + region_override=config.region, + http_client=cast(HTTPClient | None, config.transport), + ) + + def _prepare_call[I: SerializeableShape, O: DeserializeableShape]( + self, + input: I, + operation: APIOperation[I, O], + default_plugins: list[Plugin], + plugins: list[Plugin] | None, + ) -> tuple[RequestPipeline[Any, Any], ClientCall[I, O]]: + if self._closed: + raise RuntimeError( + "Cannot invoke an operation on a client that has been closed." + ) + config = self._ensure_setup() + if default_plugins or plugins: + # Keep operation-plugin mutations scoped to this call. + config = deepcopy(config) + for plugin in default_plugins: + plugin(config) + if plugins: + for plugin in plugins: + plugin(config) + if ( + config.protocol is None + or config.transport is None + or config.endpoint_resolver is None + or config.auth_scheme_resolver is None + or config.auth_schemes is None + ): + raise ExpectationNotMetError( + "protocol, transport, endpoint_resolver, auth_scheme_resolver," + " and auth_schemes MUST be set on the config to make calls." + ) + retry_strategy = self._retry_strategy_resolver.resolve_retry_strategy( + retry_strategy=config.retry_strategy, + retry_mode=config.retry_mode, + max_attempts=config.max_attempts, + ) + return self._build_call( + input, operation, config=config, retry_strategy=retry_strategy + ) + + def content_type_parameters( + self, + input: ContentTypeParametersInput | None = None, + plugins: list[Plugin] | None = None, + ) -> ContentTypeParametersOutput: + """ + The example tests how servers must support requests containing a + `Content-Type` header with parameters. + + Args: + input: + An instance of `ContentTypeParametersInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `ContentTypeParametersOutput`. + """ + if input is None: + input = CONTENT_TYPE_PARAMETERS.input() + pipeline, call = self._prepare_call(input, CONTENT_TYPE_PARAMETERS, [], plugins) + return pipeline(call) + + def empty_input_and_empty_output( + self, + input: EmptyInputAndEmptyOutputInput | None = None, + plugins: list[Plugin] | None = None, + ) -> EmptyInputAndEmptyOutputOutput: + """ + The example tests how requests and responses are serialized when + there's no request or response payload because the operation has an + empty input and empty output structure that reuses the same shape. While + this should be rare, code generators must support this. + + Args: + input: + An instance of `EmptyInputAndEmptyOutputInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `EmptyInputAndEmptyOutputOutput`. + """ + if input is None: + input = EMPTY_INPUT_AND_EMPTY_OUTPUT.input() + pipeline, call = self._prepare_call( + input, EMPTY_INPUT_AND_EMPTY_OUTPUT, [], plugins + ) + return pipeline(call) + + def endpoint_operation( + self, + input: EndpointOperationInput | None = None, + plugins: list[Plugin] | None = None, + ) -> EndpointOperationOutput: + """ + Invokes the EndpointOperation operation. + + Args: + input: + An instance of `EndpointOperationInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `EndpointOperationOutput`. + """ + if input is None: + input = ENDPOINT_OPERATION.input() + pipeline, call = self._prepare_call(input, ENDPOINT_OPERATION, [], plugins) + return pipeline(call) + + def endpoint_with_host_label_operation( + self, + input: EndpointWithHostLabelOperationInput, + plugins: list[Plugin] | None = None, + ) -> EndpointWithHostLabelOperationOutput: + """ + Invokes the EndpointWithHostLabelOperation operation. + + Args: + input: + An instance of `EndpointWithHostLabelOperationInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `EndpointWithHostLabelOperationOutput`. + """ + pipeline, call = self._prepare_call( + input, ENDPOINT_WITH_HOST_LABEL_OPERATION, [], plugins + ) + return pipeline(call) + + def greeting_with_errors( + self, + input: GreetingWithErrorsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> GreetingWithErrorsOutput: + """ + This operation has three possible return values: 1. A successful + response in the form of GreetingWithErrorsOutput 2. An InvalidGreeting + error. 3. A ComplexError error. Implementations must be able to + successfully take a response and properly deserialize successful and + error responses. + + Args: + input: + An instance of `GreetingWithErrorsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `GreetingWithErrorsOutput`. + """ + if input is None: + input = GREETING_WITH_ERRORS.input() + pipeline, call = self._prepare_call(input, GREETING_WITH_ERRORS, [], plugins) + return pipeline(call) + + def host_with_path_operation( + self, + input: HostWithPathOperationInput | None = None, + plugins: list[Plugin] | None = None, + ) -> HostWithPathOperationOutput: + """ + Invokes the HostWithPathOperation operation. + + Args: + input: + An instance of `HostWithPathOperationInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `HostWithPathOperationOutput`. + """ + if input is None: + input = HOST_WITH_PATH_OPERATION.input() + pipeline, call = self._prepare_call( + input, HOST_WITH_PATH_OPERATION, [], plugins + ) + return pipeline(call) + + def json_unions( + self, input: JsonUnionsInput | None = None, plugins: list[Plugin] | None = None + ) -> JsonUnionsOutput: + """ + This operation uses unions for inputs and outputs. + + Args: + input: + An instance of `JsonUnionsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `JsonUnionsOutput`. + """ + if input is None: + input = JSON_UNIONS.input() + pipeline, call = self._prepare_call(input, JSON_UNIONS, [], plugins) + return pipeline(call) + + def no_input_and_no_output( + self, + input: NoInputAndNoOutputInput | None = None, + plugins: list[Plugin] | None = None, + ) -> NoInputAndNoOutputOutput: + """ + The example tests how requests and responses are serialized when + there's no request or response payload because the operation has no + input or output. While this should be rare, code generators must support + this. + + Args: + input: + An instance of `NoInputAndNoOutputInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `NoInputAndNoOutputOutput`. + """ + if input is None: + input = NO_INPUT_AND_NO_OUTPUT.input() + pipeline, call = self._prepare_call(input, NO_INPUT_AND_NO_OUTPUT, [], plugins) + return pipeline(call) + + def no_input_and_output( + self, + input: NoInputAndOutputInput | None = None, + plugins: list[Plugin] | None = None, + ) -> NoInputAndOutputOutput: + """ + The example tests how requests and responses are serialized when + there's no request or response payload because the operation has no + input and the output is empty. While this should be rare, code + generators must support this. + + Args: + input: + An instance of `NoInputAndOutputInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `NoInputAndOutputOutput`. + """ + if input is None: + input = NO_INPUT_AND_OUTPUT.input() + pipeline, call = self._prepare_call(input, NO_INPUT_AND_OUTPUT, [], plugins) + return pipeline(call) + + def operation_with_defaults( + self, + input: OperationWithDefaultsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> OperationWithDefaultsOutput: + """ + Invokes the OperationWithDefaults operation. + + Args: + input: + An instance of `OperationWithDefaultsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `OperationWithDefaultsOutput`. + """ + if input is None: + input = OPERATION_WITH_DEFAULTS.input() + pipeline, call = self._prepare_call(input, OPERATION_WITH_DEFAULTS, [], plugins) + return pipeline(call) + + def operation_with_nested_structure( + self, + input: OperationWithNestedStructureInput, + plugins: list[Plugin] | None = None, + ) -> OperationWithNestedStructureOutput: + """ + Invokes the OperationWithNestedStructure operation. + + Args: + input: + An instance of `OperationWithNestedStructureInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `OperationWithNestedStructureOutput`. + """ + pipeline, call = self._prepare_call( + input, OPERATION_WITH_NESTED_STRUCTURE, [], plugins + ) + return pipeline(call) + + def operation_with_required_members( + self, + input: OperationWithRequiredMembersInput | None = None, + plugins: list[Plugin] | None = None, + ) -> OperationWithRequiredMembersOutput: + """ + Invokes the OperationWithRequiredMembers operation. + + Args: + input: + An instance of `OperationWithRequiredMembersInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `OperationWithRequiredMembersOutput`. + """ + if input is None: + input = OPERATION_WITH_REQUIRED_MEMBERS.input() + pipeline, call = self._prepare_call( + input, OPERATION_WITH_REQUIRED_MEMBERS, [], plugins + ) + return pipeline(call) + + def operation_with_required_members_with_defaults( + self, + input: OperationWithRequiredMembersWithDefaultsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> OperationWithRequiredMembersWithDefaultsOutput: + """ + Invokes the OperationWithRequiredMembersWithDefaults operation. + + Args: + input: + An instance of `OperationWithRequiredMembersWithDefaultsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `OperationWithRequiredMembersWithDefaultsOutput`. + """ + if input is None: + input = OPERATION_WITH_REQUIRED_MEMBERS_WITH_DEFAULTS.input() + pipeline, call = self._prepare_call( + input, OPERATION_WITH_REQUIRED_MEMBERS_WITH_DEFAULTS, [], plugins + ) + return pipeline(call) + + def put_with_content_encoding( + self, + input: PutWithContentEncodingInput | None = None, + plugins: list[Plugin] | None = None, + ) -> PutWithContentEncodingOutput: + """ + Invokes the PutWithContentEncoding operation. + + Args: + input: + An instance of `PutWithContentEncodingInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `PutWithContentEncodingOutput`. + """ + if input is None: + input = PUT_WITH_CONTENT_ENCODING.input() + pipeline, call = self._prepare_call( + input, PUT_WITH_CONTENT_ENCODING, [], plugins + ) + return pipeline(call) + + def query_incompatible_operation( + self, + input: QueryIncompatibleOperationInput | None = None, + plugins: list[Plugin] | None = None, + ) -> QueryIncompatibleOperationOutput: + """ + Invokes the QueryIncompatibleOperation operation. + + Args: + input: + An instance of `QueryIncompatibleOperationInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `QueryIncompatibleOperationOutput`. + """ + if input is None: + input = QUERY_INCOMPATIBLE_OPERATION.input() + pipeline, call = self._prepare_call( + input, QUERY_INCOMPATIBLE_OPERATION, [], plugins + ) + return pipeline(call) + + def simple_scalar_properties( + self, + input: SimpleScalarPropertiesInput | None = None, + plugins: list[Plugin] | None = None, + ) -> SimpleScalarPropertiesOutput: + """ + Invokes the SimpleScalarProperties operation. + + Args: + input: + An instance of `SimpleScalarPropertiesInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `SimpleScalarPropertiesOutput`. + """ + if input is None: + input = SIMPLE_SCALAR_PROPERTIES.input() + pipeline, call = self._prepare_call( + input, SIMPLE_SCALAR_PROPERTIES, [], plugins + ) + return pipeline(call) diff --git a/codegen-output/aws-json-1-0/src/awsjson10/sync/config.py b/codegen-output/aws-json-1-0/src/awsjson10/sync/config.py new file mode 100644 index 000000000..2dfd4108c --- /dev/null +++ b/codegen-output/aws-json-1-0/src/awsjson10/sync/config.py @@ -0,0 +1,270 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from dataclasses import dataclass, field +from typing import ( + Any, + Callable, + ClassVar, + Self, + TYPE_CHECKING, + TypeAlias, + Union, + Unpack, +) + +from smithy_aws_core.auth.sigv4 import SigV4AuthScheme +from smithy_aws_core.config import FileSystem +from smithy_aws_core.config.aws_config import AwsConfig, AwsConfigOverrides +from smithy_aws_core.config.resolvers import EndpointUriResolver +from smithy_aws_core.config.types import FieldSpec +from smithy_aws_core.endpoints.standard_regional import ( + StandardRegionalEndpointsResolver, +) +from smithy_aws_core.identity import AWSCredentialsIdentity, AWSIdentityProperties +from smithy_aws_core.protocols import AwsJson10ClientProtocol +from smithy_core.interfaces import ( + ClientProtocol, + EndpointResolver, + ProtocolConstructor, + ProtocolSettings, +) +from smithy_core.interfaces.auth import AuthScheme, AuthSchemeResolver +from smithy_core.interfaces.identity import IdentityResolver +from smithy_core.shapes import ShapeID +from smithy_http.urllib3 import Urllib3HTTPClient + +from .._private.schemas import JSON_RPC10 +from ..auth import HTTPAuthSchemeResolver + + +if TYPE_CHECKING: + from smithy_core.interceptors import Interceptor + from ..models import ( + ContentTypeParametersInput, + ContentTypeParametersOutput, + EmptyInputAndEmptyOutputInput, + EmptyInputAndEmptyOutputOutput, + EndpointOperationInput, + EndpointOperationOutput, + EndpointWithHostLabelOperationInput, + EndpointWithHostLabelOperationOutput, + GreetingWithErrorsInput, + GreetingWithErrorsOutput, + HostWithPathOperationInput, + HostWithPathOperationOutput, + JsonUnionsInput, + JsonUnionsOutput, + NoInputAndNoOutputInput, + NoInputAndNoOutputOutput, + NoInputAndOutputInput, + NoInputAndOutputOutput, + OperationWithDefaultsInput, + OperationWithDefaultsOutput, + OperationWithNestedStructureInput, + OperationWithNestedStructureOutput, + OperationWithRequiredMembersInput, + OperationWithRequiredMembersOutput, + OperationWithRequiredMembersWithDefaultsInput, + OperationWithRequiredMembersWithDefaultsOutput, + PutWithContentEncodingInput, + PutWithContentEncodingOutput, + QueryIncompatibleOperationInput, + QueryIncompatibleOperationOutput, + SimpleScalarPropertiesInput, + SimpleScalarPropertiesOutput, + ) + + _ServiceInterceptor = Union[ + Interceptor[ContentTypeParametersInput, ContentTypeParametersOutput, Any, Any], + Interceptor[ + EmptyInputAndEmptyOutputInput, EmptyInputAndEmptyOutputOutput, Any, Any + ], + Interceptor[EndpointOperationInput, EndpointOperationOutput, Any, Any], + Interceptor[ + EndpointWithHostLabelOperationInput, + EndpointWithHostLabelOperationOutput, + Any, + Any, + ], + Interceptor[GreetingWithErrorsInput, GreetingWithErrorsOutput, Any, Any], + Interceptor[HostWithPathOperationInput, HostWithPathOperationOutput, Any, Any], + Interceptor[JsonUnionsInput, JsonUnionsOutput, Any, Any], + Interceptor[NoInputAndNoOutputInput, NoInputAndNoOutputOutput, Any, Any], + Interceptor[NoInputAndOutputInput, NoInputAndOutputOutput, Any, Any], + Interceptor[OperationWithDefaultsInput, OperationWithDefaultsOutput, Any, Any], + Interceptor[ + OperationWithNestedStructureInput, + OperationWithNestedStructureOutput, + Any, + Any, + ], + Interceptor[ + OperationWithRequiredMembersInput, + OperationWithRequiredMembersOutput, + Any, + Any, + ], + Interceptor[ + OperationWithRequiredMembersWithDefaultsInput, + OperationWithRequiredMembersWithDefaultsOutput, + Any, + Any, + ], + Interceptor[ + PutWithContentEncodingInput, PutWithContentEncodingOutput, Any, Any + ], + Interceptor[ + QueryIncompatibleOperationInput, QueryIncompatibleOperationOutput, Any, Any + ], + Interceptor[ + SimpleScalarPropertiesInput, SimpleScalarPropertiesOutput, Any, Any + ], + ] +else: + _ServiceInterceptor = Any + +_PROTOCOL_SETTINGS = ProtocolSettings( + namespace="aws.protocoltests.json10", + service_target="JsonRpc10", + service_schema=JSON_RPC10, +) + + +class _JSONRPC10ConfigOverrides(AwsConfigOverrides, total=False): + endpoint_resolver: EndpointResolver | None + protocol: ( + ClientProtocol[Any, Any] | ProtocolConstructor[ClientProtocol[Any, Any]] | None + ) + auth_schemes: dict[ShapeID, AuthScheme[Any, Any, Any, Any]] | None + auth_scheme_resolver: AuthSchemeResolver | None + + +@dataclass(kw_only=True, repr=False, init=False) +class JSONRPC10Config(AwsConfig): + """JSON RPC 10 configuration.""" + + endpoint_resolver: EndpointResolver | None = None + """ + The endpoint resolver used to resolve the final endpoint per-operation + based on the configuration. + """ + + protocol: ClientProtocol[Any, Any] | None = None + """ + Pass a protocol class reference from smithy_aws_core.protocols to select + the protocol, e.g. protocol=AwsJson10ClientProtocol. For custom + protocols a protocol instance may also be passed. + """ + + interceptors: list[_ServiceInterceptor] = field(default_factory=lambda: []) + """ + The list of interceptors, which are hooks that are called during the + execution of a request. + """ + + auth_schemes: dict[ShapeID, AuthScheme[Any, Any, Any, Any]] | None = None + """A map of auth scheme ids to auth schemes.""" + + auth_scheme_resolver: AuthSchemeResolver | None = None + """ + An auth scheme resolver that determines the auth scheme for each + operation. + """ + + aws_credentials_identity_resolver: ( + IdentityResolver[AWSCredentialsIdentity, AWSIdentityProperties] | None + ) = None + """Resolves AWS Credentials. Required for operations that use Sigv4 Auth.""" + + region: str | None = None + """ + The AWS region to connect to. The configured region is used to determine + the service endpoint. + """ + + aws_access_key_id: str | None = None + """The identifier for a secret access key.""" + + aws_secret_access_key: str | None = None + """A secret access key that can be used to sign requests.""" + + aws_session_token: str | None = None + """The session token used with temporary AWS credentials.""" + + user_agent_extra: str | None = None + """Additional suffix to be added to the User-Agent header.""" + + sdk_ua_app_id: str | None = None + """ + A unique and opaque application ID that is appended to the User-Agent + header. + """ + + _FIELDS: ClassVar[dict[str, FieldSpec]] = { + "aws_credentials_identity_resolver": FieldSpec(default=None), + "region": FieldSpec(default=None), + "aws_access_key_id": FieldSpec(default=None), + "aws_secret_access_key": FieldSpec(default=None), + "aws_session_token": FieldSpec(default=None), + "user_agent_extra": FieldSpec(default=None), + "sdk_ua_app_id": FieldSpec(default=None), + **AwsConfig._FIELDS, + "endpoint_uri": FieldSpec( + default=None, resolver=EndpointUriResolver("json_rpc_10") + ), + "endpoint_resolver": FieldSpec( + default_factory=lambda: StandardRegionalEndpointsResolver( + endpoint_prefix="jsonrpc10" + ) + ), + "protocol": FieldSpec( + default_factory=lambda: AwsJson10ClientProtocol(_PROTOCOL_SETTINGS), + converter=lambda p: p(_PROTOCOL_SETTINGS) if isinstance(p, type) else p, + ), + "auth_schemes": FieldSpec( + default_factory=lambda: { + ShapeID("aws.auth#sigv4"): SigV4AuthScheme(service="jsonrpc10") + } + ), + "auth_scheme_resolver": FieldSpec(default_factory=HTTPAuthSchemeResolver), + "transport": FieldSpec(default_factory=lambda: Urllib3HTTPClient()), + } + + def set_auth_scheme(self, scheme: AuthScheme[Any, Any, Any, Any]) -> None: + """ + Set an auth scheme implementation using its scheme ID. :param scheme: + The auth scheme to add or replace. + """ + auth_schemes = dict(self.auth_schemes or {}) + auth_schemes[scheme.scheme_id] = scheme + self.auth_schemes = auth_schemes + + @classmethod + def resolve( # pyright: ignore[reportIncompatibleMethodOverride] + cls, + *, + profile: str | None = None, + fs: FileSystem | None = None, + config_file_path: str | None = None, + credentials_file_path: str | None = None, + **overrides: Unpack[_JSONRPC10ConfigOverrides], + ) -> Self: + """ + Resolve config from environment, config files, defaults, and explicit + overrides. + """ + return cls._resolve( + profile=profile, + fs=fs, + config_file_path=config_file_path, + credentials_file_path=credentials_file_path, + overrides=overrides, + ) + + +Plugin: TypeAlias = Callable[[JSONRPC10Config], None] +""" +A callable that customizes a client configuration. Service-level plugins +are applied once to the base configuration inherited by every operation. +Operation-level plugins apply only to a single operation invocation. +""" diff --git a/codegen-output/aws-json-1-0/src/awsjson10/sync/user_agent.py b/codegen-output/aws-json-1-0/src/awsjson10/sync/user_agent.py new file mode 100644 index 000000000..65dbe43c4 --- /dev/null +++ b/codegen-output/aws-json-1-0/src/awsjson10/sync/user_agent.py @@ -0,0 +1,17 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from smithy_aws_core.interceptors.user_agent import UserAgentInterceptor + +from .. import __version__ +from .config import JSONRPC10Config + + +def aws_user_agent_plugin(config: JSONRPC10Config): + config.interceptors.append( + UserAgentInterceptor( + ua_suffix=config.user_agent_extra, + ua_app_id=config.sdk_ua_app_id, + sdk_version=__version__, + service_id="JSON_RPC_10", + ) + ) diff --git a/codegen-output/aws-json-1-1/pyproject.toml b/codegen-output/aws-json-1-1/pyproject.toml index 60313efa0..b309ec1c5 100644 --- a/codegen-output/aws-json-1-1/pyproject.toml +++ b/codegen-output/aws-json-1-1/pyproject.toml @@ -26,7 +26,7 @@ classifiers = [ dependencies = [ "smithy_aws_core[json]~=0.12.0", "smithy_core~=0.9.0", - "smithy_http[aiohttp]~=0.6.0" + "smithy_http[aiohttp, urllib3]~=0.6.0" ] [project.optional-dependencies] diff --git a/codegen-output/aws-json-1-1/src/awsjson11/__init__.py b/codegen-output/aws-json-1-1/src/awsjson11/__init__.py index 30f6d44e9..ff169d977 100644 --- a/codegen-output/aws-json-1-1/src/awsjson11/__init__.py +++ b/codegen-output/aws-json-1-1/src/awsjson11/__init__.py @@ -1,3 +1,20 @@ # Code generated by smithy-python-codegen DO NOT EDIT. +from typing import Any, TYPE_CHECKING + + +if TYPE_CHECKING: + from .sync import JsonProtocolClient, JsonProtocolConfig + +__all__ = ["JsonProtocolClient", "JsonProtocolConfig"] + + +def __getattr__(name: str) -> Any: + if name in __all__: + from . import sync + + return getattr(sync, name) + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + __version__: str = "0.0.1" diff --git a/codegen-output/aws-json-1-1/src/awsjson11/sync/__init__.py b/codegen-output/aws-json-1-1/src/awsjson11/sync/__init__.py new file mode 100644 index 000000000..57d68c287 --- /dev/null +++ b/codegen-output/aws-json-1-1/src/awsjson11/sync/__init__.py @@ -0,0 +1,6 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from .client import JsonProtocolClient +from .config import JsonProtocolConfig, Plugin + +__all__ = ["JsonProtocolClient", "JsonProtocolConfig", "Plugin"] diff --git a/codegen-output/aws-json-1-1/src/awsjson11/sync/client.py b/codegen-output/aws-json-1-1/src/awsjson11/sync/client.py new file mode 100644 index 000000000..53d9f0306 --- /dev/null +++ b/codegen-output/aws-json-1-1/src/awsjson11/sync/client.py @@ -0,0 +1,659 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from __future__ import annotations + +from copy import deepcopy +import logging +from typing import Any, cast + +from smithy_aws_core.config import ConfigSource +from smithy_aws_core.identity import AWSCredentialsIdentity +from smithy_aws_core.identity.chain import IdentityChain +from smithy_core.client import Client, ClientCall, RequestPipeline +from smithy_core.deserializers import DeserializeableShape +from smithy_core.exceptions import ExpectationNotMetError +from smithy_core.schemas import APIOperation +from smithy_core.serializers import SerializeableShape +from smithy_http.interfaces import HTTPClient +from smithy_http.plugins import user_agent_plugin + +from ..models import ( + CONTENT_TYPE_PARAMETERS, + DATETIME_OFFSETS, + EMPTY_OPERATION, + ENDPOINT_OPERATION, + ENDPOINT_WITH_HOST_LABEL_OPERATION, + FRACTIONAL_SECONDS, + GREETING_WITH_ERRORS, + HOST_WITH_PATH_OPERATION, + JSON_ENUMS, + JSON_INT_ENUMS, + JSON_UNIONS, + KITCHEN_SINK_OPERATION, + NULL_OPERATION, + OPERATION_WITH_OPTIONAL_INPUT_OUTPUT, + PUT_AND_GET_INLINE_DOCUMENTS, + PUT_WITH_CONTENT_ENCODING, + SIMPLE_SCALAR_PROPERTIES, + SPARSE_NULLS_OPERATION, +) +from .config import JsonProtocolConfig, Plugin +from .user_agent import aws_user_agent_plugin + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from ..models import ( + ContentTypeParametersInput, + ContentTypeParametersOutput, + DatetimeOffsetsInput, + DatetimeOffsetsOutput, + EmptyOperationInput, + EmptyOperationOutput, + EndpointOperationInput, + EndpointOperationOutput, + EndpointWithHostLabelOperationInput, + EndpointWithHostLabelOperationOutput, + FractionalSecondsInput, + FractionalSecondsOutput, + GreetingWithErrorsInput, + GreetingWithErrorsOutput, + HostWithPathOperationInput, + HostWithPathOperationOutput, + JsonEnumsInput, + JsonEnumsOutput, + JsonIntEnumsInput, + JsonIntEnumsOutput, + JsonUnionsInput, + JsonUnionsOutput, + KitchenSinkOperationInput, + KitchenSinkOperationOutput, + NullOperationInput, + NullOperationOutput, + OperationWithOptionalInputOutputInput, + OperationWithOptionalInputOutputOutput, + PutAndGetInlineDocumentsInput, + PutAndGetInlineDocumentsOutput, + PutWithContentEncodingInput, + PutWithContentEncodingOutput, + SimpleScalarPropertiesInput, + SimpleScalarPropertiesOutput, + SparseNullsOperationInput, + SparseNullsOperationOutput, + ) + + +logger = logging.getLogger(__name__) + + +class JsonProtocolClient(Client): + """Client for JsonProtocol""" + + def __init__( + self, + config: JsonProtocolConfig | None = None, + plugins: list[Plugin] | None = None, + ): + """ + Constructor for `JsonProtocolClient`. + + Args: + config: + Optional configuration for the client. Here you can set things like + the endpoint for HTTP services or auth credentials. + plugins: + A list of callables applied once to the client's base configuration. + Their changes are inherited by every operation invocation. + """ + super().__init__() + self._config = config + self._plugins = plugins + self._client_plugins: list[Plugin] = [aws_user_agent_plugin, user_agent_plugin] + + def _ensure_setup(self) -> JsonProtocolConfig: + if not self._setup_done: + with self._derive_lock: + if self._closed: + raise RuntimeError( + "Cannot invoke an operation on a client that has been closed." + ) + if not self._setup_done: + if self._config is None: + config = JsonProtocolConfig.resolve() + else: + # Copy so plugins don't mutate the caller's config. + config = deepcopy(self._config) + for plugin in self._client_plugins: + plugin(config) + if self._plugins: + for plugin in self._plugins: + plugin(config) + # Publish state only after setup fully succeeds, so a + # failed _post_setup leaves the caller's config untouched + # (no re-applied plugins) and the transport closeable. + self._post_setup(config) + self._transport = config.transport + self._config = config + self._setup_done = True + assert self._config is not None + return self._config + + def _post_setup(self, config: JsonProtocolConfig) -> None: + if config.aws_credentials_identity_resolver is None: + config_context = config.resolution_context() + config_file = None + profile_name = None + if config_context is not None: + config_file = config_context.parsed_profiles() + if config_context.profile_source is ConfigSource.OVERRIDE: + profile_name = config_context.profile_name + config.aws_credentials_identity_resolver = IdentityChain.create( + AWSCredentialsIdentity, + config_file=config_file, + profile_name=profile_name, + region_override=config.region, + http_client=cast(HTTPClient | None, config.transport), + ) + + def _prepare_call[I: SerializeableShape, O: DeserializeableShape]( + self, + input: I, + operation: APIOperation[I, O], + default_plugins: list[Plugin], + plugins: list[Plugin] | None, + ) -> tuple[RequestPipeline[Any, Any], ClientCall[I, O]]: + if self._closed: + raise RuntimeError( + "Cannot invoke an operation on a client that has been closed." + ) + config = self._ensure_setup() + if default_plugins or plugins: + # Keep operation-plugin mutations scoped to this call. + config = deepcopy(config) + for plugin in default_plugins: + plugin(config) + if plugins: + for plugin in plugins: + plugin(config) + if ( + config.protocol is None + or config.transport is None + or config.endpoint_resolver is None + or config.auth_scheme_resolver is None + or config.auth_schemes is None + ): + raise ExpectationNotMetError( + "protocol, transport, endpoint_resolver, auth_scheme_resolver," + " and auth_schemes MUST be set on the config to make calls." + ) + retry_strategy = self._retry_strategy_resolver.resolve_retry_strategy( + retry_strategy=config.retry_strategy, + retry_mode=config.retry_mode, + max_attempts=config.max_attempts, + ) + return self._build_call( + input, operation, config=config, retry_strategy=retry_strategy + ) + + def content_type_parameters( + self, + input: ContentTypeParametersInput | None = None, + plugins: list[Plugin] | None = None, + ) -> ContentTypeParametersOutput: + """ + The example tests how servers must support requests containing a + `Content-Type` header with parameters. + + Args: + input: + An instance of `ContentTypeParametersInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `ContentTypeParametersOutput`. + """ + if input is None: + input = CONTENT_TYPE_PARAMETERS.input() + pipeline, call = self._prepare_call(input, CONTENT_TYPE_PARAMETERS, [], plugins) + return pipeline(call) + + def datetime_offsets( + self, + input: DatetimeOffsetsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> DatetimeOffsetsOutput: + """ + Invokes the DatetimeOffsets operation. + + Args: + input: + An instance of `DatetimeOffsetsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `DatetimeOffsetsOutput`. + """ + if input is None: + input = DATETIME_OFFSETS.input() + pipeline, call = self._prepare_call(input, DATETIME_OFFSETS, [], plugins) + return pipeline(call) + + def empty_operation( + self, + input: EmptyOperationInput | None = None, + plugins: list[Plugin] | None = None, + ) -> EmptyOperationOutput: + """ + Invokes the EmptyOperation operation. + + Args: + input: + An instance of `EmptyOperationInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `EmptyOperationOutput`. + """ + if input is None: + input = EMPTY_OPERATION.input() + pipeline, call = self._prepare_call(input, EMPTY_OPERATION, [], plugins) + return pipeline(call) + + def endpoint_operation( + self, + input: EndpointOperationInput | None = None, + plugins: list[Plugin] | None = None, + ) -> EndpointOperationOutput: + """ + Invokes the EndpointOperation operation. + + Args: + input: + An instance of `EndpointOperationInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `EndpointOperationOutput`. + """ + if input is None: + input = ENDPOINT_OPERATION.input() + pipeline, call = self._prepare_call(input, ENDPOINT_OPERATION, [], plugins) + return pipeline(call) + + def endpoint_with_host_label_operation( + self, + input: EndpointWithHostLabelOperationInput, + plugins: list[Plugin] | None = None, + ) -> EndpointWithHostLabelOperationOutput: + """ + Invokes the EndpointWithHostLabelOperation operation. + + Args: + input: + An instance of `EndpointWithHostLabelOperationInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `EndpointWithHostLabelOperationOutput`. + """ + pipeline, call = self._prepare_call( + input, ENDPOINT_WITH_HOST_LABEL_OPERATION, [], plugins + ) + return pipeline(call) + + def fractional_seconds( + self, + input: FractionalSecondsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> FractionalSecondsOutput: + """ + Invokes the FractionalSeconds operation. + + Args: + input: + An instance of `FractionalSecondsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `FractionalSecondsOutput`. + """ + if input is None: + input = FRACTIONAL_SECONDS.input() + pipeline, call = self._prepare_call(input, FRACTIONAL_SECONDS, [], plugins) + return pipeline(call) + + def greeting_with_errors( + self, + input: GreetingWithErrorsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> GreetingWithErrorsOutput: + """ + This operation has three possible return values: 1. A successful + response in the form of GreetingWithErrorsOutput 2. An InvalidGreeting + error. 3. A ComplexError error. Implementations must be able to + successfully take a response and properly deserialize successful and + error responses. + + Args: + input: + An instance of `GreetingWithErrorsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `GreetingWithErrorsOutput`. + """ + if input is None: + input = GREETING_WITH_ERRORS.input() + pipeline, call = self._prepare_call(input, GREETING_WITH_ERRORS, [], plugins) + return pipeline(call) + + def host_with_path_operation( + self, + input: HostWithPathOperationInput | None = None, + plugins: list[Plugin] | None = None, + ) -> HostWithPathOperationOutput: + """ + Invokes the HostWithPathOperation operation. + + Args: + input: + An instance of `HostWithPathOperationInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `HostWithPathOperationOutput`. + """ + if input is None: + input = HOST_WITH_PATH_OPERATION.input() + pipeline, call = self._prepare_call( + input, HOST_WITH_PATH_OPERATION, [], plugins + ) + return pipeline(call) + + def json_enums( + self, input: JsonEnumsInput | None = None, plugins: list[Plugin] | None = None + ) -> JsonEnumsOutput: + """ + This example serializes enums as top level properties, in lists, sets, + and maps. + + Args: + input: + An instance of `JsonEnumsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `JsonEnumsOutput`. + """ + if input is None: + input = JSON_ENUMS.input() + pipeline, call = self._prepare_call(input, JSON_ENUMS, [], plugins) + return pipeline(call) + + def json_int_enums( + self, + input: JsonIntEnumsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> JsonIntEnumsOutput: + """ + This example serializes intEnums as top level properties, in lists, + sets, and maps. + + Args: + input: + An instance of `JsonIntEnumsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `JsonIntEnumsOutput`. + """ + if input is None: + input = JSON_INT_ENUMS.input() + pipeline, call = self._prepare_call(input, JSON_INT_ENUMS, [], plugins) + return pipeline(call) + + def json_unions( + self, input: JsonUnionsInput | None = None, plugins: list[Plugin] | None = None + ) -> JsonUnionsOutput: + """ + This operation uses unions for inputs and outputs. + + Args: + input: + An instance of `JsonUnionsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `JsonUnionsOutput`. + """ + if input is None: + input = JSON_UNIONS.input() + pipeline, call = self._prepare_call(input, JSON_UNIONS, [], plugins) + return pipeline(call) + + def kitchen_sink_operation( + self, + input: KitchenSinkOperationInput | None = None, + plugins: list[Plugin] | None = None, + ) -> KitchenSinkOperationOutput: + """ + Invokes the KitchenSinkOperation operation. + + Args: + input: + An instance of `KitchenSinkOperationInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `KitchenSinkOperationOutput`. + """ + if input is None: + input = KITCHEN_SINK_OPERATION.input() + pipeline, call = self._prepare_call(input, KITCHEN_SINK_OPERATION, [], plugins) + return pipeline(call) + + def null_operation( + self, + input: NullOperationInput | None = None, + plugins: list[Plugin] | None = None, + ) -> NullOperationOutput: + """ + Invokes the NullOperation operation. + + Args: + input: + An instance of `NullOperationInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `NullOperationOutput`. + """ + if input is None: + input = NULL_OPERATION.input() + pipeline, call = self._prepare_call(input, NULL_OPERATION, [], plugins) + return pipeline(call) + + def operation_with_optional_input_output( + self, + input: OperationWithOptionalInputOutputInput | None = None, + plugins: list[Plugin] | None = None, + ) -> OperationWithOptionalInputOutputOutput: + """ + Invokes the OperationWithOptionalInputOutput operation. + + Args: + input: + An instance of `OperationWithOptionalInputOutputInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `OperationWithOptionalInputOutputOutput`. + """ + if input is None: + input = OPERATION_WITH_OPTIONAL_INPUT_OUTPUT.input() + pipeline, call = self._prepare_call( + input, OPERATION_WITH_OPTIONAL_INPUT_OUTPUT, [], plugins + ) + return pipeline(call) + + def put_and_get_inline_documents( + self, + input: PutAndGetInlineDocumentsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> PutAndGetInlineDocumentsOutput: + """ + This example serializes an inline document as part of the payload. + + Args: + input: + An instance of `PutAndGetInlineDocumentsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `PutAndGetInlineDocumentsOutput`. + """ + if input is None: + input = PUT_AND_GET_INLINE_DOCUMENTS.input() + pipeline, call = self._prepare_call( + input, PUT_AND_GET_INLINE_DOCUMENTS, [], plugins + ) + return pipeline(call) + + def put_with_content_encoding( + self, + input: PutWithContentEncodingInput | None = None, + plugins: list[Plugin] | None = None, + ) -> PutWithContentEncodingOutput: + """ + Invokes the PutWithContentEncoding operation. + + Args: + input: + An instance of `PutWithContentEncodingInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `PutWithContentEncodingOutput`. + """ + if input is None: + input = PUT_WITH_CONTENT_ENCODING.input() + pipeline, call = self._prepare_call( + input, PUT_WITH_CONTENT_ENCODING, [], plugins + ) + return pipeline(call) + + def simple_scalar_properties( + self, + input: SimpleScalarPropertiesInput | None = None, + plugins: list[Plugin] | None = None, + ) -> SimpleScalarPropertiesOutput: + """ + Invokes the SimpleScalarProperties operation. + + Args: + input: + An instance of `SimpleScalarPropertiesInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `SimpleScalarPropertiesOutput`. + """ + if input is None: + input = SIMPLE_SCALAR_PROPERTIES.input() + pipeline, call = self._prepare_call( + input, SIMPLE_SCALAR_PROPERTIES, [], plugins + ) + return pipeline(call) + + def sparse_nulls_operation( + self, + input: SparseNullsOperationInput | None = None, + plugins: list[Plugin] | None = None, + ) -> SparseNullsOperationOutput: + """ + Invokes the SparseNullsOperation operation. + + Args: + input: + An instance of `SparseNullsOperationInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `SparseNullsOperationOutput`. + """ + if input is None: + input = SPARSE_NULLS_OPERATION.input() + pipeline, call = self._prepare_call(input, SPARSE_NULLS_OPERATION, [], plugins) + return pipeline(call) diff --git a/codegen-output/aws-json-1-1/src/awsjson11/sync/config.py b/codegen-output/aws-json-1-1/src/awsjson11/sync/config.py new file mode 100644 index 000000000..f4f445c8b --- /dev/null +++ b/codegen-output/aws-json-1-1/src/awsjson11/sync/config.py @@ -0,0 +1,264 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from dataclasses import dataclass, field +from typing import ( + Any, + Callable, + ClassVar, + Self, + TYPE_CHECKING, + TypeAlias, + Union, + Unpack, +) + +from smithy_aws_core.auth.sigv4 import SigV4AuthScheme +from smithy_aws_core.config import FileSystem +from smithy_aws_core.config.aws_config import AwsConfig, AwsConfigOverrides +from smithy_aws_core.config.resolvers import EndpointUriResolver +from smithy_aws_core.config.types import FieldSpec +from smithy_aws_core.endpoints.standard_regional import ( + StandardRegionalEndpointsResolver, +) +from smithy_aws_core.identity import AWSCredentialsIdentity, AWSIdentityProperties +from smithy_aws_core.protocols import AwsJson11ClientProtocol +from smithy_core.interfaces import ( + ClientProtocol, + EndpointResolver, + ProtocolConstructor, + ProtocolSettings, +) +from smithy_core.interfaces.auth import AuthScheme, AuthSchemeResolver +from smithy_core.interfaces.identity import IdentityResolver +from smithy_core.shapes import ShapeID +from smithy_http.urllib3 import Urllib3HTTPClient + +from .._private.schemas import JSON_PROTOCOL +from ..auth import HTTPAuthSchemeResolver + + +if TYPE_CHECKING: + from smithy_core.interceptors import Interceptor + from ..models import ( + ContentTypeParametersInput, + ContentTypeParametersOutput, + DatetimeOffsetsInput, + DatetimeOffsetsOutput, + EmptyOperationInput, + EmptyOperationOutput, + EndpointOperationInput, + EndpointOperationOutput, + EndpointWithHostLabelOperationInput, + EndpointWithHostLabelOperationOutput, + FractionalSecondsInput, + FractionalSecondsOutput, + GreetingWithErrorsInput, + GreetingWithErrorsOutput, + HostWithPathOperationInput, + HostWithPathOperationOutput, + JsonEnumsInput, + JsonEnumsOutput, + JsonIntEnumsInput, + JsonIntEnumsOutput, + JsonUnionsInput, + JsonUnionsOutput, + KitchenSinkOperationInput, + KitchenSinkOperationOutput, + NullOperationInput, + NullOperationOutput, + OperationWithOptionalInputOutputInput, + OperationWithOptionalInputOutputOutput, + PutAndGetInlineDocumentsInput, + PutAndGetInlineDocumentsOutput, + PutWithContentEncodingInput, + PutWithContentEncodingOutput, + SimpleScalarPropertiesInput, + SimpleScalarPropertiesOutput, + SparseNullsOperationInput, + SparseNullsOperationOutput, + ) + + _ServiceInterceptor = Union[ + Interceptor[ContentTypeParametersInput, ContentTypeParametersOutput, Any, Any], + Interceptor[DatetimeOffsetsInput, DatetimeOffsetsOutput, Any, Any], + Interceptor[EmptyOperationInput, EmptyOperationOutput, Any, Any], + Interceptor[EndpointOperationInput, EndpointOperationOutput, Any, Any], + Interceptor[ + EndpointWithHostLabelOperationInput, + EndpointWithHostLabelOperationOutput, + Any, + Any, + ], + Interceptor[FractionalSecondsInput, FractionalSecondsOutput, Any, Any], + Interceptor[GreetingWithErrorsInput, GreetingWithErrorsOutput, Any, Any], + Interceptor[HostWithPathOperationInput, HostWithPathOperationOutput, Any, Any], + Interceptor[JsonEnumsInput, JsonEnumsOutput, Any, Any], + Interceptor[JsonIntEnumsInput, JsonIntEnumsOutput, Any, Any], + Interceptor[JsonUnionsInput, JsonUnionsOutput, Any, Any], + Interceptor[KitchenSinkOperationInput, KitchenSinkOperationOutput, Any, Any], + Interceptor[NullOperationInput, NullOperationOutput, Any, Any], + Interceptor[ + OperationWithOptionalInputOutputInput, + OperationWithOptionalInputOutputOutput, + Any, + Any, + ], + Interceptor[ + PutAndGetInlineDocumentsInput, PutAndGetInlineDocumentsOutput, Any, Any + ], + Interceptor[ + PutWithContentEncodingInput, PutWithContentEncodingOutput, Any, Any + ], + Interceptor[ + SimpleScalarPropertiesInput, SimpleScalarPropertiesOutput, Any, Any + ], + Interceptor[SparseNullsOperationInput, SparseNullsOperationOutput, Any, Any], + ] +else: + _ServiceInterceptor = Any + +_PROTOCOL_SETTINGS = ProtocolSettings( + namespace="aws.protocoltests.json", + service_target="JsonProtocol", + service_schema=JSON_PROTOCOL, +) + + +class _JsonProtocolConfigOverrides(AwsConfigOverrides, total=False): + endpoint_resolver: EndpointResolver | None + protocol: ( + ClientProtocol[Any, Any] | ProtocolConstructor[ClientProtocol[Any, Any]] | None + ) + auth_schemes: dict[ShapeID, AuthScheme[Any, Any, Any, Any]] | None + auth_scheme_resolver: AuthSchemeResolver | None + + +@dataclass(kw_only=True, repr=False, init=False) +class JsonProtocolConfig(AwsConfig): + """Json Protocol configuration.""" + + endpoint_resolver: EndpointResolver | None = None + """ + The endpoint resolver used to resolve the final endpoint per-operation + based on the configuration. + """ + + protocol: ClientProtocol[Any, Any] | None = None + """ + Pass a protocol class reference from smithy_aws_core.protocols to select + the protocol, e.g. protocol=AwsJson10ClientProtocol. For custom + protocols a protocol instance may also be passed. + """ + + interceptors: list[_ServiceInterceptor] = field(default_factory=lambda: []) + """ + The list of interceptors, which are hooks that are called during the + execution of a request. + """ + + auth_schemes: dict[ShapeID, AuthScheme[Any, Any, Any, Any]] | None = None + """A map of auth scheme ids to auth schemes.""" + + auth_scheme_resolver: AuthSchemeResolver | None = None + """ + An auth scheme resolver that determines the auth scheme for each + operation. + """ + + aws_credentials_identity_resolver: ( + IdentityResolver[AWSCredentialsIdentity, AWSIdentityProperties] | None + ) = None + """Resolves AWS Credentials. Required for operations that use Sigv4 Auth.""" + + region: str | None = None + """ + The AWS region to connect to. The configured region is used to determine + the service endpoint. + """ + + aws_access_key_id: str | None = None + """The identifier for a secret access key.""" + + aws_secret_access_key: str | None = None + """A secret access key that can be used to sign requests.""" + + aws_session_token: str | None = None + """The session token used with temporary AWS credentials.""" + + user_agent_extra: str | None = None + """Additional suffix to be added to the User-Agent header.""" + + sdk_ua_app_id: str | None = None + """ + A unique and opaque application ID that is appended to the User-Agent + header. + """ + + _FIELDS: ClassVar[dict[str, FieldSpec]] = { + "aws_credentials_identity_resolver": FieldSpec(default=None), + "region": FieldSpec(default=None), + "aws_access_key_id": FieldSpec(default=None), + "aws_secret_access_key": FieldSpec(default=None), + "aws_session_token": FieldSpec(default=None), + "user_agent_extra": FieldSpec(default=None), + "sdk_ua_app_id": FieldSpec(default=None), + **AwsConfig._FIELDS, + "endpoint_uri": FieldSpec( + default=None, resolver=EndpointUriResolver("json_protocol") + ), + "endpoint_resolver": FieldSpec( + default_factory=lambda: StandardRegionalEndpointsResolver( + endpoint_prefix="jsonprotocol" + ) + ), + "protocol": FieldSpec( + default_factory=lambda: AwsJson11ClientProtocol(_PROTOCOL_SETTINGS), + converter=lambda p: p(_PROTOCOL_SETTINGS) if isinstance(p, type) else p, + ), + "auth_schemes": FieldSpec( + default_factory=lambda: { + ShapeID("aws.auth#sigv4"): SigV4AuthScheme(service="jsonprotocol") + } + ), + "auth_scheme_resolver": FieldSpec(default_factory=HTTPAuthSchemeResolver), + "transport": FieldSpec(default_factory=lambda: Urllib3HTTPClient()), + } + + def set_auth_scheme(self, scheme: AuthScheme[Any, Any, Any, Any]) -> None: + """ + Set an auth scheme implementation using its scheme ID. :param scheme: + The auth scheme to add or replace. + """ + auth_schemes = dict(self.auth_schemes or {}) + auth_schemes[scheme.scheme_id] = scheme + self.auth_schemes = auth_schemes + + @classmethod + def resolve( # pyright: ignore[reportIncompatibleMethodOverride] + cls, + *, + profile: str | None = None, + fs: FileSystem | None = None, + config_file_path: str | None = None, + credentials_file_path: str | None = None, + **overrides: Unpack[_JsonProtocolConfigOverrides], + ) -> Self: + """ + Resolve config from environment, config files, defaults, and explicit + overrides. + """ + return cls._resolve( + profile=profile, + fs=fs, + config_file_path=config_file_path, + credentials_file_path=credentials_file_path, + overrides=overrides, + ) + + +Plugin: TypeAlias = Callable[[JsonProtocolConfig], None] +""" +A callable that customizes a client configuration. Service-level plugins +are applied once to the base configuration inherited by every operation. +Operation-level plugins apply only to a single operation invocation. +""" diff --git a/codegen-output/aws-json-1-1/src/awsjson11/sync/user_agent.py b/codegen-output/aws-json-1-1/src/awsjson11/sync/user_agent.py new file mode 100644 index 000000000..683f8893e --- /dev/null +++ b/codegen-output/aws-json-1-1/src/awsjson11/sync/user_agent.py @@ -0,0 +1,17 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from smithy_aws_core.interceptors.user_agent import UserAgentInterceptor + +from .. import __version__ +from .config import JsonProtocolConfig + + +def aws_user_agent_plugin(config: JsonProtocolConfig): + config.interceptors.append( + UserAgentInterceptor( + ua_suffix=config.user_agent_extra, + ua_app_id=config.sdk_ua_app_id, + sdk_version=__version__, + service_id="Json_Protocol", + ) + ) diff --git a/codegen-output/aws-query/pyproject.toml b/codegen-output/aws-query/pyproject.toml index b74bcb3a3..0bb2747c8 100644 --- a/codegen-output/aws-query/pyproject.toml +++ b/codegen-output/aws-query/pyproject.toml @@ -26,7 +26,7 @@ classifiers = [ dependencies = [ "smithy_aws_core[xml]~=0.12.0", "smithy_core~=0.9.0", - "smithy_http[aiohttp]~=0.6.0" + "smithy_http[aiohttp, urllib3]~=0.6.0" ] [project.optional-dependencies] diff --git a/codegen-output/aws-query/src/awsquery/__init__.py b/codegen-output/aws-query/src/awsquery/__init__.py index 30f6d44e9..2f2a97db0 100644 --- a/codegen-output/aws-query/src/awsquery/__init__.py +++ b/codegen-output/aws-query/src/awsquery/__init__.py @@ -1,3 +1,20 @@ # Code generated by smithy-python-codegen DO NOT EDIT. +from typing import Any, TYPE_CHECKING + + +if TYPE_CHECKING: + from .sync import QueryProtocolClient, QueryProtocolConfig + +__all__ = ["QueryProtocolClient", "QueryProtocolConfig"] + + +def __getattr__(name: str) -> Any: + if name in __all__: + from . import sync + + return getattr(sync, name) + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + __version__: str = "0.0.1" diff --git a/codegen-output/aws-query/src/awsquery/sync/__init__.py b/codegen-output/aws-query/src/awsquery/sync/__init__.py new file mode 100644 index 000000000..8193df1d3 --- /dev/null +++ b/codegen-output/aws-query/src/awsquery/sync/__init__.py @@ -0,0 +1,6 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from .client import QueryProtocolClient +from .config import QueryProtocolConfig, Plugin + +__all__ = ["QueryProtocolClient", "QueryProtocolConfig", "Plugin"] diff --git a/codegen-output/aws-query/src/awsquery/sync/client.py b/codegen-output/aws-query/src/awsquery/sync/client.py new file mode 100644 index 000000000..517646aff --- /dev/null +++ b/codegen-output/aws-query/src/awsquery/sync/client.py @@ -0,0 +1,1088 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from __future__ import annotations + +from copy import deepcopy +import logging +from typing import Any, cast + +from smithy_aws_core.config import ConfigSource +from smithy_aws_core.identity import AWSCredentialsIdentity +from smithy_aws_core.identity.chain import IdentityChain +from smithy_core.client import Client, ClientCall, RequestPipeline +from smithy_core.deserializers import DeserializeableShape +from smithy_core.exceptions import ExpectationNotMetError +from smithy_core.schemas import APIOperation +from smithy_core.serializers import SerializeableShape +from smithy_http.interfaces import HTTPClient +from smithy_http.plugins import user_agent_plugin + +from ..models import ( + DATETIME_OFFSETS, + EMPTY_INPUT_AND_EMPTY_OUTPUT, + ENDPOINT_OPERATION, + ENDPOINT_WITH_HOST_LABEL_OPERATION, + FLATTENED_XML_MAP, + FLATTENED_XML_MAP_WITH_XML_NAME, + FLATTENED_XML_MAP_WITH_XML_NAMESPACE, + FRACTIONAL_SECONDS, + GREETING_WITH_ERRORS, + HOST_WITH_PATH_OPERATION, + IGNORES_WRAPPING_XML_NAME, + NESTED_STRUCTURES, + NO_INPUT_AND_NO_OUTPUT, + NO_INPUT_AND_OUTPUT, + PUT_WITH_CONTENT_ENCODING, + QUERY_IDEMPOTENCY_TOKEN_AUTO_FILL, + QUERY_LISTS, + QUERY_MAPS, + QUERY_TIMESTAMPS, + RECURSIVE_XML_SHAPES, + SIMPLE_INPUT_PARAMS, + SIMPLE_SCALAR_XML_PROPERTIES, + XML_BLOBS, + XML_EMPTY_BLOBS, + XML_EMPTY_LISTS, + XML_EMPTY_MAPS, + XML_ENUMS, + XML_INT_ENUMS, + XML_LISTS, + XML_MAPS, + XML_MAPS_XML_NAME, + XML_NAMESPACES, + XML_TIMESTAMPS, +) +from .config import Plugin, QueryProtocolConfig +from .user_agent import aws_user_agent_plugin + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from ..models import ( + DatetimeOffsetsInput, + DatetimeOffsetsOutput, + EmptyInputAndEmptyOutputInput, + EmptyInputAndEmptyOutputOutput, + EndpointOperationInput, + EndpointOperationOutput, + EndpointWithHostLabelOperationInput, + EndpointWithHostLabelOperationOutput, + FlattenedXmlMapInput, + FlattenedXmlMapOutput, + FlattenedXmlMapWithXmlNameInput, + FlattenedXmlMapWithXmlNameOutput, + FlattenedXmlMapWithXmlNamespaceInput, + FlattenedXmlMapWithXmlNamespaceOutput, + FractionalSecondsInput, + FractionalSecondsOutput, + GreetingWithErrorsInput, + GreetingWithErrorsOutput, + HostWithPathOperationInput, + HostWithPathOperationOutput, + IgnoresWrappingXmlNameInput, + IgnoresWrappingXmlNameOutput, + NestedStructuresInput, + NestedStructuresOutput, + NoInputAndNoOutputInput, + NoInputAndNoOutputOutput, + NoInputAndOutputInput, + NoInputAndOutputOutput, + PutWithContentEncodingInput, + PutWithContentEncodingOutput, + QueryIdempotencyTokenAutoFillInput, + QueryIdempotencyTokenAutoFillOutput, + QueryListsInput, + QueryListsOutput, + QueryMapsInput, + QueryMapsOutput, + QueryTimestampsInput, + QueryTimestampsOutput, + RecursiveXmlShapesInput, + RecursiveXmlShapesOutput, + SimpleInputParamsInput, + SimpleInputParamsOutput, + SimpleScalarXmlPropertiesInput, + SimpleScalarXmlPropertiesOutput, + XmlBlobsInput, + XmlBlobsOperationOutput, + XmlEmptyBlobsInput, + XmlEmptyBlobsOutput, + XmlEmptyListsInput, + XmlEmptyListsOutput, + XmlEmptyMapsInput, + XmlEmptyMapsOutput, + XmlEnumsInput, + XmlEnumsOutput, + XmlIntEnumsInput, + XmlIntEnumsOutput, + XmlListsInput, + XmlListsOperationOutput, + XmlMapsInput, + XmlMapsOperationOutput, + XmlMapsXmlNameInput, + XmlMapsXmlNameOutput, + XmlNamespacesInput, + XmlNamespacesOutput, + XmlTimestampsInput, + XmlTimestampsOutput, + ) + + +logger = logging.getLogger(__name__) + + +class QueryProtocolClient(Client): + """A query service that sends query requests and XML responses.""" + + def __init__( + self, + config: QueryProtocolConfig | None = None, + plugins: list[Plugin] | None = None, + ): + """ + Constructor for `QueryProtocolClient`. + + Args: + config: + Optional configuration for the client. Here you can set things like + the endpoint for HTTP services or auth credentials. + plugins: + A list of callables applied once to the client's base configuration. + Their changes are inherited by every operation invocation. + """ + super().__init__() + self._config = config + self._plugins = plugins + self._client_plugins: list[Plugin] = [aws_user_agent_plugin, user_agent_plugin] + + def _ensure_setup(self) -> QueryProtocolConfig: + if not self._setup_done: + with self._derive_lock: + if self._closed: + raise RuntimeError( + "Cannot invoke an operation on a client that has been closed." + ) + if not self._setup_done: + if self._config is None: + config = QueryProtocolConfig.resolve() + else: + # Copy so plugins don't mutate the caller's config. + config = deepcopy(self._config) + for plugin in self._client_plugins: + plugin(config) + if self._plugins: + for plugin in self._plugins: + plugin(config) + # Publish state only after setup fully succeeds, so a + # failed _post_setup leaves the caller's config untouched + # (no re-applied plugins) and the transport closeable. + self._post_setup(config) + self._transport = config.transport + self._config = config + self._setup_done = True + assert self._config is not None + return self._config + + def _post_setup(self, config: QueryProtocolConfig) -> None: + if config.aws_credentials_identity_resolver is None: + config_context = config.resolution_context() + config_file = None + profile_name = None + if config_context is not None: + config_file = config_context.parsed_profiles() + if config_context.profile_source is ConfigSource.OVERRIDE: + profile_name = config_context.profile_name + config.aws_credentials_identity_resolver = IdentityChain.create( + AWSCredentialsIdentity, + config_file=config_file, + profile_name=profile_name, + region_override=config.region, + http_client=cast(HTTPClient | None, config.transport), + ) + + def _prepare_call[I: SerializeableShape, O: DeserializeableShape]( + self, + input: I, + operation: APIOperation[I, O], + default_plugins: list[Plugin], + plugins: list[Plugin] | None, + ) -> tuple[RequestPipeline[Any, Any], ClientCall[I, O]]: + if self._closed: + raise RuntimeError( + "Cannot invoke an operation on a client that has been closed." + ) + config = self._ensure_setup() + if default_plugins or plugins: + # Keep operation-plugin mutations scoped to this call. + config = deepcopy(config) + for plugin in default_plugins: + plugin(config) + if plugins: + for plugin in plugins: + plugin(config) + if ( + config.protocol is None + or config.transport is None + or config.endpoint_resolver is None + or config.auth_scheme_resolver is None + or config.auth_schemes is None + ): + raise ExpectationNotMetError( + "protocol, transport, endpoint_resolver, auth_scheme_resolver," + " and auth_schemes MUST be set on the config to make calls." + ) + retry_strategy = self._retry_strategy_resolver.resolve_retry_strategy( + retry_strategy=config.retry_strategy, + retry_mode=config.retry_mode, + max_attempts=config.max_attempts, + ) + return self._build_call( + input, operation, config=config, retry_strategy=retry_strategy + ) + + def datetime_offsets( + self, + input: DatetimeOffsetsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> DatetimeOffsetsOutput: + """ + Invokes the DatetimeOffsets operation. + + Args: + input: + An instance of `DatetimeOffsetsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `DatetimeOffsetsOutput`. + """ + if input is None: + input = DATETIME_OFFSETS.input() + pipeline, call = self._prepare_call(input, DATETIME_OFFSETS, [], plugins) + return pipeline(call) + + def empty_input_and_empty_output( + self, + input: EmptyInputAndEmptyOutputInput | None = None, + plugins: list[Plugin] | None = None, + ) -> EmptyInputAndEmptyOutputOutput: + """ + The example tests how requests and responses are serialized when + there's no request or response members. While this should be rare, code + generators must support this. + + Args: + input: + An instance of `EmptyInputAndEmptyOutputInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `EmptyInputAndEmptyOutputOutput`. + """ + if input is None: + input = EMPTY_INPUT_AND_EMPTY_OUTPUT.input() + pipeline, call = self._prepare_call( + input, EMPTY_INPUT_AND_EMPTY_OUTPUT, [], plugins + ) + return pipeline(call) + + def endpoint_operation( + self, + input: EndpointOperationInput | None = None, + plugins: list[Plugin] | None = None, + ) -> EndpointOperationOutput: + """ + Invokes the EndpointOperation operation. + + Args: + input: + An instance of `EndpointOperationInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `EndpointOperationOutput`. + """ + if input is None: + input = ENDPOINT_OPERATION.input() + pipeline, call = self._prepare_call(input, ENDPOINT_OPERATION, [], plugins) + return pipeline(call) + + def endpoint_with_host_label_operation( + self, + input: EndpointWithHostLabelOperationInput, + plugins: list[Plugin] | None = None, + ) -> EndpointWithHostLabelOperationOutput: + """ + Invokes the EndpointWithHostLabelOperation operation. + + Args: + input: + An instance of `EndpointWithHostLabelOperationInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `EndpointWithHostLabelOperationOutput`. + """ + pipeline, call = self._prepare_call( + input, ENDPOINT_WITH_HOST_LABEL_OPERATION, [], plugins + ) + return pipeline(call) + + def flattened_xml_map( + self, + input: FlattenedXmlMapInput | None = None, + plugins: list[Plugin] | None = None, + ) -> FlattenedXmlMapOutput: + """ + Flattened maps + + Args: + input: + An instance of `FlattenedXmlMapInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `FlattenedXmlMapOutput`. + """ + if input is None: + input = FLATTENED_XML_MAP.input() + pipeline, call = self._prepare_call(input, FLATTENED_XML_MAP, [], plugins) + return pipeline(call) + + def flattened_xml_map_with_xml_name( + self, + input: FlattenedXmlMapWithXmlNameInput | None = None, + plugins: list[Plugin] | None = None, + ) -> FlattenedXmlMapWithXmlNameOutput: + """ + Flattened maps with @xmlName + + Args: + input: + An instance of `FlattenedXmlMapWithXmlNameInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `FlattenedXmlMapWithXmlNameOutput`. + """ + if input is None: + input = FLATTENED_XML_MAP_WITH_XML_NAME.input() + pipeline, call = self._prepare_call( + input, FLATTENED_XML_MAP_WITH_XML_NAME, [], plugins + ) + return pipeline(call) + + def flattened_xml_map_with_xml_namespace( + self, + input: FlattenedXmlMapWithXmlNamespaceInput | None = None, + plugins: list[Plugin] | None = None, + ) -> FlattenedXmlMapWithXmlNamespaceOutput: + """ + Flattened maps with @xmlNamespace and @xmlName + + Args: + input: + An instance of `FlattenedXmlMapWithXmlNamespaceInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `FlattenedXmlMapWithXmlNamespaceOutput`. + """ + if input is None: + input = FLATTENED_XML_MAP_WITH_XML_NAMESPACE.input() + pipeline, call = self._prepare_call( + input, FLATTENED_XML_MAP_WITH_XML_NAMESPACE, [], plugins + ) + return pipeline(call) + + def fractional_seconds( + self, + input: FractionalSecondsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> FractionalSecondsOutput: + """ + Invokes the FractionalSeconds operation. + + Args: + input: + An instance of `FractionalSecondsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `FractionalSecondsOutput`. + """ + if input is None: + input = FRACTIONAL_SECONDS.input() + pipeline, call = self._prepare_call(input, FRACTIONAL_SECONDS, [], plugins) + return pipeline(call) + + def greeting_with_errors( + self, + input: GreetingWithErrorsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> GreetingWithErrorsOutput: + """ + This operation has three possible return values: 1. A successful + response in the form of GreetingWithErrorsOutput 2. An InvalidGreeting + error. 3. A BadRequest error. + + Args: + input: + An instance of `GreetingWithErrorsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `GreetingWithErrorsOutput`. + """ + if input is None: + input = GREETING_WITH_ERRORS.input() + pipeline, call = self._prepare_call(input, GREETING_WITH_ERRORS, [], plugins) + return pipeline(call) + + def host_with_path_operation( + self, + input: HostWithPathOperationInput | None = None, + plugins: list[Plugin] | None = None, + ) -> HostWithPathOperationOutput: + """ + Invokes the HostWithPathOperation operation. + + Args: + input: + An instance of `HostWithPathOperationInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `HostWithPathOperationOutput`. + """ + if input is None: + input = HOST_WITH_PATH_OPERATION.input() + pipeline, call = self._prepare_call( + input, HOST_WITH_PATH_OPERATION, [], plugins + ) + return pipeline(call) + + def ignores_wrapping_xml_name( + self, + input: IgnoresWrappingXmlNameInput | None = None, + plugins: list[Plugin] | None = None, + ) -> IgnoresWrappingXmlNameOutput: + """ + The xmlName trait on the output structure is ignored in AWS Query. The + wrapping element is always operation name + \"Response\", and inside of + that wrapper is another wrapper named operation name + \"Result\". + + Args: + input: + An instance of `IgnoresWrappingXmlNameInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `IgnoresWrappingXmlNameOutput`. + """ + if input is None: + input = IGNORES_WRAPPING_XML_NAME.input() + pipeline, call = self._prepare_call( + input, IGNORES_WRAPPING_XML_NAME, [], plugins + ) + return pipeline(call) + + def nested_structures( + self, + input: NestedStructuresInput | None = None, + plugins: list[Plugin] | None = None, + ) -> NestedStructuresOutput: + """ + This test serializes nested and recursive structure members. + + Args: + input: + An instance of `NestedStructuresInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `NestedStructuresOutput`. + """ + if input is None: + input = NESTED_STRUCTURES.input() + pipeline, call = self._prepare_call(input, NESTED_STRUCTURES, [], plugins) + return pipeline(call) + + def no_input_and_no_output( + self, + input: NoInputAndNoOutputInput | None = None, + plugins: list[Plugin] | None = None, + ) -> NoInputAndNoOutputOutput: + """ + The example tests how requests and responses are serialized when + there's no request or response payload because the operation has no + input or output. While this should be rare, code generators must support + this. + + Args: + input: + An instance of `NoInputAndNoOutputInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `NoInputAndNoOutputOutput`. + """ + if input is None: + input = NO_INPUT_AND_NO_OUTPUT.input() + pipeline, call = self._prepare_call(input, NO_INPUT_AND_NO_OUTPUT, [], plugins) + return pipeline(call) + + def no_input_and_output( + self, + input: NoInputAndOutputInput | None = None, + plugins: list[Plugin] | None = None, + ) -> NoInputAndOutputOutput: + """ + The example tests how requests and responses are serialized when + there's no request payload or response members. While this should be + rare, code generators must support this. + + Args: + input: + An instance of `NoInputAndOutputInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `NoInputAndOutputOutput`. + """ + if input is None: + input = NO_INPUT_AND_OUTPUT.input() + pipeline, call = self._prepare_call(input, NO_INPUT_AND_OUTPUT, [], plugins) + return pipeline(call) + + def put_with_content_encoding( + self, + input: PutWithContentEncodingInput | None = None, + plugins: list[Plugin] | None = None, + ) -> PutWithContentEncodingOutput: + """ + Invokes the PutWithContentEncoding operation. + + Args: + input: + An instance of `PutWithContentEncodingInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `PutWithContentEncodingOutput`. + """ + if input is None: + input = PUT_WITH_CONTENT_ENCODING.input() + pipeline, call = self._prepare_call( + input, PUT_WITH_CONTENT_ENCODING, [], plugins + ) + return pipeline(call) + + def query_idempotency_token_auto_fill( + self, + input: QueryIdempotencyTokenAutoFillInput | None = None, + plugins: list[Plugin] | None = None, + ) -> QueryIdempotencyTokenAutoFillOutput: + """ + Automatically adds idempotency tokens. + + Args: + input: + An instance of `QueryIdempotencyTokenAutoFillInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `QueryIdempotencyTokenAutoFillOutput`. + """ + if input is None: + input = QUERY_IDEMPOTENCY_TOKEN_AUTO_FILL.input() + pipeline, call = self._prepare_call( + input, QUERY_IDEMPOTENCY_TOKEN_AUTO_FILL, [], plugins + ) + return pipeline(call) + + def query_lists( + self, input: QueryListsInput | None = None, plugins: list[Plugin] | None = None + ) -> QueryListsOutput: + """ + This test serializes simple and complex lists. + + Args: + input: + An instance of `QueryListsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `QueryListsOutput`. + """ + if input is None: + input = QUERY_LISTS.input() + pipeline, call = self._prepare_call(input, QUERY_LISTS, [], plugins) + return pipeline(call) + + def query_maps( + self, input: QueryMapsInput | None = None, plugins: list[Plugin] | None = None + ) -> QueryMapsOutput: + """ + This test serializes simple and complex maps. + + Args: + input: + An instance of `QueryMapsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `QueryMapsOutput`. + """ + if input is None: + input = QUERY_MAPS.input() + pipeline, call = self._prepare_call(input, QUERY_MAPS, [], plugins) + return pipeline(call) + + def query_timestamps( + self, + input: QueryTimestampsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> QueryTimestampsOutput: + """ + This test serializes timestamps. 1. Timestamps are serialized as RFC + 3339 date-time values by default. 2. A timestampFormat trait on a member + changes the format. 3. A timestampFormat trait on the shape targeted by + the member changes the format. + + Args: + input: + An instance of `QueryTimestampsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `QueryTimestampsOutput`. + """ + if input is None: + input = QUERY_TIMESTAMPS.input() + pipeline, call = self._prepare_call(input, QUERY_TIMESTAMPS, [], plugins) + return pipeline(call) + + def recursive_xml_shapes( + self, + input: RecursiveXmlShapesInput | None = None, + plugins: list[Plugin] | None = None, + ) -> RecursiveXmlShapesOutput: + """ + Recursive shapes + + Args: + input: + An instance of `RecursiveXmlShapesInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `RecursiveXmlShapesOutput`. + """ + if input is None: + input = RECURSIVE_XML_SHAPES.input() + pipeline, call = self._prepare_call(input, RECURSIVE_XML_SHAPES, [], plugins) + return pipeline(call) + + def simple_input_params( + self, + input: SimpleInputParamsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> SimpleInputParamsOutput: + """ + This test serializes strings, numbers, and boolean values. + + Args: + input: + An instance of `SimpleInputParamsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `SimpleInputParamsOutput`. + """ + if input is None: + input = SIMPLE_INPUT_PARAMS.input() + pipeline, call = self._prepare_call(input, SIMPLE_INPUT_PARAMS, [], plugins) + return pipeline(call) + + def simple_scalar_xml_properties( + self, + input: SimpleScalarXmlPropertiesInput | None = None, + plugins: list[Plugin] | None = None, + ) -> SimpleScalarXmlPropertiesOutput: + """ + Invokes the SimpleScalarXmlProperties operation. + + Args: + input: + An instance of `SimpleScalarXmlPropertiesInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `SimpleScalarXmlPropertiesOutput`. + """ + if input is None: + input = SIMPLE_SCALAR_XML_PROPERTIES.input() + pipeline, call = self._prepare_call( + input, SIMPLE_SCALAR_XML_PROPERTIES, [], plugins + ) + return pipeline(call) + + def xml_blobs( + self, input: XmlBlobsInput | None = None, plugins: list[Plugin] | None = None + ) -> XmlBlobsOperationOutput: + """ + Blobs are base64 encoded + + Args: + input: + An instance of `XmlBlobsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `XmlBlobsOperationOutput`. + """ + if input is None: + input = XML_BLOBS.input() + pipeline, call = self._prepare_call(input, XML_BLOBS, [], plugins) + return pipeline(call) + + def xml_empty_blobs( + self, + input: XmlEmptyBlobsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> XmlEmptyBlobsOutput: + """ + Invokes the XmlEmptyBlobs operation. + + Args: + input: + An instance of `XmlEmptyBlobsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `XmlEmptyBlobsOutput`. + """ + if input is None: + input = XML_EMPTY_BLOBS.input() + pipeline, call = self._prepare_call(input, XML_EMPTY_BLOBS, [], plugins) + return pipeline(call) + + def xml_empty_lists( + self, + input: XmlEmptyListsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> XmlEmptyListsOutput: + """ + Invokes the XmlEmptyLists operation. + + Args: + input: + An instance of `XmlEmptyListsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `XmlEmptyListsOutput`. + """ + if input is None: + input = XML_EMPTY_LISTS.input() + pipeline, call = self._prepare_call(input, XML_EMPTY_LISTS, [], plugins) + return pipeline(call) + + def xml_empty_maps( + self, + input: XmlEmptyMapsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> XmlEmptyMapsOutput: + """ + Invokes the XmlEmptyMaps operation. + + Args: + input: + An instance of `XmlEmptyMapsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `XmlEmptyMapsOutput`. + """ + if input is None: + input = XML_EMPTY_MAPS.input() + pipeline, call = self._prepare_call(input, XML_EMPTY_MAPS, [], plugins) + return pipeline(call) + + def xml_enums( + self, input: XmlEnumsInput | None = None, plugins: list[Plugin] | None = None + ) -> XmlEnumsOutput: + """ + This example serializes enums as top level properties, in lists, sets, + and maps. + + Args: + input: + An instance of `XmlEnumsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `XmlEnumsOutput`. + """ + if input is None: + input = XML_ENUMS.input() + pipeline, call = self._prepare_call(input, XML_ENUMS, [], plugins) + return pipeline(call) + + def xml_int_enums( + self, input: XmlIntEnumsInput | None = None, plugins: list[Plugin] | None = None + ) -> XmlIntEnumsOutput: + """ + This example serializes enums as top level properties, in lists, sets, + and maps. + + Args: + input: + An instance of `XmlIntEnumsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `XmlIntEnumsOutput`. + """ + if input is None: + input = XML_INT_ENUMS.input() + pipeline, call = self._prepare_call(input, XML_INT_ENUMS, [], plugins) + return pipeline(call) + + def xml_lists( + self, input: XmlListsInput | None = None, plugins: list[Plugin] | None = None + ) -> XmlListsOperationOutput: + """ + This test case serializes XML lists for the following cases for both + input and output: 1. Normal XML lists. 2. Normal XML sets. 3. XML lists + of lists. 4. XML lists with @xmlName on its members 5. Flattened XML + lists. 6. Flattened XML lists with @xmlName. 7. Lists of structures. + + Args: + input: + An instance of `XmlListsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `XmlListsOperationOutput`. + """ + if input is None: + input = XML_LISTS.input() + pipeline, call = self._prepare_call(input, XML_LISTS, [], plugins) + return pipeline(call) + + def xml_maps( + self, input: XmlMapsInput | None = None, plugins: list[Plugin] | None = None + ) -> XmlMapsOperationOutput: + """ + The example tests basic map serialization. + + Args: + input: + An instance of `XmlMapsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `XmlMapsOperationOutput`. + """ + if input is None: + input = XML_MAPS.input() + pipeline, call = self._prepare_call(input, XML_MAPS, [], plugins) + return pipeline(call) + + def xml_maps_xml_name( + self, + input: XmlMapsXmlNameInput | None = None, + plugins: list[Plugin] | None = None, + ) -> XmlMapsXmlNameOutput: + """ + Invokes the XmlMapsXmlName operation. + + Args: + input: + An instance of `XmlMapsXmlNameInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `XmlMapsXmlNameOutput`. + """ + if input is None: + input = XML_MAPS_XML_NAME.input() + pipeline, call = self._prepare_call(input, XML_MAPS_XML_NAME, [], plugins) + return pipeline(call) + + def xml_namespaces( + self, + input: XmlNamespacesInput | None = None, + plugins: list[Plugin] | None = None, + ) -> XmlNamespacesOutput: + """ + Invokes the XmlNamespaces operation. + + Args: + input: + An instance of `XmlNamespacesInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `XmlNamespacesOutput`. + """ + if input is None: + input = XML_NAMESPACES.input() + pipeline, call = self._prepare_call(input, XML_NAMESPACES, [], plugins) + return pipeline(call) + + def xml_timestamps( + self, + input: XmlTimestampsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> XmlTimestampsOutput: + """ + This tests how timestamps are serialized, including using the default + format of date-time and various @timestampFormat trait values. + + Args: + input: + An instance of `XmlTimestampsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `XmlTimestampsOutput`. + """ + if input is None: + input = XML_TIMESTAMPS.input() + pipeline, call = self._prepare_call(input, XML_TIMESTAMPS, [], plugins) + return pipeline(call) diff --git a/codegen-output/aws-query/src/awsquery/sync/config.py b/codegen-output/aws-query/src/awsquery/sync/config.py new file mode 100644 index 000000000..81250c4ae --- /dev/null +++ b/codegen-output/aws-query/src/awsquery/sync/config.py @@ -0,0 +1,319 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from dataclasses import dataclass, field +from typing import ( + Any, + Callable, + ClassVar, + Self, + TYPE_CHECKING, + TypeAlias, + Union, + Unpack, +) + +from smithy_aws_core.auth.sigv4 import SigV4AuthScheme +from smithy_aws_core.config import FileSystem +from smithy_aws_core.config.aws_config import AwsConfig, AwsConfigOverrides +from smithy_aws_core.config.resolvers import EndpointUriResolver +from smithy_aws_core.config.types import FieldSpec +from smithy_aws_core.endpoints.standard_regional import ( + StandardRegionalEndpointsResolver, +) +from smithy_aws_core.identity import AWSCredentialsIdentity, AWSIdentityProperties +from smithy_aws_core.protocols import AwsQueryClientProtocol +from smithy_core.interfaces import ( + ClientProtocol, + EndpointResolver, + ProtocolConstructor, + ProtocolSettings, +) +from smithy_core.interfaces.auth import AuthScheme, AuthSchemeResolver +from smithy_core.interfaces.identity import IdentityResolver +from smithy_core.shapes import ShapeID +from smithy_http.urllib3 import Urllib3HTTPClient + +from .._private.schemas import AWS_QUERY +from ..auth import HTTPAuthSchemeResolver + + +if TYPE_CHECKING: + from smithy_core.interceptors import Interceptor + from ..models import ( + DatetimeOffsetsInput, + DatetimeOffsetsOutput, + EmptyInputAndEmptyOutputInput, + EmptyInputAndEmptyOutputOutput, + EndpointOperationInput, + EndpointOperationOutput, + EndpointWithHostLabelOperationInput, + EndpointWithHostLabelOperationOutput, + FlattenedXmlMapInput, + FlattenedXmlMapOutput, + FlattenedXmlMapWithXmlNameInput, + FlattenedXmlMapWithXmlNameOutput, + FlattenedXmlMapWithXmlNamespaceInput, + FlattenedXmlMapWithXmlNamespaceOutput, + FractionalSecondsInput, + FractionalSecondsOutput, + GreetingWithErrorsInput, + GreetingWithErrorsOutput, + HostWithPathOperationInput, + HostWithPathOperationOutput, + IgnoresWrappingXmlNameInput, + IgnoresWrappingXmlNameOutput, + NestedStructuresInput, + NestedStructuresOutput, + NoInputAndNoOutputInput, + NoInputAndNoOutputOutput, + NoInputAndOutputInput, + NoInputAndOutputOutput, + PutWithContentEncodingInput, + PutWithContentEncodingOutput, + QueryIdempotencyTokenAutoFillInput, + QueryIdempotencyTokenAutoFillOutput, + QueryListsInput, + QueryListsOutput, + QueryMapsInput, + QueryMapsOutput, + QueryTimestampsInput, + QueryTimestampsOutput, + RecursiveXmlShapesInput, + RecursiveXmlShapesOutput, + SimpleInputParamsInput, + SimpleInputParamsOutput, + SimpleScalarXmlPropertiesInput, + SimpleScalarXmlPropertiesOutput, + XmlBlobsInput, + XmlBlobsOperationOutput, + XmlEmptyBlobsInput, + XmlEmptyBlobsOutput, + XmlEmptyListsInput, + XmlEmptyListsOutput, + XmlEmptyMapsInput, + XmlEmptyMapsOutput, + XmlEnumsInput, + XmlEnumsOutput, + XmlIntEnumsInput, + XmlIntEnumsOutput, + XmlListsInput, + XmlListsOperationOutput, + XmlMapsInput, + XmlMapsOperationOutput, + XmlMapsXmlNameInput, + XmlMapsXmlNameOutput, + XmlNamespacesInput, + XmlNamespacesOutput, + XmlTimestampsInput, + XmlTimestampsOutput, + ) + + _ServiceInterceptor = Union[ + Interceptor[DatetimeOffsetsInput, DatetimeOffsetsOutput, Any, Any], + Interceptor[ + EmptyInputAndEmptyOutputInput, EmptyInputAndEmptyOutputOutput, Any, Any + ], + Interceptor[EndpointOperationInput, EndpointOperationOutput, Any, Any], + Interceptor[ + EndpointWithHostLabelOperationInput, + EndpointWithHostLabelOperationOutput, + Any, + Any, + ], + Interceptor[FlattenedXmlMapInput, FlattenedXmlMapOutput, Any, Any], + Interceptor[ + FlattenedXmlMapWithXmlNameInput, FlattenedXmlMapWithXmlNameOutput, Any, Any + ], + Interceptor[ + FlattenedXmlMapWithXmlNamespaceInput, + FlattenedXmlMapWithXmlNamespaceOutput, + Any, + Any, + ], + Interceptor[FractionalSecondsInput, FractionalSecondsOutput, Any, Any], + Interceptor[GreetingWithErrorsInput, GreetingWithErrorsOutput, Any, Any], + Interceptor[HostWithPathOperationInput, HostWithPathOperationOutput, Any, Any], + Interceptor[ + IgnoresWrappingXmlNameInput, IgnoresWrappingXmlNameOutput, Any, Any + ], + Interceptor[NestedStructuresInput, NestedStructuresOutput, Any, Any], + Interceptor[NoInputAndNoOutputInput, NoInputAndNoOutputOutput, Any, Any], + Interceptor[NoInputAndOutputInput, NoInputAndOutputOutput, Any, Any], + Interceptor[ + PutWithContentEncodingInput, PutWithContentEncodingOutput, Any, Any + ], + Interceptor[ + QueryIdempotencyTokenAutoFillInput, + QueryIdempotencyTokenAutoFillOutput, + Any, + Any, + ], + Interceptor[QueryListsInput, QueryListsOutput, Any, Any], + Interceptor[QueryMapsInput, QueryMapsOutput, Any, Any], + Interceptor[QueryTimestampsInput, QueryTimestampsOutput, Any, Any], + Interceptor[RecursiveXmlShapesInput, RecursiveXmlShapesOutput, Any, Any], + Interceptor[SimpleInputParamsInput, SimpleInputParamsOutput, Any, Any], + Interceptor[ + SimpleScalarXmlPropertiesInput, SimpleScalarXmlPropertiesOutput, Any, Any + ], + Interceptor[XmlBlobsInput, XmlBlobsOperationOutput, Any, Any], + Interceptor[XmlEmptyBlobsInput, XmlEmptyBlobsOutput, Any, Any], + Interceptor[XmlEmptyListsInput, XmlEmptyListsOutput, Any, Any], + Interceptor[XmlEmptyMapsInput, XmlEmptyMapsOutput, Any, Any], + Interceptor[XmlEnumsInput, XmlEnumsOutput, Any, Any], + Interceptor[XmlIntEnumsInput, XmlIntEnumsOutput, Any, Any], + Interceptor[XmlListsInput, XmlListsOperationOutput, Any, Any], + Interceptor[XmlMapsInput, XmlMapsOperationOutput, Any, Any], + Interceptor[XmlMapsXmlNameInput, XmlMapsXmlNameOutput, Any, Any], + Interceptor[XmlNamespacesInput, XmlNamespacesOutput, Any, Any], + Interceptor[XmlTimestampsInput, XmlTimestampsOutput, Any, Any], + ] +else: + _ServiceInterceptor = Any + +_PROTOCOL_SETTINGS = ProtocolSettings( + namespace="aws.protocoltests.query", + service_target="AwsQuery", + version="2020-01-08", + service_schema=AWS_QUERY, +) + + +class _QueryProtocolConfigOverrides(AwsConfigOverrides, total=False): + endpoint_resolver: EndpointResolver | None + protocol: ( + ClientProtocol[Any, Any] | ProtocolConstructor[ClientProtocol[Any, Any]] | None + ) + auth_schemes: dict[ShapeID, AuthScheme[Any, Any, Any, Any]] | None + auth_scheme_resolver: AuthSchemeResolver | None + + +@dataclass(kw_only=True, repr=False, init=False) +class QueryProtocolConfig(AwsConfig): + """Query Protocol configuration.""" + + endpoint_resolver: EndpointResolver | None = None + """ + The endpoint resolver used to resolve the final endpoint per-operation + based on the configuration. + """ + + protocol: ClientProtocol[Any, Any] | None = None + """ + Pass a protocol class reference from smithy_aws_core.protocols to select + the protocol, e.g. protocol=AwsJson10ClientProtocol. For custom + protocols a protocol instance may also be passed. + """ + + interceptors: list[_ServiceInterceptor] = field(default_factory=lambda: []) + """ + The list of interceptors, which are hooks that are called during the + execution of a request. + """ + + auth_schemes: dict[ShapeID, AuthScheme[Any, Any, Any, Any]] | None = None + """A map of auth scheme ids to auth schemes.""" + + auth_scheme_resolver: AuthSchemeResolver | None = None + """ + An auth scheme resolver that determines the auth scheme for each + operation. + """ + + aws_credentials_identity_resolver: ( + IdentityResolver[AWSCredentialsIdentity, AWSIdentityProperties] | None + ) = None + """Resolves AWS Credentials. Required for operations that use Sigv4 Auth.""" + + region: str | None = None + """ + The AWS region to connect to. The configured region is used to determine + the service endpoint. + """ + + aws_access_key_id: str | None = None + """The identifier for a secret access key.""" + + aws_secret_access_key: str | None = None + """A secret access key that can be used to sign requests.""" + + aws_session_token: str | None = None + """The session token used with temporary AWS credentials.""" + + user_agent_extra: str | None = None + """Additional suffix to be added to the User-Agent header.""" + + sdk_ua_app_id: str | None = None + """ + A unique and opaque application ID that is appended to the User-Agent + header. + """ + + _FIELDS: ClassVar[dict[str, FieldSpec]] = { + "aws_credentials_identity_resolver": FieldSpec(default=None), + "region": FieldSpec(default=None), + "aws_access_key_id": FieldSpec(default=None), + "aws_secret_access_key": FieldSpec(default=None), + "aws_session_token": FieldSpec(default=None), + "user_agent_extra": FieldSpec(default=None), + "sdk_ua_app_id": FieldSpec(default=None), + **AwsConfig._FIELDS, + "endpoint_uri": FieldSpec( + default=None, resolver=EndpointUriResolver("query_protocol") + ), + "endpoint_resolver": FieldSpec( + default_factory=lambda: StandardRegionalEndpointsResolver( + endpoint_prefix="awsquery" + ) + ), + "protocol": FieldSpec( + default_factory=lambda: AwsQueryClientProtocol(_PROTOCOL_SETTINGS), + converter=lambda p: p(_PROTOCOL_SETTINGS) if isinstance(p, type) else p, + ), + "auth_schemes": FieldSpec( + default_factory=lambda: { + ShapeID("aws.auth#sigv4"): SigV4AuthScheme(service="awsquery") + } + ), + "auth_scheme_resolver": FieldSpec(default_factory=HTTPAuthSchemeResolver), + "transport": FieldSpec(default_factory=lambda: Urllib3HTTPClient()), + } + + def set_auth_scheme(self, scheme: AuthScheme[Any, Any, Any, Any]) -> None: + """ + Set an auth scheme implementation using its scheme ID. :param scheme: + The auth scheme to add or replace. + """ + auth_schemes = dict(self.auth_schemes or {}) + auth_schemes[scheme.scheme_id] = scheme + self.auth_schemes = auth_schemes + + @classmethod + def resolve( # pyright: ignore[reportIncompatibleMethodOverride] + cls, + *, + profile: str | None = None, + fs: FileSystem | None = None, + config_file_path: str | None = None, + credentials_file_path: str | None = None, + **overrides: Unpack[_QueryProtocolConfigOverrides], + ) -> Self: + """ + Resolve config from environment, config files, defaults, and explicit + overrides. + """ + return cls._resolve( + profile=profile, + fs=fs, + config_file_path=config_file_path, + credentials_file_path=credentials_file_path, + overrides=overrides, + ) + + +Plugin: TypeAlias = Callable[[QueryProtocolConfig], None] +""" +A callable that customizes a client configuration. Service-level plugins +are applied once to the base configuration inherited by every operation. +Operation-level plugins apply only to a single operation invocation. +""" diff --git a/codegen-output/aws-query/src/awsquery/sync/user_agent.py b/codegen-output/aws-query/src/awsquery/sync/user_agent.py new file mode 100644 index 000000000..df720f436 --- /dev/null +++ b/codegen-output/aws-query/src/awsquery/sync/user_agent.py @@ -0,0 +1,17 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from smithy_aws_core.interceptors.user_agent import UserAgentInterceptor + +from .. import __version__ +from .config import QueryProtocolConfig + + +def aws_user_agent_plugin(config: QueryProtocolConfig): + config.interceptors.append( + UserAgentInterceptor( + ua_suffix=config.user_agent_extra, + ua_app_id=config.sdk_ua_app_id, + sdk_version=__version__, + service_id="Query_Protocol", + ) + ) diff --git a/codegen-output/ec2-query/pyproject.toml b/codegen-output/ec2-query/pyproject.toml index 668b9e244..6d0bc5867 100644 --- a/codegen-output/ec2-query/pyproject.toml +++ b/codegen-output/ec2-query/pyproject.toml @@ -26,7 +26,7 @@ classifiers = [ dependencies = [ "smithy_aws_core[xml]~=0.12.0", "smithy_core~=0.9.0", - "smithy_http[aiohttp]~=0.6.0" + "smithy_http[aiohttp, urllib3]~=0.6.0" ] [project.optional-dependencies] diff --git a/codegen-output/ec2-query/src/ec2query/__init__.py b/codegen-output/ec2-query/src/ec2query/__init__.py index 30f6d44e9..d1c46b8f4 100644 --- a/codegen-output/ec2-query/src/ec2query/__init__.py +++ b/codegen-output/ec2-query/src/ec2query/__init__.py @@ -1,3 +1,20 @@ # Code generated by smithy-python-codegen DO NOT EDIT. +from typing import Any, TYPE_CHECKING + + +if TYPE_CHECKING: + from .sync import EC2ProtocolClient, EC2ProtocolConfig + +__all__ = ["EC2ProtocolClient", "EC2ProtocolConfig"] + + +def __getattr__(name: str) -> Any: + if name in __all__: + from . import sync + + return getattr(sync, name) + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + __version__: str = "0.0.1" diff --git a/codegen-output/ec2-query/src/ec2query/sync/__init__.py b/codegen-output/ec2-query/src/ec2query/sync/__init__.py new file mode 100644 index 000000000..0410c1e4a --- /dev/null +++ b/codegen-output/ec2-query/src/ec2query/sync/__init__.py @@ -0,0 +1,6 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from .client import EC2ProtocolClient +from .config import EC2ProtocolConfig, Plugin + +__all__ = ["EC2ProtocolClient", "EC2ProtocolConfig", "Plugin"] diff --git a/codegen-output/ec2-query/src/ec2query/sync/client.py b/codegen-output/ec2-query/src/ec2query/sync/client.py new file mode 100644 index 000000000..673e17175 --- /dev/null +++ b/codegen-output/ec2-query/src/ec2query/sync/client.py @@ -0,0 +1,861 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from __future__ import annotations + +from copy import deepcopy +import logging +from typing import Any, cast + +from smithy_aws_core.config import ConfigSource +from smithy_aws_core.identity import AWSCredentialsIdentity +from smithy_aws_core.identity.chain import IdentityChain +from smithy_core.client import Client, ClientCall, RequestPipeline +from smithy_core.deserializers import DeserializeableShape +from smithy_core.exceptions import ExpectationNotMetError +from smithy_core.schemas import APIOperation +from smithy_core.serializers import SerializeableShape +from smithy_http.interfaces import HTTPClient +from smithy_http.plugins import user_agent_plugin + +from ..models import ( + DATETIME_OFFSETS, + EMPTY_INPUT_AND_EMPTY_OUTPUT, + ENDPOINT_OPERATION, + ENDPOINT_WITH_HOST_LABEL_OPERATION, + FRACTIONAL_SECONDS, + GREETING_WITH_ERRORS, + HOST_WITH_PATH_OPERATION, + IGNORES_WRAPPING_XML_NAME, + NESTED_STRUCTURES, + NO_INPUT_AND_OUTPUT, + PUT_WITH_CONTENT_ENCODING, + QUERY_IDEMPOTENCY_TOKEN_AUTO_FILL, + QUERY_LISTS, + QUERY_TIMESTAMPS, + RECURSIVE_XML_SHAPES, + SIMPLE_INPUT_PARAMS, + SIMPLE_SCALAR_XML_PROPERTIES, + XML_BLOBS, + XML_EMPTY_BLOBS, + XML_EMPTY_LISTS, + XML_ENUMS, + XML_INT_ENUMS, + XML_LISTS, + XML_NAMESPACES, + XML_TIMESTAMPS, +) +from .config import EC2ProtocolConfig, Plugin +from .user_agent import aws_user_agent_plugin + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from ..models import ( + DatetimeOffsetsInput, + DatetimeOffsetsOutput, + EmptyInputAndEmptyOutputInput, + EmptyInputAndEmptyOutputOutput, + EndpointOperationInput, + EndpointOperationOutput, + EndpointWithHostLabelOperationInput, + EndpointWithHostLabelOperationOutput, + FractionalSecondsInput, + FractionalSecondsOutput, + GreetingWithErrorsInput, + GreetingWithErrorsOutput, + HostWithPathOperationInput, + HostWithPathOperationOutput, + IgnoresWrappingXmlNameInput, + IgnoresWrappingXmlNameOutput, + NestedStructuresInput, + NestedStructuresOutput, + NoInputAndOutputInput, + NoInputAndOutputOutput, + PutWithContentEncodingInput, + PutWithContentEncodingOutput, + QueryIdempotencyTokenAutoFillInput, + QueryIdempotencyTokenAutoFillOutput, + QueryListsInput, + QueryListsOutput, + QueryTimestampsInput, + QueryTimestampsOutput, + RecursiveXmlShapesInput, + RecursiveXmlShapesOutput, + SimpleInputParamsInput, + SimpleInputParamsOutput, + SimpleScalarXmlPropertiesInput, + SimpleScalarXmlPropertiesOutput, + XmlBlobsInput, + XmlBlobsOperationOutput, + XmlEmptyBlobsInput, + XmlEmptyBlobsOutput, + XmlEmptyListsInput, + XmlEmptyListsOutput, + XmlEnumsInput, + XmlEnumsOutput, + XmlIntEnumsInput, + XmlIntEnumsOutput, + XmlListsInput, + XmlListsOperationOutput, + XmlNamespacesInput, + XmlNamespacesOutput, + XmlTimestampsInput, + XmlTimestampsOutput, + ) + + +logger = logging.getLogger(__name__) + + +class EC2ProtocolClient(Client): + """An EC2 query service that sends query requests and XML responses.""" + + def __init__( + self, + config: EC2ProtocolConfig | None = None, + plugins: list[Plugin] | None = None, + ): + """ + Constructor for `EC2ProtocolClient`. + + Args: + config: + Optional configuration for the client. Here you can set things like + the endpoint for HTTP services or auth credentials. + plugins: + A list of callables applied once to the client's base configuration. + Their changes are inherited by every operation invocation. + """ + super().__init__() + self._config = config + self._plugins = plugins + self._client_plugins: list[Plugin] = [aws_user_agent_plugin, user_agent_plugin] + + def _ensure_setup(self) -> EC2ProtocolConfig: + if not self._setup_done: + with self._derive_lock: + if self._closed: + raise RuntimeError( + "Cannot invoke an operation on a client that has been closed." + ) + if not self._setup_done: + if self._config is None: + config = EC2ProtocolConfig.resolve() + else: + # Copy so plugins don't mutate the caller's config. + config = deepcopy(self._config) + for plugin in self._client_plugins: + plugin(config) + if self._plugins: + for plugin in self._plugins: + plugin(config) + # Publish state only after setup fully succeeds, so a + # failed _post_setup leaves the caller's config untouched + # (no re-applied plugins) and the transport closeable. + self._post_setup(config) + self._transport = config.transport + self._config = config + self._setup_done = True + assert self._config is not None + return self._config + + def _post_setup(self, config: EC2ProtocolConfig) -> None: + if config.aws_credentials_identity_resolver is None: + config_context = config.resolution_context() + config_file = None + profile_name = None + if config_context is not None: + config_file = config_context.parsed_profiles() + if config_context.profile_source is ConfigSource.OVERRIDE: + profile_name = config_context.profile_name + config.aws_credentials_identity_resolver = IdentityChain.create( + AWSCredentialsIdentity, + config_file=config_file, + profile_name=profile_name, + region_override=config.region, + http_client=cast(HTTPClient | None, config.transport), + ) + + def _prepare_call[I: SerializeableShape, O: DeserializeableShape]( + self, + input: I, + operation: APIOperation[I, O], + default_plugins: list[Plugin], + plugins: list[Plugin] | None, + ) -> tuple[RequestPipeline[Any, Any], ClientCall[I, O]]: + if self._closed: + raise RuntimeError( + "Cannot invoke an operation on a client that has been closed." + ) + config = self._ensure_setup() + if default_plugins or plugins: + # Keep operation-plugin mutations scoped to this call. + config = deepcopy(config) + for plugin in default_plugins: + plugin(config) + if plugins: + for plugin in plugins: + plugin(config) + if ( + config.protocol is None + or config.transport is None + or config.endpoint_resolver is None + or config.auth_scheme_resolver is None + or config.auth_schemes is None + ): + raise ExpectationNotMetError( + "protocol, transport, endpoint_resolver, auth_scheme_resolver," + " and auth_schemes MUST be set on the config to make calls." + ) + retry_strategy = self._retry_strategy_resolver.resolve_retry_strategy( + retry_strategy=config.retry_strategy, + retry_mode=config.retry_mode, + max_attempts=config.max_attempts, + ) + return self._build_call( + input, operation, config=config, retry_strategy=retry_strategy + ) + + def datetime_offsets( + self, + input: DatetimeOffsetsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> DatetimeOffsetsOutput: + """ + Invokes the DatetimeOffsets operation. + + Args: + input: + An instance of `DatetimeOffsetsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `DatetimeOffsetsOutput`. + """ + if input is None: + input = DATETIME_OFFSETS.input() + pipeline, call = self._prepare_call(input, DATETIME_OFFSETS, [], plugins) + return pipeline(call) + + def empty_input_and_empty_output( + self, + input: EmptyInputAndEmptyOutputInput | None = None, + plugins: list[Plugin] | None = None, + ) -> EmptyInputAndEmptyOutputOutput: + """ + The example tests how requests and responses are serialized when + there's no request or response members. While this should be rare, code + generators must support this. + + Args: + input: + An instance of `EmptyInputAndEmptyOutputInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `EmptyInputAndEmptyOutputOutput`. + """ + if input is None: + input = EMPTY_INPUT_AND_EMPTY_OUTPUT.input() + pipeline, call = self._prepare_call( + input, EMPTY_INPUT_AND_EMPTY_OUTPUT, [], plugins + ) + return pipeline(call) + + def endpoint_operation( + self, + input: EndpointOperationInput | None = None, + plugins: list[Plugin] | None = None, + ) -> EndpointOperationOutput: + """ + Invokes the EndpointOperation operation. + + Args: + input: + An instance of `EndpointOperationInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `EndpointOperationOutput`. + """ + if input is None: + input = ENDPOINT_OPERATION.input() + pipeline, call = self._prepare_call(input, ENDPOINT_OPERATION, [], plugins) + return pipeline(call) + + def endpoint_with_host_label_operation( + self, + input: EndpointWithHostLabelOperationInput, + plugins: list[Plugin] | None = None, + ) -> EndpointWithHostLabelOperationOutput: + """ + Invokes the EndpointWithHostLabelOperation operation. + + Args: + input: + An instance of `EndpointWithHostLabelOperationInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `EndpointWithHostLabelOperationOutput`. + """ + pipeline, call = self._prepare_call( + input, ENDPOINT_WITH_HOST_LABEL_OPERATION, [], plugins + ) + return pipeline(call) + + def fractional_seconds( + self, + input: FractionalSecondsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> FractionalSecondsOutput: + """ + Invokes the FractionalSeconds operation. + + Args: + input: + An instance of `FractionalSecondsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `FractionalSecondsOutput`. + """ + if input is None: + input = FRACTIONAL_SECONDS.input() + pipeline, call = self._prepare_call(input, FRACTIONAL_SECONDS, [], plugins) + return pipeline(call) + + def greeting_with_errors( + self, + input: GreetingWithErrorsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> GreetingWithErrorsOutput: + """ + This operation has three possible return values: 1. A successful + response in the form of GreetingWithErrorsOutput 2. An InvalidGreeting + error. 3. A BadRequest error. + + Args: + input: + An instance of `GreetingWithErrorsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `GreetingWithErrorsOutput`. + """ + if input is None: + input = GREETING_WITH_ERRORS.input() + pipeline, call = self._prepare_call(input, GREETING_WITH_ERRORS, [], plugins) + return pipeline(call) + + def host_with_path_operation( + self, + input: HostWithPathOperationInput | None = None, + plugins: list[Plugin] | None = None, + ) -> HostWithPathOperationOutput: + """ + Invokes the HostWithPathOperation operation. + + Args: + input: + An instance of `HostWithPathOperationInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `HostWithPathOperationOutput`. + """ + if input is None: + input = HOST_WITH_PATH_OPERATION.input() + pipeline, call = self._prepare_call( + input, HOST_WITH_PATH_OPERATION, [], plugins + ) + return pipeline(call) + + def ignores_wrapping_xml_name( + self, + input: IgnoresWrappingXmlNameInput | None = None, + plugins: list[Plugin] | None = None, + ) -> IgnoresWrappingXmlNameOutput: + """ + The xmlName trait on the output structure is ignored in AWS Query. The + wrapping element is always operation name + \"Response\". + + Args: + input: + An instance of `IgnoresWrappingXmlNameInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `IgnoresWrappingXmlNameOutput`. + """ + if input is None: + input = IGNORES_WRAPPING_XML_NAME.input() + pipeline, call = self._prepare_call( + input, IGNORES_WRAPPING_XML_NAME, [], plugins + ) + return pipeline(call) + + def nested_structures( + self, + input: NestedStructuresInput | None = None, + plugins: list[Plugin] | None = None, + ) -> NestedStructuresOutput: + """ + This test serializes nested and recursive structure members. + + Args: + input: + An instance of `NestedStructuresInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `NestedStructuresOutput`. + """ + if input is None: + input = NESTED_STRUCTURES.input() + pipeline, call = self._prepare_call(input, NESTED_STRUCTURES, [], plugins) + return pipeline(call) + + def no_input_and_output( + self, + input: NoInputAndOutputInput | None = None, + plugins: list[Plugin] | None = None, + ) -> NoInputAndOutputOutput: + """ + The example tests how requests and responses are serialized when + there's no request payload or response members. While this should be + rare, code generators must support this. + + Args: + input: + An instance of `NoInputAndOutputInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `NoInputAndOutputOutput`. + """ + if input is None: + input = NO_INPUT_AND_OUTPUT.input() + pipeline, call = self._prepare_call(input, NO_INPUT_AND_OUTPUT, [], plugins) + return pipeline(call) + + def put_with_content_encoding( + self, + input: PutWithContentEncodingInput | None = None, + plugins: list[Plugin] | None = None, + ) -> PutWithContentEncodingOutput: + """ + Invokes the PutWithContentEncoding operation. + + Args: + input: + An instance of `PutWithContentEncodingInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `PutWithContentEncodingOutput`. + """ + if input is None: + input = PUT_WITH_CONTENT_ENCODING.input() + pipeline, call = self._prepare_call( + input, PUT_WITH_CONTENT_ENCODING, [], plugins + ) + return pipeline(call) + + def query_idempotency_token_auto_fill( + self, + input: QueryIdempotencyTokenAutoFillInput | None = None, + plugins: list[Plugin] | None = None, + ) -> QueryIdempotencyTokenAutoFillOutput: + """ + Automatically adds idempotency tokens. + + Args: + input: + An instance of `QueryIdempotencyTokenAutoFillInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `QueryIdempotencyTokenAutoFillOutput`. + """ + if input is None: + input = QUERY_IDEMPOTENCY_TOKEN_AUTO_FILL.input() + pipeline, call = self._prepare_call( + input, QUERY_IDEMPOTENCY_TOKEN_AUTO_FILL, [], plugins + ) + return pipeline(call) + + def query_lists( + self, input: QueryListsInput | None = None, plugins: list[Plugin] | None = None + ) -> QueryListsOutput: + """ + This test serializes simple and complex lists. + + Args: + input: + An instance of `QueryListsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `QueryListsOutput`. + """ + if input is None: + input = QUERY_LISTS.input() + pipeline, call = self._prepare_call(input, QUERY_LISTS, [], plugins) + return pipeline(call) + + def query_timestamps( + self, + input: QueryTimestampsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> QueryTimestampsOutput: + """ + This test serializes timestamps. 1. Timestamps are serialized as RFC + 3339 date-time values by default. 2. A timestampFormat trait on a member + changes the format. 3. A timestampFormat trait on the shape targeted by + the member changes the format. + + Args: + input: + An instance of `QueryTimestampsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `QueryTimestampsOutput`. + """ + if input is None: + input = QUERY_TIMESTAMPS.input() + pipeline, call = self._prepare_call(input, QUERY_TIMESTAMPS, [], plugins) + return pipeline(call) + + def recursive_xml_shapes( + self, + input: RecursiveXmlShapesInput | None = None, + plugins: list[Plugin] | None = None, + ) -> RecursiveXmlShapesOutput: + """ + Recursive shapes + + Args: + input: + An instance of `RecursiveXmlShapesInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `RecursiveXmlShapesOutput`. + """ + if input is None: + input = RECURSIVE_XML_SHAPES.input() + pipeline, call = self._prepare_call(input, RECURSIVE_XML_SHAPES, [], plugins) + return pipeline(call) + + def simple_input_params( + self, + input: SimpleInputParamsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> SimpleInputParamsOutput: + """ + This test serializes strings, numbers, and boolean values. + + Args: + input: + An instance of `SimpleInputParamsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `SimpleInputParamsOutput`. + """ + if input is None: + input = SIMPLE_INPUT_PARAMS.input() + pipeline, call = self._prepare_call(input, SIMPLE_INPUT_PARAMS, [], plugins) + return pipeline(call) + + def simple_scalar_xml_properties( + self, + input: SimpleScalarXmlPropertiesInput | None = None, + plugins: list[Plugin] | None = None, + ) -> SimpleScalarXmlPropertiesOutput: + """ + Invokes the SimpleScalarXmlProperties operation. + + Args: + input: + An instance of `SimpleScalarXmlPropertiesInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `SimpleScalarXmlPropertiesOutput`. + """ + if input is None: + input = SIMPLE_SCALAR_XML_PROPERTIES.input() + pipeline, call = self._prepare_call( + input, SIMPLE_SCALAR_XML_PROPERTIES, [], plugins + ) + return pipeline(call) + + def xml_blobs( + self, input: XmlBlobsInput | None = None, plugins: list[Plugin] | None = None + ) -> XmlBlobsOperationOutput: + """ + Blobs are base64 encoded + + Args: + input: + An instance of `XmlBlobsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `XmlBlobsOperationOutput`. + """ + if input is None: + input = XML_BLOBS.input() + pipeline, call = self._prepare_call(input, XML_BLOBS, [], plugins) + return pipeline(call) + + def xml_empty_blobs( + self, + input: XmlEmptyBlobsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> XmlEmptyBlobsOutput: + """ + Invokes the XmlEmptyBlobs operation. + + Args: + input: + An instance of `XmlEmptyBlobsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `XmlEmptyBlobsOutput`. + """ + if input is None: + input = XML_EMPTY_BLOBS.input() + pipeline, call = self._prepare_call(input, XML_EMPTY_BLOBS, [], plugins) + return pipeline(call) + + def xml_empty_lists( + self, + input: XmlEmptyListsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> XmlEmptyListsOutput: + """ + Invokes the XmlEmptyLists operation. + + Args: + input: + An instance of `XmlEmptyListsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `XmlEmptyListsOutput`. + """ + if input is None: + input = XML_EMPTY_LISTS.input() + pipeline, call = self._prepare_call(input, XML_EMPTY_LISTS, [], plugins) + return pipeline(call) + + def xml_enums( + self, input: XmlEnumsInput | None = None, plugins: list[Plugin] | None = None + ) -> XmlEnumsOutput: + """ + This example serializes enums as top level properties, in lists, sets, + and maps. + + Args: + input: + An instance of `XmlEnumsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `XmlEnumsOutput`. + """ + if input is None: + input = XML_ENUMS.input() + pipeline, call = self._prepare_call(input, XML_ENUMS, [], plugins) + return pipeline(call) + + def xml_int_enums( + self, input: XmlIntEnumsInput | None = None, plugins: list[Plugin] | None = None + ) -> XmlIntEnumsOutput: + """ + This example serializes intEnums as top level properties, in lists, + sets, and maps. + + Args: + input: + An instance of `XmlIntEnumsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `XmlIntEnumsOutput`. + """ + if input is None: + input = XML_INT_ENUMS.input() + pipeline, call = self._prepare_call(input, XML_INT_ENUMS, [], plugins) + return pipeline(call) + + def xml_lists( + self, input: XmlListsInput | None = None, plugins: list[Plugin] | None = None + ) -> XmlListsOperationOutput: + """ + This test case serializes XML lists for the following cases for both + input and output: 1. Normal XML lists. 2. Normal XML sets. 3. XML lists + of lists. 4. XML lists with @xmlName on its members 5. Flattened XML + lists. 6. Flattened XML lists with @xmlName. 7. Flattened XML lists + with @xmlNamespace. 8. Lists of structures. + + Args: + input: + An instance of `XmlListsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `XmlListsOperationOutput`. + """ + if input is None: + input = XML_LISTS.input() + pipeline, call = self._prepare_call(input, XML_LISTS, [], plugins) + return pipeline(call) + + def xml_namespaces( + self, + input: XmlNamespacesInput | None = None, + plugins: list[Plugin] | None = None, + ) -> XmlNamespacesOutput: + """ + Invokes the XmlNamespaces operation. + + Args: + input: + An instance of `XmlNamespacesInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `XmlNamespacesOutput`. + """ + if input is None: + input = XML_NAMESPACES.input() + pipeline, call = self._prepare_call(input, XML_NAMESPACES, [], plugins) + return pipeline(call) + + def xml_timestamps( + self, + input: XmlTimestampsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> XmlTimestampsOutput: + """ + This tests how timestamps are serialized, including using the default + format of date-time and various @timestampFormat trait values. + + Args: + input: + An instance of `XmlTimestampsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `XmlTimestampsOutput`. + """ + if input is None: + input = XML_TIMESTAMPS.input() + pipeline, call = self._prepare_call(input, XML_TIMESTAMPS, [], plugins) + return pipeline(call) diff --git a/codegen-output/ec2-query/src/ec2query/sync/config.py b/codegen-output/ec2-query/src/ec2query/sync/config.py new file mode 100644 index 000000000..1d3dbd878 --- /dev/null +++ b/codegen-output/ec2-query/src/ec2query/sync/config.py @@ -0,0 +1,288 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from dataclasses import dataclass, field +from typing import ( + Any, + Callable, + ClassVar, + Self, + TYPE_CHECKING, + TypeAlias, + Union, + Unpack, +) + +from smithy_aws_core.auth.sigv4 import SigV4AuthScheme +from smithy_aws_core.config import FileSystem +from smithy_aws_core.config.aws_config import AwsConfig, AwsConfigOverrides +from smithy_aws_core.config.resolvers import EndpointUriResolver +from smithy_aws_core.config.types import FieldSpec +from smithy_aws_core.endpoints.standard_regional import ( + StandardRegionalEndpointsResolver, +) +from smithy_aws_core.identity import AWSCredentialsIdentity, AWSIdentityProperties +from smithy_aws_core.protocols import Ec2QueryClientProtocol +from smithy_core.interfaces import ( + ClientProtocol, + EndpointResolver, + ProtocolConstructor, + ProtocolSettings, +) +from smithy_core.interfaces.auth import AuthScheme, AuthSchemeResolver +from smithy_core.interfaces.identity import IdentityResolver +from smithy_core.shapes import ShapeID +from smithy_http.urllib3 import Urllib3HTTPClient + +from .._private.schemas import AWS_EC2 +from ..auth import HTTPAuthSchemeResolver + + +if TYPE_CHECKING: + from smithy_core.interceptors import Interceptor + from ..models import ( + DatetimeOffsetsInput, + DatetimeOffsetsOutput, + EmptyInputAndEmptyOutputInput, + EmptyInputAndEmptyOutputOutput, + EndpointOperationInput, + EndpointOperationOutput, + EndpointWithHostLabelOperationInput, + EndpointWithHostLabelOperationOutput, + FractionalSecondsInput, + FractionalSecondsOutput, + GreetingWithErrorsInput, + GreetingWithErrorsOutput, + HostWithPathOperationInput, + HostWithPathOperationOutput, + IgnoresWrappingXmlNameInput, + IgnoresWrappingXmlNameOutput, + NestedStructuresInput, + NestedStructuresOutput, + NoInputAndOutputInput, + NoInputAndOutputOutput, + PutWithContentEncodingInput, + PutWithContentEncodingOutput, + QueryIdempotencyTokenAutoFillInput, + QueryIdempotencyTokenAutoFillOutput, + QueryListsInput, + QueryListsOutput, + QueryTimestampsInput, + QueryTimestampsOutput, + RecursiveXmlShapesInput, + RecursiveXmlShapesOutput, + SimpleInputParamsInput, + SimpleInputParamsOutput, + SimpleScalarXmlPropertiesInput, + SimpleScalarXmlPropertiesOutput, + XmlBlobsInput, + XmlBlobsOperationOutput, + XmlEmptyBlobsInput, + XmlEmptyBlobsOutput, + XmlEmptyListsInput, + XmlEmptyListsOutput, + XmlEnumsInput, + XmlEnumsOutput, + XmlIntEnumsInput, + XmlIntEnumsOutput, + XmlListsInput, + XmlListsOperationOutput, + XmlNamespacesInput, + XmlNamespacesOutput, + XmlTimestampsInput, + XmlTimestampsOutput, + ) + + _ServiceInterceptor = Union[ + Interceptor[DatetimeOffsetsInput, DatetimeOffsetsOutput, Any, Any], + Interceptor[ + EmptyInputAndEmptyOutputInput, EmptyInputAndEmptyOutputOutput, Any, Any + ], + Interceptor[EndpointOperationInput, EndpointOperationOutput, Any, Any], + Interceptor[ + EndpointWithHostLabelOperationInput, + EndpointWithHostLabelOperationOutput, + Any, + Any, + ], + Interceptor[FractionalSecondsInput, FractionalSecondsOutput, Any, Any], + Interceptor[GreetingWithErrorsInput, GreetingWithErrorsOutput, Any, Any], + Interceptor[HostWithPathOperationInput, HostWithPathOperationOutput, Any, Any], + Interceptor[ + IgnoresWrappingXmlNameInput, IgnoresWrappingXmlNameOutput, Any, Any + ], + Interceptor[NestedStructuresInput, NestedStructuresOutput, Any, Any], + Interceptor[NoInputAndOutputInput, NoInputAndOutputOutput, Any, Any], + Interceptor[ + PutWithContentEncodingInput, PutWithContentEncodingOutput, Any, Any + ], + Interceptor[ + QueryIdempotencyTokenAutoFillInput, + QueryIdempotencyTokenAutoFillOutput, + Any, + Any, + ], + Interceptor[QueryListsInput, QueryListsOutput, Any, Any], + Interceptor[QueryTimestampsInput, QueryTimestampsOutput, Any, Any], + Interceptor[RecursiveXmlShapesInput, RecursiveXmlShapesOutput, Any, Any], + Interceptor[SimpleInputParamsInput, SimpleInputParamsOutput, Any, Any], + Interceptor[ + SimpleScalarXmlPropertiesInput, SimpleScalarXmlPropertiesOutput, Any, Any + ], + Interceptor[XmlBlobsInput, XmlBlobsOperationOutput, Any, Any], + Interceptor[XmlEmptyBlobsInput, XmlEmptyBlobsOutput, Any, Any], + Interceptor[XmlEmptyListsInput, XmlEmptyListsOutput, Any, Any], + Interceptor[XmlEnumsInput, XmlEnumsOutput, Any, Any], + Interceptor[XmlIntEnumsInput, XmlIntEnumsOutput, Any, Any], + Interceptor[XmlListsInput, XmlListsOperationOutput, Any, Any], + Interceptor[XmlNamespacesInput, XmlNamespacesOutput, Any, Any], + Interceptor[XmlTimestampsInput, XmlTimestampsOutput, Any, Any], + ] +else: + _ServiceInterceptor = Any + +_PROTOCOL_SETTINGS = ProtocolSettings( + namespace="aws.protocoltests.ec2", + service_target="AwsEc2", + version="2020-01-08", + service_schema=AWS_EC2, +) + + +class _EC2ProtocolConfigOverrides(AwsConfigOverrides, total=False): + endpoint_resolver: EndpointResolver | None + protocol: ( + ClientProtocol[Any, Any] | ProtocolConstructor[ClientProtocol[Any, Any]] | None + ) + auth_schemes: dict[ShapeID, AuthScheme[Any, Any, Any, Any]] | None + auth_scheme_resolver: AuthSchemeResolver | None + + +@dataclass(kw_only=True, repr=False, init=False) +class EC2ProtocolConfig(AwsConfig): + """EC2 Protocol configuration.""" + + endpoint_resolver: EndpointResolver | None = None + """ + The endpoint resolver used to resolve the final endpoint per-operation + based on the configuration. + """ + + protocol: ClientProtocol[Any, Any] | None = None + """ + Pass a protocol class reference from smithy_aws_core.protocols to select + the protocol, e.g. protocol=AwsJson10ClientProtocol. For custom + protocols a protocol instance may also be passed. + """ + + interceptors: list[_ServiceInterceptor] = field(default_factory=lambda: []) + """ + The list of interceptors, which are hooks that are called during the + execution of a request. + """ + + auth_schemes: dict[ShapeID, AuthScheme[Any, Any, Any, Any]] | None = None + """A map of auth scheme ids to auth schemes.""" + + auth_scheme_resolver: AuthSchemeResolver | None = None + """ + An auth scheme resolver that determines the auth scheme for each + operation. + """ + + aws_credentials_identity_resolver: ( + IdentityResolver[AWSCredentialsIdentity, AWSIdentityProperties] | None + ) = None + """Resolves AWS Credentials. Required for operations that use Sigv4 Auth.""" + + region: str | None = None + """ + The AWS region to connect to. The configured region is used to determine + the service endpoint. + """ + + aws_access_key_id: str | None = None + """The identifier for a secret access key.""" + + aws_secret_access_key: str | None = None + """A secret access key that can be used to sign requests.""" + + aws_session_token: str | None = None + """The session token used with temporary AWS credentials.""" + + user_agent_extra: str | None = None + """Additional suffix to be added to the User-Agent header.""" + + sdk_ua_app_id: str | None = None + """ + A unique and opaque application ID that is appended to the User-Agent + header. + """ + + _FIELDS: ClassVar[dict[str, FieldSpec]] = { + "aws_credentials_identity_resolver": FieldSpec(default=None), + "region": FieldSpec(default=None), + "aws_access_key_id": FieldSpec(default=None), + "aws_secret_access_key": FieldSpec(default=None), + "aws_session_token": FieldSpec(default=None), + "user_agent_extra": FieldSpec(default=None), + "sdk_ua_app_id": FieldSpec(default=None), + **AwsConfig._FIELDS, + "endpoint_uri": FieldSpec( + default=None, resolver=EndpointUriResolver("ec2_protocol") + ), + "endpoint_resolver": FieldSpec( + default_factory=lambda: StandardRegionalEndpointsResolver( + endpoint_prefix="awsec2" + ) + ), + "protocol": FieldSpec( + default_factory=lambda: Ec2QueryClientProtocol(_PROTOCOL_SETTINGS), + converter=lambda p: p(_PROTOCOL_SETTINGS) if isinstance(p, type) else p, + ), + "auth_schemes": FieldSpec( + default_factory=lambda: { + ShapeID("aws.auth#sigv4"): SigV4AuthScheme(service="ec2query") + } + ), + "auth_scheme_resolver": FieldSpec(default_factory=HTTPAuthSchemeResolver), + "transport": FieldSpec(default_factory=lambda: Urllib3HTTPClient()), + } + + def set_auth_scheme(self, scheme: AuthScheme[Any, Any, Any, Any]) -> None: + """ + Set an auth scheme implementation using its scheme ID. :param scheme: + The auth scheme to add or replace. + """ + auth_schemes = dict(self.auth_schemes or {}) + auth_schemes[scheme.scheme_id] = scheme + self.auth_schemes = auth_schemes + + @classmethod + def resolve( # pyright: ignore[reportIncompatibleMethodOverride] + cls, + *, + profile: str | None = None, + fs: FileSystem | None = None, + config_file_path: str | None = None, + credentials_file_path: str | None = None, + **overrides: Unpack[_EC2ProtocolConfigOverrides], + ) -> Self: + """ + Resolve config from environment, config files, defaults, and explicit + overrides. + """ + return cls._resolve( + profile=profile, + fs=fs, + config_file_path=config_file_path, + credentials_file_path=credentials_file_path, + overrides=overrides, + ) + + +Plugin: TypeAlias = Callable[[EC2ProtocolConfig], None] +""" +A callable that customizes a client configuration. Service-level plugins +are applied once to the base configuration inherited by every operation. +Operation-level plugins apply only to a single operation invocation. +""" diff --git a/codegen-output/ec2-query/src/ec2query/sync/user_agent.py b/codegen-output/ec2-query/src/ec2query/sync/user_agent.py new file mode 100644 index 000000000..4ff3f02a3 --- /dev/null +++ b/codegen-output/ec2-query/src/ec2query/sync/user_agent.py @@ -0,0 +1,17 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from smithy_aws_core.interceptors.user_agent import UserAgentInterceptor + +from .. import __version__ +from .config import EC2ProtocolConfig + + +def aws_user_agent_plugin(config: EC2ProtocolConfig): + config.interceptors.append( + UserAgentInterceptor( + ua_suffix=config.user_agent_extra, + ua_app_id=config.sdk_ua_app_id, + sdk_version=__version__, + service_id="EC2_Protocol", + ) + ) diff --git a/codegen-output/rest-json-1/pyproject.toml b/codegen-output/rest-json-1/pyproject.toml index 24151978a..408d9b654 100644 --- a/codegen-output/rest-json-1/pyproject.toml +++ b/codegen-output/rest-json-1/pyproject.toml @@ -26,7 +26,7 @@ classifiers = [ dependencies = [ "smithy_aws_core[eventstream, json]~=0.12.0", "smithy_core~=0.9.0", - "smithy_http[aiohttp]~=0.6.0" + "smithy_http[aiohttp, urllib3]~=0.6.0" ] [project.optional-dependencies] diff --git a/codegen-output/rest-json-1/src/restjson/__init__.py b/codegen-output/rest-json-1/src/restjson/__init__.py index 30f6d44e9..3bc8b17a3 100644 --- a/codegen-output/rest-json-1/src/restjson/__init__.py +++ b/codegen-output/rest-json-1/src/restjson/__init__.py @@ -1,3 +1,20 @@ # Code generated by smithy-python-codegen DO NOT EDIT. +from typing import Any, TYPE_CHECKING + + +if TYPE_CHECKING: + from .sync import RestJsonProtocolClient, RestJsonProtocolConfig + +__all__ = ["RestJsonProtocolClient", "RestJsonProtocolConfig"] + + +def __getattr__(name: str) -> Any: + if name in __all__: + from . import sync + + return getattr(sync, name) + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + __version__: str = "0.0.1" diff --git a/codegen-output/rest-json-1/src/restjson/sync/__init__.py b/codegen-output/rest-json-1/src/restjson/sync/__init__.py new file mode 100644 index 000000000..112363110 --- /dev/null +++ b/codegen-output/rest-json-1/src/restjson/sync/__init__.py @@ -0,0 +1,6 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from .client import RestJsonProtocolClient +from .config import RestJsonProtocolConfig, Plugin + +__all__ = ["RestJsonProtocolClient", "RestJsonProtocolConfig", "Plugin"] diff --git a/codegen-output/rest-json-1/src/restjson/sync/client.py b/codegen-output/rest-json-1/src/restjson/sync/client.py new file mode 100644 index 000000000..21d7ce182 --- /dev/null +++ b/codegen-output/rest-json-1/src/restjson/sync/client.py @@ -0,0 +1,3203 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from __future__ import annotations + +from copy import deepcopy +import logging +from typing import Any, cast + +from smithy_aws_core.config import ConfigSource +from smithy_aws_core.identity import AWSCredentialsIdentity +from smithy_aws_core.identity.chain import IdentityChain +from smithy_core.client import Client, ClientCall, RequestPipeline +from smithy_core.deserializers import DeserializeableShape +from smithy_core.exceptions import ExpectationNotMetError +from smithy_core.schemas import APIOperation +from smithy_core.serializers import SerializeableShape +from smithy_http.interfaces import HTTPClient +from smithy_http.plugins import user_agent_plugin + +from ..models import ( + ALL_QUERY_STRING_TYPES, + CONSTANT_AND_VARIABLE_QUERY_STRING, + CONSTANT_QUERY_STRING, + CONTENT_TYPE_PARAMETERS, + DATETIME_OFFSETS, + DOCUMENT_TYPE, + DOCUMENT_TYPE_AS_MAP_VALUE, + DOCUMENT_TYPE_AS_PAYLOAD, + EMPTY_INPUT_AND_EMPTY_OUTPUT, + ENDPOINT_OPERATION, + ENDPOINT_WITH_HOST_LABEL_OPERATION, + FRACTIONAL_SECONDS, + GREETING_WITH_ERRORS, + HOST_WITH_PATH_OPERATION, + HTTP_CHECKSUM_REQUIRED, + HTTP_EMPTY_PREFIX_HEADERS, + HTTP_ENUM_PAYLOAD, + HTTP_PAYLOAD_TRAITS, + HTTP_PAYLOAD_TRAITS_WITH_MEDIA_TYPE, + HTTP_PAYLOAD_WITH_STRUCTURE, + HTTP_PAYLOAD_WITH_UNION, + HTTP_PREFIX_HEADERS, + HTTP_PREFIX_HEADERS_IN_RESPONSE, + HTTP_QUERY_PARAMS_ONLY_OPERATION, + HTTP_REQUEST_WITH_FLOAT_LABELS, + HTTP_REQUEST_WITH_GREEDY_LABEL_IN_PATH, + HTTP_REQUEST_WITH_LABELS, + HTTP_REQUEST_WITH_LABELS_AND_TIMESTAMP_FORMAT, + HTTP_REQUEST_WITH_REGEX_LITERAL, + HTTP_RESPONSE_CODE, + HTTP_STRING_PAYLOAD, + IGNORE_QUERY_PARAMS_IN_RESPONSE, + INPUT_AND_OUTPUT_WITH_HEADERS, + JSON_BLOBS, + JSON_ENUMS, + JSON_INT_ENUMS, + JSON_LISTS, + JSON_MAPS, + JSON_TIMESTAMPS, + JSON_UNIONS, + MALFORMED_ACCEPT_WITH_BODY, + MALFORMED_ACCEPT_WITH_GENERIC_STRING, + MALFORMED_ACCEPT_WITH_PAYLOAD, + MALFORMED_BLOB, + MALFORMED_BOOLEAN, + MALFORMED_BYTE, + MALFORMED_CONTENT_TYPE_WITHOUT_BODY, + MALFORMED_CONTENT_TYPE_WITHOUT_BODY_EMPTY_INPUT, + MALFORMED_CONTENT_TYPE_WITH_BODY, + MALFORMED_CONTENT_TYPE_WITH_GENERIC_STRING, + MALFORMED_CONTENT_TYPE_WITH_PAYLOAD, + MALFORMED_DOUBLE, + MALFORMED_FLOAT, + MALFORMED_INTEGER, + MALFORMED_LIST, + MALFORMED_LONG, + MALFORMED_MAP, + MALFORMED_REQUEST_BODY, + MALFORMED_SHORT, + MALFORMED_STRING, + MALFORMED_TIMESTAMP_BODY_DATE_TIME, + MALFORMED_TIMESTAMP_BODY_DEFAULT, + MALFORMED_TIMESTAMP_BODY_HTTP_DATE, + MALFORMED_TIMESTAMP_HEADER_DATE_TIME, + MALFORMED_TIMESTAMP_HEADER_DEFAULT, + MALFORMED_TIMESTAMP_HEADER_EPOCH, + MALFORMED_TIMESTAMP_PATH_DEFAULT, + MALFORMED_TIMESTAMP_PATH_EPOCH, + MALFORMED_TIMESTAMP_PATH_HTTP_DATE, + MALFORMED_TIMESTAMP_QUERY_DEFAULT, + MALFORMED_TIMESTAMP_QUERY_EPOCH, + MALFORMED_TIMESTAMP_QUERY_HTTP_DATE, + MALFORMED_UNION, + MEDIA_TYPE_HEADER, + NO_INPUT_AND_NO_OUTPUT, + NO_INPUT_AND_OUTPUT, + NULL_AND_EMPTY_HEADERS_CLIENT, + NULL_AND_EMPTY_HEADERS_SERVER, + OMITS_NULL_SERIALIZES_EMPTY_STRING, + OMITS_SERIALIZING_EMPTY_LISTS, + OPERATION_WITH_DEFAULTS, + OPERATION_WITH_NESTED_STRUCTURE, + POST_PLAYER_ACTION, + POST_UNION_WITH_JSON_NAME, + PUT_WITH_CONTENT_ENCODING, + QUERY_IDEMPOTENCY_TOKEN_AUTO_FILL, + QUERY_PARAMS_AS_STRING_LIST_MAP, + QUERY_PRECEDENCE, + RECURSIVE_SHAPES, + RESPONSE_CODE_HTTP_FALLBACK, + RESPONSE_CODE_REQUIRED, + SIMPLE_SCALAR_PROPERTIES, + SPARSE_JSON_LISTS, + SPARSE_JSON_MAPS, + STREAMING_TRAITS, + STREAMING_TRAITS_REQUIRE_LENGTH, + STREAMING_TRAITS_WITH_MEDIA_TYPE, + TEST_BODY_STRUCTURE, + TEST_GET_NO_INPUT_NO_PAYLOAD, + TEST_GET_NO_PAYLOAD, + TEST_PAYLOAD_BLOB, + TEST_PAYLOAD_STRUCTURE, + TEST_POST_NO_INPUT_NO_PAYLOAD, + TEST_POST_NO_PAYLOAD, + TIMESTAMP_FORMAT_HEADERS, + UNIT_INPUT_AND_OUTPUT, +) +from .config import Plugin, RestJsonProtocolConfig +from .user_agent import aws_user_agent_plugin + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from ..models import ( + AllQueryStringTypesInput, + AllQueryStringTypesOutput, + ConstantAndVariableQueryStringInput, + ConstantAndVariableQueryStringOutput, + ConstantQueryStringInput, + ConstantQueryStringOutput, + ContentTypeParametersInput, + ContentTypeParametersOutput, + DatetimeOffsetsInput, + DatetimeOffsetsOutput, + DocumentTypeAsMapValueInput, + DocumentTypeAsMapValueOutput, + DocumentTypeAsPayloadInput, + DocumentTypeAsPayloadOutput, + DocumentTypeInput, + DocumentTypeOutput, + EmptyInputAndEmptyOutputInput, + EmptyInputAndEmptyOutputOutput, + EndpointOperationInput, + EndpointOperationOutput, + EndpointWithHostLabelOperationInput, + EndpointWithHostLabelOperationOutput, + FractionalSecondsInput, + FractionalSecondsOutput, + GreetingWithErrorsInput, + GreetingWithErrorsOutput, + HostWithPathOperationInput, + HostWithPathOperationOutput, + HttpChecksumRequiredInput, + HttpChecksumRequiredOutput, + HttpEmptyPrefixHeadersInput, + HttpEmptyPrefixHeadersOutput, + HttpEnumPayloadInput, + HttpEnumPayloadOutput, + HttpPayloadTraitsInput, + HttpPayloadTraitsOutput, + HttpPayloadTraitsWithMediaTypeInput, + HttpPayloadTraitsWithMediaTypeOutput, + HttpPayloadWithStructureInput, + HttpPayloadWithStructureOutput, + HttpPayloadWithUnionInput, + HttpPayloadWithUnionOutput, + HttpPrefixHeadersInResponseInput, + HttpPrefixHeadersInResponseOutput, + HttpPrefixHeadersInput, + HttpPrefixHeadersOutput, + HttpQueryParamsOnlyOperationInput, + HttpQueryParamsOnlyOperationOutput, + HttpRequestWithFloatLabelsInput, + HttpRequestWithFloatLabelsOutput, + HttpRequestWithGreedyLabelInPathInput, + HttpRequestWithGreedyLabelInPathOutput, + HttpRequestWithLabelsAndTimestampFormatInput, + HttpRequestWithLabelsAndTimestampFormatOutput, + HttpRequestWithLabelsInput, + HttpRequestWithLabelsOutput, + HttpRequestWithRegexLiteralInput, + HttpRequestWithRegexLiteralOutput, + HttpResponseCodeInput, + HttpResponseCodeOutput, + HttpStringPayloadInput, + HttpStringPayloadOutput, + IgnoreQueryParamsInResponseInput, + IgnoreQueryParamsInResponseOutput, + InputAndOutputWithHeadersInput, + InputAndOutputWithHeadersOutput, + JsonBlobsInput, + JsonBlobsOutput, + JsonEnumsInput, + JsonEnumsOutput, + JsonIntEnumsInput, + JsonIntEnumsOutput, + JsonListsInput, + JsonListsOutput, + JsonMapsInput, + JsonMapsOutput, + JsonTimestampsInput, + JsonTimestampsOutput, + JsonUnionsInput, + JsonUnionsOutput, + MalformedAcceptWithBodyInput, + MalformedAcceptWithBodyOutput, + MalformedAcceptWithGenericStringInput, + MalformedAcceptWithGenericStringOutput, + MalformedAcceptWithPayloadInput, + MalformedAcceptWithPayloadOutput, + MalformedBlobInput, + MalformedBlobOutput, + MalformedBooleanInput, + MalformedBooleanOutput, + MalformedByteInput, + MalformedByteOutput, + MalformedContentTypeWithBodyInput, + MalformedContentTypeWithBodyOutput, + MalformedContentTypeWithGenericStringInput, + MalformedContentTypeWithGenericStringOutput, + MalformedContentTypeWithPayloadInput, + MalformedContentTypeWithPayloadOutput, + MalformedContentTypeWithoutBodyEmptyInputInput, + MalformedContentTypeWithoutBodyEmptyInputOutput, + MalformedContentTypeWithoutBodyInput, + MalformedContentTypeWithoutBodyOutput, + MalformedDoubleInput, + MalformedDoubleOutput, + MalformedFloatInput, + MalformedFloatOutput, + MalformedIntegerInput, + MalformedIntegerOutput, + MalformedListInput, + MalformedListOutput, + MalformedLongInput, + MalformedLongOutput, + MalformedMapInput, + MalformedMapOutput, + MalformedRequestBodyInput, + MalformedRequestBodyOutput, + MalformedShortInput, + MalformedShortOutput, + MalformedStringInput, + MalformedStringOutput, + MalformedTimestampBodyDateTimeInput, + MalformedTimestampBodyDateTimeOutput, + MalformedTimestampBodyDefaultInput, + MalformedTimestampBodyDefaultOutput, + MalformedTimestampBodyHttpDateInput, + MalformedTimestampBodyHttpDateOutput, + MalformedTimestampHeaderDateTimeInput, + MalformedTimestampHeaderDateTimeOutput, + MalformedTimestampHeaderDefaultInput, + MalformedTimestampHeaderDefaultOutput, + MalformedTimestampHeaderEpochInput, + MalformedTimestampHeaderEpochOutput, + MalformedTimestampPathDefaultInput, + MalformedTimestampPathDefaultOutput, + MalformedTimestampPathEpochInput, + MalformedTimestampPathEpochOutput, + MalformedTimestampPathHttpDateInput, + MalformedTimestampPathHttpDateOutput, + MalformedTimestampQueryDefaultInput, + MalformedTimestampQueryDefaultOutput, + MalformedTimestampQueryEpochInput, + MalformedTimestampQueryEpochOutput, + MalformedTimestampQueryHttpDateInput, + MalformedTimestampQueryHttpDateOutput, + MalformedUnionInput, + MalformedUnionOutput, + MediaTypeHeaderInput, + MediaTypeHeaderOutput, + NoInputAndNoOutputInput, + NoInputAndNoOutputOutput, + NoInputAndOutputInput, + NoInputAndOutputOutput, + NullAndEmptyHeadersClientInput, + NullAndEmptyHeadersClientOutput, + NullAndEmptyHeadersServerInput, + NullAndEmptyHeadersServerOutput, + OmitsNullSerializesEmptyStringInput, + OmitsNullSerializesEmptyStringOutput, + OmitsSerializingEmptyListsInput, + OmitsSerializingEmptyListsOutput, + OperationWithDefaultsInput, + OperationWithDefaultsOutput, + OperationWithNestedStructureInput, + OperationWithNestedStructureOutput, + PostPlayerActionInput, + PostPlayerActionOutput, + PostUnionWithJsonNameInput, + PostUnionWithJsonNameOutput, + PutWithContentEncodingInput, + PutWithContentEncodingOutput, + QueryIdempotencyTokenAutoFillInput, + QueryIdempotencyTokenAutoFillOutput, + QueryParamsAsStringListMapInput, + QueryParamsAsStringListMapOutput, + QueryPrecedenceInput, + QueryPrecedenceOutput, + RecursiveShapesInput, + RecursiveShapesOutput, + ResponseCodeHttpFallbackInput, + ResponseCodeHttpFallbackOutput, + ResponseCodeRequiredInput, + ResponseCodeRequiredOutput, + SimpleScalarPropertiesInput, + SimpleScalarPropertiesOutput, + SparseJsonListsInput, + SparseJsonListsOutput, + SparseJsonMapsInput, + SparseJsonMapsOutput, + StreamingTraitsInput, + StreamingTraitsOutput, + StreamingTraitsRequireLengthInput, + StreamingTraitsRequireLengthOutput, + StreamingTraitsWithMediaTypeInput, + StreamingTraitsWithMediaTypeOutput, + TestBodyStructureInput, + TestBodyStructureOutput, + TestGetNoInputNoPayloadInput, + TestGetNoInputNoPayloadOutput, + TestGetNoPayloadInput, + TestGetNoPayloadOutput, + TestPayloadBlobInput, + TestPayloadBlobOutput, + TestPayloadStructureInput, + TestPayloadStructureOutput, + TestPostNoInputNoPayloadInput, + TestPostNoInputNoPayloadOutput, + TestPostNoPayloadInput, + TestPostNoPayloadOutput, + TimestampFormatHeadersInput, + TimestampFormatHeadersOutput, + UnitInputAndOutputInput, + UnitInputAndOutputOutput, + ) + + +logger = logging.getLogger(__name__) + + +class RestJsonProtocolClient(Client): + """Client for RestJson""" + + def __init__( + self, + config: RestJsonProtocolConfig | None = None, + plugins: list[Plugin] | None = None, + ): + """ + Constructor for `RestJsonProtocolClient`. + + Args: + config: + Optional configuration for the client. Here you can set things like + the endpoint for HTTP services or auth credentials. + plugins: + A list of callables applied once to the client's base configuration. + Their changes are inherited by every operation invocation. + """ + super().__init__() + self._config = config + self._plugins = plugins + self._client_plugins: list[Plugin] = [aws_user_agent_plugin, user_agent_plugin] + + def _ensure_setup(self) -> RestJsonProtocolConfig: + if not self._setup_done: + with self._derive_lock: + if self._closed: + raise RuntimeError( + "Cannot invoke an operation on a client that has been closed." + ) + if not self._setup_done: + if self._config is None: + config = RestJsonProtocolConfig.resolve() + else: + # Copy so plugins don't mutate the caller's config. + config = deepcopy(self._config) + for plugin in self._client_plugins: + plugin(config) + if self._plugins: + for plugin in self._plugins: + plugin(config) + # Publish state only after setup fully succeeds, so a + # failed _post_setup leaves the caller's config untouched + # (no re-applied plugins) and the transport closeable. + self._post_setup(config) + self._transport = config.transport + self._config = config + self._setup_done = True + assert self._config is not None + return self._config + + def _post_setup(self, config: RestJsonProtocolConfig) -> None: + if config.aws_credentials_identity_resolver is None: + config_context = config.resolution_context() + config_file = None + profile_name = None + if config_context is not None: + config_file = config_context.parsed_profiles() + if config_context.profile_source is ConfigSource.OVERRIDE: + profile_name = config_context.profile_name + config.aws_credentials_identity_resolver = IdentityChain.create( + AWSCredentialsIdentity, + config_file=config_file, + profile_name=profile_name, + region_override=config.region, + http_client=cast(HTTPClient | None, config.transport), + ) + + def _prepare_call[I: SerializeableShape, O: DeserializeableShape]( + self, + input: I, + operation: APIOperation[I, O], + default_plugins: list[Plugin], + plugins: list[Plugin] | None, + ) -> tuple[RequestPipeline[Any, Any], ClientCall[I, O]]: + if self._closed: + raise RuntimeError( + "Cannot invoke an operation on a client that has been closed." + ) + config = self._ensure_setup() + if default_plugins or plugins: + # Keep operation-plugin mutations scoped to this call. + config = deepcopy(config) + for plugin in default_plugins: + plugin(config) + if plugins: + for plugin in plugins: + plugin(config) + if ( + config.protocol is None + or config.transport is None + or config.endpoint_resolver is None + or config.auth_scheme_resolver is None + or config.auth_schemes is None + ): + raise ExpectationNotMetError( + "protocol, transport, endpoint_resolver, auth_scheme_resolver," + " and auth_schemes MUST be set on the config to make calls." + ) + retry_strategy = self._retry_strategy_resolver.resolve_retry_strategy( + retry_strategy=config.retry_strategy, + retry_mode=config.retry_mode, + max_attempts=config.max_attempts, + ) + return self._build_call( + input, operation, config=config, retry_strategy=retry_strategy + ) + + def all_query_string_types( + self, + input: AllQueryStringTypesInput | None = None, + plugins: list[Plugin] | None = None, + ) -> AllQueryStringTypesOutput: + """ + This example uses all query string types. + + Args: + input: + An instance of `AllQueryStringTypesInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `AllQueryStringTypesOutput`. + """ + if input is None: + input = ALL_QUERY_STRING_TYPES.input() + pipeline, call = self._prepare_call(input, ALL_QUERY_STRING_TYPES, [], plugins) + return pipeline(call) + + def constant_and_variable_query_string( + self, + input: ConstantAndVariableQueryStringInput | None = None, + plugins: list[Plugin] | None = None, + ) -> ConstantAndVariableQueryStringOutput: + """ + This example uses fixed query string params and variable query string + params. The fixed query string parameters and variable parameters must + both be serialized (implementations may need to merge them together). + + Args: + input: + An instance of `ConstantAndVariableQueryStringInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `ConstantAndVariableQueryStringOutput`. + """ + if input is None: + input = CONSTANT_AND_VARIABLE_QUERY_STRING.input() + pipeline, call = self._prepare_call( + input, CONSTANT_AND_VARIABLE_QUERY_STRING, [], plugins + ) + return pipeline(call) + + def constant_query_string( + self, input: ConstantQueryStringInput, plugins: list[Plugin] | None = None + ) -> ConstantQueryStringOutput: + """ + This example uses a constant query string parameters and a label. This + simply tests that labels and query string parameters are compatible. The + fixed query string parameter named \"hello\" should in no way conflict + with the label, `{hello}`. + + Args: + input: + An instance of `ConstantQueryStringInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `ConstantQueryStringOutput`. + """ + pipeline, call = self._prepare_call(input, CONSTANT_QUERY_STRING, [], plugins) + return pipeline(call) + + def content_type_parameters( + self, + input: ContentTypeParametersInput | None = None, + plugins: list[Plugin] | None = None, + ) -> ContentTypeParametersOutput: + """ + The example tests how servers must support requests containing a + `Content-Type` header with parameters. + + Args: + input: + An instance of `ContentTypeParametersInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `ContentTypeParametersOutput`. + """ + if input is None: + input = CONTENT_TYPE_PARAMETERS.input() + pipeline, call = self._prepare_call(input, CONTENT_TYPE_PARAMETERS, [], plugins) + return pipeline(call) + + def datetime_offsets( + self, + input: DatetimeOffsetsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> DatetimeOffsetsOutput: + """ + Invokes the DatetimeOffsets operation. + + Args: + input: + An instance of `DatetimeOffsetsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `DatetimeOffsetsOutput`. + """ + if input is None: + input = DATETIME_OFFSETS.input() + pipeline, call = self._prepare_call(input, DATETIME_OFFSETS, [], plugins) + return pipeline(call) + + def document_type( + self, + input: DocumentTypeInput | None = None, + plugins: list[Plugin] | None = None, + ) -> DocumentTypeOutput: + """ + This example serializes a document as part of the payload. + + Args: + input: + An instance of `DocumentTypeInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `DocumentTypeOutput`. + """ + if input is None: + input = DOCUMENT_TYPE.input() + pipeline, call = self._prepare_call(input, DOCUMENT_TYPE, [], plugins) + return pipeline(call) + + def document_type_as_map_value( + self, + input: DocumentTypeAsMapValueInput | None = None, + plugins: list[Plugin] | None = None, + ) -> DocumentTypeAsMapValueOutput: + """ + This example serializes documents as the value of maps. + + Args: + input: + An instance of `DocumentTypeAsMapValueInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `DocumentTypeAsMapValueOutput`. + """ + if input is None: + input = DOCUMENT_TYPE_AS_MAP_VALUE.input() + pipeline, call = self._prepare_call( + input, DOCUMENT_TYPE_AS_MAP_VALUE, [], plugins + ) + return pipeline(call) + + def document_type_as_payload( + self, + input: DocumentTypeAsPayloadInput | None = None, + plugins: list[Plugin] | None = None, + ) -> DocumentTypeAsPayloadOutput: + """ + This example serializes a document as the entire HTTP payload. + + Args: + input: + An instance of `DocumentTypeAsPayloadInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `DocumentTypeAsPayloadOutput`. + """ + if input is None: + input = DOCUMENT_TYPE_AS_PAYLOAD.input() + pipeline, call = self._prepare_call( + input, DOCUMENT_TYPE_AS_PAYLOAD, [], plugins + ) + return pipeline(call) + + def empty_input_and_empty_output( + self, + input: EmptyInputAndEmptyOutputInput | None = None, + plugins: list[Plugin] | None = None, + ) -> EmptyInputAndEmptyOutputOutput: + """ + The example tests how requests and responses are serialized when + there's no request or response payload because the operation has an + empty input and empty output structure that reuses the same shape. While + this should be rare, code generators must support this. + + Args: + input: + An instance of `EmptyInputAndEmptyOutputInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `EmptyInputAndEmptyOutputOutput`. + """ + if input is None: + input = EMPTY_INPUT_AND_EMPTY_OUTPUT.input() + pipeline, call = self._prepare_call( + input, EMPTY_INPUT_AND_EMPTY_OUTPUT, [], plugins + ) + return pipeline(call) + + def endpoint_operation( + self, + input: EndpointOperationInput | None = None, + plugins: list[Plugin] | None = None, + ) -> EndpointOperationOutput: + """ + Invokes the EndpointOperation operation. + + Args: + input: + An instance of `EndpointOperationInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `EndpointOperationOutput`. + """ + if input is None: + input = ENDPOINT_OPERATION.input() + pipeline, call = self._prepare_call(input, ENDPOINT_OPERATION, [], plugins) + return pipeline(call) + + def endpoint_with_host_label_operation( + self, + input: EndpointWithHostLabelOperationInput, + plugins: list[Plugin] | None = None, + ) -> EndpointWithHostLabelOperationOutput: + """ + Invokes the EndpointWithHostLabelOperation operation. + + Args: + input: + An instance of `EndpointWithHostLabelOperationInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `EndpointWithHostLabelOperationOutput`. + """ + pipeline, call = self._prepare_call( + input, ENDPOINT_WITH_HOST_LABEL_OPERATION, [], plugins + ) + return pipeline(call) + + def fractional_seconds( + self, + input: FractionalSecondsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> FractionalSecondsOutput: + """ + Invokes the FractionalSeconds operation. + + Args: + input: + An instance of `FractionalSecondsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `FractionalSecondsOutput`. + """ + if input is None: + input = FRACTIONAL_SECONDS.input() + pipeline, call = self._prepare_call(input, FRACTIONAL_SECONDS, [], plugins) + return pipeline(call) + + def greeting_with_errors( + self, + input: GreetingWithErrorsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> GreetingWithErrorsOutput: + """ + This operation has four possible return values: 1. A successful response + in the form of GreetingWithErrorsOutput 2. An InvalidGreeting error. 3. + A BadRequest error. 4. A FooError. Implementations must be able to + successfully take a response and properly (de)serialize successful and + error responses based on the the presence of the + + Args: + input: + An instance of `GreetingWithErrorsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `GreetingWithErrorsOutput`. + """ + if input is None: + input = GREETING_WITH_ERRORS.input() + pipeline, call = self._prepare_call(input, GREETING_WITH_ERRORS, [], plugins) + return pipeline(call) + + def host_with_path_operation( + self, + input: HostWithPathOperationInput | None = None, + plugins: list[Plugin] | None = None, + ) -> HostWithPathOperationOutput: + """ + Invokes the HostWithPathOperation operation. + + Args: + input: + An instance of `HostWithPathOperationInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `HostWithPathOperationOutput`. + """ + if input is None: + input = HOST_WITH_PATH_OPERATION.input() + pipeline, call = self._prepare_call( + input, HOST_WITH_PATH_OPERATION, [], plugins + ) + return pipeline(call) + + def http_checksum_required( + self, + input: HttpChecksumRequiredInput | None = None, + plugins: list[Plugin] | None = None, + ) -> HttpChecksumRequiredOutput: + """ + This example tests httpChecksumRequired trait + + Args: + input: + An instance of `HttpChecksumRequiredInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `HttpChecksumRequiredOutput`. + """ + if input is None: + input = HTTP_CHECKSUM_REQUIRED.input() + pipeline, call = self._prepare_call(input, HTTP_CHECKSUM_REQUIRED, [], plugins) + return pipeline(call) + + def http_empty_prefix_headers( + self, + input: HttpEmptyPrefixHeadersInput | None = None, + plugins: list[Plugin] | None = None, + ) -> HttpEmptyPrefixHeadersOutput: + """ + Clients that perform this test extract all headers from the response. + + Args: + input: + An instance of `HttpEmptyPrefixHeadersInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `HttpEmptyPrefixHeadersOutput`. + """ + if input is None: + input = HTTP_EMPTY_PREFIX_HEADERS.input() + pipeline, call = self._prepare_call( + input, HTTP_EMPTY_PREFIX_HEADERS, [], plugins + ) + return pipeline(call) + + def http_enum_payload( + self, + input: HttpEnumPayloadInput | None = None, + plugins: list[Plugin] | None = None, + ) -> HttpEnumPayloadOutput: + """ + Invokes the HttpEnumPayload operation. + + Args: + input: + An instance of `HttpEnumPayloadInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `HttpEnumPayloadOutput`. + """ + if input is None: + input = HTTP_ENUM_PAYLOAD.input() + pipeline, call = self._prepare_call(input, HTTP_ENUM_PAYLOAD, [], plugins) + return pipeline(call) + + def http_payload_traits( + self, + input: HttpPayloadTraitsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> HttpPayloadTraitsOutput: + """ + This example serializes a blob shape in the payload. In this example, no + JSON document is synthesized because the payload is not a structure or a + union type. + + Args: + input: + An instance of `HttpPayloadTraitsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `HttpPayloadTraitsOutput`. + """ + if input is None: + input = HTTP_PAYLOAD_TRAITS.input() + pipeline, call = self._prepare_call(input, HTTP_PAYLOAD_TRAITS, [], plugins) + return pipeline(call) + + def http_payload_traits_with_media_type( + self, + input: HttpPayloadTraitsWithMediaTypeInput | None = None, + plugins: list[Plugin] | None = None, + ) -> HttpPayloadTraitsWithMediaTypeOutput: + """ + This example uses a `@mediaType` trait on the payload to force a + custom content-type to be serialized. + + Args: + input: + An instance of `HttpPayloadTraitsWithMediaTypeInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `HttpPayloadTraitsWithMediaTypeOutput`. + """ + if input is None: + input = HTTP_PAYLOAD_TRAITS_WITH_MEDIA_TYPE.input() + pipeline, call = self._prepare_call( + input, HTTP_PAYLOAD_TRAITS_WITH_MEDIA_TYPE, [], plugins + ) + return pipeline(call) + + def http_payload_with_structure( + self, + input: HttpPayloadWithStructureInput | None = None, + plugins: list[Plugin] | None = None, + ) -> HttpPayloadWithStructureOutput: + """ + This example serializes a structure in the payload. Note that + serializing a structure changes the wrapper element name to match the + targeted structure. + + Args: + input: + An instance of `HttpPayloadWithStructureInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `HttpPayloadWithStructureOutput`. + """ + if input is None: + input = HTTP_PAYLOAD_WITH_STRUCTURE.input() + pipeline, call = self._prepare_call( + input, HTTP_PAYLOAD_WITH_STRUCTURE, [], plugins + ) + return pipeline(call) + + def http_payload_with_union( + self, + input: HttpPayloadWithUnionInput | None = None, + plugins: list[Plugin] | None = None, + ) -> HttpPayloadWithUnionOutput: + """ + This example serializes a union in the payload. + + Args: + input: + An instance of `HttpPayloadWithUnionInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `HttpPayloadWithUnionOutput`. + """ + if input is None: + input = HTTP_PAYLOAD_WITH_UNION.input() + pipeline, call = self._prepare_call(input, HTTP_PAYLOAD_WITH_UNION, [], plugins) + return pipeline(call) + + def http_prefix_headers( + self, + input: HttpPrefixHeadersInput | None = None, + plugins: list[Plugin] | None = None, + ) -> HttpPrefixHeadersOutput: + """ + This examples adds headers to the input of a request and response by + prefix. + + Args: + input: + An instance of `HttpPrefixHeadersInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `HttpPrefixHeadersOutput`. + """ + if input is None: + input = HTTP_PREFIX_HEADERS.input() + pipeline, call = self._prepare_call(input, HTTP_PREFIX_HEADERS, [], plugins) + return pipeline(call) + + def http_prefix_headers_in_response( + self, + input: HttpPrefixHeadersInResponseInput | None = None, + plugins: list[Plugin] | None = None, + ) -> HttpPrefixHeadersInResponseOutput: + """ + Clients that perform this test extract all headers from the response. + + Args: + input: + An instance of `HttpPrefixHeadersInResponseInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `HttpPrefixHeadersInResponseOutput`. + """ + if input is None: + input = HTTP_PREFIX_HEADERS_IN_RESPONSE.input() + pipeline, call = self._prepare_call( + input, HTTP_PREFIX_HEADERS_IN_RESPONSE, [], plugins + ) + return pipeline(call) + + def http_query_params_only_operation( + self, + input: HttpQueryParamsOnlyOperationInput | None = None, + plugins: list[Plugin] | None = None, + ) -> HttpQueryParamsOnlyOperationOutput: + """ + This example tests httpQueryParams when no other query parameters exist. + + Args: + input: + An instance of `HttpQueryParamsOnlyOperationInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `HttpQueryParamsOnlyOperationOutput`. + """ + if input is None: + input = HTTP_QUERY_PARAMS_ONLY_OPERATION.input() + pipeline, call = self._prepare_call( + input, HTTP_QUERY_PARAMS_ONLY_OPERATION, [], plugins + ) + return pipeline(call) + + def http_request_with_float_labels( + self, + input: HttpRequestWithFloatLabelsInput, + plugins: list[Plugin] | None = None, + ) -> HttpRequestWithFloatLabelsOutput: + """ + Invokes the HttpRequestWithFloatLabels operation. + + Args: + input: + An instance of `HttpRequestWithFloatLabelsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `HttpRequestWithFloatLabelsOutput`. + """ + pipeline, call = self._prepare_call( + input, HTTP_REQUEST_WITH_FLOAT_LABELS, [], plugins + ) + return pipeline(call) + + def http_request_with_greedy_label_in_path( + self, + input: HttpRequestWithGreedyLabelInPathInput, + plugins: list[Plugin] | None = None, + ) -> HttpRequestWithGreedyLabelInPathOutput: + """ + Invokes the HttpRequestWithGreedyLabelInPath operation. + + Args: + input: + An instance of `HttpRequestWithGreedyLabelInPathInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `HttpRequestWithGreedyLabelInPathOutput`. + """ + pipeline, call = self._prepare_call( + input, HTTP_REQUEST_WITH_GREEDY_LABEL_IN_PATH, [], plugins + ) + return pipeline(call) + + def http_request_with_labels( + self, input: HttpRequestWithLabelsInput, plugins: list[Plugin] | None = None + ) -> HttpRequestWithLabelsOutput: + """ + The example tests how requests are serialized when there's no input + payload but there are HTTP labels. + + Args: + input: + An instance of `HttpRequestWithLabelsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `HttpRequestWithLabelsOutput`. + """ + pipeline, call = self._prepare_call( + input, HTTP_REQUEST_WITH_LABELS, [], plugins + ) + return pipeline(call) + + def http_request_with_labels_and_timestamp_format( + self, + input: HttpRequestWithLabelsAndTimestampFormatInput, + plugins: list[Plugin] | None = None, + ) -> HttpRequestWithLabelsAndTimestampFormatOutput: + """ + The example tests how requests serialize different timestamp formats in + the URI path. + + Args: + input: + An instance of `HttpRequestWithLabelsAndTimestampFormatInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `HttpRequestWithLabelsAndTimestampFormatOutput`. + """ + pipeline, call = self._prepare_call( + input, HTTP_REQUEST_WITH_LABELS_AND_TIMESTAMP_FORMAT, [], plugins + ) + return pipeline(call) + + def http_request_with_regex_literal( + self, + input: HttpRequestWithRegexLiteralInput, + plugins: list[Plugin] | None = None, + ) -> HttpRequestWithRegexLiteralOutput: + """ + Invokes the HttpRequestWithRegexLiteral operation. + + Args: + input: + An instance of `HttpRequestWithRegexLiteralInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `HttpRequestWithRegexLiteralOutput`. + """ + pipeline, call = self._prepare_call( + input, HTTP_REQUEST_WITH_REGEX_LITERAL, [], plugins + ) + return pipeline(call) + + def http_response_code( + self, + input: HttpResponseCodeInput | None = None, + plugins: list[Plugin] | None = None, + ) -> HttpResponseCodeOutput: + """ + Invokes the HttpResponseCode operation. + + Args: + input: + An instance of `HttpResponseCodeInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `HttpResponseCodeOutput`. + """ + if input is None: + input = HTTP_RESPONSE_CODE.input() + pipeline, call = self._prepare_call(input, HTTP_RESPONSE_CODE, [], plugins) + return pipeline(call) + + def http_string_payload( + self, + input: HttpStringPayloadInput | None = None, + plugins: list[Plugin] | None = None, + ) -> HttpStringPayloadOutput: + """ + Invokes the HttpStringPayload operation. + + Args: + input: + An instance of `HttpStringPayloadInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `HttpStringPayloadOutput`. + """ + if input is None: + input = HTTP_STRING_PAYLOAD.input() + pipeline, call = self._prepare_call(input, HTTP_STRING_PAYLOAD, [], plugins) + return pipeline(call) + + def ignore_query_params_in_response( + self, + input: IgnoreQueryParamsInResponseInput | None = None, + plugins: list[Plugin] | None = None, + ) -> IgnoreQueryParamsInResponseOutput: + """ + This example ensures that query string bound request parameters are + serialized in the body of responses if the structure is used in both the + request and response. + + Args: + input: + An instance of `IgnoreQueryParamsInResponseInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `IgnoreQueryParamsInResponseOutput`. + """ + if input is None: + input = IGNORE_QUERY_PARAMS_IN_RESPONSE.input() + pipeline, call = self._prepare_call( + input, IGNORE_QUERY_PARAMS_IN_RESPONSE, [], plugins + ) + return pipeline(call) + + def input_and_output_with_headers( + self, + input: InputAndOutputWithHeadersInput | None = None, + plugins: list[Plugin] | None = None, + ) -> InputAndOutputWithHeadersOutput: + """ + The example tests how requests and responses are serialized when there + is no input or output payload but there are HTTP header bindings. + + Args: + input: + An instance of `InputAndOutputWithHeadersInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `InputAndOutputWithHeadersOutput`. + """ + if input is None: + input = INPUT_AND_OUTPUT_WITH_HEADERS.input() + pipeline, call = self._prepare_call( + input, INPUT_AND_OUTPUT_WITH_HEADERS, [], plugins + ) + return pipeline(call) + + def json_blobs( + self, input: JsonBlobsInput | None = None, plugins: list[Plugin] | None = None + ) -> JsonBlobsOutput: + """ + Blobs are base64 encoded + + Args: + input: + An instance of `JsonBlobsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `JsonBlobsOutput`. + """ + if input is None: + input = JSON_BLOBS.input() + pipeline, call = self._prepare_call(input, JSON_BLOBS, [], plugins) + return pipeline(call) + + def json_enums( + self, input: JsonEnumsInput | None = None, plugins: list[Plugin] | None = None + ) -> JsonEnumsOutput: + """ + This example serializes enums as top level properties, in lists, sets, + and maps. + + Args: + input: + An instance of `JsonEnumsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `JsonEnumsOutput`. + """ + if input is None: + input = JSON_ENUMS.input() + pipeline, call = self._prepare_call(input, JSON_ENUMS, [], plugins) + return pipeline(call) + + def json_int_enums( + self, + input: JsonIntEnumsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> JsonIntEnumsOutput: + """ + This example serializes intEnums as top level properties, in lists, + sets, and maps. + + Args: + input: + An instance of `JsonIntEnumsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `JsonIntEnumsOutput`. + """ + if input is None: + input = JSON_INT_ENUMS.input() + pipeline, call = self._prepare_call(input, JSON_INT_ENUMS, [], plugins) + return pipeline(call) + + def json_lists( + self, input: JsonListsInput | None = None, plugins: list[Plugin] | None = None + ) -> JsonListsOutput: + """ + This test case serializes JSON lists for the following cases for both + input and output: 1. Normal JSON lists. 2. Normal JSON sets. 3. JSON + lists of lists. 4. Lists of structures. + + Args: + input: + An instance of `JsonListsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `JsonListsOutput`. + """ + if input is None: + input = JSON_LISTS.input() + pipeline, call = self._prepare_call(input, JSON_LISTS, [], plugins) + return pipeline(call) + + def json_maps( + self, input: JsonMapsInput | None = None, plugins: list[Plugin] | None = None + ) -> JsonMapsOutput: + """ + The example tests basic map serialization. + + Args: + input: + An instance of `JsonMapsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `JsonMapsOutput`. + """ + if input is None: + input = JSON_MAPS.input() + pipeline, call = self._prepare_call(input, JSON_MAPS, [], plugins) + return pipeline(call) + + def json_timestamps( + self, + input: JsonTimestampsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> JsonTimestampsOutput: + """ + This tests how timestamps are serialized, including using the default + format of date-time and various @timestampFormat trait values. + + Args: + input: + An instance of `JsonTimestampsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `JsonTimestampsOutput`. + """ + if input is None: + input = JSON_TIMESTAMPS.input() + pipeline, call = self._prepare_call(input, JSON_TIMESTAMPS, [], plugins) + return pipeline(call) + + def json_unions( + self, input: JsonUnionsInput | None = None, plugins: list[Plugin] | None = None + ) -> JsonUnionsOutput: + """ + This operation uses unions for inputs and outputs. + + Args: + input: + An instance of `JsonUnionsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `JsonUnionsOutput`. + """ + if input is None: + input = JSON_UNIONS.input() + pipeline, call = self._prepare_call(input, JSON_UNIONS, [], plugins) + return pipeline(call) + + def malformed_accept_with_body( + self, + input: MalformedAcceptWithBodyInput | None = None, + plugins: list[Plugin] | None = None, + ) -> MalformedAcceptWithBodyOutput: + """ + Invokes the MalformedAcceptWithBody operation. + + Args: + input: + An instance of `MalformedAcceptWithBodyInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `MalformedAcceptWithBodyOutput`. + """ + if input is None: + input = MALFORMED_ACCEPT_WITH_BODY.input() + pipeline, call = self._prepare_call( + input, MALFORMED_ACCEPT_WITH_BODY, [], plugins + ) + return pipeline(call) + + def malformed_accept_with_generic_string( + self, + input: MalformedAcceptWithGenericStringInput | None = None, + plugins: list[Plugin] | None = None, + ) -> MalformedAcceptWithGenericStringOutput: + """ + Invokes the MalformedAcceptWithGenericString operation. + + Args: + input: + An instance of `MalformedAcceptWithGenericStringInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `MalformedAcceptWithGenericStringOutput`. + """ + if input is None: + input = MALFORMED_ACCEPT_WITH_GENERIC_STRING.input() + pipeline, call = self._prepare_call( + input, MALFORMED_ACCEPT_WITH_GENERIC_STRING, [], plugins + ) + return pipeline(call) + + def malformed_accept_with_payload( + self, + input: MalformedAcceptWithPayloadInput | None = None, + plugins: list[Plugin] | None = None, + ) -> MalformedAcceptWithPayloadOutput: + """ + Invokes the MalformedAcceptWithPayload operation. + + Args: + input: + An instance of `MalformedAcceptWithPayloadInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `MalformedAcceptWithPayloadOutput`. + """ + if input is None: + input = MALFORMED_ACCEPT_WITH_PAYLOAD.input() + pipeline, call = self._prepare_call( + input, MALFORMED_ACCEPT_WITH_PAYLOAD, [], plugins + ) + return pipeline(call) + + def malformed_blob( + self, + input: MalformedBlobInput | None = None, + plugins: list[Plugin] | None = None, + ) -> MalformedBlobOutput: + """ + Invokes the MalformedBlob operation. + + Args: + input: + An instance of `MalformedBlobInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `MalformedBlobOutput`. + """ + if input is None: + input = MALFORMED_BLOB.input() + pipeline, call = self._prepare_call(input, MALFORMED_BLOB, [], plugins) + return pipeline(call) + + def malformed_boolean( + self, input: MalformedBooleanInput, plugins: list[Plugin] | None = None + ) -> MalformedBooleanOutput: + """ + Invokes the MalformedBoolean operation. + + Args: + input: + An instance of `MalformedBooleanInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `MalformedBooleanOutput`. + """ + pipeline, call = self._prepare_call(input, MALFORMED_BOOLEAN, [], plugins) + return pipeline(call) + + def malformed_byte( + self, input: MalformedByteInput, plugins: list[Plugin] | None = None + ) -> MalformedByteOutput: + """ + Invokes the MalformedByte operation. + + Args: + input: + An instance of `MalformedByteInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `MalformedByteOutput`. + """ + pipeline, call = self._prepare_call(input, MALFORMED_BYTE, [], plugins) + return pipeline(call) + + def malformed_content_type_with_body( + self, + input: MalformedContentTypeWithBodyInput | None = None, + plugins: list[Plugin] | None = None, + ) -> MalformedContentTypeWithBodyOutput: + """ + Invokes the MalformedContentTypeWithBody operation. + + Args: + input: + An instance of `MalformedContentTypeWithBodyInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `MalformedContentTypeWithBodyOutput`. + """ + if input is None: + input = MALFORMED_CONTENT_TYPE_WITH_BODY.input() + pipeline, call = self._prepare_call( + input, MALFORMED_CONTENT_TYPE_WITH_BODY, [], plugins + ) + return pipeline(call) + + def malformed_content_type_with_generic_string( + self, + input: MalformedContentTypeWithGenericStringInput | None = None, + plugins: list[Plugin] | None = None, + ) -> MalformedContentTypeWithGenericStringOutput: + """ + Invokes the MalformedContentTypeWithGenericString operation. + + Args: + input: + An instance of `MalformedContentTypeWithGenericStringInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `MalformedContentTypeWithGenericStringOutput`. + """ + if input is None: + input = MALFORMED_CONTENT_TYPE_WITH_GENERIC_STRING.input() + pipeline, call = self._prepare_call( + input, MALFORMED_CONTENT_TYPE_WITH_GENERIC_STRING, [], plugins + ) + return pipeline(call) + + def malformed_content_type_without_body( + self, + input: MalformedContentTypeWithoutBodyInput | None = None, + plugins: list[Plugin] | None = None, + ) -> MalformedContentTypeWithoutBodyOutput: + """ + Invokes the MalformedContentTypeWithoutBody operation. + + Args: + input: + An instance of `MalformedContentTypeWithoutBodyInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `MalformedContentTypeWithoutBodyOutput`. + """ + if input is None: + input = MALFORMED_CONTENT_TYPE_WITHOUT_BODY.input() + pipeline, call = self._prepare_call( + input, MALFORMED_CONTENT_TYPE_WITHOUT_BODY, [], plugins + ) + return pipeline(call) + + def malformed_content_type_without_body_empty_input( + self, + input: MalformedContentTypeWithoutBodyEmptyInputInput | None = None, + plugins: list[Plugin] | None = None, + ) -> MalformedContentTypeWithoutBodyEmptyInputOutput: + """ + Invokes the MalformedContentTypeWithoutBodyEmptyInput operation. + + Args: + input: + An instance of `MalformedContentTypeWithoutBodyEmptyInputInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `MalformedContentTypeWithoutBodyEmptyInputOutput`. + """ + if input is None: + input = MALFORMED_CONTENT_TYPE_WITHOUT_BODY_EMPTY_INPUT.input() + pipeline, call = self._prepare_call( + input, MALFORMED_CONTENT_TYPE_WITHOUT_BODY_EMPTY_INPUT, [], plugins + ) + return pipeline(call) + + def malformed_content_type_with_payload( + self, + input: MalformedContentTypeWithPayloadInput | None = None, + plugins: list[Plugin] | None = None, + ) -> MalformedContentTypeWithPayloadOutput: + """ + Invokes the MalformedContentTypeWithPayload operation. + + Args: + input: + An instance of `MalformedContentTypeWithPayloadInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `MalformedContentTypeWithPayloadOutput`. + """ + if input is None: + input = MALFORMED_CONTENT_TYPE_WITH_PAYLOAD.input() + pipeline, call = self._prepare_call( + input, MALFORMED_CONTENT_TYPE_WITH_PAYLOAD, [], plugins + ) + return pipeline(call) + + def malformed_double( + self, input: MalformedDoubleInput, plugins: list[Plugin] | None = None + ) -> MalformedDoubleOutput: + """ + Invokes the MalformedDouble operation. + + Args: + input: + An instance of `MalformedDoubleInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `MalformedDoubleOutput`. + """ + pipeline, call = self._prepare_call(input, MALFORMED_DOUBLE, [], plugins) + return pipeline(call) + + def malformed_float( + self, input: MalformedFloatInput, plugins: list[Plugin] | None = None + ) -> MalformedFloatOutput: + """ + Invokes the MalformedFloat operation. + + Args: + input: + An instance of `MalformedFloatInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `MalformedFloatOutput`. + """ + pipeline, call = self._prepare_call(input, MALFORMED_FLOAT, [], plugins) + return pipeline(call) + + def malformed_integer( + self, input: MalformedIntegerInput, plugins: list[Plugin] | None = None + ) -> MalformedIntegerOutput: + """ + Invokes the MalformedInteger operation. + + Args: + input: + An instance of `MalformedIntegerInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `MalformedIntegerOutput`. + """ + pipeline, call = self._prepare_call(input, MALFORMED_INTEGER, [], plugins) + return pipeline(call) + + def malformed_list( + self, + input: MalformedListInput | None = None, + plugins: list[Plugin] | None = None, + ) -> MalformedListOutput: + """ + Invokes the MalformedList operation. + + Args: + input: + An instance of `MalformedListInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `MalformedListOutput`. + """ + if input is None: + input = MALFORMED_LIST.input() + pipeline, call = self._prepare_call(input, MALFORMED_LIST, [], plugins) + return pipeline(call) + + def malformed_long( + self, input: MalformedLongInput, plugins: list[Plugin] | None = None + ) -> MalformedLongOutput: + """ + Invokes the MalformedLong operation. + + Args: + input: + An instance of `MalformedLongInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `MalformedLongOutput`. + """ + pipeline, call = self._prepare_call(input, MALFORMED_LONG, [], plugins) + return pipeline(call) + + def malformed_map( + self, + input: MalformedMapInput | None = None, + plugins: list[Plugin] | None = None, + ) -> MalformedMapOutput: + """ + Invokes the MalformedMap operation. + + Args: + input: + An instance of `MalformedMapInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `MalformedMapOutput`. + """ + if input is None: + input = MALFORMED_MAP.input() + pipeline, call = self._prepare_call(input, MALFORMED_MAP, [], plugins) + return pipeline(call) + + def malformed_request_body( + self, + input: MalformedRequestBodyInput | None = None, + plugins: list[Plugin] | None = None, + ) -> MalformedRequestBodyOutput: + """ + Invokes the MalformedRequestBody operation. + + Args: + input: + An instance of `MalformedRequestBodyInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `MalformedRequestBodyOutput`. + """ + if input is None: + input = MALFORMED_REQUEST_BODY.input() + pipeline, call = self._prepare_call(input, MALFORMED_REQUEST_BODY, [], plugins) + return pipeline(call) + + def malformed_short( + self, input: MalformedShortInput, plugins: list[Plugin] | None = None + ) -> MalformedShortOutput: + """ + Invokes the MalformedShort operation. + + Args: + input: + An instance of `MalformedShortInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `MalformedShortOutput`. + """ + pipeline, call = self._prepare_call(input, MALFORMED_SHORT, [], plugins) + return pipeline(call) + + def malformed_string( + self, + input: MalformedStringInput | None = None, + plugins: list[Plugin] | None = None, + ) -> MalformedStringOutput: + """ + Invokes the MalformedString operation. + + Args: + input: + An instance of `MalformedStringInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `MalformedStringOutput`. + """ + if input is None: + input = MALFORMED_STRING.input() + pipeline, call = self._prepare_call(input, MALFORMED_STRING, [], plugins) + return pipeline(call) + + def malformed_timestamp_body_date_time( + self, + input: MalformedTimestampBodyDateTimeInput, + plugins: list[Plugin] | None = None, + ) -> MalformedTimestampBodyDateTimeOutput: + """ + Invokes the MalformedTimestampBodyDateTime operation. + + Args: + input: + An instance of `MalformedTimestampBodyDateTimeInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `MalformedTimestampBodyDateTimeOutput`. + """ + pipeline, call = self._prepare_call( + input, MALFORMED_TIMESTAMP_BODY_DATE_TIME, [], plugins + ) + return pipeline(call) + + def malformed_timestamp_body_default( + self, + input: MalformedTimestampBodyDefaultInput, + plugins: list[Plugin] | None = None, + ) -> MalformedTimestampBodyDefaultOutput: + """ + Invokes the MalformedTimestampBodyDefault operation. + + Args: + input: + An instance of `MalformedTimestampBodyDefaultInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `MalformedTimestampBodyDefaultOutput`. + """ + pipeline, call = self._prepare_call( + input, MALFORMED_TIMESTAMP_BODY_DEFAULT, [], plugins + ) + return pipeline(call) + + def malformed_timestamp_body_http_date( + self, + input: MalformedTimestampBodyHttpDateInput, + plugins: list[Plugin] | None = None, + ) -> MalformedTimestampBodyHttpDateOutput: + """ + Invokes the MalformedTimestampBodyHttpDate operation. + + Args: + input: + An instance of `MalformedTimestampBodyHttpDateInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `MalformedTimestampBodyHttpDateOutput`. + """ + pipeline, call = self._prepare_call( + input, MALFORMED_TIMESTAMP_BODY_HTTP_DATE, [], plugins + ) + return pipeline(call) + + def malformed_timestamp_header_date_time( + self, + input: MalformedTimestampHeaderDateTimeInput, + plugins: list[Plugin] | None = None, + ) -> MalformedTimestampHeaderDateTimeOutput: + """ + Invokes the MalformedTimestampHeaderDateTime operation. + + Args: + input: + An instance of `MalformedTimestampHeaderDateTimeInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `MalformedTimestampHeaderDateTimeOutput`. + """ + pipeline, call = self._prepare_call( + input, MALFORMED_TIMESTAMP_HEADER_DATE_TIME, [], plugins + ) + return pipeline(call) + + def malformed_timestamp_header_default( + self, + input: MalformedTimestampHeaderDefaultInput, + plugins: list[Plugin] | None = None, + ) -> MalformedTimestampHeaderDefaultOutput: + """ + Invokes the MalformedTimestampHeaderDefault operation. + + Args: + input: + An instance of `MalformedTimestampHeaderDefaultInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `MalformedTimestampHeaderDefaultOutput`. + """ + pipeline, call = self._prepare_call( + input, MALFORMED_TIMESTAMP_HEADER_DEFAULT, [], plugins + ) + return pipeline(call) + + def malformed_timestamp_header_epoch( + self, + input: MalformedTimestampHeaderEpochInput, + plugins: list[Plugin] | None = None, + ) -> MalformedTimestampHeaderEpochOutput: + """ + Invokes the MalformedTimestampHeaderEpoch operation. + + Args: + input: + An instance of `MalformedTimestampHeaderEpochInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `MalformedTimestampHeaderEpochOutput`. + """ + pipeline, call = self._prepare_call( + input, MALFORMED_TIMESTAMP_HEADER_EPOCH, [], plugins + ) + return pipeline(call) + + def malformed_timestamp_path_default( + self, + input: MalformedTimestampPathDefaultInput, + plugins: list[Plugin] | None = None, + ) -> MalformedTimestampPathDefaultOutput: + """ + Invokes the MalformedTimestampPathDefault operation. + + Args: + input: + An instance of `MalformedTimestampPathDefaultInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `MalformedTimestampPathDefaultOutput`. + """ + pipeline, call = self._prepare_call( + input, MALFORMED_TIMESTAMP_PATH_DEFAULT, [], plugins + ) + return pipeline(call) + + def malformed_timestamp_path_epoch( + self, + input: MalformedTimestampPathEpochInput, + plugins: list[Plugin] | None = None, + ) -> MalformedTimestampPathEpochOutput: + """ + Invokes the MalformedTimestampPathEpoch operation. + + Args: + input: + An instance of `MalformedTimestampPathEpochInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `MalformedTimestampPathEpochOutput`. + """ + pipeline, call = self._prepare_call( + input, MALFORMED_TIMESTAMP_PATH_EPOCH, [], plugins + ) + return pipeline(call) + + def malformed_timestamp_path_http_date( + self, + input: MalformedTimestampPathHttpDateInput, + plugins: list[Plugin] | None = None, + ) -> MalformedTimestampPathHttpDateOutput: + """ + Invokes the MalformedTimestampPathHttpDate operation. + + Args: + input: + An instance of `MalformedTimestampPathHttpDateInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `MalformedTimestampPathHttpDateOutput`. + """ + pipeline, call = self._prepare_call( + input, MALFORMED_TIMESTAMP_PATH_HTTP_DATE, [], plugins + ) + return pipeline(call) + + def malformed_timestamp_query_default( + self, + input: MalformedTimestampQueryDefaultInput, + plugins: list[Plugin] | None = None, + ) -> MalformedTimestampQueryDefaultOutput: + """ + Invokes the MalformedTimestampQueryDefault operation. + + Args: + input: + An instance of `MalformedTimestampQueryDefaultInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `MalformedTimestampQueryDefaultOutput`. + """ + pipeline, call = self._prepare_call( + input, MALFORMED_TIMESTAMP_QUERY_DEFAULT, [], plugins + ) + return pipeline(call) + + def malformed_timestamp_query_epoch( + self, + input: MalformedTimestampQueryEpochInput, + plugins: list[Plugin] | None = None, + ) -> MalformedTimestampQueryEpochOutput: + """ + Invokes the MalformedTimestampQueryEpoch operation. + + Args: + input: + An instance of `MalformedTimestampQueryEpochInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `MalformedTimestampQueryEpochOutput`. + """ + pipeline, call = self._prepare_call( + input, MALFORMED_TIMESTAMP_QUERY_EPOCH, [], plugins + ) + return pipeline(call) + + def malformed_timestamp_query_http_date( + self, + input: MalformedTimestampQueryHttpDateInput, + plugins: list[Plugin] | None = None, + ) -> MalformedTimestampQueryHttpDateOutput: + """ + Invokes the MalformedTimestampQueryHttpDate operation. + + Args: + input: + An instance of `MalformedTimestampQueryHttpDateInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `MalformedTimestampQueryHttpDateOutput`. + """ + pipeline, call = self._prepare_call( + input, MALFORMED_TIMESTAMP_QUERY_HTTP_DATE, [], plugins + ) + return pipeline(call) + + def malformed_union( + self, + input: MalformedUnionInput | None = None, + plugins: list[Plugin] | None = None, + ) -> MalformedUnionOutput: + """ + Invokes the MalformedUnion operation. + + Args: + input: + An instance of `MalformedUnionInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `MalformedUnionOutput`. + """ + if input is None: + input = MALFORMED_UNION.input() + pipeline, call = self._prepare_call(input, MALFORMED_UNION, [], plugins) + return pipeline(call) + + def media_type_header( + self, + input: MediaTypeHeaderInput | None = None, + plugins: list[Plugin] | None = None, + ) -> MediaTypeHeaderOutput: + """ + This example ensures that mediaType strings are base64 encoded in + headers. + + Args: + input: + An instance of `MediaTypeHeaderInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `MediaTypeHeaderOutput`. + """ + if input is None: + input = MEDIA_TYPE_HEADER.input() + pipeline, call = self._prepare_call(input, MEDIA_TYPE_HEADER, [], plugins) + return pipeline(call) + + def no_input_and_no_output( + self, + input: NoInputAndNoOutputInput | None = None, + plugins: list[Plugin] | None = None, + ) -> NoInputAndNoOutputOutput: + """ + The example tests how requests and responses are serialized when + there's no request or response payload because the operation has no + input or output. While this should be rare, code generators must support + this. + + Args: + input: + An instance of `NoInputAndNoOutputInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `NoInputAndNoOutputOutput`. + """ + if input is None: + input = NO_INPUT_AND_NO_OUTPUT.input() + pipeline, call = self._prepare_call(input, NO_INPUT_AND_NO_OUTPUT, [], plugins) + return pipeline(call) + + def no_input_and_output( + self, + input: NoInputAndOutputInput | None = None, + plugins: list[Plugin] | None = None, + ) -> NoInputAndOutputOutput: + """ + The example tests how requests and responses are serialized when + there's no request or response payload because the operation has no + input and the output is empty. While this should be rare, code + generators must support this. + + Args: + input: + An instance of `NoInputAndOutputInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `NoInputAndOutputOutput`. + """ + if input is None: + input = NO_INPUT_AND_OUTPUT.input() + pipeline, call = self._prepare_call(input, NO_INPUT_AND_OUTPUT, [], plugins) + return pipeline(call) + + def null_and_empty_headers_client( + self, + input: NullAndEmptyHeadersClientInput | None = None, + plugins: list[Plugin] | None = None, + ) -> NullAndEmptyHeadersClientOutput: + """ + Null headers are not sent over the wire, empty headers are serialized to + \"\" + + Args: + input: + An instance of `NullAndEmptyHeadersClientInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `NullAndEmptyHeadersClientOutput`. + """ + if input is None: + input = NULL_AND_EMPTY_HEADERS_CLIENT.input() + pipeline, call = self._prepare_call( + input, NULL_AND_EMPTY_HEADERS_CLIENT, [], plugins + ) + return pipeline(call) + + def null_and_empty_headers_server( + self, + input: NullAndEmptyHeadersServerInput | None = None, + plugins: list[Plugin] | None = None, + ) -> NullAndEmptyHeadersServerOutput: + """ + Null headers are not sent over the wire, empty headers are serialized to + \"\" + + Args: + input: + An instance of `NullAndEmptyHeadersServerInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `NullAndEmptyHeadersServerOutput`. + """ + if input is None: + input = NULL_AND_EMPTY_HEADERS_SERVER.input() + pipeline, call = self._prepare_call( + input, NULL_AND_EMPTY_HEADERS_SERVER, [], plugins + ) + return pipeline(call) + + def omits_null_serializes_empty_string( + self, + input: OmitsNullSerializesEmptyStringInput | None = None, + plugins: list[Plugin] | None = None, + ) -> OmitsNullSerializesEmptyStringOutput: + """ + Omits null, but serializes empty string value. + + Args: + input: + An instance of `OmitsNullSerializesEmptyStringInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `OmitsNullSerializesEmptyStringOutput`. + """ + if input is None: + input = OMITS_NULL_SERIALIZES_EMPTY_STRING.input() + pipeline, call = self._prepare_call( + input, OMITS_NULL_SERIALIZES_EMPTY_STRING, [], plugins + ) + return pipeline(call) + + def omits_serializing_empty_lists( + self, + input: OmitsSerializingEmptyListsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> OmitsSerializingEmptyListsOutput: + """ + Omits serializing empty lists. Because empty strings are serilized as + `Foo=`, empty lists cannot also be serialized as `Foo=` and instead + must be omitted. + + Args: + input: + An instance of `OmitsSerializingEmptyListsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `OmitsSerializingEmptyListsOutput`. + """ + if input is None: + input = OMITS_SERIALIZING_EMPTY_LISTS.input() + pipeline, call = self._prepare_call( + input, OMITS_SERIALIZING_EMPTY_LISTS, [], plugins + ) + return pipeline(call) + + def operation_with_defaults( + self, + input: OperationWithDefaultsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> OperationWithDefaultsOutput: + """ + Invokes the OperationWithDefaults operation. + + Args: + input: + An instance of `OperationWithDefaultsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `OperationWithDefaultsOutput`. + """ + if input is None: + input = OPERATION_WITH_DEFAULTS.input() + pipeline, call = self._prepare_call(input, OPERATION_WITH_DEFAULTS, [], plugins) + return pipeline(call) + + def operation_with_nested_structure( + self, + input: OperationWithNestedStructureInput, + plugins: list[Plugin] | None = None, + ) -> OperationWithNestedStructureOutput: + """ + Invokes the OperationWithNestedStructure operation. + + Args: + input: + An instance of `OperationWithNestedStructureInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `OperationWithNestedStructureOutput`. + """ + pipeline, call = self._prepare_call( + input, OPERATION_WITH_NESTED_STRUCTURE, [], plugins + ) + return pipeline(call) + + def post_player_action( + self, + input: PostPlayerActionInput | None = None, + plugins: list[Plugin] | None = None, + ) -> PostPlayerActionOutput: + """ + This operation defines a union with a Unit member. + + Args: + input: + An instance of `PostPlayerActionInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `PostPlayerActionOutput`. + """ + if input is None: + input = POST_PLAYER_ACTION.input() + pipeline, call = self._prepare_call(input, POST_PLAYER_ACTION, [], plugins) + return pipeline(call) + + def post_union_with_json_name( + self, + input: PostUnionWithJsonNameInput | None = None, + plugins: list[Plugin] | None = None, + ) -> PostUnionWithJsonNameOutput: + """ + This operation defines a union that uses jsonName on some members. + + Args: + input: + An instance of `PostUnionWithJsonNameInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `PostUnionWithJsonNameOutput`. + """ + if input is None: + input = POST_UNION_WITH_JSON_NAME.input() + pipeline, call = self._prepare_call( + input, POST_UNION_WITH_JSON_NAME, [], plugins + ) + return pipeline(call) + + def put_with_content_encoding( + self, + input: PutWithContentEncodingInput | None = None, + plugins: list[Plugin] | None = None, + ) -> PutWithContentEncodingOutput: + """ + Invokes the PutWithContentEncoding operation. + + Args: + input: + An instance of `PutWithContentEncodingInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `PutWithContentEncodingOutput`. + """ + if input is None: + input = PUT_WITH_CONTENT_ENCODING.input() + pipeline, call = self._prepare_call( + input, PUT_WITH_CONTENT_ENCODING, [], plugins + ) + return pipeline(call) + + def query_idempotency_token_auto_fill( + self, + input: QueryIdempotencyTokenAutoFillInput | None = None, + plugins: list[Plugin] | None = None, + ) -> QueryIdempotencyTokenAutoFillOutput: + """ + Automatically adds idempotency tokens. + + Args: + input: + An instance of `QueryIdempotencyTokenAutoFillInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `QueryIdempotencyTokenAutoFillOutput`. + """ + if input is None: + input = QUERY_IDEMPOTENCY_TOKEN_AUTO_FILL.input() + pipeline, call = self._prepare_call( + input, QUERY_IDEMPOTENCY_TOKEN_AUTO_FILL, [], plugins + ) + return pipeline(call) + + def query_params_as_string_list_map( + self, + input: QueryParamsAsStringListMapInput | None = None, + plugins: list[Plugin] | None = None, + ) -> QueryParamsAsStringListMapOutput: + """ + Invokes the QueryParamsAsStringListMap operation. + + Args: + input: + An instance of `QueryParamsAsStringListMapInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `QueryParamsAsStringListMapOutput`. + """ + if input is None: + input = QUERY_PARAMS_AS_STRING_LIST_MAP.input() + pipeline, call = self._prepare_call( + input, QUERY_PARAMS_AS_STRING_LIST_MAP, [], plugins + ) + return pipeline(call) + + def query_precedence( + self, + input: QueryPrecedenceInput | None = None, + plugins: list[Plugin] | None = None, + ) -> QueryPrecedenceOutput: + """ + Invokes the QueryPrecedence operation. + + Args: + input: + An instance of `QueryPrecedenceInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `QueryPrecedenceOutput`. + """ + if input is None: + input = QUERY_PRECEDENCE.input() + pipeline, call = self._prepare_call(input, QUERY_PRECEDENCE, [], plugins) + return pipeline(call) + + def recursive_shapes( + self, + input: RecursiveShapesInput | None = None, + plugins: list[Plugin] | None = None, + ) -> RecursiveShapesOutput: + """ + Recursive shapes + + Args: + input: + An instance of `RecursiveShapesInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `RecursiveShapesOutput`. + """ + if input is None: + input = RECURSIVE_SHAPES.input() + pipeline, call = self._prepare_call(input, RECURSIVE_SHAPES, [], plugins) + return pipeline(call) + + def response_code_http_fallback( + self, + input: ResponseCodeHttpFallbackInput | None = None, + plugins: list[Plugin] | None = None, + ) -> ResponseCodeHttpFallbackOutput: + """ + Invokes the ResponseCodeHttpFallback operation. + + Args: + input: + An instance of `ResponseCodeHttpFallbackInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `ResponseCodeHttpFallbackOutput`. + """ + if input is None: + input = RESPONSE_CODE_HTTP_FALLBACK.input() + pipeline, call = self._prepare_call( + input, RESPONSE_CODE_HTTP_FALLBACK, [], plugins + ) + return pipeline(call) + + def response_code_required( + self, + input: ResponseCodeRequiredInput | None = None, + plugins: list[Plugin] | None = None, + ) -> ResponseCodeRequiredOutput: + """ + Invokes the ResponseCodeRequired operation. + + Args: + input: + An instance of `ResponseCodeRequiredInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `ResponseCodeRequiredOutput`. + """ + if input is None: + input = RESPONSE_CODE_REQUIRED.input() + pipeline, call = self._prepare_call(input, RESPONSE_CODE_REQUIRED, [], plugins) + return pipeline(call) + + def simple_scalar_properties( + self, + input: SimpleScalarPropertiesInput | None = None, + plugins: list[Plugin] | None = None, + ) -> SimpleScalarPropertiesOutput: + """ + Invokes the SimpleScalarProperties operation. + + Args: + input: + An instance of `SimpleScalarPropertiesInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `SimpleScalarPropertiesOutput`. + """ + if input is None: + input = SIMPLE_SCALAR_PROPERTIES.input() + pipeline, call = self._prepare_call( + input, SIMPLE_SCALAR_PROPERTIES, [], plugins + ) + return pipeline(call) + + def sparse_json_lists( + self, + input: SparseJsonListsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> SparseJsonListsOutput: + """ + Invokes the SparseJsonLists operation. + + Args: + input: + An instance of `SparseJsonListsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `SparseJsonListsOutput`. + """ + if input is None: + input = SPARSE_JSON_LISTS.input() + pipeline, call = self._prepare_call(input, SPARSE_JSON_LISTS, [], plugins) + return pipeline(call) + + def sparse_json_maps( + self, + input: SparseJsonMapsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> SparseJsonMapsOutput: + """ + This example tests sparse map serialization. + + Args: + input: + An instance of `SparseJsonMapsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `SparseJsonMapsOutput`. + """ + if input is None: + input = SPARSE_JSON_MAPS.input() + pipeline, call = self._prepare_call(input, SPARSE_JSON_MAPS, [], plugins) + return pipeline(call) + + def streaming_traits( + self, + input: StreamingTraitsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> StreamingTraitsOutput: + """ + This examples serializes a streaming blob shape in the request body. In + this example, no JSON document is synthesized because the payload is not + a structure or a union type. + + Args: + input: + An instance of `StreamingTraitsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `StreamingTraitsOutput`. + """ + if input is None: + input = STREAMING_TRAITS.input() + pipeline, call = self._prepare_call(input, STREAMING_TRAITS, [], plugins) + return pipeline(call) + + def streaming_traits_require_length( + self, + input: StreamingTraitsRequireLengthInput | None = None, + plugins: list[Plugin] | None = None, + ) -> StreamingTraitsRequireLengthOutput: + """ + This examples serializes a streaming blob shape with a required content + length in the request body. In this example, no JSON document is + synthesized because the payload is not a structure or a union type. + + Args: + input: + An instance of `StreamingTraitsRequireLengthInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `StreamingTraitsRequireLengthOutput`. + """ + if input is None: + input = STREAMING_TRAITS_REQUIRE_LENGTH.input() + pipeline, call = self._prepare_call( + input, STREAMING_TRAITS_REQUIRE_LENGTH, [], plugins + ) + return pipeline(call) + + def streaming_traits_with_media_type( + self, + input: StreamingTraitsWithMediaTypeInput | None = None, + plugins: list[Plugin] | None = None, + ) -> StreamingTraitsWithMediaTypeOutput: + """ + This examples serializes a streaming media-typed blob shape in the + request body. This examples uses a `@mediaType` trait on the payload + to force a custom content-type to be serialized. + + Args: + input: + An instance of `StreamingTraitsWithMediaTypeInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `StreamingTraitsWithMediaTypeOutput`. + """ + if input is None: + input = STREAMING_TRAITS_WITH_MEDIA_TYPE.input() + pipeline, call = self._prepare_call( + input, STREAMING_TRAITS_WITH_MEDIA_TYPE, [], plugins + ) + return pipeline(call) + + def test_body_structure( + self, + input: TestBodyStructureInput | None = None, + plugins: list[Plugin] | None = None, + ) -> TestBodyStructureOutput: + """ + This example operation serializes a structure in the HTTP body. It + should ensure Content-Type: application/json is used in all requests and + that an \"empty\" body is an empty JSON document ({}). + + Args: + input: + An instance of `TestBodyStructureInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `TestBodyStructureOutput`. + """ + if input is None: + input = TEST_BODY_STRUCTURE.input() + pipeline, call = self._prepare_call(input, TEST_BODY_STRUCTURE, [], plugins) + return pipeline(call) + + def test_get_no_input_no_payload( + self, + input: TestGetNoInputNoPayloadInput | None = None, + plugins: list[Plugin] | None = None, + ) -> TestGetNoInputNoPayloadOutput: + """ + This example GET operation has no input and serializes a request without + a HTTP body. These tests are to ensure we do not attach a body or + related headers (Content-Length, Content-Type) to operations that + semantically cannot produce an HTTP body. + + Args: + input: + An instance of `TestGetNoInputNoPayloadInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `TestGetNoInputNoPayloadOutput`. + """ + if input is None: + input = TEST_GET_NO_INPUT_NO_PAYLOAD.input() + pipeline, call = self._prepare_call( + input, TEST_GET_NO_INPUT_NO_PAYLOAD, [], plugins + ) + return pipeline(call) + + def test_get_no_payload( + self, + input: TestGetNoPayloadInput | None = None, + plugins: list[Plugin] | None = None, + ) -> TestGetNoPayloadOutput: + """ + This example GET operation serializes a request without a modeled HTTP + body. These tests are to ensure we do not attach a body or related + headers (Content-Length, Content-Type) to operations that semantically + cannot produce an HTTP body. + + Args: + input: + An instance of `TestGetNoPayloadInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `TestGetNoPayloadOutput`. + """ + if input is None: + input = TEST_GET_NO_PAYLOAD.input() + pipeline, call = self._prepare_call(input, TEST_GET_NO_PAYLOAD, [], plugins) + return pipeline(call) + + def test_payload_blob( + self, + input: TestPayloadBlobInput | None = None, + plugins: list[Plugin] | None = None, + ) -> TestPayloadBlobOutput: + """ + This example operation serializes a payload targeting a blob. The Blob + shape is not structured content and we cannot make assumptions about + what data will be sent. This test ensures only a generic \"Content-Type: + application/octet-stream\" header is used, and that we are not treating + an empty body as an empty JSON document. + + Args: + input: + An instance of `TestPayloadBlobInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `TestPayloadBlobOutput`. + """ + if input is None: + input = TEST_PAYLOAD_BLOB.input() + pipeline, call = self._prepare_call(input, TEST_PAYLOAD_BLOB, [], plugins) + return pipeline(call) + + def test_payload_structure( + self, + input: TestPayloadStructureInput | None = None, + plugins: list[Plugin] | None = None, + ) -> TestPayloadStructureOutput: + """ + This example operation serializes a payload targeting a structure. This + enforces the same requirements as TestBodyStructure but with the body + specified by the @httpPayload trait. + + Args: + input: + An instance of `TestPayloadStructureInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `TestPayloadStructureOutput`. + """ + if input is None: + input = TEST_PAYLOAD_STRUCTURE.input() + pipeline, call = self._prepare_call(input, TEST_PAYLOAD_STRUCTURE, [], plugins) + return pipeline(call) + + def test_post_no_input_no_payload( + self, + input: TestPostNoInputNoPayloadInput | None = None, + plugins: list[Plugin] | None = None, + ) -> TestPostNoInputNoPayloadOutput: + """ + This example POST operation has no input and serializes a request + without a HTTP body. These tests are to ensure we do not attach a body + or related headers (Content-Type) to a POST operation with no modeled + input. + + Args: + input: + An instance of `TestPostNoInputNoPayloadInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `TestPostNoInputNoPayloadOutput`. + """ + if input is None: + input = TEST_POST_NO_INPUT_NO_PAYLOAD.input() + pipeline, call = self._prepare_call( + input, TEST_POST_NO_INPUT_NO_PAYLOAD, [], plugins + ) + return pipeline(call) + + def test_post_no_payload( + self, + input: TestPostNoPayloadInput | None = None, + plugins: list[Plugin] | None = None, + ) -> TestPostNoPayloadOutput: + """ + This example POST operation serializes a request without a modeled HTTP + body. These tests are to ensure we do not attach a body or related + headers (Content-Type) to a POST operation with no modeled payload. + + Args: + input: + An instance of `TestPostNoPayloadInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `TestPostNoPayloadOutput`. + """ + if input is None: + input = TEST_POST_NO_PAYLOAD.input() + pipeline, call = self._prepare_call(input, TEST_POST_NO_PAYLOAD, [], plugins) + return pipeline(call) + + def timestamp_format_headers( + self, + input: TimestampFormatHeadersInput | None = None, + plugins: list[Plugin] | None = None, + ) -> TimestampFormatHeadersOutput: + """ + This example tests how timestamp request and response headers are + serialized. + + Args: + input: + An instance of `TimestampFormatHeadersInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `TimestampFormatHeadersOutput`. + """ + if input is None: + input = TIMESTAMP_FORMAT_HEADERS.input() + pipeline, call = self._prepare_call( + input, TIMESTAMP_FORMAT_HEADERS, [], plugins + ) + return pipeline(call) + + def unit_input_and_output( + self, + input: UnitInputAndOutputInput | None = None, + plugins: list[Plugin] | None = None, + ) -> UnitInputAndOutputOutput: + """ + This test is similar to NoInputAndNoOutput, but uses explicit Unit + types. + + Args: + input: + An instance of `UnitInputAndOutputInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `UnitInputAndOutputOutput`. + """ + if input is None: + input = UNIT_INPUT_AND_OUTPUT.input() + pipeline, call = self._prepare_call(input, UNIT_INPUT_AND_OUTPUT, [], plugins) + return pipeline(call) diff --git a/codegen-output/rest-json-1/src/restjson/sync/config.py b/codegen-output/rest-json-1/src/restjson/sync/config.py new file mode 100644 index 000000000..602ae73e2 --- /dev/null +++ b/codegen-output/rest-json-1/src/restjson/sync/config.py @@ -0,0 +1,749 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from dataclasses import dataclass, field +from typing import ( + Any, + Callable, + ClassVar, + Self, + TYPE_CHECKING, + TypeAlias, + Union, + Unpack, +) + +from smithy_aws_core.auth.sigv4 import SigV4AuthScheme +from smithy_aws_core.config import FileSystem +from smithy_aws_core.config.aws_config import AwsConfig, AwsConfigOverrides +from smithy_aws_core.config.resolvers import EndpointUriResolver +from smithy_aws_core.config.types import FieldSpec +from smithy_aws_core.endpoints.standard_regional import ( + StandardRegionalEndpointsResolver, +) +from smithy_aws_core.identity import AWSCredentialsIdentity, AWSIdentityProperties +from smithy_aws_core.protocols import RestJsonClientProtocol +from smithy_core.interfaces import ( + ClientProtocol, + EndpointResolver, + ProtocolConstructor, + ProtocolSettings, +) +from smithy_core.interfaces.auth import AuthScheme, AuthSchemeResolver +from smithy_core.interfaces.identity import IdentityResolver +from smithy_core.shapes import ShapeID +from smithy_http.urllib3 import Urllib3HTTPClient + +from .._private.schemas import REST_JSON +from ..auth import HTTPAuthSchemeResolver + + +if TYPE_CHECKING: + from smithy_core.interceptors import Interceptor + from ..models import ( + AllQueryStringTypesInput, + AllQueryStringTypesOutput, + ConstantAndVariableQueryStringInput, + ConstantAndVariableQueryStringOutput, + ConstantQueryStringInput, + ConstantQueryStringOutput, + ContentTypeParametersInput, + ContentTypeParametersOutput, + DatetimeOffsetsInput, + DatetimeOffsetsOutput, + DocumentTypeAsMapValueInput, + DocumentTypeAsMapValueOutput, + DocumentTypeAsPayloadInput, + DocumentTypeAsPayloadOutput, + DocumentTypeInput, + DocumentTypeOutput, + DuplexStreamInput, + DuplexStreamOutput, + DuplexStreamWithDistinctStreamsInput, + DuplexStreamWithDistinctStreamsOutput, + DuplexStreamWithInitialMessagesInput, + DuplexStreamWithInitialMessagesOutput, + EmptyInputAndEmptyOutputInput, + EmptyInputAndEmptyOutputOutput, + EndpointOperationInput, + EndpointOperationOutput, + EndpointWithHostLabelOperationInput, + EndpointWithHostLabelOperationOutput, + FractionalSecondsInput, + FractionalSecondsOutput, + GreetingWithErrorsInput, + GreetingWithErrorsOutput, + HostWithPathOperationInput, + HostWithPathOperationOutput, + HttpChecksumRequiredInput, + HttpChecksumRequiredOutput, + HttpEmptyPrefixHeadersInput, + HttpEmptyPrefixHeadersOutput, + HttpEnumPayloadInput, + HttpEnumPayloadOutput, + HttpPayloadTraitsInput, + HttpPayloadTraitsOutput, + HttpPayloadTraitsWithMediaTypeInput, + HttpPayloadTraitsWithMediaTypeOutput, + HttpPayloadWithStructureInput, + HttpPayloadWithStructureOutput, + HttpPayloadWithUnionInput, + HttpPayloadWithUnionOutput, + HttpPrefixHeadersInResponseInput, + HttpPrefixHeadersInResponseOutput, + HttpPrefixHeadersInput, + HttpPrefixHeadersOutput, + HttpQueryParamsOnlyOperationInput, + HttpQueryParamsOnlyOperationOutput, + HttpRequestWithFloatLabelsInput, + HttpRequestWithFloatLabelsOutput, + HttpRequestWithGreedyLabelInPathInput, + HttpRequestWithGreedyLabelInPathOutput, + HttpRequestWithLabelsAndTimestampFormatInput, + HttpRequestWithLabelsAndTimestampFormatOutput, + HttpRequestWithLabelsInput, + HttpRequestWithLabelsOutput, + HttpRequestWithRegexLiteralInput, + HttpRequestWithRegexLiteralOutput, + HttpResponseCodeInput, + HttpResponseCodeOutput, + HttpStringPayloadInput, + HttpStringPayloadOutput, + IgnoreQueryParamsInResponseInput, + IgnoreQueryParamsInResponseOutput, + InputAndOutputWithHeadersInput, + InputAndOutputWithHeadersOutput, + InputStreamInput, + InputStreamOutput, + InputStreamWithInitialRequestInput, + InputStreamWithInitialRequestOutput, + JsonBlobsInput, + JsonBlobsOutput, + JsonEnumsInput, + JsonEnumsOutput, + JsonIntEnumsInput, + JsonIntEnumsOutput, + JsonListsInput, + JsonListsOutput, + JsonMapsInput, + JsonMapsOutput, + JsonTimestampsInput, + JsonTimestampsOutput, + JsonUnionsInput, + JsonUnionsOutput, + MalformedAcceptWithBodyInput, + MalformedAcceptWithBodyOutput, + MalformedAcceptWithGenericStringInput, + MalformedAcceptWithGenericStringOutput, + MalformedAcceptWithPayloadInput, + MalformedAcceptWithPayloadOutput, + MalformedBlobInput, + MalformedBlobOutput, + MalformedBooleanInput, + MalformedBooleanOutput, + MalformedByteInput, + MalformedByteOutput, + MalformedContentTypeWithBodyInput, + MalformedContentTypeWithBodyOutput, + MalformedContentTypeWithGenericStringInput, + MalformedContentTypeWithGenericStringOutput, + MalformedContentTypeWithPayloadInput, + MalformedContentTypeWithPayloadOutput, + MalformedContentTypeWithoutBodyEmptyInputInput, + MalformedContentTypeWithoutBodyEmptyInputOutput, + MalformedContentTypeWithoutBodyInput, + MalformedContentTypeWithoutBodyOutput, + MalformedDoubleInput, + MalformedDoubleOutput, + MalformedFloatInput, + MalformedFloatOutput, + MalformedIntegerInput, + MalformedIntegerOutput, + MalformedListInput, + MalformedListOutput, + MalformedLongInput, + MalformedLongOutput, + MalformedMapInput, + MalformedMapOutput, + MalformedRequestBodyInput, + MalformedRequestBodyOutput, + MalformedShortInput, + MalformedShortOutput, + MalformedStringInput, + MalformedStringOutput, + MalformedTimestampBodyDateTimeInput, + MalformedTimestampBodyDateTimeOutput, + MalformedTimestampBodyDefaultInput, + MalformedTimestampBodyDefaultOutput, + MalformedTimestampBodyHttpDateInput, + MalformedTimestampBodyHttpDateOutput, + MalformedTimestampHeaderDateTimeInput, + MalformedTimestampHeaderDateTimeOutput, + MalformedTimestampHeaderDefaultInput, + MalformedTimestampHeaderDefaultOutput, + MalformedTimestampHeaderEpochInput, + MalformedTimestampHeaderEpochOutput, + MalformedTimestampPathDefaultInput, + MalformedTimestampPathDefaultOutput, + MalformedTimestampPathEpochInput, + MalformedTimestampPathEpochOutput, + MalformedTimestampPathHttpDateInput, + MalformedTimestampPathHttpDateOutput, + MalformedTimestampQueryDefaultInput, + MalformedTimestampQueryDefaultOutput, + MalformedTimestampQueryEpochInput, + MalformedTimestampQueryEpochOutput, + MalformedTimestampQueryHttpDateInput, + MalformedTimestampQueryHttpDateOutput, + MalformedUnionInput, + MalformedUnionOutput, + MediaTypeHeaderInput, + MediaTypeHeaderOutput, + NoInputAndNoOutputInput, + NoInputAndNoOutputOutput, + NoInputAndOutputInput, + NoInputAndOutputOutput, + NullAndEmptyHeadersClientInput, + NullAndEmptyHeadersClientOutput, + NullAndEmptyHeadersServerInput, + NullAndEmptyHeadersServerOutput, + OmitsNullSerializesEmptyStringInput, + OmitsNullSerializesEmptyStringOutput, + OmitsSerializingEmptyListsInput, + OmitsSerializingEmptyListsOutput, + OperationWithDefaultsInput, + OperationWithDefaultsOutput, + OperationWithNestedStructureInput, + OperationWithNestedStructureOutput, + OutputStreamInput, + OutputStreamOutput, + OutputStreamWithInitialResponseInput, + OutputStreamWithInitialResponseOutput, + PostPlayerActionInput, + PostPlayerActionOutput, + PostUnionWithJsonNameInput, + PostUnionWithJsonNameOutput, + PutWithContentEncodingInput, + PutWithContentEncodingOutput, + QueryIdempotencyTokenAutoFillInput, + QueryIdempotencyTokenAutoFillOutput, + QueryParamsAsStringListMapInput, + QueryParamsAsStringListMapOutput, + QueryPrecedenceInput, + QueryPrecedenceOutput, + RecursiveShapesInput, + RecursiveShapesOutput, + ResponseCodeHttpFallbackInput, + ResponseCodeHttpFallbackOutput, + ResponseCodeRequiredInput, + ResponseCodeRequiredOutput, + SimpleScalarPropertiesInput, + SimpleScalarPropertiesOutput, + SparseJsonListsInput, + SparseJsonListsOutput, + SparseJsonMapsInput, + SparseJsonMapsOutput, + StreamingTraitsInput, + StreamingTraitsOutput, + StreamingTraitsRequireLengthInput, + StreamingTraitsRequireLengthOutput, + StreamingTraitsWithMediaTypeInput, + StreamingTraitsWithMediaTypeOutput, + TestBodyStructureInput, + TestBodyStructureOutput, + TestGetNoInputNoPayloadInput, + TestGetNoInputNoPayloadOutput, + TestGetNoPayloadInput, + TestGetNoPayloadOutput, + TestPayloadBlobInput, + TestPayloadBlobOutput, + TestPayloadStructureInput, + TestPayloadStructureOutput, + TestPostNoInputNoPayloadInput, + TestPostNoInputNoPayloadOutput, + TestPostNoPayloadInput, + TestPostNoPayloadOutput, + TimestampFormatHeadersInput, + TimestampFormatHeadersOutput, + UnitInputAndOutputInput, + UnitInputAndOutputOutput, + ) + + _ServiceInterceptor = Union[ + Interceptor[AllQueryStringTypesInput, AllQueryStringTypesOutput, Any, Any], + Interceptor[ + ConstantAndVariableQueryStringInput, + ConstantAndVariableQueryStringOutput, + Any, + Any, + ], + Interceptor[ConstantQueryStringInput, ConstantQueryStringOutput, Any, Any], + Interceptor[ContentTypeParametersInput, ContentTypeParametersOutput, Any, Any], + Interceptor[DatetimeOffsetsInput, DatetimeOffsetsOutput, Any, Any], + Interceptor[DocumentTypeInput, DocumentTypeOutput, Any, Any], + Interceptor[ + DocumentTypeAsMapValueInput, DocumentTypeAsMapValueOutput, Any, Any + ], + Interceptor[DocumentTypeAsPayloadInput, DocumentTypeAsPayloadOutput, Any, Any], + Interceptor[DuplexStreamInput, DuplexStreamOutput, Any, Any], + Interceptor[ + DuplexStreamWithDistinctStreamsInput, + DuplexStreamWithDistinctStreamsOutput, + Any, + Any, + ], + Interceptor[ + DuplexStreamWithInitialMessagesInput, + DuplexStreamWithInitialMessagesOutput, + Any, + Any, + ], + Interceptor[ + EmptyInputAndEmptyOutputInput, EmptyInputAndEmptyOutputOutput, Any, Any + ], + Interceptor[EndpointOperationInput, EndpointOperationOutput, Any, Any], + Interceptor[ + EndpointWithHostLabelOperationInput, + EndpointWithHostLabelOperationOutput, + Any, + Any, + ], + Interceptor[FractionalSecondsInput, FractionalSecondsOutput, Any, Any], + Interceptor[GreetingWithErrorsInput, GreetingWithErrorsOutput, Any, Any], + Interceptor[HostWithPathOperationInput, HostWithPathOperationOutput, Any, Any], + Interceptor[HttpChecksumRequiredInput, HttpChecksumRequiredOutput, Any, Any], + Interceptor[ + HttpEmptyPrefixHeadersInput, HttpEmptyPrefixHeadersOutput, Any, Any + ], + Interceptor[HttpEnumPayloadInput, HttpEnumPayloadOutput, Any, Any], + Interceptor[HttpPayloadTraitsInput, HttpPayloadTraitsOutput, Any, Any], + Interceptor[ + HttpPayloadTraitsWithMediaTypeInput, + HttpPayloadTraitsWithMediaTypeOutput, + Any, + Any, + ], + Interceptor[ + HttpPayloadWithStructureInput, HttpPayloadWithStructureOutput, Any, Any + ], + Interceptor[HttpPayloadWithUnionInput, HttpPayloadWithUnionOutput, Any, Any], + Interceptor[HttpPrefixHeadersInput, HttpPrefixHeadersOutput, Any, Any], + Interceptor[ + HttpPrefixHeadersInResponseInput, + HttpPrefixHeadersInResponseOutput, + Any, + Any, + ], + Interceptor[ + HttpQueryParamsOnlyOperationInput, + HttpQueryParamsOnlyOperationOutput, + Any, + Any, + ], + Interceptor[ + HttpRequestWithFloatLabelsInput, HttpRequestWithFloatLabelsOutput, Any, Any + ], + Interceptor[ + HttpRequestWithGreedyLabelInPathInput, + HttpRequestWithGreedyLabelInPathOutput, + Any, + Any, + ], + Interceptor[HttpRequestWithLabelsInput, HttpRequestWithLabelsOutput, Any, Any], + Interceptor[ + HttpRequestWithLabelsAndTimestampFormatInput, + HttpRequestWithLabelsAndTimestampFormatOutput, + Any, + Any, + ], + Interceptor[ + HttpRequestWithRegexLiteralInput, + HttpRequestWithRegexLiteralOutput, + Any, + Any, + ], + Interceptor[HttpResponseCodeInput, HttpResponseCodeOutput, Any, Any], + Interceptor[HttpStringPayloadInput, HttpStringPayloadOutput, Any, Any], + Interceptor[ + IgnoreQueryParamsInResponseInput, + IgnoreQueryParamsInResponseOutput, + Any, + Any, + ], + Interceptor[ + InputAndOutputWithHeadersInput, InputAndOutputWithHeadersOutput, Any, Any + ], + Interceptor[InputStreamInput, InputStreamOutput, Any, Any], + Interceptor[ + InputStreamWithInitialRequestInput, + InputStreamWithInitialRequestOutput, + Any, + Any, + ], + Interceptor[JsonBlobsInput, JsonBlobsOutput, Any, Any], + Interceptor[JsonEnumsInput, JsonEnumsOutput, Any, Any], + Interceptor[JsonIntEnumsInput, JsonIntEnumsOutput, Any, Any], + Interceptor[JsonListsInput, JsonListsOutput, Any, Any], + Interceptor[JsonMapsInput, JsonMapsOutput, Any, Any], + Interceptor[JsonTimestampsInput, JsonTimestampsOutput, Any, Any], + Interceptor[JsonUnionsInput, JsonUnionsOutput, Any, Any], + Interceptor[ + MalformedAcceptWithBodyInput, MalformedAcceptWithBodyOutput, Any, Any + ], + Interceptor[ + MalformedAcceptWithGenericStringInput, + MalformedAcceptWithGenericStringOutput, + Any, + Any, + ], + Interceptor[ + MalformedAcceptWithPayloadInput, MalformedAcceptWithPayloadOutput, Any, Any + ], + Interceptor[MalformedBlobInput, MalformedBlobOutput, Any, Any], + Interceptor[MalformedBooleanInput, MalformedBooleanOutput, Any, Any], + Interceptor[MalformedByteInput, MalformedByteOutput, Any, Any], + Interceptor[ + MalformedContentTypeWithBodyInput, + MalformedContentTypeWithBodyOutput, + Any, + Any, + ], + Interceptor[ + MalformedContentTypeWithGenericStringInput, + MalformedContentTypeWithGenericStringOutput, + Any, + Any, + ], + Interceptor[ + MalformedContentTypeWithoutBodyInput, + MalformedContentTypeWithoutBodyOutput, + Any, + Any, + ], + Interceptor[ + MalformedContentTypeWithoutBodyEmptyInputInput, + MalformedContentTypeWithoutBodyEmptyInputOutput, + Any, + Any, + ], + Interceptor[ + MalformedContentTypeWithPayloadInput, + MalformedContentTypeWithPayloadOutput, + Any, + Any, + ], + Interceptor[MalformedDoubleInput, MalformedDoubleOutput, Any, Any], + Interceptor[MalformedFloatInput, MalformedFloatOutput, Any, Any], + Interceptor[MalformedIntegerInput, MalformedIntegerOutput, Any, Any], + Interceptor[MalformedListInput, MalformedListOutput, Any, Any], + Interceptor[MalformedLongInput, MalformedLongOutput, Any, Any], + Interceptor[MalformedMapInput, MalformedMapOutput, Any, Any], + Interceptor[MalformedRequestBodyInput, MalformedRequestBodyOutput, Any, Any], + Interceptor[MalformedShortInput, MalformedShortOutput, Any, Any], + Interceptor[MalformedStringInput, MalformedStringOutput, Any, Any], + Interceptor[ + MalformedTimestampBodyDateTimeInput, + MalformedTimestampBodyDateTimeOutput, + Any, + Any, + ], + Interceptor[ + MalformedTimestampBodyDefaultInput, + MalformedTimestampBodyDefaultOutput, + Any, + Any, + ], + Interceptor[ + MalformedTimestampBodyHttpDateInput, + MalformedTimestampBodyHttpDateOutput, + Any, + Any, + ], + Interceptor[ + MalformedTimestampHeaderDateTimeInput, + MalformedTimestampHeaderDateTimeOutput, + Any, + Any, + ], + Interceptor[ + MalformedTimestampHeaderDefaultInput, + MalformedTimestampHeaderDefaultOutput, + Any, + Any, + ], + Interceptor[ + MalformedTimestampHeaderEpochInput, + MalformedTimestampHeaderEpochOutput, + Any, + Any, + ], + Interceptor[ + MalformedTimestampPathDefaultInput, + MalformedTimestampPathDefaultOutput, + Any, + Any, + ], + Interceptor[ + MalformedTimestampPathEpochInput, + MalformedTimestampPathEpochOutput, + Any, + Any, + ], + Interceptor[ + MalformedTimestampPathHttpDateInput, + MalformedTimestampPathHttpDateOutput, + Any, + Any, + ], + Interceptor[ + MalformedTimestampQueryDefaultInput, + MalformedTimestampQueryDefaultOutput, + Any, + Any, + ], + Interceptor[ + MalformedTimestampQueryEpochInput, + MalformedTimestampQueryEpochOutput, + Any, + Any, + ], + Interceptor[ + MalformedTimestampQueryHttpDateInput, + MalformedTimestampQueryHttpDateOutput, + Any, + Any, + ], + Interceptor[MalformedUnionInput, MalformedUnionOutput, Any, Any], + Interceptor[MediaTypeHeaderInput, MediaTypeHeaderOutput, Any, Any], + Interceptor[NoInputAndNoOutputInput, NoInputAndNoOutputOutput, Any, Any], + Interceptor[NoInputAndOutputInput, NoInputAndOutputOutput, Any, Any], + Interceptor[ + NullAndEmptyHeadersClientInput, NullAndEmptyHeadersClientOutput, Any, Any + ], + Interceptor[ + NullAndEmptyHeadersServerInput, NullAndEmptyHeadersServerOutput, Any, Any + ], + Interceptor[ + OmitsNullSerializesEmptyStringInput, + OmitsNullSerializesEmptyStringOutput, + Any, + Any, + ], + Interceptor[ + OmitsSerializingEmptyListsInput, OmitsSerializingEmptyListsOutput, Any, Any + ], + Interceptor[OperationWithDefaultsInput, OperationWithDefaultsOutput, Any, Any], + Interceptor[ + OperationWithNestedStructureInput, + OperationWithNestedStructureOutput, + Any, + Any, + ], + Interceptor[OutputStreamInput, OutputStreamOutput, Any, Any], + Interceptor[ + OutputStreamWithInitialResponseInput, + OutputStreamWithInitialResponseOutput, + Any, + Any, + ], + Interceptor[PostPlayerActionInput, PostPlayerActionOutput, Any, Any], + Interceptor[PostUnionWithJsonNameInput, PostUnionWithJsonNameOutput, Any, Any], + Interceptor[ + PutWithContentEncodingInput, PutWithContentEncodingOutput, Any, Any + ], + Interceptor[ + QueryIdempotencyTokenAutoFillInput, + QueryIdempotencyTokenAutoFillOutput, + Any, + Any, + ], + Interceptor[ + QueryParamsAsStringListMapInput, QueryParamsAsStringListMapOutput, Any, Any + ], + Interceptor[QueryPrecedenceInput, QueryPrecedenceOutput, Any, Any], + Interceptor[RecursiveShapesInput, RecursiveShapesOutput, Any, Any], + Interceptor[ + ResponseCodeHttpFallbackInput, ResponseCodeHttpFallbackOutput, Any, Any + ], + Interceptor[ResponseCodeRequiredInput, ResponseCodeRequiredOutput, Any, Any], + Interceptor[ + SimpleScalarPropertiesInput, SimpleScalarPropertiesOutput, Any, Any + ], + Interceptor[SparseJsonListsInput, SparseJsonListsOutput, Any, Any], + Interceptor[SparseJsonMapsInput, SparseJsonMapsOutput, Any, Any], + Interceptor[StreamingTraitsInput, StreamingTraitsOutput, Any, Any], + Interceptor[ + StreamingTraitsRequireLengthInput, + StreamingTraitsRequireLengthOutput, + Any, + Any, + ], + Interceptor[ + StreamingTraitsWithMediaTypeInput, + StreamingTraitsWithMediaTypeOutput, + Any, + Any, + ], + Interceptor[TestBodyStructureInput, TestBodyStructureOutput, Any, Any], + Interceptor[ + TestGetNoInputNoPayloadInput, TestGetNoInputNoPayloadOutput, Any, Any + ], + Interceptor[TestGetNoPayloadInput, TestGetNoPayloadOutput, Any, Any], + Interceptor[TestPayloadBlobInput, TestPayloadBlobOutput, Any, Any], + Interceptor[TestPayloadStructureInput, TestPayloadStructureOutput, Any, Any], + Interceptor[ + TestPostNoInputNoPayloadInput, TestPostNoInputNoPayloadOutput, Any, Any + ], + Interceptor[TestPostNoPayloadInput, TestPostNoPayloadOutput, Any, Any], + Interceptor[ + TimestampFormatHeadersInput, TimestampFormatHeadersOutput, Any, Any + ], + Interceptor[UnitInputAndOutputInput, UnitInputAndOutputOutput, Any, Any], + ] +else: + _ServiceInterceptor = Any + +_PROTOCOL_SETTINGS = ProtocolSettings( + namespace="aws.protocoltests.restjson", + service_target="RestJson", + service_schema=REST_JSON, +) + + +class _RestJsonProtocolConfigOverrides(AwsConfigOverrides, total=False): + endpoint_resolver: EndpointResolver | None + protocol: ( + ClientProtocol[Any, Any] | ProtocolConstructor[ClientProtocol[Any, Any]] | None + ) + auth_schemes: dict[ShapeID, AuthScheme[Any, Any, Any, Any]] | None + auth_scheme_resolver: AuthSchemeResolver | None + + +@dataclass(kw_only=True, repr=False, init=False) +class RestJsonProtocolConfig(AwsConfig): + """Rest Json Protocol configuration.""" + + endpoint_resolver: EndpointResolver | None = None + """ + The endpoint resolver used to resolve the final endpoint per-operation + based on the configuration. + """ + + protocol: ClientProtocol[Any, Any] | None = None + """ + Pass a protocol class reference from smithy_aws_core.protocols to select + the protocol, e.g. protocol=AwsJson10ClientProtocol. For custom + protocols a protocol instance may also be passed. + """ + + interceptors: list[_ServiceInterceptor] = field(default_factory=lambda: []) + """ + The list of interceptors, which are hooks that are called during the + execution of a request. + """ + + auth_schemes: dict[ShapeID, AuthScheme[Any, Any, Any, Any]] | None = None + """A map of auth scheme ids to auth schemes.""" + + auth_scheme_resolver: AuthSchemeResolver | None = None + """ + An auth scheme resolver that determines the auth scheme for each + operation. + """ + + aws_credentials_identity_resolver: ( + IdentityResolver[AWSCredentialsIdentity, AWSIdentityProperties] | None + ) = None + """Resolves AWS Credentials. Required for operations that use Sigv4 Auth.""" + + region: str | None = None + """ + The AWS region to connect to. The configured region is used to determine + the service endpoint. + """ + + aws_access_key_id: str | None = None + """The identifier for a secret access key.""" + + aws_secret_access_key: str | None = None + """A secret access key that can be used to sign requests.""" + + aws_session_token: str | None = None + """The session token used with temporary AWS credentials.""" + + user_agent_extra: str | None = None + """Additional suffix to be added to the User-Agent header.""" + + sdk_ua_app_id: str | None = None + """ + A unique and opaque application ID that is appended to the User-Agent + header. + """ + + _FIELDS: ClassVar[dict[str, FieldSpec]] = { + "aws_credentials_identity_resolver": FieldSpec(default=None), + "region": FieldSpec(default=None), + "aws_access_key_id": FieldSpec(default=None), + "aws_secret_access_key": FieldSpec(default=None), + "aws_session_token": FieldSpec(default=None), + "user_agent_extra": FieldSpec(default=None), + "sdk_ua_app_id": FieldSpec(default=None), + **AwsConfig._FIELDS, + "endpoint_uri": FieldSpec( + default=None, resolver=EndpointUriResolver("rest_json_protocol") + ), + "endpoint_resolver": FieldSpec( + default_factory=lambda: StandardRegionalEndpointsResolver( + endpoint_prefix="restjson" + ) + ), + "protocol": FieldSpec( + default_factory=lambda: RestJsonClientProtocol(_PROTOCOL_SETTINGS), + converter=lambda p: p(_PROTOCOL_SETTINGS) if isinstance(p, type) else p, + ), + "auth_schemes": FieldSpec( + default_factory=lambda: { + ShapeID("aws.auth#sigv4"): SigV4AuthScheme(service="restjson") + } + ), + "auth_scheme_resolver": FieldSpec(default_factory=HTTPAuthSchemeResolver), + "transport": FieldSpec(default_factory=lambda: Urllib3HTTPClient()), + } + + def set_auth_scheme(self, scheme: AuthScheme[Any, Any, Any, Any]) -> None: + """ + Set an auth scheme implementation using its scheme ID. :param scheme: + The auth scheme to add or replace. + """ + auth_schemes = dict(self.auth_schemes or {}) + auth_schemes[scheme.scheme_id] = scheme + self.auth_schemes = auth_schemes + + @classmethod + def resolve( # pyright: ignore[reportIncompatibleMethodOverride] + cls, + *, + profile: str | None = None, + fs: FileSystem | None = None, + config_file_path: str | None = None, + credentials_file_path: str | None = None, + **overrides: Unpack[_RestJsonProtocolConfigOverrides], + ) -> Self: + """ + Resolve config from environment, config files, defaults, and explicit + overrides. + """ + return cls._resolve( + profile=profile, + fs=fs, + config_file_path=config_file_path, + credentials_file_path=credentials_file_path, + overrides=overrides, + ) + + +Plugin: TypeAlias = Callable[[RestJsonProtocolConfig], None] +""" +A callable that customizes a client configuration. Service-level plugins +are applied once to the base configuration inherited by every operation. +Operation-level plugins apply only to a single operation invocation. +""" diff --git a/codegen-output/rest-json-1/src/restjson/sync/user_agent.py b/codegen-output/rest-json-1/src/restjson/sync/user_agent.py new file mode 100644 index 000000000..1661a61ac --- /dev/null +++ b/codegen-output/rest-json-1/src/restjson/sync/user_agent.py @@ -0,0 +1,17 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from smithy_aws_core.interceptors.user_agent import UserAgentInterceptor + +from .. import __version__ +from .config import RestJsonProtocolConfig + + +def aws_user_agent_plugin(config: RestJsonProtocolConfig): + config.interceptors.append( + UserAgentInterceptor( + ua_suffix=config.user_agent_extra, + ua_app_id=config.sdk_ua_app_id, + sdk_version=__version__, + service_id="Rest_Json_Protocol", + ) + ) diff --git a/codegen-output/rest-xml-with-namespace/pyproject.toml b/codegen-output/rest-xml-with-namespace/pyproject.toml index 346956bfb..040d4ccbc 100644 --- a/codegen-output/rest-xml-with-namespace/pyproject.toml +++ b/codegen-output/rest-xml-with-namespace/pyproject.toml @@ -26,7 +26,7 @@ classifiers = [ dependencies = [ "smithy_aws_core[xml]~=0.12.0", "smithy_core~=0.9.0", - "smithy_http[aiohttp]~=0.6.0" + "smithy_http[aiohttp, urllib3]~=0.6.0" ] [project.optional-dependencies] diff --git a/codegen-output/rest-xml-with-namespace/src/restxmlwithnamespace/__init__.py b/codegen-output/rest-xml-with-namespace/src/restxmlwithnamespace/__init__.py index 30f6d44e9..49d5188aa 100644 --- a/codegen-output/rest-xml-with-namespace/src/restxmlwithnamespace/__init__.py +++ b/codegen-output/rest-xml-with-namespace/src/restxmlwithnamespace/__init__.py @@ -1,3 +1,20 @@ # Code generated by smithy-python-codegen DO NOT EDIT. +from typing import Any, TYPE_CHECKING + + +if TYPE_CHECKING: + from .sync import RestXmlProtocolNamespaceClient, RestXmlProtocolNamespaceConfig + +__all__ = ["RestXmlProtocolNamespaceClient", "RestXmlProtocolNamespaceConfig"] + + +def __getattr__(name: str) -> Any: + if name in __all__: + from . import sync + + return getattr(sync, name) + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + __version__: str = "0.0.1" diff --git a/codegen-output/rest-xml-with-namespace/src/restxmlwithnamespace/sync/__init__.py b/codegen-output/rest-xml-with-namespace/src/restxmlwithnamespace/sync/__init__.py new file mode 100644 index 000000000..cb044a249 --- /dev/null +++ b/codegen-output/rest-xml-with-namespace/src/restxmlwithnamespace/sync/__init__.py @@ -0,0 +1,6 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from .client import RestXmlProtocolNamespaceClient +from .config import RestXmlProtocolNamespaceConfig, Plugin + +__all__ = ["RestXmlProtocolNamespaceClient", "RestXmlProtocolNamespaceConfig", "Plugin"] diff --git a/codegen-output/rest-xml-with-namespace/src/restxmlwithnamespace/sync/client.py b/codegen-output/rest-xml-with-namespace/src/restxmlwithnamespace/sync/client.py new file mode 100644 index 000000000..2ba813187 --- /dev/null +++ b/codegen-output/rest-xml-with-namespace/src/restxmlwithnamespace/sync/client.py @@ -0,0 +1,173 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from __future__ import annotations + +from copy import deepcopy +import logging +from typing import Any, cast + +from smithy_aws_core.config import ConfigSource +from smithy_aws_core.identity import AWSCredentialsIdentity +from smithy_aws_core.identity.chain import IdentityChain +from smithy_core.client import Client, ClientCall, RequestPipeline +from smithy_core.deserializers import DeserializeableShape +from smithy_core.exceptions import ExpectationNotMetError +from smithy_core.schemas import APIOperation +from smithy_core.serializers import SerializeableShape +from smithy_http.interfaces import HTTPClient +from smithy_http.plugins import user_agent_plugin + +from ..models import SIMPLE_SCALAR_PROPERTIES +from .config import Plugin, RestXmlProtocolNamespaceConfig +from .user_agent import aws_user_agent_plugin + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from ..models import SimpleScalarPropertiesInput, SimpleScalarPropertiesOutput + + +logger = logging.getLogger(__name__) + + +class RestXmlProtocolNamespaceClient(Client): + """ + A REST XML service that sends XML requests and responses. This service + and test case is complementary to the test cases in the `restXml` + directory, but the service under test here has the `xmlNamespace` + trait applied to it. See + https://github.com/smithy-lang/smithy/issues/616 + """ + + def __init__( + self, + config: RestXmlProtocolNamespaceConfig | None = None, + plugins: list[Plugin] | None = None, + ): + """ + Constructor for `RestXmlProtocolNamespaceClient`. + + Args: + config: + Optional configuration for the client. Here you can set things like + the endpoint for HTTP services or auth credentials. + plugins: + A list of callables applied once to the client's base configuration. + Their changes are inherited by every operation invocation. + """ + super().__init__() + self._config = config + self._plugins = plugins + self._client_plugins: list[Plugin] = [aws_user_agent_plugin, user_agent_plugin] + + def _ensure_setup(self) -> RestXmlProtocolNamespaceConfig: + if not self._setup_done: + with self._derive_lock: + if self._closed: + raise RuntimeError( + "Cannot invoke an operation on a client that has been closed." + ) + if not self._setup_done: + if self._config is None: + config = RestXmlProtocolNamespaceConfig.resolve() + else: + # Copy so plugins don't mutate the caller's config. + config = deepcopy(self._config) + for plugin in self._client_plugins: + plugin(config) + if self._plugins: + for plugin in self._plugins: + plugin(config) + # Publish state only after setup fully succeeds, so a + # failed _post_setup leaves the caller's config untouched + # (no re-applied plugins) and the transport closeable. + self._post_setup(config) + self._transport = config.transport + self._config = config + self._setup_done = True + assert self._config is not None + return self._config + + def _post_setup(self, config: RestXmlProtocolNamespaceConfig) -> None: + if config.aws_credentials_identity_resolver is None: + config_context = config.resolution_context() + config_file = None + profile_name = None + if config_context is not None: + config_file = config_context.parsed_profiles() + if config_context.profile_source is ConfigSource.OVERRIDE: + profile_name = config_context.profile_name + config.aws_credentials_identity_resolver = IdentityChain.create( + AWSCredentialsIdentity, + config_file=config_file, + profile_name=profile_name, + region_override=config.region, + http_client=cast(HTTPClient | None, config.transport), + ) + + def _prepare_call[I: SerializeableShape, O: DeserializeableShape]( + self, + input: I, + operation: APIOperation[I, O], + default_plugins: list[Plugin], + plugins: list[Plugin] | None, + ) -> tuple[RequestPipeline[Any, Any], ClientCall[I, O]]: + if self._closed: + raise RuntimeError( + "Cannot invoke an operation on a client that has been closed." + ) + config = self._ensure_setup() + if default_plugins or plugins: + # Keep operation-plugin mutations scoped to this call. + config = deepcopy(config) + for plugin in default_plugins: + plugin(config) + if plugins: + for plugin in plugins: + plugin(config) + if ( + config.protocol is None + or config.transport is None + or config.endpoint_resolver is None + or config.auth_scheme_resolver is None + or config.auth_schemes is None + ): + raise ExpectationNotMetError( + "protocol, transport, endpoint_resolver, auth_scheme_resolver," + " and auth_schemes MUST be set on the config to make calls." + ) + retry_strategy = self._retry_strategy_resolver.resolve_retry_strategy( + retry_strategy=config.retry_strategy, + retry_mode=config.retry_mode, + max_attempts=config.max_attempts, + ) + return self._build_call( + input, operation, config=config, retry_strategy=retry_strategy + ) + + def simple_scalar_properties( + self, + input: SimpleScalarPropertiesInput | None = None, + plugins: list[Plugin] | None = None, + ) -> SimpleScalarPropertiesOutput: + """ + Invokes the SimpleScalarProperties operation. + + Args: + input: + An instance of `SimpleScalarPropertiesInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `SimpleScalarPropertiesOutput`. + """ + if input is None: + input = SIMPLE_SCALAR_PROPERTIES.input() + pipeline, call = self._prepare_call( + input, SIMPLE_SCALAR_PROPERTIES, [], plugins + ) + return pipeline(call) diff --git a/codegen-output/rest-xml-with-namespace/src/restxmlwithnamespace/sync/config.py b/codegen-output/rest-xml-with-namespace/src/restxmlwithnamespace/sync/config.py new file mode 100644 index 000000000..bee3b05c8 --- /dev/null +++ b/codegen-output/rest-xml-with-namespace/src/restxmlwithnamespace/sync/config.py @@ -0,0 +1,199 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from dataclasses import dataclass, field +from typing import ( + Any, + Callable, + ClassVar, + Self, + TYPE_CHECKING, + TypeAlias, + Union, + Unpack, +) + +from smithy_aws_core.auth.sigv4 import SigV4AuthScheme +from smithy_aws_core.config import FileSystem +from smithy_aws_core.config.aws_config import AwsConfig, AwsConfigOverrides +from smithy_aws_core.config.resolvers import EndpointUriResolver +from smithy_aws_core.config.types import FieldSpec +from smithy_aws_core.endpoints.standard_regional import ( + StandardRegionalEndpointsResolver, +) +from smithy_aws_core.identity import AWSCredentialsIdentity, AWSIdentityProperties +from smithy_aws_core.protocols import RestXmlClientProtocol +from smithy_core.interfaces import ( + ClientProtocol, + EndpointResolver, + ProtocolConstructor, + ProtocolSettings, +) +from smithy_core.interfaces.auth import AuthScheme, AuthSchemeResolver +from smithy_core.interfaces.identity import IdentityResolver +from smithy_core.shapes import ShapeID +from smithy_http.urllib3 import Urllib3HTTPClient + +from .._private.schemas import REST_XML_WITH_NAMESPACE +from ..auth import HTTPAuthSchemeResolver + + +if TYPE_CHECKING: + from smithy_core.interceptors import Interceptor + from ..models import SimpleScalarPropertiesInput, SimpleScalarPropertiesOutput + + _ServiceInterceptor = Union[ + Interceptor[ + SimpleScalarPropertiesInput, SimpleScalarPropertiesOutput, Any, Any + ], + ] +else: + _ServiceInterceptor = Any + +_PROTOCOL_SETTINGS = ProtocolSettings( + namespace="aws.protocoltests.restxml.xmlns", + service_target="RestXmlWithNamespace", + xml_namespace="https://example.com", + service_schema=REST_XML_WITH_NAMESPACE, +) + + +class _RestXmlProtocolNamespaceConfigOverrides(AwsConfigOverrides, total=False): + endpoint_resolver: EndpointResolver | None + protocol: ( + ClientProtocol[Any, Any] | ProtocolConstructor[ClientProtocol[Any, Any]] | None + ) + auth_schemes: dict[ShapeID, AuthScheme[Any, Any, Any, Any]] | None + auth_scheme_resolver: AuthSchemeResolver | None + + +@dataclass(kw_only=True, repr=False, init=False) +class RestXmlProtocolNamespaceConfig(AwsConfig): + """Rest Xml Protocol Namespace configuration.""" + + endpoint_resolver: EndpointResolver | None = None + """ + The endpoint resolver used to resolve the final endpoint per-operation + based on the configuration. + """ + + protocol: ClientProtocol[Any, Any] | None = None + """ + Pass a protocol class reference from smithy_aws_core.protocols to select + the protocol, e.g. protocol=AwsJson10ClientProtocol. For custom + protocols a protocol instance may also be passed. + """ + + interceptors: list[_ServiceInterceptor] = field(default_factory=lambda: []) + """ + The list of interceptors, which are hooks that are called during the + execution of a request. + """ + + auth_schemes: dict[ShapeID, AuthScheme[Any, Any, Any, Any]] | None = None + """A map of auth scheme ids to auth schemes.""" + + auth_scheme_resolver: AuthSchemeResolver | None = None + """ + An auth scheme resolver that determines the auth scheme for each + operation. + """ + + aws_credentials_identity_resolver: ( + IdentityResolver[AWSCredentialsIdentity, AWSIdentityProperties] | None + ) = None + """Resolves AWS Credentials. Required for operations that use Sigv4 Auth.""" + + region: str | None = None + """ + The AWS region to connect to. The configured region is used to determine + the service endpoint. + """ + + aws_access_key_id: str | None = None + """The identifier for a secret access key.""" + + aws_secret_access_key: str | None = None + """A secret access key that can be used to sign requests.""" + + aws_session_token: str | None = None + """The session token used with temporary AWS credentials.""" + + user_agent_extra: str | None = None + """Additional suffix to be added to the User-Agent header.""" + + sdk_ua_app_id: str | None = None + """ + A unique and opaque application ID that is appended to the User-Agent + header. + """ + + _FIELDS: ClassVar[dict[str, FieldSpec]] = { + "aws_credentials_identity_resolver": FieldSpec(default=None), + "region": FieldSpec(default=None), + "aws_access_key_id": FieldSpec(default=None), + "aws_secret_access_key": FieldSpec(default=None), + "aws_session_token": FieldSpec(default=None), + "user_agent_extra": FieldSpec(default=None), + "sdk_ua_app_id": FieldSpec(default=None), + **AwsConfig._FIELDS, + "endpoint_uri": FieldSpec( + default=None, resolver=EndpointUriResolver("rest_xml_protocol_namespace") + ), + "endpoint_resolver": FieldSpec( + default_factory=lambda: StandardRegionalEndpointsResolver( + endpoint_prefix="restxmlwithnamespace" + ) + ), + "protocol": FieldSpec( + default_factory=lambda: RestXmlClientProtocol(_PROTOCOL_SETTINGS), + converter=lambda p: p(_PROTOCOL_SETTINGS) if isinstance(p, type) else p, + ), + "auth_schemes": FieldSpec( + default_factory=lambda: { + ShapeID("aws.auth#sigv4"): SigV4AuthScheme( + service="restxmlwithnamespace" + ) + } + ), + "auth_scheme_resolver": FieldSpec(default_factory=HTTPAuthSchemeResolver), + "transport": FieldSpec(default_factory=lambda: Urllib3HTTPClient()), + } + + def set_auth_scheme(self, scheme: AuthScheme[Any, Any, Any, Any]) -> None: + """ + Set an auth scheme implementation using its scheme ID. :param scheme: + The auth scheme to add or replace. + """ + auth_schemes = dict(self.auth_schemes or {}) + auth_schemes[scheme.scheme_id] = scheme + self.auth_schemes = auth_schemes + + @classmethod + def resolve( # pyright: ignore[reportIncompatibleMethodOverride] + cls, + *, + profile: str | None = None, + fs: FileSystem | None = None, + config_file_path: str | None = None, + credentials_file_path: str | None = None, + **overrides: Unpack[_RestXmlProtocolNamespaceConfigOverrides], + ) -> Self: + """ + Resolve config from environment, config files, defaults, and explicit + overrides. + """ + return cls._resolve( + profile=profile, + fs=fs, + config_file_path=config_file_path, + credentials_file_path=credentials_file_path, + overrides=overrides, + ) + + +Plugin: TypeAlias = Callable[[RestXmlProtocolNamespaceConfig], None] +""" +A callable that customizes a client configuration. Service-level plugins +are applied once to the base configuration inherited by every operation. +Operation-level plugins apply only to a single operation invocation. +""" diff --git a/codegen-output/rest-xml-with-namespace/src/restxmlwithnamespace/sync/user_agent.py b/codegen-output/rest-xml-with-namespace/src/restxmlwithnamespace/sync/user_agent.py new file mode 100644 index 000000000..6bc71844c --- /dev/null +++ b/codegen-output/rest-xml-with-namespace/src/restxmlwithnamespace/sync/user_agent.py @@ -0,0 +1,17 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from smithy_aws_core.interceptors.user_agent import UserAgentInterceptor + +from .. import __version__ +from .config import RestXmlProtocolNamespaceConfig + + +def aws_user_agent_plugin(config: RestXmlProtocolNamespaceConfig): + config.interceptors.append( + UserAgentInterceptor( + ua_suffix=config.user_agent_extra, + ua_app_id=config.sdk_ua_app_id, + sdk_version=__version__, + service_id="Rest_Xml_Protocol_Namespace", + ) + ) diff --git a/codegen-output/rest-xml/pyproject.toml b/codegen-output/rest-xml/pyproject.toml index 5eed18642..ae259e641 100644 --- a/codegen-output/rest-xml/pyproject.toml +++ b/codegen-output/rest-xml/pyproject.toml @@ -26,7 +26,7 @@ classifiers = [ dependencies = [ "smithy_aws_core[xml]~=0.12.0", "smithy_core~=0.9.0", - "smithy_http[aiohttp]~=0.6.0" + "smithy_http[aiohttp, urllib3]~=0.6.0" ] [project.optional-dependencies] diff --git a/codegen-output/rest-xml/src/restxml/__init__.py b/codegen-output/rest-xml/src/restxml/__init__.py index 30f6d44e9..468e78962 100644 --- a/codegen-output/rest-xml/src/restxml/__init__.py +++ b/codegen-output/rest-xml/src/restxml/__init__.py @@ -1,3 +1,20 @@ # Code generated by smithy-python-codegen DO NOT EDIT. +from typing import Any, TYPE_CHECKING + + +if TYPE_CHECKING: + from .sync import RestXmlProtocolClient, RestXmlProtocolConfig + +__all__ = ["RestXmlProtocolClient", "RestXmlProtocolConfig"] + + +def __getattr__(name: str) -> Any: + if name in __all__: + from . import sync + + return getattr(sync, name) + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + __version__: str = "0.0.1" diff --git a/codegen-output/rest-xml/src/restxml/sync/__init__.py b/codegen-output/rest-xml/src/restxml/sync/__init__.py new file mode 100644 index 000000000..d85681dd0 --- /dev/null +++ b/codegen-output/rest-xml/src/restxml/sync/__init__.py @@ -0,0 +1,6 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from .client import RestXmlProtocolClient +from .config import RestXmlProtocolConfig, Plugin + +__all__ = ["RestXmlProtocolClient", "RestXmlProtocolConfig", "Plugin"] diff --git a/codegen-output/rest-xml/src/restxml/sync/client.py b/codegen-output/rest-xml/src/restxml/sync/client.py new file mode 100644 index 000000000..5f2a8a8fa --- /dev/null +++ b/codegen-output/rest-xml/src/restxml/sync/client.py @@ -0,0 +1,2043 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from __future__ import annotations + +from copy import deepcopy +import logging +from typing import Any, cast + +from smithy_aws_core.config import ConfigSource +from smithy_aws_core.identity import AWSCredentialsIdentity +from smithy_aws_core.identity.chain import IdentityChain +from smithy_core.client import Client, ClientCall, RequestPipeline +from smithy_core.deserializers import DeserializeableShape +from smithy_core.exceptions import ExpectationNotMetError +from smithy_core.schemas import APIOperation +from smithy_core.serializers import SerializeableShape +from smithy_http.interfaces import HTTPClient +from smithy_http.plugins import user_agent_plugin + +from ..models import ( + ALL_QUERY_STRING_TYPES, + BODY_WITH_XML_NAME, + CONSTANT_AND_VARIABLE_QUERY_STRING, + CONSTANT_QUERY_STRING, + CONTENT_TYPE_PARAMETERS, + DATETIME_OFFSETS, + EMPTY_INPUT_AND_EMPTY_OUTPUT, + ENDPOINT_OPERATION, + ENDPOINT_WITH_HOST_LABEL_HEADER_OPERATION, + ENDPOINT_WITH_HOST_LABEL_OPERATION, + FLATTENED_XML_MAP, + FLATTENED_XML_MAP_WITH_XML_NAME, + FLATTENED_XML_MAP_WITH_XML_NAMESPACE, + FRACTIONAL_SECONDS, + GREETING_WITH_ERRORS, + HTTP_EMPTY_PREFIX_HEADERS, + HTTP_ENUM_PAYLOAD, + HTTP_PAYLOAD_TRAITS, + HTTP_PAYLOAD_TRAITS_WITH_MEDIA_TYPE, + HTTP_PAYLOAD_WITH_MEMBER_XML_NAME, + HTTP_PAYLOAD_WITH_STRUCTURE, + HTTP_PAYLOAD_WITH_UNION, + HTTP_PAYLOAD_WITH_XML_NAME, + HTTP_PAYLOAD_WITH_XML_NAMESPACE, + HTTP_PAYLOAD_WITH_XML_NAMESPACE_AND_PREFIX, + HTTP_PREFIX_HEADERS, + HTTP_REQUEST_WITH_FLOAT_LABELS, + HTTP_REQUEST_WITH_GREEDY_LABEL_IN_PATH, + HTTP_REQUEST_WITH_LABELS, + HTTP_REQUEST_WITH_LABELS_AND_TIMESTAMP_FORMAT, + HTTP_RESPONSE_CODE, + HTTP_STRING_PAYLOAD, + IGNORE_QUERY_PARAMS_IN_RESPONSE, + INPUT_AND_OUTPUT_WITH_HEADERS, + NESTED_XML_MAPS, + NESTED_XML_MAP_WITH_XML_NAME, + NO_INPUT_AND_NO_OUTPUT, + NO_INPUT_AND_OUTPUT, + NULL_AND_EMPTY_HEADERS_CLIENT, + NULL_AND_EMPTY_HEADERS_SERVER, + OMITS_NULL_SERIALIZES_EMPTY_STRING, + PUT_WITH_CONTENT_ENCODING, + QUERY_IDEMPOTENCY_TOKEN_AUTO_FILL, + QUERY_PARAMS_AS_STRING_LIST_MAP, + QUERY_PRECEDENCE, + RECURSIVE_SHAPES, + SIMPLE_SCALAR_PROPERTIES, + TIMESTAMP_FORMAT_HEADERS, + XML_ATTRIBUTES, + XML_ATTRIBUTES_IN_MIDDLE, + XML_ATTRIBUTES_ON_PAYLOAD, + XML_BLOBS, + XML_EMPTY_BLOBS, + XML_EMPTY_LISTS, + XML_EMPTY_MAPS, + XML_EMPTY_STRINGS, + XML_ENUMS, + XML_INT_ENUMS, + XML_LISTS, + XML_MAPS, + XML_MAPS_XML_NAME, + XML_MAP_WITH_XML_NAMESPACE, + XML_NAMESPACES, + XML_TIMESTAMPS, + XML_UNIONS, +) +from .config import Plugin, RestXmlProtocolConfig +from .user_agent import aws_user_agent_plugin + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from ..models import ( + AllQueryStringTypesInput, + AllQueryStringTypesOutput, + BodyWithXmlNameInput, + BodyWithXmlNameOutput, + ConstantAndVariableQueryStringInput, + ConstantAndVariableQueryStringOutput, + ConstantQueryStringInput, + ConstantQueryStringOutput, + ContentTypeParametersInput, + ContentTypeParametersOutput, + DatetimeOffsetsInput, + DatetimeOffsetsOutput, + EmptyInputAndEmptyOutputInput, + EmptyInputAndEmptyOutputOutput, + EndpointOperationInput, + EndpointOperationOutput, + EndpointWithHostLabelHeaderOperationInput, + EndpointWithHostLabelHeaderOperationOutput, + EndpointWithHostLabelOperationInput, + EndpointWithHostLabelOperationOutput, + FlattenedXmlMapInput, + FlattenedXmlMapOutput, + FlattenedXmlMapWithXmlNameInput, + FlattenedXmlMapWithXmlNameOutput, + FlattenedXmlMapWithXmlNamespaceInput, + FlattenedXmlMapWithXmlNamespaceOutput, + FractionalSecondsInput, + FractionalSecondsOutput, + GreetingWithErrorsInput, + GreetingWithErrorsOutput, + HttpEmptyPrefixHeadersInput, + HttpEmptyPrefixHeadersOutput, + HttpEnumPayloadInput, + HttpEnumPayloadOutput, + HttpPayloadTraitsInput, + HttpPayloadTraitsOutput, + HttpPayloadTraitsWithMediaTypeInput, + HttpPayloadTraitsWithMediaTypeOutput, + HttpPayloadWithMemberXmlNameInput, + HttpPayloadWithMemberXmlNameOutput, + HttpPayloadWithStructureInput, + HttpPayloadWithStructureOutput, + HttpPayloadWithUnionInput, + HttpPayloadWithUnionOutput, + HttpPayloadWithXmlNameInput, + HttpPayloadWithXmlNameOutput, + HttpPayloadWithXmlNamespaceAndPrefixInput, + HttpPayloadWithXmlNamespaceAndPrefixOutput, + HttpPayloadWithXmlNamespaceInput, + HttpPayloadWithXmlNamespaceOutput, + HttpPrefixHeadersInput, + HttpPrefixHeadersOutput, + HttpRequestWithFloatLabelsInput, + HttpRequestWithFloatLabelsOutput, + HttpRequestWithGreedyLabelInPathInput, + HttpRequestWithGreedyLabelInPathOutput, + HttpRequestWithLabelsAndTimestampFormatInput, + HttpRequestWithLabelsAndTimestampFormatOutput, + HttpRequestWithLabelsInput, + HttpRequestWithLabelsOutput, + HttpResponseCodeInput, + HttpResponseCodeOutput, + HttpStringPayloadInput, + HttpStringPayloadOutput, + IgnoreQueryParamsInResponseInput, + IgnoreQueryParamsInResponseOutput, + InputAndOutputWithHeadersInput, + InputAndOutputWithHeadersOutput, + NestedXmlMapWithXmlNameInput, + NestedXmlMapWithXmlNameOutput, + NestedXmlMapsInput, + NestedXmlMapsOutput, + NoInputAndNoOutputInput, + NoInputAndNoOutputOutput, + NoInputAndOutputInput, + NoInputAndOutputOutput, + NullAndEmptyHeadersClientInput, + NullAndEmptyHeadersClientOutput, + NullAndEmptyHeadersServerInput, + NullAndEmptyHeadersServerOutput, + OmitsNullSerializesEmptyStringInput, + OmitsNullSerializesEmptyStringOutput, + PutWithContentEncodingInput, + PutWithContentEncodingOutput, + QueryIdempotencyTokenAutoFillInput, + QueryIdempotencyTokenAutoFillOutput, + QueryParamsAsStringListMapInput, + QueryParamsAsStringListMapOutput, + QueryPrecedenceInput, + QueryPrecedenceOutput, + RecursiveShapesInput, + RecursiveShapesOutput, + SimpleScalarPropertiesInput, + SimpleScalarPropertiesOutput, + TimestampFormatHeadersInput, + TimestampFormatHeadersOutput, + XmlAttributesInMiddleInput, + XmlAttributesInMiddleOutput, + XmlAttributesInput, + XmlAttributesOnPayloadInput, + XmlAttributesOnPayloadOutput, + XmlAttributesOutput, + XmlBlobsInput, + XmlBlobsOutput, + XmlEmptyBlobsInput, + XmlEmptyBlobsOutput, + XmlEmptyListsInput, + XmlEmptyListsOutput, + XmlEmptyMapsInput, + XmlEmptyMapsOutput, + XmlEmptyStringsInput, + XmlEmptyStringsOutput, + XmlEnumsInput, + XmlEnumsOutput, + XmlIntEnumsInput, + XmlIntEnumsOutput, + XmlListsInput, + XmlListsOutput, + XmlMapWithXmlNamespaceInput, + XmlMapWithXmlNamespaceOutput, + XmlMapsInput, + XmlMapsOutput, + XmlMapsXmlNameInput, + XmlMapsXmlNameOutput, + XmlNamespacesInput, + XmlNamespacesOutput, + XmlTimestampsInput, + XmlTimestampsOutput, + XmlUnionsInput, + XmlUnionsOutput, + ) + + +logger = logging.getLogger(__name__) + + +class RestXmlProtocolClient(Client): + """A REST XML service that sends XML requests and responses.""" + + def __init__( + self, + config: RestXmlProtocolConfig | None = None, + plugins: list[Plugin] | None = None, + ): + """ + Constructor for `RestXmlProtocolClient`. + + Args: + config: + Optional configuration for the client. Here you can set things like + the endpoint for HTTP services or auth credentials. + plugins: + A list of callables applied once to the client's base configuration. + Their changes are inherited by every operation invocation. + """ + super().__init__() + self._config = config + self._plugins = plugins + self._client_plugins: list[Plugin] = [aws_user_agent_plugin, user_agent_plugin] + + def _ensure_setup(self) -> RestXmlProtocolConfig: + if not self._setup_done: + with self._derive_lock: + if self._closed: + raise RuntimeError( + "Cannot invoke an operation on a client that has been closed." + ) + if not self._setup_done: + if self._config is None: + config = RestXmlProtocolConfig.resolve() + else: + # Copy so plugins don't mutate the caller's config. + config = deepcopy(self._config) + for plugin in self._client_plugins: + plugin(config) + if self._plugins: + for plugin in self._plugins: + plugin(config) + # Publish state only after setup fully succeeds, so a + # failed _post_setup leaves the caller's config untouched + # (no re-applied plugins) and the transport closeable. + self._post_setup(config) + self._transport = config.transport + self._config = config + self._setup_done = True + assert self._config is not None + return self._config + + def _post_setup(self, config: RestXmlProtocolConfig) -> None: + if config.aws_credentials_identity_resolver is None: + config_context = config.resolution_context() + config_file = None + profile_name = None + if config_context is not None: + config_file = config_context.parsed_profiles() + if config_context.profile_source is ConfigSource.OVERRIDE: + profile_name = config_context.profile_name + config.aws_credentials_identity_resolver = IdentityChain.create( + AWSCredentialsIdentity, + config_file=config_file, + profile_name=profile_name, + region_override=config.region, + http_client=cast(HTTPClient | None, config.transport), + ) + + def _prepare_call[I: SerializeableShape, O: DeserializeableShape]( + self, + input: I, + operation: APIOperation[I, O], + default_plugins: list[Plugin], + plugins: list[Plugin] | None, + ) -> tuple[RequestPipeline[Any, Any], ClientCall[I, O]]: + if self._closed: + raise RuntimeError( + "Cannot invoke an operation on a client that has been closed." + ) + config = self._ensure_setup() + if default_plugins or plugins: + # Keep operation-plugin mutations scoped to this call. + config = deepcopy(config) + for plugin in default_plugins: + plugin(config) + if plugins: + for plugin in plugins: + plugin(config) + if ( + config.protocol is None + or config.transport is None + or config.endpoint_resolver is None + or config.auth_scheme_resolver is None + or config.auth_schemes is None + ): + raise ExpectationNotMetError( + "protocol, transport, endpoint_resolver, auth_scheme_resolver," + " and auth_schemes MUST be set on the config to make calls." + ) + retry_strategy = self._retry_strategy_resolver.resolve_retry_strategy( + retry_strategy=config.retry_strategy, + retry_mode=config.retry_mode, + max_attempts=config.max_attempts, + ) + return self._build_call( + input, operation, config=config, retry_strategy=retry_strategy + ) + + def all_query_string_types( + self, + input: AllQueryStringTypesInput | None = None, + plugins: list[Plugin] | None = None, + ) -> AllQueryStringTypesOutput: + """ + This example uses all query string types. + + Args: + input: + An instance of `AllQueryStringTypesInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `AllQueryStringTypesOutput`. + """ + if input is None: + input = ALL_QUERY_STRING_TYPES.input() + pipeline, call = self._prepare_call(input, ALL_QUERY_STRING_TYPES, [], plugins) + return pipeline(call) + + def body_with_xml_name( + self, + input: BodyWithXmlNameInput | None = None, + plugins: list[Plugin] | None = None, + ) -> BodyWithXmlNameOutput: + """ + The following example serializes a body that uses an XML name, changing + the wrapper name. + + Args: + input: + An instance of `BodyWithXmlNameInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `BodyWithXmlNameOutput`. + """ + if input is None: + input = BODY_WITH_XML_NAME.input() + pipeline, call = self._prepare_call(input, BODY_WITH_XML_NAME, [], plugins) + return pipeline(call) + + def constant_and_variable_query_string( + self, + input: ConstantAndVariableQueryStringInput | None = None, + plugins: list[Plugin] | None = None, + ) -> ConstantAndVariableQueryStringOutput: + """ + This example uses fixed query string params and variable query string + params. The fixed query string parameters and variable parameters must + both be serialized (implementations may need to merge them together). + + Args: + input: + An instance of `ConstantAndVariableQueryStringInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `ConstantAndVariableQueryStringOutput`. + """ + if input is None: + input = CONSTANT_AND_VARIABLE_QUERY_STRING.input() + pipeline, call = self._prepare_call( + input, CONSTANT_AND_VARIABLE_QUERY_STRING, [], plugins + ) + return pipeline(call) + + def constant_query_string( + self, input: ConstantQueryStringInput, plugins: list[Plugin] | None = None + ) -> ConstantQueryStringOutput: + """ + This example uses a constant query string parameters and a label. This + simply tests that labels and query string parameters are compatible. The + fixed query string parameter named \"hello\" should in no way conflict + with the label, `{hello}`. + + Args: + input: + An instance of `ConstantQueryStringInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `ConstantQueryStringOutput`. + """ + pipeline, call = self._prepare_call(input, CONSTANT_QUERY_STRING, [], plugins) + return pipeline(call) + + def content_type_parameters( + self, + input: ContentTypeParametersInput | None = None, + plugins: list[Plugin] | None = None, + ) -> ContentTypeParametersOutput: + """ + The example tests how servers must support requests containing a + `Content-Type` header with parameters. + + Args: + input: + An instance of `ContentTypeParametersInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `ContentTypeParametersOutput`. + """ + if input is None: + input = CONTENT_TYPE_PARAMETERS.input() + pipeline, call = self._prepare_call(input, CONTENT_TYPE_PARAMETERS, [], plugins) + return pipeline(call) + + def datetime_offsets( + self, + input: DatetimeOffsetsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> DatetimeOffsetsOutput: + """ + Invokes the DatetimeOffsets operation. + + Args: + input: + An instance of `DatetimeOffsetsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `DatetimeOffsetsOutput`. + """ + if input is None: + input = DATETIME_OFFSETS.input() + pipeline, call = self._prepare_call(input, DATETIME_OFFSETS, [], plugins) + return pipeline(call) + + def empty_input_and_empty_output( + self, + input: EmptyInputAndEmptyOutputInput | None = None, + plugins: list[Plugin] | None = None, + ) -> EmptyInputAndEmptyOutputOutput: + """ + The example tests how requests and responses are serialized when + there's no request or response payload because the operation has an + empty input and empty output structure that reuses the same shape. While + this should be rare, code generators must support this. + + Args: + input: + An instance of `EmptyInputAndEmptyOutputInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `EmptyInputAndEmptyOutputOutput`. + """ + if input is None: + input = EMPTY_INPUT_AND_EMPTY_OUTPUT.input() + pipeline, call = self._prepare_call( + input, EMPTY_INPUT_AND_EMPTY_OUTPUT, [], plugins + ) + return pipeline(call) + + def endpoint_operation( + self, + input: EndpointOperationInput | None = None, + plugins: list[Plugin] | None = None, + ) -> EndpointOperationOutput: + """ + Invokes the EndpointOperation operation. + + Args: + input: + An instance of `EndpointOperationInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `EndpointOperationOutput`. + """ + if input is None: + input = ENDPOINT_OPERATION.input() + pipeline, call = self._prepare_call(input, ENDPOINT_OPERATION, [], plugins) + return pipeline(call) + + def endpoint_with_host_label_header_operation( + self, + input: EndpointWithHostLabelHeaderOperationInput, + plugins: list[Plugin] | None = None, + ) -> EndpointWithHostLabelHeaderOperationOutput: + """ + Invokes the EndpointWithHostLabelHeaderOperation operation. + + Args: + input: + An instance of `EndpointWithHostLabelHeaderOperationInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `EndpointWithHostLabelHeaderOperationOutput`. + """ + pipeline, call = self._prepare_call( + input, ENDPOINT_WITH_HOST_LABEL_HEADER_OPERATION, [], plugins + ) + return pipeline(call) + + def endpoint_with_host_label_operation( + self, + input: EndpointWithHostLabelOperationInput, + plugins: list[Plugin] | None = None, + ) -> EndpointWithHostLabelOperationOutput: + """ + Invokes the EndpointWithHostLabelOperation operation. + + Args: + input: + An instance of `EndpointWithHostLabelOperationInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `EndpointWithHostLabelOperationOutput`. + """ + pipeline, call = self._prepare_call( + input, ENDPOINT_WITH_HOST_LABEL_OPERATION, [], plugins + ) + return pipeline(call) + + def flattened_xml_map( + self, + input: FlattenedXmlMapInput | None = None, + plugins: list[Plugin] | None = None, + ) -> FlattenedXmlMapOutput: + """ + Flattened maps + + Args: + input: + An instance of `FlattenedXmlMapInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `FlattenedXmlMapOutput`. + """ + if input is None: + input = FLATTENED_XML_MAP.input() + pipeline, call = self._prepare_call(input, FLATTENED_XML_MAP, [], plugins) + return pipeline(call) + + def flattened_xml_map_with_xml_name( + self, + input: FlattenedXmlMapWithXmlNameInput | None = None, + plugins: list[Plugin] | None = None, + ) -> FlattenedXmlMapWithXmlNameOutput: + """ + Flattened maps with @xmlName + + Args: + input: + An instance of `FlattenedXmlMapWithXmlNameInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `FlattenedXmlMapWithXmlNameOutput`. + """ + if input is None: + input = FLATTENED_XML_MAP_WITH_XML_NAME.input() + pipeline, call = self._prepare_call( + input, FLATTENED_XML_MAP_WITH_XML_NAME, [], plugins + ) + return pipeline(call) + + def flattened_xml_map_with_xml_namespace( + self, + input: FlattenedXmlMapWithXmlNamespaceInput | None = None, + plugins: list[Plugin] | None = None, + ) -> FlattenedXmlMapWithXmlNamespaceOutput: + """ + Flattened maps with @xmlNamespace and @xmlName + + Args: + input: + An instance of `FlattenedXmlMapWithXmlNamespaceInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `FlattenedXmlMapWithXmlNamespaceOutput`. + """ + if input is None: + input = FLATTENED_XML_MAP_WITH_XML_NAMESPACE.input() + pipeline, call = self._prepare_call( + input, FLATTENED_XML_MAP_WITH_XML_NAMESPACE, [], plugins + ) + return pipeline(call) + + def fractional_seconds( + self, + input: FractionalSecondsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> FractionalSecondsOutput: + """ + Invokes the FractionalSeconds operation. + + Args: + input: + An instance of `FractionalSecondsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `FractionalSecondsOutput`. + """ + if input is None: + input = FRACTIONAL_SECONDS.input() + pipeline, call = self._prepare_call(input, FRACTIONAL_SECONDS, [], plugins) + return pipeline(call) + + def greeting_with_errors( + self, + input: GreetingWithErrorsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> GreetingWithErrorsOutput: + """ + This operation has three possible return values: 1. A successful + response in the form of GreetingWithErrorsOutput 2. An InvalidGreeting + error. 3. A BadRequest error. Implementations must be able to + successfully take a response and properly (de)serialize successful and + error responses based on the the presence of the + + Args: + input: + An instance of `GreetingWithErrorsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `GreetingWithErrorsOutput`. + """ + if input is None: + input = GREETING_WITH_ERRORS.input() + pipeline, call = self._prepare_call(input, GREETING_WITH_ERRORS, [], plugins) + return pipeline(call) + + def http_empty_prefix_headers( + self, + input: HttpEmptyPrefixHeadersInput | None = None, + plugins: list[Plugin] | None = None, + ) -> HttpEmptyPrefixHeadersOutput: + """ + Clients that perform this test extract all headers from the response. + + Args: + input: + An instance of `HttpEmptyPrefixHeadersInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `HttpEmptyPrefixHeadersOutput`. + """ + if input is None: + input = HTTP_EMPTY_PREFIX_HEADERS.input() + pipeline, call = self._prepare_call( + input, HTTP_EMPTY_PREFIX_HEADERS, [], plugins + ) + return pipeline(call) + + def http_enum_payload( + self, + input: HttpEnumPayloadInput | None = None, + plugins: list[Plugin] | None = None, + ) -> HttpEnumPayloadOutput: + """ + Invokes the HttpEnumPayload operation. + + Args: + input: + An instance of `HttpEnumPayloadInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `HttpEnumPayloadOutput`. + """ + if input is None: + input = HTTP_ENUM_PAYLOAD.input() + pipeline, call = self._prepare_call(input, HTTP_ENUM_PAYLOAD, [], plugins) + return pipeline(call) + + def http_payload_traits( + self, + input: HttpPayloadTraitsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> HttpPayloadTraitsOutput: + """ + This example serializes a blob shape in the payload. In this example, no + XML document is synthesized because the payload is not a structure or a + union type. + + Args: + input: + An instance of `HttpPayloadTraitsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `HttpPayloadTraitsOutput`. + """ + if input is None: + input = HTTP_PAYLOAD_TRAITS.input() + pipeline, call = self._prepare_call(input, HTTP_PAYLOAD_TRAITS, [], plugins) + return pipeline(call) + + def http_payload_traits_with_media_type( + self, + input: HttpPayloadTraitsWithMediaTypeInput | None = None, + plugins: list[Plugin] | None = None, + ) -> HttpPayloadTraitsWithMediaTypeOutput: + """ + This example uses a `@mediaType` trait on the payload to force a + custom content-type to be serialized. + + Args: + input: + An instance of `HttpPayloadTraitsWithMediaTypeInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `HttpPayloadTraitsWithMediaTypeOutput`. + """ + if input is None: + input = HTTP_PAYLOAD_TRAITS_WITH_MEDIA_TYPE.input() + pipeline, call = self._prepare_call( + input, HTTP_PAYLOAD_TRAITS_WITH_MEDIA_TYPE, [], plugins + ) + return pipeline(call) + + def http_payload_with_member_xml_name( + self, + input: HttpPayloadWithMemberXmlNameInput | None = None, + plugins: list[Plugin] | None = None, + ) -> HttpPayloadWithMemberXmlNameOutput: + """ + The following example serializes a payload that uses an XML name on the + member, changing the wrapper name. + + Args: + input: + An instance of `HttpPayloadWithMemberXmlNameInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `HttpPayloadWithMemberXmlNameOutput`. + """ + if input is None: + input = HTTP_PAYLOAD_WITH_MEMBER_XML_NAME.input() + pipeline, call = self._prepare_call( + input, HTTP_PAYLOAD_WITH_MEMBER_XML_NAME, [], plugins + ) + return pipeline(call) + + def http_payload_with_structure( + self, + input: HttpPayloadWithStructureInput | None = None, + plugins: list[Plugin] | None = None, + ) -> HttpPayloadWithStructureOutput: + """ + This example serializes a structure in the payload. Note that + serializing a structure changes the wrapper element name to match the + targeted structure. + + Args: + input: + An instance of `HttpPayloadWithStructureInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `HttpPayloadWithStructureOutput`. + """ + if input is None: + input = HTTP_PAYLOAD_WITH_STRUCTURE.input() + pipeline, call = self._prepare_call( + input, HTTP_PAYLOAD_WITH_STRUCTURE, [], plugins + ) + return pipeline(call) + + def http_payload_with_union( + self, + input: HttpPayloadWithUnionInput | None = None, + plugins: list[Plugin] | None = None, + ) -> HttpPayloadWithUnionOutput: + """ + This example serializes a union in the payload. + + Args: + input: + An instance of `HttpPayloadWithUnionInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `HttpPayloadWithUnionOutput`. + """ + if input is None: + input = HTTP_PAYLOAD_WITH_UNION.input() + pipeline, call = self._prepare_call(input, HTTP_PAYLOAD_WITH_UNION, [], plugins) + return pipeline(call) + + def http_payload_with_xml_name( + self, + input: HttpPayloadWithXmlNameInput | None = None, + plugins: list[Plugin] | None = None, + ) -> HttpPayloadWithXmlNameOutput: + """ + The following example serializes a payload that uses an XML name, + changing the wrapper name. + + Args: + input: + An instance of `HttpPayloadWithXmlNameInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `HttpPayloadWithXmlNameOutput`. + """ + if input is None: + input = HTTP_PAYLOAD_WITH_XML_NAME.input() + pipeline, call = self._prepare_call( + input, HTTP_PAYLOAD_WITH_XML_NAME, [], plugins + ) + return pipeline(call) + + def http_payload_with_xml_namespace( + self, + input: HttpPayloadWithXmlNamespaceInput | None = None, + plugins: list[Plugin] | None = None, + ) -> HttpPayloadWithXmlNamespaceOutput: + """ + The following example serializes a payload that uses an XML namespace. + + Args: + input: + An instance of `HttpPayloadWithXmlNamespaceInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `HttpPayloadWithXmlNamespaceOutput`. + """ + if input is None: + input = HTTP_PAYLOAD_WITH_XML_NAMESPACE.input() + pipeline, call = self._prepare_call( + input, HTTP_PAYLOAD_WITH_XML_NAMESPACE, [], plugins + ) + return pipeline(call) + + def http_payload_with_xml_namespace_and_prefix( + self, + input: HttpPayloadWithXmlNamespaceAndPrefixInput | None = None, + plugins: list[Plugin] | None = None, + ) -> HttpPayloadWithXmlNamespaceAndPrefixOutput: + """ + The following example serializes a payload that uses an XML namespace. + + Args: + input: + An instance of `HttpPayloadWithXmlNamespaceAndPrefixInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `HttpPayloadWithXmlNamespaceAndPrefixOutput`. + """ + if input is None: + input = HTTP_PAYLOAD_WITH_XML_NAMESPACE_AND_PREFIX.input() + pipeline, call = self._prepare_call( + input, HTTP_PAYLOAD_WITH_XML_NAMESPACE_AND_PREFIX, [], plugins + ) + return pipeline(call) + + def http_prefix_headers( + self, + input: HttpPrefixHeadersInput | None = None, + plugins: list[Plugin] | None = None, + ) -> HttpPrefixHeadersOutput: + """ + This examples adds headers to the input of a request and response by + prefix. + + Args: + input: + An instance of `HttpPrefixHeadersInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `HttpPrefixHeadersOutput`. + """ + if input is None: + input = HTTP_PREFIX_HEADERS.input() + pipeline, call = self._prepare_call(input, HTTP_PREFIX_HEADERS, [], plugins) + return pipeline(call) + + def http_request_with_float_labels( + self, + input: HttpRequestWithFloatLabelsInput, + plugins: list[Plugin] | None = None, + ) -> HttpRequestWithFloatLabelsOutput: + """ + Invokes the HttpRequestWithFloatLabels operation. + + Args: + input: + An instance of `HttpRequestWithFloatLabelsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `HttpRequestWithFloatLabelsOutput`. + """ + pipeline, call = self._prepare_call( + input, HTTP_REQUEST_WITH_FLOAT_LABELS, [], plugins + ) + return pipeline(call) + + def http_request_with_greedy_label_in_path( + self, + input: HttpRequestWithGreedyLabelInPathInput, + plugins: list[Plugin] | None = None, + ) -> HttpRequestWithGreedyLabelInPathOutput: + """ + Invokes the HttpRequestWithGreedyLabelInPath operation. + + Args: + input: + An instance of `HttpRequestWithGreedyLabelInPathInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `HttpRequestWithGreedyLabelInPathOutput`. + """ + pipeline, call = self._prepare_call( + input, HTTP_REQUEST_WITH_GREEDY_LABEL_IN_PATH, [], plugins + ) + return pipeline(call) + + def http_request_with_labels( + self, input: HttpRequestWithLabelsInput, plugins: list[Plugin] | None = None + ) -> HttpRequestWithLabelsOutput: + """ + The example tests how requests are serialized when there's no input + payload but there are HTTP labels. + + Args: + input: + An instance of `HttpRequestWithLabelsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `HttpRequestWithLabelsOutput`. + """ + pipeline, call = self._prepare_call( + input, HTTP_REQUEST_WITH_LABELS, [], plugins + ) + return pipeline(call) + + def http_request_with_labels_and_timestamp_format( + self, + input: HttpRequestWithLabelsAndTimestampFormatInput, + plugins: list[Plugin] | None = None, + ) -> HttpRequestWithLabelsAndTimestampFormatOutput: + """ + The example tests how requests serialize different timestamp formats in + the URI path. + + Args: + input: + An instance of `HttpRequestWithLabelsAndTimestampFormatInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `HttpRequestWithLabelsAndTimestampFormatOutput`. + """ + pipeline, call = self._prepare_call( + input, HTTP_REQUEST_WITH_LABELS_AND_TIMESTAMP_FORMAT, [], plugins + ) + return pipeline(call) + + def http_response_code( + self, + input: HttpResponseCodeInput | None = None, + plugins: list[Plugin] | None = None, + ) -> HttpResponseCodeOutput: + """ + Invokes the HttpResponseCode operation. + + Args: + input: + An instance of `HttpResponseCodeInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `HttpResponseCodeOutput`. + """ + if input is None: + input = HTTP_RESPONSE_CODE.input() + pipeline, call = self._prepare_call(input, HTTP_RESPONSE_CODE, [], plugins) + return pipeline(call) + + def http_string_payload( + self, + input: HttpStringPayloadInput | None = None, + plugins: list[Plugin] | None = None, + ) -> HttpStringPayloadOutput: + """ + Invokes the HttpStringPayload operation. + + Args: + input: + An instance of `HttpStringPayloadInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `HttpStringPayloadOutput`. + """ + if input is None: + input = HTTP_STRING_PAYLOAD.input() + pipeline, call = self._prepare_call(input, HTTP_STRING_PAYLOAD, [], plugins) + return pipeline(call) + + def ignore_query_params_in_response( + self, + input: IgnoreQueryParamsInResponseInput | None = None, + plugins: list[Plugin] | None = None, + ) -> IgnoreQueryParamsInResponseOutput: + """ + This example ensures that query string bound request parameters are + serialized in the body of responses if the structure is used in both the + request and response. + + Args: + input: + An instance of `IgnoreQueryParamsInResponseInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `IgnoreQueryParamsInResponseOutput`. + """ + if input is None: + input = IGNORE_QUERY_PARAMS_IN_RESPONSE.input() + pipeline, call = self._prepare_call( + input, IGNORE_QUERY_PARAMS_IN_RESPONSE, [], plugins + ) + return pipeline(call) + + def input_and_output_with_headers( + self, + input: InputAndOutputWithHeadersInput | None = None, + plugins: list[Plugin] | None = None, + ) -> InputAndOutputWithHeadersOutput: + """ + The example tests how requests and responses are serialized when there + is no input or output payload but there are HTTP header bindings. + + Args: + input: + An instance of `InputAndOutputWithHeadersInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `InputAndOutputWithHeadersOutput`. + """ + if input is None: + input = INPUT_AND_OUTPUT_WITH_HEADERS.input() + pipeline, call = self._prepare_call( + input, INPUT_AND_OUTPUT_WITH_HEADERS, [], plugins + ) + return pipeline(call) + + def nested_xml_maps( + self, + input: NestedXmlMapsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> NestedXmlMapsOutput: + """ + Invokes the NestedXmlMaps operation. + + Args: + input: + An instance of `NestedXmlMapsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `NestedXmlMapsOutput`. + """ + if input is None: + input = NESTED_XML_MAPS.input() + pipeline, call = self._prepare_call(input, NESTED_XML_MAPS, [], plugins) + return pipeline(call) + + def nested_xml_map_with_xml_name( + self, + input: NestedXmlMapWithXmlNameInput | None = None, + plugins: list[Plugin] | None = None, + ) -> NestedXmlMapWithXmlNameOutput: + """ + Nested Xml Maps with key/values with @xmlName + + Args: + input: + An instance of `NestedXmlMapWithXmlNameInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `NestedXmlMapWithXmlNameOutput`. + """ + if input is None: + input = NESTED_XML_MAP_WITH_XML_NAME.input() + pipeline, call = self._prepare_call( + input, NESTED_XML_MAP_WITH_XML_NAME, [], plugins + ) + return pipeline(call) + + def no_input_and_no_output( + self, + input: NoInputAndNoOutputInput | None = None, + plugins: list[Plugin] | None = None, + ) -> NoInputAndNoOutputOutput: + """ + The example tests how requests and responses are serialized when + there's no request or response payload because the operation has no + input or output. While this should be rare, code generators must support + this. + + Args: + input: + An instance of `NoInputAndNoOutputInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `NoInputAndNoOutputOutput`. + """ + if input is None: + input = NO_INPUT_AND_NO_OUTPUT.input() + pipeline, call = self._prepare_call(input, NO_INPUT_AND_NO_OUTPUT, [], plugins) + return pipeline(call) + + def no_input_and_output( + self, + input: NoInputAndOutputInput | None = None, + plugins: list[Plugin] | None = None, + ) -> NoInputAndOutputOutput: + """ + The example tests how requests and responses are serialized when + there's no request or response payload because the operation has no + input and the output is empty. While this should be rare, code + generators must support this. + + Args: + input: + An instance of `NoInputAndOutputInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `NoInputAndOutputOutput`. + """ + if input is None: + input = NO_INPUT_AND_OUTPUT.input() + pipeline, call = self._prepare_call(input, NO_INPUT_AND_OUTPUT, [], plugins) + return pipeline(call) + + def null_and_empty_headers_client( + self, + input: NullAndEmptyHeadersClientInput | None = None, + plugins: list[Plugin] | None = None, + ) -> NullAndEmptyHeadersClientOutput: + """ + Null headers are not sent over the wire, empty headers are serialized to + \"\" + + Args: + input: + An instance of `NullAndEmptyHeadersClientInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `NullAndEmptyHeadersClientOutput`. + """ + if input is None: + input = NULL_AND_EMPTY_HEADERS_CLIENT.input() + pipeline, call = self._prepare_call( + input, NULL_AND_EMPTY_HEADERS_CLIENT, [], plugins + ) + return pipeline(call) + + def null_and_empty_headers_server( + self, + input: NullAndEmptyHeadersServerInput | None = None, + plugins: list[Plugin] | None = None, + ) -> NullAndEmptyHeadersServerOutput: + """ + Null headers are not sent over the wire, empty headers are serialized to + \"\" + + Args: + input: + An instance of `NullAndEmptyHeadersServerInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `NullAndEmptyHeadersServerOutput`. + """ + if input is None: + input = NULL_AND_EMPTY_HEADERS_SERVER.input() + pipeline, call = self._prepare_call( + input, NULL_AND_EMPTY_HEADERS_SERVER, [], plugins + ) + return pipeline(call) + + def omits_null_serializes_empty_string( + self, + input: OmitsNullSerializesEmptyStringInput | None = None, + plugins: list[Plugin] | None = None, + ) -> OmitsNullSerializesEmptyStringOutput: + """ + Omits null, but serializes empty string value. + + Args: + input: + An instance of `OmitsNullSerializesEmptyStringInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `OmitsNullSerializesEmptyStringOutput`. + """ + if input is None: + input = OMITS_NULL_SERIALIZES_EMPTY_STRING.input() + pipeline, call = self._prepare_call( + input, OMITS_NULL_SERIALIZES_EMPTY_STRING, [], plugins + ) + return pipeline(call) + + def put_with_content_encoding( + self, + input: PutWithContentEncodingInput | None = None, + plugins: list[Plugin] | None = None, + ) -> PutWithContentEncodingOutput: + """ + Invokes the PutWithContentEncoding operation. + + Args: + input: + An instance of `PutWithContentEncodingInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `PutWithContentEncodingOutput`. + """ + if input is None: + input = PUT_WITH_CONTENT_ENCODING.input() + pipeline, call = self._prepare_call( + input, PUT_WITH_CONTENT_ENCODING, [], plugins + ) + return pipeline(call) + + def query_idempotency_token_auto_fill( + self, + input: QueryIdempotencyTokenAutoFillInput | None = None, + plugins: list[Plugin] | None = None, + ) -> QueryIdempotencyTokenAutoFillOutput: + """ + Automatically adds idempotency tokens. + + Args: + input: + An instance of `QueryIdempotencyTokenAutoFillInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `QueryIdempotencyTokenAutoFillOutput`. + """ + if input is None: + input = QUERY_IDEMPOTENCY_TOKEN_AUTO_FILL.input() + pipeline, call = self._prepare_call( + input, QUERY_IDEMPOTENCY_TOKEN_AUTO_FILL, [], plugins + ) + return pipeline(call) + + def query_params_as_string_list_map( + self, + input: QueryParamsAsStringListMapInput | None = None, + plugins: list[Plugin] | None = None, + ) -> QueryParamsAsStringListMapOutput: + """ + Invokes the QueryParamsAsStringListMap operation. + + Args: + input: + An instance of `QueryParamsAsStringListMapInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `QueryParamsAsStringListMapOutput`. + """ + if input is None: + input = QUERY_PARAMS_AS_STRING_LIST_MAP.input() + pipeline, call = self._prepare_call( + input, QUERY_PARAMS_AS_STRING_LIST_MAP, [], plugins + ) + return pipeline(call) + + def query_precedence( + self, + input: QueryPrecedenceInput | None = None, + plugins: list[Plugin] | None = None, + ) -> QueryPrecedenceOutput: + """ + Invokes the QueryPrecedence operation. + + Args: + input: + An instance of `QueryPrecedenceInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `QueryPrecedenceOutput`. + """ + if input is None: + input = QUERY_PRECEDENCE.input() + pipeline, call = self._prepare_call(input, QUERY_PRECEDENCE, [], plugins) + return pipeline(call) + + def recursive_shapes( + self, + input: RecursiveShapesInput | None = None, + plugins: list[Plugin] | None = None, + ) -> RecursiveShapesOutput: + """ + Recursive shapes + + Args: + input: + An instance of `RecursiveShapesInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `RecursiveShapesOutput`. + """ + if input is None: + input = RECURSIVE_SHAPES.input() + pipeline, call = self._prepare_call(input, RECURSIVE_SHAPES, [], plugins) + return pipeline(call) + + def simple_scalar_properties( + self, + input: SimpleScalarPropertiesInput | None = None, + plugins: list[Plugin] | None = None, + ) -> SimpleScalarPropertiesOutput: + """ + Invokes the SimpleScalarProperties operation. + + Args: + input: + An instance of `SimpleScalarPropertiesInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `SimpleScalarPropertiesOutput`. + """ + if input is None: + input = SIMPLE_SCALAR_PROPERTIES.input() + pipeline, call = self._prepare_call( + input, SIMPLE_SCALAR_PROPERTIES, [], plugins + ) + return pipeline(call) + + def timestamp_format_headers( + self, + input: TimestampFormatHeadersInput | None = None, + plugins: list[Plugin] | None = None, + ) -> TimestampFormatHeadersOutput: + """ + The example tests how timestamp request and response headers are + serialized. + + Args: + input: + An instance of `TimestampFormatHeadersInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `TimestampFormatHeadersOutput`. + """ + if input is None: + input = TIMESTAMP_FORMAT_HEADERS.input() + pipeline, call = self._prepare_call( + input, TIMESTAMP_FORMAT_HEADERS, [], plugins + ) + return pipeline(call) + + def xml_attributes( + self, + input: XmlAttributesInput | None = None, + plugins: list[Plugin] | None = None, + ) -> XmlAttributesOutput: + """ + This example serializes an XML attributes on synthesized document. + + Args: + input: + An instance of `XmlAttributesInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `XmlAttributesOutput`. + """ + if input is None: + input = XML_ATTRIBUTES.input() + pipeline, call = self._prepare_call(input, XML_ATTRIBUTES, [], plugins) + return pipeline(call) + + def xml_attributes_in_middle( + self, + input: XmlAttributesInMiddleInput | None = None, + plugins: list[Plugin] | None = None, + ) -> XmlAttributesInMiddleOutput: + """ + This example serializes an XML attribute on a payload when it's defined + in the middle of the member list. This tests that implementations + correctly write attributes immediately after the element start tag, + which is critical for languages like C# where attribute writing must + happen before child elements. + + Args: + input: + An instance of `XmlAttributesInMiddleInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `XmlAttributesInMiddleOutput`. + """ + if input is None: + input = XML_ATTRIBUTES_IN_MIDDLE.input() + pipeline, call = self._prepare_call( + input, XML_ATTRIBUTES_IN_MIDDLE, [], plugins + ) + return pipeline(call) + + def xml_attributes_on_payload( + self, + input: XmlAttributesOnPayloadInput | None = None, + plugins: list[Plugin] | None = None, + ) -> XmlAttributesOnPayloadOutput: + """ + This example serializes an XML attributes on a document targeted by + httpPayload. + + Args: + input: + An instance of `XmlAttributesOnPayloadInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `XmlAttributesOnPayloadOutput`. + """ + if input is None: + input = XML_ATTRIBUTES_ON_PAYLOAD.input() + pipeline, call = self._prepare_call( + input, XML_ATTRIBUTES_ON_PAYLOAD, [], plugins + ) + return pipeline(call) + + def xml_blobs( + self, input: XmlBlobsInput | None = None, plugins: list[Plugin] | None = None + ) -> XmlBlobsOutput: + """ + Blobs are base64 encoded + + Args: + input: + An instance of `XmlBlobsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `XmlBlobsOutput`. + """ + if input is None: + input = XML_BLOBS.input() + pipeline, call = self._prepare_call(input, XML_BLOBS, [], plugins) + return pipeline(call) + + def xml_empty_blobs( + self, + input: XmlEmptyBlobsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> XmlEmptyBlobsOutput: + """ + Blobs are base64 encoded + + Args: + input: + An instance of `XmlEmptyBlobsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `XmlEmptyBlobsOutput`. + """ + if input is None: + input = XML_EMPTY_BLOBS.input() + pipeline, call = self._prepare_call(input, XML_EMPTY_BLOBS, [], plugins) + return pipeline(call) + + def xml_empty_lists( + self, + input: XmlEmptyListsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> XmlEmptyListsOutput: + """ + Invokes the XmlEmptyLists operation. + + Args: + input: + An instance of `XmlEmptyListsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `XmlEmptyListsOutput`. + """ + if input is None: + input = XML_EMPTY_LISTS.input() + pipeline, call = self._prepare_call(input, XML_EMPTY_LISTS, [], plugins) + return pipeline(call) + + def xml_empty_maps( + self, + input: XmlEmptyMapsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> XmlEmptyMapsOutput: + """ + Invokes the XmlEmptyMaps operation. + + Args: + input: + An instance of `XmlEmptyMapsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `XmlEmptyMapsOutput`. + """ + if input is None: + input = XML_EMPTY_MAPS.input() + pipeline, call = self._prepare_call(input, XML_EMPTY_MAPS, [], plugins) + return pipeline(call) + + def xml_empty_strings( + self, + input: XmlEmptyStringsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> XmlEmptyStringsOutput: + """ + Invokes the XmlEmptyStrings operation. + + Args: + input: + An instance of `XmlEmptyStringsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `XmlEmptyStringsOutput`. + """ + if input is None: + input = XML_EMPTY_STRINGS.input() + pipeline, call = self._prepare_call(input, XML_EMPTY_STRINGS, [], plugins) + return pipeline(call) + + def xml_enums( + self, input: XmlEnumsInput | None = None, plugins: list[Plugin] | None = None + ) -> XmlEnumsOutput: + """ + This example serializes enums as top level properties, in lists, sets, + and maps. + + Args: + input: + An instance of `XmlEnumsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `XmlEnumsOutput`. + """ + if input is None: + input = XML_ENUMS.input() + pipeline, call = self._prepare_call(input, XML_ENUMS, [], plugins) + return pipeline(call) + + def xml_int_enums( + self, input: XmlIntEnumsInput | None = None, plugins: list[Plugin] | None = None + ) -> XmlIntEnumsOutput: + """ + This example serializes enums as top level properties, in lists, sets, + and maps. + + Args: + input: + An instance of `XmlIntEnumsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `XmlIntEnumsOutput`. + """ + if input is None: + input = XML_INT_ENUMS.input() + pipeline, call = self._prepare_call(input, XML_INT_ENUMS, [], plugins) + return pipeline(call) + + def xml_lists( + self, input: XmlListsInput | None = None, plugins: list[Plugin] | None = None + ) -> XmlListsOutput: + """ + This test case serializes XML lists for the following cases for both + input and output: 1. Normal XML lists. 2. Normal XML sets. 3. XML lists + of lists. 4. XML lists with @xmlName on its members 5. Flattened XML + lists. 6. Flattened XML lists with @xmlName. 7. Flattened XML lists + with @xmlNamespace. 8. Lists of structures. 9. Flattened XML list of + structures + + Args: + input: + An instance of `XmlListsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `XmlListsOutput`. + """ + if input is None: + input = XML_LISTS.input() + pipeline, call = self._prepare_call(input, XML_LISTS, [], plugins) + return pipeline(call) + + def xml_maps( + self, input: XmlMapsInput | None = None, plugins: list[Plugin] | None = None + ) -> XmlMapsOutput: + """ + The example tests basic map serialization. + + Args: + input: + An instance of `XmlMapsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `XmlMapsOutput`. + """ + if input is None: + input = XML_MAPS.input() + pipeline, call = self._prepare_call(input, XML_MAPS, [], plugins) + return pipeline(call) + + def xml_maps_xml_name( + self, + input: XmlMapsXmlNameInput | None = None, + plugins: list[Plugin] | None = None, + ) -> XmlMapsXmlNameOutput: + """ + Invokes the XmlMapsXmlName operation. + + Args: + input: + An instance of `XmlMapsXmlNameInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `XmlMapsXmlNameOutput`. + """ + if input is None: + input = XML_MAPS_XML_NAME.input() + pipeline, call = self._prepare_call(input, XML_MAPS_XML_NAME, [], plugins) + return pipeline(call) + + def xml_map_with_xml_namespace( + self, + input: XmlMapWithXmlNamespaceInput | None = None, + plugins: list[Plugin] | None = None, + ) -> XmlMapWithXmlNamespaceOutput: + """ + Maps with @xmlNamespace and @xmlName + + Args: + input: + An instance of `XmlMapWithXmlNamespaceInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `XmlMapWithXmlNamespaceOutput`. + """ + if input is None: + input = XML_MAP_WITH_XML_NAMESPACE.input() + pipeline, call = self._prepare_call( + input, XML_MAP_WITH_XML_NAMESPACE, [], plugins + ) + return pipeline(call) + + def xml_namespaces( + self, + input: XmlNamespacesInput | None = None, + plugins: list[Plugin] | None = None, + ) -> XmlNamespacesOutput: + """ + Invokes the XmlNamespaces operation. + + Args: + input: + An instance of `XmlNamespacesInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `XmlNamespacesOutput`. + """ + if input is None: + input = XML_NAMESPACES.input() + pipeline, call = self._prepare_call(input, XML_NAMESPACES, [], plugins) + return pipeline(call) + + def xml_timestamps( + self, + input: XmlTimestampsInput | None = None, + plugins: list[Plugin] | None = None, + ) -> XmlTimestampsOutput: + """ + This tests how timestamps are serialized, including using the default + format of date-time and various @timestampFormat trait values. + + Args: + input: + An instance of `XmlTimestampsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `XmlTimestampsOutput`. + """ + if input is None: + input = XML_TIMESTAMPS.input() + pipeline, call = self._prepare_call(input, XML_TIMESTAMPS, [], plugins) + return pipeline(call) + + def xml_unions( + self, input: XmlUnionsInput | None = None, plugins: list[Plugin] | None = None + ) -> XmlUnionsOutput: + """ + Invokes the XmlUnions operation. + + Args: + input: + An instance of `XmlUnionsInput`. + plugins: + A list of callables that modify the configuration dynamically. + Changes made by these plugins only apply for the duration of the + operation execution and will not affect any other operation + invocations. + + Returns: + An instance of `XmlUnionsOutput`. + """ + if input is None: + input = XML_UNIONS.input() + pipeline, call = self._prepare_call(input, XML_UNIONS, [], plugins) + return pipeline(call) diff --git a/codegen-output/rest-xml/src/restxml/sync/config.py b/codegen-output/rest-xml/src/restxml/sync/config.py new file mode 100644 index 000000000..a803c968e --- /dev/null +++ b/codegen-output/rest-xml/src/restxml/sync/config.py @@ -0,0 +1,486 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from dataclasses import dataclass, field +from typing import ( + Any, + Callable, + ClassVar, + Self, + TYPE_CHECKING, + TypeAlias, + Union, + Unpack, +) + +from smithy_aws_core.auth.sigv4 import SigV4AuthScheme +from smithy_aws_core.config import FileSystem +from smithy_aws_core.config.aws_config import AwsConfig, AwsConfigOverrides +from smithy_aws_core.config.resolvers import EndpointUriResolver +from smithy_aws_core.config.types import FieldSpec +from smithy_aws_core.endpoints.standard_regional import ( + StandardRegionalEndpointsResolver, +) +from smithy_aws_core.identity import AWSCredentialsIdentity, AWSIdentityProperties +from smithy_aws_core.protocols import RestXmlClientProtocol +from smithy_core.interfaces import ( + ClientProtocol, + EndpointResolver, + ProtocolConstructor, + ProtocolSettings, +) +from smithy_core.interfaces.auth import AuthScheme, AuthSchemeResolver +from smithy_core.interfaces.identity import IdentityResolver +from smithy_core.shapes import ShapeID +from smithy_http.urllib3 import Urllib3HTTPClient + +from .._private.schemas import REST_XML +from ..auth import HTTPAuthSchemeResolver + + +if TYPE_CHECKING: + from smithy_core.interceptors import Interceptor + from ..models import ( + AllQueryStringTypesInput, + AllQueryStringTypesOutput, + BodyWithXmlNameInput, + BodyWithXmlNameOutput, + ConstantAndVariableQueryStringInput, + ConstantAndVariableQueryStringOutput, + ConstantQueryStringInput, + ConstantQueryStringOutput, + ContentTypeParametersInput, + ContentTypeParametersOutput, + DatetimeOffsetsInput, + DatetimeOffsetsOutput, + EmptyInputAndEmptyOutputInput, + EmptyInputAndEmptyOutputOutput, + EndpointOperationInput, + EndpointOperationOutput, + EndpointWithHostLabelHeaderOperationInput, + EndpointWithHostLabelHeaderOperationOutput, + EndpointWithHostLabelOperationInput, + EndpointWithHostLabelOperationOutput, + FlattenedXmlMapInput, + FlattenedXmlMapOutput, + FlattenedXmlMapWithXmlNameInput, + FlattenedXmlMapWithXmlNameOutput, + FlattenedXmlMapWithXmlNamespaceInput, + FlattenedXmlMapWithXmlNamespaceOutput, + FractionalSecondsInput, + FractionalSecondsOutput, + GreetingWithErrorsInput, + GreetingWithErrorsOutput, + HttpEmptyPrefixHeadersInput, + HttpEmptyPrefixHeadersOutput, + HttpEnumPayloadInput, + HttpEnumPayloadOutput, + HttpPayloadTraitsInput, + HttpPayloadTraitsOutput, + HttpPayloadTraitsWithMediaTypeInput, + HttpPayloadTraitsWithMediaTypeOutput, + HttpPayloadWithMemberXmlNameInput, + HttpPayloadWithMemberXmlNameOutput, + HttpPayloadWithStructureInput, + HttpPayloadWithStructureOutput, + HttpPayloadWithUnionInput, + HttpPayloadWithUnionOutput, + HttpPayloadWithXmlNameInput, + HttpPayloadWithXmlNameOutput, + HttpPayloadWithXmlNamespaceAndPrefixInput, + HttpPayloadWithXmlNamespaceAndPrefixOutput, + HttpPayloadWithXmlNamespaceInput, + HttpPayloadWithXmlNamespaceOutput, + HttpPrefixHeadersInput, + HttpPrefixHeadersOutput, + HttpRequestWithFloatLabelsInput, + HttpRequestWithFloatLabelsOutput, + HttpRequestWithGreedyLabelInPathInput, + HttpRequestWithGreedyLabelInPathOutput, + HttpRequestWithLabelsAndTimestampFormatInput, + HttpRequestWithLabelsAndTimestampFormatOutput, + HttpRequestWithLabelsInput, + HttpRequestWithLabelsOutput, + HttpResponseCodeInput, + HttpResponseCodeOutput, + HttpStringPayloadInput, + HttpStringPayloadOutput, + IgnoreQueryParamsInResponseInput, + IgnoreQueryParamsInResponseOutput, + InputAndOutputWithHeadersInput, + InputAndOutputWithHeadersOutput, + NestedXmlMapWithXmlNameInput, + NestedXmlMapWithXmlNameOutput, + NestedXmlMapsInput, + NestedXmlMapsOutput, + NoInputAndNoOutputInput, + NoInputAndNoOutputOutput, + NoInputAndOutputInput, + NoInputAndOutputOutput, + NullAndEmptyHeadersClientInput, + NullAndEmptyHeadersClientOutput, + NullAndEmptyHeadersServerInput, + NullAndEmptyHeadersServerOutput, + OmitsNullSerializesEmptyStringInput, + OmitsNullSerializesEmptyStringOutput, + PutWithContentEncodingInput, + PutWithContentEncodingOutput, + QueryIdempotencyTokenAutoFillInput, + QueryIdempotencyTokenAutoFillOutput, + QueryParamsAsStringListMapInput, + QueryParamsAsStringListMapOutput, + QueryPrecedenceInput, + QueryPrecedenceOutput, + RecursiveShapesInput, + RecursiveShapesOutput, + SimpleScalarPropertiesInput, + SimpleScalarPropertiesOutput, + TimestampFormatHeadersInput, + TimestampFormatHeadersOutput, + XmlAttributesInMiddleInput, + XmlAttributesInMiddleOutput, + XmlAttributesInput, + XmlAttributesOnPayloadInput, + XmlAttributesOnPayloadOutput, + XmlAttributesOutput, + XmlBlobsInput, + XmlBlobsOutput, + XmlEmptyBlobsInput, + XmlEmptyBlobsOutput, + XmlEmptyListsInput, + XmlEmptyListsOutput, + XmlEmptyMapsInput, + XmlEmptyMapsOutput, + XmlEmptyStringsInput, + XmlEmptyStringsOutput, + XmlEnumsInput, + XmlEnumsOutput, + XmlIntEnumsInput, + XmlIntEnumsOutput, + XmlListsInput, + XmlListsOutput, + XmlMapWithXmlNamespaceInput, + XmlMapWithXmlNamespaceOutput, + XmlMapsInput, + XmlMapsOutput, + XmlMapsXmlNameInput, + XmlMapsXmlNameOutput, + XmlNamespacesInput, + XmlNamespacesOutput, + XmlTimestampsInput, + XmlTimestampsOutput, + XmlUnionsInput, + XmlUnionsOutput, + ) + + _ServiceInterceptor = Union[ + Interceptor[AllQueryStringTypesInput, AllQueryStringTypesOutput, Any, Any], + Interceptor[BodyWithXmlNameInput, BodyWithXmlNameOutput, Any, Any], + Interceptor[ + ConstantAndVariableQueryStringInput, + ConstantAndVariableQueryStringOutput, + Any, + Any, + ], + Interceptor[ConstantQueryStringInput, ConstantQueryStringOutput, Any, Any], + Interceptor[ContentTypeParametersInput, ContentTypeParametersOutput, Any, Any], + Interceptor[DatetimeOffsetsInput, DatetimeOffsetsOutput, Any, Any], + Interceptor[ + EmptyInputAndEmptyOutputInput, EmptyInputAndEmptyOutputOutput, Any, Any + ], + Interceptor[EndpointOperationInput, EndpointOperationOutput, Any, Any], + Interceptor[ + EndpointWithHostLabelHeaderOperationInput, + EndpointWithHostLabelHeaderOperationOutput, + Any, + Any, + ], + Interceptor[ + EndpointWithHostLabelOperationInput, + EndpointWithHostLabelOperationOutput, + Any, + Any, + ], + Interceptor[FlattenedXmlMapInput, FlattenedXmlMapOutput, Any, Any], + Interceptor[ + FlattenedXmlMapWithXmlNameInput, FlattenedXmlMapWithXmlNameOutput, Any, Any + ], + Interceptor[ + FlattenedXmlMapWithXmlNamespaceInput, + FlattenedXmlMapWithXmlNamespaceOutput, + Any, + Any, + ], + Interceptor[FractionalSecondsInput, FractionalSecondsOutput, Any, Any], + Interceptor[GreetingWithErrorsInput, GreetingWithErrorsOutput, Any, Any], + Interceptor[ + HttpEmptyPrefixHeadersInput, HttpEmptyPrefixHeadersOutput, Any, Any + ], + Interceptor[HttpEnumPayloadInput, HttpEnumPayloadOutput, Any, Any], + Interceptor[HttpPayloadTraitsInput, HttpPayloadTraitsOutput, Any, Any], + Interceptor[ + HttpPayloadTraitsWithMediaTypeInput, + HttpPayloadTraitsWithMediaTypeOutput, + Any, + Any, + ], + Interceptor[ + HttpPayloadWithMemberXmlNameInput, + HttpPayloadWithMemberXmlNameOutput, + Any, + Any, + ], + Interceptor[ + HttpPayloadWithStructureInput, HttpPayloadWithStructureOutput, Any, Any + ], + Interceptor[HttpPayloadWithUnionInput, HttpPayloadWithUnionOutput, Any, Any], + Interceptor[ + HttpPayloadWithXmlNameInput, HttpPayloadWithXmlNameOutput, Any, Any + ], + Interceptor[ + HttpPayloadWithXmlNamespaceInput, + HttpPayloadWithXmlNamespaceOutput, + Any, + Any, + ], + Interceptor[ + HttpPayloadWithXmlNamespaceAndPrefixInput, + HttpPayloadWithXmlNamespaceAndPrefixOutput, + Any, + Any, + ], + Interceptor[HttpPrefixHeadersInput, HttpPrefixHeadersOutput, Any, Any], + Interceptor[ + HttpRequestWithFloatLabelsInput, HttpRequestWithFloatLabelsOutput, Any, Any + ], + Interceptor[ + HttpRequestWithGreedyLabelInPathInput, + HttpRequestWithGreedyLabelInPathOutput, + Any, + Any, + ], + Interceptor[HttpRequestWithLabelsInput, HttpRequestWithLabelsOutput, Any, Any], + Interceptor[ + HttpRequestWithLabelsAndTimestampFormatInput, + HttpRequestWithLabelsAndTimestampFormatOutput, + Any, + Any, + ], + Interceptor[HttpResponseCodeInput, HttpResponseCodeOutput, Any, Any], + Interceptor[HttpStringPayloadInput, HttpStringPayloadOutput, Any, Any], + Interceptor[ + IgnoreQueryParamsInResponseInput, + IgnoreQueryParamsInResponseOutput, + Any, + Any, + ], + Interceptor[ + InputAndOutputWithHeadersInput, InputAndOutputWithHeadersOutput, Any, Any + ], + Interceptor[NestedXmlMapsInput, NestedXmlMapsOutput, Any, Any], + Interceptor[ + NestedXmlMapWithXmlNameInput, NestedXmlMapWithXmlNameOutput, Any, Any + ], + Interceptor[NoInputAndNoOutputInput, NoInputAndNoOutputOutput, Any, Any], + Interceptor[NoInputAndOutputInput, NoInputAndOutputOutput, Any, Any], + Interceptor[ + NullAndEmptyHeadersClientInput, NullAndEmptyHeadersClientOutput, Any, Any + ], + Interceptor[ + NullAndEmptyHeadersServerInput, NullAndEmptyHeadersServerOutput, Any, Any + ], + Interceptor[ + OmitsNullSerializesEmptyStringInput, + OmitsNullSerializesEmptyStringOutput, + Any, + Any, + ], + Interceptor[ + PutWithContentEncodingInput, PutWithContentEncodingOutput, Any, Any + ], + Interceptor[ + QueryIdempotencyTokenAutoFillInput, + QueryIdempotencyTokenAutoFillOutput, + Any, + Any, + ], + Interceptor[ + QueryParamsAsStringListMapInput, QueryParamsAsStringListMapOutput, Any, Any + ], + Interceptor[QueryPrecedenceInput, QueryPrecedenceOutput, Any, Any], + Interceptor[RecursiveShapesInput, RecursiveShapesOutput, Any, Any], + Interceptor[ + SimpleScalarPropertiesInput, SimpleScalarPropertiesOutput, Any, Any + ], + Interceptor[ + TimestampFormatHeadersInput, TimestampFormatHeadersOutput, Any, Any + ], + Interceptor[XmlAttributesInput, XmlAttributesOutput, Any, Any], + Interceptor[XmlAttributesInMiddleInput, XmlAttributesInMiddleOutput, Any, Any], + Interceptor[ + XmlAttributesOnPayloadInput, XmlAttributesOnPayloadOutput, Any, Any + ], + Interceptor[XmlBlobsInput, XmlBlobsOutput, Any, Any], + Interceptor[XmlEmptyBlobsInput, XmlEmptyBlobsOutput, Any, Any], + Interceptor[XmlEmptyListsInput, XmlEmptyListsOutput, Any, Any], + Interceptor[XmlEmptyMapsInput, XmlEmptyMapsOutput, Any, Any], + Interceptor[XmlEmptyStringsInput, XmlEmptyStringsOutput, Any, Any], + Interceptor[XmlEnumsInput, XmlEnumsOutput, Any, Any], + Interceptor[XmlIntEnumsInput, XmlIntEnumsOutput, Any, Any], + Interceptor[XmlListsInput, XmlListsOutput, Any, Any], + Interceptor[XmlMapsInput, XmlMapsOutput, Any, Any], + Interceptor[XmlMapsXmlNameInput, XmlMapsXmlNameOutput, Any, Any], + Interceptor[ + XmlMapWithXmlNamespaceInput, XmlMapWithXmlNamespaceOutput, Any, Any + ], + Interceptor[XmlNamespacesInput, XmlNamespacesOutput, Any, Any], + Interceptor[XmlTimestampsInput, XmlTimestampsOutput, Any, Any], + Interceptor[XmlUnionsInput, XmlUnionsOutput, Any, Any], + ] +else: + _ServiceInterceptor = Any + +_PROTOCOL_SETTINGS = ProtocolSettings( + namespace="aws.protocoltests.restxml", + service_target="RestXml", + service_schema=REST_XML, +) + + +class _RestXmlProtocolConfigOverrides(AwsConfigOverrides, total=False): + endpoint_resolver: EndpointResolver | None + protocol: ( + ClientProtocol[Any, Any] | ProtocolConstructor[ClientProtocol[Any, Any]] | None + ) + auth_schemes: dict[ShapeID, AuthScheme[Any, Any, Any, Any]] | None + auth_scheme_resolver: AuthSchemeResolver | None + + +@dataclass(kw_only=True, repr=False, init=False) +class RestXmlProtocolConfig(AwsConfig): + """Rest Xml Protocol configuration.""" + + endpoint_resolver: EndpointResolver | None = None + """ + The endpoint resolver used to resolve the final endpoint per-operation + based on the configuration. + """ + + protocol: ClientProtocol[Any, Any] | None = None + """ + Pass a protocol class reference from smithy_aws_core.protocols to select + the protocol, e.g. protocol=AwsJson10ClientProtocol. For custom + protocols a protocol instance may also be passed. + """ + + interceptors: list[_ServiceInterceptor] = field(default_factory=lambda: []) + """ + The list of interceptors, which are hooks that are called during the + execution of a request. + """ + + auth_schemes: dict[ShapeID, AuthScheme[Any, Any, Any, Any]] | None = None + """A map of auth scheme ids to auth schemes.""" + + auth_scheme_resolver: AuthSchemeResolver | None = None + """ + An auth scheme resolver that determines the auth scheme for each + operation. + """ + + aws_credentials_identity_resolver: ( + IdentityResolver[AWSCredentialsIdentity, AWSIdentityProperties] | None + ) = None + """Resolves AWS Credentials. Required for operations that use Sigv4 Auth.""" + + region: str | None = None + """ + The AWS region to connect to. The configured region is used to determine + the service endpoint. + """ + + aws_access_key_id: str | None = None + """The identifier for a secret access key.""" + + aws_secret_access_key: str | None = None + """A secret access key that can be used to sign requests.""" + + aws_session_token: str | None = None + """The session token used with temporary AWS credentials.""" + + user_agent_extra: str | None = None + """Additional suffix to be added to the User-Agent header.""" + + sdk_ua_app_id: str | None = None + """ + A unique and opaque application ID that is appended to the User-Agent + header. + """ + + _FIELDS: ClassVar[dict[str, FieldSpec]] = { + "aws_credentials_identity_resolver": FieldSpec(default=None), + "region": FieldSpec(default=None), + "aws_access_key_id": FieldSpec(default=None), + "aws_secret_access_key": FieldSpec(default=None), + "aws_session_token": FieldSpec(default=None), + "user_agent_extra": FieldSpec(default=None), + "sdk_ua_app_id": FieldSpec(default=None), + **AwsConfig._FIELDS, + "endpoint_uri": FieldSpec( + default=None, resolver=EndpointUriResolver("rest_xml_protocol") + ), + "endpoint_resolver": FieldSpec( + default_factory=lambda: StandardRegionalEndpointsResolver( + endpoint_prefix="restxml" + ) + ), + "protocol": FieldSpec( + default_factory=lambda: RestXmlClientProtocol(_PROTOCOL_SETTINGS), + converter=lambda p: p(_PROTOCOL_SETTINGS) if isinstance(p, type) else p, + ), + "auth_schemes": FieldSpec( + default_factory=lambda: { + ShapeID("aws.auth#sigv4"): SigV4AuthScheme(service="restxml") + } + ), + "auth_scheme_resolver": FieldSpec(default_factory=HTTPAuthSchemeResolver), + "transport": FieldSpec(default_factory=lambda: Urllib3HTTPClient()), + } + + def set_auth_scheme(self, scheme: AuthScheme[Any, Any, Any, Any]) -> None: + """ + Set an auth scheme implementation using its scheme ID. :param scheme: + The auth scheme to add or replace. + """ + auth_schemes = dict(self.auth_schemes or {}) + auth_schemes[scheme.scheme_id] = scheme + self.auth_schemes = auth_schemes + + @classmethod + def resolve( # pyright: ignore[reportIncompatibleMethodOverride] + cls, + *, + profile: str | None = None, + fs: FileSystem | None = None, + config_file_path: str | None = None, + credentials_file_path: str | None = None, + **overrides: Unpack[_RestXmlProtocolConfigOverrides], + ) -> Self: + """ + Resolve config from environment, config files, defaults, and explicit + overrides. + """ + return cls._resolve( + profile=profile, + fs=fs, + config_file_path=config_file_path, + credentials_file_path=credentials_file_path, + overrides=overrides, + ) + + +Plugin: TypeAlias = Callable[[RestXmlProtocolConfig], None] +""" +A callable that customizes a client configuration. Service-level plugins +are applied once to the base configuration inherited by every operation. +Operation-level plugins apply only to a single operation invocation. +""" diff --git a/codegen-output/rest-xml/src/restxml/sync/user_agent.py b/codegen-output/rest-xml/src/restxml/sync/user_agent.py new file mode 100644 index 000000000..d845f3897 --- /dev/null +++ b/codegen-output/rest-xml/src/restxml/sync/user_agent.py @@ -0,0 +1,17 @@ +# Code generated by smithy-python-codegen DO NOT EDIT. + +from smithy_aws_core.interceptors.user_agent import UserAgentInterceptor + +from .. import __version__ +from .config import RestXmlProtocolConfig + + +def aws_user_agent_plugin(config: RestXmlProtocolConfig): + config.interceptors.append( + UserAgentInterceptor( + ua_suffix=config.user_agent_extra, + ua_app_id=config.sdk_ua_app_id, + sdk_version=__version__, + service_id="Rest_Xml_Protocol", + ) + ) diff --git a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsAsyncConfigIntegration.java b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsAsyncConfigIntegration.java index 6fc34c56a..b29956786 100644 --- a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsAsyncConfigIntegration.java +++ b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsAsyncConfigIntegration.java @@ -7,6 +7,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Set; +import java.util.function.UnaryOperator; import software.amazon.smithy.aws.traits.ServiceTrait; import software.amazon.smithy.codegen.core.Symbol; import software.amazon.smithy.model.knowledge.ServiceIndex; @@ -15,6 +16,7 @@ import software.amazon.smithy.python.codegen.GenerationContext; import software.amazon.smithy.python.codegen.RuntimeTypes; import software.amazon.smithy.python.codegen.SmithyPythonDependency; +import software.amazon.smithy.python.codegen.SyncVariant; import software.amazon.smithy.python.codegen.integrations.PythonIntegration; import software.amazon.smithy.python.codegen.integrations.RuntimeClientPlugin; import software.amazon.smithy.python.codegen.sections.AsyncConfigSection; @@ -88,6 +90,11 @@ public void write(PythonWriter writer, String previousText, AsyncConfigSection s return; } var asyncConfigSymbol = maybeAsyncConfigSymbol.get(); + var sync = section.sync(); + UnaryOperator code = sync ? SyncVariant::toSyncCode : UnaryOperator.identity(); + var configName = sync + ? SyncVariant.toSyncName(asyncConfigSymbol.getName()) + : asyncConfigSymbol.getName(); final String serviceId = service.getTrait(ServiceTrait.class) .map(ServiceTrait::getSdkId) @@ -165,7 +172,7 @@ public void write(PythonWriter writer, String previousText, AsyncConfigSection s .namespace("smithy_core.interfaces.auth", ".") .addDependency(SmithyPythonDependency.SMITHY_CORE) .build(); - var overridesTypeName = "_" + asyncConfigSymbol.getName() + "Overrides"; + var overridesTypeName = "_" + configName + "Overrides"; writer.addStdlibImport("typing", "ClassVar"); writer.addStdlibImport("typing", "Any"); @@ -198,8 +205,8 @@ public void write(PythonWriter writer, String previousText, AsyncConfigSection s // credential fields, and a generated __repr__ on this subclass would shadow it // and leak secrets. writer.write("@dataclass(kw_only=True, repr=False, init=False)"); - writer.openBlock("class $L($T):", asyncConfigSymbol.getName(), asyncAwsConfigSymbol); - writer.writeDocs(serviceId + " configuration (async-resolved).", context); + writer.openBlock("class $L($T):", configName, asyncAwsConfigSymbol); + writer.writeDocs(serviceId + (sync ? " configuration." : " configuration (async-resolved)."), context); writer.write(""); // Write service-specific field declarations @@ -209,9 +216,12 @@ public void write(PythonWriter writer, String previousText, AsyncConfigSection s writer.write(""); writer.write("protocol: $T | None = None", protocolSymbol); - writer.writeDocs("Pass a protocol class reference from smithy_aws_core.aio.protocols " + var protocolDocs = "Pass a protocol class reference from smithy_aws_core.aio.protocols " + "to select the protocol, e.g. protocol=AsyncAwsJson10ClientProtocol. For custom " - + "protocols a protocol instance may also be passed.", context); + + "protocols a protocol instance may also be passed."; + writer.writeDocs(sync + ? SyncVariant.toSyncName(protocolDocs.replace(".aio.", ".")) + : protocolDocs, context); writer.write(""); writer.write("interceptors: list[_ServiceInterceptor] = field(default_factory=lambda: [])"); @@ -313,7 +323,9 @@ public void write(PythonWriter writer, String previousText, AsyncConfigSection s // transport FieldSpec writer.write("\"transport\": $T(", fieldSpecSymbol); writer.indent(); - writer.addDependency(SmithyPythonDependency.SMITHY_HTTP.withOptionalDependencies("aiohttp")); + if (!sync) { + writer.addDependency(SmithyPythonDependency.SMITHY_HTTP.withOptionalDependencies("aiohttp")); + } writer.write("default_factory=lambda: $T(),", RuntimeTypes.AIOHTTP_CLIENT); writer.dedent(); writer.write("),"); @@ -335,7 +347,7 @@ public void write(PythonWriter writer, String previousText, AsyncConfigSection s writer.write(""); } writer.write("@classmethod"); - writer.write("async def resolve( # pyright: ignore[reportIncompatibleMethodOverride]"); + writer.write(code.apply("async def resolve( # pyright: ignore[reportIncompatibleMethodOverride]")); writer.indent(); writer.write("cls,"); writer.write("*,"); @@ -350,7 +362,7 @@ public void write(PythonWriter writer, String previousText, AsyncConfigSection s writer.writeDocs( "Resolve config from environment, config files, defaults, and explicit overrides.", context); - writer.write("return await cls._resolve("); + writer.write(code.apply("return await cls._resolve(")); writer.indent(); writer.write("profile=profile,"); writer.write("fs=fs,"); diff --git a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsIdentityIntegration.java b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsIdentityIntegration.java index 0e2170a9b..fd625926e 100644 --- a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsIdentityIntegration.java +++ b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsIdentityIntegration.java @@ -9,6 +9,7 @@ import software.amazon.smithy.codegen.core.Symbol; import software.amazon.smithy.python.codegen.GenerationContext; import software.amazon.smithy.python.codegen.SmithyPythonDependency; +import software.amazon.smithy.python.codegen.SyncVariant; import software.amazon.smithy.python.codegen.integrations.PythonIntegration; import software.amazon.smithy.python.codegen.sections.ClientSetupSection; import software.amazon.smithy.python.codegen.writer.PythonWriter; @@ -52,7 +53,7 @@ public void write(PythonWriter writer, String previousText, ClientSetupSection s writer.write(previousText); } writer.addStdlibImport("typing", "cast"); - writer.write(""" + var template = """ if config.aws_credentials_identity_resolver is None: config_context = config.resolution_context() config_file = None @@ -67,7 +68,8 @@ public void write(PythonWriter writer, String previousText, ClientSetupSection s profile_name=profile_name, region_override=config.region, http_client=cast($3T | None, config.transport), - )""", + )"""; + writer.write(section.sync() ? SyncVariant.toSyncCode(template) : template, Symbol.builder() .name("AsyncIdentityChain") .namespace("smithy_aws_core.aio.identity.chain", ".") diff --git a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsJson10ProtocolGenerator.java b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsJson10ProtocolGenerator.java index f91e1b1d6..99642d257 100644 --- a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsJson10ProtocolGenerator.java +++ b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsJson10ProtocolGenerator.java @@ -53,8 +53,7 @@ public ApplicationProtocol getApplicationProtocol(GenerationContext context) { @Override public void initializeProtocol(GenerationContext context, PythonWriter writer) { writer.addDependency(AwsPythonDependency.SMITHY_AWS_CORE.withOptionalDependencies("json")); - writer.addImport("smithy_aws_core.aio.protocols", "AsyncAwsJson10ClientProtocol"); - writer.write("AsyncAwsJson10ClientProtocol(_PROTOCOL_SETTINGS)"); + writer.write("$T(_PROTOCOL_SETTINGS)", AwsRuntimeTypes.AWS_JSON_10_CLIENT_PROTOCOL); } @Override diff --git a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsJson11ProtocolGenerator.java b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsJson11ProtocolGenerator.java index 8453d5252..713ab3ae0 100644 --- a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsJson11ProtocolGenerator.java +++ b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsJson11ProtocolGenerator.java @@ -47,8 +47,7 @@ public ApplicationProtocol getApplicationProtocol(GenerationContext context) { @Override public void initializeProtocol(GenerationContext context, PythonWriter writer) { writer.addDependency(AwsPythonDependency.SMITHY_AWS_CORE.withOptionalDependencies("json")); - writer.addImport("smithy_aws_core.aio.protocols", "AsyncAwsJson11ClientProtocol"); - writer.write("AsyncAwsJson11ClientProtocol(_PROTOCOL_SETTINGS)"); + writer.write("$T(_PROTOCOL_SETTINGS)", AwsRuntimeTypes.AWS_JSON_11_CLIENT_PROTOCOL); } @Override diff --git a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsRuntimeTypes.java b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsRuntimeTypes.java index b226c8337..4c945ff7e 100644 --- a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsRuntimeTypes.java +++ b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsRuntimeTypes.java @@ -16,6 +16,16 @@ @SmithyUnstableApi public final class AwsRuntimeTypes { + // smithy_aws_core.aio.protocols + public static final Symbol AWS_JSON_10_CLIENT_PROTOCOL = createSymbol( + "aio.protocols", + "AsyncAwsJson10ClientProtocol"); + + // smithy_aws_core.aio.protocols + public static final Symbol AWS_JSON_11_CLIENT_PROTOCOL = createSymbol( + "aio.protocols", + "AsyncAwsJson11ClientProtocol"); + // smithy_aws_core.aio.protocols public static final Symbol AWS_QUERY_CLIENT_PROTOCOL = createSymbol( "aio.protocols", diff --git a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsUserAgentIntegration.java b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsUserAgentIntegration.java index 5989913ca..bb2627d00 100644 --- a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsUserAgentIntegration.java +++ b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsUserAgentIntegration.java @@ -11,6 +11,7 @@ import software.amazon.smithy.python.codegen.CodegenUtils; import software.amazon.smithy.python.codegen.ConfigProperty; import software.amazon.smithy.python.codegen.GenerationContext; +import software.amazon.smithy.python.codegen.SyncVariant; import software.amazon.smithy.python.codegen.integrations.PythonIntegration; import software.amazon.smithy.python.codegen.integrations.RuntimeClientPlugin; import software.amazon.smithy.utils.SmithyInternalApi; @@ -126,12 +127,19 @@ public List getClientPlugins(GenerationContext context) { serviceId); } }); - return List.of(filename); + var syncFile = CodegenUtils.getAsyncConfigSymbol(c.settings(), c.model()) + .flatMap(asyncConfig -> SyncVariant.writeSyncTwin(c, + userAgentPlugin.getSymbol(), + USER_AGENT_PLUGIN, + asyncConfig, + userAgentInterceptor, + versionSymbol, + serviceId)); + return syncFile.map(f -> List.of(filename, f)).orElse(List.of(filename)); }) .build()); } else { return List.of(); } } - } diff --git a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/customizations/dynamodb/AwsDynamoDbRetryIntegration.java b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/customizations/dynamodb/AwsDynamoDbRetryIntegration.java index 4459b3e0a..99bb205b1 100644 --- a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/customizations/dynamodb/AwsDynamoDbRetryIntegration.java +++ b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/customizations/dynamodb/AwsDynamoDbRetryIntegration.java @@ -13,6 +13,7 @@ import software.amazon.smithy.python.codegen.CodegenUtils; import software.amazon.smithy.python.codegen.GenerationContext; import software.amazon.smithy.python.codegen.SmithyPythonDependency; +import software.amazon.smithy.python.codegen.SyncVariant; import software.amazon.smithy.python.codegen.integrations.PythonIntegration; import software.amazon.smithy.python.codegen.integrations.RuntimeClientPlugin; @@ -112,6 +113,16 @@ public List getClientPlugins(GenerationContext context) { .pythonPlugin(dynamodbRetryPlugin) .writeAdditionalFiles((c) -> { String filename = "src/%s/%s.py".formatted(moduleName, pluginFile); + var asyncConfig = CodegenUtils.getAsyncConfigSymbol(c.settings(), c.model()) + .orElseThrow(); + Object[] args = { + asyncConfig, + retryStrategyOptions, + standardRetryStrategy, + configSource, + exponentialBackoffStrategy, + exponentialBackoffJitterType, + adaptiveRetryStrategy}; c.writerDelegator() .useFileWriter( filename, @@ -119,18 +130,12 @@ public List getClientPlugins(GenerationContext context) { writer -> { writer.addDependency(SmithyPythonDependency.SMITHY_CORE); writer.addDependency(AwsPythonDependency.SMITHY_AWS_CORE); - writer.write( - DYNAMODB_RETRY_MODULE, - CodegenUtils.getAsyncConfigSymbol(c.settings(), c.model()) - .orElseThrow(), - retryStrategyOptions, - standardRetryStrategy, - configSource, - exponentialBackoffStrategy, - exponentialBackoffJitterType, - adaptiveRetryStrategy); + writer.write(DYNAMODB_RETRY_MODULE, args); }); - return List.of(filename); + return SyncVariant + .writeSyncTwin(c, dynamodbRetryPlugin.getSymbol(), DYNAMODB_RETRY_MODULE, args) + .map(f -> List.of(filename, f)) + .orElse(List.of(filename)); }) .build()); } diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/ClientGenerator.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/ClientGenerator.java index 45f2c1dcd..e92533c07 100644 --- a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/ClientGenerator.java +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/ClientGenerator.java @@ -9,6 +9,8 @@ import java.util.Collection; import java.util.LinkedHashSet; +import java.util.function.UnaryOperator; +import software.amazon.smithy.codegen.core.Symbol; import software.amazon.smithy.codegen.core.SymbolProvider; import software.amazon.smithy.codegen.core.SymbolReference; import software.amazon.smithy.model.Model; @@ -19,6 +21,7 @@ import software.amazon.smithy.model.shapes.ServiceShape; import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.traits.DocumentationTrait; +import software.amazon.smithy.model.traits.RequiredTrait; import software.amazon.smithy.model.traits.StringTrait; import software.amazon.smithy.python.codegen.integrations.PythonIntegration; import software.amazon.smithy.python.codegen.integrations.RuntimeClientPlugin; @@ -46,11 +49,58 @@ final class ClientGenerator implements Runnable { @Override public void run() { - context.writerDelegator().useShapeWriter(service, this::generateService); + context.writerDelegator().useShapeWriter(service, writer -> generateService(writer, false)); + // The sync variant needs a generated config to resolve; other services stay async-only. + if (CodegenUtils.getAsyncConfigSymbol(context.settings(), context.model()).isPresent()) { + var mapper = SyncVariant.mapper(context.settings()); + var syncClient = mapper.apply(symbolProvider.toSymbol(service)); + context.writerDelegator() + .useFileWriter(syncClient.getDefinitionFile(), syncClient.getNamespace(), writer -> { + writer.setSymbolMapper(mapper); + generateService(writer, true); + }); + writeSyncExports(syncClient, mapper); + } + } + + private void writeSyncExports(Symbol syncClient, UnaryOperator mapper) { + var syncConfig = mapper.apply(CodegenUtils.getAsyncConfigSymbol(context.settings(), model).get()); + var syncPackage = context.settings().moduleName() + ".sync"; + var syncInit = "./src/" + context.settings().moduleName() + "/sync/__init__.py"; + context.writerDelegator().useFileWriter(syncInit, syncPackage, writer -> { + writer.write("from .client import $L", syncClient.getName()); + writer.write("from .config import $L, Plugin", syncConfig.getName()); + writer.write(""); + writer.write("__all__ = [$S, $S, \"Plugin\"]", syncClient.getName(), syncConfig.getName()); + }); + + // Lazily exported from the package root so importing the package, or the async + // client, does not import the sync client. + var init = "./src/" + context.settings().moduleName() + "/__init__.py"; + context.writerDelegator().useFileWriter(init, context.settings().moduleName(), writer -> { + writer.addStdlibImport("typing", "TYPE_CHECKING"); + writer.addStdlibImport("typing", "Any"); + writer.write(""" + if TYPE_CHECKING: + from .sync import $1L, $2L + + __all__ = [$1S, $2S] + + + def __getattr__(name: str) -> Any: + if name in __all__: + from . import sync + + return getattr(sync, name) + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + """, syncClient.getName(), syncConfig.getName()); + }); } - private void generateService(PythonWriter writer) { - var serviceSymbol = symbolProvider.toSymbol(service); + private void generateService(PythonWriter writer, boolean sync) { + UnaryOperator code = sync ? SyncVariant::toSyncCode : UnaryOperator.identity(); + var serviceSymbol = writer.getSymbolMapper().apply(symbolProvider.toSymbol(service)); writer.addLocallyDefinedSymbol(serviceSymbol); var configSymbol = CodegenUtils.getConfigSymbol(context.settings()); var pluginSymbol = CodegenUtils.getPluginSymbol(context.settings()); @@ -87,7 +137,7 @@ private void generateService(PythonWriter writer) { writer.addStdlibImport("copy", "deepcopy"); - writer.write(""" + writer.write(code.apply(""" def __init__( self, config: $1T | None = None, @@ -175,20 +225,20 @@ raise RuntimeError( config=config, retry_strategy=retry_strategy, ) - """, + """), configSym, pluginSymbol, writer.consumer(w -> writeConstructorDocs(w, serviceSymbol.getName())), writer.consumer(w -> writeDefaultPlugins(w, servicePlugins)), writer.consumer(w -> { if (isAsyncConfig) { - w.write("config = await $T.resolve()", configSym); + w.write(code.apply("config = await $T.resolve()"), configSym); } else { w.write("config = $T()", configSym); } }), writer.consumer(w -> { - w.pushState(new ClientSetupSection()); + w.pushState(new ClientSetupSection(sync)); w.write("pass"); w.popState(); }), @@ -210,9 +260,12 @@ raise RuntimeError( for (OperationShape operation : topDownIndex.getContainedOperations(service)) { if (eventStreamIndex.getInputInfo(operation).isPresent() || eventStreamIndex.getOutputInfo(operation).isPresent()) { - generateEventStreamOperation(writer, operation); + // Sync clients don't support event streams. + if (!sync) { + generateEventStreamOperation(writer, operation); + } } else { - generateOperation(writer, operation); + generateOperation(writer, operation, sync); } } }); @@ -244,7 +297,7 @@ private void writeConstructorDocs(PythonWriter writer, String clientName) { /** * Generates the function for a single operation. */ - private void generateOperation(PythonWriter writer, OperationShape operation) { + private void generateOperation(PythonWriter writer, OperationShape operation, boolean sync) { var operationSymbol = symbolProvider.toSymbol(operation); var operationMethodSymbol = operationSymbol.expectProperty(OPERATION_METHOD); var pluginSymbol = CodegenUtils.getPluginSymbol(context.settings()); @@ -276,23 +329,30 @@ private void generateOperation(PythonWriter writer, OperationShape operation) { writer.putContext("plugin", pluginSymbol); writer.putContext("operationName", operationMethodSymbol.getName()); writer.putContext("operation", symbolProvider.toSymbol(operation)); - writer.write(""" - async def ${operationName:L}( - self, - input: ${input:L}, - plugins: list[${plugin:T}] | None = None - ) -> ${output:L}: - ${C|} - pipeline, call = await self._prepare_call( - input, - ${operation:T}, - [ + // Sync callers may omit an input that has no required members. + var optionalInput = sync && input.members().stream().noneMatch(m -> m.hasTrait(RequiredTrait.class)); + writer.putContext("optionalInput", optionalInput); + var template = + """ + async def ${operationName:L}( + self, + ${?optionalInput}input: ${input:L} | None = None,${/optionalInput}${^optionalInput}input: ${input:L},${/optionalInput} + plugins: list[${plugin:T}] | None = None + ) -> ${output:L}: ${C|} - ], - plugins, - ) - return await pipeline(call) - """, + ${?optionalInput}if input is None: + input = ${operation:T}.input() + ${/optionalInput}pipeline, call = await self._prepare_call( + input, + ${operation:T}, + [ + ${C|} + ], + plugins, + ) + return await pipeline(call) + """; + writer.write(sync ? SyncVariant.toSyncCode(template) : template, writer.consumer(w -> writeOperationDocs(w, operation, input, output)), writer.consumer(w -> writeDefaultOperationPlugins(w, operation))); } diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/SyncVariant.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/SyncVariant.java new file mode 100644 index 000000000..1880783f2 --- /dev/null +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/SyncVariant.java @@ -0,0 +1,166 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.python.codegen; + +import java.util.ArrayList; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.UnaryOperator; +import java.util.regex.Pattern; +import software.amazon.smithy.codegen.core.Symbol; +import software.amazon.smithy.codegen.core.SymbolReference; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Derives the sync variant of generated client code from its async variant. + * + *

Runtime libraries pair every async component with a sync twin by convention: the + * async one lives under an {@code aio} package and carries an {@code Async} prefix, the + * sync one drops both. Generators therefore write each file once, and the sync pass + * maps symbols and strips {@code async}/{@code await} from the code templates. + */ +@SmithyInternalApi +public final class SyncVariant { + + private static final Pattern ASYNC_PREFIX = Pattern.compile("\\bAsync(?=[A-Z])"); + private static final Pattern ASYNC_KEYWORD = Pattern.compile("\\basync (?=def |with |for )"); + private static final Pattern AWAIT_KEYWORD = Pattern.compile("\\bawait "); + + // Sync twins that don't follow the naming convention. + private static final Map SPECIAL_CASES = Map.of( + "smithy_http.aio.aiohttp.AIOHTTPClient", + Symbol.builder() + .name("Urllib3HTTPClient") + .namespace("smithy_http.urllib3", ".") + .addDependency(SmithyPythonDependency.SMITHY_HTTP.withOptionalDependencies("urllib3")) + .build()); + + // Generated modules typed by the config, so each has a sync twin. + private static final Set SYNC_MODULES = Set.of("client", "config", "user_agent", "retries"); + + private SyncVariant() {} + + /** + * Strips the {@code async} and {@code await} keywords from a code template. + * + *

Only apply this to code templates, never to documentation pulled from a model. + * + * @param template The async code template. + * @return Returns the sync code template. + */ + public static String toSyncCode(String template) { + return AWAIT_KEYWORD.matcher(ASYNC_KEYWORD.matcher(template).replaceAll("")).replaceAll(""); + } + + /** + * @param name An async class or type name, such as {@code AsyncSTSClient}. + * @return Returns the name of its sync twin, such as {@code STSClient}. + */ + public static String toSyncName(String name) { + return ASYNC_PREFIX.matcher(name).replaceAll(""); + } + + /** + * Maps an async runtime symbol to its sync twin. + * + * @param symbol The symbol to map. + * @return Returns the sync twin, or the symbol itself if it is mode-neutral. + */ + public static Symbol toSync(Symbol symbol) { + var special = SPECIAL_CASES.get(symbol.getFullName()); + if (special != null) { + return special; + } + var references = new ArrayList(); + var name = symbol.getName(); + var changed = false; + for (var reference : symbol.getReferences()) { + var mapped = toSync(reference.getSymbol()); + if (!mapped.equals(reference.getSymbol())) { + changed = true; + name = Pattern.compile("\\b" + Pattern.quote(reference.getSymbol().getName()) + "\\b") + .matcher(name) + .replaceAll(mapped.getName()); + } + var alias = reference.getAlias().equals(reference.getSymbol().getName()) + ? mapped.getName() + : reference.getAlias(); + references.add(reference.toBuilder().symbol(mapped).alias(alias).build()); + } + var namespace = symbol.getNamespace(); + if (isAioNamespace(namespace)) { + namespace = namespace.replace(".aio.", ".").replaceAll("\\.aio$", ""); + name = toSyncName(name); + changed = true; + } + if (!changed) { + return symbol; + } + return symbol.toBuilder() + .name(name) + .namespace(namespace, ".") + .references(references) + .build(); + } + + /** + * Creates the symbol mapper for the sync variant of a generated client. + * + *

Generated symbols typed by the config move into the {@code sync} subpackage; all + * other symbols map through {@link #toSync(Symbol)}. + * + * @param settings The client settings. + * @return Returns the mapper. + */ + public static UnaryOperator mapper(PythonSettings settings) { + var module = settings.moduleName(); + return symbol -> { + var namespace = symbol.getNamespace(); + if (namespace.startsWith(module + ".") + && SYNC_MODULES.contains(namespace.substring(module.length() + 1))) { + var file = namespace.substring(module.length() + 1); + return symbol.toBuilder() + .name(toSyncName(symbol.getName())) + .namespace(module + ".sync." + file, ".") + .definitionFile("./src/" + module + "/sync/" + file + ".py") + .build(); + } + return toSync(symbol); + }; + } + + /** + * Writes the sync twin of a generated plugin module, if the client has a sync variant. + * + * @param context The generation context. + * @param plugin The async plugin symbol. + * @param template The async code template that defines the plugin. + * @param args The template arguments, written with the sync symbol mapper. + * @return Returns the written file, if any. + */ + public static Optional writeSyncTwin( + GenerationContext context, + Symbol plugin, + String template, + Object... args + ) { + if (CodegenUtils.getAsyncConfigSymbol(context.settings(), context.model()).isEmpty()) { + return Optional.empty(); + } + var mapper = mapper(context.settings()); + var syncPlugin = mapper.apply(plugin); + context.writerDelegator() + .useFileWriter(syncPlugin.getDefinitionFile(), syncPlugin.getNamespace(), writer -> { + writer.setSymbolMapper(mapper); + writer.write(toSyncCode(template), args); + }); + return Optional.of(syncPlugin.getDefinitionFile()); + } + + private static boolean isAioNamespace(String namespace) { + return namespace.contains(".aio.") || namespace.endsWith(".aio"); + } +} diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/ConfigGenerator.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/ConfigGenerator.java index 2a388c7f8..9576f9b4a 100644 --- a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/ConfigGenerator.java +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/ConfigGenerator.java @@ -23,6 +23,7 @@ import software.amazon.smithy.python.codegen.RuntimeTypes; import software.amazon.smithy.python.codegen.SmithyPythonDependency; import software.amazon.smithy.python.codegen.SymbolProperties; +import software.amazon.smithy.python.codegen.SyncVariant; import software.amazon.smithy.python.codegen.integrations.PythonIntegration; import software.amazon.smithy.python.codegen.integrations.RuntimeClientPlugin; import software.amazon.smithy.python.codegen.sections.AsyncConfigSection; @@ -280,6 +281,8 @@ public void run() { writer.popState(); }); + asyncConfigForPlugin.ifPresent(this::generateSyncConfig); + // Generate the plugin symbol. This is just a callable. We could do something // like have a class to implement, but that seems unnecessarily burdensome for // a single function. @@ -304,6 +307,29 @@ public void run() { }); } + // The sync config lives in its own module so the async config module stays as is. + private void generateSyncConfig(Symbol asyncConfig) { + var mapper = SyncVariant.mapper(settings); + var syncConfig = mapper.apply(asyncConfig); + var plugin = mapper.apply(CodegenUtils.getPluginSymbol(settings)); + context.writerDelegator().useFileWriter(syncConfig.getDefinitionFile(), syncConfig.getNamespace(), writer -> { + writer.setSymbolMapper(mapper); + writeInterceptorsType(writer, "..models"); + stageProtocolSettings(context, writer); + writer.pushState(new AsyncConfigSection(true)); + writer.popState(); + + writer.addStdlibImport("typing", "Callable"); + writer.addStdlibImport("typing", "TypeAlias"); + writer.write("$L: TypeAlias = Callable[[$T], None]", plugin.getName(), asyncConfig); + writer.writeDocs(""" + A callable that customizes a client configuration. Service-level plugins are + applied once to the base configuration inherited by every operation. + Operation-level plugins apply only to a single operation invocation. + """, context); + }); + } + // Emit the shared _PROTOCOL_SETTINGS bag as the union of the fields every protocol // the service resolves needs. private void stageProtocolSettings(GenerationContext context, PythonWriter writer) { @@ -360,6 +386,10 @@ private void stageProtocolSettings(GenerationContext context, PythonWriter write } private void writeInterceptorsType(PythonWriter writer) { + writeInterceptorsType(writer, ".models"); + } + + private void writeInterceptorsType(PythonWriter writer, String modelsModule) { var symbolProvider = context.symbolProvider(); var operationShapes = TopDownIndex.of(context.model()) .getContainedOperations(settings.service()); @@ -386,7 +416,7 @@ private void writeInterceptorsType(PythonWriter writer) { writer.write("if TYPE_CHECKING:").indent(); writer.write("from smithy_core.interceptors import Interceptor"); - writer.openBlock("from .models import (", ")", () -> { + writer.openBlock("from $L import (", ")", modelsModule, () -> { for (var name : names) { writer.write("$L,", name); } diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/sections/AsyncConfigSection.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/sections/AsyncConfigSection.java index 6f5eb6378..4e841777d 100644 --- a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/sections/AsyncConfigSection.java +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/sections/AsyncConfigSection.java @@ -12,6 +12,12 @@ * *

AWS integrations intercept this section to generate the async config * subclass (e.g., AsyncBedrockRuntimeConfig) that inherits from AsyncAwsConfig. + * + * @param sync Whether to generate the sync config (e.g., BedrockRuntimeConfig) instead. */ @SmithyInternalApi -public record AsyncConfigSection() implements CodeSection {} +public record AsyncConfigSection(boolean sync) implements CodeSection { + public AsyncConfigSection() { + this(false); + } +} diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/sections/ClientSetupSection.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/sections/ClientSetupSection.java index 3be51b2db..2731cd6dc 100644 --- a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/sections/ClientSetupSection.java +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/sections/ClientSetupSection.java @@ -8,5 +8,11 @@ /** * Section for service-specific lazy client setup. + * + * @param sync Whether the section is in the sync variant of the client. */ -public record ClientSetupSection() implements CodeSection {} +public record ClientSetupSection(boolean sync) implements CodeSection { + public ClientSetupSection() { + this(false); + } +} diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/writer/ImportDeclarations.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/writer/ImportDeclarations.java index 2c364fe69..dc0a8d6d9 100644 --- a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/writer/ImportDeclarations.java +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/writer/ImportDeclarations.java @@ -91,7 +91,7 @@ private String relativize(String namespace) { var prefix = StringUtils.repeat(".", localParts.length - commonSegments); String[] segments = namespace.split("\\.", commonSegments + 1); if (commonSegments >= segments.length) { - return "."; + return prefix.isEmpty() ? "." : prefix; } else { return prefix + segments[commonSegments]; } diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/writer/PythonWriter.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/writer/PythonWriter.java index 1888ff3a8..e73b9212d 100644 --- a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/writer/PythonWriter.java +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/writer/PythonWriter.java @@ -12,6 +12,7 @@ import java.util.Optional; import java.util.Set; import java.util.function.BiFunction; +import java.util.function.UnaryOperator; import java.util.logging.Logger; import software.amazon.smithy.codegen.core.CodegenException; import software.amazon.smithy.codegen.core.Symbol; @@ -53,6 +54,7 @@ public final class PythonWriter extends SymbolWriter symbolMapper = UnaryOperator.identity(); /** * Tracks all symbols referenced via {@code $T}, keyed by simple name. @@ -91,6 +93,27 @@ public PythonWriter(PythonSettings settings, String fullPackageName, String file putFormatter('N', new PythonNodeFormatter(this)); } + /** + * Rewrites every symbol formatted with {@code $T} before it is imported and written. + * + *

Lets one generator emit both the async and the sync variant of a file: the sync + * pass maps each async runtime symbol to its sync twin. + * + * @param symbolMapper The mapping to apply. + * @return Returns the writer. + */ + public PythonWriter setSymbolMapper(UnaryOperator symbolMapper) { + this.symbolMapper = symbolMapper; + return this; + } + + /** + * @return Returns the mapping applied to symbols formatted with {@code $T}. + */ + public UnaryOperator getSymbolMapper() { + return symbolMapper; + } + /** * A factory class to create {@link PythonWriter}s. */ @@ -499,7 +522,8 @@ private static String buildModuleAlias(Symbol symbol) { private final class PythonSymbolFormatter implements BiFunction { @Override public String apply(Object type, String indent) { - if (type instanceof Symbol typeSymbol) { + if (type instanceof Symbol unmappedSymbol) { + var typeSymbol = symbolMapper.apply(unmappedSymbol); // If a symbol has the IMPORTABLE property set to false, don't import it and // treat the lack of the property being set as true if (typeSymbol.getProperty(IMPORTABLE).orElse(true)) { @@ -507,8 +531,12 @@ public String apply(Object type, String indent) { } return resolvePlaceholder(typeSymbol); } else if (type instanceof SymbolReference typeSymbol) { - addImport(typeSymbol.getSymbol(), typeSymbol.getAlias(), SymbolReference.ContextOption.USE); - return typeSymbol.getAlias(); + var mapped = symbolMapper.apply(typeSymbol.getSymbol()); + var alias = typeSymbol.getAlias().equals(typeSymbol.getSymbol().getName()) + ? mapped.getName() + : typeSymbol.getAlias(); + addImport(mapped, alias, SymbolReference.ContextOption.USE); + return alias; } else { throw new CodegenException( "Invalid type provided to $T. Expected a Symbol, but found `" + type + "`"); diff --git a/packages/smithy-aws-core/pyproject.toml b/packages/smithy-aws-core/pyproject.toml index b3952f5a7..f86916eeb 100644 --- a/packages/smithy-aws-core/pyproject.toml +++ b/packages/smithy-aws-core/pyproject.toml @@ -45,6 +45,13 @@ ProfileSessionKeys = "smithy_aws_core.aio.identity.chain.providers.profile:Async ProfileStaticKeys = "smithy_aws_core.aio.identity.chain.providers.profile:AsyncProfileStaticCredentialsProvider" ProfileCredentialProcess = "smithy_aws_core.aio.identity.chain.providers.process:AsyncProfileCredentialProcessProvider" +[project.entry-points."smithy_aws_core.identity.chain_providers"] +Environment = "smithy_aws_core.identity.chain.providers.environment:EnvironmentCredentialsProvider" +SharedConfig = "smithy_aws_core.identity.chain.providers.shared_config:SharedConfigProvider" +ProfileSessionKeys = "smithy_aws_core.identity.chain.providers.profile:ProfileSessionCredentialsProvider" +ProfileStaticKeys = "smithy_aws_core.identity.chain.providers.profile:ProfileStaticCredentialsProvider" +ProfileCredentialProcess = "smithy_aws_core.identity.chain.providers.process:ProfileCredentialProcessProvider" + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" diff --git a/packages/smithy-aws-core/src/smithy_aws_core/aio/auth/sigv4.py b/packages/smithy-aws-core/src/smithy_aws_core/aio/auth/sigv4.py index 0905bb639..2838eb996 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/aio/auth/sigv4.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/aio/auth/sigv4.py @@ -1,7 +1,7 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import re -from typing import TYPE_CHECKING, Protocol, Self +from typing import TYPE_CHECKING, Protocol from aws_sdk_signers import AsyncEventSigner, AsyncSigV4Signer, SigV4SigningProperties from smithy_core.aio.interfaces.auth import ( @@ -11,17 +11,12 @@ from smithy_core.aio.interfaces.auth import ( AsyncEventSigner as AsyncEventSignerProtocol, ) -from smithy_core.exceptions import SmithyIdentityError from smithy_core.interfaces import TypedProperties as _TypedProperties from smithy_core.types import PropertyKey from smithy_http.aio.interfaces import HTTPRequest -from ...identity import ( - AWS_IDENTITY_CONFIG, - AWSCredentialsIdentity, - AWSIdentityProperties, -) -from ...traits import SigV4Trait +from ...auth.sigv4 import _SigV4AuthSchemeBase # pyright: ignore[reportPrivateUsage] +from ...identity import AWSCredentialsIdentity, AWSIdentityProperties from ..identity import AsyncAWSCredentialsResolver if TYPE_CHECKING: @@ -48,16 +43,16 @@ class SigV4Config(Protocol): class AsyncSigV4AuthScheme( + _SigV4AuthSchemeBase, AsyncAuthScheme[ HTTPRequest, AWSCredentialsIdentity, AWSIdentityProperties, SigV4SigningProperties, - ] + ], ): """SigV4 AsyncAuthScheme.""" - scheme_id = SigV4Trait.id _signer: SigV4Signer def __init__( @@ -68,45 +63,18 @@ def __init__( ) -> None: """Constructor. - :param identity_resolver: The identity resolver to extract the api key identity. + :param service: The name of the service to sign requests for. :param signer: The signer used to sign the request. """ + super().__init__(service=service) # TODO: There are type mismatches in the signature of the "sign" method. # The issues seems to be that it's not using protocols in its signature self._signer = signer or AsyncSigV4Signer() # type: ignore - self._service = service - - def identity_properties( - self, *, context: _TypedProperties - ) -> AWSIdentityProperties: - config = context[AWS_IDENTITY_CONFIG] - return { - "access_key_id": config.aws_access_key_id, - "secret_access_key": config.aws_secret_access_key, - "session_token": config.aws_session_token, - } def identity_resolver( self, *, context: _TypedProperties ) -> AsyncAWSCredentialsResolver: - config = context.get(SIGV4_CONFIG) - if config is None or config.aws_credentials_identity_resolver is None: - raise SmithyIdentityError( - "Attempted to use SigV4 auth, but aws_credentials_identity_resolver was not " - "set on the config." - ) - return config.aws_credentials_identity_resolver - - def signer_properties(self, *, context: _TypedProperties) -> SigV4SigningProperties: - config = context.get(SIGV4_CONFIG) - if config is None or config.region is None: - raise SmithyIdentityError( - "Attempted to use SigV4 auth, but region was not set on the config." - ) - return { - "region": config.region, - "service": self._service, - } + return super().identity_resolver(context=context) def signer(self) -> SigV4Signer: return self._signer @@ -126,7 +94,3 @@ def event_signer( initial_signature=signature.encode("utf-8"), event_encoder_cls=EventHeaderEncoder, ) - - @classmethod - def from_trait(cls, trait: SigV4Trait, /) -> Self: - return cls(service=trait.name) diff --git a/packages/smithy-aws-core/src/smithy_aws_core/aio/config/aws_config.py b/packages/smithy-aws-core/src/smithy_aws_core/aio/config/aws_config.py index b6443ca17..e861627d6 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/aio/config/aws_config.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/aio/config/aws_config.py @@ -2,10 +2,10 @@ # SPDX-License-Identifier: Apache-2.0 from collections.abc import Mapping -from copy import deepcopy -from dataclasses import dataclass, field, fields +from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any, ClassVar, Self, TypedDict, Unpack +from smithy_core.aio._program import run_program_async from smithy_core.retries import RetryStrategyOptions, RetryStrategyType if TYPE_CHECKING: @@ -20,15 +20,11 @@ AWSIdentityProperties, ) -from ...config.exceptions import ConfigError, ConfigValidationError -from ...config.types import UNSET, ConfigSource, FieldSpec, Resolved -from ...config.validators import ( - validate_max_attempts, - validate_profile, - validate_region, - validate_retry_mode, - validate_sdk_ua_app_id, +from ...config.aws_config import ( + _AwsConfigBase, # pyright: ignore[reportPrivateUsage] + _base_fields, # pyright: ignore[reportPrivateUsage] ) +from ...config.types import ConfigSource, FieldSpec from .context import AsyncSharedConfigContext from .filesystem import AsyncFileSystem from .resolvers import ( @@ -39,8 +35,6 @@ resolve_sdk_ua_app_id, ) -_CREDENTIAL_FIELDS = ("aws_access_key_id", "aws_secret_access_key", "aws_session_token") - class AwsConfigOverrides(TypedDict, total=False): """Common keyword overrides accepted by AWS config resolution.""" @@ -63,8 +57,8 @@ class AwsConfigOverrides(TypedDict, total=False): retry_strategy: "AsyncRetryStrategy | RetryStrategyOptions | None" -@dataclass(kw_only=True, init=False) -class AsyncAwsConfig: +@dataclass(kw_only=True, init=False, repr=False) +class AsyncAwsConfig(_AwsConfigBase): """Base configuration class for all AWS services. Fields are resolved asynchronously from multiple sources (env vars, @@ -150,82 +144,14 @@ class AsyncAwsConfig: compare=False, ) - _FIELDS: ClassVar[dict[str, FieldSpec]] = { - "region": FieldSpec( - default=None, - resolver=resolve_region, - validator=validate_region, - ), - "retry_mode": FieldSpec( - default=RetryStrategyOptions.retry_mode, - resolver=resolve_retry_mode, - validator=validate_retry_mode, - ), - "max_attempts": FieldSpec( - default=RetryStrategyOptions.max_attempts, - resolver=resolve_max_attempts, - validator=validate_max_attempts, - ), - "endpoint_uri": FieldSpec( - default=None, - resolver=resolve_endpoint_uri, - ), - "aws_access_key_id": FieldSpec( - default=None, - ), - "aws_secret_access_key": FieldSpec( - default=None, - ), - "aws_session_token": FieldSpec( - default=None, - ), - "aws_credentials_identity_resolver": FieldSpec( - default=None, - ), - "sdk_ua_app_id": FieldSpec( - default=None, - resolver=resolve_sdk_ua_app_id, - validator=validate_sdk_ua_app_id, - ), - "user_agent_extra": FieldSpec( - default=None, - ), - "interceptors": FieldSpec( - default_factory=list, - ), - "http_request_config": FieldSpec( - default=None, - ), - "transport": FieldSpec( - default=None, - ), - "retry_strategy": FieldSpec( - default=None, - ), - } - - def __repr__(self) -> str: - """Render the config without exposing credential material. - - Defined on the base class so that every subclass inherits the - filtering, rather than relying on each subclass to mark its own - credential fields ``repr=False``. Subclasses must be declared with - ``@dataclass(repr=False)`` so they inherit this instead of generating - their own ``__repr__``. - """ - rendered = ", ".join( - f"{f.name}={getattr(self, f.name)!r}" - for f in fields(self) - if f.repr and f.name not in _CREDENTIAL_FIELDS - ) - return f"{type(self).__name__}({rendered})" - - def __init__(self, *args: object, **kwargs: object) -> None: - """Block direct construction without advertising config fields as parameters.""" - raise ConfigError( - f"{type(self).__name__} cannot be constructed directly. " - f"Use `await {type(self).__name__}.resolve(...)` instead." - ) + _FIELDS: ClassVar[dict[str, FieldSpec]] = _base_fields( + resolve_region=resolve_region, + resolve_retry_mode=resolve_retry_mode, + resolve_max_attempts=resolve_max_attempts, + resolve_endpoint_uri=resolve_endpoint_uri, + resolve_sdk_ua_app_id=resolve_sdk_ua_app_id, + ) + _RESOLVE_PREFIX: ClassVar[str] = "await " @classmethod async def resolve( @@ -270,36 +196,16 @@ async def _resolve( overrides: Mapping[str, object], ) -> Self: """Internal resolution entry point for generated typed config factories.""" - ctx = AsyncSharedConfigContext( - profile_name=profile, - fs=fs, - config_file_path=config_file_path, - credentials_file_path=credentials_file_path, + return await run_program_async( + cls._resolve_program( + profile=profile, + fs=fs, + config_file_path=config_file_path, + credentials_file_path=credentials_file_path, + overrides=overrides, + ) ) - # Fail fast on a bad profile when one was provided (not the default) - profile_source = ctx.profile_source - if profile_source is not ConfigSource.DEFAULT: - config_file = await ctx.parsed_profiles() - validate_profile(ctx.profile_name, config_file.profiles, profile_source) - - # Create the instance without calling the blocked constructor - instance = cls._create_instance() - instance._ctx = ctx - - # Resolve each field - await instance._resolve_fields(dict(overrides)) - - return instance - - def source_of(self, field_name: str) -> ConfigSource | None: - """Get the source that provided a field's value. - - :param field_name: The config field name. - :returns: The ConfigSource, or None if not tracked. - """ - return self._sources.get(field_name) - def resolution_context(self) -> AsyncSharedConfigContext | None: """Get the resolution context used to create this config. @@ -308,159 +214,10 @@ def resolution_context(self) -> AsyncSharedConfigContext | None: return self._ctx @classmethod - def _create_instance(cls) -> Self: - """Create an instance that bypasses construction blocking.""" - instance = object.__new__(cls) - object.__setattr__(instance, "_sources", {}) - object.__setattr__(instance, "_ctx", None) - for field_name in cls._FIELDS: - object.__setattr__(instance, field_name, UNSET) - return instance - - async def _resolve_fields(self, overrides: dict[str, Any]) -> None: - """Run the resolution pipeline for all fields.""" - unknown = set(overrides) - set(self._FIELDS) - if unknown: - raise ConfigValidationError( - f"Unknown config field(s): {sorted(unknown)}. " - f"Valid fields are: {sorted(self._FIELDS)}" - ) + def _new_context(cls, **kwargs: Any) -> AsyncSharedConfigContext: + return AsyncSharedConfigContext(**kwargs) - # Validate credential overrides and auto-wire the identity resolver - # before the field loop, so the loop sees the resolver as an override. - self._resolve_credentials(overrides) - - for field_name, spec in self._FIELDS.items(): - # check for overrides first - if field_name in overrides: - value = overrides[field_name] - if spec.converter is not None: - value = spec.converter(value) - setattr(self, field_name, value) - self._sources[field_name] = ConfigSource.OVERRIDE - # check in resolver - elif spec.resolver is not None: - result: Resolved[Any] = await spec.resolver(self._ctx) - if result.value is not UNSET: - setattr(self, field_name, result.value) - self._sources[field_name] = result.source - else: - # If resolver returned UNSET, fall back to default - self._apply_default(field_name, spec) - - else: - # No resolver, use default directly - self._apply_default(field_name, spec) - - # Run validator for all sources - if spec.validator is not None: - spec.validator(getattr(self, field_name)) - - def _apply_default(self, field_name: str, spec: FieldSpec) -> None: - """Apply the default value for a field.""" - if spec.default_factory is not None: - value = spec.default_factory() - else: - value = spec.default - setattr(self, field_name, value) - self._sources[field_name] = ConfigSource.DEFAULT - - def _resolve_credentials(self, overrides: dict[str, Any]) -> None: - """Validate in-code credentials and auto-wire StaticCredentialsResolver. - - Rules: - - If both aws_access_key_id and aws_secret_access_key are overridden, - auto-set aws_credentials_identity_resolver to a - StaticCredentialsResolver (unless the caller already provided one). - Only the overridden values are used, so a session token present in a - profile is not picked up here. - - If credentials are overridden but the key/secret pair is incomplete, - raise ConfigValidationError. - - If no credential is overridden, credentials are resolved from the - remaining sources. - """ - - required = {"aws_access_key_id", "aws_secret_access_key"} - - cred_overrides = {f for f in _CREDENTIAL_FIELDS if f in overrides} - - if not cred_overrides: - return - - if not required <= cred_overrides: - raise ConfigValidationError( - f"Partial credential override: {sorted(cred_overrides)}. " - "Both 'aws_access_key_id' and 'aws_secret_access_key' must be " - "provided together when overriding credentials." - ) - - # Auto-wire StaticCredentialsResolver if user didn't provide one - if overrides.get("aws_credentials_identity_resolver") is None: - # Lazy import to avoid circular dependency - from smithy_aws_core.aio.identity.static import ( - AsyncStaticCredentialsResolver, - ) - from smithy_aws_core.identity.components import AWSCredentialsIdentity - - identity = AWSCredentialsIdentity( - access_key_id=overrides["aws_access_key_id"], - secret_access_key=overrides["aws_secret_access_key"], - session_token=overrides.get("aws_session_token"), - ) - overrides["aws_credentials_identity_resolver"] = ( - AsyncStaticCredentialsResolver(identity=identity) - ) - - def __setattr__(self, name: str, value: Any) -> None: - """Guard and track config fields set after resolution. - - Rejects unknown field names, blocks credential mutation, validates the - new value, and records the field as an override so ``source_of()`` - stays accurate when plugins customize a config per request. - """ - # Reject unknown fields - if not name.startswith("_") and name not in self.__class__._FIELDS: - raise AttributeError( - f"'{type(self).__name__}' has no config field '{name}'" - ) - - # Apply the field's converter so a post-resolution assignment coerces - # the same way an override passed to resolve() does. - spec = self.__class__._FIELDS.get(name) - if spec is not None and spec.converter is not None and value is not UNSET: - value = spec.converter(value) - - # Block override for credentials after resolution - if ( - name in _CREDENTIAL_FIELDS - and hasattr(self, "_sources") - and name in self._sources - ): - raise AttributeError( - f"'{name}' cannot be modified after resolution. Pass credentials " - f"to `await {type(self).__name__}.resolve(...)`, or set " - "'aws_credentials_identity_resolver' to supply credentials " - "dynamically." - ) + def _static_credentials_resolver(self, identity: "AWSCredentialsIdentity") -> Any: + from smithy_aws_core.aio.identity.static import AsyncStaticCredentialsResolver - # Mark as override only if the field is in _FIELDS and was already resolved - if spec is not None and hasattr(self, "_sources") and name in self._sources: - if spec.validator is not None: - spec.validator(value) - self._sources[name] = ConfigSource.OVERRIDE - super().__setattr__(name, value) - - def __deepcopy__(self, memo: dict[int, Any]) -> Self: - """Deep-copy the config while sharing resources that must not be duplicated.""" - for shared in ( - self.aws_credentials_identity_resolver, - self.transport, - self.retry_strategy, - ): - if shared is not None: - memo[id(shared)] = shared - new = self._create_instance() - memo[id(self)] = new - for f in fields(self): - object.__setattr__(new, f.name, deepcopy(getattr(self, f.name), memo)) - return new + return AsyncStaticCredentialsResolver(identity=identity) diff --git a/packages/smithy-aws-core/src/smithy_aws_core/aio/config/context.py b/packages/smithy-aws-core/src/smithy_aws_core/aio/config/context.py index 90b3c6ffd..d6c597442 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/aio/config/context.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/aio/config/context.py @@ -1,26 +1,17 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 - -import logging -import os from pathlib import Path from typing import Any -from ...config.context import _resolve_config_paths # type: ignore[reportPrivateUsage] -from ...config.file_parser import ( - FileType, - standardize, +from smithy_core.aio._program import run_program_async + +from ...config.context import ( + _load_config, # type: ignore[reportPrivateUsage] + _SharedConfigContextBase, # type: ignore[reportPrivateUsage] ) from ...config.merged_config import MergedConfig -from ...config.types import ConfigSource -from .file_parser import parse_config_file_async from .filesystem import AsyncDefaultFileSystem, AsyncFileSystem -logger = logging.getLogger(__name__) - -_PROFILE_ENV_VAR = "AWS_PROFILE" -_DEFAULT_PROFILE = "default" - async def load_config_async( config_file_path: Path | None = None, @@ -40,21 +31,14 @@ async def load_config_async( Defaults to AsyncDefaultFileSystem (real disk I/O). :returns: A MergedConfig with merged profiles from both files. """ - filesystem = fs or AsyncDefaultFileSystem() - config_path, credentials_path = _resolve_config_paths( - config_file_path, credentials_file_path + return await run_program_async( + _load_config( + config_file_path, credentials_file_path, fs or AsyncDefaultFileSystem() + ) ) - raw_config = await parse_config_file_async(str(config_path), filesystem) - raw_credentials = await parse_config_file_async(str(credentials_path), filesystem) - - std_config = standardize(raw_config, FileType.CONFIG) - std_credentials = standardize(raw_credentials, FileType.CREDENTIALS) - - return MergedConfig(std_config, std_credentials) - -class AsyncSharedConfigContext: +class AsyncSharedConfigContext(_SharedConfigContextBase): """Resolution context shared across resolvers during config construction. Holds environment state and cached file data that resolvers use to @@ -81,83 +65,23 @@ def __init__( :param config_file_path: Override path for config file. :param credentials_file_path: Override path for credentials file. """ - self._fs: AsyncFileSystem = fs if fs is not None else AsyncDefaultFileSystem() - self._http_client: Any | None = http_client - self._profile_name, self._profile_source = self._resolve_profile_name( - profile_name - ) - self._config_file_path: Path | None = ( - Path(config_file_path) if config_file_path is not None else None - ) - self._credentials_file_path: Path | None = ( - Path(credentials_file_path) if credentials_file_path is not None else None + super().__init__( + profile_name=profile_name, + fs=fs if fs is not None else AsyncDefaultFileSystem(), + http_client=http_client, + config_file_path=config_file_path, + credentials_file_path=credentials_file_path, ) - self._cached_config_file: MergedConfig | None = None - - @property - def profile_name(self) -> str: - """The active profile name.""" - return self._profile_name - - @property - def profile_source(self) -> ConfigSource: - """The source of the active profile name.""" - return self._profile_source @property def fs(self) -> AsyncFileSystem: """The filesystem abstraction.""" return self._fs - @property - def http_client(self) -> Any | None: - """HTTP client for network-based resolvers.""" - return self._http_client - - def __deepcopy__(self, memo: Any) -> "AsyncSharedConfigContext": - """Return self rather than a copy. - - The context is read-only once resolution finishes: resolvers have - already pulled their values onto the config's fields, and nothing on - the request path reads it again. Generated clients deep-copy the - config on every operation call to keep plugin mutations scoped to - that call, which would otherwise rebuild the whole parsed profile - tree per request — work proportional to the size of the caller's - shared config files. Sharing this instead keeps that cost flat - without weakening the isolation of the fields plugins actually write. - """ - return self - async def parsed_profiles(self) -> MergedConfig: """Get the parsed and merged config/credentials file data. The result is cached after the first call so files are only read from disk once per context. """ - if self._cached_config_file is None: - self._cached_config_file = await load_config_async( - config_file_path=self._config_file_path, - credentials_file_path=self._credentials_file_path, - fs=self._fs, - ) - return self._cached_config_file - - def _resolve_profile_name( - self, explicit_profile: str | None - ) -> tuple[str, ConfigSource]: - """Determine the active profile name and where it came from. - - Priority: explicit argument > AWS_PROFILE env var > "default" - - :returns: Tuple of (profile_name, source), where source is the - provenance of the name: ``OVERRIDE`` for the explicit argument, - ``ENV`` for ``AWS_PROFILE``, and ``DEFAULT`` for the fallback. - """ - if explicit_profile is not None: - return explicit_profile, ConfigSource.OVERRIDE - - env_profile = os.environ.get(_PROFILE_ENV_VAR) - if env_profile: - return env_profile, ConfigSource.ENV - - return _DEFAULT_PROFILE, ConfigSource.DEFAULT + return await run_program_async(self._parsed_profiles()) diff --git a/packages/smithy-aws-core/src/smithy_aws_core/aio/config/file_parser.py b/packages/smithy-aws-core/src/smithy_aws_core/aio/config/file_parser.py index c26ec9b2d..e94feebad 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/aio/config/file_parser.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/aio/config/file_parser.py @@ -1,9 +1,10 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -from ...config.exceptions import ConfigParseError +from smithy_core.aio._program import run_program_async + from ...config.file_parser import ( RawParsedSections, - _parse_content, # type: ignore[reportPrivateUsage] + _parse_config_file, # type: ignore[reportPrivateUsage] ) from .filesystem import AsyncFileSystem @@ -20,10 +21,4 @@ async def parse_config_file_async( :returns: Raw sections dict {section_name: {key: value}}. :raises ConfigParseError: If the file has invalid syntax. """ - content = await filesystem.read_file(file_path) - if content is None: - return {} - try: - return _parse_content(content) - except ConfigParseError as e: - raise ConfigParseError(f"Unable to parse config file '{file_path}': {e}") from e + return await run_program_async(_parse_config_file(file_path, filesystem)) diff --git a/packages/smithy-aws-core/src/smithy_aws_core/aio/config/filesystem.py b/packages/smithy-aws-core/src/smithy_aws_core/aio/config/filesystem.py index 0a02d148f..80fadd1e9 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/aio/config/filesystem.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/aio/config/filesystem.py @@ -2,16 +2,16 @@ # SPDX-License-Identifier: Apache-2.0 import asyncio -import logging -from pathlib import Path from typing import Protocol, runtime_checkable +from ...config.filesystem import DefaultFileSystem + __all__ = [ "AsyncDefaultFileSystem", "AsyncFileSystem", ] -logger = logging.getLogger(__name__) +_SYNC_FILESYSTEM = DefaultFileSystem() @runtime_checkable @@ -43,13 +43,4 @@ async def read_file(self, path: str) -> str | None: :param path: Resolved file path. :returns: File content, or None if the file is inaccessible. """ - try: - content: str = await asyncio.to_thread( - Path(path).read_text, encoding="utf-8" - ) - return content - except FileNotFoundError: - return None - except (PermissionError, OSError) as e: - logger.warning("Unable to read config file '%s': %s", path, e) - return None + return await asyncio.to_thread(_SYNC_FILESYSTEM.read_file, path) diff --git a/packages/smithy-aws-core/src/smithy_aws_core/aio/config/resolvers.py b/packages/smithy-aws-core/src/smithy_aws_core/aio/config/resolvers.py index 70573b675..08f6a2d87 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/aio/config/resolvers.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/aio/config/resolvers.py @@ -1,12 +1,16 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 - -import os -import warnings -from collections.abc import Sequence - -from ...config.exceptions import ConfigValidationError -from ...config.types import UNSET, ConfigSource, Resolved +from smithy_core.aio._program import run_program_async + +from ...config.resolvers import ( + _endpoint_uri, # pyright: ignore[reportPrivateUsage] + _EndpointUriResolverBase, # pyright: ignore[reportPrivateUsage] + _max_attempts, # pyright: ignore[reportPrivateUsage] + _region, # pyright: ignore[reportPrivateUsage] + _retry_mode, # pyright: ignore[reportPrivateUsage] + _sdk_ua_app_id, # pyright: ignore[reportPrivateUsage] +) +from ...config.types import Resolved from .context import AsyncSharedConfigContext __all__ = [ @@ -19,74 +23,13 @@ ] -async def _resolve_str( - ctx: AsyncSharedConfigContext, - *, - env_vars: Sequence[str] = (), - profile_keys: Sequence[str] = (), -) -> Resolved[str]: - """Resolve a string value by checking providers in priority order. - - Priority: env vars (first match) > config file (profile keys, first match) > unresolved. - - :param ctx: The shared resolution context. - :param env_vars: Environment variable names to check, in order. - :param profile_keys: Config file profile keys to check, in order. - :returns: Resolved value with source, or Resolved(value=UNSET) if not found. - """ - # Check environment variables first - for var_name in env_vars: - value: str | None = os.environ.get(var_name) - if value: - return Resolved(value=value, source=ConfigSource.ENV) - - # Check config file profile keys - if profile_keys: - config_file = await ctx.parsed_profiles() - for key in profile_keys: - value = config_file.get(ctx.profile_name, key) - if value: - return Resolved(value=value, source=ConfigSource.PROFILE) - - return Resolved(value=UNSET, source=ConfigSource.DEFAULT) # type: ignore[arg-type] - - -async def _resolve_int( - ctx: AsyncSharedConfigContext, - *, - env_vars: Sequence[str] = (), - profile_keys: Sequence[str] = (), -) -> Resolved[int | None]: - """Resolve an integer value by checking providers in priority order. - - :param ctx: The shared resolution context. - :param env_vars: Environment variable names to check, in order. - :param profile_keys: Config file profile keys to check, in order. - :returns: Resolved int value with source, or Resolved(value=UNSET) if not found. - """ - result = await _resolve_str(ctx, env_vars=env_vars, profile_keys=profile_keys) - if result.value is UNSET: - return Resolved(value=UNSET, source=ConfigSource.DEFAULT) # type: ignore[arg-type] - try: - return Resolved(value=int(result.value), source=result.source) - except (ValueError, TypeError) as e: - raise ConfigValidationError( - f"Invalid integer value {result.value!r} for config key. " - "Expected a valid integer." - ) from e - - async def resolve_region(ctx: AsyncSharedConfigContext) -> Resolved[str | None]: """Resolve the AWS region from environment or config file. :param ctx: The shared resolution context. :returns: Resolved region value with source. """ - return await _resolve_str( - ctx, - env_vars=("AWS_REGION", "AWS_DEFAULT_REGION"), - profile_keys=("region",), - ) + return await run_program_async(_region(ctx)) async def resolve_retry_mode(ctx: AsyncSharedConfigContext) -> Resolved[str | None]: @@ -96,20 +39,7 @@ async def resolve_retry_mode(ctx: AsyncSharedConfigContext) -> Resolved[str | No :param ctx: The shared resolution context. :returns: Resolved retry mode value with source. """ - result = await _resolve_str( - ctx, - env_vars=("AWS_RETRY_MODE",), - profile_keys=("retry_mode",), - ) - - if result.value == "legacy": - warnings.warn( - "'legacy' retry mode is not supported, using 'standard' instead.", - UserWarning, - stacklevel=2, - ) - return Resolved(value="standard", source=result.source) - return result + return await run_program_async(_retry_mode(ctx)) async def resolve_max_attempts(ctx: AsyncSharedConfigContext) -> Resolved[int | None]: @@ -118,11 +48,7 @@ async def resolve_max_attempts(ctx: AsyncSharedConfigContext) -> Resolved[int | :param ctx: The shared resolution context. :returns: Resolved max attempts value with source. """ - return await _resolve_int( - ctx, - env_vars=("AWS_MAX_ATTEMPTS",), - profile_keys=("max_attempts",), - ) + return await run_program_async(_max_attempts(ctx)) async def resolve_endpoint_uri(ctx: AsyncSharedConfigContext) -> Resolved[str | None]: @@ -134,11 +60,7 @@ async def resolve_endpoint_uri(ctx: AsyncSharedConfigContext) -> Resolved[str | :param ctx: The shared resolution context. :returns: Resolved endpoint URI value with source. """ - return await _resolve_str( - ctx, - env_vars=("AWS_ENDPOINT_URL",), - profile_keys=("endpoint_url",), - ) + return await run_program_async(_endpoint_uri(ctx)) async def resolve_sdk_ua_app_id(ctx: AsyncSharedConfigContext) -> Resolved[str | None]: @@ -147,14 +69,10 @@ async def resolve_sdk_ua_app_id(ctx: AsyncSharedConfigContext) -> Resolved[str | :param ctx: The shared resolution context. :returns: Resolved app ID value with source. """ - return await _resolve_str( - ctx, - env_vars=("AWS_SDK_UA_APP_ID",), - profile_keys=("sdk_ua_app_id",), - ) + return await run_program_async(_sdk_ua_app_id(ctx)) -class EndpointUriResolver: +class EndpointUriResolver(_EndpointUriResolverBase): """Service-aware endpoint URI resolver. Resolution order (first match wins): @@ -164,41 +82,10 @@ class EndpointUriResolver: 4. Global config file (profile -> endpoint_url) """ - def __init__(self, service_id: str): - """Initialize with a service identifier. - - :param service_id: The service identifier (e.g., "bedrock_runtime"). - Used to construct the service-specific env var and config lookup key. - """ - self._service_env_var = ( - f"AWS_ENDPOINT_URL_{service_id.replace(' ', '_').replace('-', '_').upper()}" - ) - - self._service_key = service_id.replace(" ", "_").replace("-", "_").lower() - async def __call__(self, ctx: AsyncSharedConfigContext) -> Resolved[str | None]: """Resolve the endpoint URI from all sources. :param ctx: The shared resolution context. :returns: Resolved endpoint URI value with source. """ - value = os.environ.get(self._service_env_var) - if value: - return Resolved(value=value, source=ConfigSource.ENV) - - value = os.environ.get("AWS_ENDPOINT_URL") - if value: - return Resolved(value=value, source=ConfigSource.ENV) - - config_file = await ctx.parsed_profiles() - value = config_file.get_service_config( - ctx.profile_name, self._service_key, "endpoint_url" - ) - if value: - return Resolved(value=value, source=ConfigSource.PROFILE) - - value = config_file.get(ctx.profile_name, "endpoint_url") - if value: - return Resolved(value=value, source=ConfigSource.PROFILE) - - return Resolved(value=UNSET, source=ConfigSource.DEFAULT) # type: ignore[arg-type] + return await run_program_async(self._resolve(ctx)) diff --git a/packages/smithy-aws-core/src/smithy_aws_core/aio/endpoints/standard_regional.py b/packages/smithy-aws-core/src/smithy_aws_core/aio/endpoints/standard_regional.py index f575810f2..77e9dd400 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/aio/endpoints/standard_regional.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/aio/endpoints/standard_regional.py @@ -2,12 +2,10 @@ # SPDX-License-Identifier: Apache-2.0 from typing import Any -from smithy_core import URI from smithy_core.aio.interfaces import AsyncEndpointResolver -from smithy_core.endpoints import Endpoint, EndpointResolverParams, resolve_static_uri -from smithy_core.exceptions import EndpointResolutionError +from smithy_core.endpoints import Endpoint, EndpointResolverParams -from ...endpoints import REGIONAL_ENDPOINT_CONFIG +from ...endpoints.standard_regional import StandardRegionalEndpointsResolver __all__ = ["AsyncStandardRegionalEndpointsResolver"] @@ -16,20 +14,7 @@ class AsyncStandardRegionalEndpointsResolver(AsyncEndpointResolver): """Resolves endpoints for services with standard regional endpoints.""" def __init__(self, endpoint_prefix: str = "bedrock-runtime"): - self._endpoint_prefix = endpoint_prefix + self._sync = StandardRegionalEndpointsResolver(endpoint_prefix) async def resolve_endpoint(self, params: EndpointResolverParams[Any]) -> Endpoint: - if (static_uri := resolve_static_uri(params)) is not None: - return Endpoint(uri=static_uri) - - region_config = params.context.get(REGIONAL_ENDPOINT_CONFIG) - if region_config is not None and region_config.region is not None: - # TODO: use dns suffix determined from partition metadata - dns_suffix = "amazonaws.com" - hostname = f"{self._endpoint_prefix}.{region_config.region}.{dns_suffix}" - - return Endpoint(uri=URI(host=hostname)) - - raise EndpointResolutionError( - "Unable to resolve endpoint - either endpoint_url or region are required." - ) + return self._sync.resolve_endpoint(params) diff --git a/packages/smithy-aws-core/src/smithy_aws_core/aio/identity/chain/__init__.py b/packages/smithy-aws-core/src/smithy_aws_core/aio/identity/chain/__init__.py index d2f510205..e099f9983 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/aio/identity/chain/__init__.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/aio/identity/chain/__init__.py @@ -5,25 +5,18 @@ from importlib import metadata from typing import Any, cast +from smithy_core.aio._program import run_program_async from smithy_core.aio.interfaces.identity import AsyncIdentityResolver -from smithy_core.exceptions import SmithyIdentityError from smithy_core.interfaces.identity import Identity from smithy_http.aio.interfaces import AsyncHTTPClient from ....config.merged_config import MergedConfig -from ....identity.chain.exceptions import ( - IdentityChainConfigurationError, - IdentityChainError, - IdentityResolverFailure, - UnclaimedSource, +from ....identity.chain.exceptions import UnclaimedSource +from ....identity.chain.resolver import ( + _assemble, # pyright: ignore[reportPrivateUsage] + _IdentityChainBase, # pyright: ignore[reportPrivateUsage] ) -from ....identity.chain.ordering import ( - After, - Before, - Standard, - StandardProvider, -) -from .provider import ChainIdentityProvider, ChainSetup, NamedResolver +from .provider import ChainIdentityProvider, ChainSetup __all__ = ("AsyncIdentityChain",) @@ -31,13 +24,11 @@ logger = logging.getLogger(__name__) -class AsyncIdentityChain[I: Identity](AsyncIdentityResolver[I, Mapping[str, Any]]): +class AsyncIdentityChain[I: Identity]( + _IdentityChainBase[I], AsyncIdentityResolver[I, Mapping[str, Any]] +): """Resolves identities from an assembled sequence of resolvers.""" - _resolvers: tuple[AsyncIdentityResolver[I, Any], ...] - _identity_type: type[I] | None - _unclaimed_sources: tuple[UnclaimedSource, ...] - def __init__( self, resolvers: Sequence[AsyncIdentityResolver[I, Any]], @@ -54,14 +45,9 @@ def __init__( chain assembly. This parameter is used by :meth:`create`; omit it when constructing a chain directly. """ - self._resolvers = tuple(resolvers) - self._identity_type = identity_type - self._unclaimed_sources = tuple(unclaimed_sources) - - @property - def identity_type(self) -> type[I] | None: - """The identity type this chain resolves, or None if not declared.""" - return self._identity_type + super().__init__( + resolvers, identity_type=identity_type, unclaimed_sources=unclaimed_sources + ) @staticmethod async def create[ChainIdentity: Identity]( @@ -86,24 +72,15 @@ async def create[ChainIdentity: Identity]( network calls. """ discovered_providers = _discover_chain_identity_providers() - _validate_providers(discovered_providers) - providers = _sort_by_ordering(discovered_providers) setup = ChainSetup( config_file=config_file, profile_name=profile_name, region_override=region_override, http_client=http_client, ) - unclaimed_sources = _find_unclaimed_sources(discovered_providers) - - for provider in providers: - setup.set_current_provider(provider) - await provider.setup(identity_type, setup) - if setup.terminal: - break - - for source in unclaimed_sources: - logger.warning(str(source)) + unclaimed_sources = await run_program_async( + _assemble(identity_type, discovered_providers, setup, logger) + ) return AsyncIdentityChain( setup.resolvers, identity_type=identity_type, @@ -112,31 +89,11 @@ async def create[ChainIdentity: Identity]( async def get_identity(self, *, properties: Mapping[str, Any]) -> I: """Return the first identity resolved by the chain.""" - failures: list[IdentityResolverFailure] = [] - for resolver in self._resolvers: - try: - return await resolver.get_identity(properties=properties) - except SmithyIdentityError as error: - if isinstance(resolver, NamedResolver): - provider_name = resolver.provider_name - else: - provider_name = type(resolver).__name__ - failures.append( - IdentityResolverFailure( - provider_name=provider_name, - error=error, - ) - ) - - raise IdentityChainError( - failures=tuple(failures), - unclaimed_sources=self._unclaimed_sources, - ) + return await run_program_async(self._get_identity(properties)) async def invalidate(self) -> None: """Invalidate every resolver in the chain.""" - for resolver in self._resolvers: - await resolver.invalidate() + await run_program_async(self._invalidate()) def _discover_chain_identity_providers() -> tuple[ChainIdentityProvider, ...]: @@ -148,82 +105,3 @@ def _discover_chain_identity_providers() -> tuple[ChainIdentityProvider, ...]: ) discovered.append(provider_factory()) return tuple(discovered) - - -def _sort_by_ordering( - providers: Sequence[ChainIdentityProvider], -) -> tuple[ChainIdentityProvider, ...]: - if not providers: - return () - slot_indexes = {slot: index for index, slot in enumerate(StandardProvider)} - - def sort_key( - indexed_provider: tuple[int, ChainIdentityProvider], - ) -> tuple[int, int, int]: - discovery_index, provider = indexed_provider - ordering = provider.ordering - - match ordering: - case Before(): - constraint_precedence = 0 - case Standard(): - constraint_precedence = 1 - case After(): - constraint_precedence = 2 - case _: - raise IdentityChainConfigurationError( - f"Provider {type(provider).__name__} returned an unsupported " - f"ordering constraint: {ordering!r}." - ) - - return (slot_indexes[ordering.slot], constraint_precedence, discovery_index) - - indexed_providers = enumerate(providers) - ordered_providers = sorted(indexed_providers, key=sort_key) - return tuple(provider for _, provider in ordered_providers) - - -def _validate_providers(providers: Sequence[ChainIdentityProvider]) -> None: - discovered_names: dict[str, ChainIdentityProvider] = {} - discovered_standard_slots: dict[StandardProvider, ChainIdentityProvider] = {} - - for provider in providers: - normalized_name = provider.name.casefold() - if (previous := discovered_names.get(normalized_name)) is not None: - raise IdentityChainConfigurationError( - f"Credential providers {type(previous).__name__} and " - f"{type(provider).__name__} use the same name: {provider.name}." - ) - discovered_names[normalized_name] = provider - - ordering = provider.ordering - if isinstance(ordering, Standard): - if (previous := discovered_standard_slots.get(ordering.slot)) is not None: - raise IdentityChainConfigurationError( - f"Credential providers {type(previous).__name__} and " - f"{type(provider).__name__} both claim standard slot: " - f"{ordering.slot.name}." - ) - discovered_standard_slots[ordering.slot] = provider - - -def _find_unclaimed_sources( - providers: Sequence[ChainIdentityProvider], -) -> tuple[UnclaimedSource, ...]: - claimed_slots = { - provider.ordering.slot - for provider in providers - if isinstance(provider.ordering, Standard) - } - unclaimed_sources: list[UnclaimedSource] = [] - - for slot in StandardProvider: - if slot in claimed_slots or not slot.is_detected(): - continue - package = slot.module_suggestion - if package: - unclaimed_sources.append( - UnclaimedSource(source_name=slot.canonical_name, package=package) - ) - - return tuple(unclaimed_sources) diff --git a/packages/smithy-aws-core/src/smithy_aws_core/aio/identity/chain/provider.py b/packages/smithy-aws-core/src/smithy_aws_core/aio/identity/chain/provider.py index c8e8b8514..dc1ff3685 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/aio/identity/chain/provider.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/aio/identity/chain/provider.py @@ -12,6 +12,10 @@ from ....config.merged_config import MergedConfig from ....identity.chain.ordering import OrderingConstraint +from ....identity.chain.provider import ( + _ChainSetupBase, # pyright: ignore[reportPrivateUsage] + _NamedResolverBase, # pyright: ignore[reportPrivateUsage] +) __all__ = [ "ChainIdentityProvider", @@ -21,10 +25,9 @@ @dataclass(frozen=True, kw_only=True) -class NamedResolver: +class NamedResolver(_NamedResolverBase): """Associates an identity resolver with its provider's metadata.""" - provider_name: str resolver: AsyncIdentityResolver[Any, Any] async def get_identity(self, *, properties: Mapping[str, Any]) -> Any: @@ -36,9 +39,11 @@ async def invalidate(self) -> None: await self.resolver.invalidate() -class ChainSetup: +class ChainSetup(_ChainSetupBase): """Tracks shared state and resolvers during chain setup.""" + _NAMED_RESOLVER = NamedResolver + def __init__( self, *, @@ -48,85 +53,31 @@ def __init__( http_client: AsyncHTTPClient | None = None, properties: MutableMapping[str, Any] | None = None, ) -> None: - self._config_file = config_file - self._profile_name = profile_name - self._region_override = region_override - self._http_client = http_client - self._properties: MutableMapping[str, Any] = ( - {} if properties is None else properties + super().__init__( + config_file=config_file, + profile_name=profile_name, + region_override=region_override, + http_client=http_client, + properties=properties, ) - self._resolvers: list[NamedResolver] = [] - self._current_provider: ChainIdentityProvider | None = None - self._terminal = False - - @property - def config_file(self) -> MergedConfig | None: - """Return the parsed config/credentials file, if loaded.""" - return self._config_file - - @property - def profile_name(self) -> str | None: - """Return the profile name, if selected.""" - return self._profile_name - - @property - def region_override(self) -> str | None: - """Return the region override, if provided.""" - return self._region_override @property def http_client(self) -> AsyncHTTPClient | None: """Return the HTTP client to use for network calls, if provided.""" return self._http_client - @property - def properties(self) -> MutableMapping[str, Any]: - """Return the property bag shared by custom providers.""" - return self._properties - @property def resolvers(self) -> tuple[NamedResolver, ...]: """Return named resolvers in the order they were added.""" return tuple(self._resolvers) - @property - def terminal(self) -> bool: - """Return whether a provider added a terminal resolver.""" - return self._terminal - - def set_current_provider(self, provider: ChainIdentityProvider) -> None: - """Set the provider whose setup method is currently running.""" - if self._terminal: - raise RuntimeError("Cannot change provider after a terminal resolver.") - self._current_provider = provider - - def set_config_file(self, config_file: MergedConfig) -> None: - """Set the parsed config file without overwriting an existing value.""" - if self._config_file is not None: - raise RuntimeError("Cannot overwrite a config file already present.") - self._config_file = config_file - - def set_profile_name(self, profile_name: str) -> None: - """Set the resolved name of the active profile.""" - self._profile_name = profile_name - def add_resolver(self, resolver: AsyncIdentityResolver[Any, Any]) -> None: """Add a named resolver and continue assembly.""" - if self._terminal: - raise RuntimeError("Cannot add a resolver after a terminal resolver.") - if self._current_provider is None: - raise RuntimeError("Cannot add a resolver without a current provider.") - self._resolvers.append( - NamedResolver( - provider_name=self._current_provider.name, - resolver=resolver, - ) - ) + self._add(resolver) def add_terminal_resolver(self, resolver: AsyncIdentityResolver[Any, Any]) -> None: """Add a named resolver and stop assembly.""" - self.add_resolver(resolver) - self._terminal = True + self._add_terminal(resolver) class ChainIdentityProvider(Protocol): diff --git a/packages/smithy-aws-core/src/smithy_aws_core/aio/identity/chain/providers/environment.py b/packages/smithy-aws-core/src/smithy_aws_core/aio/identity/chain/providers/environment.py index 02a1f671e..146cf4f95 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/aio/identity/chain/providers/environment.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/aio/identity/chain/providers/environment.py @@ -1,32 +1,21 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -import os - from smithy_core.interfaces.identity import Identity -from .....identity.chain.ordering import Standard, StandardProvider -from .....identity.components import AWSCredentialsIdentity +from .....identity.chain.providers.environment import ( + _EnvironmentCredentialsProviderBase, # pyright: ignore[reportPrivateUsage] +) from ...environment import AsyncEnvironmentCredentialsResolver from ..provider import ChainSetup __all__ = ["AsyncEnvironmentCredentialsProvider"] -_ACCESS_KEY_ID = "AWS_ACCESS_KEY_ID" -_SECRET_ACCESS_KEY = "AWS_SECRET_ACCESS_KEY" # noqa: S105 - -class AsyncEnvironmentCredentialsProvider: +class AsyncEnvironmentCredentialsProvider(_EnvironmentCredentialsProviderBase): """Adds an environment resolver unless an explicit profile is selected.""" - @property - def name(self) -> str: - """Return the canonical provider name.""" - return StandardProvider.ENVIRONMENT.canonical_name - - @property - def ordering(self) -> Standard: - """Return the provider's standard chain position.""" - return Standard(slot=StandardProvider.ENVIRONMENT) + def _new_resolver(self) -> AsyncEnvironmentCredentialsResolver: + return AsyncEnvironmentCredentialsResolver() async def setup( self, @@ -34,13 +23,4 @@ async def setup( setup: ChainSetup, ) -> None: """Add an environment resolver when env credentials are configured.""" - if identity_type is not AWSCredentialsIdentity: - return - - if setup.profile_name is not None: - return - - if not os.getenv(_ACCESS_KEY_ID) or not os.getenv(_SECRET_ACCESS_KEY): - return - - setup.add_terminal_resolver(AsyncEnvironmentCredentialsResolver()) + self._setup(identity_type, setup) diff --git a/packages/smithy-aws-core/src/smithy_aws_core/aio/identity/chain/providers/process.py b/packages/smithy-aws-core/src/smithy_aws_core/aio/identity/chain/providers/process.py index bf2b9c036..2131bc1f8 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/aio/identity/chain/providers/process.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/aio/identity/chain/providers/process.py @@ -2,50 +2,21 @@ # SPDX-License-Identifier: Apache-2.0 from smithy_core.interfaces.identity import Identity -from .....identity.chain.ordering import Standard, StandardProvider from .....identity.chain.providers.process import ( - _split_process_command, # type: ignore[reportPrivateUsage] + _ProfileCredentialProcessProviderBase, # pyright: ignore[reportPrivateUsage] ) -from .....identity.components import AWSCredentialsIdentity from ....identity.process import AsyncProcessCredentialsResolver from ..provider import ChainSetup -_CREDENTIAL_PROCESS = "credential_process" -_ACCOUNT_ID = "aws_account_id" - -class AsyncProfileCredentialProcessProvider: +class AsyncProfileCredentialProcessProvider(_ProfileCredentialProcessProviderBase): """Adds a process credential resolver configured by the active profile.""" - @property - def name(self) -> str: - """Return the canonical provider name.""" - return StandardProvider.PROFILE_CREDENTIAL_PROCESS.canonical_name - - @property - def ordering(self) -> Standard: - """Return the provider's standard chain position.""" - return Standard(slot=StandardProvider.PROFILE_CREDENTIAL_PROCESS) + def _new_resolver( + self, command: list[str], account_id: str | None + ) -> AsyncProcessCredentialsResolver: + return AsyncProcessCredentialsResolver(command, account_id=account_id) async def setup(self, identity_type: type[Identity], setup: ChainSetup) -> None: """Add a resolver when the active profile configures a credential process.""" - if identity_type is not AWSCredentialsIdentity: - return - - config_file = setup.config_file - profile_name = setup.profile_name - if config_file is None or profile_name is None: - return - - command = config_file.get(profile_name, _CREDENTIAL_PROCESS) - if not command: - return - - # The process output's AccountId takes precedence; the profile's - # aws_account_id is only used as a fallback. - setup.add_terminal_resolver( - AsyncProcessCredentialsResolver( - _split_process_command(command), - account_id=config_file.get(profile_name, _ACCOUNT_ID), - ) - ) + self._setup(identity_type, setup) diff --git a/packages/smithy-aws-core/src/smithy_aws_core/aio/identity/chain/providers/profile.py b/packages/smithy-aws-core/src/smithy_aws_core/aio/identity/chain/providers/profile.py index 22f7901d5..55e4b0811 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/aio/identity/chain/providers/profile.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/aio/identity/chain/providers/profile.py @@ -2,7 +2,10 @@ # SPDX-License-Identifier: Apache-2.0 from smithy_core.interfaces.identity import Identity -from .....identity.chain.ordering import Standard, StandardProvider +from .....identity.chain.providers.profile import ( + _ProfileSessionCredentialsProviderBase, # pyright: ignore[reportPrivateUsage] + _ProfileStaticCredentialsProviderBase, # pyright: ignore[reportPrivateUsage] +) from .....identity.components import AWSCredentialsIdentity from ...static import AsyncStaticCredentialsResolver from ..provider import ChainSetup @@ -12,88 +15,28 @@ "AsyncProfileStaticCredentialsProvider", ] -_ACCESS_KEY_ID = "aws_access_key_id" -_SECRET_ACCESS_KEY = "aws_secret_access_key" # noqa: S105 -_SESSION_TOKEN = "aws_session_token" # noqa: S105 -_ACCOUNT_ID = "aws_account_id" -_ROLE_ARN = "role_arn" - -class AsyncProfileSessionCredentialsProvider: +class AsyncProfileSessionCredentialsProvider(_ProfileSessionCredentialsProviderBase): """Adds a resolver for session credentials from the active profile.""" - @property - def name(self) -> str: - """Return the canonical provider name.""" - return StandardProvider.PROFILE_SESSION_KEYS.canonical_name - - @property - def ordering(self) -> Standard: - """Return the provider's standard chain position.""" - return Standard(slot=StandardProvider.PROFILE_SESSION_KEYS) + def _new_resolver( + self, identity: AWSCredentialsIdentity + ) -> AsyncStaticCredentialsResolver: + return AsyncStaticCredentialsResolver(identity) async def setup(self, identity_type: type[Identity], setup: ChainSetup) -> None: """Add a resolver for complete session credentials from the active profile.""" - if identity_type is not AWSCredentialsIdentity: - return - - config_file = setup.config_file - profile_name = setup.profile_name - if config_file is None or profile_name is None: - return - - if config_file.get(profile_name, _ROLE_ARN) is not None: - return - - access_key_id = config_file.get(profile_name, _ACCESS_KEY_ID) - secret_access_key = config_file.get(profile_name, _SECRET_ACCESS_KEY) - session_token = config_file.get(profile_name, _SESSION_TOKEN) - if access_key_id is None or secret_access_key is None or session_token is None: - return - - identity = AWSCredentialsIdentity( - access_key_id=access_key_id, - secret_access_key=secret_access_key, - session_token=session_token, - account_id=config_file.get(profile_name, _ACCOUNT_ID), - ) - setup.add_terminal_resolver(AsyncStaticCredentialsResolver(identity)) + self._setup(identity_type, setup) -class AsyncProfileStaticCredentialsProvider: +class AsyncProfileStaticCredentialsProvider(_ProfileStaticCredentialsProviderBase): """Adds a resolver for static credentials from the active profile.""" - @property - def name(self) -> str: - """Return the canonical provider name.""" - return StandardProvider.PROFILE_STATIC_KEYS.canonical_name - - @property - def ordering(self) -> Standard: - """Return the provider's standard chain position.""" - return Standard(slot=StandardProvider.PROFILE_STATIC_KEYS) + def _new_resolver( + self, identity: AWSCredentialsIdentity + ) -> AsyncStaticCredentialsResolver: + return AsyncStaticCredentialsResolver(identity) async def setup(self, identity_type: type[Identity], setup: ChainSetup) -> None: """Add a resolver for complete static credentials from the active profile.""" - if identity_type is not AWSCredentialsIdentity: - return - - config_file = setup.config_file - profile_name = setup.profile_name - if config_file is None or profile_name is None: - return - - if config_file.get(profile_name, _ROLE_ARN) is not None: - return - - access_key_id = config_file.get(profile_name, _ACCESS_KEY_ID) - secret_access_key = config_file.get(profile_name, _SECRET_ACCESS_KEY) - if access_key_id is None or secret_access_key is None: - return - - identity = AWSCredentialsIdentity( - access_key_id=access_key_id, - secret_access_key=secret_access_key, - account_id=config_file.get(profile_name, _ACCOUNT_ID), - ) - setup.add_terminal_resolver(AsyncStaticCredentialsResolver(identity)) + self._setup(identity_type, setup) diff --git a/packages/smithy-aws-core/src/smithy_aws_core/aio/identity/chain/providers/shared_config.py b/packages/smithy-aws-core/src/smithy_aws_core/aio/identity/chain/providers/shared_config.py index 0396ed928..992214013 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/aio/identity/chain/providers/shared_config.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/aio/identity/chain/providers/shared_config.py @@ -1,39 +1,25 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -import os +from typing import Any +from smithy_core.aio._program import run_program_async from smithy_core.interfaces.identity import Identity -from .....identity.chain.ordering import Standard, StandardProvider -from .....identity.components import AWSCredentialsIdentity +from .....identity.chain.providers.shared_config import ( + _SharedConfigProviderBase, # pyright: ignore[reportPrivateUsage] +) from ....config import load_config_async from ..provider import ChainSetup __all__ = ["AsyncSharedConfigProvider"] -class AsyncSharedConfigProvider: +class AsyncSharedConfigProvider(_SharedConfigProviderBase): """Loads and selects the active AWS profile for downstream providers.""" - @property - def name(self) -> str: - """Return the canonical provider name.""" - return StandardProvider.SHARED_CONFIG.canonical_name - - @property - def ordering(self) -> Standard: - """Return the provider's standard chain position.""" - return Standard(slot=StandardProvider.SHARED_CONFIG) + def _load_config(self) -> Any: + return load_config_async() async def setup(self, identity_type: type[Identity], setup: ChainSetup) -> None: """Load and select the active profile for AWS credentials.""" - if identity_type is not AWSCredentialsIdentity: - return - - config_file = setup.config_file - if config_file is None: - config_file = await load_config_async() - setup.set_config_file(config_file) - - profile_name = setup.profile_name or os.getenv("AWS_PROFILE") or "default" - setup.set_profile_name(profile_name) + await run_program_async(self._setup(identity_type, setup)) diff --git a/packages/smithy-aws-core/src/smithy_aws_core/aio/identity/process.py b/packages/smithy-aws-core/src/smithy_aws_core/aio/identity/process.py index 6d68a17c8..05522e0e6 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/aio/identity/process.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/aio/identity/process.py @@ -1,26 +1,21 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import asyncio -import json -from datetime import UTC, datetime -from typing import TypeGuard, cast from smithy_core.aio.interfaces.identity import AsyncIdentityResolver from smithy_core.exceptions import SmithyIdentityError from ...identity.components import AWSCredentialsIdentity, AWSIdentityProperties +from ...identity.process import ( + _ProcessCredentialsResolverBase, # pyright: ignore[reportPrivateUsage] +) __all__ = ["AsyncProcessCredentialsResolver"] -def _is_command_list(command: object) -> TypeGuard[list[str]]: - if not isinstance(command, list) or not command: - return False - return all(isinstance(argument, str) for argument in cast(list[object], command)) - - class AsyncProcessCredentialsResolver( - AsyncIdentityResolver[AWSCredentialsIdentity, AWSIdentityProperties] + _ProcessCredentialsResolverBase, + AsyncIdentityResolver[AWSCredentialsIdentity, AWSIdentityProperties], ): """Resolves AWS Credentials from a process. @@ -31,30 +26,11 @@ class AsyncProcessCredentialsResolver( credentials when the process output does not include an ``AccountId``. """ - def __init__( - self, - command: list[str], - *, - timeout: float | None = None, - account_id: str | None = None, - ) -> None: - if not _is_command_list(command): - raise ValueError("command must be a non-empty list of strings") - self._command = list(command) - self._timeout = timeout - self._account_id = account_id - self._credentials: AWSCredentialsIdentity | None = None - async def get_identity( self, *, properties: AWSIdentityProperties ) -> AWSCredentialsIdentity: - if self._credentials is not None: - # Long-term credentials (no expiration) should always be reused - if self._credentials.expiration is None: - return self._credentials - # Temporary credentials should be reused if not expired - if datetime.now(UTC) < self._credentials.expiration: - return self._credentials + if (cached := self._cached()) is not None: + return cached try: process = await asyncio.create_subprocess_exec( @@ -76,81 +52,6 @@ async def get_identity( except ProcessLookupError: pass await process.wait() - raise SmithyIdentityError( - f"Credential process timed out after {self._timeout} seconds" - ) from e - - if process.returncode != 0: - raise SmithyIdentityError( - f"Credential process failed with exit code {process.returncode}: " - f"{stderr.decode('utf-8', errors='replace')}" - ) - # These exceptions retain the full process output, which may contain - # credentials. Suppress chaining to avoid exposing it in tracebacks. - try: - decoded = stdout.decode("utf-8") - creds = json.loads(decoded) - except UnicodeDecodeError as e: - raise SmithyIdentityError( - "Credential process output is not valid UTF-8 " - f"at byte {e.start}: {e.reason}" - ) from None - except json.JSONDecodeError as e: - raise SmithyIdentityError( - "Credential process output is not valid JSON " - f"at line {e.lineno}, column {e.colno}: {e.msg}" - ) from None - - if not isinstance(creds, dict): - raise SmithyIdentityError( - "Credential process output must be a JSON object, " - f"got {type(creds).__name__}" - ) - creds = cast(dict[str, object], creds) + raise self._timeout_error() from e - version = creds.get("Version") - if version != 1: - raise SmithyIdentityError( - f"Unsupported version '{version}' for credential process provider, supported versions: 1" - ) - access_key_id = self._get_string_field(creds, "AccessKeyId") - secret_access_key = self._get_string_field(creds, "SecretAccessKey") - session_token = self._get_string_field(creds, "SessionToken") - expiration = self._get_string_field(creds, "Expiration") - # Prefer the process output's AccountId, falling back to the profile's - # aws_account_id when the process omits it. - account_id = self._get_string_field(creds, "AccountId") or self._account_id - - if expiration is not None: - try: - dt = datetime.fromisoformat(expiration) - except (TypeError, ValueError) as e: - raise SmithyIdentityError( - "Invalid credential process Expiration; " - f"expected an ISO 8601 string: {e}" - ) from e - expiration = dt.astimezone(UTC) if dt.tzinfo else dt.replace(tzinfo=UTC) - - if access_key_id is None or secret_access_key is None: - raise SmithyIdentityError( - "AccessKeyId and SecretAccessKey are required for process credentials" - ) - - self._credentials = AWSCredentialsIdentity( - access_key_id=access_key_id, - secret_access_key=secret_access_key, - session_token=session_token, - expiration=expiration, - account_id=account_id, - ) - return self._credentials - - @staticmethod - def _get_string_field(creds: dict[str, object], key: str) -> str | None: - value = creds.get(key) - if value is not None and not isinstance(value, str): - raise SmithyIdentityError( - f"Credential process output field '{key}' must be a string, " - f"got {type(value).__name__}" - ) - return value + return self._credentials_from_output(process.returncode, stdout, stderr) diff --git a/packages/smithy-aws-core/src/smithy_aws_core/aio/protocols.py b/packages/smithy-aws-core/src/smithy_aws_core/aio/protocols.py index cb3172b48..9f61e59fc 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/aio/protocols.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/aio/protocols.py @@ -1,13 +1,9 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 from collections.abc import Callable -from dataclasses import dataclass, replace from inspect import iscoroutinefunction -from io import BytesIO -from typing import TYPE_CHECKING, Any, ClassVar, Final -from xml.etree.ElementTree import Element +from typing import TYPE_CHECKING, Any, ClassVar -from smithy_core import URI as _URI from smithy_core.aio.interfaces import AsyncWriter from smithy_core.aio.interfaces.auth import AsyncAuthScheme from smithy_core.aio.interfaces.eventstream import ( @@ -15,74 +11,31 @@ AsyncEventReceiver, ) from smithy_core.aio.types import AsyncBytesReader -from smithy_core.codecs import Codec from smithy_core.deserializers import DeserializeableShape, ShapeDeserializer from smithy_core.documents import TypeRegistry -from smithy_core.exceptions import ( - CallError, - DiscriminatorError, - ExpectationNotMetError, - MissingDependencyError, - ModeledError, - UnsupportedStreamError, -) -from smithy_core.interfaces import ( - BytesReader, - BytesWriter, - ProtocolSettings, - TypedProperties, - URI, -) -from smithy_core.interfaces import StreamingBlob as SyncStreamingBlob -from smithy_core.prelude import DOCUMENT -from smithy_core.response import ResponseMetadata +from smithy_core.exceptions import MissingDependencyError, UnsupportedStreamError +from smithy_core.interfaces import ProtocolSettings as ProtocolSettings +from smithy_core.interfaces import TypedProperties from smithy_core.schemas import APIOperation -from smithy_core.serializers import SerializeableShape, ShapeSerializer -from smithy_core.shapes import ShapeID, ShapeType -from smithy_core.traits import HTTPTrait -from smithy_core.types import PropertyKey, TimestampFormat -from smithy_http import tuples_to_fields -from smithy_http.aio import HTTPRequest as _HTTPRequest -from smithy_http.aio.interfaces import HTTPErrorIdentifier, HTTPRequest, HTTPResponse +from smithy_core.serializers import SerializeableShape +from smithy_core.shapes import ShapeID +from smithy_http.aio.interfaces import HTTPRequest, HTTPResponse from smithy_http.aio.protocols import ( AsyncHttpBindingClientProtocol, AsyncRpcClientProtocol, ) -from smithy_http.deserializers import HTTPResponseDeserializer - -from .._private.query._xml import parse_xml_root -from .._private.query.errors import create_aws_query_error -from .._private.query.metadata import parse_aws_query_request_id -from .._private.query.serializers import Ec2QueryShapeSerializer, QueryShapeSerializer -from .._private.restxml import parse_rest_xml_error -from ..traits import ( - AwsJson1_0Trait, - AwsJson1_1Trait, - AwsQueryTrait, - Ec2QueryTrait, - RestJson1Trait, - RestXmlTrait, -) -from ..utils import ( - parse_document_discriminator, - parse_error_code, - parse_response_metadata, - parse_retry_after, -) -try: - from smithy_json import JSONCodec, JSONDocument - - _HAS_JSON = True -except ImportError: - _HAS_JSON = False # type: ignore - -try: - from smithy_xml import XMLCodec - - _HAS_XML = True -except ImportError: - _HAS_XML = False # type: ignore +from ..protocols import ( + AWSErrorIdentifier, + AWSJSONDocument, + _AWSJSONProtocolBase, # pyright: ignore[reportPrivateUsage] + _AwsQueryProtocolBase, # pyright: ignore[reportPrivateUsage] + _AWSResponseMetadataMixin, # pyright: ignore[reportPrivateUsage] + _Ec2QueryProtocolBase, # pyright: ignore[reportPrivateUsage] + _RestJsonProtocolBase, # pyright: ignore[reportPrivateUsage] + _RestXmlProtocolBase, # pyright: ignore[reportPrivateUsage] +) +from ..traits import AwsJson1_0Trait, AwsJson1_1Trait try: from smithy_aws_event_stream.aio import ( @@ -107,8 +60,6 @@ SigningConfig, ) from smithy_aws_event_stream.exceptions import MissingInitialResponse - from smithy_json import JSONCodec, JSONDocument - from smithy_xml import XMLCodec from typing_extensions import TypeForm @@ -124,20 +75,6 @@ ] -def _assert_json() -> None: - if not _HAS_JSON: - raise MissingDependencyError( - "Attempted to use JSON codec, but smithy-json is not installed." - ) - - -def _assert_xml() -> None: - if not _HAS_XML: - raise MissingDependencyError( - "Attempted to use XML codec, but smithy-xml is not installed." - ) - - def _assert_event_stream() -> None: if not _HAS_EVENT_STREAM: raise MissingDependencyError( @@ -146,62 +83,6 @@ def _assert_event_stream() -> None: ) -class AWSErrorIdentifier(HTTPErrorIdentifier): - _HEADER_KEY: Final = "x-amzn-errortype" - - def identify( - self, - *, - operation: APIOperation[Any, Any], - response: HTTPResponse, - ) -> ShapeID | None: - if self._HEADER_KEY not in response.fields: - return None - - error_field = response.fields[self._HEADER_KEY] - code = error_field.values[0] if len(error_field.values) > 0 else None - if code is not None: - return parse_error_code(code, operation.schema.id.namespace) - return None - - -if TYPE_CHECKING or _HAS_JSON: - - class AWSJSONDocument(JSONDocument): - @property - def discriminator(self) -> ShapeID: - if self.shape_type is ShapeType.STRUCTURE: - return self._schema.id - parsed = parse_document_discriminator( - self, self._settings.default_namespace - ) - if parsed is None: - raise DiscriminatorError( - f"Unable to parse discriminator for {self.shape_type} document." - ) - return parsed -else: - - class AWSJSONDocument: # type: ignore[no-redef] - pass - - -class _AWSResponseMetadataMixin: - """Adds AWS request identifiers to extracted response metadata. - - Mixed in ahead of the HTTP base class, which supplies only the status code. - AWS protocols built on different HTTP bases each include it. - """ - - def extract_response_metadata( - self, - *, - response: HTTPResponse, - context: TypedProperties, - ) -> ResponseMetadata: - return parse_response_metadata(response) - - class _AWSHttpBindingClientProtocol( _AWSResponseMetadataMixin, AsyncHttpBindingClientProtocol ): @@ -277,260 +158,15 @@ def create_event_receiver[ ) -class AsyncRestJsonClientProtocol(_AWSHttpBindingClientProtocol): +class AsyncRestJsonClientProtocol(_RestJsonProtocolBase, _AWSHttpBindingClientProtocol): """An implementation of the aws.protocols#restJson1 protocol.""" - _id: Final = RestJson1Trait.id - _content_type: Final = "application/json" - _error_identifier: Final = AWSErrorIdentifier() - - def __init__(self, settings: ProtocolSettings) -> None: - _assert_json() - self._codec: Final = JSONCodec( - document_class=AWSJSONDocument, - default_namespace=settings.namespace, - default_timestamp_format=TimestampFormat.EPOCH_SECONDS, - ) - - @property - def id(self) -> ShapeID: - return self._id - - @property - def payload_codec(self) -> Codec: - return self._codec - - @property - def content_type(self) -> str: - return self._content_type - - @property - def error_identifier(self) -> HTTPErrorIdentifier: - return self._error_identifier - - def _retry_after(self, response: HTTPResponse) -> float | None: - return parse_retry_after(response) - - def _resolve_error_id( - self, - *, - operation: APIOperation[Any, Any], - error_id: ShapeID, - ) -> ShapeID: - for error_schema in operation.error_schemas: - if error_schema.id.name == error_id.name: - return error_schema.id - return error_id - -class AsyncRestXmlClientProtocol(_AWSHttpBindingClientProtocol): +class AsyncRestXmlClientProtocol(_RestXmlProtocolBase, _AWSHttpBindingClientProtocol): """An implementation of the aws.protocols#restXml protocol.""" - _id: Final = RestXmlTrait.id - _content_type: Final = "application/xml" - - def __init__(self, settings: ProtocolSettings) -> None: - _assert_xml() - self._namespace: Final = settings.namespace - self._codec: Final = XMLCodec(default_namespace=settings.xml_namespace) - - @property - def id(self) -> ShapeID: - return self._id - - @property - def payload_codec(self) -> Codec: - return self._codec - - @property - def content_type(self) -> str: - return self._content_type - - async def _create_error( - self, - operation: APIOperation[Any, Any], - request: HTTPRequest, - response: HTTPResponse, - response_body: SyncStreamingBlob, - error_registry: TypeRegistry, - context: TypedProperties, - ) -> CallError: - if isinstance(response_body, bytes | bytearray): - body = bytes(response_body) - else: - body = response_body.read() - - retry_after = parse_retry_after(response) - root = parse_xml_root(body) - error_info = parse_rest_xml_error(root) - code = error_info.code - error_id = None - if code is not None: - error_id = ShapeID.from_parts(namespace=self._namespace, name=code) - for error_schema in operation.error_schemas: - if error_schema.id.name == code: - error_id = error_schema.id - break - - if error_id is not None and error_id in error_registry and root is not None: - error_shape = error_registry.get(error_id) - if not issubclass(error_shape, ModeledError): - raise ExpectationNotMetError( - "Modeled errors must be derived from 'ModeledError', " - f"but got {error_shape}" - ) - - deserializer = HTTPResponseDeserializer( - payload_codec=_XMLErrorCodec( - self._codec, - error_info.wrapper_elements, - root, - ), - http_trait=operation.schema.expect_trait(HTTPTrait), - response=response, - body=body, - ) - modeled_error = error_shape.deserialize(deserializer) - if not modeled_error.message and error_info.message: - modeled_error.message = error_info.message - # The exception's args were taken from the empty message when it - # was constructed, so they're replaced for the message to display. - modeled_error.args = (error_info.message,) - if retry_after is not None: - modeled_error.retry_after = retry_after - return modeled_error - - message = ( - f"Unknown error for operation {operation.schema.id} " - f"- status: {response.status}" - ) - if code is not None: - message += f" - code: {code}" - if error_info.message: - message += f" - message: {error_info.message}" - if response.reason is not None: - message += f" - reason: {response.reason}" - - is_timeout = response.status == 408 - is_throttle = response.status == 429 - return CallError( - message=message, - fault="client" if response.status < 500 else "server", - is_throttling_error=is_throttle, - is_timeout_error=is_timeout, - is_retry_safe=is_throttle or is_timeout or None, - retry_after=retry_after, - ) - - -@dataclass(frozen=True) -class _XMLErrorCodec(Codec): - """Reads an error's members from inside its restXml wrapper elements.""" - - codec: "XMLCodec" - wrapper_elements: tuple[str, ...] - root: Element - - @property - def media_type(self) -> str: - return self.codec.media_type - - def create_serializer(self, sink: BytesWriter) -> ShapeSerializer: - return self.codec.create_serializer(sink) - - def create_deserializer(self, source: bytes | BytesReader) -> ShapeDeserializer: - return self.codec.create_deserializer_from_element( - self.root, wrapper_elements=self.wrapper_elements - ) - - -class _AWSJSONClientProtocol(_AWSResponseMetadataMixin, AsyncRpcClientProtocol): - _error_identifier: Final = AWSErrorIdentifier() - - _id: ClassVar[ShapeID] - _content_type: ClassVar[str] - - def __init__(self, settings: ProtocolSettings) -> None: - _assert_json() - self._service_name: Final = settings.service_target - self._event_stream_http = self._resolve_event_stream_http( - settings.service_schema, self._id - ) - self._codec: Final = JSONCodec( - document_class=AWSJSONDocument, - default_namespace=settings.namespace, - default_timestamp_format=TimestampFormat.EPOCH_SECONDS, - use_json_name=False, - ) - - @property - def id(self) -> ShapeID: - return self._id - - @property - def payload_codec(self) -> Codec: - return self._codec - - @property - def content_type(self) -> str: - return self._content_type - - @property - def error_identifier(self) -> HTTPErrorIdentifier: - return self._error_identifier - - def serialize_request[ - OperationInput: SerializeableShape, - OperationOutput: DeserializeableShape, - ]( - self, - *, - operation: APIOperation[OperationInput, OperationOutput], - input: OperationInput, - endpoint: URI, - context: TypedProperties, - ) -> HTTPRequest: - # An event-stream operation must negotiate HTTP/2 (see _requires_http2); the - # transport does so per operation, leaving non-streaming calls on HTTP/1.1. - http2_required = self._requires_http2(operation) - - if self._is_event_stream_member(operation.input_stream_member): - # RPC event streams carry the input as an initial-request event at the head - # of the stream body; the envelope keeps x-amz-target as a normal request. - return _HTTPRequest( - method="POST", - destination=_URI(host="", path="/"), - fields=tuples_to_fields( - [ - ("content-type", "application/vnd.amazon.eventstream"), - ( - "x-amz-target", - f"{self._service_name}.{operation.schema.id.name}", - ), - ] - ), - body=self._build_initial_request_body(input, context), - http2_required=http2_required, - ) - - payload = self.payload_codec.serialize(shape=input) - return _HTTPRequest( - method="POST", - destination=_URI(host="", path="/"), - fields=tuples_to_fields( - [ - ("content-type", self.content_type), - ("content-length", str(len(payload))), - ( - "x-amz-target", - f"{self._service_name}.{operation.schema.id.name}", - ), - ] - ), - body=AsyncBytesReader(payload), - http2_required=http2_required, - ) +class _AWSJSONClientProtocol(_AWSJSONProtocolBase, AsyncRpcClientProtocol): async def deserialize_response[ OperationInput: SerializeableShape, OperationOutput: DeserializeableShape, @@ -551,20 +187,13 @@ async def deserialize_response[ operation=operation, response=response, context=context ) - body = await response.consume_body_async() - - if not self._is_success(operation, context, response): - raise await self._create_error( - operation=operation, - response=response, - response_body=body, - error_registry=error_registry, - context=context, - ) - - if len(body) == 0: - body = b"{}" - return self.payload_codec.deserialize(source=body, shape=operation.output) + return self._deserialize_body( + operation=operation, + response=response, + body=await response.consume_body_async(), + error_registry=error_registry, + context=context, + ) def _default_output[ OperationInput: SerializeableShape, @@ -578,103 +207,6 @@ def _default_output[ raise MissingInitialResponse() return self.payload_codec.deserialize(source=b"{}", shape=operation.output) - def _is_success( - self, - operation: APIOperation[Any, Any], - context: TypedProperties, - response: HTTPResponse, - ) -> bool: - return 200 <= response.status < 300 - - async def _create_error( - self, - *, - operation: APIOperation[Any, Any], - response: HTTPResponse, - response_body: bytes, - error_registry: TypeRegistry, - context: TypedProperties, - ) -> CallError: - error_id = self.error_identifier.identify( - operation=operation, response=response - ) - if error_id is not None and error_id not in error_registry: - error_id = self._resolve_error_id(operation=operation, error_id=error_id) - - retry_after = parse_retry_after(response) - - if ( - error_id is None - and len(response_body) > 0 - and self._matches_content_type(response) - ): - deserializer = self.payload_codec.create_deserializer(response_body) - document = deserializer.read_document(schema=DOCUMENT) - document_error_id = document.discriminator - if document_error_id not in error_registry: - document_error_id = self._resolve_error_id( - operation=operation, error_id=document_error_id - ) - - if document_error_id in error_registry: - error_id = document_error_id - - if error_id is not None and error_id in error_registry: - error_shape = error_registry.get(error_id) - - # make sure the error shape is derived from modeled exception - if not issubclass(error_shape, ModeledError): - raise ExpectationNotMetError( - f"Modeled errors must be derived from 'ModeledError', " - f"but got {error_shape}" - ) - - body = response_body if len(response_body) > 0 else b"{}" - deserializer = self.payload_codec.create_deserializer(body) - modeled_error = error_shape.deserialize(deserializer) - if retry_after is not None: - modeled_error.retry_after = retry_after - return modeled_error - - message = ( - f"Unknown error for operation {operation.schema.id} " - f"- status: {response.status}" - ) - if error_id is not None: - message += f" - id: {error_id}" - if response.reason is not None: - message += f" - reason: {response.reason}" - - is_timeout = response.status == 408 - is_throttle = response.status == 429 - fault = "client" if response.status < 500 else "server" - - return CallError( - message=message, - fault=fault, - is_throttling_error=is_throttle, - is_timeout_error=is_timeout, - is_retry_safe=is_throttle or is_timeout or None, - retry_after=retry_after, - ) - - def _matches_content_type(self, response: HTTPResponse) -> bool: - if "content-type" not in response.fields: - return False - actual = response.fields["content-type"].as_string() - return actual.split(";", 1)[0].strip().lower() == self.content_type.lower() - - def _resolve_error_id( - self, - *, - operation: APIOperation[Any, Any], - error_id: ShapeID, - ) -> ShapeID: - for error_schema in operation.error_schemas: - if error_schema.id.name == error_id.name: - return error_schema.id - return error_id - class AsyncAwsJson10ClientProtocol(_AWSJSONClientProtocol): """An implementation of the aws.protocols#awsJson1_0 protocol.""" @@ -690,120 +222,9 @@ class AsyncAwsJson11ClientProtocol(_AWSJSONClientProtocol): _content_type: ClassVar[str] = "application/x-amz-json-1.1" -@dataclass(frozen=True) -class _RecordedQueryRequestId: - """A body-sourced request ID bound to the response it was parsed from. - - Retry attempts share one properties object, so the ID is tagged with its - source response. ``extract_response_metadata`` only trusts it for that exact - response, so a later attempt that fails before recording its own ID (for - example, when a pre-deserialization hook raises) cannot surface an earlier - attempt's ID. - """ - - response: HTTPResponse - value: str - - -_QUERY_REQUEST_ID = PropertyKey( - key="aws_query_request_id", value_type=_RecordedQueryRequestId -) -"""Where :py:class:`AsyncAwsQueryClientProtocol` records a body-sourced request ID. - -The body is only available while deserializing, so the value is stored there for -``extract_response_metadata`` to read back. -""" - - -class AsyncAwsQueryClientProtocol(_AWSResponseMetadataMixin, AsyncRpcClientProtocol): +class AsyncAwsQueryClientProtocol(_AwsQueryProtocolBase, AsyncRpcClientProtocol): """An implementation of the aws.protocols#awsQuery protocol.""" - _id: ClassVar[ShapeID] = AwsQueryTrait.id - _content_type: Final = "application/x-www-form-urlencoded" - - def __init__(self, settings: ProtocolSettings) -> None: - _assert_xml() - if settings.version is None: - raise ExpectationNotMetError( - f"The {self._id.name} protocol requires a service version, but " - "ProtocolSettings.version was None." - ) - self._default_namespace: Final = settings.namespace - self._version: Final = settings.version - self._codec: Final = XMLCodec() - - @property - def id(self) -> ShapeID: - return self._id - - @property - def payload_codec(self) -> "XMLCodec": - return self._codec - - @property - def content_type(self) -> str: - return self._content_type - - def extract_response_metadata( - self, - *, - response: HTTPResponse, - context: TypedProperties, - ) -> ResponseMetadata: - """Report the request ID, using the one recorded from the body as a fallback. - - awsQuery normally carries the identifier in the body rather than a header, so - the mixin's header lookup usually finds nothing and the body value recorded - during deserialization is used. A header still wins when a service sends one. - """ - metadata = _AWSResponseMetadataMixin.extract_response_metadata( - self, response=response, context=context - ) - if metadata.request_id is not None: - return metadata - # Relies on transport_response identity being preserved from - # deserialize_response through to here; if that ever changes, this safely - # reports no ID rather than a wrong one. - recorded = context.get(_QUERY_REQUEST_ID) - if recorded is None or recorded.response is not response: - return metadata - return replace(metadata, request_id=recorded.value) - - def serialize_request[ - OperationInput: SerializeableShape, - OperationOutput: DeserializeableShape, - ]( - self, - *, - operation: APIOperation[OperationInput, OperationOutput], - input: OperationInput, - endpoint: URI, - context: TypedProperties, - ) -> HTTPRequest: - sink = BytesIO() - params: list[tuple[str, str]] = [] - serializer = self._create_serializer( - sink=sink, - action=self._action_name(operation), - params=params, - ) - input.serialize(serializer) - serializer.flush() - content_length = sink.tell() - sink.seek(0) - body = AsyncBytesReader(sink) - return _HTTPRequest( - method="POST", - destination=_URI(host="", path="/"), - fields=tuples_to_fields( - [ - ("content-type", self.content_type), - ("content-length", str(content_length)), - ] - ), - body=body, - ) - async def deserialize_response[ OperationInput: SerializeableShape, OperationOutput: DeserializeableShape, @@ -816,134 +237,19 @@ async def deserialize_response[ error_registry: TypeRegistry, context: TypedProperties, ) -> OperationOutput: - body = await response.consume_body_async() - root = parse_xml_root(body) - - # Recorded before any branch below returns or raises, so successes, empty - # outputs and errors alike can report the identifier. Bound to this - # response so extraction never attributes it to a different attempt. - request_id = parse_aws_query_request_id(root) - if request_id is not None: - context[_QUERY_REQUEST_ID] = _RecordedQueryRequestId(response, request_id) - - if not self._is_success(operation, context, response): - raise await self._create_error( - operation=operation, - response=response, - error_registry=error_registry, - context=context, - root=root, - ) - - if len(body) == 0: - return operation.output.deserialize( - HTTPResponseDeserializer( - payload_codec=self.payload_codec, - response=response, - body=body, - ) - ) - - wrapper_elements = self._response_wrapper_elements(operation) - deserializer = ( - self.payload_codec.create_deserializer( - body, wrapper_elements=wrapper_elements - ) - if root is None - else self.payload_codec.create_deserializer_from_element( - root, wrapper_elements=wrapper_elements - ) - ) - return operation.output.deserialize(deserializer) - - def _is_success( - self, - operation: APIOperation[Any, Any], - context: TypedProperties, - response: HTTPResponse, - ) -> bool: - return 200 <= response.status < 300 - - async def _create_error( - self, - *, - operation: APIOperation[Any, Any], - response: HTTPResponse, - error_registry: TypeRegistry, - context: TypedProperties, - root: Element | None, - ) -> CallError: - return create_aws_query_error( - body=root, + return self._deserialize_body( operation=operation, + response=response, + body=await response.consume_body_async(), error_registry=error_registry, - default_namespace=self._default_namespace, - wrapper_elements=self._error_wrapper_elements(), - status=response.status, context=context, - retry_after=parse_retry_after(response), - ) - - def _create_serializer( - self, - *, - sink: BytesIO, - action: str, - params: list[tuple[str, str]], - ) -> QueryShapeSerializer: - return QueryShapeSerializer( - sink=sink, action=action, version=self._version, params=params - ) - - def _action_name[I: SerializeableShape, O: DeserializeableShape]( - self, - operation: APIOperation[I, O], - ) -> str: - return operation.schema.id.name - - def _response_wrapper_elements[I: SerializeableShape, O: DeserializeableShape]( - self, - operation: APIOperation[I, O], - ) -> tuple[str, ...]: - name = operation.schema.id.name - # Operations with no output members will omit the element. - if not operation.output_schema.members: - return (f"{name}Response",) - return ( - f"{name}Response", - f"{name}Result", ) - def _error_wrapper_elements(self) -> tuple[str, ...]: - return ("ErrorResponse", "Error") - -class AsyncEc2QueryClientProtocol(AsyncAwsQueryClientProtocol): +class AsyncEc2QueryClientProtocol(_Ec2QueryProtocolBase, AsyncAwsQueryClientProtocol): """An implementation of the aws.protocols#ec2Query protocol. An EC2-specific extension of awsQuery: input keys resolve via ``@ec2QueryName`` and lists are always flattened; responses have no ``Result`` wrapper and errors nest under ````. """ - - _id: ClassVar[ShapeID] = Ec2QueryTrait.id - - def _create_serializer( - self, - *, - sink: BytesIO, - action: str, - params: list[tuple[str, str]], - ) -> QueryShapeSerializer: - return Ec2QueryShapeSerializer( - sink=sink, action=action, version=self._version, params=params - ) - - def _response_wrapper_elements[I: SerializeableShape, O: DeserializeableShape]( - self, - operation: APIOperation[I, O], - ) -> tuple[str, ...]: - return (f"{operation.schema.id.name}Response",) - - def _error_wrapper_elements(self) -> tuple[str, ...]: - return ("Response", "Errors", "Error") diff --git a/packages/smithy-aws-core/src/smithy_aws_core/auth/sigv4.py b/packages/smithy-aws-core/src/smithy_aws_core/auth/sigv4.py index 9eb8cdb44..720d30d3c 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/auth/sigv4.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/auth/sigv4.py @@ -1,12 +1,101 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 +from typing import Any, Self +from aws_sdk_signers import SigV4Signer, SigV4SigningProperties +from smithy_core.exceptions import SmithyIdentityError +from smithy_core.interfaces import TypedProperties as _TypedProperties +from smithy_core.interfaces.auth import AuthScheme, Signer +from smithy_http.aio.interfaces import HTTPRequest -class SigV4AuthScheme: - """SigV4 auth scheme. +from ..identity import ( + AWS_IDENTITY_CONFIG, + AWSCredentialsIdentity, + AWSCredentialsResolver, + AWSIdentityProperties, +) +from ..traits import SigV4Trait - Placeholder for the synchronous auth scheme; populated alongside the sync client - implementation. - """ +__all__ = ["SigV4AuthScheme"] - # TODO(sync-api): implement alongside the sync client + +class _SigV4AuthSchemeBase: + """SigV4 auth scheme logic shared by the sync and async schemes.""" + + scheme_id = SigV4Trait.id + + def __init__(self, *, service: str) -> None: + self._service = service + + def identity_properties( + self, *, context: _TypedProperties + ) -> AWSIdentityProperties: + config = context[AWS_IDENTITY_CONFIG] + return { + "access_key_id": config.aws_access_key_id, + "secret_access_key": config.aws_secret_access_key, + "session_token": config.aws_session_token, + } + + def identity_resolver(self, *, context: _TypedProperties) -> Any: + config = context.get("config") + if config is None or config.aws_credentials_identity_resolver is None: + raise SmithyIdentityError( + "Attempted to use SigV4 auth, but aws_credentials_identity_resolver was not " + "set on the config." + ) + return config.aws_credentials_identity_resolver + + def signer_properties(self, *, context: _TypedProperties) -> SigV4SigningProperties: + config = context.get("config") + if config is None or config.region is None: + raise SmithyIdentityError( + "Attempted to use SigV4 auth, but region was not set on the config." + ) + return { + "region": config.region, + "service": self._service, + } + + @classmethod + def from_trait(cls, trait: SigV4Trait, /) -> Self: + return cls(service=trait.name) + + +class SigV4AuthScheme( + _SigV4AuthSchemeBase, + AuthScheme[ + HTTPRequest, + AWSCredentialsIdentity, + AWSIdentityProperties, + SigV4SigningProperties, + ], +): + """SigV4 AuthScheme.""" + + _signer: Signer[HTTPRequest, AWSCredentialsIdentity, SigV4SigningProperties] + + def __init__( + self, + *, + service: str, + signer: Signer[HTTPRequest, AWSCredentialsIdentity, SigV4SigningProperties] + | None = None, + ) -> None: + """Constructor. + + :param service: The name of the service to sign requests for. + :param signer: The signer used to sign the request. + """ + super().__init__(service=service) + # The signer's own request type is structurally compatible but not declared + # as an HTTPRequest protocol. + self._signer = signer or SigV4Signer() # type: ignore + + def identity_resolver(self, *, context: _TypedProperties) -> AWSCredentialsResolver: + return super().identity_resolver(context=context) + + def signer( + self, + ) -> Signer[HTTPRequest, AWSCredentialsIdentity, SigV4SigningProperties]: + return self._signer diff --git a/packages/smithy-aws-core/src/smithy_aws_core/config/__init__.py b/packages/smithy-aws-core/src/smithy_aws_core/config/__init__.py index ded30f5fe..42c77736d 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/config/__init__.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/config/__init__.py @@ -3,13 +3,15 @@ from typing import TYPE_CHECKING, Any -from .context import shared_config_files_exist +from .aws_config import AwsConfig +from .context import SharedConfigContext, load_config, shared_config_files_exist from .exceptions import ( ConfigError, ConfigParseError, ConfigValidationError, ProfileNotFoundError, ) +from .filesystem import DefaultFileSystem, FileSystem from .merged_config import MergedConfig from .types import ConfigSource @@ -23,13 +25,18 @@ "AsyncDefaultFileSystem", "AsyncFileSystem", "AsyncSharedConfigContext", + "AwsConfig", "AwsConfigOverrides", "ConfigError", "ConfigParseError", "ConfigSource", "ConfigValidationError", + "DefaultFileSystem", + "FileSystem", "MergedConfig", "ProfileNotFoundError", + "SharedConfigContext", + "load_config", "load_config_async", "shared_config_files_exist", ] diff --git a/packages/smithy-aws-core/src/smithy_aws_core/config/aws_config.py b/packages/smithy-aws-core/src/smithy_aws_core/config/aws_config.py index 8b63494ac..3a4abb95c 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/config/aws_config.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/config/aws_config.py @@ -1,16 +1,533 @@ -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: Apache-2.0 +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +from collections.abc import Mapping +from copy import deepcopy +from dataclasses import dataclass, field, fields +from typing import TYPE_CHECKING, Any, ClassVar, Self, TypedDict, Unpack, cast + +from smithy_core._program import Program, run_program +from smithy_core.retries import RetryStrategyOptions, RetryStrategyType + +if TYPE_CHECKING: + from smithy_core.interfaces import ClientTransport, URI + from smithy_core.interfaces.identity import IdentityResolver + from smithy_core.interfaces.retries import RetryStrategy + from smithy_http.interfaces import HTTPRequestConfiguration + + from smithy_aws_core.identity.components import ( + AWSCredentialsIdentity, + AWSIdentityProperties, + ) + +from .context import SharedConfigContext +from .exceptions import ConfigError, ConfigValidationError +from .filesystem import FileSystem +from .resolvers import ( + resolve_endpoint_uri, + resolve_max_attempts, + resolve_region, + resolve_retry_mode, + resolve_sdk_ua_app_id, +) +from .types import UNSET, ConfigSource, FieldSpec, Resolved +from .validators import ( + validate_max_attempts, + validate_profile, + validate_region, + validate_retry_mode, + validate_sdk_ua_app_id, +) __all__ = [ "AwsConfig", + "AwsConfigOverrides", ] +_CREDENTIAL_FIELDS = ("aws_access_key_id", "aws_secret_access_key", "aws_session_token") + + +def _base_fields( # type: ignore[reportUnusedFunction] + *, + resolve_region: Any, + resolve_retry_mode: Any, + resolve_max_attempts: Any, + resolve_endpoint_uri: Any, + resolve_sdk_ua_app_id: Any, +) -> dict[str, FieldSpec]: + """Build the field specs shared by every AWS config, given mode-specific resolvers.""" + return { + "region": FieldSpec( + default=None, + resolver=resolve_region, + validator=validate_region, + ), + "retry_mode": FieldSpec( + default=RetryStrategyOptions.retry_mode, + resolver=resolve_retry_mode, + validator=validate_retry_mode, + ), + "max_attempts": FieldSpec( + default=RetryStrategyOptions.max_attempts, + resolver=resolve_max_attempts, + validator=validate_max_attempts, + ), + "endpoint_uri": FieldSpec( + default=None, + resolver=resolve_endpoint_uri, + ), + "aws_access_key_id": FieldSpec( + default=None, + ), + "aws_secret_access_key": FieldSpec( + default=None, + ), + "aws_session_token": FieldSpec( + default=None, + ), + "aws_credentials_identity_resolver": FieldSpec( + default=None, + ), + "sdk_ua_app_id": FieldSpec( + default=None, + resolver=resolve_sdk_ua_app_id, + validator=validate_sdk_ua_app_id, + ), + "user_agent_extra": FieldSpec( + default=None, + ), + "interceptors": FieldSpec( + default_factory=list, + ), + "http_request_config": FieldSpec( + default=None, + ), + "transport": FieldSpec( + default=None, + ), + "retry_strategy": FieldSpec( + default=None, + ), + } + + +class _AwsConfigBase: + """Config resolution logic shared by the sync and async AWS configs.""" + + _FIELDS: ClassVar[dict[str, FieldSpec]] + _RESOLVE_PREFIX: ClassVar[str] = "" + + _ctx: Any + _sources: dict[str, ConfigSource] + aws_credentials_identity_resolver: Any + transport: Any + retry_strategy: Any + + def __repr__(self) -> str: + """Render the config without exposing credential material. + + Defined on the base class so that every subclass inherits the + filtering, rather than relying on each subclass to mark its own + credential fields ``repr=False``. Subclasses must be declared with + ``@dataclass(repr=False)`` so they inherit this instead of generating + their own ``__repr__``. + """ + rendered = ", ".join( + f"{f.name}={getattr(self, f.name)!r}" + for f in fields(cast(Any, self)) + if f.repr and f.name not in _CREDENTIAL_FIELDS + ) + return f"{type(self).__name__}({rendered})" + + def __init__(self, *args: object, **kwargs: object) -> None: + """Block direct construction without advertising config fields as parameters.""" + raise ConfigError( + f"{type(self).__name__} cannot be constructed directly. " + f"Use `{self._RESOLVE_PREFIX}{type(self).__name__}.resolve(...)` instead." + ) + + @classmethod + def _new_context(cls, **kwargs: Any) -> Any: + raise NotImplementedError + + def _static_credentials_resolver(self, identity: "AWSCredentialsIdentity") -> Any: + raise NotImplementedError + + @classmethod + def _resolve_program( + cls, + *, + profile: str | None, + fs: Any, + config_file_path: str | None, + credentials_file_path: str | None, + overrides: Mapping[str, object], + ) -> Program[Self]: + ctx = cls._new_context( + profile_name=profile, + fs=fs, + config_file_path=config_file_path, + credentials_file_path=credentials_file_path, + ) + + # Fail fast on a bad profile when one was provided (not the default) + profile_source = ctx.profile_source + if profile_source is not ConfigSource.DEFAULT: + config_file = yield ctx.parsed_profiles + validate_profile(ctx.profile_name, config_file.profiles, profile_source) + + # Create the instance without calling the blocked constructor + instance = cls._create_instance() + instance._ctx = ctx + + # Resolve each field + yield from instance._resolve_fields(dict(overrides)) + + return instance + + def source_of(self, field_name: str) -> ConfigSource | None: + """Get the source that provided a field's value. + + :param field_name: The config field name. + :returns: The ConfigSource, or None if not tracked. + """ + return self._sources.get(field_name) + + @classmethod + def _create_instance(cls) -> Self: + """Create an instance that bypasses construction blocking.""" + instance = object.__new__(cls) + object.__setattr__(instance, "_sources", {}) + object.__setattr__(instance, "_ctx", None) + for field_name in cls._FIELDS: + object.__setattr__(instance, field_name, UNSET) + return instance + + def _resolve_fields(self, overrides: dict[str, Any]) -> Program[None]: + """Run the resolution pipeline for all fields.""" + unknown = set(overrides) - set(self._FIELDS) + if unknown: + raise ConfigValidationError( + f"Unknown config field(s): {sorted(unknown)}. " + f"Valid fields are: {sorted(self._FIELDS)}" + ) + + # Validate credential overrides and auto-wire the identity resolver + # before the field loop, so the loop sees the resolver as an override. + self._resolve_credentials(overrides) + + for field_name, spec in self._FIELDS.items(): + # check for overrides first + if field_name in overrides: + value = overrides[field_name] + if spec.converter is not None: + value = spec.converter(value) + setattr(self, field_name, value) + self._sources[field_name] = ConfigSource.OVERRIDE + # check in resolver + elif spec.resolver is not None: + resolver = spec.resolver + result: Resolved[Any] = yield lambda: resolver(self._ctx) + if result.value is not UNSET: + setattr(self, field_name, result.value) + self._sources[field_name] = result.source + else: + # If resolver returned UNSET, fall back to default + self._apply_default(field_name, spec) + + else: + # No resolver, use default directly + self._apply_default(field_name, spec) + + # Run validator for all sources + if spec.validator is not None: + spec.validator(getattr(self, field_name)) + + def _apply_default(self, field_name: str, spec: FieldSpec) -> None: + """Apply the default value for a field.""" + if spec.default_factory is not None: + value = spec.default_factory() + else: + value = spec.default + setattr(self, field_name, value) + self._sources[field_name] = ConfigSource.DEFAULT + + def _resolve_credentials(self, overrides: dict[str, Any]) -> None: + """Validate in-code credentials and auto-wire StaticCredentialsResolver. + + Rules: + - If both aws_access_key_id and aws_secret_access_key are overridden, + auto-set aws_credentials_identity_resolver to a + StaticCredentialsResolver (unless the caller already provided one). + Only the overridden values are used, so a session token present in a + profile is not picked up here. + - If credentials are overridden but the key/secret pair is incomplete, + raise ConfigValidationError. + - If no credential is overridden, credentials are resolved from the + remaining sources. + """ + + required = {"aws_access_key_id", "aws_secret_access_key"} + + cred_overrides = {f for f in _CREDENTIAL_FIELDS if f in overrides} + + if not cred_overrides: + return + + if not required <= cred_overrides: + raise ConfigValidationError( + f"Partial credential override: {sorted(cred_overrides)}. " + "Both 'aws_access_key_id' and 'aws_secret_access_key' must be " + "provided together when overriding credentials." + ) + + # Auto-wire StaticCredentialsResolver if user didn't provide one + if overrides.get("aws_credentials_identity_resolver") is None: + # Lazy import to avoid circular dependency + from smithy_aws_core.identity.components import AWSCredentialsIdentity + + identity = AWSCredentialsIdentity( + access_key_id=overrides["aws_access_key_id"], + secret_access_key=overrides["aws_secret_access_key"], + session_token=overrides.get("aws_session_token"), + ) + overrides["aws_credentials_identity_resolver"] = ( + self._static_credentials_resolver(identity) + ) + + def __setattr__(self, name: str, value: Any) -> None: + """Guard and track config fields set after resolution. + + Rejects unknown field names, blocks credential mutation, validates the + new value, and records the field as an override so ``source_of()`` + stays accurate when plugins customize a config per request. + """ + # Reject unknown fields + if not name.startswith("_") and name not in self.__class__._FIELDS: + raise AttributeError( + f"'{type(self).__name__}' has no config field '{name}'" + ) + + # Apply the field's converter so a post-resolution assignment coerces + # the same way an override passed to resolve() does. + spec = self.__class__._FIELDS.get(name) + if spec is not None and spec.converter is not None and value is not UNSET: + value = spec.converter(value) + + # Block override for credentials after resolution + if ( + name in _CREDENTIAL_FIELDS + and hasattr(self, "_sources") + and name in self._sources + ): + raise AttributeError( + f"'{name}' cannot be modified after resolution. Pass credentials " + f"to `{self._RESOLVE_PREFIX}{type(self).__name__}.resolve(...)`, or set " + "'aws_credentials_identity_resolver' to supply credentials " + "dynamically." + ) + + # Mark as override only if the field is in _FIELDS and was already resolved + if spec is not None and hasattr(self, "_sources") and name in self._sources: + if spec.validator is not None: + spec.validator(value) + self._sources[name] = ConfigSource.OVERRIDE + super().__setattr__(name, value) -class AwsConfig: - """Base configuration class for all AWS services. + def __deepcopy__(self, memo: dict[int, Any]) -> Self: + """Deep-copy the config while sharing resources that must not be duplicated.""" + for shared in ( + self.aws_credentials_identity_resolver, + self.transport, + self.retry_strategy, + ): + if shared is not None: + memo[id(shared)] = shared + new = self._create_instance() + memo[id(self)] = new + for f in fields(cast(Any, self)): + object.__setattr__(new, f.name, deepcopy(getattr(self, f.name), memo)) + return new - Placeholder for the synchronous configuration; populated alongside the sync client - implementation. + +class AwsConfigOverrides(TypedDict, total=False): + """Common keyword overrides accepted by synchronous AWS config resolution.""" + + region: str | None + retry_mode: RetryStrategyType | None + max_attempts: int | None + endpoint_uri: "str | URI | None" + aws_access_key_id: str | None + aws_secret_access_key: str | None + aws_session_token: str | None + aws_credentials_identity_resolver: ( + "IdentityResolver[AWSCredentialsIdentity, AWSIdentityProperties] | None" + ) + sdk_ua_app_id: str | None + user_agent_extra: str | None + interceptors: list[Any] + http_request_config: "HTTPRequestConfiguration | None" + transport: "ClientTransport[Any, Any] | None" + retry_strategy: "RetryStrategy | RetryStrategyOptions | None" + + +@dataclass(kw_only=True, init=False, repr=False) +class AwsConfig(_AwsConfigBase): + """Base configuration class for all synchronous AWS service clients. + + Fields are resolved from multiple sources (env vars, config files, defaults) + through the resolve() classmethod. + + Do not instantiate directly — use: + config = AwsConfig.resolve() + """ + + region: str | None = None + """The AWS region to connect to. + """ + + retry_mode: RetryStrategyType | None = None + """The retry mode to use. Accepted overrides are ``standard`` and ``adaptive``. + + ``legacy`` is rejected when set here; when it comes from the environment or a + config file it warns and falls back to ``standard``. + """ + + max_attempts: int | None = None + """The maximum number of attempts to make per request, including the initial + attempt. Must be an integer of at least 1.""" + + endpoint_uri: "str | URI | None" = None + """A static URI to route requests to.""" + + aws_access_key_id: str | None = field(default=None, repr=False) + """The identifier for a secret access key. + + Set this together with ``aws_secret_access_key`` to supply credentials in + code. Cannot be modified after resolution; see + ``aws_credentials_identity_resolver`` to supply credentials dynamically. + """ + + aws_secret_access_key: str | None = field(default=None, repr=False) + """A secret access key that can be used to sign requests. + + Must be set together with ``aws_access_key_id``. + """ + + aws_session_token: str | None = field(default=None, repr=False) + """The session token used with temporary AWS credentials. + + Set this together with ``aws_access_key_id`` and ``aws_secret_access_key`` + when supplying temporary credentials in code. + """ + + aws_credentials_identity_resolver: "IdentityResolver[AWSCredentialsIdentity, AWSIdentityProperties] | None" = None + """Resolves AWS Credentials. + + Set automatically to a ``StaticCredentialsResolver`` when + ``aws_access_key_id`` and ``aws_secret_access_key`` are supplied in code. + """ + + sdk_ua_app_id: str | None = None + """A unique and opaque application ID that is appended to the User-Agent + header.""" + + user_agent_extra: str | None = None + """Additional suffix to be added to the User-Agent header.""" + + interceptors: list[Any] = field(default_factory=list) # type: ignore + """The list of interceptors, which are hooks that are called during the + execution of a request.""" + + http_request_config: "HTTPRequestConfiguration | None" = None + """Configuration for individual HTTP requests.""" + + transport: "ClientTransport[Any, Any] | None" = None + """The transport to use to send requests""" + + retry_strategy: Any | None = None + """The retry strategy or options for configuring retry behavior. """ - # TODO(sync-api): implement alongside the sync client + _ctx: SharedConfigContext | None = field(default=None, repr=False, compare=False) + _sources: dict[str, ConfigSource] = field( # type: ignore[assignment] + default_factory=dict, + repr=False, + compare=False, + ) + + _FIELDS: ClassVar[dict[str, FieldSpec]] = _base_fields( + resolve_region=resolve_region, + resolve_retry_mode=resolve_retry_mode, + resolve_max_attempts=resolve_max_attempts, + resolve_endpoint_uri=resolve_endpoint_uri, + resolve_sdk_ua_app_id=resolve_sdk_ua_app_id, + ) + + @classmethod + def resolve( + cls, + *, + profile: str | None = None, + fs: FileSystem | None = None, + config_file_path: str | None = None, + credentials_file_path: str | None = None, + **overrides: Unpack[AwsConfigOverrides], + ) -> Self: + """Resolve a config object from environment, config files, and defaults. + + This is the only supported way to create a config instance. + + :param profile: Override the active profile name. + :param fs: Override the filesystem abstraction. + :param config_file_path: Override path for config file. + :param credentials_file_path: Override path for credentials file. + :param overrides: Explicit field values that skip resolution. + :returns: A fully-resolved config instance. + :raises ProfileNotFoundError: If a profile is requested via ``profile`` or + the ``AWS_PROFILE`` environment variable but is not defined in the + config files. + """ + return cls._resolve( + profile=profile, + fs=fs, + config_file_path=config_file_path, + credentials_file_path=credentials_file_path, + overrides=overrides, + ) + + @classmethod + def _resolve( + cls, + *, + profile: str | None, + fs: FileSystem | None, + config_file_path: str | None, + credentials_file_path: str | None, + overrides: Mapping[str, object], + ) -> Self: + """Internal resolution entry point for generated typed config factories.""" + return run_program( + cls._resolve_program( + profile=profile, + fs=fs, + config_file_path=config_file_path, + credentials_file_path=credentials_file_path, + overrides=overrides, + ) + ) + + @classmethod + def _new_context(cls, **kwargs: Any) -> SharedConfigContext: + return SharedConfigContext(**kwargs) + + def _static_credentials_resolver(self, identity: "AWSCredentialsIdentity") -> Any: + from smithy_aws_core.identity.static import StaticCredentialsResolver + + return StaticCredentialsResolver(identity=identity) + + def resolution_context(self) -> SharedConfigContext | None: + """Get the resolution context used to create this config. + + :returns: The SharedConfigContext, or None if not available. + """ + return self._ctx diff --git a/packages/smithy-aws-core/src/smithy_aws_core/config/context.py b/packages/smithy-aws-core/src/smithy_aws_core/config/context.py index d266ec5db..24917f07b 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/config/context.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/config/context.py @@ -1,7 +1,27 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 +import logging import os from pathlib import Path +from typing import Any + +from smithy_core._program import Program, run_program + +from .file_parser import ( + FileType, + _parse_config_file, # pyright: ignore[reportPrivateUsage] + standardize, +) +from .filesystem import DefaultFileSystem, FileSystem +from .merged_config import MergedConfig +from .types import ConfigSource + +__all__ = ["SharedConfigContext", "load_config", "shared_config_files_exist"] + +logger = logging.getLogger(__name__) + +_PROFILE_ENV_VAR = "AWS_PROFILE" +_DEFAULT_PROFILE = "default" _DEFAULT_CONFIG_FILE = "~/.aws/config" _DEFAULT_CREDENTIALS_FILE = "~/.aws/credentials" @@ -58,3 +78,168 @@ def shared_config_files_exist( config_file_path, credentials_file_path ) return config_path.is_file() or credentials_path.is_file() + + +def _load_config( + config_file_path: Path | None, credentials_file_path: Path | None, fs: Any +) -> Program[MergedConfig]: + config_path, credentials_path = _resolve_config_paths( + config_file_path, credentials_file_path + ) + + raw_config = yield from _parse_config_file(str(config_path), fs) + raw_credentials = yield from _parse_config_file(str(credentials_path), fs) + + std_config = standardize(raw_config, FileType.CONFIG) + std_credentials = standardize(raw_credentials, FileType.CREDENTIALS) + + return MergedConfig(std_config, std_credentials) + + +def load_config( + config_file_path: Path | None = None, + credentials_file_path: Path | None = None, + fs: FileSystem | None = None, +) -> MergedConfig: + """Load and merge AWS config and credentials files. + + :param config_file_path: Override path for config file. + Defaults to AWS_CONFIG_FILE env var or ~/.aws/config. + :param credentials_file_path: Override path for credentials file. + Defaults to AWS_SHARED_CREDENTIALS_FILE env var or ~/.aws/credentials. + :param fs: FileSystem to use for reading files. Defaults to real disk I/O. + :returns: A MergedConfig with merged profiles from both files. + """ + return run_program( + _load_config(config_file_path, credentials_file_path, fs or DefaultFileSystem()) + ) + + +class _SharedConfigContextBase: + """Resolution context logic shared by the sync and async contexts.""" + + def __init__( + self, + *, + profile_name: str | None, + fs: Any, + http_client: Any | None, + config_file_path: str | Path | None, + credentials_file_path: str | Path | None, + ) -> None: + self._fs = fs + self._http_client: Any | None = http_client + self._profile_name, self._profile_source = self._resolve_profile_name( + profile_name + ) + self._config_file_path: Path | None = ( + Path(config_file_path) if config_file_path is not None else None + ) + self._credentials_file_path: Path | None = ( + Path(credentials_file_path) if credentials_file_path is not None else None + ) + self._cached_config_file: MergedConfig | None = None + + @property + def profile_name(self) -> str: + """The active profile name.""" + return self._profile_name + + @property + def profile_source(self) -> ConfigSource: + """The source of the active profile name.""" + return self._profile_source + + @property + def http_client(self) -> Any | None: + """HTTP client for network-based resolvers.""" + return self._http_client + + def __deepcopy__(self, memo: Any) -> Any: + """Return self rather than a copy. + + The context is read-only once resolution finishes: resolvers have + already pulled their values onto the config's fields, and nothing on + the request path reads it again. Generated clients deep-copy the + config on every operation call to keep plugin mutations scoped to + that call, which would otherwise rebuild the whole parsed profile + tree per request — work proportional to the size of the caller's + shared config files. Sharing this instead keeps that cost flat + without weakening the isolation of the fields plugins actually write. + """ + return self + + def _parsed_profiles(self) -> Program[MergedConfig]: + if self._cached_config_file is None: + self._cached_config_file = yield from _load_config( + self._config_file_path, self._credentials_file_path, self._fs + ) + return self._cached_config_file + + def _resolve_profile_name( + self, explicit_profile: str | None + ) -> tuple[str, ConfigSource]: + """Determine the active profile name and where it came from. + + Priority: explicit argument > AWS_PROFILE env var > "default" + + :returns: Tuple of (profile_name, source), where source is the + provenance of the name: ``OVERRIDE`` for the explicit argument, + ``ENV`` for ``AWS_PROFILE``, and ``DEFAULT`` for the fallback. + """ + if explicit_profile is not None: + return explicit_profile, ConfigSource.OVERRIDE + + env_profile = os.environ.get(_PROFILE_ENV_VAR) + if env_profile: + return env_profile, ConfigSource.ENV + + return _DEFAULT_PROFILE, ConfigSource.DEFAULT + + +class SharedConfigContext(_SharedConfigContextBase): + """Resolution context shared across resolvers during config construction. + + Holds environment state and cached file data that resolvers use to look up + values. Created once per resolve() call. + """ + + def __init__( + self, + *, + profile_name: str | None = None, + fs: FileSystem | None = None, + http_client: Any | None = None, + config_file_path: str | Path | None = None, + credentials_file_path: str | Path | None = None, + ) -> None: + """Initialize the resolution context. + + :param profile_name: The active profile to use. Defaults to + AWS_PROFILE env var, then "default". + :param fs: Filesystem abstraction for reading config files. + Defaults to real disk I/O. + :param http_client: HTTP client for network-based resolvers. + :param config_file_path: Override path for config file. + :param credentials_file_path: Override path for credentials file. + """ + super().__init__( + profile_name=profile_name, + fs=fs if fs is not None else DefaultFileSystem(), + http_client=http_client, + config_file_path=config_file_path, + credentials_file_path=credentials_file_path, + ) + + @property + def fs(self) -> FileSystem: + """The filesystem abstraction.""" + return self._fs + + def parsed_profiles(self) -> MergedConfig: + """Get the parsed and merged config/credentials file data. + + The result is cached after the first call so files are only read from + disk once per context. + """ + return run_program(self._parsed_profiles()) diff --git a/packages/smithy-aws-core/src/smithy_aws_core/config/file_parser.py b/packages/smithy-aws-core/src/smithy_aws_core/config/file_parser.py index 903d81de1..11f6a48d0 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/config/file_parser.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/config/file_parser.py @@ -5,15 +5,22 @@ import re from dataclasses import dataclass, field from enum import Enum +from typing import TYPE_CHECKING, Any + +from smithy_core._program import Program, run_program from .exceptions import ConfigParseError +if TYPE_CHECKING: + from .filesystem import FileSystem + __all__ = [ "FileType", "RawParsedSections", "Section", "SectionMap", "StandardizedOutput", + "parse_config_file", "standardize", ] @@ -100,6 +107,27 @@ def standardize( ) +def parse_config_file(file_path: str, filesystem: "FileSystem") -> RawParsedSections: + """Parse an AWS config or credentials file. + + :param file_path: Resolved path to the file. + :param filesystem: FileSystem object to use. + :returns: Raw sections dict {section_name: {key: value}}. + :raises ConfigParseError: If the file has invalid syntax. + """ + return run_program(_parse_config_file(file_path, filesystem)) + + +def _parse_config_file(file_path: str, filesystem: Any) -> Program[RawParsedSections]: # type: ignore[reportUnusedFunction] + content: str | None = yield lambda: filesystem.read_file(file_path) + if content is None: + return {} + try: + return _parse_content(content) + except ConfigParseError as e: + raise ConfigParseError(f"Unable to parse config file '{file_path}': {e}") from e + + def _parse_content(content: str) -> RawParsedSections: # type: ignore[reportUnusedFunction] """Parse config file content from a string into raw sections. diff --git a/packages/smithy-aws-core/src/smithy_aws_core/config/filesystem.py b/packages/smithy-aws-core/src/smithy_aws_core/config/filesystem.py new file mode 100644 index 000000000..38e40c3f2 --- /dev/null +++ b/packages/smithy-aws-core/src/smithy_aws_core/config/filesystem.py @@ -0,0 +1,47 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +import logging +from pathlib import Path +from typing import Protocol, runtime_checkable + +__all__ = ["DefaultFileSystem", "FileSystem"] + +logger = logging.getLogger(__name__) + + +@runtime_checkable +class FileSystem(Protocol): + """Protocol for filesystem operations. + + Abstraction over file I/O so tests can provide mock implementations without + touching the real filesystem. + """ + + def read_file(self, path: str) -> str | None: + """Read a file's content as UTF-8. + + :param path: Resolved file path. + :returns: File content, or None if the file is inaccessible. + """ + ... + + +class DefaultFileSystem: + """Default filesystem implementation using real disk I/O.""" + + def read_file(self, path: str) -> str | None: + """Read a file from disk. + + Missing files and permission errors return None with a warning. Encoding + errors (invalid UTF-8) are raised to the caller. + + :param path: Resolved file path. + :returns: File content, or None if the file is inaccessible. + """ + try: + return Path(path).read_text(encoding="utf-8") + except FileNotFoundError: + return None + except (PermissionError, OSError) as e: + logger.warning("Unable to read config file '%s': %s", path, e) + return None diff --git a/packages/smithy-aws-core/src/smithy_aws_core/config/resolvers.py b/packages/smithy-aws-core/src/smithy_aws_core/config/resolvers.py new file mode 100644 index 000000000..0f4849968 --- /dev/null +++ b/packages/smithy-aws-core/src/smithy_aws_core/config/resolvers.py @@ -0,0 +1,244 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +import os +import warnings +from collections.abc import Sequence +from typing import Any + +from smithy_core._program import Program, run_program + +from .context import SharedConfigContext +from .exceptions import ConfigValidationError +from .merged_config import MergedConfig +from .types import UNSET, ConfigSource, Resolved + +__all__ = [ + "EndpointUriResolver", + "resolve_endpoint_uri", + "resolve_max_attempts", + "resolve_region", + "resolve_retry_mode", + "resolve_sdk_ua_app_id", +] + + +def _resolve_str( + ctx: Any, + *, + env_vars: Sequence[str] = (), + profile_keys: Sequence[str] = (), +) -> Program[Resolved[str]]: + """Resolve a string value by checking providers in priority order. + + Priority: env vars (first match) > config file (profile keys, first match) > unresolved. + + :param ctx: The shared resolution context. + :param env_vars: Environment variable names to check, in order. + :param profile_keys: Config file profile keys to check, in order. + :returns: Resolved value with source, or Resolved(value=UNSET) if not found. + """ + # Check environment variables first + for var_name in env_vars: + value: str | None = os.environ.get(var_name) + if value: + return Resolved(value=value, source=ConfigSource.ENV) + + # Check config file profile keys + if profile_keys: + config_file: MergedConfig = yield ctx.parsed_profiles + for key in profile_keys: + value = config_file.get(ctx.profile_name, key) + if value: + return Resolved(value=value, source=ConfigSource.PROFILE) + + return Resolved(value=UNSET, source=ConfigSource.DEFAULT) # type: ignore[arg-type] + + +def _resolve_int( + ctx: Any, + *, + env_vars: Sequence[str] = (), + profile_keys: Sequence[str] = (), +) -> Program[Resolved[int | None]]: + """Resolve an integer value by checking providers in priority order. + + :returns: Resolved int value with source, or Resolved(value=UNSET) if not found. + """ + result = yield from _resolve_str(ctx, env_vars=env_vars, profile_keys=profile_keys) + if result.value is UNSET: + return Resolved(value=UNSET, source=ConfigSource.DEFAULT) # type: ignore[arg-type] + try: + return Resolved(value=int(result.value), source=result.source) + except (ValueError, TypeError) as e: + raise ConfigValidationError( + f"Invalid integer value {result.value!r} for config key. " + "Expected a valid integer." + ) from e + + +def _region(ctx: Any) -> Program[Resolved[str | None]]: # type: ignore[reportUnusedFunction] + return ( + yield from _resolve_str( + ctx, + env_vars=("AWS_REGION", "AWS_DEFAULT_REGION"), + profile_keys=("region",), + ) + ) + + +def _retry_mode(ctx: Any) -> Program[Resolved[str | None]]: # type: ignore[reportUnusedFunction] + result = yield from _resolve_str( + ctx, + env_vars=("AWS_RETRY_MODE",), + profile_keys=("retry_mode",), + ) + + if result.value == "legacy": + warnings.warn( + "'legacy' retry mode is not supported, using 'standard' instead.", + UserWarning, + stacklevel=2, + ) + return Resolved(value="standard", source=result.source) + return result + + +def _max_attempts(ctx: Any) -> Program[Resolved[int | None]]: # type: ignore[reportUnusedFunction] + return ( + yield from _resolve_int( + ctx, + env_vars=("AWS_MAX_ATTEMPTS",), + profile_keys=("max_attempts",), + ) + ) + + +def _endpoint_uri(ctx: Any) -> Program[Resolved[str | None]]: # type: ignore[reportUnusedFunction] + return ( + yield from _resolve_str( + ctx, + env_vars=("AWS_ENDPOINT_URL",), + profile_keys=("endpoint_url",), + ) + ) + + +def _sdk_ua_app_id(ctx: Any) -> Program[Resolved[str | None]]: # type: ignore[reportUnusedFunction] + return ( + yield from _resolve_str( + ctx, + env_vars=("AWS_SDK_UA_APP_ID",), + profile_keys=("sdk_ua_app_id",), + ) + ) + + +def resolve_region(ctx: SharedConfigContext) -> Resolved[str | None]: + """Resolve the AWS region from environment or config file. + + :param ctx: The shared resolution context. + :returns: Resolved region value with source. + """ + return run_program(_region(ctx)) + + +def resolve_retry_mode(ctx: SharedConfigContext) -> Resolved[str | None]: + """Resolve the AWS retry mode from environment or config file. + + :param ctx: The shared resolution context. + :returns: Resolved retry mode value with source. + """ + return run_program(_retry_mode(ctx)) + + +def resolve_max_attempts(ctx: SharedConfigContext) -> Resolved[int | None]: + """Resolve the maximum number of retry attempts from environment or config file. + + :param ctx: The shared resolution context. + :returns: Resolved max attempts value with source. + """ + return run_program(_max_attempts(ctx)) + + +def resolve_endpoint_uri(ctx: SharedConfigContext) -> Resolved[str | None]: + """Resolve the endpoint URI from global environment or config file. + + For service-specific resolution, use EndpointUriResolver(). + + :param ctx: The shared resolution context. + :returns: Resolved endpoint URI value with source. + """ + return run_program(_endpoint_uri(ctx)) + + +def resolve_sdk_ua_app_id(ctx: SharedConfigContext) -> Resolved[str | None]: + """Resolve the SDK user-agent app ID from environment or config file. + + :param ctx: The shared resolution context. + :returns: Resolved app ID value with source. + """ + return run_program(_sdk_ua_app_id(ctx)) + + +class _EndpointUriResolverBase: + """Service-aware endpoint URI resolution shared by the sync and async resolvers. + + Resolution order (first match wins): + 1. Service-specific env var (AWS_ENDPOINT_URL_) + 2. Global env var (AWS_ENDPOINT_URL) + 3. Service-specific config file (services section -> service_id -> endpoint_url) + 4. Global config file (profile -> endpoint_url) + """ + + def __init__(self, service_id: str): + """Initialize with a service identifier. + + :param service_id: The service identifier (e.g., "bedrock_runtime"). + Used to construct the service-specific env var and config lookup key. + """ + self._service_env_var = ( + f"AWS_ENDPOINT_URL_{service_id.replace(' ', '_').replace('-', '_').upper()}" + ) + + self._service_key = service_id.replace(" ", "_").replace("-", "_").lower() + + def _resolve(self, ctx: Any) -> Program[Resolved[str | None]]: + value = os.environ.get(self._service_env_var) + if value: + return Resolved(value=value, source=ConfigSource.ENV) + + value = os.environ.get("AWS_ENDPOINT_URL") + if value: + return Resolved(value=value, source=ConfigSource.ENV) + + config_file: MergedConfig = yield ctx.parsed_profiles + value = config_file.get_service_config( + ctx.profile_name, self._service_key, "endpoint_url" + ) + if value: + return Resolved(value=value, source=ConfigSource.PROFILE) + + value = config_file.get(ctx.profile_name, "endpoint_url") + if value: + return Resolved(value=value, source=ConfigSource.PROFILE) + + return Resolved(value=UNSET, source=ConfigSource.DEFAULT) # type: ignore[arg-type] + + +class EndpointUriResolver(_EndpointUriResolverBase): + """Service-aware endpoint URI resolver. + + Resolution order (first match wins): + 1. Service-specific env var (AWS_ENDPOINT_URL_) + 2. Global env var (AWS_ENDPOINT_URL) + 3. Service-specific config file (services section -> service_id -> endpoint_url) + 4. Global config file (profile -> endpoint_url) + """ + + def __call__(self, ctx: SharedConfigContext) -> Resolved[str | None]: + """Resolve the endpoint URI from all sources. + + :param ctx: The shared resolution context. + :returns: Resolved endpoint URI value with source. + """ + return run_program(self._resolve(ctx)) diff --git a/packages/smithy-aws-core/src/smithy_aws_core/config/types.py b/packages/smithy-aws-core/src/smithy_aws_core/config/types.py index 247758c33..3841f587e 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/config/types.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/config/types.py @@ -58,8 +58,11 @@ class FieldSpec: default_factory: Callable[[], Any] | None = None """Factory for mutable defaults (lists, dicts) that need a fresh instance per config.""" - resolver: Callable[..., Awaitable[Resolved[Any]]] | None = None - """Async function that resolves the field from env/profile/etc.""" + resolver: Callable[..., Resolved[Any] | Awaitable[Resolved[Any]]] | None = None + """Function that resolves the field from env/profile/etc. + + Sync configs take a sync function; async configs may take either. + """ validator: Callable[[Any], None] | None = None """Function that validates the resolved value. Raises on invalid input.""" diff --git a/packages/smithy-aws-core/src/smithy_aws_core/endpoints/standard_regional.py b/packages/smithy-aws-core/src/smithy_aws_core/endpoints/standard_regional.py new file mode 100644 index 000000000..562e4e5d7 --- /dev/null +++ b/packages/smithy-aws-core/src/smithy_aws_core/endpoints/standard_regional.py @@ -0,0 +1,35 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +from typing import Any + +from smithy_core import URI +from smithy_core.endpoints import Endpoint, EndpointResolverParams, resolve_static_uri +from smithy_core.exceptions import EndpointResolutionError +from smithy_core.interfaces import EndpointResolver + +from . import REGIONAL_ENDPOINT_CONFIG + +__all__ = ["StandardRegionalEndpointsResolver"] + + +class StandardRegionalEndpointsResolver(EndpointResolver): + """Resolves endpoints for services with standard regional endpoints.""" + + def __init__(self, endpoint_prefix: str = "bedrock-runtime"): + self._endpoint_prefix = endpoint_prefix + + def resolve_endpoint(self, params: EndpointResolverParams[Any]) -> Endpoint: + if (static_uri := resolve_static_uri(params)) is not None: + return Endpoint(uri=static_uri) + + region_config = params.context.get(REGIONAL_ENDPOINT_CONFIG) + if region_config is not None and region_config.region is not None: + # TODO: use dns suffix determined from partition metadata + dns_suffix = "amazonaws.com" + hostname = f"{self._endpoint_prefix}.{region_config.region}.{dns_suffix}" + + return Endpoint(uri=URI(host=hostname)) + + raise EndpointResolutionError( + "Unable to resolve endpoint - either endpoint_url or region are required." + ) diff --git a/packages/smithy-aws-core/src/smithy_aws_core/identity/__init__.py b/packages/smithy-aws-core/src/smithy_aws_core/identity/__init__.py index 61e5025ba..e11487f90 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/identity/__init__.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/identity/__init__.py @@ -2,18 +2,6 @@ # SPDX-License-Identifier: Apache-2.0 from smithy_core.types import PropertyKey -# TODO(sync-api): The shipped public name smithy_aws_core.identity.EnvironmentCredentialsResolver -# TODO(sync-api): is the ASYNC resolver -- the async client pipeline awaits its get_identity, and -# TODO(sync-api): aws-sdk-python's hand-written integration harness imports it from this path and -# TODO(sync-api): passes it to an async client. The sync twin lives at .environment and is NOT -# TODO(sync-api): re-exported here to avoid shadowing that awaitable contract. -# TODO(sync-api): This re-export makes a non-aio module import aio, which the sync-renames design -# TODO(sync-api): forbids; it is the same pre-existing root->aio dependency the branch defers, not a -# TODO(sync-api): new one. Repoint this to the sync twin and drop the aio import once the SDK harness -# TODO(sync-api): imports an async-aware resolver from smithy_aws_core.aio.identity instead. -from ..aio.identity.environment import ( - AsyncEnvironmentCredentialsResolver as EnvironmentCredentialsResolver, -) from .chain import IdentityChainError, UnclaimedSource from .components import ( AWSCredentialsIdentity, @@ -21,6 +9,8 @@ AWSIdentityConfig, AWSIdentityProperties, ) +from .environment import EnvironmentCredentialsResolver +from .process import ProcessCredentialsResolver from .static import StaticCredentialsResolver __all__ = ( @@ -29,6 +19,7 @@ "AWSIdentityProperties", "EnvironmentCredentialsResolver", "IdentityChainError", + "ProcessCredentialsResolver", "StaticCredentialsResolver", "UnclaimedSource", ) diff --git a/packages/smithy-aws-core/src/smithy_aws_core/identity/chain/__init__.py b/packages/smithy-aws-core/src/smithy_aws_core/identity/chain/__init__.py index fe74cef81..acf53e037 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/identity/chain/__init__.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/identity/chain/__init__.py @@ -13,10 +13,12 @@ Standard, StandardProvider, ) +from .resolver import IdentityChain __all__ = ( "After", "Before", + "IdentityChain", "IdentityChainConfigurationError", "IdentityChainError", "IdentityResolverFailure", diff --git a/packages/smithy-aws-core/src/smithy_aws_core/identity/chain/provider.py b/packages/smithy-aws-core/src/smithy_aws_core/identity/chain/provider.py new file mode 100644 index 000000000..3649f335e --- /dev/null +++ b/packages/smithy-aws-core/src/smithy_aws_core/identity/chain/provider.py @@ -0,0 +1,194 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + +from collections.abc import Mapping, MutableMapping +from dataclasses import dataclass +from typing import Any, ClassVar, Protocol + +from smithy_core.interfaces.identity import Identity, IdentityResolver +from smithy_http.interfaces import HTTPClient + +from ...config.merged_config import MergedConfig +from .ordering import OrderingConstraint + +__all__ = [ + "ChainIdentityProvider", + "ChainSetup", + "NamedResolver", +] + + +@dataclass(frozen=True, kw_only=True) +class _NamedResolverBase: + """Associates an identity resolver with its provider's metadata.""" + + provider_name: str + resolver: Any + + +@dataclass(frozen=True, kw_only=True) +class NamedResolver(_NamedResolverBase): + """Associates an identity resolver with its provider's metadata.""" + + resolver: IdentityResolver[Any, Any] + + def get_identity(self, *, properties: Mapping[str, Any]) -> Any: + """Resolve an identity using the underlying resolver.""" + return self.resolver.get_identity(properties=properties) + + def invalidate(self) -> None: + """Invalidate any identity cached by the underlying resolver.""" + self.resolver.invalidate() + + +class _ChainSetupBase: + """Chain setup state shared by the sync and async chains.""" + + _NAMED_RESOLVER: ClassVar[Any] + + def __init__( + self, + *, + config_file: MergedConfig | None = None, + profile_name: str | None = None, + region_override: str | None = None, + http_client: Any | None = None, + properties: MutableMapping[str, Any] | None = None, + ) -> None: + self._config_file = config_file + self._profile_name = profile_name + self._region_override = region_override + self._http_client = http_client + self._properties: MutableMapping[str, Any] = ( + {} if properties is None else properties + ) + self._resolvers: list[Any] = [] + self._current_provider: Any | None = None + self._terminal = False + + @property + def config_file(self) -> MergedConfig | None: + """Return the parsed config/credentials file, if loaded.""" + return self._config_file + + @property + def profile_name(self) -> str | None: + """Return the profile name, if selected.""" + return self._profile_name + + @property + def region_override(self) -> str | None: + """Return the region override, if provided.""" + return self._region_override + + @property + def properties(self) -> MutableMapping[str, Any]: + """Return the property bag shared by custom providers.""" + return self._properties + + @property + def resolvers(self) -> tuple[Any, ...]: + """Return named resolvers in the order they were added.""" + return tuple(self._resolvers) + + @property + def terminal(self) -> bool: + """Return whether a provider added a terminal resolver.""" + return self._terminal + + def set_current_provider(self, provider: Any) -> None: + """Set the provider whose setup method is currently running.""" + if self._terminal: + raise RuntimeError("Cannot change provider after a terminal resolver.") + self._current_provider = provider + + def set_config_file(self, config_file: MergedConfig) -> None: + """Set the parsed config file without overwriting an existing value.""" + if self._config_file is not None: + raise RuntimeError("Cannot overwrite a config file already present.") + self._config_file = config_file + + def set_profile_name(self, profile_name: str) -> None: + """Set the resolved name of the active profile.""" + self._profile_name = profile_name + + def _add(self, resolver: Any) -> None: + """Add a named resolver and continue assembly.""" + if self._terminal: + raise RuntimeError("Cannot add a resolver after a terminal resolver.") + if self._current_provider is None: + raise RuntimeError("Cannot add a resolver without a current provider.") + self._resolvers.append( + self._NAMED_RESOLVER( + provider_name=self._current_provider.name, + resolver=resolver, + ) + ) + + def _add_terminal(self, resolver: Any) -> None: + self._add(resolver) + self._terminal = True + + +class ChainSetup(_ChainSetupBase): + """Tracks shared state and resolvers during chain setup.""" + + _NAMED_RESOLVER = NamedResolver + + def __init__( + self, + *, + config_file: MergedConfig | None = None, + profile_name: str | None = None, + region_override: str | None = None, + http_client: HTTPClient | None = None, + properties: MutableMapping[str, Any] | None = None, + ) -> None: + super().__init__( + config_file=config_file, + profile_name=profile_name, + region_override=region_override, + http_client=http_client, + properties=properties, + ) + + @property + def http_client(self) -> HTTPClient | None: + """Return the HTTP client to use for network calls, if provided.""" + return self._http_client + + @property + def resolvers(self) -> tuple[NamedResolver, ...]: + """Return named resolvers in the order they were added.""" + return tuple(self._resolvers) + + def add_resolver(self, resolver: IdentityResolver[Any, Any]) -> None: + """Add a named resolver and continue assembly.""" + self._add(resolver) + + def add_terminal_resolver(self, resolver: IdentityResolver[Any, Any]) -> None: + """Add a named resolver and stop assembly.""" + self._add_terminal(resolver) + + +class ChainIdentityProvider(Protocol): + """Inspects setup state and conditionally adds sync identity resolvers.""" + + @property + def name(self) -> str: + """Return the canonical provider name.""" + ... + + @property + def ordering(self) -> OrderingConstraint: + """Return the provider's chain ordering constraint.""" + ... + + def setup( + self, + identity_type: type[Identity], + setup: ChainSetup, + ) -> None: + """Add resolvers for the requested identity type.""" + ... diff --git a/packages/smithy-aws-core/src/smithy_aws_core/identity/chain/providers/__init__.py b/packages/smithy-aws-core/src/smithy_aws_core/identity/chain/providers/__init__.py index f3ca3e3b5..b20c7fcff 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/identity/chain/providers/__init__.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/identity/chain/providers/__init__.py @@ -1,7 +1,17 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 +from .environment import EnvironmentCredentialsProvider +from .process import ProfileCredentialProcessProvider +from .profile import ( + ProfileSessionCredentialsProvider, + ProfileStaticCredentialsProvider, +) +from .shared_config import SharedConfigProvider -# Reserves the "smithy_aws_core.identity.chain_providers" entry-point group for -# sync providers; the async chain discovers under "...aio.identity.chain_providers". -# TODO(sync-api): populate alongside the sync credential chain. -__all__: list[str] = [] +__all__ = [ + "EnvironmentCredentialsProvider", + "ProfileCredentialProcessProvider", + "ProfileSessionCredentialsProvider", + "ProfileStaticCredentialsProvider", + "SharedConfigProvider", +] diff --git a/packages/smithy-aws-core/src/smithy_aws_core/identity/chain/providers/environment.py b/packages/smithy-aws-core/src/smithy_aws_core/identity/chain/providers/environment.py new file mode 100644 index 000000000..8fc42c1cc --- /dev/null +++ b/packages/smithy-aws-core/src/smithy_aws_core/identity/chain/providers/environment.py @@ -0,0 +1,56 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +import os +from typing import Any + +from smithy_core.interfaces.identity import Identity + +from ...components import AWSCredentialsIdentity +from ...environment import EnvironmentCredentialsResolver +from ..ordering import Standard, StandardProvider +from ..provider import ChainSetup + +__all__ = ["EnvironmentCredentialsProvider"] + +_ACCESS_KEY_ID = "AWS_ACCESS_KEY_ID" +_SECRET_ACCESS_KEY = "AWS_SECRET_ACCESS_KEY" # noqa: S105 + + +class _EnvironmentCredentialsProviderBase: + """Adds an environment resolver unless an explicit profile is selected.""" + + @property + def name(self) -> str: + """Return the canonical provider name.""" + return StandardProvider.ENVIRONMENT.canonical_name + + @property + def ordering(self) -> Standard: + """Return the provider's standard chain position.""" + return Standard(slot=StandardProvider.ENVIRONMENT) + + def _new_resolver(self) -> Any: + raise NotImplementedError + + def _setup(self, identity_type: type[Identity], setup: Any) -> None: + if identity_type is not AWSCredentialsIdentity: + return + + if setup.profile_name is not None: + return + + if not os.getenv(_ACCESS_KEY_ID) or not os.getenv(_SECRET_ACCESS_KEY): + return + + setup.add_terminal_resolver(self._new_resolver()) + + +class EnvironmentCredentialsProvider(_EnvironmentCredentialsProviderBase): + """Adds an environment resolver unless an explicit profile is selected.""" + + def _new_resolver(self) -> EnvironmentCredentialsResolver: + return EnvironmentCredentialsResolver() + + def setup(self, identity_type: type[Identity], setup: ChainSetup) -> None: + """Add an environment resolver when env credentials are configured.""" + self._setup(identity_type, setup) diff --git a/packages/smithy-aws-core/src/smithy_aws_core/identity/chain/providers/process.py b/packages/smithy-aws-core/src/smithy_aws_core/identity/chain/providers/process.py index 015fa4081..40f3f6bf4 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/identity/chain/providers/process.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/identity/chain/providers/process.py @@ -2,8 +2,20 @@ # SPDX-License-Identifier: Apache-2.0 import shlex import sys +from typing import Any from smithy_core.exceptions import SmithyError +from smithy_core.interfaces.identity import Identity + +from ...components import AWSCredentialsIdentity +from ...process import ProcessCredentialsResolver +from ..ordering import Standard, StandardProvider +from ..provider import ChainSetup + +__all__ = ["ProcessConfigurationError", "ProfileCredentialProcessProvider"] + +_CREDENTIAL_PROCESS = "credential_process" +_ACCOUNT_ID = "aws_account_id" class ProcessConfigurationError(SmithyError): @@ -85,3 +97,55 @@ def _split_windows_command(command: str) -> list[str]: arguments.append("".join(argument)) return arguments + + +class _ProfileCredentialProcessProviderBase: + """Adds a process credential resolver configured by the active profile.""" + + @property + def name(self) -> str: + """Return the canonical provider name.""" + return StandardProvider.PROFILE_CREDENTIAL_PROCESS.canonical_name + + @property + def ordering(self) -> Standard: + """Return the provider's standard chain position.""" + return Standard(slot=StandardProvider.PROFILE_CREDENTIAL_PROCESS) + + def _new_resolver(self, command: list[str], account_id: str | None) -> Any: + raise NotImplementedError + + def _setup(self, identity_type: type[Identity], setup: Any) -> None: + if identity_type is not AWSCredentialsIdentity: + return + + config_file = setup.config_file + profile_name = setup.profile_name + if config_file is None or profile_name is None: + return + + command = config_file.get(profile_name, _CREDENTIAL_PROCESS) + if not command: + return + + # The process output's AccountId takes precedence; the profile's + # aws_account_id is only used as a fallback. + setup.add_terminal_resolver( + self._new_resolver( + _split_process_command(command), + config_file.get(profile_name, _ACCOUNT_ID), + ) + ) + + +class ProfileCredentialProcessProvider(_ProfileCredentialProcessProviderBase): + """Adds a process credential resolver configured by the active profile.""" + + def _new_resolver( + self, command: list[str], account_id: str | None + ) -> ProcessCredentialsResolver: + return ProcessCredentialsResolver(command, account_id=account_id) + + def setup(self, identity_type: type[Identity], setup: ChainSetup) -> None: + """Add a resolver when the active profile configures a credential process.""" + self._setup(identity_type, setup) diff --git a/packages/smithy-aws-core/src/smithy_aws_core/identity/chain/providers/profile.py b/packages/smithy-aws-core/src/smithy_aws_core/identity/chain/providers/profile.py new file mode 100644 index 000000000..4ed45b534 --- /dev/null +++ b/packages/smithy-aws-core/src/smithy_aws_core/identity/chain/providers/profile.py @@ -0,0 +1,131 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +from typing import Any + +from smithy_core.interfaces.identity import Identity + +from ...components import AWSCredentialsIdentity +from ...static import StaticCredentialsResolver +from ..ordering import Standard, StandardProvider +from ..provider import ChainSetup + +__all__ = [ + "ProfileSessionCredentialsProvider", + "ProfileStaticCredentialsProvider", +] + +_ACCESS_KEY_ID = "aws_access_key_id" +_SECRET_ACCESS_KEY = "aws_secret_access_key" # noqa: S105 +_SESSION_TOKEN = "aws_session_token" # noqa: S105 +_ACCOUNT_ID = "aws_account_id" +_ROLE_ARN = "role_arn" + + +class _ProfileSessionCredentialsProviderBase: + """Adds a resolver for session credentials from the active profile.""" + + @property + def name(self) -> str: + """Return the canonical provider name.""" + return StandardProvider.PROFILE_SESSION_KEYS.canonical_name + + @property + def ordering(self) -> Standard: + """Return the provider's standard chain position.""" + return Standard(slot=StandardProvider.PROFILE_SESSION_KEYS) + + def _new_resolver(self, identity: AWSCredentialsIdentity) -> Any: + raise NotImplementedError + + def _setup(self, identity_type: type[Identity], setup: Any) -> None: + if identity_type is not AWSCredentialsIdentity: + return + + config_file = setup.config_file + profile_name = setup.profile_name + if config_file is None or profile_name is None: + return + + if config_file.get(profile_name, _ROLE_ARN) is not None: + return + + access_key_id = config_file.get(profile_name, _ACCESS_KEY_ID) + secret_access_key = config_file.get(profile_name, _SECRET_ACCESS_KEY) + session_token = config_file.get(profile_name, _SESSION_TOKEN) + if access_key_id is None or secret_access_key is None or session_token is None: + return + + identity = AWSCredentialsIdentity( + access_key_id=access_key_id, + secret_access_key=secret_access_key, + session_token=session_token, + account_id=config_file.get(profile_name, _ACCOUNT_ID), + ) + setup.add_terminal_resolver(self._new_resolver(identity)) + + +class _ProfileStaticCredentialsProviderBase: + """Adds a resolver for static credentials from the active profile.""" + + @property + def name(self) -> str: + """Return the canonical provider name.""" + return StandardProvider.PROFILE_STATIC_KEYS.canonical_name + + @property + def ordering(self) -> Standard: + """Return the provider's standard chain position.""" + return Standard(slot=StandardProvider.PROFILE_STATIC_KEYS) + + def _new_resolver(self, identity: AWSCredentialsIdentity) -> Any: + raise NotImplementedError + + def _setup(self, identity_type: type[Identity], setup: Any) -> None: + if identity_type is not AWSCredentialsIdentity: + return + + config_file = setup.config_file + profile_name = setup.profile_name + if config_file is None or profile_name is None: + return + + if config_file.get(profile_name, _ROLE_ARN) is not None: + return + + access_key_id = config_file.get(profile_name, _ACCESS_KEY_ID) + secret_access_key = config_file.get(profile_name, _SECRET_ACCESS_KEY) + if access_key_id is None or secret_access_key is None: + return + + identity = AWSCredentialsIdentity( + access_key_id=access_key_id, + secret_access_key=secret_access_key, + account_id=config_file.get(profile_name, _ACCOUNT_ID), + ) + setup.add_terminal_resolver(self._new_resolver(identity)) + + +class ProfileSessionCredentialsProvider(_ProfileSessionCredentialsProviderBase): + """Adds a resolver for session credentials from the active profile.""" + + def _new_resolver( + self, identity: AWSCredentialsIdentity + ) -> StaticCredentialsResolver: + return StaticCredentialsResolver(identity) + + def setup(self, identity_type: type[Identity], setup: ChainSetup) -> None: + """Add a resolver for complete session credentials from the active profile.""" + self._setup(identity_type, setup) + + +class ProfileStaticCredentialsProvider(_ProfileStaticCredentialsProviderBase): + """Adds a resolver for static credentials from the active profile.""" + + def _new_resolver( + self, identity: AWSCredentialsIdentity + ) -> StaticCredentialsResolver: + return StaticCredentialsResolver(identity) + + def setup(self, identity_type: type[Identity], setup: ChainSetup) -> None: + """Add a resolver for complete static credentials from the active profile.""" + self._setup(identity_type, setup) diff --git a/packages/smithy-aws-core/src/smithy_aws_core/identity/chain/providers/shared_config.py b/packages/smithy-aws-core/src/smithy_aws_core/identity/chain/providers/shared_config.py new file mode 100644 index 000000000..7f6f4d1e3 --- /dev/null +++ b/packages/smithy-aws-core/src/smithy_aws_core/identity/chain/providers/shared_config.py @@ -0,0 +1,54 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +import os +from typing import Any + +from smithy_core._program import Program, run_program +from smithy_core.interfaces.identity import Identity + +from ....config.context import load_config +from ...components import AWSCredentialsIdentity +from ..ordering import Standard, StandardProvider +from ..provider import ChainSetup + +__all__ = ["SharedConfigProvider"] + + +class _SharedConfigProviderBase: + """Loads and selects the active AWS profile for downstream providers.""" + + @property + def name(self) -> str: + """Return the canonical provider name.""" + return StandardProvider.SHARED_CONFIG.canonical_name + + @property + def ordering(self) -> Standard: + """Return the provider's standard chain position.""" + return Standard(slot=StandardProvider.SHARED_CONFIG) + + def _load_config(self) -> Any: + raise NotImplementedError + + def _setup(self, identity_type: type[Identity], setup: Any) -> Program[None]: + if identity_type is not AWSCredentialsIdentity: + return + + config_file = setup.config_file + if config_file is None: + config_file = yield self._load_config + setup.set_config_file(config_file) + + profile_name = setup.profile_name or os.getenv("AWS_PROFILE") or "default" + setup.set_profile_name(profile_name) + + +class SharedConfigProvider(_SharedConfigProviderBase): + """Loads and selects the active AWS profile for downstream providers.""" + + def _load_config(self) -> Any: + return load_config() + + def setup(self, identity_type: type[Identity], setup: ChainSetup) -> None: + """Load and select the active profile for AWS credentials.""" + run_program(self._setup(identity_type, setup)) diff --git a/packages/smithy-aws-core/src/smithy_aws_core/identity/chain/resolver.py b/packages/smithy-aws-core/src/smithy_aws_core/identity/chain/resolver.py new file mode 100644 index 000000000..da15db611 --- /dev/null +++ b/packages/smithy-aws-core/src/smithy_aws_core/identity/chain/resolver.py @@ -0,0 +1,270 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +import logging +from collections.abc import Callable, Mapping, Sequence +from importlib import metadata +from typing import Any, cast + +from smithy_core._program import Program, run_program +from smithy_core.exceptions import SmithyIdentityError +from smithy_core.interfaces.identity import Identity, IdentityResolver +from smithy_http.interfaces import HTTPClient + +from ...config.merged_config import MergedConfig +from .exceptions import ( + IdentityChainConfigurationError, + IdentityChainError, + IdentityResolverFailure, + UnclaimedSource, +) +from .ordering import After, Before, Standard, StandardProvider +from .provider import ( + ChainIdentityProvider, + ChainSetup, + _NamedResolverBase, # pyright: ignore[reportPrivateUsage] +) + +__all__ = ("IdentityChain",) + +_CHAIN_PROVIDER_ENTRY_POINT_GROUP = "smithy_aws_core.identity.chain_providers" +logger = logging.getLogger(__name__) + + +def _discover_providers(group: str) -> tuple[Any, ...]: + discovered: list[Any] = [] + for entry_point in metadata.entry_points(group=group): + provider_factory = cast(Callable[[], Any], entry_point.load()) + discovered.append(provider_factory()) + return tuple(discovered) + + +def _assemble( + identity_type: type[Identity], + discovered_providers: Sequence[Any], + setup: Any, + chain_logger: logging.Logger, +) -> Program[tuple[UnclaimedSource, ...]]: + """Run each discovered provider's setup in chain order.""" + _validate_providers(discovered_providers) + providers = _sort_by_ordering(discovered_providers) + unclaimed_sources = _find_unclaimed_sources(discovered_providers) + + for provider in providers: + setup.set_current_provider(provider) + yield lambda: provider.setup(identity_type, setup) + if setup.terminal: + break + + for source in unclaimed_sources: + chain_logger.warning(str(source)) + return unclaimed_sources + + +class _IdentityChainBase[I: Identity]: + """Identity chain logic shared by the sync and async chains.""" + + _resolvers: tuple[Any, ...] + _identity_type: type[I] | None + _unclaimed_sources: tuple[UnclaimedSource, ...] + + def __init__( + self, + resolvers: Sequence[Any], + *, + identity_type: type[I] | None = None, + unclaimed_sources: Sequence[UnclaimedSource] = (), + ) -> None: + """Initialize the chain with resolvers in precedence order. + + :param resolvers: Identity resolvers to iterate in precedence order. + :param identity_type: The identity type this chain resolves, or None when + constructing a chain directly without declaring one. + :param unclaimed_sources: Detected-but-unclaimed sources discovered during + chain assembly. This parameter is used by :meth:`create`; omit it when + constructing a chain directly. + """ + self._resolvers = tuple(resolvers) + self._identity_type = identity_type + self._unclaimed_sources = tuple(unclaimed_sources) + + @property + def identity_type(self) -> type[I] | None: + """The identity type this chain resolves, or None if not declared.""" + return self._identity_type + + def _get_identity(self, properties: Mapping[str, Any]) -> Program[I]: + failures: list[IdentityResolverFailure] = [] + for resolver in self._resolvers: + try: + return (yield lambda: resolver.get_identity(properties=properties)) + except SmithyIdentityError as error: + if isinstance(resolver, _NamedResolverBase): + provider_name = resolver.provider_name + else: + provider_name = type(resolver).__name__ + failures.append( + IdentityResolverFailure( + provider_name=provider_name, + error=error, + ) + ) + + raise IdentityChainError( + failures=tuple(failures), + unclaimed_sources=self._unclaimed_sources, + ) + + def _invalidate(self) -> Program[None]: + for resolver in self._resolvers: + yield resolver.invalidate + + +class IdentityChain[I: Identity]( + _IdentityChainBase[I], IdentityResolver[I, Mapping[str, Any]] +): + """Resolves identities from an assembled sequence of resolvers.""" + + def __init__( + self, + resolvers: Sequence[IdentityResolver[I, Any]], + *, + identity_type: type[I] | None = None, + unclaimed_sources: Sequence[UnclaimedSource] = (), + ) -> None: + """Initialize the chain with resolvers in precedence order. + + :param resolvers: Identity resolvers to iterate in precedence order. + :param identity_type: The identity type this chain resolves, or None when + constructing a chain directly without declaring one. + :param unclaimed_sources: Detected-but-unclaimed sources discovered during + chain assembly. + """ + super().__init__( + resolvers, identity_type=identity_type, unclaimed_sources=unclaimed_sources + ) + + @staticmethod + def create[ChainIdentity: Identity]( + identity_type: type[ChainIdentity], + *, + config_file: MergedConfig | None = None, + profile_name: str | None = None, + region_override: str | None = None, + http_client: HTTPClient | None = None, + ) -> "IdentityChain[ChainIdentity]": + """Create an identity chain from discovered providers. + + :param identity_type: The identity type to resolve. + :param config_file: Parsed config/credentials file. Loaded from disk + when not set. + :param profile_name: Explicit profile name to use. When set, top-level + environment credential resolution is suppressed. + :param region_override: Region to use for providers whose resolvers + fetch credentials through a service call. + :param http_client: HTTP client to use for providers whose resolvers make + network calls. + """ + setup = ChainSetup( + config_file=config_file, + profile_name=profile_name, + region_override=region_override, + http_client=http_client, + ) + discovered: tuple[ChainIdentityProvider, ...] = _discover_providers( + _CHAIN_PROVIDER_ENTRY_POINT_GROUP + ) + unclaimed_sources = run_program( + _assemble(identity_type, discovered, setup, logger) + ) + return IdentityChain( + setup.resolvers, + identity_type=identity_type, + unclaimed_sources=unclaimed_sources, + ) + + def get_identity(self, *, properties: Mapping[str, Any]) -> I: + """Return the first identity resolved by the chain.""" + return run_program(self._get_identity(properties)) + + def invalidate(self) -> None: + """Invalidate every resolver in the chain.""" + run_program(self._invalidate()) + + +def _sort_by_ordering( + providers: Sequence[Any], +) -> tuple[Any, ...]: + if not providers: + return () + slot_indexes = {slot: index for index, slot in enumerate(StandardProvider)} + + def sort_key( + indexed_provider: tuple[int, Any], + ) -> tuple[int, int, int]: + discovery_index, provider = indexed_provider + ordering = provider.ordering + + match ordering: + case Before(): + constraint_precedence = 0 + case Standard(): + constraint_precedence = 1 + case After(): + constraint_precedence = 2 + case _: + raise IdentityChainConfigurationError( + f"Provider {type(provider).__name__} returned an unsupported " + f"ordering constraint: {ordering!r}." + ) + + return (slot_indexes[ordering.slot], constraint_precedence, discovery_index) + + indexed_providers = enumerate(providers) + ordered_providers = sorted(indexed_providers, key=sort_key) + return tuple(provider for _, provider in ordered_providers) + + +def _validate_providers(providers: Sequence[Any]) -> None: + discovered_names: dict[str, Any] = {} + discovered_standard_slots: dict[StandardProvider, Any] = {} + + for provider in providers: + normalized_name = provider.name.casefold() + if (previous := discovered_names.get(normalized_name)) is not None: + raise IdentityChainConfigurationError( + f"Credential providers {type(previous).__name__} and " + f"{type(provider).__name__} use the same name: {provider.name}." + ) + discovered_names[normalized_name] = provider + + ordering = provider.ordering + if isinstance(ordering, Standard): + if (previous := discovered_standard_slots.get(ordering.slot)) is not None: + raise IdentityChainConfigurationError( + f"Credential providers {type(previous).__name__} and " + f"{type(provider).__name__} both claim standard slot: " + f"{ordering.slot.name}." + ) + discovered_standard_slots[ordering.slot] = provider + + +def _find_unclaimed_sources( + providers: Sequence[Any], +) -> tuple[UnclaimedSource, ...]: + claimed_slots = { + provider.ordering.slot + for provider in providers + if isinstance(provider.ordering, Standard) + } + unclaimed_sources: list[UnclaimedSource] = [] + + for slot in StandardProvider: + if slot in claimed_slots or not slot.is_detected(): + continue + package = slot.module_suggestion + if package: + unclaimed_sources.append( + UnclaimedSource(source_name=slot.canonical_name, package=package) + ) + + return tuple(unclaimed_sources) diff --git a/packages/smithy-aws-core/src/smithy_aws_core/identity/environment.py b/packages/smithy-aws-core/src/smithy_aws_core/identity/environment.py index 635477a81..d9cec5d8a 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/identity/environment.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/identity/environment.py @@ -3,17 +3,20 @@ import os from smithy_core.exceptions import SmithyIdentityError +from smithy_core.interfaces.identity import IdentityResolver from .components import AWSCredentialsIdentity, AWSIdentityProperties __all__ = ["EnvironmentCredentialsResolver"] -class EnvironmentCredentialsResolver: +class EnvironmentCredentialsResolver( + IdentityResolver[AWSCredentialsIdentity, AWSIdentityProperties] +): """Resolves AWS Credentials from system environment variables.""" def __init__(self): - self._credentials = None + self._credentials: AWSCredentialsIdentity | None = None def get_identity( self, *, properties: AWSIdentityProperties diff --git a/packages/smithy-aws-core/src/smithy_aws_core/identity/process.py b/packages/smithy-aws-core/src/smithy_aws_core/identity/process.py new file mode 100644 index 000000000..3141b2806 --- /dev/null +++ b/packages/smithy-aws-core/src/smithy_aws_core/identity/process.py @@ -0,0 +1,168 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +import json +import subprocess +from datetime import UTC, datetime +from typing import TypeGuard, cast + +from smithy_core.exceptions import SmithyIdentityError +from smithy_core.interfaces.identity import IdentityResolver + +from .components import AWSCredentialsIdentity, AWSIdentityProperties + +__all__ = ["ProcessCredentialsResolver"] + + +def _is_command_list(command: object) -> TypeGuard[list[str]]: + if not isinstance(command, list) or not command: + return False + return all(isinstance(argument, str) for argument in cast(list[object], command)) + + +class _ProcessCredentialsResolverBase: + """Credential process logic shared by the sync and async resolvers. + + :param command: The process command and arguments to execute, as a + non-empty list of strings. + :param timeout: Maximum time in seconds to wait for the process to complete. + :param account_id: Fallback account ID to associate with the resolved + credentials when the process output does not include an ``AccountId``. + """ + + def __init__( + self, + command: list[str], + *, + timeout: float | None = None, + account_id: str | None = None, + ) -> None: + if not _is_command_list(command): + raise ValueError("command must be a non-empty list of strings") + self._command = list(command) + self._timeout = timeout + self._account_id = account_id + self._credentials: AWSCredentialsIdentity | None = None + + def _cached(self) -> AWSCredentialsIdentity | None: + if self._credentials is not None: + # Long-term credentials (no expiration) should always be reused + if self._credentials.expiration is None: + return self._credentials + # Temporary credentials should be reused if not expired + if datetime.now(UTC) < self._credentials.expiration: + return self._credentials + return None + + def _timeout_error(self) -> SmithyIdentityError: + return SmithyIdentityError( + f"Credential process timed out after {self._timeout} seconds" + ) + + def _credentials_from_output( + self, returncode: int | None, stdout: bytes, stderr: bytes + ) -> AWSCredentialsIdentity: + if returncode != 0: + raise SmithyIdentityError( + f"Credential process failed with exit code {returncode}: " + f"{stderr.decode('utf-8', errors='replace')}" + ) + # These exceptions retain the full process output, which may contain + # credentials. Suppress chaining to avoid exposing it in tracebacks. + try: + decoded = stdout.decode("utf-8") + creds = json.loads(decoded) + except UnicodeDecodeError as e: + raise SmithyIdentityError( + "Credential process output is not valid UTF-8 " + f"at byte {e.start}: {e.reason}" + ) from None + except json.JSONDecodeError as e: + raise SmithyIdentityError( + "Credential process output is not valid JSON " + f"at line {e.lineno}, column {e.colno}: {e.msg}" + ) from None + + if not isinstance(creds, dict): + raise SmithyIdentityError( + "Credential process output must be a JSON object, " + f"got {type(creds).__name__}" + ) + creds = cast(dict[str, object], creds) + + version = creds.get("Version") + if version != 1: + raise SmithyIdentityError( + f"Unsupported version '{version}' for credential process provider, supported versions: 1" + ) + access_key_id = self._get_string_field(creds, "AccessKeyId") + secret_access_key = self._get_string_field(creds, "SecretAccessKey") + session_token = self._get_string_field(creds, "SessionToken") + expiration = self._get_string_field(creds, "Expiration") + # Prefer the process output's AccountId, falling back to the profile's + # aws_account_id when the process omits it. + account_id = self._get_string_field(creds, "AccountId") or self._account_id + + if expiration is not None: + try: + dt = datetime.fromisoformat(expiration) + except (TypeError, ValueError) as e: + raise SmithyIdentityError( + "Invalid credential process Expiration; " + f"expected an ISO 8601 string: {e}" + ) from e + expiration = dt.astimezone(UTC) if dt.tzinfo else dt.replace(tzinfo=UTC) + + if access_key_id is None or secret_access_key is None: + raise SmithyIdentityError( + "AccessKeyId and SecretAccessKey are required for process credentials" + ) + + self._credentials = AWSCredentialsIdentity( + access_key_id=access_key_id, + secret_access_key=secret_access_key, + session_token=session_token, + expiration=expiration, + account_id=account_id, + ) + return self._credentials + + @staticmethod + def _get_string_field(creds: dict[str, object], key: str) -> str | None: + value = creds.get(key) + if value is not None and not isinstance(value, str): + raise SmithyIdentityError( + f"Credential process output field '{key}' must be a string, " + f"got {type(value).__name__}" + ) + return value + + +class ProcessCredentialsResolver( + _ProcessCredentialsResolverBase, + IdentityResolver[AWSCredentialsIdentity, AWSIdentityProperties], +): + """Resolves AWS Credentials from a process. + + :param command: The process command and arguments to execute, as a + non-empty list of strings. + :param timeout: Maximum time in seconds to wait for the process to complete. + :param account_id: Fallback account ID to associate with the resolved + credentials when the process output does not include an ``AccountId``. + """ + + def get_identity( + self, *, properties: AWSIdentityProperties + ) -> AWSCredentialsIdentity: + if (cached := self._cached()) is not None: + return cached + try: + completed = subprocess.run( # noqa: S603 + self._command, capture_output=True, timeout=self._timeout, check=False + ) + except OSError as e: + raise SmithyIdentityError(f"Credential process failed to start: {e}") from e + except subprocess.TimeoutExpired as e: + raise self._timeout_error() from e + return self._credentials_from_output( + completed.returncode, completed.stdout, completed.stderr + ) diff --git a/packages/smithy-aws-core/src/smithy_aws_core/protocols.py b/packages/smithy-aws-core/src/smithy_aws_core/protocols.py new file mode 100644 index 000000000..55ef5a406 --- /dev/null +++ b/packages/smithy-aws-core/src/smithy_aws_core/protocols.py @@ -0,0 +1,873 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +from dataclasses import dataclass, replace +from io import BytesIO +from typing import TYPE_CHECKING, Any, ClassVar, Final +from xml.etree.ElementTree import Element + +from smithy_core import URI as _URI +from smithy_core.codecs import Codec +from smithy_core.deserializers import DeserializeableShape, ShapeDeserializer +from smithy_core.documents import TypeRegistry +from smithy_core.exceptions import ( + CallError, + DiscriminatorError, + ExpectationNotMetError, + MissingDependencyError, + ModeledError, +) +from smithy_core.interfaces import ( + BytesReader, + BytesWriter, + ProtocolSettings, + TypedProperties, + URI, +) +from smithy_core.interfaces import StreamingBlob as SyncStreamingBlob +from smithy_core.prelude import DOCUMENT +from smithy_core.response import ResponseMetadata +from smithy_core.schemas import APIOperation +from smithy_core.serializers import SerializeableShape, ShapeSerializer +from smithy_core.shapes import ShapeID, ShapeType +from smithy_core.traits import HTTPTrait +from smithy_core.types import PropertyKey, TimestampFormat +from smithy_http import tuples_to_fields +from smithy_http.aio import HTTPRequest as _HTTPRequest +from smithy_http.aio.interfaces import HTTPErrorIdentifier, HTTPRequest, HTTPResponse +from smithy_http.deserializers import HTTPResponseDeserializer +from smithy_http.protocols import ( + HttpBindingClientProtocol, + RpcClientProtocol, + _HttpBindingProtocolBase, # pyright: ignore[reportPrivateUsage] + _RpcProtocolBase, # pyright: ignore[reportPrivateUsage] +) + +from ._private.query._xml import parse_xml_root +from ._private.query.errors import create_aws_query_error +from ._private.query.metadata import parse_aws_query_request_id +from ._private.query.serializers import Ec2QueryShapeSerializer, QueryShapeSerializer +from ._private.restxml import parse_rest_xml_error +from .traits import ( + AwsJson1_0Trait, + AwsJson1_1Trait, + AwsQueryTrait, + Ec2QueryTrait, + RestJson1Trait, + RestXmlTrait, +) +from .utils import ( + parse_document_discriminator, + parse_error_code, + parse_response_metadata, + parse_retry_after, +) + +try: + from smithy_json import JSONCodec, JSONDocument + + _HAS_JSON = True +except ImportError: + _HAS_JSON = False # type: ignore + +try: + from smithy_xml import XMLCodec + + _HAS_XML = True +except ImportError: + _HAS_XML = False # type: ignore + +if TYPE_CHECKING: + from smithy_json import JSONCodec, JSONDocument + from smithy_xml import XMLCodec + + +__all__ = [ + "AWSErrorIdentifier", + "AWSJSONDocument", + "AwsJson10ClientProtocol", + "AwsJson11ClientProtocol", + "AwsQueryClientProtocol", + "Ec2QueryClientProtocol", + "RestJsonClientProtocol", + "RestXmlClientProtocol", +] + + +def _assert_json() -> None: + if not _HAS_JSON: + raise MissingDependencyError( + "Attempted to use JSON codec, but smithy-json is not installed." + ) + + +def _assert_xml() -> None: + if not _HAS_XML: + raise MissingDependencyError( + "Attempted to use XML codec, but smithy-xml is not installed." + ) + + +class AWSErrorIdentifier(HTTPErrorIdentifier): + _HEADER_KEY: Final = "x-amzn-errortype" + + def identify( + self, + *, + operation: APIOperation[Any, Any], + response: HTTPResponse, + ) -> ShapeID | None: + if self._HEADER_KEY not in response.fields: + return None + + error_field = response.fields[self._HEADER_KEY] + code = error_field.values[0] if len(error_field.values) > 0 else None + if code is not None: + return parse_error_code(code, operation.schema.id.namespace) + return None + + +if TYPE_CHECKING or _HAS_JSON: + + class AWSJSONDocument(JSONDocument): + @property + def discriminator(self) -> ShapeID: + if self.shape_type is ShapeType.STRUCTURE: + return self._schema.id + parsed = parse_document_discriminator( + self, self._settings.default_namespace + ) + if parsed is None: + raise DiscriminatorError( + f"Unable to parse discriminator for {self.shape_type} document." + ) + return parsed +else: + + class AWSJSONDocument: # type: ignore[no-redef] + pass + + +class _AWSResponseMetadataMixin: + """Adds AWS request identifiers to extracted response metadata. + + Mixed in ahead of the HTTP base class, which supplies only the status code. + AWS protocols built on different HTTP bases each include it. + """ + + def extract_response_metadata( + self, + *, + response: HTTPResponse, + context: TypedProperties, + ) -> ResponseMetadata: + return parse_response_metadata(response) + + +class _RestJsonProtocolBase(_AWSResponseMetadataMixin, _HttpBindingProtocolBase): + """aws.protocols#restJson1 logic shared by the sync and async protocols.""" + + _id: Final = RestJson1Trait.id + _content_type: Final = "application/json" + _error_identifier: Final = AWSErrorIdentifier() + + def __init__(self, settings: ProtocolSettings) -> None: + _assert_json() + self._codec: Final = JSONCodec( + document_class=AWSJSONDocument, + default_namespace=settings.namespace, + default_timestamp_format=TimestampFormat.EPOCH_SECONDS, + ) + + @property + def id(self) -> ShapeID: + return self._id + + @property + def payload_codec(self) -> Codec: + return self._codec + + @property + def content_type(self) -> str: + return self._content_type + + @property + def error_identifier(self) -> HTTPErrorIdentifier: + return self._error_identifier + + def _retry_after(self, response: HTTPResponse) -> float | None: + return parse_retry_after(response) + + def _resolve_error_id( + self, + *, + operation: APIOperation[Any, Any], + error_id: ShapeID, + ) -> ShapeID: + for error_schema in operation.error_schemas: + if error_schema.id.name == error_id.name: + return error_schema.id + return error_id + + +class _RestXmlProtocolBase(_AWSResponseMetadataMixin, _HttpBindingProtocolBase): + """aws.protocols#restXml logic shared by the sync and async protocols.""" + + _id: Final = RestXmlTrait.id + _content_type: Final = "application/xml" + + def __init__(self, settings: ProtocolSettings) -> None: + _assert_xml() + self._namespace: Final = settings.namespace + self._codec: Final = XMLCodec(default_namespace=settings.xml_namespace) + + @property + def id(self) -> ShapeID: + return self._id + + @property + def payload_codec(self) -> Codec: + return self._codec + + @property + def content_type(self) -> str: + return self._content_type + + def _create_error( + self, + operation: APIOperation[Any, Any], + request: HTTPRequest, + response: HTTPResponse, + response_body: SyncStreamingBlob, + error_registry: TypeRegistry, + context: TypedProperties, + ) -> CallError: + if isinstance(response_body, bytes | bytearray): + body = bytes(response_body) + else: + body = response_body.read() + + retry_after = parse_retry_after(response) + root = parse_xml_root(body) + error_info = parse_rest_xml_error(root) + code = error_info.code + error_id = None + if code is not None: + error_id = ShapeID.from_parts(namespace=self._namespace, name=code) + for error_schema in operation.error_schemas: + if error_schema.id.name == code: + error_id = error_schema.id + break + + if error_id is not None and error_id in error_registry and root is not None: + error_shape = error_registry.get(error_id) + if not issubclass(error_shape, ModeledError): + raise ExpectationNotMetError( + "Modeled errors must be derived from 'ModeledError', " + f"but got {error_shape}" + ) + + deserializer = HTTPResponseDeserializer( + payload_codec=_XMLErrorCodec( + self._codec, + error_info.wrapper_elements, + root, + ), + http_trait=operation.schema.expect_trait(HTTPTrait), + response=response, + body=body, + ) + modeled_error = error_shape.deserialize(deserializer) + if not modeled_error.message and error_info.message: + modeled_error.message = error_info.message + # The exception's args were taken from the empty message when it + # was constructed, so they're replaced for the message to display. + modeled_error.args = (error_info.message,) + if retry_after is not None: + modeled_error.retry_after = retry_after + return modeled_error + + message = ( + f"Unknown error for operation {operation.schema.id} " + f"- status: {response.status}" + ) + if code is not None: + message += f" - code: {code}" + if error_info.message: + message += f" - message: {error_info.message}" + if response.reason is not None: + message += f" - reason: {response.reason}" + + is_timeout = response.status == 408 + is_throttle = response.status == 429 + return CallError( + message=message, + fault="client" if response.status < 500 else "server", + is_throttling_error=is_throttle, + is_timeout_error=is_timeout, + is_retry_safe=is_throttle or is_timeout or None, + retry_after=retry_after, + ) + + +@dataclass(frozen=True) +class _XMLErrorCodec(Codec): + """Reads an error's members from inside its restXml wrapper elements.""" + + codec: "XMLCodec" + wrapper_elements: tuple[str, ...] + root: Element + + @property + def media_type(self) -> str: + return self.codec.media_type + + def create_serializer(self, sink: BytesWriter) -> ShapeSerializer: + return self.codec.create_serializer(sink) + + def create_deserializer(self, source: bytes | BytesReader) -> ShapeDeserializer: + return self.codec.create_deserializer_from_element( + self.root, wrapper_elements=self.wrapper_elements + ) + + +class _AWSJSONProtocolBase(_AWSResponseMetadataMixin, _RpcProtocolBase): + """awsJson logic shared by the sync and async protocols.""" + + _error_identifier: Final = AWSErrorIdentifier() + + _id: ClassVar[ShapeID] + _content_type: ClassVar[str] + + def __init__(self, settings: ProtocolSettings) -> None: + _assert_json() + self._service_name: Final = settings.service_target + self._event_stream_http = self._resolve_event_stream_http( + settings.service_schema, self._id + ) + self._codec: Final = JSONCodec( + document_class=AWSJSONDocument, + default_namespace=settings.namespace, + default_timestamp_format=TimestampFormat.EPOCH_SECONDS, + use_json_name=False, + ) + + @property + def id(self) -> ShapeID: + return self._id + + @property + def payload_codec(self) -> Codec: + return self._codec + + @property + def content_type(self) -> str: + return self._content_type + + @property + def error_identifier(self) -> HTTPErrorIdentifier: + return self._error_identifier + + def serialize_request[ + OperationInput: SerializeableShape, + OperationOutput: DeserializeableShape, + ]( + self, + *, + operation: APIOperation[OperationInput, OperationOutput], + input: OperationInput, + endpoint: URI, + context: TypedProperties, + ) -> HTTPRequest: + # An event-stream operation must negotiate HTTP/2 (see _requires_http2); the + # transport does so per operation, leaving non-streaming calls on HTTP/1.1. + http2_required = self._requires_http2(operation) + + if self._is_event_stream_member(operation.input_stream_member): + # RPC event streams carry the input as an initial-request event at the head + # of the stream body; the envelope keeps x-amz-target as a normal request. + return _HTTPRequest( + method="POST", + destination=_URI(host="", path="/"), + fields=tuples_to_fields( + [ + ("content-type", "application/vnd.amazon.eventstream"), + ( + "x-amz-target", + f"{self._service_name}.{operation.schema.id.name}", + ), + ] + ), + body=self._build_initial_request_body(input, context), + http2_required=http2_required, + ) + + payload = self.payload_codec.serialize(shape=input) + return _HTTPRequest( + method="POST", + destination=_URI(host="", path="/"), + fields=tuples_to_fields( + [ + ("content-type", self.content_type), + ("content-length", str(len(payload))), + ( + "x-amz-target", + f"{self._service_name}.{operation.schema.id.name}", + ), + ] + ), + body=self._request_body(payload), + http2_required=http2_required, + ) + + def _deserialize_body[ + OperationInput: SerializeableShape, + OperationOutput: DeserializeableShape, + ]( + self, + *, + operation: APIOperation[OperationInput, OperationOutput], + response: HTTPResponse, + body: bytes, + error_registry: TypeRegistry, + context: TypedProperties, + ) -> OperationOutput: + if not self._is_success(operation, context, response): + raise self._create_error( + operation=operation, + response=response, + response_body=body, + error_registry=error_registry, + context=context, + ) + + if len(body) == 0: + body = b"{}" + return self.payload_codec.deserialize(source=body, shape=operation.output) + + def _is_success( + self, + operation: APIOperation[Any, Any], + context: TypedProperties, + response: HTTPResponse, + ) -> bool: + return 200 <= response.status < 300 + + def _create_error( + self, + *, + operation: APIOperation[Any, Any], + response: HTTPResponse, + response_body: bytes, + error_registry: TypeRegistry, + context: TypedProperties, + ) -> CallError: + error_id = self.error_identifier.identify( + operation=operation, response=response + ) + if error_id is not None and error_id not in error_registry: + error_id = self._resolve_error_id(operation=operation, error_id=error_id) + + retry_after = parse_retry_after(response) + + if ( + error_id is None + and len(response_body) > 0 + and self._matches_content_type(response) + ): + deserializer = self.payload_codec.create_deserializer(response_body) + document = deserializer.read_document(schema=DOCUMENT) + document_error_id = document.discriminator + if document_error_id not in error_registry: + document_error_id = self._resolve_error_id( + operation=operation, error_id=document_error_id + ) + + if document_error_id in error_registry: + error_id = document_error_id + + if error_id is not None and error_id in error_registry: + error_shape = error_registry.get(error_id) + + # make sure the error shape is derived from modeled exception + if not issubclass(error_shape, ModeledError): + raise ExpectationNotMetError( + f"Modeled errors must be derived from 'ModeledError', " + f"but got {error_shape}" + ) + + body = response_body if len(response_body) > 0 else b"{}" + deserializer = self.payload_codec.create_deserializer(body) + modeled_error = error_shape.deserialize(deserializer) + if retry_after is not None: + modeled_error.retry_after = retry_after + return modeled_error + + message = ( + f"Unknown error for operation {operation.schema.id} " + f"- status: {response.status}" + ) + if error_id is not None: + message += f" - id: {error_id}" + if response.reason is not None: + message += f" - reason: {response.reason}" + + is_timeout = response.status == 408 + is_throttle = response.status == 429 + fault = "client" if response.status < 500 else "server" + + return CallError( + message=message, + fault=fault, + is_throttling_error=is_throttle, + is_timeout_error=is_timeout, + is_retry_safe=is_throttle or is_timeout or None, + retry_after=retry_after, + ) + + def _matches_content_type(self, response: HTTPResponse) -> bool: + if "content-type" not in response.fields: + return False + actual = response.fields["content-type"].as_string() + return actual.split(";", 1)[0].strip().lower() == self.content_type.lower() + + def _resolve_error_id( + self, + *, + operation: APIOperation[Any, Any], + error_id: ShapeID, + ) -> ShapeID: + for error_schema in operation.error_schemas: + if error_schema.id.name == error_id.name: + return error_schema.id + return error_id + + +@dataclass(frozen=True) +class _RecordedQueryRequestId: + """A body-sourced request ID bound to the response it was parsed from. + + Retry attempts share one properties object, so the ID is tagged with its + source response. ``extract_response_metadata`` only trusts it for that exact + response, so a later attempt that fails before recording its own ID (for + example, when a pre-deserialization hook raises) cannot surface an earlier + attempt's ID. + """ + + response: HTTPResponse + value: str + + +_QUERY_REQUEST_ID = PropertyKey( + key="aws_query_request_id", value_type=_RecordedQueryRequestId +) +"""Where the awsQuery protocols record a body-sourced request ID. + +The body is only available while deserializing, so the value is stored there for +``extract_response_metadata`` to read back. +""" + + +class _AwsQueryProtocolBase(_AWSResponseMetadataMixin, _RpcProtocolBase): + """aws.protocols#awsQuery logic shared by the sync and async protocols.""" + + _id: ClassVar[ShapeID] = AwsQueryTrait.id + _content_type: Final = "application/x-www-form-urlencoded" + + def __init__(self, settings: ProtocolSettings) -> None: + _assert_xml() + if settings.version is None: + raise ExpectationNotMetError( + f"The {self._id.name} protocol requires a service version, but " + "ProtocolSettings.version was None." + ) + self._default_namespace: Final = settings.namespace + self._version: Final = settings.version + self._codec: Final = XMLCodec() + + @property + def id(self) -> ShapeID: + return self._id + + @property + def payload_codec(self) -> "XMLCodec": + return self._codec + + @property + def content_type(self) -> str: + return self._content_type + + def extract_response_metadata( + self, + *, + response: HTTPResponse, + context: TypedProperties, + ) -> ResponseMetadata: + """Report the request ID, using the one recorded from the body as a fallback. + + awsQuery normally carries the identifier in the body rather than a header, so + the mixin's header lookup usually finds nothing and the body value recorded + during deserialization is used. A header still wins when a service sends one. + """ + metadata = _AWSResponseMetadataMixin.extract_response_metadata( + self, response=response, context=context + ) + if metadata.request_id is not None: + return metadata + # Relies on transport_response identity being preserved from + # deserialize_response through to here; if that ever changes, this safely + # reports no ID rather than a wrong one. + recorded = context.get(_QUERY_REQUEST_ID) + if recorded is None or recorded.response is not response: + return metadata + return replace(metadata, request_id=recorded.value) + + def serialize_request[ + OperationInput: SerializeableShape, + OperationOutput: DeserializeableShape, + ]( + self, + *, + operation: APIOperation[OperationInput, OperationOutput], + input: OperationInput, + endpoint: URI, + context: TypedProperties, + ) -> HTTPRequest: + sink = BytesIO() + params: list[tuple[str, str]] = [] + serializer = self._create_serializer( + sink=sink, + action=self._action_name(operation), + params=params, + ) + input.serialize(serializer) + serializer.flush() + content_length = sink.tell() + body = self._request_body(sink.getvalue()) + return _HTTPRequest( + method="POST", + destination=_URI(host="", path="/"), + fields=tuples_to_fields( + [ + ("content-type", self.content_type), + ("content-length", str(content_length)), + ] + ), + body=body, + ) + + def _deserialize_body[ + OperationInput: SerializeableShape, + OperationOutput: DeserializeableShape, + ]( + self, + *, + operation: APIOperation[OperationInput, OperationOutput], + response: HTTPResponse, + body: bytes, + error_registry: TypeRegistry, + context: TypedProperties, + ) -> OperationOutput: + root = parse_xml_root(body) + + # Recorded before any branch below returns or raises, so successes, empty + # outputs and errors alike can report the identifier. Bound to this + # response so extraction never attributes it to a different attempt. + request_id = parse_aws_query_request_id(root) + if request_id is not None: + context[_QUERY_REQUEST_ID] = _RecordedQueryRequestId(response, request_id) + + if not self._is_success(operation, context, response): + raise self._create_error( + operation=operation, + response=response, + error_registry=error_registry, + context=context, + root=root, + ) + + if len(body) == 0: + return operation.output.deserialize( + HTTPResponseDeserializer( + payload_codec=self.payload_codec, + response=response, + body=body, + ) + ) + + wrapper_elements = self._response_wrapper_elements(operation) + deserializer = ( + self.payload_codec.create_deserializer( + body, wrapper_elements=wrapper_elements + ) + if root is None + else self.payload_codec.create_deserializer_from_element( + root, wrapper_elements=wrapper_elements + ) + ) + return operation.output.deserialize(deserializer) + + def _is_success( + self, + operation: APIOperation[Any, Any], + context: TypedProperties, + response: HTTPResponse, + ) -> bool: + return 200 <= response.status < 300 + + def _create_error( + self, + *, + operation: APIOperation[Any, Any], + response: HTTPResponse, + error_registry: TypeRegistry, + context: TypedProperties, + root: Element | None, + ) -> CallError: + return create_aws_query_error( + body=root, + operation=operation, + error_registry=error_registry, + default_namespace=self._default_namespace, + wrapper_elements=self._error_wrapper_elements(), + status=response.status, + context=context, + retry_after=parse_retry_after(response), + ) + + def _create_serializer( + self, + *, + sink: BytesIO, + action: str, + params: list[tuple[str, str]], + ) -> QueryShapeSerializer: + return QueryShapeSerializer( + sink=sink, action=action, version=self._version, params=params + ) + + def _action_name[I: SerializeableShape, O: DeserializeableShape]( + self, + operation: APIOperation[I, O], + ) -> str: + return operation.schema.id.name + + def _response_wrapper_elements[I: SerializeableShape, O: DeserializeableShape]( + self, + operation: APIOperation[I, O], + ) -> tuple[str, ...]: + name = operation.schema.id.name + # Operations with no output members will omit the element. + if not operation.output_schema.members: + return (f"{name}Response",) + return ( + f"{name}Response", + f"{name}Result", + ) + + def _error_wrapper_elements(self) -> tuple[str, ...]: + return ("ErrorResponse", "Error") + + +class _Ec2QueryProtocolBase(_AwsQueryProtocolBase): + """aws.protocols#ec2Query logic shared by the sync and async protocols. + + An EC2-specific extension of awsQuery: input keys resolve via ``@ec2QueryName`` + and lists are always flattened; responses have no ``Result`` wrapper and errors + nest under ````. + """ + + _id: ClassVar[ShapeID] = Ec2QueryTrait.id + + def _create_serializer( + self, + *, + sink: BytesIO, + action: str, + params: list[tuple[str, str]], + ) -> QueryShapeSerializer: + return Ec2QueryShapeSerializer( + sink=sink, action=action, version=self._version, params=params + ) + + def _response_wrapper_elements[I: SerializeableShape, O: DeserializeableShape]( + self, + operation: APIOperation[I, O], + ) -> tuple[str, ...]: + return (f"{operation.schema.id.name}Response",) + + def _error_wrapper_elements(self) -> tuple[str, ...]: + return ("Response", "Errors", "Error") + + +class RestJsonClientProtocol(_RestJsonProtocolBase, HttpBindingClientProtocol): + """An implementation of the aws.protocols#restJson1 protocol.""" + + +class RestXmlClientProtocol(_RestXmlProtocolBase, HttpBindingClientProtocol): + """An implementation of the aws.protocols#restXml protocol.""" + + +class _AWSJSONSyncClientProtocol(_AWSJSONProtocolBase, RpcClientProtocol): + def deserialize_response[ + OperationInput: SerializeableShape, + OperationOutput: DeserializeableShape, + ]( + self, + *, + operation: APIOperation[OperationInput, OperationOutput], + request: HTTPRequest, + response: HTTPResponse, + error_registry: TypeRegistry, + context: TypedProperties, + ) -> OperationOutput: + if response.status == 200: + self._raise_if_output_event_stream(operation) + return self._deserialize_body( + operation=operation, + response=response, + body=response.consume_body(), + error_registry=error_registry, + context=context, + ) + + +class AwsJson10ClientProtocol(_AWSJSONSyncClientProtocol): + """An implementation of the aws.protocols#awsJson1_0 protocol.""" + + _id: ClassVar[ShapeID] = AwsJson1_0Trait.id + _content_type: ClassVar[str] = "application/x-amz-json-1.0" + + +class AwsJson11ClientProtocol(_AWSJSONSyncClientProtocol): + """An implementation of the aws.protocols#awsJson1_1 protocol.""" + + _id: ClassVar[ShapeID] = AwsJson1_1Trait.id + _content_type: ClassVar[str] = "application/x-amz-json-1.1" + + +class AwsQueryClientProtocol(_AwsQueryProtocolBase, RpcClientProtocol): + """An implementation of the aws.protocols#awsQuery protocol.""" + + def deserialize_response[ + OperationInput: SerializeableShape, + OperationOutput: DeserializeableShape, + ]( + self, + *, + operation: APIOperation[OperationInput, OperationOutput], + request: HTTPRequest, + response: HTTPResponse, + error_registry: TypeRegistry, + context: TypedProperties, + ) -> OperationOutput: + return self._deserialize_body( + operation=operation, + response=response, + body=response.consume_body(), + error_registry=error_registry, + context=context, + ) + + +class Ec2QueryClientProtocol(_Ec2QueryProtocolBase, AwsQueryClientProtocol): + """An implementation of the aws.protocols#ec2Query protocol.""" diff --git a/packages/smithy-aws-core/tests/unit/identity/chain/test_chain.py b/packages/smithy-aws-core/tests/unit/identity/chain/test_chain.py index 63ada74f3..de49d00d2 100644 --- a/packages/smithy-aws-core/tests/unit/identity/chain/test_chain.py +++ b/packages/smithy-aws-core/tests/unit/identity/chain/test_chain.py @@ -6,6 +6,7 @@ import pytest import smithy_aws_core.aio.identity.chain as chain_module +import smithy_aws_core.identity.chain.resolver as resolver_module from smithy_aws_core.aio.identity import AsyncIdentityChain from smithy_aws_core.identity import ( AWSCredentialsIdentity, @@ -142,7 +143,7 @@ def test_sort_orders_standards_by_slot_declaration() -> None: env = _StubProvider("env", Standard(slot=StandardProvider.ENVIRONMENT)) shared = _StubProvider("shared", Standard(slot=StandardProvider.SHARED_CONFIG)) - ordered = chain_module._sort_by_ordering( + ordered = resolver_module._sort_by_ordering( (static, session, env, assume_role, shared) ) @@ -160,7 +161,7 @@ def test_sort_places_before_and_after_around_slot() -> None: shared = _StubProvider("shared", Standard(slot=StandardProvider.SHARED_CONFIG)) after = _StubProvider("after", After(slot=StandardProvider.SHARED_CONFIG)) - ordered = chain_module._sort_by_ordering((after, shared, before)) + ordered = resolver_module._sort_by_ordering((after, shared, before)) assert [p.name for p in ordered] == ["before", "shared", "after"] @@ -174,7 +175,7 @@ def test_sort_resolves_relative_constraints_without_a_claiming_provider() -> Non "static", Standard(slot=StandardProvider.PROFILE_STATIC_KEYS) ) - ordered = chain_module._sort_by_ordering((static, before, env)) + ordered = resolver_module._sort_by_ordering((static, before, env)) assert [p.name for p in ordered] == ["env", "before", "static"] @@ -183,7 +184,7 @@ def test_sort_keeps_discovery_order_for_same_constraint() -> None: first = _StubProvider("first", Before(slot=StandardProvider.ENVIRONMENT)) second = _StubProvider("second", Before(slot=StandardProvider.ENVIRONMENT)) - ordered = chain_module._sort_by_ordering((first, second)) + ordered = resolver_module._sort_by_ordering((first, second)) assert [p.name for p in ordered] == ["first", "second"] @@ -197,7 +198,7 @@ def test_validate_rejects_duplicate_names() -> None: match="Credential providers _StubProvider and _StubProvider use the " "same name: dup", ): - chain_module._validate_providers((first, second)) + resolver_module._validate_providers((first, second)) def test_validate_rejects_case_insensitive_duplicate_names() -> None: @@ -209,7 +210,7 @@ def test_validate_rejects_case_insensitive_duplicate_names() -> None: match="Credential providers _StubProvider and _StubProvider use the " "same name: duplicate", ): - chain_module._validate_providers((first, second)) + resolver_module._validate_providers((first, second)) def test_validate_rejects_duplicate_standard_slots() -> None: @@ -221,7 +222,7 @@ def test_validate_rejects_duplicate_standard_slots() -> None: match="Credential providers _StubProvider and _StubProvider both claim " "standard slot: ENVIRONMENT", ): - chain_module._validate_providers((first, second)) + resolver_module._validate_providers((first, second)) def test_sort_rejects_unsupported_ordering_constraint() -> None: @@ -234,7 +235,7 @@ class _Unsupported: IdentityChainConfigurationError, match="Provider _StubProvider returned an unsupported ordering constraint", ): - chain_module._sort_by_ordering((provider,)) + resolver_module._sort_by_ordering((provider,)) async def test_all_miss_raises_with_per_provider_failures() -> None: diff --git a/packages/smithy-core/src/smithy_core/_pipeline.py b/packages/smithy-core/src/smithy_core/_pipeline.py new file mode 100644 index 000000000..117f007e5 --- /dev/null +++ b/packages/smithy-core/src/smithy_core/_pipeline.py @@ -0,0 +1,572 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""The request pipeline shared by the sync and async clients. + +Every step is a :py:data:`~smithy_core._program.Program`, so the sync pipeline and the +async pipeline run identical logic and differ only in how effects are driven and in +the few hooks below. +""" + +import logging +from collections.abc import Mapping, Sequence +from copy import copy +from dataclasses import replace +from typing import Any, Protocol + +from . import URI +from ._program import Program +from .auth import AuthParams +from .deserializers import DeserializeableShape +from .endpoints import EndpointResolverParams +from .exceptions import ClientTimeoutError, RetryError, SmithyError +from .interceptors import ( + InputContext, + Interceptor, + OutputContext, + RequestContext, + ResponseContext, +) +from .interfaces import Endpoint, TypedProperties +from .interfaces.auth import AuthOption, AuthSchemeResolver +from .schemas import APIOperation +from .serializers import SerializeableShape +from .shapes import ShapeID, ShapeType +from .types import PropertyKey + +__all__ = ["AUTH_SCHEME", "CallLike", "RequestPipelineBase", "is_retryable"] + +AUTH_SCHEME = PropertyKey(key="auth_scheme", value_type=object) +"""The auth scheme selected for a call. Exposed with a mode-specific type by each +client module.""" + +_UNRESOLVED = URI(host="", path="/") +_LOGGER = logging.getLogger("smithy_core.aio.client") + + +class CallLike[I: SerializeableShape, O: DeserializeableShape](Protocol): + """The fields of a client call that the shared pipeline reads.""" + + @property + def input(self) -> I: ... + @property + def operation(self) -> APIOperation[I, O]: ... + @property + def context(self) -> TypedProperties: ... + @property + def interceptor(self) -> Interceptor[I, O, Any, Any]: ... + @property + def auth_scheme_resolver(self) -> AuthSchemeResolver: ... + @property + def supported_auth_schemes(self) -> Mapping[ShapeID, Any]: ... + @property + def endpoint_resolver(self) -> Any: ... + @property + def retry_strategy(self) -> Any: ... + @property + def retry_scope(self) -> str | None: ... + def retryable(self) -> bool: ... + + +def is_retryable(operation: APIOperation[Any, Any]) -> bool: + """Whether an operation can be retried. + + Operations with an input event stream are never retried. A request for any other + operation is retried only if its body can be rewound to where it started. + """ + member = operation.input_stream_member + return member is None or member.shape_type is not ShapeType.UNION + + +def _seek(stream: Any, offset: int, whence: int = 0) -> Any: + """Seek a stream, returning the stream's own (possibly awaitable) result.""" + if (seekable := getattr(stream, "seekable", None)) is not None and not seekable(): + return None + if (seek := getattr(stream, "seek", None)) is not None: + return seek(offset, whence) + return None + + +def _rewind_position(body: Any) -> Program[int | None]: + """Find where a request body is rewound to before it's resent. + + :returns: The body's current position, or None if it can't be rewound. + """ + if body is None or isinstance(body, bytes | bytearray): + return 0 + try: + return (yield lambda: _seek(body, 0, 1)) + except (OSError, ValueError, NotImplementedError): + return None + + +class RequestPipelineBase[TRequest, TResponse]: + """Mode-neutral request pipeline. + + Subclasses supply the protocol and transport and implement the hooks that + differ between sync and async execution. + """ + + protocol: Any + transport: Any + + def _sleep(self, delay: float) -> Any: + """Effect: wait ``delay`` seconds.""" + raise NotImplementedError + + def _transmit( + self, request_context: RequestContext[Any, TRequest], request_future: Any + ) -> Any: + """Effect: send a request over the transport.""" + raise NotImplementedError + + def _adaptive_strategy(self, retry_strategy: Any) -> Any: + """Return the retry strategy if it rate limits requests, else None.""" + raise NotImplementedError + + def _fail_request_future(self, request_future: Any, error: Exception) -> None: + """Report a failure that happened before the request was sent.""" + + def _execute[I: SerializeableShape, O: DeserializeableShape]( + self, call: CallLike[I, O], request_future: Any + ) -> Program[tuple[O, OutputContext[I, O, TRequest, TResponse]]]: + _LOGGER.debug( + 'Making request for operation "%s" with parameters: %s', + call.operation.schema.id.name, + call.input, + ) + output_context = yield from self._handle_execution(call, request_future) + output_context = self._finalize_execution(call, output_context) + + self._attach_response_metadata(output_context) + + if isinstance(output_context.response, Exception): + e = output_context.response + if not isinstance(e, SmithyError): + raise SmithyError(e) from e + raise e + + return output_context.response, output_context # type: ignore + + def _attach_response_metadata[I: SerializeableShape, O: DeserializeableShape]( + self, + output_context: OutputContext[I, O, TRequest | None, TResponse | None], + ) -> None: + """Attach metadata about the transport response to the result or error. + + This runs for successes and failures alike, so that request identifiers + stay available to callers for debugging. Results that do not carry the + attribute are left untouched, as are cases where no response was received + at all. + """ + result = output_context.response + transport_response = output_context.transport_response + + # Only reachable for errors raised before a response arrived, such as a + # connection timeout. The default empty metadata is left in place, where + # a null status code records that nothing came back. + if transport_response is None: + return + + if not hasattr(result, "response_metadata"): + return + + try: + metadata = self.protocol.extract_response_metadata( + response=transport_response, + context=output_context.properties, + ) + setattr(result, "response_metadata", metadata) + except Exception as e: + # Metadata is diagnostic and must never fail a call. Both statements + # above can raise: a broken protocol, or the assignment itself if the + # output shape is frozen. + _LOGGER.debug("Unable to attach response metadata: %s", e) + + def _handle_execution[I: SerializeableShape, O: DeserializeableShape]( + self, call: CallLike[I, O], request_future: Any + ) -> Program[OutputContext[I, O, TRequest | None, TResponse | None]]: + interceptor = call.interceptor + input_context = InputContext(request=call.input, properties=call.context) + try: + interceptor.read_before_execution(input_context) + + input_context = replace( + input_context, + request=interceptor.modify_before_serialization(input_context), + ) + + interceptor.read_before_serialization(input_context) + _LOGGER.debug("Serializing request for: %s", input_context.request) + + transport_request: TRequest = self.protocol.serialize_request( + operation=call.operation, + input=input_context.request, + endpoint=_UNRESOLVED, + context=input_context.properties, + ) + request_context = RequestContext( + request=input_context.request, + transport_request=transport_request, + properties=input_context.properties, + ) + + _LOGGER.debug( + "Serialization complete. Transport request: %s", transport_request + ) + except Exception as e: + return OutputContext( + request=input_context.request, + response=e, + transport_request=None, + transport_response=None, + properties=input_context.properties, + ) + + try: + interceptor.read_after_serialization(request_context) + request_context = replace( + request_context, + transport_request=interceptor.modify_before_retry_loop(request_context), + ) + + return (yield from self._retry(call, request_context, request_future)) + except Exception as e: + return OutputContext( + request=request_context.request, + response=e, + transport_request=request_context.transport_request, + transport_response=None, + properties=request_context.properties, + ) + + def _retry[I: SerializeableShape, O: DeserializeableShape]( + self, + call: CallLike[I, O], + request_context: RequestContext[I, TRequest], + request_future: Any, + ) -> Program[OutputContext[I, O, TRequest | None, TResponse | None]]: + body = getattr(request_context.transport_request, "body", None) + if ( + not call.retryable() + or (start := (yield from _rewind_position(body))) is None + ): + return ( + yield from self._send_with_rate_limiting( + call, request_context, request_future + ) + ) + + retry_strategy = call.retry_strategy + retry_token = yield lambda: retry_strategy.acquire_initial_retry_token( + token_scope=call.retry_scope + ) + + last_output_context: OutputContext[I, O, TRequest, TResponse | None] | None = ( + None + ) + while True: + if retry_token.retry_delay: + delay = retry_token.retry_delay + yield lambda: self._sleep(delay) + + try: + output_context = yield from self._send_with_rate_limiting( + call, + replace( + request_context, + transport_request=copy(request_context.transport_request), + ), + request_future, + ) + except TimeoutError as e: + # Only the adaptive send-token wait raises this. If it happens on a + # retry, return the previous failed attempt (as when attempts run out) + # so the caller's error still includes that response's request ID. + if last_output_context is None: + raise + last_error = last_output_context.response + if isinstance(last_error, Exception): + last_error.add_note(f"Retry not attempted: {e}") + return last_output_context + + if isinstance(output_context.response, Exception): + last_output_context = output_context + error = output_context.response + token = retry_token + try: + retry_token = ( + yield lambda: retry_strategy.refresh_retry_token_for_retry( + token_to_renew=token, error=error + ) + ) + except RetryError as retry_error: + # Long-polling operations back off even when the retry quota + # is exhausted; the strategy surfaces that delay here. + if ( + call.operation.long_polling + and (retry_after := retry_error.retry_after) is not None + ): + yield lambda: self._sleep(retry_after) + # Keeps the final attempt's response, so throttling and 5xx + # failures still carry a request ID when _execute raises. + return output_context + + _LOGGER.debug( + "Retry needed. Attempting request #%s in %.4f seconds.", + retry_token.retry_count + 1, + retry_token.retry_delay, + ) + + try: + yield lambda: _seek(body, start) + except (OSError, ValueError, NotImplementedError) as e: + error.add_note(f"Retry not attempted: {e}") + return output_context + else: + token = retry_token + yield lambda: retry_strategy.record_success(token=token) + return output_context + + def _send_with_rate_limiting[I: SerializeableShape, O: DeserializeableShape]( + self, + call: CallLike[I, O], + request_context: RequestContext[I, TRequest], + request_future: Any, + ) -> Program[OutputContext[I, O, TRequest, TResponse | None]]: + """Send a single attempt, applying client-side rate limiting (adaptive only). + + Every attempt, retryable or not, acquires a send token before it is sent. Once a + response comes back from the service, the rate limiter is updated with it. + """ + adaptive = self._adaptive_strategy(call.retry_strategy) + if adaptive is not None: + try: + yield adaptive.acquire_from_token_bucket + except Exception as e: + self._fail_request_future(request_future, e) + raise + + output_context = yield from self._handle_attempt( + call, request_context, request_future + ) + + if output_context.transport_response is None or adaptive is None: + # Failed before sending, or the connection failed: not a service response + return output_context + + if isinstance(output_context.response, Exception): + is_throttling = adaptive.is_throttling_error(output_context.response) + yield lambda: adaptive.rate_limiter.after_receiving_response(is_throttling) + else: + yield lambda: adaptive.rate_limiter.after_receiving_response( + throttling_error=False + ) + return output_context + + def _handle_attempt[I: SerializeableShape, O: DeserializeableShape]( + self, + call: CallLike[I, O], + request_context: RequestContext[I, TRequest], + request_future: Any, + ) -> Program[OutputContext[I, O, TRequest, TResponse | None]]: + output_context: OutputContext[I, O, TRequest, TResponse | None] + # A modeled error arrives after the response does, so the response is kept + # here to report alongside it. Stays None if nothing came back. + transport_response: TResponse | None = None + try: + interceptor = call.interceptor + interceptor.read_before_attempt(request_context) + + endpoint_params = EndpointResolverParams( + operation=call.operation, + input=request_context.request, + context=request_context.properties, + ) + _LOGGER.debug("Calling endpoint resolver with params: %s", endpoint_params) + endpoint: Endpoint = yield lambda: call.endpoint_resolver.resolve_endpoint( + endpoint_params + ) + _LOGGER.debug("Endpoint resolver result: %s", endpoint) + + request_context = replace( + request_context, + transport_request=self.protocol.set_service_endpoint( + request=request_context.transport_request, endpoint=endpoint + ), + ) + + request_context = replace( + request_context, + transport_request=interceptor.modify_before_signing(request_context), + ) + interceptor.read_before_signing(request_context) + + auth_params = AuthParams[I, O]( + protocol_id=self.protocol.id, + operation=call.operation, + context=request_context.properties, + ) + auth = self._resolve_auth(call, auth_params) + if auth is not None: + option, scheme = auth + request_context.properties[AUTH_SCHEME] = scheme + identity_resolver = scheme.identity_resolver(context=call.context) + + identity_properties = scheme.identity_properties( + context=request_context.properties + ) + identity_properties.update(option.identity_properties) + + identity = yield lambda: identity_resolver.get_identity( + properties=identity_properties + ) + + signer_properties = scheme.signer_properties( + context=request_context.properties + ) + signer_properties.update(option.signer_properties) + _LOGGER.debug("Request to sign: %s", request_context.transport_request) + _LOGGER.debug("Signer properties: %s", signer_properties) + + signer = scheme.signer() + unsigned = request_context.transport_request + signed: TRequest = yield lambda: signer.sign( + request=unsigned, + identity=identity, + properties=signer_properties, + ) + request_context = replace(request_context, transport_request=signed) + + interceptor.read_after_signing(request_context) + request_context = replace( + request_context, + transport_request=interceptor.modify_before_transmit(request_context), + ) + interceptor.read_before_transmit(request_context) + + _LOGGER.debug("Sending request %s", request_context.transport_request) + + to_send = request_context + try: + transport_response = yield lambda: self._transmit( + to_send, request_future + ) + except Exception as e: + if isinstance(e, self.transport.TIMEOUT_EXCEPTIONS): + raise ClientTimeoutError(message="A timeout error occurred.") from e + raise + + _LOGGER.debug("Received response: %s", transport_response) + + response_context = ResponseContext( + request=request_context.request, + transport_request=request_context.transport_request, + transport_response=transport_response, + properties=request_context.properties, + ) + + interceptor.read_after_transmit(response_context) + + response_context = replace( + response_context, + transport_response=interceptor.modify_before_deserialization( + response_context + ), + ) + transport_response = response_context.transport_response + + interceptor.read_before_deserialization(response_context) + + _LOGGER.debug( + "Deserializing response: %s", response_context.transport_response + ) + + received = response_context + output = yield lambda: self.protocol.deserialize_response( + operation=call.operation, + request=received.transport_request, + response=received.transport_response, + error_registry=call.operation.error_registry, + context=received.properties, + ) + + _LOGGER.debug("Deserialization complete. Output: %s", output) + + output_context = OutputContext( + request=response_context.request, + response=output, + transport_request=response_context.transport_request, + transport_response=response_context.transport_response, + properties=response_context.properties, + ) + + interceptor.read_after_deserialization(output_context) + except Exception as e: + output_context = OutputContext( + request=request_context.request, + response=e, + transport_request=request_context.transport_request, + transport_response=transport_response, + properties=request_context.properties, + ) + + return self._finalize_attempt(call, output_context) + + def _resolve_auth[I: SerializeableShape, O: DeserializeableShape]( + self, call: CallLike[Any, Any], params: AuthParams[I, O] + ) -> tuple[AuthOption, Any] | None: + auth_options: Sequence[AuthOption] = ( + call.auth_scheme_resolver.resolve_auth_scheme(auth_parameters=params) + ) + + for option in auth_options: + if ( + scheme := call.supported_auth_schemes.get(option.scheme_id) + ) is not None: + return option, scheme + + return None + + def _finalize_attempt[I: SerializeableShape, O: DeserializeableShape]( + self, + call: CallLike[I, O], + output_context: OutputContext[I, O, TRequest, TResponse | None], + ) -> OutputContext[I, O, TRequest, TResponse | None]: + interceptor = call.interceptor + try: + output_context = replace( + output_context, + response=interceptor.modify_before_attempt_completion(output_context), + ) + except Exception as e: + output_context = replace(output_context, response=e) + + try: + interceptor.read_after_attempt(output_context) + except Exception as e: + output_context = replace(output_context, response=e) + + return output_context + + def _finalize_execution[I: SerializeableShape, O: DeserializeableShape]( + self, + call: CallLike[I, O], + output_context: OutputContext[I, O, TRequest | None, TResponse | None], + ) -> OutputContext[I, O, TRequest | None, TResponse | None]: + interceptor = call.interceptor + try: + output_context = replace( + output_context, + response=interceptor.modify_before_completion(output_context), + ) + + # TODO trace probe + except Exception as e: + output_context = replace(output_context, response=e) + + try: + interceptor.read_after_execution(output_context) + except Exception as e: + output_context = replace(output_context, response=e) + + return output_context diff --git a/packages/smithy-core/src/smithy_core/_program.py b/packages/smithy-core/src/smithy_core/_program.py new file mode 100644 index 000000000..9d30d7918 --- /dev/null +++ b/packages/smithy-core/src/smithy_core/_program.py @@ -0,0 +1,45 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Mode-neutral programs shared by the sync and async clients. + +A program is a generator that yields zero-argument effects and receives each +effect's result. Effects invoke the injected components (transports, resolvers, +signers), so the same program runs synchronously over sync components and +asynchronously over async ones. +""" + +from collections.abc import Callable, Generator +from inspect import isawaitable +from typing import Any + +from .exceptions import AsyncBodyError + +__all__ = ["Effect", "Program", "run_program"] + +type Effect = Callable[[], Any] +type Program[T] = Generator[Effect, Any, T] + + +def run_program[T](program: Program[T]) -> T: + """Drive a program to completion, calling each effect synchronously. + + :raises AsyncBodyError: If an effect returns an awaitable. + """ + value: Any = None + error: BaseException | None = None + while True: + try: + effect = program.send(value) if error is None else program.throw(error) + except StopIteration as stop: + return stop.value + try: + value, error = effect(), None + if isawaitable(value): + close = getattr(value, "close", None) + if close is not None: + close() + raise AsyncBodyError( + f"A synchronous client received an asynchronous result from {effect}." + ) + except BaseException as e: + value, error = None, e diff --git a/packages/smithy-core/src/smithy_core/aio/_program.py b/packages/smithy-core/src/smithy_core/aio/_program.py new file mode 100644 index 000000000..b21c34873 --- /dev/null +++ b/packages/smithy-core/src/smithy_core/aio/_program.py @@ -0,0 +1,25 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +from inspect import isawaitable +from typing import Any + +from .._program import Program + +__all__ = ["run_program_async"] + + +async def run_program_async[T](program: Program[T]) -> T: + """Drive a program to completion, awaiting each effect that returns an awaitable.""" + value: Any = None + error: BaseException | None = None + while True: + try: + effect = program.send(value) if error is None else program.throw(error) + except StopIteration as stop: + return stop.value + try: + value, error = effect(), None + if isawaitable(value): + value = await value + except BaseException as e: + value, error = None, e diff --git a/packages/smithy-core/src/smithy_core/aio/client.py b/packages/smithy-core/src/smithy_core/aio/client.py index 257c99ec7..9de9fae6b 100644 --- a/packages/smithy-core/src/smithy_core/aio/client.py +++ b/packages/smithy-core/src/smithy_core/aio/client.py @@ -2,38 +2,28 @@ # SPDX-License-Identifier: Apache-2.0 import asyncio import contextlib -import logging from asyncio import Future, sleep -from collections.abc import Awaitable, Callable, Sequence -from copy import copy -from dataclasses import dataclass, field, replace +from collections.abc import Awaitable, Callable +from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any, Self -from .. import URI -from ..auth import AuthParams +from .._pipeline import RequestPipelineBase, is_retryable from ..deserializers import DeserializeableShape, ShapeDeserializer -from ..endpoints import EndpointResolverParams -from ..exceptions import ( - ClientTimeoutError, - RetryError, - SmithyError, - UnsupportedTransportError, -) +from ..exceptions import UnsupportedTransportError from ..interceptors import ( - InputContext, Interceptor, InterceptorChain, OutputContext, RequestContext, - ResponseContext, ) -from ..interfaces import Endpoint, TypedProperties -from ..interfaces.auth import AuthOption, AuthSchemeResolver +from ..interfaces import TypedProperties +from ..interfaces.auth import AuthSchemeResolver from ..schemas import APIOperation from ..serializers import SerializeableShape -from ..shapes import ShapeID, ShapeType +from ..shapes import ShapeID from ..types import PropertyKey from ..types import TypedProperties as _TypedProperties +from ._program import run_program_async from .eventstream import ( AsyncDuplexEventStream, AsyncInputEventStream, @@ -50,7 +40,7 @@ from .interfaces.eventstream import AsyncEventReceiver from .interfaces.retries import AsyncRetryStrategy from .retries import AsyncAdaptiveRetryStrategy, AsyncRetryStrategyResolver -from .utils import close, seek +from .utils import close if TYPE_CHECKING: from typing_extensions import TypeForm @@ -59,9 +49,6 @@ key="auth_scheme", value_type=AsyncAuthScheme[Any, Any, Any, Any] ) -_UNRESOLVED = URI(host="", path="/") -_LOGGER = logging.getLogger(__name__) - @dataclass(kw_only=True, frozen=True) class AsyncClientCall[I: SerializeableShape, O: DeserializeableShape]: @@ -104,24 +91,12 @@ def retryable(self) -> bool: Operations with an input event stream are never retried. A request for any other operation is retried only if its body can be rewound to where it started. """ - member = self.operation.input_stream_member - return member is None or member.shape_type is not ShapeType.UNION - - -async def _rewind_position(body: Any) -> int | None: - """Find where a request body is rewound to before it's resent. + return is_retryable(self.operation) - :returns: The body's current position, or None if it can't be rewound. - """ - if body is None or isinstance(body, bytes | bytearray): - return 0 - try: - return await seek(body, 0, 1) - except (OSError, ValueError, NotImplementedError): - return None - -class AsyncRequestPipeline[TRequest: Request, TResponse: Response]: +class AsyncRequestPipeline[TRequest: Request, TResponse: Response]( + RequestPipelineBase[TRequest, TResponse] +): """Invokes client operations asynchronously.""" protocol: AsyncClientProtocol[TRequest, TResponse] @@ -309,479 +284,45 @@ async def _execute_request[I: SerializeableShape, O: DeserializeableShape]( call: AsyncClientCall[I, O], request_future: Future[RequestContext[I, TRequest]] | None, ) -> tuple[O, OutputContext[I, O, TRequest, TResponse]]: - _LOGGER.debug( - 'Making request for operation "%s" with parameters: %s', - call.operation.schema.id.name, - call.input, - ) - output_context = await self._handle_execution(call, request_future) - output_context = self._finalize_execution(call, output_context) - - self._attach_response_metadata(output_context) - - if isinstance(output_context.response, Exception): - e = output_context.response - if not isinstance(e, SmithyError): - raise SmithyError(e) from e - raise e - - return output_context.response, output_context # type: ignore - - def _attach_response_metadata[I: SerializeableShape, O: DeserializeableShape]( - self, - output_context: OutputContext[I, O, TRequest | None, TResponse | None], - ) -> None: - """Attach metadata about the transport response to the result or error. - - This runs for successes and failures alike, so that request identifiers - stay available to callers for debugging. Results that do not carry the - attribute are left untouched, as are cases where no response was received - at all. - """ - result = output_context.response - transport_response = output_context.transport_response + return await run_program_async(self._execute(call, request_future)) - # Only reachable for errors raised before a response arrived, such as a - # connection timeout. The default empty metadata is left in place, where - # a null status code records that nothing came back. - if transport_response is None: - return - - if not hasattr(result, "response_metadata"): - return + async def _sleep(self, delay: float) -> None: + await sleep(delay) - try: - metadata = self.protocol.extract_response_metadata( - response=transport_response, - context=output_context.properties, - ) - setattr(result, "response_metadata", metadata) - except Exception as e: - # Metadata is diagnostic and must never fail a call. Both statements - # above can raise: a broken protocol, or the assignment itself if the - # output shape is frozen. - _LOGGER.debug("Unable to attach response metadata: %s", e) - - async def _handle_execution[I: SerializeableShape, O: DeserializeableShape]( + async def _transmit( self, - call: AsyncClientCall[I, O], - request_future: Future[RequestContext[I, TRequest]] | None, - ) -> OutputContext[I, O, TRequest | None, TResponse | None]: - interceptor = call.interceptor - input_context = InputContext(request=call.input, properties=call.context) - try: - interceptor.read_before_execution(input_context) - - input_context = replace( - input_context, - request=interceptor.modify_before_serialization(input_context), - ) - - interceptor.read_before_serialization(input_context) - _LOGGER.debug("Serializing request for: %s", input_context.request) - - transport_request = self.protocol.serialize_request( - operation=call.operation, - input=input_context.request, - endpoint=_UNRESOLVED, - context=input_context.properties, - ) - request_context = RequestContext( - request=input_context.request, - transport_request=transport_request, - properties=input_context.properties, - ) - - _LOGGER.debug( - "Serialization complete. Transport request: %s", transport_request - ) - except Exception as e: - return OutputContext( - request=input_context.request, - response=e, - transport_request=None, - transport_response=None, - properties=input_context.properties, - ) - - try: - interceptor.read_after_serialization(request_context) - request_context = replace( - request_context, - transport_request=interceptor.modify_before_retry_loop(request_context), - ) - - return await self._retry(call, request_context, request_future) - except Exception as e: - return OutputContext( - request=request_context.request, - response=e, - transport_request=request_context.transport_request, - transport_response=None, - properties=request_context.properties, - ) - - async def _retry[I: SerializeableShape, O: DeserializeableShape]( - self, - call: AsyncClientCall[I, O], - request_context: RequestContext[I, TRequest], - request_future: Future[RequestContext[I, TRequest]] | None, - ) -> OutputContext[I, O, TRequest | None, TResponse | None]: - body = request_context.transport_request.body - if not call.retryable() or (start := await _rewind_position(body)) is None: - return await self._send_with_rate_limiting( - call, request_context, request_future - ) - - retry_strategy = call.retry_strategy - retry_token = await retry_strategy.acquire_initial_retry_token( - token_scope=call.retry_scope - ) - - last_output_context: OutputContext[I, O, TRequest, TResponse | None] | None = ( - None + request_context: RequestContext[Any, TRequest], + request_future: Future[RequestContext[Any, TRequest]] | None, + ) -> TResponse: + if request_future is None: + # If we don't have an input stream, there's no point in creating a + # task, so we just immediately await the coroutine. + return await self.transport.send(request=request_context.transport_request) + # If we have an input event stream (or duplex event stream) then we need to + # let the client return ASAP so that it can start sending events. So here we + # start the transport send in a background task then set the result of the + # request future. It's important to sequence it just like that so that the + # client gets a stream that's ready to send. + transport_task = asyncio.create_task( + self.transport.send(request=request_context.transport_request) ) - while True: - if retry_token.retry_delay: - await sleep(retry_token.retry_delay) - - try: - output_context = await self._send_with_rate_limiting( - call, - replace( - request_context, - transport_request=copy(request_context.transport_request), - ), - request_future, - ) - except TimeoutError as e: - # Only the adaptive send-token wait raises this. If it happens on a - # retry, return the previous failed attempt (as when attempts run out) - # so the caller's error still includes that response's request ID. - if last_output_context is None: - raise - last_error = last_output_context.response - if isinstance(last_error, Exception): - last_error.add_note(f"Retry not attempted: {e}") - return last_output_context - - if isinstance(output_context.response, Exception): - last_output_context = output_context - try: - retry_token = await retry_strategy.refresh_retry_token_for_retry( - token_to_renew=retry_token, - error=output_context.response, - ) - except RetryError as retry_error: - # Long-polling operations back off even when the retry quota - # is exhausted; the strategy surfaces that delay here. - if ( - call.operation.long_polling - and retry_error.retry_after is not None - ): - await sleep(retry_error.retry_after) - # Keeps the final attempt's response, so throttling and 5xx - # failures still carry a request ID when _execute_request raises. - return output_context - - _LOGGER.debug( - "Retry needed. Attempting request #%s in %.4f seconds.", - retry_token.retry_count + 1, - retry_token.retry_delay, - ) - - try: - await seek(body, start) - except (OSError, ValueError, NotImplementedError) as e: - output_context.response.add_note(f"Retry not attempted: {e}") - return output_context - else: - await retry_strategy.record_success(token=retry_token) - return output_context - - async def _send_with_rate_limiting[ - I: SerializeableShape, - O: DeserializeableShape, - ]( - self, - call: AsyncClientCall[I, O], - request_context: RequestContext[I, TRequest], - request_future: Future[RequestContext[I, TRequest]] | None, - ) -> OutputContext[I, O, TRequest, TResponse | None]: - """Send a single attempt, applying client-side rate limiting (adaptive only). - - Every attempt, retryable or not, acquires a send token before it is sent. Once a - response comes back from the service, the rate limiter is updated with it. - """ - retry_strategy = call.retry_strategy - try: - await self._handle_pre_request_rate_limiting(retry_strategy) - except Exception as e: - if request_future is not None and not request_future.done(): - request_future.set_exception(e) - raise - - output_context = await self._handle_attempt( - call, request_context, request_future - ) - - if output_context.transport_response is None: - # Failed before sending, or the connection failed: not a service response - return output_context - - if isinstance(output_context.response, Exception): - await self._handle_post_error_response_rate_limiting( - retry_strategy, output_context.response - ) - else: - await self._handle_success_rate_limiting(retry_strategy) - return output_context - - async def _handle_pre_request_rate_limiting( - self, retry_strategy: AsyncRetryStrategy - ) -> None: - """Handle rate limiting before sending request.""" - if isinstance(retry_strategy, AsyncAdaptiveRetryStrategy): - await retry_strategy.acquire_from_token_bucket() - - async def _handle_post_error_response_rate_limiting( - self, retry_strategy: AsyncRetryStrategy, error: Exception - ) -> None: - """Handle rate limiting after failed response.""" - if isinstance(retry_strategy, AsyncAdaptiveRetryStrategy): - is_throttling = retry_strategy.is_throttling_error(error) - await retry_strategy.rate_limiter.after_receiving_response(is_throttling) + request_future.set_result(request_context) + return await transport_task - async def _handle_success_rate_limiting( + def _adaptive_strategy( self, retry_strategy: AsyncRetryStrategy - ) -> None: - """Handle rate limiting after successful response.""" + ) -> AsyncAdaptiveRetryStrategy | None: if isinstance(retry_strategy, AsyncAdaptiveRetryStrategy): - await retry_strategy.rate_limiter.after_receiving_response( - throttling_error=False - ) - - async def _handle_attempt[I: SerializeableShape, O: DeserializeableShape]( - self, - call: AsyncClientCall[I, O], - request_context: RequestContext[I, TRequest], - request_future: Future[RequestContext[I, TRequest]] | None, - ) -> OutputContext[I, O, TRequest, TResponse | None]: - output_context: OutputContext[I, O, TRequest, TResponse | None] - # A modeled error arrives after the response does, so the response is kept - # here to report alongside it. Stays None if nothing came back. - transport_response: TResponse | None = None - try: - interceptor = call.interceptor - interceptor.read_before_attempt(request_context) - - endpoint_params = EndpointResolverParams( - operation=call.operation, - input=request_context.request, - context=request_context.properties, - ) - _LOGGER.debug("Calling endpoint resolver with params: %s", endpoint_params) - endpoint: Endpoint = await call.endpoint_resolver.resolve_endpoint( - endpoint_params - ) - _LOGGER.debug("Endpoint resolver result: %s", endpoint) - - request_context = replace( - request_context, - transport_request=self.protocol.set_service_endpoint( - request=request_context.transport_request, endpoint=endpoint - ), - ) - - request_context = replace( - request_context, - transport_request=interceptor.modify_before_signing(request_context), - ) - interceptor.read_before_signing(request_context) - - auth_params = AuthParams[I, O]( - protocol_id=self.protocol.id, - operation=call.operation, - context=request_context.properties, - ) - auth = self._resolve_auth(call, auth_params) - if auth is not None: - option, scheme = auth - request_context.properties[AUTH_SCHEME] = scheme - identity_resolver = scheme.identity_resolver(context=call.context) - - identity_properties = scheme.identity_properties( - context=request_context.properties - ) - identity_properties.update(option.identity_properties) - - identity = await identity_resolver.get_identity( - properties=identity_properties - ) - - signer_properties = scheme.signer_properties( - context=request_context.properties - ) - signer_properties.update(option.signer_properties) - _LOGGER.debug("Request to sign: %s", request_context.transport_request) - _LOGGER.debug("Signer properties: %s", signer_properties) - - signer = scheme.signer() - request_context = replace( - request_context, - transport_request=await signer.sign( - request=request_context.transport_request, - identity=identity, - properties=signer_properties, - ), - ) - - interceptor.read_after_signing(request_context) - request_context = replace( - request_context, - transport_request=interceptor.modify_before_transmit(request_context), - ) - interceptor.read_before_transmit(request_context) - - _LOGGER.debug("Sending request %s", request_context.transport_request) - - try: - if request_future is not None: - # If we have an input event stream (or duplex event stream) then we - # need to let the client return ASAP so that it can start sending - # events. So here we start the transport send in a background task - # then set the result of the request future. It's important to sequence - # it just like that so that the client gets a stream that's ready - # to send. - transport_task = asyncio.create_task( - self.transport.send(request=request_context.transport_request) - ) - request_future.set_result(request_context) - transport_response = await transport_task - else: - # If we don't have an input stream, there's no point in creating a - # task, so we just immediately await the coroutine. - transport_response = await self.transport.send( - request=request_context.transport_request - ) - except Exception as e: - if isinstance(e, self.transport.TIMEOUT_EXCEPTIONS): - raise ClientTimeoutError(message="A timeout error occurred.") from e - raise - - _LOGGER.debug("Received response: %s", transport_response) - - response_context = ResponseContext( - request=request_context.request, - transport_request=request_context.transport_request, - transport_response=transport_response, - properties=request_context.properties, - ) - - interceptor.read_after_transmit(response_context) - - response_context = replace( - response_context, - transport_response=interceptor.modify_before_deserialization( - response_context - ), - ) - transport_response = response_context.transport_response - - interceptor.read_before_deserialization(response_context) - - _LOGGER.debug( - "Deserializing response: %s", response_context.transport_response - ) - - output = await self.protocol.deserialize_response( - operation=call.operation, - request=response_context.transport_request, - response=response_context.transport_response, - error_registry=call.operation.error_registry, - context=response_context.properties, - ) - - _LOGGER.debug("Deserialization complete. Output: %s", output) - - output_context = OutputContext( - request=response_context.request, - response=output, - transport_request=response_context.transport_request, - transport_response=response_context.transport_response, - properties=response_context.properties, - ) - - interceptor.read_after_deserialization(output_context) - except Exception as e: - output_context = OutputContext( - request=request_context.request, - response=e, - transport_request=request_context.transport_request, - transport_response=transport_response, - properties=request_context.properties, - ) - - return self._finalize_attempt(call, output_context) - - def _resolve_auth[I: SerializeableShape, O: DeserializeableShape]( - self, call: AsyncClientCall[Any, Any], params: AuthParams[I, O] - ) -> tuple[AuthOption, AsyncAuthScheme[TRequest, Any, Any, Any]] | None: - auth_options: Sequence[AuthOption] = ( - call.auth_scheme_resolver.resolve_auth_scheme(auth_parameters=params) - ) - - for option in auth_options: - if ( - scheme := call.supported_auth_schemes.get(option.scheme_id) - ) is not None: - return option, scheme - + return retry_strategy return None - def _finalize_attempt[I: SerializeableShape, O: DeserializeableShape]( + def _fail_request_future( self, - call: AsyncClientCall[I, O], - output_context: OutputContext[I, O, TRequest, TResponse | None], - ) -> OutputContext[I, O, TRequest, TResponse | None]: - interceptor = call.interceptor - try: - output_context = replace( - output_context, - response=interceptor.modify_before_attempt_completion(output_context), - ) - except Exception as e: - output_context = replace(output_context, response=e) - - try: - interceptor.read_after_attempt(output_context) - except Exception as e: - output_context = replace(output_context, response=e) - - return output_context - - def _finalize_execution[I: SerializeableShape, O: DeserializeableShape]( - self, - call: AsyncClientCall[I, O], - output_context: OutputContext[I, O, TRequest | None, TResponse | None], - ) -> OutputContext[I, O, TRequest | None, TResponse | None]: - interceptor = call.interceptor - try: - output_context = replace( - output_context, - response=interceptor.modify_before_completion(output_context), - ) - - # TODO trace probe - except Exception as e: - output_context = replace(output_context, response=e) - - try: - interceptor.read_after_execution(output_context) - except Exception as e: - output_context = replace(output_context, response=e) - - return output_context + request_future: Future[RequestContext[Any, TRequest]] | None, + error: Exception, + ) -> None: + if request_future is not None and not request_future.done(): + request_future.set_exception(error) class AsyncClient: diff --git a/packages/smithy-core/src/smithy_core/aio/endpoints.py b/packages/smithy-core/src/smithy_core/aio/endpoints.py index 80b93f704..354d4b862 100644 --- a/packages/smithy-core/src/smithy_core/aio/endpoints.py +++ b/packages/smithy-core/src/smithy_core/aio/endpoints.py @@ -2,22 +2,17 @@ # SPDX-License-Identifier: Apache-2.0 from typing import Any -from ..endpoints import Endpoint, EndpointResolverParams, resolve_static_uri -from ..exceptions import EndpointResolutionError +from ..endpoints import EndpointResolverParams, StaticEndpointResolver from ..interfaces import Endpoint as _Endpoint from .interfaces import AsyncEndpointResolver __all__ = ["AsyncStaticEndpointResolver"] +_STATIC = StaticEndpointResolver() + class AsyncStaticEndpointResolver(AsyncEndpointResolver): """A basic endpoint resolver that forwards a static URI.""" async def resolve_endpoint(self, params: EndpointResolverParams[Any]) -> _Endpoint: - static_uri = resolve_static_uri(params) - if static_uri is None: - raise EndpointResolutionError( - "Unable to resolve endpoint: endpoint_uri is required" - ) - - return Endpoint(uri=static_uri) + return _STATIC.resolve_endpoint(params) diff --git a/packages/smithy-core/src/smithy_core/aio/identity.py b/packages/smithy-core/src/smithy_core/aio/identity.py index 4c622b6db..4ab1526ac 100644 --- a/packages/smithy-core/src/smithy_core/aio/identity.py +++ b/packages/smithy-core/src/smithy_core/aio/identity.py @@ -4,8 +4,12 @@ from collections.abc import Mapping, Sequence from typing import Any, Final -from ..exceptions import SmithyIdentityError +from ..identity import ( + _invalidate_all, # pyright: ignore[reportPrivateUsage] + _resolve_from_chain, # pyright: ignore[reportPrivateUsage] +) from ..interfaces.identity import Identity +from ._program import run_program_async from .interfaces.identity import AsyncIdentityResolver __all__ = ["AsyncCachingIdentityResolver", "AsyncChainedIdentityResolver"] @@ -51,20 +55,11 @@ def __init__(self, resolvers: Sequence[AsyncIdentityResolver[I, IP]]) -> None: self._resolvers = resolvers async def _get_identity(self, *, properties: IP) -> I: - logger.debug("Attempting to resolve identity from resolver chain.") - for resolver in self._resolvers: - try: - logger.debug("Attempting to resolve identity from %s.", type(resolver)) - return await resolver.get_identity(properties=properties) - except SmithyIdentityError as e: - logger.debug( - "Failed to resolve identity from %s: %s", type(resolver), e - ) - - raise SmithyIdentityError("Failed to resolve identity from resolver chain.") + return await run_program_async( + _resolve_from_chain(self._resolvers, properties) # pyright: ignore[reportPrivateUsage] + ) async def invalidate(self) -> None: """Discard this chain's cached identity and invalidate every sub-resolver.""" await super().invalidate() - for resolver in self._resolvers: - await resolver.invalidate() + await run_program_async(_invalidate_all(self._resolvers)) # pyright: ignore[reportPrivateUsage] diff --git a/packages/smithy-core/src/smithy_core/aio/retries.py b/packages/smithy-core/src/smithy_core/aio/retries.py index 7e21a7767..7cf3644f9 100644 --- a/packages/smithy-core/src/smithy_core/aio/retries.py +++ b/packages/smithy-core/src/smithy_core/aio/retries.py @@ -1,23 +1,23 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import asyncio -import math -import threading -import time import weakref -from typing import Any +from typing import Any, ClassVar -from ..exceptions import RetryError from ..interfaces import retries as retries_interface from ..retries import ( - CubicCalculator, - ExponentialBackoffJitterType, - ExponentialRetryBackoffStrategy, RetryStrategyOptions, RetryStrategyType, SimpleRetryToken, StandardRetryQuota, StandardRetryToken, + _AdaptiveRetryStrategyBase, # pyright: ignore[reportPrivateUsage] + _ClientRateLimiterBase, # pyright: ignore[reportPrivateUsage] + _RequestRateTrackerBase, # pyright: ignore[reportPrivateUsage] + _RetryStrategyResolverBase, # pyright: ignore[reportPrivateUsage] + _SimpleRetryStrategyBase, # pyright: ignore[reportPrivateUsage] + _StandardRetryStrategyBase, # pyright: ignore[reportPrivateUsage] + _TokenBucketBase, # pyright: ignore[reportPrivateUsage] ) from .interfaces.retries import AsyncRetryStrategy @@ -32,97 +32,7 @@ ] -class AsyncRetryStrategyResolver: - """Retry strategy resolver that caches retry strategies based on configuration options. - - This resolver caches retry strategy instances based on their configuration to reuse existing - instances of AsyncRetryStrategy with the same settings. Each resolver has its own cache, so a - client keeps its strategies, and any state they hold such as an adaptive rate limiter, - for as long as the client exists, no matter how many other clients are created. - """ - - def __init__(self) -> None: - self._strategies: dict[ - tuple[RetryStrategyType, int | None], AsyncRetryStrategy - ] = {} - - async def resolve_retry_strategy( - self, - *, - retry_strategy: AsyncRetryStrategy | RetryStrategyOptions | None, - retry_mode: RetryStrategyType | None = None, - max_attempts: int | None = None, - ) -> AsyncRetryStrategy: - """Resolve a retry strategy from the provided options, using cache when possible. - - :param retry_strategy: An explicitly configured retry strategy or options for - creating one. Takes precedence over ``retry_mode``/``max_attempts``. - :param retry_mode: Retry mode to fall back on when ``retry_strategy`` is None, - typically resolved from the ``AWS_RETRY_MODE`` env var or a config profile. - :param max_attempts: Maximum attempts to fall back on when ``retry_strategy`` is - None, typically resolved from ``AWS_MAX_ATTEMPTS`` or a config profile. - """ - if isinstance(retry_strategy, AsyncRetryStrategy): - return retry_strategy - elif retry_strategy is None: - # Fall back to the separately-resolved config values. - retry_strategy = RetryStrategyOptions( - retry_mode=retry_mode if retry_mode is not None else "standard", - max_attempts=max_attempts, - ) - elif not isinstance(retry_strategy, RetryStrategyOptions): # type: ignore[reportUnnecessaryIsInstance] - raise TypeError( - f"retry_strategy must be AsyncRetryStrategy, RetryStrategyOptions, or None, " - f"got {type(retry_strategy).__name__}" - ) - return self._create_retry_strategy( - retry_strategy.retry_mode, retry_strategy.max_attempts - ) - - def _create_retry_strategy( - self, retry_mode: RetryStrategyType, max_attempts: int | None - ) -> AsyncRetryStrategy: - key = (retry_mode, max_attempts) - if key not in self._strategies: - self._strategies[key] = self._new_retry_strategy(retry_mode, max_attempts) - return self._strategies[key] - - def _new_retry_strategy( - self, retry_mode: RetryStrategyType, max_attempts: int | None - ) -> AsyncRetryStrategy: - kwargs: dict[str, Any] = {"max_attempts": max_attempts} - filtered_kwargs: dict[str, Any] = { - k: v for k, v in kwargs.items() if v is not None - } - match retry_mode: - case "simple": - return AsyncSimpleRetryStrategy(**filtered_kwargs) - case "standard": - return AsyncStandardRetryStrategy(**filtered_kwargs) - case "adaptive": - return AsyncAdaptiveRetryStrategy(**filtered_kwargs) - case _: - raise ValueError(f"Unknown retry mode: {retry_mode}") - - -class AsyncSimpleRetryStrategy: - def __init__( - self, - *, - backoff_strategy: retries_interface.RetryBackoffStrategy | None = None, - max_attempts: int = 5, - ): - """Retry strategy that simply invokes the given backoff strategy. - - :param backoff_strategy: The backoff strategy used by returned tokens to compute - the retry delay. Defaults to :py:class:`ExponentialRetryBackoffStrategy`. - - :param max_attempts: Upper limit on total number of attempts made, including - initial attempt and retries. - """ - self.backoff_strategy = backoff_strategy or ExponentialRetryBackoffStrategy() - self.max_attempts = max_attempts - +class AsyncSimpleRetryStrategy(_SimpleRetryStrategyBase): async def acquire_initial_retry_token( self, *, token_scope: str | None = None ) -> SimpleRetryToken: @@ -130,8 +40,7 @@ async def acquire_initial_retry_token( :param token_scope: This argument is ignored by this retry strategy. """ - retry_delay = self.backoff_strategy.compute_next_backoff_delay(0) - return SimpleRetryToken(retry_count=0, retry_delay=retry_delay) + return self._initial_token() async def refresh_retry_token_for_retry( self, *, token_to_renew: retries_interface.RetryToken, error: Exception @@ -145,84 +54,13 @@ async def refresh_retry_token_for_retry( :param error: The error that triggered the need for a retry. :raises RetryError: If no further retry attempts are allowed. """ - if isinstance(error, retries_interface.ErrorRetryInfo) and error.is_retry_safe: - retry_count = token_to_renew.retry_count + 1 - if retry_count >= self.max_attempts: - raise RetryError( - f"Reached maximum number of allowed attempts: {self.max_attempts}" - ) from error - retry_delay = self.backoff_strategy.compute_next_backoff_delay(retry_count) - return SimpleRetryToken(retry_count=retry_count, retry_delay=retry_delay) - else: - raise RetryError(f"Error is not retryable: {error}") from error + return self._refresh_token(token_to_renew, error) async def record_success(self, *, token: retries_interface.RetryToken) -> None: """Not used by this retry strategy.""" - def __deepcopy__(self, memo: Any) -> "AsyncSimpleRetryStrategy": - return self - - -class AsyncStandardRetryStrategy: - _RETRY_AFTER_MAX_ADDITIONAL: float = 5 - """Upper bound (seconds) for additional delay beyond the computed backoff.""" - - _NON_THROTTLING_BACKOFF_SCALE: float = 0.05 - """Base backoff scale (seconds) for non-throttling errors (50ms).""" - - _THROTTLING_BACKOFF_SCALE: float = 1 - """Base backoff scale (seconds) for throttling errors (1000ms).""" - - _MAX_BACKOFF: float = 20 - """Upper bound (seconds) for the computed backoff, applied before jitter.""" - - def __init__( - self, - *, - backoff_strategy: retries_interface.RetryBackoffStrategy | None = None, - throttling_backoff_strategy: retries_interface.RetryBackoffStrategy - | None = None, - max_attempts: int = 3, - retry_quota: StandardRetryQuota | None = None, - ): - """Standard retry strategy using truncated binary exponential backoff - with full jitter. - - :param backoff_strategy: The backoff strategy used to compute the retry delay - for non-throttling errors. Defaults to a 50ms-base - :py:class:`ExponentialRetryBackoffStrategy`. - - :param throttling_backoff_strategy: The backoff strategy used to compute the - retry delay for throttling errors. Defaults to a 1000ms-base - :py:class:`ExponentialRetryBackoffStrategy`. - - :param max_attempts: Upper limit on total number of attempts made, including - initial attempt and retries. - - :param retry_quota: The retry quota to use for managing retry capacity. Defaults - to a new :py:class:`StandardRetryQuota` instance. - """ - if max_attempts < 0: - raise ValueError( - f"max_attempts must be a non-negative integer, got {max_attempts}" - ) - - self.backoff_strategy = backoff_strategy or ExponentialRetryBackoffStrategy( - backoff_scale_value=self._NON_THROTTLING_BACKOFF_SCALE, - max_backoff=self._MAX_BACKOFF, - jitter_type=ExponentialBackoffJitterType.FULL, - ) - self.throttling_backoff_strategy = ( - throttling_backoff_strategy - or ExponentialRetryBackoffStrategy( - backoff_scale_value=self._THROTTLING_BACKOFF_SCALE, - max_backoff=self._MAX_BACKOFF, - jitter_type=ExponentialBackoffJitterType.FULL, - ) - ) - self.max_attempts = max_attempts - self._retry_quota = retry_quota or StandardRetryQuota() +class AsyncStandardRetryStrategy(_StandardRetryStrategyBase): async def acquire_initial_retry_token( self, *, token_scope: str | None = None ) -> StandardRetryToken: @@ -230,8 +68,7 @@ async def acquire_initial_retry_token( :param token_scope: This argument is ignored by this retry strategy. """ - retry_delay = self.backoff_strategy.compute_next_backoff_delay(0) - return StandardRetryToken(retry_count=0, retry_delay=retry_delay) + return self._initial_token() async def refresh_retry_token_for_retry( self, *, token_to_renew: retries_interface.RetryToken, error: Exception @@ -247,65 +84,17 @@ async def refresh_retry_token_for_retry( quota is exhausted, the raised error carries ``retry_after`` so callers such as long-polling operations can back off before returning. """ - if not isinstance(token_to_renew, StandardRetryToken): - raise TypeError( - f"AsyncStandardRetryStrategy requires StandardRetryToken, got {type(token_to_renew).__name__}" - ) - - if isinstance(error, retries_interface.ErrorRetryInfo) and error.is_retry_safe: - retry_count = token_to_renew.retry_count + 1 - if retry_count >= self.max_attempts: - raise RetryError( - f"Reached maximum number of allowed attempts: {self.max_attempts}" - ) from error - - # Throttling errors use a larger base backoff than other errors. - backoff_strategy = ( - self.throttling_backoff_strategy - if error.is_throttling_error - else self.backoff_strategy - ) - t_i = backoff_strategy.compute_next_backoff_delay(retry_count) - - if error.retry_after is not None: - # Bound a server-directed backoff to [t_i, t_i + 5] seconds. - retry_delay = max( - t_i, min(error.retry_after, self._RETRY_AFTER_MAX_ADDITIONAL + t_i) - ) - else: - retry_delay = t_i - - try: - quota_acquired = self._retry_quota.acquire(error=error) - except RetryError as quota_error: - # Surface the computed delay so callers can back off before giving - # up; long-polling operations sleep for it before returning. - raise RetryError(str(quota_error), retry_after=retry_delay) from error - - return StandardRetryToken( - retry_count=retry_count, - retry_delay=retry_delay, - quota_acquired=quota_acquired, - ) - else: - raise RetryError(f"Error is not retryable: {error}") from error + return self._refresh_token(token_to_renew, error) async def record_success(self, *, token: retries_interface.RetryToken) -> None: """Release retry quota back based on the amount consumed by the last retry. :param token: The token used for the previous successful attempt. """ - if not isinstance(token, StandardRetryToken): - raise TypeError( - f"AsyncStandardRetryStrategy requires StandardRetryToken, got {type(token).__name__}" - ) - self._retry_quota.release(release_amount=token.quota_acquired) - - def __deepcopy__(self, memo: Any) -> "AsyncStandardRetryStrategy": - return self + self._release_quota(token) -class AsyncTokenBucket: +class AsyncTokenBucket(_TokenBucketBase): """Token bucket for rate limiting with configurable fill rate. AsyncTokenBucket provides a collection of arbitrary tokens while managing issuance @@ -321,16 +110,12 @@ class AsyncTokenBucket: uses a per-event-loop asyncio.Condition and does not hold the lock. """ - MIN_FILL_RATE = 0.5 # Minimum allowed fill rate (0.5 tokens/second) - MIN_CAPACITY = 1.0 # Minimum allowed bucket capacity (1.0 tokens) - DEFAULT_TIMEOUT = 30.0 # Default timeout for token acquisition (30.0 seconds) - def __init__( self, *, curr_capacity: float = 0, - fill_rate: float = MIN_FILL_RATE, - timeout: float = DEFAULT_TIMEOUT, + fill_rate: float = _TokenBucketBase.MIN_FILL_RATE, + timeout: float = _TokenBucketBase.DEFAULT_TIMEOUT, ): """Initialize a new AsyncTokenBucket. @@ -340,12 +125,9 @@ def __init__( :param timeout: Maximum time to wait for token acquisition before raising TimeoutError. """ - self._curr_capacity: float = curr_capacity - self._max_capacity: float = max(curr_capacity, self.MIN_CAPACITY) - self._fill_rate: float = max(fill_rate, self.MIN_FILL_RATE) - self._timeout = timeout - self._last_timestamp: float = time.monotonic() - self._lock = threading.Lock() + super().__init__( + curr_capacity=curr_capacity, fill_rate=fill_rate, timeout=timeout + ) self._conditions: weakref.WeakKeyDictionary[ asyncio.AbstractEventLoop, asyncio.Condition ] = weakref.WeakKeyDictionary() @@ -361,29 +143,13 @@ async def acquire(self, amount: float) -> None: :raises TimeoutError: Acquisition took longer than the configured timeout. """ - start_time = time.monotonic() + start_time = self._monotonic() condition = self._get_condition() async with condition: - while True: - with self._lock: - self._refill() - if self._curr_capacity >= amount: - self._curr_capacity -= amount - return - wait_time = (amount - self._curr_capacity) / self._fill_rate - - elapsed = time.monotonic() - start_time - if elapsed >= self._timeout: - # Fails the request; this is not counted as a retry attempt - raise TimeoutError( - f"Failed to acquire {amount} tokens within {self._timeout}s" - ) - # Wait outside the thread lock, and never past the timeout + while (wait := self._try_acquire(amount, start_time)) is not None: + # Wait outside the thread lock try: - await asyncio.wait_for( - condition.wait(), - timeout=min(wait_time, self._timeout - elapsed), - ) + await asyncio.wait_for(condition.wait(), timeout=wait) except TimeoutError: pass @@ -411,15 +177,6 @@ def _get_condition(self) -> asyncio.Condition: self._conditions[loop] = condition return condition - def _refill(self) -> None: - curr_time = time.monotonic() - elapsed = curr_time - self._last_timestamp - refill_amount = elapsed * self._fill_rate - self._curr_capacity = min( - self._max_capacity, self._curr_capacity + refill_amount - ) - self._last_timestamp = curr_time - async def update_rate(self, rate: float) -> None: """Update the bucket's fill rate, maximum capacity and current capacity (if its greater than maximum capacity). @@ -430,13 +187,6 @@ async def update_rate(self, rate: float) -> None: self._set_rate(rate) await self._notify_waiters() - def _set_rate(self, rate: float) -> None: - with self._lock: - self._refill() - self._fill_rate = max(rate, self.MIN_FILL_RATE) - self._max_capacity = max(rate, self.MIN_CAPACITY) - self._curr_capacity = min(self._curr_capacity, self._max_capacity) - async def _notify_waiters(self) -> None: # Wakes waiters on this event loop. Waiters on other threads' loops pick up # the new rate when their current wait times out. @@ -444,32 +194,8 @@ async def _notify_waiters(self) -> None: async with condition: condition.notify_all() - @property - def current_capacity(self) -> float: - """Get the current number of tokens in the bucket. - - :return: The current token count as of the last refill operation. - """ - return self._curr_capacity - - @property - def max_capacity(self) -> float: - """Get the maximum capacity of the bucket. - - :return: The maximum number of tokens the bucket can hold. - """ - return self._max_capacity - - @property - def fill_rate(self) -> float: - """Get the current fill rate of the bucket. - - :return: The rate at which tokens are added to the bucket (tokens/second). - """ - return self._fill_rate - -class AsyncRequestRateTracker: +class AsyncRequestRateTracker(_RequestRateTrackerBase): """AsyncRequestRateTracker tracks the client's request sending rate. AsyncRequestRateTracker measures the actual client request sending rate using @@ -478,30 +204,6 @@ class AsyncRequestRateTracker: to provide accurate measurements for adaptive rate limiting algorithms. """ - _DEFAULT_SMOOTHING = 0.8 - _TIME_BUCKET_RANGE = 0.5 - - def __init__( - self, - smoothing: float = _DEFAULT_SMOOTHING, - time_bucket_range: float = _TIME_BUCKET_RANGE, - ): - """Initialize a new AsyncRequestRateTracker. - - :param smoothing: Exponential smoothing factor. This constant - represents how much weight is given to recent measurements. - Higher values place more emphasis on the most recent observations. - :param time_bucket_range: Time bucket duration in seconds. Rate - calculations are updated when transitioning between buckets. - """ - self._smoothing = smoothing - self._time_bucket_scale = 1 / time_bucket_range - self._request_count = 0 - self._last_calculated_time_bucket = math.floor(time.monotonic()) - self._measured_rate = 0 - # The update never awaits, so it only needs protecting across threads - self._lock = threading.Lock() - async def measure_rate(self) -> float: """Measure and return the current request rate. @@ -510,36 +212,12 @@ async def measure_rate(self) -> float: without recalculation if still within the same time bucket. :return: Current smoothed request rate in requests per second. """ - with self._lock: - curr_time = time.monotonic() - current_time_bucket = ( - math.floor(curr_time * self._time_bucket_scale) - / self._time_bucket_scale - ) - self._request_count += 1 - if current_time_bucket > self._last_calculated_time_bucket: - current_rate = self._request_count / ( - current_time_bucket - self._last_calculated_time_bucket - ) - self._measured_rate = (current_rate * self._smoothing) + ( - self._measured_rate * (1 - self._smoothing) - ) - self._request_count = 0 - self._last_calculated_time_bucket = current_time_bucket - return self._measured_rate - - @property - def request_count(self) -> int: - """Get current request count. For testing only.""" - return self._request_count - - @property - def measured_client_rate(self) -> float: - """Get the client's sending rate. For testing only.""" - return self._measured_rate + return self._measure() -class AsyncClientRateLimiter: +class AsyncClientRateLimiter( + _ClientRateLimiterBase[AsyncTokenBucket, AsyncRequestRateTracker] +): """AsyncClientRateLimiter limits the rate of requests. AsyncClientRateLimiter implements adaptive rate limiting using token bucket and CUBIC @@ -549,29 +227,6 @@ class AsyncClientRateLimiter: while preventing service overload. """ - _REQUEST_COST = 1.0 - - def __init__( - self, - token_bucket: AsyncTokenBucket, - cubic_calculator: CubicCalculator, - rate_tracker: AsyncRequestRateTracker, - rate_limiter_enabled: bool, - ): - """Initialize a new AsyncClientRateLimiter. - - :param token_bucket: Token bucket for controlling request sending rates. - :param cubic_calculator: CUBIC algorithm calculator for rate adjustments. - :param rate_tracker: Tracker for measuring actual client request rates. - :param rate_limiter_enabled: Whether rate limiting is enabled. - """ - - self._rate_tracker = rate_tracker - self._token_bucket = token_bucket - self._cubic_calculator = cubic_calculator - self._rate_limiter_enabled = rate_limiter_enabled - self._lock = threading.Lock() - async def before_sending_request(self) -> None: """Acquire a token before making a request.""" if self._rate_limiter_enabled: @@ -586,38 +241,13 @@ async def after_receiving_response(self, throttling_error: bool) -> None: :param throttling_error: True if the response was a throttling error. """ measured_rate = await self._rate_tracker.measure_rate() - with self._lock: - timestamp = time.monotonic() - if throttling_error: - if not self._rate_limiter_enabled: - rate_to_use = measured_rate - else: - fill_rate = self._token_bucket.fill_rate - rate_to_use = min(measured_rate, fill_rate) - - cubic_calculated_rate = ( - self._cubic_calculator.calculate_throttled_request_rate( - rate_to_use, timestamp - ) - ) - self._rate_limiter_enabled = True - else: - cubic_calculated_rate = ( - self._cubic_calculator.calculate_scaled_request_rate(timestamp) - ) - - new_rate = min(cubic_calculated_rate, 2 * measured_rate) - # Set under the lock so rates reach the bucket in the order they're - # calculated. Waking waiters awaits, so it happens after the lock. - self._token_bucket._set_rate(new_rate) # pyright: ignore[reportPrivateUsage] + self._update_rate(measured_rate, throttling_error) await self._token_bucket._notify_waiters() # pyright: ignore[reportPrivateUsage] - @property - def rate_limit_enabled(self) -> bool: - return self._rate_limiter_enabled - -class AsyncAdaptiveRetryStrategy(AsyncStandardRetryStrategy): +class AsyncAdaptiveRetryStrategy( + _AdaptiveRetryStrategyBase, AsyncStandardRetryStrategy +): """Adaptive retry strategy with client-side rate limiting using CUBIC algorithm. Builds on top of AsyncStandardRetryStrategy by adding token bucket rate limiting and @@ -630,8 +260,6 @@ class AsyncAdaptiveRetryStrategy(AsyncStandardRetryStrategy): throttled and wasted on retries. """ - STARTING_MAX_RATE = 0 - def __init__( self, *, @@ -667,36 +295,57 @@ def __init__( max_attempts=max_attempts, retry_quota=retry_quota, ) - - if rate_limiter is None: - token_bucket = AsyncTokenBucket() - cubic_calculator = CubicCalculator( - starting_max_rate=self.STARTING_MAX_RATE, start_time=time.monotonic() - ) - rate_tracker = AsyncRequestRateTracker() - self._rate_limiter = AsyncClientRateLimiter( - token_bucket=token_bucket, - cubic_calculator=cubic_calculator, - rate_tracker=rate_tracker, - rate_limiter_enabled=False, # Disabled until first throttle - ) - else: - self._rate_limiter = rate_limiter + self._rate_limiter = rate_limiter or AsyncClientRateLimiter( + token_bucket=AsyncTokenBucket(), + cubic_calculator=self._new_cubic_calculator(), + rate_tracker=AsyncRequestRateTracker(), + rate_limiter_enabled=False, # Disabled until first throttle + ) @property def rate_limiter(self) -> AsyncClientRateLimiter: """Get the rate limiter for integration with request pipeline.""" return self._rate_limiter - def is_throttling_error(self, error: Exception) -> bool: - """Check if error is a throttling error using existing ErrorRetryInfo.""" - if isinstance(error, retries_interface.ErrorRetryInfo): - return error.is_throttling_error - return False - async def acquire_from_token_bucket(self) -> None: if self._rate_limiter.rate_limit_enabled: await self._rate_limiter.before_sending_request() - def __deepcopy__(self, memo: Any) -> "AsyncAdaptiveRetryStrategy": - return self + +class AsyncRetryStrategyResolver(_RetryStrategyResolverBase[AsyncRetryStrategy]): + """Retry strategy resolver that caches retry strategies based on configuration options. + + This resolver caches retry strategy instances based on their configuration to reuse existing + instances of AsyncRetryStrategy with the same settings. Each resolver has its own cache, so a + client keeps its strategies, and any state they hold such as an adaptive rate limiter, + for as long as the client exists, no matter how many other clients are created. + """ + + _STRATEGY_TYPES: ClassVar[dict[RetryStrategyType, Any]] = { + "simple": AsyncSimpleRetryStrategy, + "standard": AsyncStandardRetryStrategy, + "adaptive": AsyncAdaptiveRetryStrategy, + } + + async def resolve_retry_strategy( + self, + *, + retry_strategy: AsyncRetryStrategy | RetryStrategyOptions | None, + retry_mode: RetryStrategyType | None = None, + max_attempts: int | None = None, + ) -> AsyncRetryStrategy: + """Resolve a retry strategy from the provided options, using cache when possible. + + :param retry_strategy: An explicitly configured retry strategy or options for + creating one. Takes precedence over ``retry_mode``/``max_attempts``. + :param retry_mode: Retry mode to fall back on when ``retry_strategy`` is None, + typically resolved from the ``AWS_RETRY_MODE`` env var or a config profile. + :param max_attempts: Maximum attempts to fall back on when ``retry_strategy`` is + None, typically resolved from ``AWS_MAX_ATTEMPTS`` or a config profile. + """ + return self._resolve( + retry_strategy=retry_strategy, + retry_mode=retry_mode, + max_attempts=max_attempts, + strategy_protocol=AsyncRetryStrategy, + ) diff --git a/packages/smithy-core/src/smithy_core/client.py b/packages/smithy-core/src/smithy_core/client.py new file mode 100644 index 000000000..8fa3f40e4 --- /dev/null +++ b/packages/smithy-core/src/smithy_core/client.py @@ -0,0 +1,165 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +import threading +import time +from dataclasses import dataclass, field +from typing import Any, Self + +from ._pipeline import RequestPipelineBase, is_retryable +from ._program import run_program +from .deserializers import DeserializeableShape +from .interceptors import Interceptor, InterceptorChain, RequestContext +from .interfaces import ( + ClientProtocol, + ClientTransport, + EndpointResolver, + TypedProperties, +) +from .interfaces.auth import AuthScheme, AuthSchemeResolver +from .interfaces.retries import RetryStrategy +from .retries import AdaptiveRetryStrategy, RetryStrategyResolver +from .schemas import APIOperation +from .serializers import SerializeableShape +from .shapes import ShapeID +from .types import PropertyKey +from .types import TypedProperties as _TypedProperties + +__all__ = ["AUTH_SCHEME", "Client", "ClientCall", "RequestPipeline"] + +AUTH_SCHEME = PropertyKey(key="auth_scheme", value_type=AuthScheme[Any, Any, Any, Any]) + + +@dataclass(kw_only=True, frozen=True) +class ClientCall[I: SerializeableShape, O: DeserializeableShape]: + """A data class containing all the initial information about an operation + invocation.""" + + input: I + """The input of the operation.""" + + operation: APIOperation[I, O] = field(repr=False) + """The schema of the operation.""" + + context: TypedProperties + """The initial context of the operation.""" + + interceptor: Interceptor[I, O, Any, Any] + """The interceptor to use in the course of the operation invocation. + + This SHOULD be an InterceptorChain. + """ + + auth_scheme_resolver: AuthSchemeResolver + """The auth scheme resolver for the operation.""" + + supported_auth_schemes: dict[ShapeID, AuthScheme[Any, Any, Any, Any]] + """The supported auth schemes for the operation.""" + + endpoint_resolver: EndpointResolver + """The endpoint resolver for the operation.""" + + retry_strategy: RetryStrategy + """The retry strategy to use for the operation.""" + + retry_scope: str | None = None + """The retry scope for the operation.""" + + def retryable(self) -> bool: + """Whether the operation can be retried.""" + return is_retryable(self.operation) + + +class RequestPipeline[TRequest, TResponse](RequestPipelineBase[TRequest, TResponse]): + """Invokes client operations synchronously.""" + + protocol: ClientProtocol[TRequest, TResponse] + """The protocol to use to serialize the request and deserialize the response.""" + + transport: ClientTransport[TRequest, TResponse] + """The transport to use to send the request and receive the response.""" + + def __init__( + self, + protocol: ClientProtocol[TRequest, TResponse], + transport: ClientTransport[TRequest, TResponse], + ) -> None: + self.protocol = protocol + self.transport = transport + + def __call__[I: SerializeableShape, O: DeserializeableShape]( + self, call: ClientCall[I, O], / + ) -> O: + """Invoke an operation synchronously. + + :param call: The operation to invoke and associated context. + """ + output, _ = run_program(self._execute(call, None)) + return output + + def _sleep(self, delay: float) -> None: + time.sleep(delay) + + def _transmit( + self, request_context: RequestContext[Any, TRequest], request_future: None + ) -> TResponse: + return self.transport.send(request_context.transport_request) + + def _adaptive_strategy( + self, retry_strategy: RetryStrategy + ) -> AdaptiveRetryStrategy | None: + if isinstance(retry_strategy, AdaptiveRetryStrategy): + return retry_strategy + return None + + +class Client: + """Shared sync client machinery: lifecycle, setup gating, and call dispatch.""" + + def __init__(self) -> None: + self._derive_lock = threading.Lock() + self._setup_done = False + self._closed = False + self._transport: ClientTransport[Any, Any] | None = None + self._retry_strategy_resolver = RetryStrategyResolver() + + def _build_call[I: SerializeableShape, O: DeserializeableShape]( + self, + input: I, + operation: APIOperation[I, O], + *, + config: Any, + retry_strategy: RetryStrategy, + ) -> tuple[RequestPipeline[Any, Any], ClientCall[I, O]]: + """Build the pipeline and call from a resolved config and retry strategy.""" + pipeline = RequestPipeline(protocol=config.protocol, transport=config.transport) + call = ClientCall( + input=input, + operation=operation, + context=_TypedProperties({"config": config}), + interceptor=InterceptorChain(config.interceptors), + auth_scheme_resolver=config.auth_scheme_resolver, + supported_auth_schemes=config.auth_schemes, + endpoint_resolver=config.endpoint_resolver, + retry_strategy=retry_strategy, + ) + return pipeline, call + + def close(self) -> None: + """Close this client and any resources held by its transport.""" + if self._closed: + return + with self._derive_lock: + if self._closed: + return + self._closed = True + if self._setup_done and self._transport is not None: + if (close := getattr(self._transport, "close", None)) is not None: + close() + + def __enter__(self) -> Self: + if self._closed: + raise RuntimeError("Cannot enter a client that has been closed.") + return self + + def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None: + self.close() diff --git a/packages/smithy-core/src/smithy_core/endpoints.py b/packages/smithy-core/src/smithy_core/endpoints.py index 11a1959e0..5f6bbb332 100644 --- a/packages/smithy-core/src/smithy_core/endpoints.py +++ b/packages/smithy-core/src/smithy_core/endpoints.py @@ -18,6 +18,7 @@ "Endpoint", "EndpointResolverParams", "StaticEndpointConfig", + "StaticEndpointResolver", "resolve_static_uri", ] @@ -97,3 +98,15 @@ def resolve_static_uri( query=parsed.query, port=parsed.port, ) + + +class StaticEndpointResolver: + """A basic endpoint resolver that forwards a static URI.""" + + def resolve_endpoint(self, params: EndpointResolverParams[Any]) -> _Endpoint: + static_uri = resolve_static_uri(params) + if static_uri is None: + raise EndpointResolutionError( + "Unable to resolve endpoint: endpoint_uri is required" + ) + return Endpoint(uri=static_uri) diff --git a/packages/smithy-core/src/smithy_core/identity.py b/packages/smithy-core/src/smithy_core/identity.py new file mode 100644 index 000000000..4a5ebadda --- /dev/null +++ b/packages/smithy-core/src/smithy_core/identity.py @@ -0,0 +1,80 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +import logging +from collections.abc import Mapping, Sequence +from typing import Any, Final + +from ._program import Program, run_program +from .exceptions import SmithyIdentityError +from .interfaces.identity import Identity, IdentityResolver + +__all__ = ["CachingIdentityResolver", "ChainedIdentityResolver"] + +logger: Final = logging.getLogger(__name__) + + +def _resolve_from_chain[I]( + resolvers: Sequence[object], properties: Mapping[str, Any] +) -> Program[I]: + """Resolve an identity from the first resolver in a chain that can provide one.""" + logger.debug("Attempting to resolve identity from resolver chain.") + for resolver in resolvers: + resolve: Any = getattr(resolver, "get_identity") + try: + logger.debug("Attempting to resolve identity from %s.", type(resolver)) + return (yield lambda: resolve(properties=properties)) + except SmithyIdentityError as e: + logger.debug("Failed to resolve identity from %s: %s", type(resolver), e) + raise SmithyIdentityError("Failed to resolve identity from resolver chain.") + + +def _invalidate_all(resolvers: Sequence[object]) -> Program[None]: + for resolver in resolvers: + yield getattr(resolver, "invalidate") + + +class CachingIdentityResolver[I: Identity, IP: Mapping[str, Any]]( + IdentityResolver[I, IP] +): + """An identity resolver that caches an identity until it expires.""" + + def __init__(self) -> None: + self._cached: I | None = None + + def get_identity(self, *, properties: IP) -> I: + if self._cached is None or self._cached.is_expired: + self._cached = self._get_identity(properties=properties) + return self._cached + + def _get_identity(self, *, properties: IP) -> I: + raise NotImplementedError + + def invalidate(self) -> None: + """Discard the cached identity so the next resolution re-reads its source.""" + self._cached = None + + +class ChainedIdentityResolver[I: Identity, IP: Mapping[str, Any]]( + CachingIdentityResolver[I, IP] +): + """Attempts to resolve an identity by checking a sequence of sub-resolvers. + + If a nested resolver raises a :py:class:`SmithyIdentityError`, the next resolver in + the chain will be attempted. + """ + + def __init__(self, resolvers: Sequence[IdentityResolver[I, IP]]) -> None: + """Construct a ChainedIdentityResolver. + + :param resolvers: The sequence of resolvers to resolve identity from. + """ + super().__init__() + self._resolvers = resolvers + + def _get_identity(self, *, properties: IP) -> I: + return run_program(_resolve_from_chain(self._resolvers, properties)) + + def invalidate(self) -> None: + """Discard this chain's cached identity and invalidate every sub-resolver.""" + super().invalidate() + run_program(_invalidate_all(self._resolvers)) diff --git a/packages/smithy-core/src/smithy_core/interfaces/__init__.py b/packages/smithy-core/src/smithy_core/interfaces/__init__.py index 7846cf7ae..1fd7130c2 100644 --- a/packages/smithy-core/src/smithy_core/interfaces/__init__.py +++ b/packages/smithy-core/src/smithy_core/interfaces/__init__.py @@ -3,7 +3,15 @@ from collections.abc import Callable, ItemsView, Iterator, KeysView, ValuesView from dataclasses import dataclass from inspect import iscoroutinefunction -from typing import TYPE_CHECKING, Any, Protocol, TypeGuard, overload, runtime_checkable +from typing import ( + TYPE_CHECKING, + Any, + Literal, + Protocol, + TypeGuard, + overload, + runtime_checkable, +) __all__ = [ "URI", @@ -22,7 +30,13 @@ from typing_extensions import TypeForm from .._schema_compact import StaticSchema - from ..schemas import Schema + from ..deserializers import DeserializeableShape + from ..documents import TypeRegistry + from ..endpoints import EndpointResolverParams + from ..response import ResponseMetadata + from ..schemas import APIOperation, Schema + from ..serializers import SerializeableShape + from ..shapes import ShapeID class URI(Protocol): @@ -282,37 +296,100 @@ class ProtocolSettings: class EndpointResolver(Protocol): - """Resolves an operation's endpoint given parameters. + """Resolves an operation's endpoint based given parameters.""" - Placeholder for the synchronous endpoint resolver; populated alongside the sync - client implementation. - """ + def resolve_endpoint(self, params: "EndpointResolverParams[Any]") -> Endpoint: + """Resolve an endpoint for the given operation. - # TODO(sync-api): implement alongside the sync client + :param params: The parameters available to resolve the endpoint. + """ + ... -class ClientTransport(Protocol): - """Protocol-agnostic representation of a synchronous client transport. +class ClientTransport[I, O](Protocol): + """Protocol-agnostic representation of a synchronous client transport (e.g. an + HTTP client).""" - Placeholder; populated alongside the sync client implementation. - """ + TIMEOUT_EXCEPTIONS: tuple[type[Exception], ...] - # TODO(sync-api): implement alongside the sync client + def send(self, request: I) -> O: + """Send a request over the transport and receive the response.""" + ... -class DuplexClientTransport(Protocol): - """A synchronous client transport that supports duplex event streaming. +class DuplexClientTransport[I, O](ClientTransport[I, O], Protocol): + """A synchronous client transport that supports duplex event streaming.""" - Placeholder; populated alongside the sync client implementation. - """ + SUPPORTS_DUPLEX_STREAMING: Literal[True] - # TODO(sync-api): implement alongside the sync client +class ClientProtocol[I, O](Protocol): + """A protocol used by a synchronous client to communicate with a server.""" -class ClientProtocol(Protocol): - """A protocol used by a synchronous client to communicate with a server. + @property + def id(self) -> "ShapeID": + """The ID of the protocol.""" + ... - Placeholder; populated alongside the sync client implementation. - """ + def serialize_request[ + OperationInput: "SerializeableShape", + OperationOutput: "DeserializeableShape", + ]( + self, + *, + operation: "APIOperation[OperationInput, OperationOutput]", + input: OperationInput, + endpoint: URI, + context: TypedProperties, + ) -> I: + """Serialize an operation input into a transport request. + + :param operation: The operation whose request is being serialized. + :param input: The input shape to be serialized. + :param endpoint: The base endpoint to serialize. + :param context: A context bag for the request. + """ + ... - # TODO(sync-api): implement alongside the sync client + def set_service_endpoint(self, *, request: I, endpoint: Endpoint) -> I: + """Update the endpoint of a transport request. + + :param request: The request whose endpoint should be updated. + :param endpoint: The endpoint to set on the request. + """ + ... + + def deserialize_response[ + OperationInput: "SerializeableShape", + OperationOutput: "DeserializeableShape", + ]( + self, + *, + operation: "APIOperation[OperationInput, OperationOutput]", + request: I, + response: O, + error_registry: "TypeRegistry", + context: TypedProperties, + ) -> OperationOutput: + """Deserializes the output from the tranport response or throws an exception. + + :param operation: The operation whose response is being deserialized. + :param request: The transport request that was sent for this response. + :param response: The response to deserialize. + :param error_registry: A TypeRegistry used to deserialize errors. + :param context: A context bag for the request. + """ + ... + + def extract_response_metadata( + self, *, response: O, context: TypedProperties + ) -> "ResponseMetadata": + """Extract metadata about a transport response. + + Implementations MUST NOT raise: metadata is diagnostic, so any value that + cannot be determined is left unset instead. + + :param response: The response to extract metadata from. + :param context: Per-call storage shared with this protocol's other methods. + """ + ... diff --git a/packages/smithy-core/src/smithy_core/interfaces/auth.py b/packages/smithy-core/src/smithy_core/interfaces/auth.py index fe1c17720..660a45966 100644 --- a/packages/smithy-core/src/smithy_core/interfaces/auth.py +++ b/packages/smithy-core/src/smithy_core/interfaces/auth.py @@ -1,16 +1,24 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -from collections.abc import Sequence +from collections.abc import Mapping, Sequence from typing import TYPE_CHECKING, Any, Protocol from ..shapes import ShapeID from . import TypedProperties +from .identity import Identity if TYPE_CHECKING: from ..auth import AuthParams + from .identity import IdentityResolver -__all__ = ["AuthOption", "AuthSchemeResolver"] +__all__ = [ + "AuthOption", + "AuthScheme", + "AuthSchemeResolver", + "EventSigner", + "Signer", +] class AuthOption(Protocol): @@ -40,31 +48,80 @@ def resolve_auth_scheme( ... -class Signer(Protocol): - """A class that signs requests before they are sent. +class Signer[R, I, SP: Mapping[str, Any]](Protocol): + """A class that signs requests before they are sent.""" - Placeholder for the synchronous signer; populated alongside the sync client - implementation. - """ + def sign(self, *, request: R, identity: I, properties: SP) -> R: + """Get a signed version of the request. - # TODO(sync-api): implement alongside the sync client + :param request: The request to be signed. + :param identity: The identity to use to sign the request. + :param properties: Additional properties used to sign the request. + """ + ... -class EventSigner(Protocol): - """A class that signs event stream events before they are sent. +class EventSigner[I, SP: Mapping[str, Any]](Protocol): + """A class that signs event stream events before they are sent.""" - Placeholder for the synchronous event signer; populated alongside the sync client - implementation. - """ + def sign(self, *, event: Any, identity: I, properties: SP) -> Any: + """Get a signed version of the event. - # TODO(sync-api): implement alongside the sync client + :param event: The event to be signed. + :param identity: The identity to use to sign the event. + :param properties: Additional properties used to sign the event. + """ + ... + def sign_empty(self, *, event: Any, identity: I, properties: SP) -> Any: + """Get a signed version of an empty event. + + :param event: The empty event to be signed. + :param identity: The identity to use to sign the event. + :param properties: Additional properties used to sign the event. + """ + ... -class AuthScheme(Protocol): - """A class that coordinates identity and auth. - Placeholder for the synchronous auth scheme; populated alongside the sync client - implementation. - """ +class AuthScheme[R, I: Identity, IP: Mapping[str, Any], SP: Mapping[str, Any]]( + Protocol +): + """A class that coordinates identity and auth.""" + + scheme_id: ShapeID + """The ID of the auth scheme.""" + + def identity_properties(self, *, context: TypedProperties) -> IP: + """Construct identity properties from the request context. + + :param context: The context of the request. + """ + ... - # TODO(sync-api): implement alongside the sync client + def identity_resolver( + self, *, context: TypedProperties + ) -> "IdentityResolver[I, IP]": + """Get an identity resolver for the request. + + :param context: The context of the request. + """ + ... + + def signer_properties(self, *, context: TypedProperties) -> SP: + """Construct signer properties from the request context. + + :param context: The context of the request. + """ + ... + + def signer(self) -> Signer[R, I, SP]: + """Get a signer for the request.""" + ... + + def event_signer(self, *, request: R) -> EventSigner[I, SP] | None: + """Construct a signer for event stream events. + + :param request: The request that will initiate the event stream. + :returns: An event signer if the scheme supports signing events, otherwise None. + """ + return None diff --git a/packages/smithy-core/src/smithy_core/interfaces/eventstream.py b/packages/smithy-core/src/smithy_core/interfaces/eventstream.py index 039628577..ee26f0c62 100644 --- a/packages/smithy-core/src/smithy_core/interfaces/eventstream.py +++ b/packages/smithy-core/src/smithy_core/interfaces/eventstream.py @@ -1,23 +1,69 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -from typing import Protocol +from typing import Any, Protocol, Self +from ..deserializers import DeserializeableShape +from ..serializers import SerializeableShape -class EventPublisher(Protocol): +__all__ = ["EventPublisher", "EventReceiver"] + + +class EventPublisher[E: SerializeableShape](Protocol): """Sends events to a service. - Placeholder for the synchronous event publisher; populated alongside the sync - client implementation. + This may be used as a context manager to ensure the stream is closed before exiting. """ - # TODO(sync-api): implement alongside the sync client + def send(self, event: E) -> None: + """Sends an event to the service. + + :param event: The event to send. + """ + ... + + def close(self) -> None: + """Closes the event stream.""" + ... + + def __enter__(self) -> Self: + return self + + def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None: + self.close() -class EventReceiver(Protocol): +class EventReceiver[E: DeserializeableShape](Protocol): """Receives events from a service. - Placeholder for the synchronous event receiver; populated alongside the sync - client implementation. + Events may be received via the ``receive`` method or by using this class as an + iterable. This may also be used as a context manager to ensure the stream is closed + before exiting. """ - # TODO(sync-api): implement alongside the sync client + def receive(self) -> E | None: + """Receive a single event from the service. + + :returns: An event or None. None indicates that no more events will be sent by + the service. + """ + ... + + def close(self) -> None: + """Closes the event stream.""" + ... + + def __next__(self) -> E: + result = self.receive() + if result is None: + self.close() + raise StopIteration + return result + + def __iter__(self) -> Self: + return self + + def __enter__(self) -> Self: + return self + + def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None: + self.close() diff --git a/packages/smithy-core/src/smithy_core/interfaces/identity.py b/packages/smithy-core/src/smithy_core/interfaces/identity.py index 202ca675e..e5e3aac42 100644 --- a/packages/smithy-core/src/smithy_core/interfaces/identity.py +++ b/packages/smithy-core/src/smithy_core/interfaces/identity.py @@ -32,14 +32,13 @@ def is_expired(self) -> bool: class IdentityResolver[I: Identity, IP: Mapping[str, Any]](Protocol): - """Loads a user's :py:class:`Identity` from a given source. - - Placeholder for the synchronous identity resolver; populated alongside the sync - client implementation. - """ + """Loads a user's :py:class:`Identity` from a given source.""" def get_identity(self, *, properties: IP) -> I: - # TODO(sync-api): implement alongside the sync client + """Load the user's identity from this resolver. + + :param properties: Properties used to help determine the identity to return. + """ ... def invalidate(self) -> None: diff --git a/packages/smithy-core/src/smithy_core/interfaces/retries.py b/packages/smithy-core/src/smithy_core/interfaces/retries.py index 1a93ddb01..7e4182f0d 100644 --- a/packages/smithy-core/src/smithy_core/interfaces/retries.py +++ b/packages/smithy-core/src/smithy_core/interfaces/retries.py @@ -3,7 +3,12 @@ from dataclasses import dataclass from typing import Protocol, runtime_checkable -__all__ = ["ErrorRetryInfo", "RetryBackoffStrategy", "RetryToken"] +__all__ = [ + "ErrorRetryInfo", + "RetryBackoffStrategy", + "RetryStrategy", + "RetryToken", +] @runtime_checkable @@ -36,13 +41,42 @@ class ErrorRetryInfo(Protocol): @runtime_checkable class RetryStrategy(Protocol): - """Issuer of :py:class:`RetryToken`s. + """Issuer of :py:class:`RetryToken`s.""" - Placeholder for the synchronous retry-strategy interface; populated alongside the - sync retry implementations. - """ + backoff_strategy: "RetryBackoffStrategy" + """The strategy used by returned tokens to compute delay duration values.""" + + max_attempts: int + """Upper limit on total attempt count (initial attempt plus retries).""" + + def acquire_initial_retry_token( + self, *, token_scope: str | None = None + ) -> "RetryToken": + """Create a base retry token for the start of a request. + + :param token_scope: An arbitrary string accepted by the retry strategy to + separate tokens into scopes. + :raises RetryError: If the retry strategy has no available tokens. + """ + ... + + def refresh_retry_token_for_retry( + self, *, token_to_renew: "RetryToken", error: Exception + ) -> "RetryToken": + """Replace an existing retry token from a failed attempt with a new token. - # TODO(sync-api): implement alongside the sync client + :param token_to_renew: The token used for the previous failed attempt. + :param error: The error that triggered the need for a retry. + :raises RetryError: If no further retry attempts are allowed. + """ + ... + + def record_success(self, *, token: "RetryToken") -> None: + """Return token after successful completion of an operation. + + :param token: The token used for the previous successful attempt. + """ + ... class RetryBackoffStrategy(Protocol): @@ -60,7 +94,7 @@ def compute_next_backoff_delay(self, retry_attempt: int) -> float: @dataclass(kw_only=True) class RetryToken(Protocol): - """Token issued by a :py:class:`smithy_core.aio.interfaces.retries.AsyncRetryStrategy` for the next attempt.""" + """Token issued by a retry strategy for the next attempt.""" retry_count: int """Retry count is the total number of attempts minus the initial attempt.""" diff --git a/packages/smithy-core/src/smithy_core/retries.py b/packages/smithy-core/src/smithy_core/retries.py index ce58c5688..1b9a90a86 100644 --- a/packages/smithy-core/src/smithy_core/retries.py +++ b/packages/smithy-core/src/smithy_core/retries.py @@ -1,25 +1,33 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 +import math import random import threading import time from collections.abc import Callable from dataclasses import dataclass from enum import Enum -from typing import Literal +from typing import Any, ClassVar, Literal, Self from .exceptions import RetryError from .interfaces import retries as retries_interface __all__ = [ + "AdaptiveRetryStrategy", + "ClientRateLimiter", "CubicCalculator", "ExponentialBackoffJitterType", "ExponentialRetryBackoffStrategy", + "RequestRateTracker", "RetryStrategyOptions", + "RetryStrategyResolver", "RetryStrategyType", + "SimpleRetryStrategy", "SimpleRetryToken", "StandardRetryQuota", + "StandardRetryStrategy", "StandardRetryToken", + "TokenBucket", ] RetryStrategyType = Literal["simple", "standard", "adaptive"] @@ -367,3 +375,675 @@ def last_max_rate(self) -> float: @property def last_throttle_time(self) -> float: return self._last_throttle_time + + +class _RetryStrategyResolverBase[S]: + """Caches retry strategies by configuration, one cache per client.""" + + _STRATEGY_TYPES: ClassVar[dict[RetryStrategyType, Any]] + + def __init__(self) -> None: + self._strategies: dict[tuple[RetryStrategyType, int | None], S] = {} + + def _resolve( + self, + *, + retry_strategy: object, + retry_mode: RetryStrategyType | None, + max_attempts: int | None, + strategy_protocol: type, + ) -> S: + if isinstance(retry_strategy, strategy_protocol): + return retry_strategy # type: ignore[return-value] + elif retry_strategy is None: + # Fall back to the separately-resolved config values. + retry_strategy = RetryStrategyOptions( + retry_mode=retry_mode if retry_mode is not None else "standard", + max_attempts=max_attempts, + ) + elif not isinstance(retry_strategy, RetryStrategyOptions): + raise TypeError( + f"retry_strategy must be {strategy_protocol.__name__}, " + f"RetryStrategyOptions, or None, got {type(retry_strategy).__name__}" + ) + return self._create_retry_strategy( + retry_strategy.retry_mode, retry_strategy.max_attempts + ) + + def _create_retry_strategy( + self, retry_mode: RetryStrategyType, max_attempts: int | None + ) -> S: + key = (retry_mode, max_attempts) + if key not in self._strategies: + self._strategies[key] = self._new_retry_strategy(retry_mode, max_attempts) + return self._strategies[key] + + def _new_retry_strategy( + self, retry_mode: RetryStrategyType, max_attempts: int | None + ) -> S: + kwargs: dict[str, Any] = {} + if max_attempts is not None: + kwargs["max_attempts"] = max_attempts + strategy_type = self._STRATEGY_TYPES.get(retry_mode) + if strategy_type is None: + raise ValueError(f"Unknown retry mode: {retry_mode}") + return strategy_type(**kwargs) + + +class _SimpleRetryStrategyBase: + """Retry strategy that simply invokes the given backoff strategy.""" + + def __init__( + self, + *, + backoff_strategy: retries_interface.RetryBackoffStrategy | None = None, + max_attempts: int = 5, + ): + """Retry strategy that simply invokes the given backoff strategy. + + :param backoff_strategy: The backoff strategy used by returned tokens to compute + the retry delay. Defaults to :py:class:`ExponentialRetryBackoffStrategy`. + + :param max_attempts: Upper limit on total number of attempts made, including + initial attempt and retries. + """ + self.backoff_strategy = backoff_strategy or ExponentialRetryBackoffStrategy() + self.max_attempts = max_attempts + + def _initial_token(self) -> SimpleRetryToken: + retry_delay = self.backoff_strategy.compute_next_backoff_delay(0) + return SimpleRetryToken(retry_count=0, retry_delay=retry_delay) + + def _refresh_token( + self, token_to_renew: retries_interface.RetryToken, error: Exception + ) -> SimpleRetryToken: + if isinstance(error, retries_interface.ErrorRetryInfo) and error.is_retry_safe: + retry_count = token_to_renew.retry_count + 1 + if retry_count >= self.max_attempts: + raise RetryError( + f"Reached maximum number of allowed attempts: {self.max_attempts}" + ) from error + retry_delay = self.backoff_strategy.compute_next_backoff_delay(retry_count) + return SimpleRetryToken(retry_count=retry_count, retry_delay=retry_delay) + else: + raise RetryError(f"Error is not retryable: {error}") from error + + def __deepcopy__(self, memo: Any) -> Self: + return self + + +class _StandardRetryStrategyBase: + """Standard retry strategy using truncated binary exponential backoff with full + jitter.""" + + _RETRY_AFTER_MAX_ADDITIONAL: float = 5 + """Upper bound (seconds) for additional delay beyond the computed backoff.""" + + _NON_THROTTLING_BACKOFF_SCALE: float = 0.05 + """Base backoff scale (seconds) for non-throttling errors (50ms).""" + + _THROTTLING_BACKOFF_SCALE: float = 1 + """Base backoff scale (seconds) for throttling errors (1000ms).""" + + _MAX_BACKOFF: float = 20 + """Upper bound (seconds) for the computed backoff, applied before jitter.""" + + def __init__( + self, + *, + backoff_strategy: retries_interface.RetryBackoffStrategy | None = None, + throttling_backoff_strategy: retries_interface.RetryBackoffStrategy + | None = None, + max_attempts: int = 3, + retry_quota: StandardRetryQuota | None = None, + ): + """Standard retry strategy using truncated binary exponential backoff + with full jitter. + + :param backoff_strategy: The backoff strategy used to compute the retry delay + for non-throttling errors. Defaults to a 50ms-base + :py:class:`ExponentialRetryBackoffStrategy`. + + :param throttling_backoff_strategy: The backoff strategy used to compute the + retry delay for throttling errors. Defaults to a 1000ms-base + :py:class:`ExponentialRetryBackoffStrategy`. + + :param max_attempts: Upper limit on total number of attempts made, including + initial attempt and retries. + + :param retry_quota: The retry quota to use for managing retry capacity. Defaults + to a new :py:class:`StandardRetryQuota` instance. + """ + if max_attempts < 0: + raise ValueError( + f"max_attempts must be a non-negative integer, got {max_attempts}" + ) + + self.backoff_strategy = backoff_strategy or ExponentialRetryBackoffStrategy( + backoff_scale_value=self._NON_THROTTLING_BACKOFF_SCALE, + max_backoff=self._MAX_BACKOFF, + jitter_type=ExponentialBackoffJitterType.FULL, + ) + self.throttling_backoff_strategy = ( + throttling_backoff_strategy + or ExponentialRetryBackoffStrategy( + backoff_scale_value=self._THROTTLING_BACKOFF_SCALE, + max_backoff=self._MAX_BACKOFF, + jitter_type=ExponentialBackoffJitterType.FULL, + ) + ) + self.max_attempts = max_attempts + self._retry_quota = retry_quota or StandardRetryQuota() + + def _initial_token(self) -> StandardRetryToken: + retry_delay = self.backoff_strategy.compute_next_backoff_delay(0) + return StandardRetryToken(retry_count=0, retry_delay=retry_delay) + + def _refresh_token( + self, token_to_renew: retries_interface.RetryToken, error: Exception + ) -> StandardRetryToken: + if not isinstance(token_to_renew, StandardRetryToken): + raise TypeError( + f"{type(self).__name__} requires StandardRetryToken, got {type(token_to_renew).__name__}" + ) + + if isinstance(error, retries_interface.ErrorRetryInfo) and error.is_retry_safe: + retry_count = token_to_renew.retry_count + 1 + if retry_count >= self.max_attempts: + raise RetryError( + f"Reached maximum number of allowed attempts: {self.max_attempts}" + ) from error + + # Throttling errors use a larger base backoff than other errors. + backoff_strategy = ( + self.throttling_backoff_strategy + if error.is_throttling_error + else self.backoff_strategy + ) + t_i = backoff_strategy.compute_next_backoff_delay(retry_count) + + if error.retry_after is not None: + # Bound a server-directed backoff to [t_i, t_i + 5] seconds. + retry_delay = max( + t_i, min(error.retry_after, self._RETRY_AFTER_MAX_ADDITIONAL + t_i) + ) + else: + retry_delay = t_i + + try: + quota_acquired = self._retry_quota.acquire(error=error) + except RetryError as quota_error: + # Surface the computed delay so callers can back off before giving + # up; long-polling operations sleep for it before returning. + raise RetryError(str(quota_error), retry_after=retry_delay) from error + + return StandardRetryToken( + retry_count=retry_count, + retry_delay=retry_delay, + quota_acquired=quota_acquired, + ) + else: + raise RetryError(f"Error is not retryable: {error}") from error + + def _release_quota(self, token: retries_interface.RetryToken) -> None: + if not isinstance(token, StandardRetryToken): + raise TypeError( + f"{type(self).__name__} requires StandardRetryToken, got {type(token).__name__}" + ) + self._retry_quota.release(release_amount=token.quota_acquired) + + def __deepcopy__(self, memo: Any) -> Self: + return self + + +class _TokenBucketBase: + """Token bucket state for rate limiting with configurable fill rate. + + A threading.Lock guards the token count and rate. The lock only covers short + arithmetic and is never held while waiting. + """ + + MIN_FILL_RATE = 0.5 # Minimum allowed fill rate (0.5 tokens/second) + MIN_CAPACITY = 1.0 # Minimum allowed bucket capacity (1.0 tokens) + DEFAULT_TIMEOUT = 30.0 # Default timeout for token acquisition (30.0 seconds) + + def __init__( + self, + *, + curr_capacity: float = 0, + fill_rate: float = MIN_FILL_RATE, + timeout: float = DEFAULT_TIMEOUT, + ): + """Initialize a new token bucket. + + :param curr_capacity: Initial number of tokens in the bucket. The bucket can + hold at least MIN_CAPACITY tokens, but starts empty by default. + :param fill_rate: Rate at which tokens are added to the bucket (tokens/second). + :param timeout: Maximum time to wait for token acquisition before + raising TimeoutError. + """ + self._curr_capacity: float = curr_capacity + self._max_capacity: float = max(curr_capacity, self.MIN_CAPACITY) + self._fill_rate: float = max(fill_rate, self.MIN_FILL_RATE) + self._timeout = timeout + self._last_timestamp: float = time.monotonic() + self._lock = threading.Lock() + + def _monotonic(self) -> float: + return time.monotonic() + + def _try_acquire(self, amount: float, start_time: float) -> float | None: + """Take ``amount`` tokens, or return how long to wait before trying again. + + :raises TimeoutError: Acquisition took longer than the configured timeout. + """ + with self._lock: + self._refill() + if self._curr_capacity >= amount: + self._curr_capacity -= amount + return None + wait_time = (amount - self._curr_capacity) / self._fill_rate + + elapsed = time.monotonic() - start_time + if elapsed >= self._timeout: + # Fails the request; this is not counted as a retry attempt + raise TimeoutError( + f"Failed to acquire {amount} tokens within {self._timeout}s" + ) + # Never wait past the timeout + return min(wait_time, self._timeout - elapsed) + + def _refill(self) -> None: + curr_time = time.monotonic() + elapsed = curr_time - self._last_timestamp + refill_amount = elapsed * self._fill_rate + self._curr_capacity = min( + self._max_capacity, self._curr_capacity + refill_amount + ) + self._last_timestamp = curr_time + + def _set_rate(self, rate: float) -> None: + with self._lock: + self._refill() + self._fill_rate = max(rate, self.MIN_FILL_RATE) + self._max_capacity = max(rate, self.MIN_CAPACITY) + self._curr_capacity = min(self._curr_capacity, self._max_capacity) + + @property + def current_capacity(self) -> float: + """Get the current number of tokens in the bucket. + + :return: The current token count as of the last refill operation. + """ + return self._curr_capacity + + @property + def max_capacity(self) -> float: + """Get the maximum capacity of the bucket. + + :return: The maximum number of tokens the bucket can hold. + """ + return self._max_capacity + + @property + def fill_rate(self) -> float: + """Get the current fill rate of the bucket. + + :return: The rate at which tokens are added to the bucket (tokens/second). + """ + return self._fill_rate + + +class _RequestRateTrackerBase: + """Tracks the client's request sending rate. + + Uses time-bucketed sampling with exponential smoothing, tracking requests in + half-second intervals by default. + """ + + _DEFAULT_SMOOTHING = 0.8 + _TIME_BUCKET_RANGE = 0.5 + + def __init__( + self, + smoothing: float = _DEFAULT_SMOOTHING, + time_bucket_range: float = _TIME_BUCKET_RANGE, + ): + """Initialize a new request rate tracker. + + :param smoothing: Exponential smoothing factor. This constant + represents how much weight is given to recent measurements. + Higher values place more emphasis on the most recent observations. + :param time_bucket_range: Time bucket duration in seconds. Rate + calculations are updated when transitioning between buckets. + """ + self._smoothing = smoothing + self._time_bucket_scale = 1 / time_bucket_range + self._request_count = 0 + self._last_calculated_time_bucket = math.floor(time.monotonic()) + self._measured_rate = 0 + # The update never waits, so it only needs protecting across threads + self._lock = threading.Lock() + + def _measure(self) -> float: + with self._lock: + curr_time = time.monotonic() + current_time_bucket = ( + math.floor(curr_time * self._time_bucket_scale) + / self._time_bucket_scale + ) + self._request_count += 1 + if current_time_bucket > self._last_calculated_time_bucket: + current_rate = self._request_count / ( + current_time_bucket - self._last_calculated_time_bucket + ) + self._measured_rate = (current_rate * self._smoothing) + ( + self._measured_rate * (1 - self._smoothing) + ) + self._request_count = 0 + self._last_calculated_time_bucket = current_time_bucket + return self._measured_rate + + @property + def request_count(self) -> int: + """Get current request count. For testing only.""" + return self._request_count + + @property + def measured_client_rate(self) -> float: + """Get the client's sending rate. For testing only.""" + return self._measured_rate + + +class _ClientRateLimiterBase[B: _TokenBucketBase, T: _RequestRateTrackerBase]: + """Adaptive rate limiting using a token bucket and the CUBIC algorithm.""" + + _REQUEST_COST = 1.0 + + def __init__( + self, + token_bucket: B, + cubic_calculator: CubicCalculator, + rate_tracker: T, + rate_limiter_enabled: bool, + ): + """Initialize a new rate limiter. + + :param token_bucket: Token bucket for controlling request sending rates. + :param cubic_calculator: CUBIC algorithm calculator for rate adjustments. + :param rate_tracker: Tracker for measuring actual client request rates. + :param rate_limiter_enabled: Whether rate limiting is enabled. + """ + + self._rate_tracker = rate_tracker + self._token_bucket = token_bucket + self._cubic_calculator = cubic_calculator + self._rate_limiter_enabled = rate_limiter_enabled + self._lock = threading.Lock() + + def _update_rate(self, measured_rate: float, throttling_error: bool) -> None: + """Reduce the rate by 30% when throttled, or scale it up with CUBIC otherwise, + capped at 2x the measured client rate.""" + with self._lock: + timestamp = time.monotonic() + if throttling_error: + if not self._rate_limiter_enabled: + rate_to_use = measured_rate + else: + fill_rate = self._token_bucket.fill_rate + rate_to_use = min(measured_rate, fill_rate) + + cubic_calculated_rate = ( + self._cubic_calculator.calculate_throttled_request_rate( + rate_to_use, timestamp + ) + ) + self._rate_limiter_enabled = True + else: + cubic_calculated_rate = ( + self._cubic_calculator.calculate_scaled_request_rate(timestamp) + ) + + new_rate = min(cubic_calculated_rate, 2 * measured_rate) + # Set under the lock so rates reach the bucket in the order they're + # calculated. Waking waiters happens after the lock. + self._token_bucket._set_rate(new_rate) # pyright: ignore[reportPrivateUsage] + + @property + def rate_limit_enabled(self) -> bool: + return self._rate_limiter_enabled + + +class _AdaptiveRetryStrategyBase(_StandardRetryStrategyBase): + """Standard retry strategy with client-side rate limiting.""" + + STARTING_MAX_RATE = 0 + + def is_throttling_error(self, error: Exception) -> bool: + """Check if error is a throttling error using existing ErrorRetryInfo.""" + if isinstance(error, retries_interface.ErrorRetryInfo): + return error.is_throttling_error + return False + + def _new_cubic_calculator(self) -> CubicCalculator: + return CubicCalculator( + starting_max_rate=self.STARTING_MAX_RATE, start_time=time.monotonic() + ) + + +class SimpleRetryStrategy(_SimpleRetryStrategyBase): + """Retry strategy that simply invokes the given backoff strategy.""" + + def acquire_initial_retry_token( + self, *, token_scope: str | None = None + ) -> SimpleRetryToken: + """Create a base retry token for the start of a request. + + :param token_scope: This argument is ignored by this retry strategy. + """ + return self._initial_token() + + def refresh_retry_token_for_retry( + self, *, token_to_renew: retries_interface.RetryToken, error: Exception + ) -> SimpleRetryToken: + """Replace an existing retry token from a failed attempt with a new token. + + :param token_to_renew: The token used for the previous failed attempt. + :param error: The error that triggered the need for a retry. + :raises RetryError: If no further retry attempts are allowed. + """ + return self._refresh_token(token_to_renew, error) + + def record_success(self, *, token: retries_interface.RetryToken) -> None: + """Not used by this retry strategy.""" + + +class StandardRetryStrategy(_StandardRetryStrategyBase): + """Standard retry strategy using truncated binary exponential backoff with full + jitter.""" + + def acquire_initial_retry_token( + self, *, token_scope: str | None = None + ) -> StandardRetryToken: + """Create a base retry token for the start of a request. + + :param token_scope: This argument is ignored by this retry strategy. + """ + return self._initial_token() + + def refresh_retry_token_for_retry( + self, *, token_to_renew: retries_interface.RetryToken, error: Exception + ) -> StandardRetryToken: + """Replace an existing retry token from a failed attempt with a new token. + + :param token_to_renew: The token used for the previous failed attempt. + :param error: The error that triggered the need for a retry. + :raises RetryError: If no further retry attempts are allowed. When the retry + quota is exhausted, the raised error carries ``retry_after``. + """ + return self._refresh_token(token_to_renew, error) + + def record_success(self, *, token: retries_interface.RetryToken) -> None: + """Release retry quota back based on the amount consumed by the last retry. + + :param token: The token used for the previous successful attempt. + """ + self._release_quota(token) + + +class TokenBucket(_TokenBucketBase): + """Token bucket for rate limiting with configurable fill rate. + + When tokens aren't available, acquiring blocks the calling thread until enough + tokens are refilled or the timeout is reached. + """ + + def __init__( + self, + *, + curr_capacity: float = 0, + fill_rate: float = _TokenBucketBase.MIN_FILL_RATE, + timeout: float = _TokenBucketBase.DEFAULT_TIMEOUT, + ): + """Initialize a new TokenBucket. + + :param curr_capacity: Initial number of tokens in the bucket. + :param fill_rate: Rate at which tokens are added to the bucket (tokens/second). + :param timeout: Maximum time to wait for token acquisition before + raising TimeoutError. + """ + super().__init__( + curr_capacity=curr_capacity, fill_rate=fill_rate, timeout=timeout + ) + self._condition = threading.Condition() + + def acquire(self, amount: float) -> None: + """Acquire tokens from the bucket, blocking until they are available. + + :param amount: Number of tokens to acquire. + :raises TimeoutError: Acquisition took longer than the configured timeout. + """ + start_time = self._monotonic() + with self._condition: + while (wait := self._try_acquire(amount, start_time)) is not None: + self._condition.wait(timeout=wait) + + def update_rate(self, rate: float) -> None: + """Update the bucket's fill rate, maximum capacity and current capacity. + + :param rate: New fill rate (tokens/second). It won't be less than MIN_FILL_RATE. + """ + self._set_rate(rate) + self._notify_waiters() + + def _notify_waiters(self) -> None: + with self._condition: + self._condition.notify_all() + + +class RequestRateTracker(_RequestRateTrackerBase): + """Tracks the client's request sending rate.""" + + def measure_rate(self) -> float: + """Measure and return the current request rate in requests per second.""" + return self._measure() + + +class ClientRateLimiter(_ClientRateLimiterBase[TokenBucket, RequestRateTracker]): + """Limits the rate of requests using a token bucket and the CUBIC algorithm.""" + + def before_sending_request(self) -> None: + """Acquire a token before making a request.""" + if self._rate_limiter_enabled: + self._token_bucket.acquire(self._REQUEST_COST) + + def after_receiving_response(self, throttling_error: bool) -> None: + """Update the request rate based on the response using CUBIC algorithm. + + :param throttling_error: True if the response was a throttling error. + """ + self._update_rate(self._rate_tracker.measure_rate(), throttling_error) + self._token_bucket._notify_waiters() # pyright: ignore[reportPrivateUsage] + + +class AdaptiveRetryStrategy(_AdaptiveRetryStrategyBase, StandardRetryStrategy): + """Adaptive retry strategy with client-side rate limiting using CUBIC algorithm.""" + + def __init__( + self, + *, + backoff_strategy: retries_interface.RetryBackoffStrategy | None = None, + throttling_backoff_strategy: retries_interface.RetryBackoffStrategy + | None = None, + max_attempts: int = 3, + retry_quota: StandardRetryQuota | None = None, + rate_limiter: ClientRateLimiter | None = None, + ): + """Initialize AdaptiveRetryStrategy. + + :param backoff_strategy: The backoff strategy used to compute the retry delay + for non-throttling errors. + :param throttling_backoff_strategy: The backoff strategy used to compute the + retry delay for throttling errors. + :param max_attempts: Upper limit on total number of attempts made, including + initial attempt and retries. + :param retry_quota: The retry quota to use for managing retry capacity. + :param rate_limiter: Optional pre-configured rate limiter. If None, creates + default components with rate limiting initially disabled. + """ + super().__init__( + backoff_strategy=backoff_strategy, + throttling_backoff_strategy=throttling_backoff_strategy, + max_attempts=max_attempts, + retry_quota=retry_quota, + ) + self._rate_limiter = rate_limiter or ClientRateLimiter( + token_bucket=TokenBucket(), + cubic_calculator=self._new_cubic_calculator(), + rate_tracker=RequestRateTracker(), + rate_limiter_enabled=False, # Disabled until first throttle + ) + + @property + def rate_limiter(self) -> ClientRateLimiter: + """Get the rate limiter for integration with request pipeline.""" + return self._rate_limiter + + def acquire_from_token_bucket(self) -> None: + if self._rate_limiter.rate_limit_enabled: + self._rate_limiter.before_sending_request() + + +class RetryStrategyResolver( + _RetryStrategyResolverBase[retries_interface.RetryStrategy] +): + """Retry strategy resolver that caches retry strategies based on configuration + options, one cache per client.""" + + _STRATEGY_TYPES: ClassVar[dict[RetryStrategyType, Any]] = { + "simple": SimpleRetryStrategy, + "standard": StandardRetryStrategy, + "adaptive": AdaptiveRetryStrategy, + } + + def resolve_retry_strategy( + self, + *, + retry_strategy: retries_interface.RetryStrategy | RetryStrategyOptions | None, + retry_mode: RetryStrategyType | None = None, + max_attempts: int | None = None, + ) -> retries_interface.RetryStrategy: + """Resolve a retry strategy from the provided options, using cache when possible. + + :param retry_strategy: An explicitly configured retry strategy or options for + creating one. Takes precedence over ``retry_mode``/``max_attempts``. + :param retry_mode: Retry mode to fall back on when ``retry_strategy`` is None. + :param max_attempts: Maximum attempts to fall back on when ``retry_strategy`` is + None. + """ + return self._resolve( + retry_strategy=retry_strategy, + retry_mode=retry_mode, + max_attempts=max_attempts, + strategy_protocol=retries_interface.RetryStrategy, + ) diff --git a/packages/smithy-core/tests/functional/test_retries.py b/packages/smithy-core/tests/functional/test_retries.py index b61df8363..5220dc1a7 100644 --- a/packages/smithy-core/tests/functional/test_retries.py +++ b/packages/smithy-core/tests/functional/test_retries.py @@ -2,12 +2,13 @@ # SPDX-License-Identifier: Apache-2.0 from asyncio import gather, sleep, wait_for -from collections.abc import Coroutine +from collections.abc import Coroutine, Generator from types import SimpleNamespace from typing import Any from unittest.mock import AsyncMock, MagicMock, patch import pytest +from smithy_core.aio._program import run_program_async from smithy_core.aio.client import AsyncRequestPipeline from smithy_core.aio.interfaces import retries as retries_interface from smithy_core.aio.retries import ( @@ -163,6 +164,15 @@ async def test_retry_quota_handles_timeout_errors(): assert quota.available_capacity == 486 +def _program_of(mock: Any) -> Any: + """Stand in for a pipeline program step with an awaitable mock.""" + + def program(*args: Any, **kwargs: Any) -> Generator[Any, Any, Any]: + return (yield lambda: mock(*args, **kwargs)) + + return program + + class FakeClock: """Simulated time for the adaptive retry pipeline. @@ -198,7 +208,7 @@ def clock(self): clock = FakeClock() with ( patch( - "smithy_core.aio.retries.time", + "smithy_core.retries.time", SimpleNamespace(monotonic=clock.monotonic), ), patch("smithy_core.aio.client.sleep", clock.sleep), @@ -284,22 +294,26 @@ async def handle_attempt(*args: Any) -> SimpleNamespace: transport_response=MagicMock(), response=self._response(status) ) - original_pre_request = pipeline._handle_pre_request_rate_limiting # type: ignore[reportPrivateUsage] + original_acquire = strategy.acquire_from_token_bucket - async def pre_request(retry_strategy: Any) -> None: + async def acquire() -> None: nonlocal attempt_start attempt_start = clock.now - await original_pre_request(retry_strategy) + await original_acquire() with ( patch.object( - pipeline, "_handle_attempt", AsyncMock(side_effect=handle_attempt) + pipeline, + "_handle_attempt", + _program_of(AsyncMock(side_effect=handle_attempt)), ), - patch.object(pipeline, "_handle_pre_request_rate_limiting", pre_request), + patch.object(strategy, "acquire_from_token_bucket", acquire), ): try: - await pipeline._retry( # type: ignore[reportPrivateUsage] - self._call(strategy), self._request_context(), None + await run_program_async( + pipeline._retry( # type: ignore[reportPrivateUsage] + self._call(strategy), self._request_context(), None + ) ) except CallError: pass @@ -348,12 +362,16 @@ async def handle_attempt(*args: Any) -> SimpleNamespace: ) with patch.object( - pipeline, "_handle_attempt", AsyncMock(side_effect=handle_attempt) + pipeline, + "_handle_attempt", + _program_of(AsyncMock(side_effect=handle_attempt)), ): while clock.now < duration: try: - await pipeline._retry( # type: ignore[reportPrivateUsage] - self._call(strategy), self._request_context(), None + await run_program_async( + pipeline._retry( # type: ignore[reportPrivateUsage] + self._call(strategy), self._request_context(), None + ) ) except CallError: pass @@ -398,7 +416,7 @@ def _pipeline_with_429_attempts( ) ) patches = ( - patch.object(pipeline, "_handle_attempt", attempt), + patch.object(pipeline, "_handle_attempt", _program_of(attempt)), patch.object( strategy, "acquire_from_token_bucket", @@ -417,8 +435,10 @@ async def test_send_token_timeout_on_first_attempt_raises(self): with p1, p2, p3: with pytest.raises(TimeoutError, match="Failed to acquire"): - await pipeline._retry( # type: ignore[reportPrivateUsage] - self._call(strategy), self._request_context(), None + await run_program_async( + pipeline._retry( # type: ignore[reportPrivateUsage] + self._call(strategy), self._request_context(), None + ) ) # Nothing was sent, so no retry quota was spent diff --git a/packages/smithy-core/tests/unit/test_retries.py b/packages/smithy-core/tests/unit/test_retries.py index b51984a98..78435483f 100644 --- a/packages/smithy-core/tests/unit/test_retries.py +++ b/packages/smithy-core/tests/unit/test_retries.py @@ -4,13 +4,14 @@ import sys import threading import time -from collections.abc import Callable, Coroutine, Iterator +from collections.abc import Callable, Coroutine, Generator, Iterator from copy import deepcopy from types import SimpleNamespace from typing import Any from unittest.mock import AsyncMock, MagicMock, patch import pytest +from smithy_core.aio._program import run_program_async from smithy_core.aio.client import AsyncRequestPipeline from smithy_core.aio.retries import ( AsyncAdaptiveRetryStrategy, @@ -29,6 +30,15 @@ from smithy_core.retries import ExponentialBackoffJitterType as EBJT +def _program_of(mock: Any) -> Any: + """Stand in for a pipeline program step with an awaitable mock.""" + + def program(*args: Any, **kwargs: Any) -> Generator[Any, Any, Any]: + return (yield lambda: mock(*args, **kwargs)) + + return program + + @pytest.mark.parametrize( "jitter_type, scale_value, max_backoff, expected_delays", [ @@ -494,9 +504,7 @@ async def test_sending_rate_sequence(self): (False, 3.4, 4.333056, 3.452226), ] now = 0.0 - with patch( - "smithy_core.aio.retries.time", SimpleNamespace(monotonic=lambda: now) - ): + with patch("smithy_core.retries.time", SimpleNamespace(monotonic=lambda: now)): token_bucket = AsyncTokenBucket() tracker = AsyncRequestRateTracker() limiter = AsyncClientRateLimiter( @@ -751,9 +759,11 @@ async def test_non_retryable_call_applies_rate_limiting_on_success(self): return_value=SimpleNamespace(transport_response=MagicMock(), response="ok") ) - with patch.object(pipeline, "_handle_attempt", handle_attempt): - await pipeline._retry( # type: ignore[reportPrivateUsage] - self._non_retryable_call(strategy), MagicMock(), None + with patch.object(pipeline, "_handle_attempt", _program_of(handle_attempt)): + await run_program_async( + pipeline._retry( # type: ignore[reportPrivateUsage] + self._non_retryable_call(strategy), MagicMock(), None + ) ) handle_attempt.assert_awaited_once() @@ -773,9 +783,11 @@ async def test_non_retryable_call_reports_throttle_without_retrying(self): return_value=SimpleNamespace(transport_response=MagicMock(), response=error) ) - with patch.object(pipeline, "_handle_attempt", handle_attempt): - output_context = await pipeline._retry( # type: ignore[reportPrivateUsage] - self._non_retryable_call(strategy), MagicMock(), None + with patch.object(pipeline, "_handle_attempt", _program_of(handle_attempt)): + output_context = await run_program_async( + pipeline._retry( # type: ignore[reportPrivateUsage] + self._non_retryable_call(strategy), MagicMock(), None + ) ) # Sent once and returned as-is, even though the error is retry-safe @@ -796,9 +808,11 @@ async def test_failure_without_response_does_not_update_rate_limiter(self): return_value=SimpleNamespace(transport_response=None, response=error) ) - with patch.object(pipeline, "_handle_attempt", handle_attempt): - output_context = await pipeline._retry( # type: ignore[reportPrivateUsage] - self._non_retryable_call(strategy), MagicMock(), None + with patch.object(pipeline, "_handle_attempt", _program_of(handle_attempt)): + output_context = await run_program_async( + pipeline._retry( # type: ignore[reportPrivateUsage] + self._non_retryable_call(strategy), MagicMock(), None + ) ) assert output_context.response is error diff --git a/packages/smithy-http/pyproject.toml b/packages/smithy-http/pyproject.toml index bb23f9b12..086e733e7 100644 --- a/packages/smithy-http/pyproject.toml +++ b/packages/smithy-http/pyproject.toml @@ -47,6 +47,9 @@ aiohttp = [ "aiohttp>=3.11.12, <4.0", "yarl" ] +urllib3 = [ + "urllib3>=2.0, <3.0", +] [build-system] requires = ["hatchling"] diff --git a/packages/smithy-http/src/smithy_http/aio/auth/apikey.py b/packages/smithy-http/src/smithy_http/aio/auth/apikey.py index 08474aaa6..c4a2f3d25 100644 --- a/packages/smithy-http/src/smithy_http/aio/auth/apikey.py +++ b/packages/smithy-http/src/smithy_http/aio/auth/apikey.py @@ -1,16 +1,17 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -from typing import Any, Protocol, Self +from typing import Any, Protocol -from smithy_core import URI from smithy_core.aio.interfaces.auth import AsyncAuthScheme, AsyncSigner from smithy_core.aio.interfaces.identity import AsyncIdentityResolver -from smithy_core.exceptions import SmithyIdentityError from smithy_core.interfaces import TypedProperties as _TypedProperties -from smithy_core.traits import APIKeyLocation, HTTPAPIKeyAuthTrait +from smithy_core.traits import APIKeyLocation from smithy_core.types import PropertyKey -from ... import Field +from ...auth.apikey import ( + APIKeySigner, + _APIKeyAuthSchemeBase, # pyright: ignore[reportPrivateUsage] +) from ...identity import APIKeyIdentity, APIKeyIdentityProperties from ..interfaces import HTTPRequest @@ -50,9 +51,7 @@ class AsyncAPIKeySigner(AsyncSigner[HTTPRequest, APIKeyIdentity, Any]): def __init__( self, *, name: str, location: APIKeyLocation, scheme: str | None = None ) -> None: - self._name = name - self._location = location - self._scheme = scheme + self._sync = APIKeySigner(name=name, location=location, scheme=scheme) async def sign( self, @@ -61,69 +60,29 @@ async def sign( identity: APIKeyIdentity, properties: Any, ) -> HTTPRequest: - match self._location: - case APIKeyLocation.QUERY: - query = request.destination.query or "" - if query: - query += "&" - query += f"{self._name}={identity.api_key}" - request.destination = URI( - scheme=request.destination.scheme, - username=request.destination.username, - password=request.destination.password, - host=request.destination.host, - port=request.destination.port, - path=request.destination.password, - query=query, - fragment=request.destination.fragment, - ) - case APIKeyLocation.HEADER: - value = identity.api_key - if self._scheme is not None: - value = f"{self._scheme} {value}" - request.fields.set_field(Field(name=self._name, values=[value])) - - return request + return self._sync.sign( + request=request, identity=identity, properties=properties + ) class AsyncAPIKeyAuthScheme( - AsyncAuthScheme[HTTPRequest, APIKeyIdentity, APIKeyIdentityProperties, Any] + _APIKeyAuthSchemeBase, + AsyncAuthScheme[HTTPRequest, APIKeyIdentity, APIKeyIdentityProperties, Any], ): """An auth scheme containing necessary data and tools for api key auth.""" - scheme_id = HTTPAPIKeyAuthTrait.id _signer: AsyncAPIKeySigner def __init__( self, *, name: str, location: APIKeyLocation, scheme: str | None = None ) -> None: + super().__init__(name=name, location=location, scheme=scheme) self._signer = AsyncAPIKeySigner(name=name, location=location, scheme=scheme) - def identity_properties( - self, *, context: _TypedProperties - ) -> APIKeyIdentityProperties: - config = context.get(API_KEY_RESOLVER_CONFIG) - if config is not None and config.api_key is not None: - return {"api_key": config.api_key} - return {} - def identity_resolver( self, *, context: _TypedProperties ) -> AsyncIdentityResolver[APIKeyIdentity, APIKeyIdentityProperties]: - config = context.get(API_KEY_RESOLVER_CONFIG) - if config is None or config.api_key_identity_resolver is None: - raise SmithyIdentityError( - "Attempted to use API key auth, but api_key_identity_resolver was not " - "set on the config." - ) - return config.api_key_identity_resolver - - def signer_properties(self, *, context: _TypedProperties) -> Any: - return {} + return super().identity_resolver(context=context) def signer(self) -> AsyncSigner[HTTPRequest, APIKeyIdentity, Any]: return self._signer - - @classmethod - def from_trait(cls, trait: HTTPAPIKeyAuthTrait, /) -> Self: - return cls(name=trait.name, location=trait.location, scheme=trait.scheme) diff --git a/packages/smithy-http/src/smithy_http/aio/identity/apikey.py b/packages/smithy-http/src/smithy_http/aio/identity/apikey.py index a1eba7d5d..04f5c7a75 100644 --- a/packages/smithy-http/src/smithy_http/aio/identity/apikey.py +++ b/packages/smithy-http/src/smithy_http/aio/identity/apikey.py @@ -1,9 +1,12 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 from smithy_core.aio.interfaces.identity import AsyncIdentityResolver -from smithy_core.exceptions import SmithyIdentityError -from ...identity import APIKeyIdentity, APIKeyIdentityProperties +from ...identity import ( + APIKeyIdentity, + APIKeyIdentityProperties, + APIKeyIdentityResolver, +) __all__ = [ "APIKeyIdentity", @@ -11,6 +14,8 @@ "AsyncAPIKeyIdentityResolver", ] +_SYNC_RESOLVER = APIKeyIdentityResolver() + class AsyncAPIKeyIdentityResolver( AsyncIdentityResolver[APIKeyIdentity, APIKeyIdentityProperties] @@ -20,8 +25,4 @@ class AsyncAPIKeyIdentityResolver( async def get_identity( self, *, properties: APIKeyIdentityProperties ) -> APIKeyIdentity: - if (api_key := properties.get("api_key")) is not None: - return APIKeyIdentity(api_key=api_key) - raise SmithyIdentityError( - "Attempted to use API key auth, but api_key was not set on the config." - ) + return _SYNC_RESOLVER.get_identity(properties=properties) diff --git a/packages/smithy-http/src/smithy_http/aio/protocols.py b/packages/smithy-http/src/smithy_http/aio/protocols.py index 2f728c71f..546b9a6a7 100644 --- a/packages/smithy-http/src/smithy_http/aio/protocols.py +++ b/packages/smithy-http/src/smithy_http/aio/protocols.py @@ -1,12 +1,10 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import asyncio -import os -from collections.abc import AsyncIterable, Callable, Mapping, Sequence +from collections.abc import AsyncIterable, Callable from inspect import iscoroutinefunction from typing import TYPE_CHECKING, Any, cast -from smithy_core import URI as _URI from smithy_core.aio.interfaces import ( AsyncByteStream, AsyncClientProtocol, @@ -19,46 +17,29 @@ AsyncEventReceiver, ) from smithy_core.aio.types import AsyncBytesReader -from smithy_core.codecs import Codec from smithy_core.deserializers import DeserializeableShape, ShapeDeserializer from smithy_core.documents import TypeRegistry from smithy_core.exceptions import ( - CallError, ExpectationNotMetError, MissingDependencyError, - ModeledError, UnsupportedStreamError, ) +from smithy_core.interfaces import StreamingBlob as SyncStreamingBlob from smithy_core.interfaces import ( - Endpoint, - ProtocolSettings, - SeekableBytesReader, TypedProperties, - URI, -) -from smithy_core.interfaces import StreamingBlob as SyncStreamingBlob -from smithy_core.prelude import DOCUMENT, UNIT -from smithy_core.response import ResponseMetadata -from smithy_core.schemas import APIOperation, Schema -from smithy_core.serializers import SerializeableShape, SerializeableStruct -from smithy_core.shapes import ShapeID, ShapeType -from smithy_core.traits import ( - ORIGINAL_SHAPE_ID, - DefaultTrait, - RequiredTrait, - RpcV2CborTrait, ) +from smithy_core.schemas import APIOperation +from smithy_core.serializers import SerializeableShape from smithy_core.types import PropertyKey -from .. import tuples_to_fields -from ..deserializers import HTTPResponseDeserializer -from ..schema_extensions import ( - HTTP_BINDING_SCHEMA_EXTENSION, - HTTP_OPERATION_SCHEMA_EXTENSION, +from ..protocols import ( + _HttpBindingProtocolBase, # pyright: ignore[reportPrivateUsage] + _HttpProtocolBase, # pyright: ignore[reportPrivateUsage] + _RpcProtocolBase, # pyright: ignore[reportPrivateUsage] + _RpcV2CborProtocolBase, # pyright: ignore[reportPrivateUsage] + _RpcV2ProtocolBase, # pyright: ignore[reportPrivateUsage] ) -from ..serializers import HTTPBindingSerializer, HTTPRequestSerializer -from . import HTTPRequest as _HTTPRequest -from .interfaces import HTTPErrorIdentifier, HTTPRequest, HTTPResponse +from .interfaces import HTTPRequest, HTTPResponse __all__ = [ "AsyncHttpBindingClientProtocol", @@ -103,7 +84,6 @@ from smithy_aws_event_stream.events import Event as _Event from smithy_aws_event_stream.events import EventMessage as _EventMessage from smithy_aws_event_stream.exceptions import MissingInitialResponse - from smithy_core._schema_compact import StaticSchema from smithy_core.aio.types import AsyncBytesProvider from typing_extensions import TypeForm @@ -147,60 +127,11 @@ def _assert_event_stream() -> None: ) -class AsyncHttpClientProtocol(AsyncClientProtocol[HTTPRequest, HTTPResponse]): +class AsyncHttpClientProtocol( + _HttpProtocolBase, AsyncClientProtocol[HTTPRequest, HTTPResponse] +): """An HTTP-based protocol.""" - @property - def payload_codec(self) -> Codec: - """The codec used for the serde of event payloads and HTTP bodies.""" - raise NotImplementedError() - - def set_service_endpoint( - self, - *, - request: HTTPRequest, - endpoint: Endpoint, - ) -> HTTPRequest: - uri = endpoint.uri - previous = request.destination - - path = previous.path or uri.path - if uri.path is not None and previous.path is not None: - path = os.path.join(uri.path, previous.path.lstrip("/")) - - if path is not None and not path.startswith("/"): - path = "/" + path - - query = previous.query or uri.query - if uri.query and previous.query: - query = f"{uri.query}&{previous.query}" - - request.destination = _URI( - scheme=uri.scheme, - username=uri.username or previous.username, - password=uri.password or previous.password, - host=uri.host, - port=uri.port or previous.port, - path=path, - query=query, - fragment=uri.fragment or previous.fragment, - ) - - return request - - def extract_response_metadata( - self, - *, - response: HTTPResponse, - context: TypedProperties, - ) -> ResponseMetadata: - """Extract the status code from an HTTP response. - - Identifiers such as request IDs are not part of HTTP itself, so protocols - that define them are expected to override this and add them. - """ - return ResponseMetadata(http_status_code=response.status) - async def create_event_publisher[ OperationInput: SerializeableShape, OperationOutput: DeserializeableShape, @@ -273,64 +204,10 @@ def create_event_receiver[ ) -class AsyncHttpBindingClientProtocol(AsyncHttpClientProtocol): +class AsyncHttpBindingClientProtocol(_HttpBindingProtocolBase, AsyncHttpClientProtocol): """An HTTP-based protocol that uses HTTP binding traits.""" - @property - def payload_codec(self) -> Codec: - """The codec used for the serde of input and output payloads.""" - raise NotImplementedError() - - @property - def content_type(self) -> str: - """The media type of the http payload.""" - raise NotImplementedError() - - @property - def error_identifier(self) -> HTTPErrorIdentifier: - """The class used to identify the shape IDs of errors based on fields or other - response information.""" - raise NotImplementedError() - - def serialize_request[ - OperationInput: "SerializeableShape", - OperationOutput: "DeserializeableShape", - ]( - self, - *, - operation: APIOperation[OperationInput, OperationOutput], - input: OperationInput, - endpoint: URI, - context: TypedProperties, - ) -> HTTPRequest: - operation_metadata = operation.schema.get_extension( - HTTP_OPERATION_SCHEMA_EXTENSION - ) - if isinstance(input, SerializeableStruct): - serializer = HTTPBindingSerializer( - payload_codec=self.payload_codec, - schema=operation.input_schema, - http_trait=operation_metadata.http_trait, - operation_metadata=operation_metadata, - ) - try: - input.serialize_members(serializer) - except BaseException as error: - serializer.abort(type(error), error, error.__traceback__) - raise - return serializer.build_request() - - serializer = HTTPRequestSerializer( - payload_codec=self.payload_codec, - http_trait=operation_metadata.http_trait, - operation_metadata=operation_metadata, - ) - input.serialize(serializer) - if serializer.result is None: - raise ExpectationNotMetError( - "Expected request to be serialized, but was None" - ) - return serializer.result + _ASYNC_BODY = True async def deserialize_response[ OperationInput: "SerializeableShape", @@ -345,7 +222,7 @@ async def deserialize_response[ context: TypedProperties, ) -> OperationOutput: if not self._is_success(operation, context, response): - raise await self._create_error( + raise self._create_error( operation=operation, request=request, response=response, @@ -356,19 +233,10 @@ async def deserialize_response[ # if body is not streaming and is async, we have to buffer it body: SyncStreamingBlob | None = None - response_metadata = operation.output_schema.get_extension( - HTTP_BINDING_SCHEMA_EXTENSION - ).response - if response_metadata.streaming_member is None: + if not self._has_streaming_output(operation): body = await self._buffer_async_body(response.body) - deserializer = HTTPResponseDeserializer( - payload_codec=self.payload_codec, - response=response, - body=body, - ) - - return operation.output.deserialize(deserializer) + return self._deserialize_output(operation, response, body) async def _buffer_async_body(self, stream: AsyncStreamingBlob) -> SyncStreamingBlob: if isinstance(stream, bytes | bytearray): @@ -387,146 +255,8 @@ async def _buffer_async_body(self, stream: AsyncStreamingBlob) -> SyncStreamingB case _: return stream - def _is_success( - self, - operation: APIOperation[Any, Any], - context: TypedProperties, - response: HTTPResponse, - ) -> bool: - return 200 <= response.status < 300 - - async def _create_error( - self, - operation: APIOperation[Any, Any], - request: HTTPRequest, - response: HTTPResponse, - response_body: SyncStreamingBlob, - error_registry: TypeRegistry, - context: TypedProperties, - ) -> CallError: - error_id = self.error_identifier.identify( - operation=operation, response=response - ) - if error_id is not None and error_id not in error_registry: - error_id = self._resolve_error_id( - operation=operation, - error_id=error_id, - ) - - retry_after = self._retry_after(response) - - if error_id is None and self._matches_content_type(response): - if isinstance(response_body, bytearray): - response_body = bytes(response_body) - deserializer = self.payload_codec.create_deserializer(source=response_body) - document = deserializer.read_document(schema=DOCUMENT) - document_error_id = document.discriminator - if document_error_id not in error_registry: - document_error_id = self._resolve_error_id( - operation=operation, - error_id=document_error_id, - ) - - if document_error_id in error_registry: - error_id = document_error_id - if isinstance(response_body, SeekableBytesReader): - response_body.seek(0) - - if error_id is not None and error_id in error_registry: - error_shape = error_registry.get(error_id) - - # make sure the error shape is derived from modeled exception - if not issubclass(error_shape, ModeledError): - raise ExpectationNotMetError( - f"Modeled errors must be derived from 'ModeledError', " - f"but got {error_shape}" - ) - - deserializer = HTTPResponseDeserializer( - payload_codec=self.payload_codec, - response=response, - body=response_body, - ) - modeled_error = error_shape.deserialize(deserializer) - if retry_after is not None: - modeled_error.retry_after = retry_after - return modeled_error - - message = ( - f"Unknown error for operation {operation.schema.id} " - f"- status: {response.status}" - ) - if error_id is not None: - message += f" - id: {error_id}" - if response.reason is not None: - message += f" - reason: {response.status}" - - is_timeout = response.status == 408 - is_throttle = response.status == 429 - fault = "client" if response.status < 500 else "server" - - return CallError( - message=message, - fault=fault, - is_throttling_error=is_throttle, - is_timeout_error=is_timeout, - is_retry_safe=is_throttle or is_timeout or None, - retry_after=retry_after, - ) - def _retry_after(self, response: HTTPResponse) -> float | None: - """The retry delay in seconds requested by the server, if the response carries one.""" - return None - - def _resolve_error_id( - self, - *, - operation: APIOperation[Any, Any], - error_id: ShapeID, - ) -> ShapeID: - """Resolve a response discriminator to its modeled error shape ID.""" - return error_id - - def _matches_content_type(self, response: HTTPResponse) -> bool: - if "content-type" not in response.fields: - return False - return response.fields["content-type"].as_string() == self.content_type - - -try: - from smithy_cbor import CBORCodec - from smithy_cbor import loads as _cbor_loads - - _HAS_CBOR = True -except ImportError: - _HAS_CBOR = False # type: ignore - -if TYPE_CHECKING: - from smithy_cbor import CBORCodec - from smithy_cbor import loads as _cbor_loads - - -def _assert_cbor() -> None: - if not _HAS_CBOR: - raise MissingDependencyError( - "Attempted to use CBOR codec, but smithy-cbor is not installed." - ) - - -def _is_unit(schema: Schema | None) -> bool: - """rpcv2Cbor treats an operation with unit input as having no body, and unit output - as an empty response. Codegen synthesizes a wrapper struct for a unit member but - tags it with ``smithy.synthetic#originalShapeId = smithy.api#Unit``. - """ - if schema is None: - return False - if schema.id == UNIT.id: - return True - original = schema.traits.get(ORIGINAL_SHAPE_ID) - return original is not None and original.document_value == str(UNIT.id) - - -class AsyncRpcClientProtocol(AsyncHttpClientProtocol): +class AsyncRpcClientProtocol(_RpcProtocolBase, AsyncHttpClientProtocol): """Base for the RPC-based protocols (awsJson, awsQuery, rpcv2). RPC protocols serialize the whole operation input through a codec rather than via @@ -541,70 +271,8 @@ class AsyncRpcClientProtocol(AsyncHttpClientProtocol): never models an event stream (awsQuery) simply never reaches these helpers. """ - # The protocol trait's eventStreamHttp ALPN list (empty if the trait left it unset). - _event_stream_http: tuple[str, ...] = () - - @staticmethod - def _resolve_event_stream_http( - service_schema: "StaticSchema | Schema", protocol_id: ShapeID - ) -> tuple[str, ...]: - """Read this protocol's ``eventStreamHttp`` ALPN list off the service schema. - - Keyed by the protocol's own id, so a service carrying several protocol traits - yields each protocol its own value rather than a flattened one. The compact - schema codegen emits is hydrated on first read (cached thereafter). - """ - from smithy_core._schema_compact import hydrate - - trait = hydrate(service_schema).get_trait(protocol_id) - if trait is None: - return () - value = trait.document_value - if not isinstance(value, Mapping): - return () - versions = value.get("eventStreamHttp") - if not isinstance(versions, Sequence) or isinstance(versions, str | bytes): - return () - return tuple(str(v) for v in versions) - - @staticmethod - def _is_event_stream_member(member: Schema | None) -> bool: - """Whether a ``@streaming`` member is an EVENT stream (union), not a data stream. - - ``input_stream_member``/``output_stream_member`` match any ``@streaming`` - member; a blob target is a data stream, only a union target is an event stream. - """ - return member is not None and member.expect_member_target().shape_type is ( - ShapeType.UNION - ) - - def _requires_http2(self, operation: APIOperation[Any, Any]) -> bool: - # Only EVENT streams (a @streaming UNION member) force HTTP/2 -- a @streaming - # blob is a data stream and runs fine over HTTP/1.1. input_stream_member / - # output_stream_member match either, so check the target is a union here. - if not ( - self._is_event_stream_member(operation.input_stream_member) - or self._is_event_stream_member(operation.output_stream_member) - ): - return False - # Require HTTP/2 unless the model EXPLICITLY allows only HTTP/1.1: no list, or - # any list containing h2, requires h2; only an h1-and-nothing-else list opts out. - return "h2" in self._event_stream_http or not self._event_stream_http - - @staticmethod - def _requires_initial_response(output: Schema) -> bool: - """Whether the output needs an ``initial-response`` to be populated. - - An empty document satisfies any member that is optional or has a default - (clients fill defaults), so only a ``@required`` member without a ``@default`` - forces an initial-response. - """ - for member in output.members.values(): - if member.get_trait(RequiredTrait) is not None and ( - member.get_trait(DefaultTrait) is None - ): - return True - return False + def _request_body(self, payload: bytes) -> AsyncBytesReader: + return AsyncBytesReader(payload) def _build_initial_request_body( self, input: SerializeableShape, context: TypedProperties @@ -700,104 +368,8 @@ def create_event_receiver[ ) -class _RpcV2ClientProtocol(AsyncRpcClientProtocol): - """Shared framing for the Smithy RPC v2 protocol family (rpcv2Cbor, rpcv2Json). - - Subclasses supply the codec-specific pieces: the protocol id, content type, - ``Smithy-Protocol`` header value, the empty-document sentinel, codec construction, - and error ``__type`` decoding. All HTTP/RPC framing lives here. - """ - - _id: ShapeID - _content_type: str - _smithy_protocol: str - # Serialized empty document (e.g. CBOR `{}` or JSON `{}`) used when a response body - # is absent so absent members take their defaults. - _empty_document: bytes - _codec: Codec - _service_name: str - - @property - def id(self) -> ShapeID: - return self._id - - @property - def payload_codec(self) -> Codec: - return self._codec - - @property - def content_type(self) -> str: - return self._content_type - - def _decode_document(self, body: bytes) -> object: - """Decode a serialized document to a Python object, for reading the error - ``__type`` discriminator. - """ - raise NotImplementedError - - def serialize_request[ - OperationInput: SerializeableShape, - OperationOutput: DeserializeableShape, - ]( - self, - *, - operation: APIOperation[OperationInput, OperationOutput], - input: OperationInput, - endpoint: URI, - context: TypedProperties, - ) -> HTTPRequest: - operation_name = operation.schema.id.name - path = f"/service/{self._service_name}/operation/{operation_name}" - - # An event-stream operation must negotiate HTTP/2 (see _requires_http2); mark - # the request so the transport does so for this operation alone, leaving - # non-streaming operations on HTTP/1.1. - http2_required = self._requires_http2(operation) - - # Accept describes the RESPONSE and Content-Type the REQUEST body, so they turn - # on output_stream_member and input_stream_member independently: a one-way - # stream advertises the event-stream media type on only the streaming side. - # Only an EVENT stream (union member) counts; a @streaming blob is a data stream - # and takes the normal media type. - accept = ( - _EVENT_STREAM_CONTENT_TYPE - if self._is_event_stream_member(operation.output_stream_member) - else self._content_type - ) - fields: list[tuple[str, str]] = [ - ("Smithy-Protocol", self._smithy_protocol), - ("Accept", accept), - ] - - if self._is_event_stream_member(operation.input_stream_member): - # RPC event streams carry the input as an initial-request event at the head - # of the stream body; the HTTP envelope is otherwise a normal RPC request. - fields.append(("Content-Type", _EVENT_STREAM_CONTENT_TYPE)) - return _HTTPRequest( - method="POST", - destination=_URI(host="", path=path), - fields=tuples_to_fields(fields), - body=self._build_initial_request_body(input, context), - http2_required=http2_required, - ) - - # The protocol omits the body AND the Content-Type header for operations whose - # input is the unit type (no modeled input). Every other operation serializes - # the input shape, even when it has no members. - if _is_unit(operation.input_schema): - payload = b"" - else: - payload = self._codec.serialize(shape=input) - fields.append(("Content-Type", self._content_type)) - fields.append(("Content-Length", str(len(payload)))) - - return _HTTPRequest( - method="POST", - destination=_URI(host="", path=path), - fields=tuples_to_fields(fields), - body=AsyncBytesReader(payload), - http2_required=http2_required, - ) +class _RpcV2ClientProtocol(_RpcV2ProtocolBase, AsyncRpcClientProtocol): + """Shared async framing for the Smithy RPC v2 protocol family.""" async def deserialize_response[ OperationInput: SerializeableShape, @@ -819,20 +391,13 @@ async def deserialize_response[ operation=operation, response=response, context=context ) - body = await response.consume_body_async() - if response.status != 200: - raise self._create_error( - operation=operation, - response=response, - body=body, - error_registry=error_registry, - context=context, - ) - # An empty body (unit output, or a server that elides the empty document) is a - # valid response: deserialize an empty document so absent members take defaults. - if not body or _is_unit(operation.output_schema): - body = self._empty_document - return self._codec.deserialize(source=body, shape=operation.output) + return self._deserialize_body( + operation=operation, + response=response, + body=await response.consume_body_async(), + error_registry=error_registry, + context=context, + ) def _default_output[ OperationInput: SerializeableShape, @@ -850,80 +415,10 @@ def _default_output[ source=self._empty_document, shape=operation.output ) - def _create_error( - self, - *, - operation: APIOperation[Any, Any], - response: HTTPResponse, - body: bytes, - error_registry: TypeRegistry, - context: TypedProperties, - ) -> Exception: - # The protocol discriminates errors by a top-level `__type` member in the body - # holding the absolute error shape id (no error header). Resolve it against the - # registry, then deserialize the same body into the modeled error shape. - error_id: ShapeID | None = None - if body: - decoded = self._decode_document(body) - if isinstance(decoded, dict): - raw_type = cast("dict[str, object]", decoded).get("__type") - if isinstance(raw_type, str): - error_id = self._normalize_error_id(raw_type, operation) - - if error_id is not None and error_id in error_registry: - error_shape = error_registry.get(error_id) - if not issubclass(error_shape, ModeledError): - raise ExpectationNotMetError( - f"Modeled errors must derive from 'ModeledError', got {error_shape}" - ) - deserializer = self._codec.create_deserializer(body or self._empty_document) - return error_shape.deserialize(deserializer) - - message = ( - f"Unknown error for operation {operation.schema.id} " - f"- status: {response.status}" - ) - if error_id is not None: - message += f" - id: {error_id}" - return CallError( - message=message, - fault="client" if response.status < 500 else "server", - ) - @staticmethod - def _normalize_error_id( - raw_type: str, operation: APIOperation[Any, Any] - ) -> ShapeID: - # `__type` may carry a full shape id, or (per spec) a name that may include a - # trailing "#"-qualified suffix. Match on the shape name against the operation's - # modeled errors so a bare or oddly-qualified name still resolves. - candidate = ShapeID(raw_type) - for error_schema in operation.error_schemas: - if error_schema.id == candidate or error_schema.id.name == candidate.name: - return error_schema.id - return candidate - - -class AsyncRpcV2CborClientProtocol(_RpcV2ClientProtocol): +class AsyncRpcV2CborClientProtocol(_RpcV2CborProtocolBase, _RpcV2ClientProtocol): """An implementation of the smithy.protocols#rpcv2Cbor protocol.""" - _id: ShapeID = RpcV2CborTrait.id - _content_type: str = "application/cbor" - _smithy_protocol: str = "rpc-v2-cbor" - # CBOR encoding of an empty indefinite-length map (0xBF 0xFF), i.e. `{}`. - _empty_document: bytes = b"\xbf\xff" - - def __init__(self, settings: ProtocolSettings) -> None: - _assert_cbor() - self._service_name = settings.service_target - self._event_stream_http = self._resolve_event_stream_http( - settings.service_schema, self._id - ) - self._codec = CBORCodec(default_namespace=settings.namespace) - - def _decode_document(self, body: bytes) -> object: - return _cbor_loads(body) # type: ignore[possibly-unbound] - class _PrependedEventStream: """Serves one already-decoded event's bytes ahead of a live byte stream. diff --git a/packages/smithy-http/src/smithy_http/auth/apikey.py b/packages/smithy-http/src/smithy_http/auth/apikey.py index 4259ed8a7..fceae7d6b 100644 --- a/packages/smithy-http/src/smithy_http/auth/apikey.py +++ b/packages/smithy-http/src/smithy_http/auth/apikey.py @@ -1,18 +1,127 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -"""Placeholders for synchronous API key auth. +from typing import Any, Protocol, Self -Populated alongside the sync client implementation. -""" +from smithy_core import URI +from smithy_core.exceptions import SmithyIdentityError +from smithy_core.interfaces import TypedProperties as _TypedProperties +from smithy_core.interfaces.auth import AuthScheme, Signer +from smithy_core.interfaces.identity import IdentityResolver +from smithy_core.traits import APIKeyLocation, HTTPAPIKeyAuthTrait +from .. import Field +from ..aio.interfaces import HTTPRequest +from ..identity import APIKeyIdentity, APIKeyIdentityProperties -class APIKeySigner: - """A synchronous signer that signs http requests with an api key. Placeholder.""" +__all__ = ["APIKeyAuthScheme", "APIKeyResolverConfig", "APIKeySigner"] - # TODO(sync-api): implement alongside the sync client +class APIKeyResolverConfig(Protocol): + """A config bearing API key properties for a synchronous client.""" -class APIKeyAuthScheme: - """A synchronous api key auth scheme. Placeholder.""" + api_key: str | None + """An explicit API key. - # TODO(sync-api): implement alongside the sync client + If not set, it MAY be retrieved from elsewhere by the resolver. + """ + + api_key_identity_resolver: ( + IdentityResolver[APIKeyIdentity, APIKeyIdentityProperties] | None + ) + """An API key identity resolver.""" + + +class APIKeySigner(Signer[HTTPRequest, APIKeyIdentity, Any]): + """A signer that signs http requests with an api key.""" + + def __init__( + self, *, name: str, location: APIKeyLocation, scheme: str | None = None + ) -> None: + self._name = name + self._location = location + self._scheme = scheme + + def sign( + self, + *, + request: HTTPRequest, + identity: APIKeyIdentity, + properties: Any, + ) -> HTTPRequest: + match self._location: + case APIKeyLocation.QUERY: + query = request.destination.query or "" + if query: + query += "&" + query += f"{self._name}={identity.api_key}" + request.destination = URI( + scheme=request.destination.scheme, + username=request.destination.username, + password=request.destination.password, + host=request.destination.host, + port=request.destination.port, + path=request.destination.password, + query=query, + fragment=request.destination.fragment, + ) + case APIKeyLocation.HEADER: + value = identity.api_key + if self._scheme is not None: + value = f"{self._scheme} {value}" + request.fields.set_field(Field(name=self._name, values=[value])) + + return request + + +class _APIKeyAuthSchemeBase: + """API key auth scheme logic shared by the sync and async schemes.""" + + scheme_id = HTTPAPIKeyAuthTrait.id + + def __init__( + self, *, name: str, location: APIKeyLocation, scheme: str | None = None + ) -> None: + self._name = name + self._location = location + self._scheme = scheme + + def identity_properties( + self, *, context: _TypedProperties + ) -> APIKeyIdentityProperties: + config = context.get("config") + if config is not None and config.api_key is not None: + return {"api_key": config.api_key} + return {} + + def identity_resolver(self, *, context: _TypedProperties) -> Any: + config = context.get("config") + if config is None or config.api_key_identity_resolver is None: + raise SmithyIdentityError( + "Attempted to use API key auth, but api_key_identity_resolver was not " + "set on the config." + ) + return config.api_key_identity_resolver + + def signer_properties(self, *, context: _TypedProperties) -> Any: + return {} + + @classmethod + def from_trait(cls, trait: HTTPAPIKeyAuthTrait, /) -> Self: + return cls(name=trait.name, location=trait.location, scheme=trait.scheme) + + +class APIKeyAuthScheme( + _APIKeyAuthSchemeBase, + AuthScheme[HTTPRequest, APIKeyIdentity, APIKeyIdentityProperties, Any], +): + """An auth scheme containing necessary data and tools for api key auth.""" + + def identity_resolver( + self, *, context: _TypedProperties + ) -> IdentityResolver[APIKeyIdentity, APIKeyIdentityProperties]: + return super().identity_resolver(context=context) + + def signer(self) -> APIKeySigner: + return APIKeySigner( + name=self._name, location=self._location, scheme=self._scheme + ) diff --git a/packages/smithy-http/src/smithy_http/identity.py b/packages/smithy-http/src/smithy_http/identity.py index 1081d813b..7784153f3 100644 --- a/packages/smithy-http/src/smithy_http/identity.py +++ b/packages/smithy-http/src/smithy_http/identity.py @@ -4,7 +4,8 @@ from datetime import datetime from typing import TypedDict -from smithy_core.interfaces.identity import Identity +from smithy_core.exceptions import SmithyIdentityError +from smithy_core.interfaces.identity import Identity, IdentityResolver @dataclass(kw_only=True) @@ -21,11 +22,14 @@ class APIKeyIdentityProperties(TypedDict, total=False): api_key: str -class APIKeyIdentityResolver: - """Loads the API key identity from the configuration. +class APIKeyIdentityResolver( + IdentityResolver[APIKeyIdentity, APIKeyIdentityProperties] +): + """Loads the API key identity from the configuration.""" - Placeholder for the synchronous resolver; populated alongside the sync client - implementation. - """ - - # TODO(sync-api): implement alongside the sync client + def get_identity(self, *, properties: APIKeyIdentityProperties) -> APIKeyIdentity: + if (api_key := properties.get("api_key")) is not None: + return APIKeyIdentity(api_key=api_key) + raise SmithyIdentityError( + "Attempted to use API key auth, but api_key was not set on the config." + ) diff --git a/packages/smithy-http/src/smithy_http/interfaces/__init__.py b/packages/smithy-http/src/smithy_http/interfaces/__init__.py index ba4d6dd5d..f70223c2d 100644 --- a/packages/smithy-http/src/smithy_http/interfaces/__init__.py +++ b/packages/smithy-http/src/smithy_http/interfaces/__init__.py @@ -2,12 +2,16 @@ # SPDX-License-Identifier: Apache-2.0 from collections.abc import Iterator from dataclasses import dataclass -from typing import Literal, Protocol +from typing import TYPE_CHECKING, Literal, Protocol + +if TYPE_CHECKING: + from ..aio.interfaces import HTTPRequest, HTTPResponse __all__ = [ "Field", "FieldPosition", "Fields", + "HTTPClient", "HTTPClientConfiguration", "HTTPRequestConfiguration", "QueryParamsList", @@ -142,7 +146,30 @@ class HTTPRequestConfiguration: class HTTPClient(Protocol): """A synchronous HTTP client interface. - Placeholder; populated alongside the sync client implementation. + Header field names and values are not validated before reaching this layer. + Implementations of ``HTTPClient`` are responsible for validating and handling those + invalid and dangerous characters (such as CR, LF, or NUL) as defined in `RFC 9110, + Section 5.5 `_. """ - # TODO(sync-api): implement alongside the sync client + TIMEOUT_EXCEPTIONS: tuple[type[Exception], ...] + + def __init__(self, *, client_config: HTTPClientConfiguration | None) -> None: + """ + :param client_config: Configuration that applies to all requests made with this + client. + """ + ... + + def send( + self, + request: "HTTPRequest", + *, + request_config: HTTPRequestConfiguration | None = None, + ) -> "HTTPResponse": + """Send HTTP request over the wire and return the response. + + :param request: The request including destination URI, fields, payload. + :param request_config: Configuration specific to this request. + """ + ... diff --git a/packages/smithy-http/src/smithy_http/protocols.py b/packages/smithy-http/src/smithy_http/protocols.py index 8ec71dc00..6fdf875d6 100644 --- a/packages/smithy-http/src/smithy_http/protocols.py +++ b/packages/smithy-http/src/smithy_http/protocols.py @@ -1,24 +1,691 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -"""Placeholders for synchronous HTTP protocol implementations. +import os +from collections.abc import Mapping, Sequence +from io import BytesIO +from typing import TYPE_CHECKING, Any, cast -Populated alongside the sync client implementation. -""" +from smithy_core import URI as _URI +from smithy_core.codecs import Codec +from smithy_core.deserializers import DeserializeableShape +from smithy_core.documents import TypeRegistry +from smithy_core.exceptions import ( + CallError, + ExpectationNotMetError, + MissingDependencyError, + ModeledError, + UnsupportedStreamError, +) +from smithy_core.interfaces import ( + ClientProtocol, + Endpoint, + ProtocolSettings, + SeekableBytesReader, + TypedProperties, + URI, +) +from smithy_core.interfaces import StreamingBlob as SyncStreamingBlob +from smithy_core.prelude import DOCUMENT, UNIT +from smithy_core.response import ResponseMetadata +from smithy_core.schemas import APIOperation, Schema +from smithy_core.serializers import SerializeableShape, SerializeableStruct +from smithy_core.shapes import ShapeID, ShapeType +from smithy_core.traits import ( + ORIGINAL_SHAPE_ID, + DefaultTrait, + RequiredTrait, + RpcV2CborTrait, +) +from . import tuples_to_fields +from .aio import HTTPRequest as _HTTPRequest +from .aio.interfaces import HTTPErrorIdentifier, HTTPRequest, HTTPResponse +from .deserializers import HTTPResponseDeserializer +from .schema_extensions import ( + HTTP_BINDING_SCHEMA_EXTENSION, + HTTP_OPERATION_SCHEMA_EXTENSION, +) +from .serializers import HTTPBindingSerializer, HTTPRequestSerializer -class HttpClientProtocol: - """A synchronous HTTP-based protocol. Placeholder.""" +if TYPE_CHECKING: + from smithy_core._schema_compact import StaticSchema - # TODO(sync-api): implement alongside the sync client +__all__ = [ + "HttpBindingClientProtocol", + "HttpClientProtocol", + "RpcClientProtocol", + "RpcV2CborClientProtocol", +] +_EVENT_STREAM_CONTENT_TYPE = "application/vnd.amazon.eventstream" -class HttpBindingClientProtocol: - """A synchronous HTTP-binding protocol. Placeholder.""" +try: + from smithy_cbor import CBORCodec + from smithy_cbor import loads as _cbor_loads - # TODO(sync-api): implement alongside the sync client + _HAS_CBOR = True +except ImportError: + _HAS_CBOR = False # type: ignore +if TYPE_CHECKING: + from smithy_cbor import CBORCodec + from smithy_cbor import loads as _cbor_loads -class RpcV2CborClientProtocol: - """A synchronous RPCv2 CBOR protocol. Placeholder.""" - # TODO(sync-api): implement alongside the sync client +def _assert_cbor() -> None: + if not _HAS_CBOR: + raise MissingDependencyError( + "Attempted to use CBOR codec, but smithy-cbor is not installed." + ) + + +def _is_unit(schema: Schema | None) -> bool: + """rpcv2Cbor treats an operation with unit input as having no body, and unit output + as an empty response. Codegen synthesizes a wrapper struct for a unit member but + tags it with ``smithy.synthetic#originalShapeId = smithy.api#Unit``. + """ + if schema is None: + return False + if schema.id == UNIT.id: + return True + original = schema.traits.get(ORIGINAL_SHAPE_ID) + return original is not None and original.document_value == str(UNIT.id) + + +class _HttpProtocolBase: + """Logic shared by the sync and async HTTP protocols.""" + + @property + def payload_codec(self) -> Codec: + """The codec used for the serde of event payloads and HTTP bodies.""" + raise NotImplementedError() + + def set_service_endpoint( + self, + *, + request: HTTPRequest, + endpoint: Endpoint, + ) -> HTTPRequest: + uri = endpoint.uri + previous = request.destination + + path = previous.path or uri.path + if uri.path is not None and previous.path is not None: + path = os.path.join(uri.path, previous.path.lstrip("/")) + + if path is not None and not path.startswith("/"): + path = "/" + path + + query = previous.query or uri.query + if uri.query and previous.query: + query = f"{uri.query}&{previous.query}" + + request.destination = _URI( + scheme=uri.scheme, + username=uri.username or previous.username, + password=uri.password or previous.password, + host=uri.host, + port=uri.port or previous.port, + path=path, + query=query, + fragment=uri.fragment or previous.fragment, + ) + + return request + + def extract_response_metadata( + self, + *, + response: HTTPResponse, + context: TypedProperties, + ) -> ResponseMetadata: + """Extract the status code from an HTTP response. + + Identifiers such as request IDs are not part of HTTP itself, so protocols + that define them are expected to override this and add them. + """ + return ResponseMetadata(http_status_code=response.status) + + +class _HttpBindingProtocolBase(_HttpProtocolBase): + """HTTP binding logic shared by the sync and async protocols.""" + + _ASYNC_BODY: bool = False + + @property + def payload_codec(self) -> Codec: + """The codec used for the serde of input and output payloads.""" + raise NotImplementedError() + + @property + def content_type(self) -> str: + """The media type of the http payload.""" + raise NotImplementedError() + + @property + def error_identifier(self) -> HTTPErrorIdentifier: + """The class used to identify the shape IDs of errors based on fields or other + response information.""" + raise NotImplementedError() + + def serialize_request[ + OperationInput: "SerializeableShape", + OperationOutput: "DeserializeableShape", + ]( + self, + *, + operation: APIOperation[OperationInput, OperationOutput], + input: OperationInput, + endpoint: URI, + context: TypedProperties, + ) -> HTTPRequest: + operation_metadata = operation.schema.get_extension( + HTTP_OPERATION_SCHEMA_EXTENSION + ) + if isinstance(input, SerializeableStruct): + serializer = HTTPBindingSerializer( + payload_codec=self.payload_codec, + schema=operation.input_schema, + http_trait=operation_metadata.http_trait, + operation_metadata=operation_metadata, + async_body=self._ASYNC_BODY, + ) + try: + input.serialize_members(serializer) + except BaseException as error: + serializer.abort(type(error), error, error.__traceback__) + raise + return serializer.build_request() + + serializer = HTTPRequestSerializer( + payload_codec=self.payload_codec, + http_trait=operation_metadata.http_trait, + operation_metadata=operation_metadata, + ) + input.serialize(serializer) + if serializer.result is None: + raise ExpectationNotMetError( + "Expected request to be serialized, but was None" + ) + return serializer.result + + def _has_streaming_output(self, operation: APIOperation[Any, Any]) -> bool: + response_metadata = operation.output_schema.get_extension( + HTTP_BINDING_SCHEMA_EXTENSION + ).response + return response_metadata.streaming_member is not None + + def _deserialize_output[ + OperationInput: "SerializeableShape", + OperationOutput: "DeserializeableShape", + ]( + self, + operation: APIOperation[OperationInput, OperationOutput], + response: HTTPResponse, + body: SyncStreamingBlob | None, + ) -> OperationOutput: + deserializer = HTTPResponseDeserializer( + payload_codec=self.payload_codec, + response=response, + body=body, + ) + return operation.output.deserialize(deserializer) + + def _is_success( + self, + operation: APIOperation[Any, Any], + context: TypedProperties, + response: HTTPResponse, + ) -> bool: + return 200 <= response.status < 300 + + def _create_error( + self, + operation: APIOperation[Any, Any], + request: HTTPRequest, + response: HTTPResponse, + response_body: SyncStreamingBlob, + error_registry: TypeRegistry, + context: TypedProperties, + ) -> CallError: + error_id = self.error_identifier.identify( + operation=operation, response=response + ) + if error_id is not None and error_id not in error_registry: + error_id = self._resolve_error_id( + operation=operation, + error_id=error_id, + ) + + retry_after = self._retry_after(response) + + if error_id is None and self._matches_content_type(response): + if isinstance(response_body, bytearray): + response_body = bytes(response_body) + deserializer = self.payload_codec.create_deserializer(source=response_body) + document = deserializer.read_document(schema=DOCUMENT) + document_error_id = document.discriminator + if document_error_id not in error_registry: + document_error_id = self._resolve_error_id( + operation=operation, + error_id=document_error_id, + ) + + if document_error_id in error_registry: + error_id = document_error_id + if isinstance(response_body, SeekableBytesReader): + response_body.seek(0) + + if error_id is not None and error_id in error_registry: + error_shape = error_registry.get(error_id) + + # make sure the error shape is derived from modeled exception + if not issubclass(error_shape, ModeledError): + raise ExpectationNotMetError( + f"Modeled errors must be derived from 'ModeledError', " + f"but got {error_shape}" + ) + + deserializer = HTTPResponseDeserializer( + payload_codec=self.payload_codec, + response=response, + body=response_body, + ) + modeled_error = error_shape.deserialize(deserializer) + if retry_after is not None: + modeled_error.retry_after = retry_after + return modeled_error + + message = ( + f"Unknown error for operation {operation.schema.id} " + f"- status: {response.status}" + ) + if error_id is not None: + message += f" - id: {error_id}" + if response.reason is not None: + message += f" - reason: {response.status}" + + is_timeout = response.status == 408 + is_throttle = response.status == 429 + fault = "client" if response.status < 500 else "server" + + return CallError( + message=message, + fault=fault, + is_throttling_error=is_throttle, + is_timeout_error=is_timeout, + is_retry_safe=is_throttle or is_timeout or None, + retry_after=retry_after, + ) + + def _retry_after(self, response: HTTPResponse) -> float | None: + """The retry delay in seconds requested by the server, if the response carries one.""" + return None + + def _resolve_error_id( + self, + *, + operation: APIOperation[Any, Any], + error_id: ShapeID, + ) -> ShapeID: + """Resolve a response discriminator to its modeled error shape ID.""" + return error_id + + def _matches_content_type(self, response: HTTPResponse) -> bool: + if "content-type" not in response.fields: + return False + return response.fields["content-type"].as_string() == self.content_type + + +class _RpcProtocolBase(_HttpProtocolBase): + """RPC protocol logic shared by the sync and async protocols. + + RPC protocols serialize the whole operation input through a codec rather than via + HTTP bindings. + """ + + # The protocol trait's eventStreamHttp ALPN list (empty if the trait left it unset). + _event_stream_http: tuple[str, ...] = () + + def _request_body(self, payload: bytes) -> Any: + """Wrap a serialized payload as a request body.""" + return BytesIO(payload) + + def _build_initial_request_body( + self, input: SerializeableShape, context: TypedProperties + ) -> Any: + """Build the body of an RPC event stream request.""" + raise UnsupportedStreamError( + "Event streams are not supported by synchronous clients." + ) + + @staticmethod + def _resolve_event_stream_http( + service_schema: "StaticSchema | Schema", protocol_id: ShapeID + ) -> tuple[str, ...]: + """Read this protocol's ``eventStreamHttp`` ALPN list off the service schema. + + Keyed by the protocol's own id, so a service carrying several protocol traits + yields each protocol its own value rather than a flattened one. The compact + schema codegen emits is hydrated on first read (cached thereafter). + """ + from smithy_core._schema_compact import hydrate + + trait = hydrate(service_schema).get_trait(protocol_id) + if trait is None: + return () + value = trait.document_value + if not isinstance(value, Mapping): + return () + versions = value.get("eventStreamHttp") + if not isinstance(versions, Sequence) or isinstance(versions, str | bytes): + return () + return tuple(str(v) for v in versions) + + @staticmethod + def _is_event_stream_member(member: Schema | None) -> bool: + """Whether a ``@streaming`` member is an EVENT stream (union), not a data stream. + + ``input_stream_member``/``output_stream_member`` match any ``@streaming`` + member; a blob target is a data stream, only a union target is an event stream. + """ + return member is not None and member.expect_member_target().shape_type is ( + ShapeType.UNION + ) + + def _requires_http2(self, operation: APIOperation[Any, Any]) -> bool: + # Only EVENT streams (a @streaming UNION member) force HTTP/2 -- a @streaming + # blob is a data stream and runs fine over HTTP/1.1. input_stream_member / + # output_stream_member match either, so check the target is a union here. + if not ( + self._is_event_stream_member(operation.input_stream_member) + or self._is_event_stream_member(operation.output_stream_member) + ): + return False + # Require HTTP/2 unless the model EXPLICITLY allows only HTTP/1.1: no list, or + # any list containing h2, requires h2; only an h1-and-nothing-else list opts out. + return "h2" in self._event_stream_http or not self._event_stream_http + + @staticmethod + def _requires_initial_response(output: Schema) -> bool: + """Whether the output needs an ``initial-response`` to be populated. + + An empty document satisfies any member that is optional or has a default + (clients fill defaults), so only a ``@required`` member without a ``@default`` + forces an initial-response. + """ + for member in output.members.values(): + if member.get_trait(RequiredTrait) is not None and ( + member.get_trait(DefaultTrait) is None + ): + return True + return False + + def _raise_if_output_event_stream(self, operation: APIOperation[Any, Any]) -> None: + if self._is_event_stream_member(operation.output_stream_member): + raise UnsupportedStreamError( + "Event streams are not supported by synchronous clients." + ) + + +class _RpcV2ProtocolBase(_RpcProtocolBase): + """Shared framing for the Smithy RPC v2 protocol family (rpcv2Cbor, rpcv2Json). + + Subclasses supply the codec-specific pieces: the protocol id, content type, + ``Smithy-Protocol`` header value, the empty-document sentinel, codec construction, + and error ``__type`` decoding. All HTTP/RPC framing lives here. + """ + + _id: ShapeID + _content_type: str + _smithy_protocol: str + # Serialized empty document (e.g. CBOR `{}` or JSON `{}`) used when a response body + # is absent so absent members take their defaults. + _empty_document: bytes + _codec: Codec + _service_name: str + + @property + def id(self) -> ShapeID: + return self._id + + @property + def payload_codec(self) -> Codec: + return self._codec + + @property + def content_type(self) -> str: + return self._content_type + + def _decode_document(self, body: bytes) -> object: + """Decode a serialized document to a Python object, for reading the error + ``__type`` discriminator. + """ + raise NotImplementedError + + def serialize_request[ + OperationInput: SerializeableShape, + OperationOutput: DeserializeableShape, + ]( + self, + *, + operation: APIOperation[OperationInput, OperationOutput], + input: OperationInput, + endpoint: URI, + context: TypedProperties, + ) -> HTTPRequest: + operation_name = operation.schema.id.name + path = f"/service/{self._service_name}/operation/{operation_name}" + + # An event-stream operation must negotiate HTTP/2 (see _requires_http2); mark + # the request so the transport does so for this operation alone, leaving + # non-streaming operations on HTTP/1.1. + http2_required = self._requires_http2(operation) + + # Accept describes the RESPONSE and Content-Type the REQUEST body, so they turn + # on output_stream_member and input_stream_member independently: a one-way + # stream advertises the event-stream media type on only the streaming side. + # Only an EVENT stream (union member) counts; a @streaming blob is a data stream + # and takes the normal media type. + accept = ( + _EVENT_STREAM_CONTENT_TYPE + if self._is_event_stream_member(operation.output_stream_member) + else self._content_type + ) + fields: list[tuple[str, str]] = [ + ("Smithy-Protocol", self._smithy_protocol), + ("Accept", accept), + ] + + if self._is_event_stream_member(operation.input_stream_member): + # RPC event streams carry the input as an initial-request event at the head + # of the stream body; the HTTP envelope is otherwise a normal RPC request. + fields.append(("Content-Type", _EVENT_STREAM_CONTENT_TYPE)) + return _HTTPRequest( + method="POST", + destination=_URI(host="", path=path), + fields=tuples_to_fields(fields), + body=self._build_initial_request_body(input, context), + http2_required=http2_required, + ) + + # The protocol omits the body AND the Content-Type header for operations whose + # input is the unit type (no modeled input). Every other operation serializes + # the input shape, even when it has no members. + if _is_unit(operation.input_schema): + payload = b"" + else: + payload = self._codec.serialize(shape=input) + fields.append(("Content-Type", self._content_type)) + fields.append(("Content-Length", str(len(payload)))) + + return _HTTPRequest( + method="POST", + destination=_URI(host="", path=path), + fields=tuples_to_fields(fields), + body=self._request_body(payload), + http2_required=http2_required, + ) + + def _deserialize_body[ + OperationInput: SerializeableShape, + OperationOutput: DeserializeableShape, + ]( + self, + *, + operation: APIOperation[OperationInput, OperationOutput], + response: HTTPResponse, + body: bytes, + error_registry: TypeRegistry, + context: TypedProperties, + ) -> OperationOutput: + if response.status != 200: + raise self._create_error( + operation=operation, + response=response, + body=body, + error_registry=error_registry, + context=context, + ) + # An empty body (unit output, or a server that elides the empty document) is a + # valid response: deserialize an empty document so absent members take defaults. + if not body or _is_unit(operation.output_schema): + body = self._empty_document + return self._codec.deserialize(source=body, shape=operation.output) + + def _create_error( + self, + *, + operation: APIOperation[Any, Any], + response: HTTPResponse, + body: bytes, + error_registry: TypeRegistry, + context: TypedProperties, + ) -> Exception: + # The protocol discriminates errors by a top-level `__type` member in the body + # holding the absolute error shape id (no error header). Resolve it against the + # registry, then deserialize the same body into the modeled error shape. + error_id: ShapeID | None = None + if body: + decoded = self._decode_document(body) + if isinstance(decoded, dict): + raw_type = cast("dict[str, object]", decoded).get("__type") + if isinstance(raw_type, str): + error_id = self._normalize_error_id(raw_type, operation) + + if error_id is not None and error_id in error_registry: + error_shape = error_registry.get(error_id) + if not issubclass(error_shape, ModeledError): + raise ExpectationNotMetError( + f"Modeled errors must derive from 'ModeledError', got {error_shape}" + ) + deserializer = self._codec.create_deserializer(body or self._empty_document) + return error_shape.deserialize(deserializer) + + message = ( + f"Unknown error for operation {operation.schema.id} " + f"- status: {response.status}" + ) + if error_id is not None: + message += f" - id: {error_id}" + return CallError( + message=message, + fault="client" if response.status < 500 else "server", + ) + + @staticmethod + def _normalize_error_id( + raw_type: str, operation: APIOperation[Any, Any] + ) -> ShapeID: + # `__type` may carry a full shape id, or (per spec) a name that may include a + # trailing "#"-qualified suffix. Match on the shape name against the operation's + # modeled errors so a bare or oddly-qualified name still resolves. + candidate = ShapeID(raw_type) + for error_schema in operation.error_schemas: + if error_schema.id == candidate or error_schema.id.name == candidate.name: + return error_schema.id + return candidate + + +class _RpcV2CborProtocolBase(_RpcV2ProtocolBase): + """The smithy.protocols#rpcv2Cbor codec specifics.""" + + _id: ShapeID = RpcV2CborTrait.id + _content_type: str = "application/cbor" + _smithy_protocol: str = "rpc-v2-cbor" + # CBOR encoding of an empty indefinite-length map (0xBF 0xFF), i.e. `{}`. + _empty_document: bytes = b"\xbf\xff" + + def __init__(self, settings: ProtocolSettings) -> None: + _assert_cbor() + self._service_name = settings.service_target + self._event_stream_http = self._resolve_event_stream_http( + settings.service_schema, self._id + ) + self._codec = CBORCodec(default_namespace=settings.namespace) + + def _decode_document(self, body: bytes) -> object: + return _cbor_loads(body) # type: ignore[possibly-unbound] + + +class HttpClientProtocol(_HttpProtocolBase, ClientProtocol[HTTPRequest, HTTPResponse]): + """An HTTP-based protocol.""" + + +class HttpBindingClientProtocol(_HttpBindingProtocolBase, HttpClientProtocol): + """An HTTP-based protocol that uses HTTP binding traits.""" + + def deserialize_response[ + OperationInput: "SerializeableShape", + OperationOutput: "DeserializeableShape", + ]( + self, + *, + operation: APIOperation[OperationInput, OperationOutput], + request: HTTPRequest, + response: HTTPResponse, + error_registry: TypeRegistry, + context: TypedProperties, + ) -> OperationOutput: + if not self._is_success(operation, context, response): + raise self._create_error( + operation=operation, + request=request, + response=response, + response_body=response.consume_body(), + error_registry=error_registry, + context=context, + ) + body = ( + None if self._has_streaming_output(operation) else response.consume_body() + ) + return self._deserialize_output(operation, response, body) + + +class RpcClientProtocol(_RpcProtocolBase, HttpClientProtocol): + """Base for the RPC-based protocols (awsJson, awsQuery, rpcv2).""" + + +class RpcV2CborClientProtocol(_RpcV2CborProtocolBase, RpcClientProtocol): + """An implementation of the smithy.protocols#rpcv2Cbor protocol.""" + + def deserialize_response[ + OperationInput: SerializeableShape, + OperationOutput: DeserializeableShape, + ]( + self, + *, + operation: APIOperation[OperationInput, OperationOutput], + request: HTTPRequest, + response: HTTPResponse, + error_registry: TypeRegistry, + context: TypedProperties, + ) -> OperationOutput: + self._raise_if_output_event_stream(operation) + return self._deserialize_body( + operation=operation, + response=response, + body=response.consume_body(), + error_registry=error_registry, + context=context, + ) diff --git a/packages/smithy-http/src/smithy_http/serializers.py b/packages/smithy-http/src/smithy_http/serializers.py index 182ae3562..8de9288e4 100644 --- a/packages/smithy-http/src/smithy_http/serializers.py +++ b/packages/smithy-http/src/smithy_http/serializers.py @@ -198,6 +198,7 @@ def __init__( endpoint_trait: EndpointTrait | None = None, omit_empty_payload: bool = True, operation_metadata: HTTPOperationSchemaMetadata | None = None, + async_body: bool = True, ) -> None: """Initialize an HTTPBindingSerializer. @@ -207,7 +208,9 @@ def __init__( :param endpoint_trait: The optional endpoint trait of the operation. :param omit_empty_payload: Whether an empty document payload should be omitted. :param operation_metadata: Optional cached operation binding metadata. + :param async_body: Whether to wrap the request body for async reads. """ + self._async_body = async_body self._operation_metadata = operation_metadata or ( HTTPOperationSchemaMetadata.from_traits(http_trait, endpoint_trait) ) @@ -218,7 +221,7 @@ def __init__( self._body_context: AbstractContextManager[ShapeSerializer] | None = None self._sync_payload: BytesIO | None = None self._raw_payload_serializer: RawPayloadSerializer | None = None - self._payload: AsyncBytesReader | AsyncBytesProvider | None = None + self._payload: AsyncStreamingBlob | None = None self._content_type: str | None = payload_codec.media_type self._content_length_required = False self._writes_document_body = False @@ -315,7 +318,11 @@ def build_request(self) -> HTTPRequest: ): content_length = self._sync_payload.tell() self._sync_payload.seek(0) - payload = AsyncBytesReader(self._sync_payload) + payload = ( + AsyncBytesReader(self._sync_payload) + if self._async_body + else self._sync_payload + ) headers = self.header_serializer.headers if self._content_type is not None and ( @@ -329,7 +336,12 @@ def build_request(self) -> HTTPRequest: fields, content_length, raw_payload, required=self._content_length_required ) if self._raw_payload_serializer is not None: - payload = AsyncBytesReader(raw_payload or b"") + if self._async_body: + payload = AsyncBytesReader(raw_payload or b"") + elif isinstance(raw_payload, bytes | bytearray) or raw_payload is None: + payload = BytesIO(raw_payload or b"") + else: + payload = raw_payload assert payload is not None # noqa: S101 diff --git a/packages/smithy-http/src/smithy_http/urllib3.py b/packages/smithy-http/src/smithy_http/urllib3.py new file mode 100644 index 000000000..145855717 --- /dev/null +++ b/packages/smithy-http/src/smithy_http/urllib3.py @@ -0,0 +1,128 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +from itertools import chain +from typing import TYPE_CHECKING, Any, Self + +if TYPE_CHECKING: + # pyright doesn't like optional imports. + import urllib3 + +try: + import urllib3 + + HAS_URLLIB3 = True +except ImportError: + HAS_URLLIB3 = False # type: ignore + +from smithy_core.exceptions import MissingDependencyError + +from . import Field, Fields +from .aio import HTTPResponse +from .aio.interfaces import HTTPRequest +from .aio.interfaces import HTTPResponse as HTTPResponseInterface +from .exceptions import SmithyHTTPError +from .interfaces import HTTPClient, HTTPClientConfiguration, HTTPRequestConfiguration + +__all__ = ["Urllib3HTTPClient", "Urllib3HTTPClientConfig"] + + +def _assert_urllib3() -> None: + if not HAS_URLLIB3: + raise MissingDependencyError( + "Attempted to use the urllib3 component, but urllib3 is not installed." + ) + + +class Urllib3HTTPClientConfig(HTTPClientConfiguration): + def __post_init__(self) -> None: + _assert_urllib3() + + +class Urllib3HTTPClient(HTTPClient): + """Implementation of :py:class:`.interfaces.HTTPClient` using urllib3.""" + + TIMEOUT_EXCEPTIONS = (urllib3.exceptions.TimeoutError,) if HAS_URLLIB3 else () + + def __init__( + self, + *, + client_config: Urllib3HTTPClientConfig | None = None, + _pool: "urllib3.PoolManager | None" = None, + ) -> None: + """ + :param client_config: Configuration that applies to all requests made with this + client. + """ + _assert_urllib3() + self._config = client_config or Urllib3HTTPClientConfig() + self._closed = False + # Advertise 'identity' to request uncompressed responses. + self._pool = _pool or urllib3.PoolManager( + headers={"Accept-Encoding": "identity"} + ) + + def send( + self, + request: HTTPRequest, + *, + request_config: HTTPRequestConfiguration | None = None, + ) -> HTTPResponseInterface: + """Send HTTP request using urllib3. + + :param request: The request including destination URI, fields, payload. + :param request_config: Configuration specific to this request. + """ + if self._closed: + raise SmithyHTTPError( + "Cannot send a request after the HTTP client has been closed." + ) + request_config = request_config or HTTPRequestConfiguration() + timeout = ( + urllib3.Timeout(read=request_config.read_timeout) + if request_config.read_timeout is not None + else None + ) + headers = urllib3.HTTPHeaderDict() + for name, value in chain.from_iterable( + fld.as_tuples() for fld in request.fields + ): + headers.add(name, value) + + raw = self._pool.request( + method=request.method, + url=request.destination.build(), + body=request.consume_body(), + headers=headers, + redirect=False, + retries=False, + preload_content=True, + decode_content=False, + timeout=timeout, + ) + fields = Fields() + for name, value in raw.headers.items(): + if name in fields: + fields[name].add(value) + else: + fields[name] = Field(name=name, values=[value], kind="header") + return HTTPResponse( + status=raw.status, fields=fields, body=raw.data, reason=raw.reason + ) + + def close(self) -> None: + """Close the underlying connection pool.""" + if self._closed: + return + self._closed = True + self._pool.clear() + + def __enter__(self) -> Self: + if self._closed: + raise SmithyHTTPError("Cannot enter an HTTP client that has been closed.") + return self + + def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None: + self.close() + + def __deepcopy__(self, memo: Any) -> Self: + return self diff --git a/scripts/api-snapshot/api.json b/scripts/api-snapshot/api.json index 28f12f110..ddcbeee3b 100644 --- a/scripts/api-snapshot/api.json +++ b/scripts/api-snapshot/api.json @@ -75,7 +75,6 @@ "smithy_aws_core.aio.auth.sigv4": { "AWSCredentialsIdentity": "rex-class", "AWSIdentityProperties": "rex-class", - "AWS_IDENTITY_CONFIG": "rex-PropertyKey", "AsyncAWSCredentialsResolver": "rex-type_alias", "AsyncAuthScheme": "rex-protocol", "AsyncEventSigner": "rex-class[async]", @@ -89,9 +88,7 @@ "SIGV4_CONFIG": "PropertyKey", "SigV4Config": "protocol", "SigV4Signer": "rex-type_alias", - "SigV4SigningProperties": "rex-class", - "SigV4Trait": "rex-class", - "SmithyIdentityError": "rex-exception" + "SigV4SigningProperties": "rex-class" }, "smithy_aws_core.aio.config": { "AsyncAwsConfig": "rex-class[async]", @@ -101,7 +98,6 @@ "AwsConfigOverrides": "rex-class", "aws_config": "rex-module", "context": "rex-module", - "file_parser": "rex-module", "filesystem": "rex-module", "load_config_async": "rex-function[async]", "resolvers": "rex-module" @@ -111,70 +107,53 @@ "AsyncFileSystem": "rex-protocol[async]", "AsyncSharedConfigContext": "rex-class[async]", "AwsConfigOverrides": "class", - "ConfigError": "rex-exception", "ConfigSource": "rex-class", - "ConfigValidationError": "rex-exception", "FieldSpec": "rex-class", - "Resolved": "rex-class", "RetryStrategyOptions": "rex-class", - "UNSET": "rex-_UnsetType", "resolve_endpoint_uri": "rex-function[async]", "resolve_max_attempts": "rex-function[async]", "resolve_region": "rex-function[async]", "resolve_retry_mode": "rex-function[async]", "resolve_sdk_ua_app_id": "rex-function[async]", - "validate_max_attempts": "rex-function", - "validate_profile": "rex-function", - "validate_region": "rex-function", - "validate_retry_mode": "rex-function", - "validate_sdk_ua_app_id": "rex-function" + "run_program_async": "rex-function[async]" }, "smithy_aws_core.aio.config.context": { "AsyncDefaultFileSystem": "rex-class[async]", "AsyncFileSystem": "rex-protocol[async]", "AsyncSharedConfigContext": "class[async]", - "ConfigSource": "rex-class", - "FileType": "rex-class", "MergedConfig": "rex-class", "load_config_async": "function[async]", - "logger": "Logger", - "parse_config_file_async": "rex-function[async]", - "standardize": "rex-function" + "run_program_async": "rex-function[async]" }, "smithy_aws_core.aio.config.file_parser": { "AsyncFileSystem": "rex-protocol[async]", - "ConfigParseError": "rex-exception", "RawParsedSections": "rex-type_alias", - "parse_config_file_async": "function[async]" + "parse_config_file_async": "function[async]", + "run_program_async": "rex-function[async]" }, "smithy_aws_core.aio.config.filesystem": { "AsyncDefaultFileSystem": "class[async]", "AsyncFileSystem": "protocol[async]", - "logger": "Logger" + "DefaultFileSystem": "rex-class" }, "smithy_aws_core.aio.config.resolvers": { "AsyncSharedConfigContext": "rex-class[async]", - "ConfigSource": "rex-class", - "ConfigValidationError": "rex-exception", "EndpointUriResolver": "class[async]", "Resolved": "rex-class", - "UNSET": "rex-_UnsetType", "resolve_endpoint_uri": "function[async]", "resolve_max_attempts": "function[async]", "resolve_region": "function[async]", "resolve_retry_mode": "function[async]", - "resolve_sdk_ua_app_id": "function[async]" + "resolve_sdk_ua_app_id": "function[async]", + "run_program_async": "rex-function[async]" }, "smithy_aws_core.aio.endpoints": {}, "smithy_aws_core.aio.endpoints.standard_regional": { "AsyncEndpointResolver": "rex-protocol[async]", "AsyncStandardRegionalEndpointsResolver": "class[async]", "Endpoint": "rex-class", - "EndpointResolutionError": "rex-exception", "EndpointResolverParams": "rex-class", - "REGIONAL_ENDPOINT_CONFIG": "rex-PropertyKey", - "URI": "rex-class", - "resolve_static_uri": "rex-function" + "StandardRegionalEndpointsResolver": "rex-class" }, "smithy_aws_core.aio.identity": { "AWSCredentialsIdentity": "rex-class", @@ -200,26 +179,18 @@ "static": "rex-module" }, "smithy_aws_core.aio.identity.chain": { - "After": "rex-class", "AsyncHTTPClient": "rex-protocol[async]", "AsyncIdentityChain": "class[async]", "AsyncIdentityResolver": "rex-protocol[async]", - "Before": "rex-class", "ChainIdentityProvider": "rex-protocol[async]", "ChainSetup": "rex-class", "Identity": "rex-protocol", - "IdentityChainConfigurationError": "rex-exception", - "IdentityChainError": "rex-exception", - "IdentityResolverFailure": "rex-class", "MergedConfig": "rex-class", - "NamedResolver": "rex-class[async]", - "SmithyIdentityError": "rex-exception", - "Standard": "rex-class", - "StandardProvider": "rex-class", "UnclaimedSource": "rex-class", "logger": "Logger", "provider": "rex-module", - "providers": "rex-module" + "providers": "rex-module", + "run_program_async": "rex-function[async]" }, "smithy_aws_core.aio.identity.chain.provider": { "AsyncHTTPClient": "rex-protocol[async]", @@ -238,22 +209,16 @@ "shared_config": "rex-module" }, "smithy_aws_core.aio.identity.chain.providers.environment": { - "AWSCredentialsIdentity": "rex-class", "AsyncEnvironmentCredentialsProvider": "class[async]", "AsyncEnvironmentCredentialsResolver": "rex-class[async]", "ChainSetup": "rex-class", - "Identity": "rex-protocol", - "Standard": "rex-class", - "StandardProvider": "rex-class" + "Identity": "rex-protocol" }, "smithy_aws_core.aio.identity.chain.providers.process": { - "AWSCredentialsIdentity": "rex-class", "AsyncProcessCredentialsResolver": "rex-class[async]", "AsyncProfileCredentialProcessProvider": "class[async]", "ChainSetup": "rex-class", - "Identity": "rex-protocol", - "Standard": "rex-class", - "StandardProvider": "rex-class" + "Identity": "rex-protocol" }, "smithy_aws_core.aio.identity.chain.providers.profile": { "AWSCredentialsIdentity": "rex-class", @@ -261,18 +226,14 @@ "AsyncProfileStaticCredentialsProvider": "class[async]", "AsyncStaticCredentialsResolver": "rex-class[async]", "ChainSetup": "rex-class", - "Identity": "rex-protocol", - "Standard": "rex-class", - "StandardProvider": "rex-class" + "Identity": "rex-protocol" }, "smithy_aws_core.aio.identity.chain.providers.shared_config": { - "AWSCredentialsIdentity": "rex-class", "AsyncSharedConfigProvider": "class[async]", "ChainSetup": "rex-class", "Identity": "rex-protocol", - "Standard": "rex-class", - "StandardProvider": "rex-class", - "load_config_async": "rex-function[async]" + "load_config_async": "rex-function[async]", + "run_program_async": "rex-function[async]" }, "smithy_aws_core.aio.identity.container": { "AWSCredentialsIdentity": "rex-class", @@ -330,7 +291,7 @@ }, "smithy_aws_core.aio.protocols": { "APIOperation": "rex-class", - "AWSErrorIdentifier": "class", + "AWSErrorIdentifier": "rex-class", "AWSEventPublisher": "rex-class[async]", "AWSEventReceiver": "rex-class[async]", "AWSJSONDocument": "rex-class", @@ -349,89 +310,105 @@ "AsyncWriter": "rex-protocol[async]", "AwsJson1_0Trait": "rex-class", "AwsJson1_1Trait": "rex-class", - "AwsQueryTrait": "rex-class", - "BytesReader": "rex-protocol", - "BytesWriter": "rex-protocol", - "CallError": "rex-exception", - "Codec": "rex-protocol", - "DOCUMENT": "rex-Schema", "DeserializeableShape": "rex-protocol", - "DiscriminatorError": "rex-exception", - "Ec2QueryShapeSerializer": "rex-class", - "Ec2QueryTrait": "rex-class", - "ExpectationNotMetError": "rex-exception", - "HTTPErrorIdentifier": "rex-class", "HTTPRequest": "rex-protocol[async]", "HTTPResponse": "rex-protocol[async]", - "HTTPResponseDeserializer": "rex-class", - "HTTPTrait": "rex-class", - "JSONCodec": "rex-class", - "JSONDocument": "rex-class", "MissingDependencyError": "rex-exception", "MissingInitialResponse": "rex-exception", - "ModeledError": "rex-exception", - "PropertyKey": "rex-class", "ProtocolSettings": "rex-class", - "QueryShapeSerializer": "rex-class", - "ResponseMetadata": "rex-class", - "RestJson1Trait": "rex-class", - "RestXmlTrait": "rex-class", "SerializeableShape": "rex-protocol", "ShapeDeserializer": "rex-protocol", "ShapeID": "rex-class", - "ShapeSerializer": "rex-protocol", - "ShapeType": "rex-class", "SigningConfig": "rex-class", - "SyncStreamingBlob": "rex-type_alias", - "TimestampFormat": "rex-class", "TypeRegistry": "rex-class", "TypedProperties": "rex-protocol", - "URI": "rex-protocol", - "UnsupportedStreamError": "rex-exception", - "XMLCodec": "rex-class", - "create_aws_query_error": "rex-function", - "parse_aws_query_request_id": "rex-function", - "parse_document_discriminator": "rex-function", - "parse_error_code": "rex-function", - "parse_response_metadata": "rex-function", - "parse_rest_xml_error": "rex-function", - "parse_retry_after": "rex-function", - "parse_xml_root": "rex-function", - "tuples_to_fields": "rex-function" + "UnsupportedStreamError": "rex-exception" }, "smithy_aws_core.auth": { "SigV4AuthScheme": "rex-class", "sigv4": "rex-module" }, "smithy_aws_core.auth.sigv4": { - "SigV4AuthScheme": "class" + "AWSCredentialsIdentity": "rex-class", + "AWSCredentialsResolver": "rex-type_alias", + "AWSIdentityProperties": "rex-class", + "AWS_IDENTITY_CONFIG": "rex-PropertyKey", + "AuthScheme": "rex-protocol", + "HTTPRequest": "rex-protocol[async]", + "SigV4AuthScheme": "class", + "SigV4Signer": "rex-class", + "SigV4SigningProperties": "rex-class", + "SigV4Trait": "rex-class", + "Signer": "rex-protocol", + "SmithyIdentityError": "rex-exception" }, "smithy_aws_core.config": { "AsyncAwsConfig": "rex-class[async]", "AsyncDefaultFileSystem": "rex-class[async]", "AsyncFileSystem": "rex-protocol[async]", "AsyncSharedConfigContext": "rex-class[async]", + "AwsConfig": "rex-class", "AwsConfigOverrides": "rex-class", "ConfigError": "rex-exception", "ConfigParseError": "rex-exception", "ConfigSource": "rex-class", "ConfigValidationError": "rex-exception", + "DefaultFileSystem": "rex-class", + "FileSystem": "rex-protocol", "MergedConfig": "rex-class", "ProfileNotFoundError": "rex-exception", + "SharedConfigContext": "rex-class", + "aws_config": "rex-module", "context": "rex-module", "exceptions": "rex-module", "file_parser": "rex-module", + "filesystem": "rex-module", + "load_config": "rex-function", "load_config_async": "rex-function[async]", "merged_config": "rex-module", + "resolvers": "rex-module", "shared_config_files_exist": "rex-function", "types": "rex-module", "validators": "rex-module" }, "smithy_aws_core.config.aws_config": { - "AwsConfig": "class" + "AwsConfig": "class", + "AwsConfigOverrides": "class", + "ConfigError": "rex-exception", + "ConfigSource": "rex-class", + "ConfigValidationError": "rex-exception", + "FieldSpec": "rex-class", + "FileSystem": "rex-protocol", + "Program": "rex-type_alias", + "Resolved": "rex-class", + "RetryStrategyOptions": "rex-class", + "SharedConfigContext": "rex-class", + "UNSET": "rex-_UnsetType", + "resolve_endpoint_uri": "rex-function", + "resolve_max_attempts": "rex-function", + "resolve_region": "rex-function", + "resolve_retry_mode": "rex-function", + "resolve_sdk_ua_app_id": "rex-function", + "run_program": "rex-function", + "validate_max_attempts": "rex-function", + "validate_profile": "rex-function", + "validate_region": "rex-function", + "validate_retry_mode": "rex-function", + "validate_sdk_ua_app_id": "rex-function" }, "smithy_aws_core.config.context": { - "shared_config_files_exist": "function" + "ConfigSource": "rex-class", + "DefaultFileSystem": "rex-class", + "FileSystem": "rex-protocol", + "FileType": "rex-class", + "MergedConfig": "rex-class", + "Program": "rex-type_alias", + "SharedConfigContext": "class", + "load_config": "function", + "logger": "Logger", + "run_program": "rex-function", + "shared_config_files_exist": "function", + "standardize": "rex-function" }, "smithy_aws_core.config.exceptions": { "ConfigError": "exception", @@ -443,19 +420,43 @@ "smithy_aws_core.config.file_parser": { "ConfigParseError": "rex-exception", "FileType": "class", + "Program": "rex-type_alias", "RawParsedSections": "rex-type_alias", "Section": "class", "SectionMap": "rex-type_alias", "StandardizedOutput": "class", "logger": "Logger", + "parse_config_file": "function", + "run_program": "rex-function", "standardize": "function" }, + "smithy_aws_core.config.filesystem": { + "DefaultFileSystem": "class", + "FileSystem": "protocol", + "logger": "Logger" + }, "smithy_aws_core.config.merged_config": { "MergedConfig": "class", "Section": "rex-class", "SectionMap": "rex-type_alias", "StandardizedOutput": "rex-class" }, + "smithy_aws_core.config.resolvers": { + "ConfigSource": "rex-class", + "ConfigValidationError": "rex-exception", + "EndpointUriResolver": "class", + "MergedConfig": "rex-class", + "Program": "rex-type_alias", + "Resolved": "rex-class", + "SharedConfigContext": "rex-class", + "UNSET": "rex-_UnsetType", + "resolve_endpoint_uri": "function", + "resolve_max_attempts": "function", + "resolve_region": "function", + "resolve_retry_mode": "function", + "resolve_sdk_ua_app_id": "function", + "run_program": "rex-function" + }, "smithy_aws_core.config.types": { "ConfigSource": "class", "FieldSpec": "class", @@ -477,7 +478,18 @@ "PropertyKey": "rex-class", "REGIONAL_ENDPOINT_CONFIG": "PropertyKey", "RegionalEndpointConfig": "class", - "StaticEndpointConfig": "rex-protocol" + "StaticEndpointConfig": "rex-protocol", + "standard_regional": "rex-module" + }, + "smithy_aws_core.endpoints.standard_regional": { + "Endpoint": "rex-class", + "EndpointResolutionError": "rex-exception", + "EndpointResolver": "rex-protocol", + "EndpointResolverParams": "rex-class", + "REGIONAL_ENDPOINT_CONFIG": "rex-PropertyKey", + "StandardRegionalEndpointsResolver": "class", + "URI": "rex-class", + "resolve_static_uri": "rex-function" }, "smithy_aws_core.identity": { "AWSCredentialsIdentity": "rex-class", @@ -485,8 +497,9 @@ "AWSIdentityConfig": "rex-protocol", "AWSIdentityProperties": "rex-class", "AWS_IDENTITY_CONFIG": "PropertyKey", - "EnvironmentCredentialsResolver": "rex-class[async]", + "EnvironmentCredentialsResolver": "rex-class", "IdentityChainError": "rex-exception", + "ProcessCredentialsResolver": "rex-class", "PropertyKey": "rex-class", "StaticCredentialsResolver": "rex-class", "UnclaimedSource": "rex-class", @@ -495,11 +508,13 @@ "container": "rex-module", "environment": "rex-module", "imds": "rex-module", + "process": "rex-module", "static": "rex-module" }, "smithy_aws_core.identity.chain": { "After": "rex-class", "Before": "rex-class", + "IdentityChain": "rex-class", "IdentityChainConfigurationError": "rex-exception", "IdentityChainError": "rex-exception", "IdentityResolverFailure": "rex-class", @@ -509,7 +524,9 @@ "UnclaimedSource": "rex-class", "exceptions": "rex-module", "ordering": "rex-module", - "providers": "rex-module" + "provider": "rex-module", + "providers": "rex-module", + "resolver": "rex-module" }, "smithy_aws_core.identity.chain.exceptions": { "IdentityChainConfigurationError": "exception", @@ -527,12 +544,88 @@ "StandardProvider": "class", "shared_config_files_exist": "rex-function" }, + "smithy_aws_core.identity.chain.provider": { + "ChainIdentityProvider": "protocol", + "ChainSetup": "class", + "HTTPClient": "rex-protocol", + "Identity": "rex-protocol", + "IdentityResolver": "rex-protocol", + "MergedConfig": "rex-class", + "NamedResolver": "class", + "OrderingConstraint": "rex-type_alias" + }, "smithy_aws_core.identity.chain.providers": { - "process": "rex-module" + "EnvironmentCredentialsProvider": "rex-class", + "ProfileCredentialProcessProvider": "rex-class", + "ProfileSessionCredentialsProvider": "rex-class", + "ProfileStaticCredentialsProvider": "rex-class", + "SharedConfigProvider": "rex-class", + "environment": "rex-module", + "process": "rex-module", + "profile": "rex-module", + "shared_config": "rex-module" + }, + "smithy_aws_core.identity.chain.providers.environment": { + "AWSCredentialsIdentity": "rex-class", + "ChainSetup": "rex-class", + "EnvironmentCredentialsProvider": "class", + "EnvironmentCredentialsResolver": "rex-class", + "Identity": "rex-protocol", + "Standard": "rex-class", + "StandardProvider": "rex-class" }, "smithy_aws_core.identity.chain.providers.process": { + "AWSCredentialsIdentity": "rex-class", + "ChainSetup": "rex-class", + "Identity": "rex-protocol", "ProcessConfigurationError": "exception", - "SmithyError": "rex-exception" + "ProcessCredentialsResolver": "rex-class", + "ProfileCredentialProcessProvider": "class", + "SmithyError": "rex-exception", + "Standard": "rex-class", + "StandardProvider": "rex-class" + }, + "smithy_aws_core.identity.chain.providers.profile": { + "AWSCredentialsIdentity": "rex-class", + "ChainSetup": "rex-class", + "Identity": "rex-protocol", + "ProfileSessionCredentialsProvider": "class", + "ProfileStaticCredentialsProvider": "class", + "Standard": "rex-class", + "StandardProvider": "rex-class", + "StaticCredentialsResolver": "rex-class" + }, + "smithy_aws_core.identity.chain.providers.shared_config": { + "AWSCredentialsIdentity": "rex-class", + "ChainSetup": "rex-class", + "Identity": "rex-protocol", + "Program": "rex-type_alias", + "SharedConfigProvider": "class", + "Standard": "rex-class", + "StandardProvider": "rex-class", + "load_config": "rex-function", + "run_program": "rex-function" + }, + "smithy_aws_core.identity.chain.resolver": { + "After": "rex-class", + "Before": "rex-class", + "ChainIdentityProvider": "rex-protocol", + "ChainSetup": "rex-class", + "HTTPClient": "rex-protocol", + "Identity": "rex-protocol", + "IdentityChain": "class", + "IdentityChainConfigurationError": "rex-exception", + "IdentityChainError": "rex-exception", + "IdentityResolver": "rex-protocol", + "IdentityResolverFailure": "rex-class", + "MergedConfig": "rex-class", + "Program": "rex-type_alias", + "SmithyIdentityError": "rex-exception", + "Standard": "rex-class", + "StandardProvider": "rex-class", + "UnclaimedSource": "rex-class", + "logger": "Logger", + "run_program": "rex-function" }, "smithy_aws_core.identity.components": { "AWSCredentialsIdentity": "class", @@ -549,11 +642,19 @@ "AWSCredentialsIdentity": "rex-class", "AWSIdentityProperties": "rex-class", "EnvironmentCredentialsResolver": "class", + "IdentityResolver": "rex-protocol", "SmithyIdentityError": "rex-exception" }, "smithy_aws_core.identity.imds": { "Token": "class" }, + "smithy_aws_core.identity.process": { + "AWSCredentialsIdentity": "rex-class", + "AWSIdentityProperties": "rex-class", + "IdentityResolver": "rex-protocol", + "ProcessCredentialsResolver": "class", + "SmithyIdentityError": "rex-exception" + }, "smithy_aws_core.identity.static": { "AWSCredentialsIdentity": "rex-class", "AWSIdentityProperties": "rex-class", @@ -572,6 +673,67 @@ "smithy_core": "rex-module" }, "smithy_aws_core.interfaces": {}, + "smithy_aws_core.protocols": { + "APIOperation": "rex-class", + "AWSErrorIdentifier": "class", + "AWSJSONDocument": "rex-class", + "AwsJson10ClientProtocol": "class", + "AwsJson11ClientProtocol": "class", + "AwsJson1_0Trait": "rex-class", + "AwsJson1_1Trait": "rex-class", + "AwsQueryClientProtocol": "class", + "AwsQueryTrait": "rex-class", + "BytesReader": "rex-protocol", + "BytesWriter": "rex-protocol", + "CallError": "rex-exception", + "Codec": "rex-protocol", + "DOCUMENT": "rex-Schema", + "DeserializeableShape": "rex-protocol", + "DiscriminatorError": "rex-exception", + "Ec2QueryClientProtocol": "class", + "Ec2QueryShapeSerializer": "rex-class", + "Ec2QueryTrait": "rex-class", + "ExpectationNotMetError": "rex-exception", + "HTTPErrorIdentifier": "rex-class", + "HTTPRequest": "rex-protocol[async]", + "HTTPResponse": "rex-protocol[async]", + "HTTPResponseDeserializer": "rex-class", + "HTTPTrait": "rex-class", + "HttpBindingClientProtocol": "rex-class", + "JSONCodec": "rex-class", + "JSONDocument": "rex-class", + "MissingDependencyError": "rex-exception", + "ModeledError": "rex-exception", + "PropertyKey": "rex-class", + "ProtocolSettings": "rex-class", + "QueryShapeSerializer": "rex-class", + "ResponseMetadata": "rex-class", + "RestJson1Trait": "rex-class", + "RestJsonClientProtocol": "class", + "RestXmlClientProtocol": "class", + "RestXmlTrait": "rex-class", + "RpcClientProtocol": "rex-class", + "SerializeableShape": "rex-protocol", + "ShapeDeserializer": "rex-protocol", + "ShapeID": "rex-class", + "ShapeSerializer": "rex-protocol", + "ShapeType": "rex-class", + "SyncStreamingBlob": "rex-type_alias", + "TimestampFormat": "rex-class", + "TypeRegistry": "rex-class", + "TypedProperties": "rex-protocol", + "URI": "rex-protocol", + "XMLCodec": "rex-class", + "create_aws_query_error": "rex-function", + "parse_aws_query_request_id": "rex-function", + "parse_document_discriminator": "rex-function", + "parse_error_code": "rex-function", + "parse_response_metadata": "rex-function", + "parse_rest_xml_error": "rex-function", + "parse_retry_after": "rex-function", + "parse_xml_root": "rex-function", + "tuples_to_fields": "rex-function" + }, "smithy_aws_core.traits": { "AwsJson1_0Trait": "class", "AwsJson1_1Trait": "class", @@ -737,41 +899,30 @@ "AsyncRequestPipeline": "class[async]", "AsyncRetryStrategy": "rex-protocol[async]", "AsyncRetryStrategyResolver": "rex-class[async]", - "AuthOption": "rex-protocol", - "AuthParams": "rex-class", "AuthSchemeResolver": "rex-protocol", - "ClientTimeoutError": "rex-exception", "DeserializeableShape": "rex-protocol", - "Endpoint": "rex-protocol", - "EndpointResolverParams": "rex-class", - "InputContext": "rex-class", "Interceptor": "rex-class", "InterceptorChain": "rex-class", "OutputContext": "rex-class", "PropertyKey": "rex-class", "Request": "rex-protocol[async]", "RequestContext": "rex-class", + "RequestPipelineBase": "rex-class", "Response": "rex-protocol[async]", - "ResponseContext": "rex-class", - "RetryError": "rex-exception", "SerializeableShape": "rex-protocol", "ShapeDeserializer": "rex-protocol", "ShapeID": "rex-class", - "ShapeType": "rex-class", - "SmithyError": "rex-exception", "TypedProperties": "rex-protocol", - "URI": "rex-class", "UnsupportedTransportError": "rex-exception", "close": "rex-function[async]", - "seek": "rex-function[async]" + "is_retryable": "rex-function", + "run_program_async": "rex-function[async]" }, "smithy_core.aio.endpoints": { "AsyncEndpointResolver": "rex-protocol[async]", "AsyncStaticEndpointResolver": "class[async]", - "Endpoint": "rex-class", - "EndpointResolutionError": "rex-exception", "EndpointResolverParams": "rex-class", - "resolve_static_uri": "rex-function" + "StaticEndpointResolver": "rex-class" }, "smithy_core.aio.eventstream": { "AsyncDuplexEventStream": "class[async]", @@ -790,8 +941,8 @@ "AsyncChainedIdentityResolver": "class[async]", "AsyncIdentityResolver": "rex-protocol[async]", "Identity": "rex-protocol", - "SmithyIdentityError": "rex-exception", - "logger": "Logger" + "logger": "Logger", + "run_program_async": "rex-function[async]" }, "smithy_core.aio.interfaces": { "AsyncByteStream": "protocol[async]", @@ -852,10 +1003,6 @@ "AsyncSimpleRetryStrategy": "class[async]", "AsyncStandardRetryStrategy": "class[async]", "AsyncTokenBucket": "class[async]", - "CubicCalculator": "rex-class", - "ExponentialBackoffJitterType": "rex-class", - "ExponentialRetryBackoffStrategy": "rex-class", - "RetryError": "rex-exception", "RetryStrategyOptions": "rex-class", "SimpleRetryToken": "rex-class", "StandardRetryQuota": "rex-class", @@ -896,6 +1043,32 @@ "ShapeID": "rex-class", "TypedProperties": "rex-class" }, + "smithy_core.client": { + "APIOperation": "rex-class", + "AUTH_SCHEME": "PropertyKey", + "AdaptiveRetryStrategy": "rex-class", + "AuthScheme": "rex-protocol", + "AuthSchemeResolver": "rex-protocol", + "Client": "class", + "ClientCall": "class", + "ClientProtocol": "rex-protocol", + "ClientTransport": "rex-protocol", + "DeserializeableShape": "rex-protocol", + "EndpointResolver": "rex-protocol", + "Interceptor": "rex-class", + "InterceptorChain": "rex-class", + "PropertyKey": "rex-class", + "RequestContext": "rex-class", + "RequestPipeline": "class", + "RequestPipelineBase": "rex-class", + "RetryStrategy": "rex-protocol", + "RetryStrategyResolver": "rex-class", + "SerializeableShape": "rex-protocol", + "ShapeID": "rex-class", + "TypedProperties": "rex-protocol", + "is_retryable": "rex-function", + "run_program": "rex-function" + }, "smithy_core.codecs": { "BytesReader": "rex-protocol", "BytesWriter": "rex-protocol", @@ -935,6 +1108,7 @@ "STATIC_ENDPOINT_CONFIG": "PropertyKey", "SerializeableShape": "rex-protocol", "StaticEndpointConfig": "protocol", + "StaticEndpointResolver": "class", "TypedProperties": "rex-class", "URI": "rex-class", "resolve_static_uri": "function" @@ -958,6 +1132,16 @@ "UnsupportedStreamError": "exception", "UnsupportedTransportError": "exception" }, + "smithy_core.identity": { + "CachingIdentityResolver": "class", + "ChainedIdentityResolver": "class", + "Identity": "rex-protocol", + "IdentityResolver": "rex-protocol", + "Program": "rex-type_alias", + "SmithyIdentityError": "rex-exception", + "logger": "Logger", + "run_program": "rex-function" + }, "smithy_core.interceptors": { "AnyInterceptor": "callable", "DeserializeableShape": "rex-protocol", @@ -996,13 +1180,16 @@ "AuthScheme": "protocol", "AuthSchemeResolver": "protocol", "EventSigner": "protocol", + "Identity": "rex-protocol", "ShapeID": "rex-class", "Signer": "protocol", "TypedProperties": "rex-protocol" }, "smithy_core.interfaces.eventstream": { + "DeserializeableShape": "rex-protocol", "EventPublisher": "protocol", - "EventReceiver": "protocol" + "EventReceiver": "protocol", + "SerializeableShape": "rex-protocol" }, "smithy_core.interfaces.exceptions": { "Fault": "rex-type_alias", @@ -1052,15 +1239,22 @@ "ResponseMetadata": "class" }, "smithy_core.retries": { + "AdaptiveRetryStrategy": "class", + "ClientRateLimiter": "class", "CubicCalculator": "class", "ExponentialBackoffJitterType": "class", "ExponentialRetryBackoffStrategy": "class", + "RequestRateTracker": "class", "RetryError": "rex-exception", "RetryStrategyOptions": "class", + "RetryStrategyResolver": "class", "RetryStrategyType": "type_alias", + "SimpleRetryStrategy": "class", "SimpleRetryToken": "class", "StandardRetryQuota": "class", + "StandardRetryStrategy": "class", "StandardRetryToken": "class", + "TokenBucket": "class", "retries_interface": "rex-module" }, "smithy_core.rfc3986": { @@ -1205,6 +1399,7 @@ "deserializers": "rex-module", "interceptors": "rex-module", "interfaces": "rex-module", + "protocols": "rex-module", "quote_and_escape_field_value": "function", "schema_extensions": "rex-module", "serializers": "rex-module", @@ -1247,18 +1442,15 @@ "APIKeyIdentityProperties": "rex-class", "APIKeyLocation": "rex-class", "APIKeyResolverConfig": "protocol", + "APIKeySigner": "rex-class", "API_KEY_RESOLVER_CONFIG": "PropertyKey", "AsyncAPIKeyAuthScheme": "class", "AsyncAPIKeySigner": "class[async]", "AsyncAuthScheme": "rex-protocol", "AsyncIdentityResolver": "rex-protocol[async]", "AsyncSigner": "rex-protocol[async]", - "Field": "rex-class", - "HTTPAPIKeyAuthTrait": "rex-class", "HTTPRequest": "rex-protocol[async]", - "PropertyKey": "rex-class", - "SmithyIdentityError": "rex-exception", - "URI": "rex-class" + "PropertyKey": "rex-class" }, "smithy_http.aio.crt": { "AWSCRTHTTPClient": "class[async]", @@ -1282,9 +1474,9 @@ "smithy_http.aio.identity.apikey": { "APIKeyIdentity": "rex-class", "APIKeyIdentityProperties": "rex-class", + "APIKeyIdentityResolver": "rex-class", "AsyncAPIKeyIdentityResolver": "class[async]", - "AsyncIdentityResolver": "rex-protocol[async]", - "SmithyIdentityError": "rex-exception" + "AsyncIdentityResolver": "rex-protocol[async]" }, "smithy_http.aio.interfaces": { "APIOperation": "rex-class", @@ -1320,46 +1512,20 @@ "AsyncRpcV2CborClientProtocol": "class[async]", "AsyncStreamingBlob": "rex-type_alias", "AsyncWriter": "rex-protocol[async]", - "CBORCodec": "rex-class", - "CallError": "rex-exception", - "Codec": "rex-protocol", - "DOCUMENT": "rex-Schema", - "DefaultTrait": "rex-class", "DeserializeableShape": "rex-protocol", - "Endpoint": "rex-protocol", "ExpectationNotMetError": "rex-exception", - "HTTPBindingSerializer": "rex-class", - "HTTPErrorIdentifier": "rex-class", "HTTPRequest": "rex-protocol[async]", - "HTTPRequestSerializer": "rex-class", "HTTPResponse": "rex-protocol[async]", - "HTTPResponseDeserializer": "rex-class", - "HTTP_BINDING_SCHEMA_EXTENSION": "rex-SchemaExtension", - "HTTP_OPERATION_SCHEMA_EXTENSION": "rex-SchemaExtension", "MissingDependencyError": "rex-exception", "MissingInitialResponse": "rex-exception", - "ModeledError": "rex-exception", - "ORIGINAL_SHAPE_ID": "rex-ShapeID", "PropertyKey": "rex-class", - "ProtocolSettings": "rex-class", - "RequiredTrait": "rex-class", - "ResponseMetadata": "rex-class", - "RpcV2CborTrait": "rex-class", - "Schema": "rex-class", - "SeekableBytesReader": "rex-protocol", "SerializeableShape": "rex-protocol", - "SerializeableStruct": "rex-protocol", "ShapeDeserializer": "rex-protocol", - "ShapeID": "rex-class", - "ShapeType": "rex-class", "SigningConfig": "rex-class", "SyncStreamingBlob": "rex-type_alias", "TypeRegistry": "rex-class", "TypedProperties": "rex-protocol", - "UNIT": "rex-Schema", - "URI": "rex-protocol", - "UnsupportedStreamError": "rex-exception", - "tuples_to_fields": "rex-function" + "UnsupportedStreamError": "rex-exception" }, "smithy_http.aio.restjson": { "DocumentValue": "rex-type_alias", @@ -1368,10 +1534,24 @@ "expect_type": "rex-function", "parse_rest_json_error_info": "function[async]" }, - "smithy_http.auth": {}, + "smithy_http.auth": { + "apikey": "rex-module" + }, "smithy_http.auth.apikey": { "APIKeyAuthScheme": "class", - "APIKeySigner": "class" + "APIKeyIdentity": "rex-class", + "APIKeyIdentityProperties": "rex-class", + "APIKeyLocation": "rex-class", + "APIKeyResolverConfig": "protocol", + "APIKeySigner": "class", + "AuthScheme": "rex-protocol", + "Field": "rex-class", + "HTTPAPIKeyAuthTrait": "rex-class", + "HTTPRequest": "rex-protocol[async]", + "IdentityResolver": "rex-protocol", + "Signer": "rex-protocol", + "SmithyIdentityError": "rex-exception", + "URI": "rex-class" }, "smithy_http.bindings": { "Binding": "class", @@ -1442,7 +1622,9 @@ "APIKeyIdentity": "class", "APIKeyIdentityProperties": "class", "APIKeyIdentityResolver": "class", - "Identity": "rex-protocol" + "Identity": "rex-protocol", + "IdentityResolver": "rex-protocol", + "SmithyIdentityError": "rex-exception" }, "smithy_http.interceptors": { "user_agent": "rex-module" @@ -1475,9 +1657,48 @@ "user_agent_plugin": "function" }, "smithy_http.protocols": { + "APIOperation": "rex-class", + "CBORCodec": "rex-class", + "CallError": "rex-exception", + "ClientProtocol": "rex-protocol", + "Codec": "rex-protocol", + "DOCUMENT": "rex-Schema", + "DefaultTrait": "rex-class", + "DeserializeableShape": "rex-protocol", + "Endpoint": "rex-protocol", + "ExpectationNotMetError": "rex-exception", + "HTTPBindingSerializer": "rex-class", + "HTTPErrorIdentifier": "rex-class", + "HTTPRequest": "rex-protocol[async]", + "HTTPRequestSerializer": "rex-class", + "HTTPResponse": "rex-protocol[async]", + "HTTPResponseDeserializer": "rex-class", + "HTTP_BINDING_SCHEMA_EXTENSION": "rex-SchemaExtension", + "HTTP_OPERATION_SCHEMA_EXTENSION": "rex-SchemaExtension", "HttpBindingClientProtocol": "class", "HttpClientProtocol": "class", - "RpcV2CborClientProtocol": "class" + "MissingDependencyError": "rex-exception", + "ModeledError": "rex-exception", + "ORIGINAL_SHAPE_ID": "rex-ShapeID", + "ProtocolSettings": "rex-class", + "RequiredTrait": "rex-class", + "ResponseMetadata": "rex-class", + "RpcClientProtocol": "class", + "RpcV2CborClientProtocol": "class", + "RpcV2CborTrait": "rex-class", + "Schema": "rex-class", + "SeekableBytesReader": "rex-protocol", + "SerializeableShape": "rex-protocol", + "SerializeableStruct": "rex-protocol", + "ShapeID": "rex-class", + "ShapeType": "rex-class", + "SyncStreamingBlob": "rex-type_alias", + "TypeRegistry": "rex-class", + "TypedProperties": "rex-protocol", + "UNIT": "rex-Schema", + "URI": "rex-protocol", + "UnsupportedStreamError": "rex-exception", + "tuples_to_fields": "rex-function" }, "smithy_http.restjson": { "DocumentValue": "rex-type_alias", @@ -1586,6 +1807,20 @@ "create_test_request": "function", "tuples_to_fields": "rex-function" }, + "smithy_http.urllib3": { + "Field": "rex-class", + "Fields": "rex-class", + "HTTPClient": "rex-protocol", + "HTTPClientConfiguration": "rex-class", + "HTTPRequest": "rex-protocol[async]", + "HTTPRequestConfiguration": "rex-class", + "HTTPResponse": "rex-class[async]", + "HTTPResponseInterface": "rex-protocol[async]", + "MissingDependencyError": "rex-exception", + "SmithyHTTPError": "rex-exception", + "Urllib3HTTPClient": "class", + "Urllib3HTTPClientConfig": "class" + }, "smithy_http.user_agent": { "RawStringUserAgentComponent": "class", "UserAgent": "class", diff --git a/scripts/gen-client/build.gradle.kts b/scripts/gen-client/build.gradle.kts new file mode 100644 index 000000000..d7d0e9609 --- /dev/null +++ b/scripts/gen-client/build.gradle.kts @@ -0,0 +1,32 @@ +plugins { + java + alias(libs.plugins.smithy.gradle.base) +} + +repositories { + mavenLocal() + mavenCentral() +} + +smithy { + smithyBuildConfigs.set(files(providers.gradleProperty("smithyBuildConfig").get())) +} + +val smithyVersion = libs.versions.smithy.asProvider().get() + +dependencies { + // Resolved to the included codegen build's :core and :aws:core projects. + smithyBuild("software.amazon.smithy.python.codegen:core:0.0.0") + smithyBuild("software.amazon.smithy.python.codegen.aws:core:0.0.0") + + smithyBuild("software.amazon.smithy:smithy-aws-traits:$smithyVersion") + smithyBuild("software.amazon.smithy:smithy-aws-iam-traits:$smithyVersion") + smithyBuild("software.amazon.smithy:smithy-aws-cloudformation-traits:$smithyVersion") + smithyBuild("software.amazon.smithy:smithy-aws-endpoints:$smithyVersion") + smithyBuild("software.amazon.smithy:smithy-rules-engine:$smithyVersion") + smithyBuild("software.amazon.smithy:smithy-protocol-traits:$smithyVersion") + smithyBuild("software.amazon.smithy:smithy-waiters:$smithyVersion") + smithyBuild("software.amazon.smithy:smithy-smoke-test-traits:$smithyVersion") + // Defines aws.test#AwsVendorParams, referenced by service smokeTests traits. + smithyBuild("software.amazon.smithy:smithy-aws-smoke-test-model:$smithyVersion") +} diff --git a/scripts/gen-client/gen_client.py b/scripts/gen-client/gen_client.py new file mode 100644 index 000000000..92ac2994e --- /dev/null +++ b/scripts/gen-client/gen_client.py @@ -0,0 +1,153 @@ +"""Generate one AWS SDK client from a sibling aws-sdk-python checkout's model into ./workspace/clients.""" + +# ruff: noqa: T201, S603, S607 + +import json +import os +import shutil +import subprocess +import sys +from pathlib import Path + +HARNESS = Path(__file__).resolve().parent +REPO = HARNESS.parents[1] +WORKSPACE = REPO / "workspace" + + +def main() -> None: + if len(sys.argv) != 2 or not sys.argv[1]: + sys.exit("usage: make gen e.g. make gen dynamodb") + + sdk = find_aws_sdk_python() + model = resolve_model(sdk / "codegen" / "aws-models", sys.argv[1]) + slug = model.stem + module = "aws_sdk_" + slug.replace("-", "_") + service = service_id(model) + version = client_version(sdk / "clients" / f"aws-sdk-{slug}" / "pyproject.toml") + + config = WORKSPACE / ".gen" / slug / "smithy-build.json" + config.parent.mkdir(parents=True, exist_ok=True) + config.write_text( + json.dumps( + { + "version": "1.0", + "imports": [str(model)], + "projections": { + slug: { + "transforms": [ + { + "name": "includeServices", + "args": {"services": [service]}, + }, + {"name": "removeUnusedShapes"}, + ], + "plugins": { + "python-client-codegen": { + "service": service, + "module": module, + "moduleVersion": version, + } + }, + } + }, + }, + indent=4, + ) + + "\n" + ) + print(f"generating {module} ({service}) v{version} from {model}") + + subprocess.run( + [ + str(REPO / "codegen" / "gradlew"), + "-p", + str(HARNESS), + "clean", + "build", + "-q", + f"-PsmithyBuildConfig={config}", + ], + check=True, + ) + + projection = ( + HARNESS + / "build" + / "smithyprojections" + / "gen-client" + / slug + / "python-client-codegen" + ) + if not projection.is_dir(): + sys.exit(f"codegen produced no output at {projection}") + + dest = WORKSPACE / "clients" / module + if dest.exists(): + shutil.rmtree(dest) + shutil.copytree(projection, dest) + + for cmd in (["ruff", "check", "--fix", "-q"], ["ruff", "format", "-q"]): + subprocess.run(["uv", "run", *cmd, str(dest)], cwd=REPO, check=True) + print(f"wrote {dest}") + + # --no-deps keeps this checkout's editable smithy packages instead of resolving published ones. + subprocess.run( + ["uv", "pip", "install", "-q", "--no-deps", "-e", str(dest)], + cwd=REPO, + check=True, + ) + print( + f"installed {module} into {REPO / '.venv'} (run scripts with `uv run python ...`)" + ) + + +def find_aws_sdk_python() -> Path: + candidates = [] + if env := os.environ.get("AWS_SDK_PYTHON"): + candidates.append(Path(env)) + # Checkouts are colocated with a shared prefix, e.g. _4_smithy-python and _4_aws-sdk-python. + candidates.append( + REPO.parent / REPO.name.replace("smithy-python", "aws-sdk-python") + ) + candidates.append(REPO.parent / "aws-sdk-python") + for candidate in candidates: + if (candidate / "codegen" / "aws-models").is_dir(): + return candidate.resolve() + sys.exit( + "no aws-sdk-python checkout found; tried " + + ", ".join(str(c) for c in candidates) + + " (set AWS_SDK_PYTHON to override)" + ) + + +def resolve_model(models: Path, name: str) -> Path: + key = name.strip() + for prefix in ("aws-sdk-", "aws_sdk_"): + key = key.removeprefix(prefix) + for candidate in (key, key.replace("_", "-")): + if (model := models / f"{candidate}.json").exists(): + return model + available = ", ".join(sorted(p.stem for p in models.glob("*.json"))) + sys.exit(f"no model for '{name}' in {models}\navailable: {available}") + + +def service_id(model: Path) -> str: + shapes = json.loads(model.read_text())["shapes"] + services = [k for k, v in shapes.items() if v.get("type") == "service"] + if len(services) != 1: + sys.exit( + f"expected exactly one service shape in {model.name}, found {services}" + ) + return services[0] + + +def client_version(pyproject: Path) -> str: + if pyproject.exists(): + for line in pyproject.read_text().splitlines(): + if line.startswith("version"): + return line.split("=", 1)[1].strip().strip('"') + return "0.0.1" + + +if __name__ == "__main__": + main() diff --git a/scripts/gen-client/settings.gradle.kts b/scripts/gen-client/settings.gradle.kts new file mode 100644 index 000000000..132dad695 --- /dev/null +++ b/scripts/gen-client/settings.gradle.kts @@ -0,0 +1,12 @@ +rootProject.name = "gen-client" + +// Substitutes this checkout's codegen for the published jars, so no mavenLocal publish is needed. +includeBuild("../../codegen") + +dependencyResolutionManagement { + versionCatalogs { + create("libs") { + from(files("../../codegen/gradle/libs.versions.toml")) + } + } +} diff --git a/uv.lock b/uv.lock index 26a7be6e5..c56d83d28 100644 --- a/uv.lock +++ b/uv.lock @@ -753,6 +753,9 @@ awscrt = [ cbor = [ { name = "smithy-cbor" }, ] +urllib3 = [ + { name = "urllib3" }, +] [package.metadata] requires-dist = [ @@ -760,9 +763,10 @@ requires-dist = [ { name = "awscrt", marker = "extra == 'awscrt'", specifier = "~=0.32.0" }, { name = "smithy-cbor", marker = "extra == 'cbor'", editable = "packages/smithy-cbor" }, { name = "smithy-core", editable = "packages/smithy-core" }, + { name = "urllib3", marker = "extra == 'urllib3'", specifier = ">=2.0,<3.0" }, { name = "yarl", marker = "extra == 'aiohttp'" }, ] -provides-extras = ["aiohttp", "awscrt", "cbor"] +provides-extras = ["aiohttp", "awscrt", "cbor", "urllib3"] [[package]] name = "smithy-json" @@ -840,6 +844,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, ] +[[package]] +name = "urllib3" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/05/b17359e1cefb4f909b5e40b1b90a496d987258916dbbf88e842c729f510e/urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63", size = 458972, upload-time = "2026-09-15T19:29:36.253Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", size = 135717, upload-time = "2026-09-15T19:29:34.577Z" }, +] + [[package]] name = "yarl" version = "1.20.0"