From 0dc2616d76bc9cc671cd3dfbffe796ea875daa69 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Thu, 6 Mar 2025 08:11:00 -0800 Subject: [PATCH 1/3] Add python changes for regional_endpoints --- .../src/smithy_aws_core/endpoints/__init__.py | 2 + .../endpoints/standard_regional.py | 59 +++++++++++++++ .../unit/endpoints/test_standard_regional.py | 73 +++++++++++++++++++ .../src/smithy_http/aio/endpoints.py | 9 ++- .../smithy-http/src/smithy_http/endpoints.py | 6 +- 5 files changed, 146 insertions(+), 3 deletions(-) create mode 100644 packages/smithy-aws-core/src/smithy_aws_core/endpoints/__init__.py create mode 100644 packages/smithy-aws-core/src/smithy_aws_core/endpoints/standard_regional.py create mode 100644 packages/smithy-aws-core/tests/unit/endpoints/test_standard_regional.py diff --git a/packages/smithy-aws-core/src/smithy_aws_core/endpoints/__init__.py b/packages/smithy-aws-core/src/smithy_aws_core/endpoints/__init__.py new file mode 100644 index 000000000..33cbe867a --- /dev/null +++ b/packages/smithy-aws-core/src/smithy_aws_core/endpoints/__init__.py @@ -0,0 +1,2 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 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..6064d6315 --- /dev/null +++ b/packages/smithy-aws-core/src/smithy_aws_core/endpoints/standard_regional.py @@ -0,0 +1,59 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +from dataclasses import dataclass +from urllib.parse import urlparse + +import smithy_core +from smithy_core import URI +from smithy_http.aio.interfaces import EndpointResolver +from smithy_http.endpoints import Endpoint, EndpointResolutionError + + +@dataclass(kw_only=True) +class RegionalEndpointParameters: + """Endpoint parameters for services with standard regional endpoints.""" + + sdk_endpoint: str | smithy_core.interfaces.URI | None + region: str | None + + +class StandardRegionalEndpointsResolver(EndpointResolver[RegionalEndpointParameters]): + """Resolves endpoints for services with standard regional endpoints.""" + + def __init__(self, endpoint_prefix: str): + self._endpoint_prefix = endpoint_prefix + + async def resolve_endpoint(self, params: RegionalEndpointParameters) -> Endpoint: + if params.sdk_endpoint is not None: + # If it's not a string, it's already a parsed URI so just pass it along. + if not isinstance(params.sdk_endpoint, str): + return Endpoint(uri=params.sdk_endpoint) + + parsed = urlparse(params.sdk_endpoint) + + # This will end up getting wrapped in the client. + if parsed.hostname is None: + raise EndpointResolutionError( + f"Unable to parse hostname from provided URI: {params.sdk_endpoint}" + ) + + return Endpoint( + uri=URI( + host=parsed.hostname, + path=parsed.path, + scheme=parsed.scheme, + query=parsed.query, + port=parsed.port, + ) + ) + + if params.region is not None: + # TODO: use dns suffix determined from partition metadata + dns_suffix = "amazonaws.com" + hostname = f"{self._endpoint_prefix}.{params.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/tests/unit/endpoints/test_standard_regional.py b/packages/smithy-aws-core/tests/unit/endpoints/test_standard_regional.py new file mode 100644 index 000000000..332d8969d --- /dev/null +++ b/packages/smithy-aws-core/tests/unit/endpoints/test_standard_regional.py @@ -0,0 +1,73 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +from smithy_aws_core.endpoints.standard_regional import ( + StandardRegionalEndpointsResolver, + RegionalEndpointParameters, +) + +from smithy_core import URI +from smithy_http.endpoints import EndpointResolutionError + +import pytest + + +async def test_resolve_endpoint_with_valid_sdk_endpoint_string(): + resolver = StandardRegionalEndpointsResolver(endpoint_prefix="service") + params = RegionalEndpointParameters( + sdk_endpoint="https://example.com/path?query=123", region=None + ) + + endpoint = await resolver.resolve_endpoint(params) + + assert endpoint.uri.host == "example.com" + assert endpoint.uri.path == "/path" + assert endpoint.uri.scheme == "https" + assert endpoint.uri.query == "query=123" + + +async def test_resolve_endpoint_with_sdk_endpoint_uri(): + resolver = StandardRegionalEndpointsResolver(endpoint_prefix="service") + parsed_uri = URI( + host="example.com", path="/path", scheme="https", query="query=123", port=443 + ) + params = RegionalEndpointParameters(sdk_endpoint=parsed_uri, region=None) + + endpoint = await resolver.resolve_endpoint(params) + + assert endpoint.uri == parsed_uri + + +async def test_resolve_endpoint_with_invalid_sdk_endpoint(): + resolver = StandardRegionalEndpointsResolver(endpoint_prefix="service") + params = RegionalEndpointParameters(sdk_endpoint="invalid-uri", region=None) + + with pytest.raises(EndpointResolutionError): + await resolver.resolve_endpoint(params) + + +async def test_resolve_endpoint_with_region(): + resolver = StandardRegionalEndpointsResolver(endpoint_prefix="service") + params = RegionalEndpointParameters(sdk_endpoint=None, region="us-west-2") + + endpoint = await resolver.resolve_endpoint(params) + + assert endpoint.uri.host == "service.us-west-2.amazonaws.com" + + +async def test_resolve_endpoint_with_no_sdk_endpoint_or_region(): + resolver = StandardRegionalEndpointsResolver(endpoint_prefix="service") + params = RegionalEndpointParameters(sdk_endpoint=None, region=None) + + with pytest.raises(EndpointResolutionError): + await resolver.resolve_endpoint(params) + + +async def test_resolve_endpoint_with_sdk_endpoint_and_region(): + resolver = StandardRegionalEndpointsResolver(endpoint_prefix="service") + params = RegionalEndpointParameters( + sdk_endpoint="https://example.com", region="us-west-2" + ) + + endpoint = await resolver.resolve_endpoint(params) + + assert endpoint.uri.host == "example.com" diff --git a/packages/smithy-http/src/smithy_http/aio/endpoints.py b/packages/smithy-http/src/smithy_http/aio/endpoints.py index f0c703240..9ce4ae34d 100644 --- a/packages/smithy-http/src/smithy_http/aio/endpoints.py +++ b/packages/smithy-http/src/smithy_http/aio/endpoints.py @@ -5,7 +5,7 @@ from smithy_core import URI from .. import interfaces as http_interfaces -from ..endpoints import Endpoint, StaticEndpointParams +from ..endpoints import Endpoint, StaticEndpointParams, EndpointResolutionError from . import interfaces as http_aio_interfaces @@ -17,6 +17,11 @@ class StaticEndpointResolver( async def resolve_endpoint( self, params: StaticEndpointParams ) -> http_interfaces.Endpoint: + if params.uri is None: + raise EndpointResolutionError( + "Unable to resolve endpoint: endpoint_uri is required" + ) + # If it's not a string, it's already a parsed URI so just pass it along. if not isinstance(params.uri, str): return Endpoint(uri=params.uri) @@ -27,7 +32,7 @@ async def resolve_endpoint( # This will end up getting wrapped in the client. if parsed.hostname is None: - raise ValueError( + raise EndpointResolutionError( f"Unable to parse hostname from provided URI: {params.uri}" ) diff --git a/packages/smithy-http/src/smithy_http/endpoints.py b/packages/smithy-http/src/smithy_http/endpoints.py index 3e3f99dc8..afe501ef0 100644 --- a/packages/smithy-http/src/smithy_http/endpoints.py +++ b/packages/smithy-http/src/smithy_http/endpoints.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 from dataclasses import dataclass, field +from smithy_core import SmithyException from smithy_core.interfaces import URI from . import Fields, interfaces @@ -13,6 +14,9 @@ class Endpoint(interfaces.Endpoint): headers: interfaces.Fields = field(default_factory=Fields) +class EndpointResolutionError(SmithyException): + """Exception type for all exceptions raised by endpoint resolution.""" + @dataclass class StaticEndpointParams: """Static endpoint params. @@ -20,4 +24,4 @@ class StaticEndpointParams: :param uri: A static URI to route requests to. """ - uri: str | URI + uri: str | URI | None From f7fef401aac66d3992b1db46ad814ba886b2d705 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Thu, 6 Mar 2025 11:30:51 -0800 Subject: [PATCH 2/3] Implement endpoint generation --- ...sStandardRegionalEndpointsIntegration.java | 127 ++++++ ...hon.codegen.integrations.PythonIntegration | 1 + .../python/codegen/ClientGenerator.java | 369 +++++++++--------- .../smithy/python/codegen/CodegenUtils.java | 32 +- .../python/codegen/DirectedPythonCodegen.java | 2 + .../codegen/generators/ConfigGenerator.java | 50 +-- .../generators/EndpointsGenerator.java | 59 +++ .../sections/EndpointParametersSection.java | 14 + .../sections/EndpointResolverSection.java | 14 + .../InitDefaultEndpointResolverSection.java | 14 + .../endpoints/standard_regional.py | 23 +- .../src/smithy_http/aio/endpoints.py | 6 +- .../smithy_http/aio/interfaces/__init__.py | 13 +- .../smithy-http/src/smithy_http/endpoints.py | 13 +- 14 files changed, 512 insertions(+), 225 deletions(-) create mode 100644 codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsStandardRegionalEndpointsIntegration.java create mode 100644 codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/EndpointsGenerator.java create mode 100644 codegen/core/src/main/java/software/amazon/smithy/python/codegen/sections/EndpointParametersSection.java create mode 100644 codegen/core/src/main/java/software/amazon/smithy/python/codegen/sections/EndpointResolverSection.java create mode 100644 codegen/core/src/main/java/software/amazon/smithy/python/codegen/sections/InitDefaultEndpointResolverSection.java diff --git a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsStandardRegionalEndpointsIntegration.java b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsStandardRegionalEndpointsIntegration.java new file mode 100644 index 000000000..7bda7991c --- /dev/null +++ b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsStandardRegionalEndpointsIntegration.java @@ -0,0 +1,127 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.python.aws.codegen; + +import java.util.List; +import software.amazon.smithy.aws.traits.ServiceTrait; +import software.amazon.smithy.codegen.core.Symbol; +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.integrations.PythonIntegration; +import software.amazon.smithy.python.codegen.integrations.RuntimeClientPlugin; +import software.amazon.smithy.python.codegen.sections.EndpointParametersSection; +import software.amazon.smithy.python.codegen.sections.EndpointResolverSection; +import software.amazon.smithy.python.codegen.sections.InitDefaultEndpointResolverSection; +import software.amazon.smithy.python.codegen.writer.PythonWriter; +import software.amazon.smithy.utils.CodeInterceptor; +import software.amazon.smithy.utils.CodeSection; + +public class AwsStandardRegionalEndpointsIntegration implements PythonIntegration { + @Override + public List getClientPlugins(GenerationContext context) { + if (context.applicationProtocol().isHttpProtocol()) { + var region = ConfigProperty.builder() + .name("region") + .type(Symbol.builder().name("str").build()) + .documentation(" The AWS region to connect to. The configured region is used to " + + "determine the service endpoint.") + .build(); + return List.of( + RuntimeClientPlugin.builder() + .addConfigProperty(region) + .build()); + } else { + return List.of(); + } + } + + @Override + public List> interceptors( + GenerationContext context + ) { + return List.of( + new RegionalEndpointParametersInterceptor(context), + new RegionalEndpointResolverInterceptor(context), + new RegionalInitEndpointResolverInterceptor(context)); + } + + private static final class RegionalEndpointParametersInterceptor + implements CodeInterceptor { + + private final GenerationContext context; + + public RegionalEndpointParametersInterceptor(GenerationContext context) { + this.context = context; + } + + @Override + public Class sectionType() { + return EndpointParametersSection.class; + } + + @Override + public void write(PythonWriter writer, String previousText, EndpointParametersSection section) { + var params = CodegenUtils.getEndpointParametersSymbol(context.settings()); + + writer.write("from smithy_aws_core.endpoints.standard_regional import RegionalEndpointParameters"); + writer.write("$L = RegionalEndpointParameters", params.getName()); + } + } + + private static final class RegionalEndpointResolverInterceptor + implements CodeInterceptor { + + private final GenerationContext context; + + public RegionalEndpointResolverInterceptor(GenerationContext context) { + this.context = context; + } + + @Override + public Class sectionType() { + return EndpointResolverSection.class; + } + + @Override + public void write(PythonWriter writer, String previousText, EndpointResolverSection section) { + var resolver = CodegenUtils.getEndpointResolverSymbol(context.settings()); + + writer.write("from smithy_aws_core.endpoints.standard_regional import StandardRegionalEndpointsResolver"); + writer.write("$L = StandardRegionalEndpointsResolver", resolver.getName()); + } + } + + private static final class RegionalInitEndpointResolverInterceptor + implements CodeInterceptor { + + private final GenerationContext context; + + public RegionalInitEndpointResolverInterceptor(GenerationContext context) { + this.context = context; + } + + @Override + public Class sectionType() { + return InitDefaultEndpointResolverSection.class; + } + + @Override + public void write(PythonWriter writer, String previousText, InitDefaultEndpointResolverSection section) { + var resolver = CodegenUtils.getEndpointResolverSymbol(context.settings()); + + String endpointPrefix = context.settings() + .service(context.model()) + .getTrait(ServiceTrait.class) + .map(ServiceTrait::getEndpointPrefix) + .orElse(context.settings().service().getName()); + + writer.write("self.endpoint_resolver = endpoint_resolver or $T(endpoint_prefix=$S)", + resolver, + endpointPrefix); + + } + } +} diff --git a/codegen/aws/core/src/main/resources/META-INF/services/software.amazon.smithy.python.codegen.integrations.PythonIntegration b/codegen/aws/core/src/main/resources/META-INF/services/software.amazon.smithy.python.codegen.integrations.PythonIntegration index 0375294ba..c9ee36be9 100644 --- a/codegen/aws/core/src/main/resources/META-INF/services/software.amazon.smithy.python.codegen.integrations.PythonIntegration +++ b/codegen/aws/core/src/main/resources/META-INF/services/software.amazon.smithy.python.codegen.integrations.PythonIntegration @@ -6,3 +6,4 @@ software.amazon.smithy.python.aws.codegen.AwsAuthIntegration software.amazon.smithy.python.aws.codegen.AwsProtocolsIntegration software.amazon.smithy.python.aws.codegen.AwsUserAgentIntegration +software.amazon.smithy.python.aws.codegen.AwsStandardRegionalEndpointsIntegration 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 f6639a94d..2382619cd 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 @@ -187,199 +187,200 @@ def _classify_error( """); writer.dedent(); - writer.write(""" - async def _execute_operation( - self, - input: Input, - plugins: list[$1T], - serialize: Callable[[Input, $5T], Awaitable[$2T]], - deserialize: Callable[[$3T, $5T], Awaitable[Output]], - config: $5T, - operation_name: str, - ${?hasEventStream} - has_input_stream: bool = False, - event_deserializer: Callable[[ShapeDeserializer], Any] | None = None, - event_response_deserializer: DeserializeableShape | None = None, - ${/hasEventStream} - ) -> Output: - try: - return await self._handle_execution( - input, plugins, serialize, deserialize, config, operation_name, + writer.write( + """ + async def _execute_operation( + self, + input: Input, + plugins: list[$1T], + serialize: Callable[[Input, $5T], Awaitable[$2T]], + deserialize: Callable[[$3T, $5T], Awaitable[Output]], + config: $5T, + operation_name: str, ${?hasEventStream} - has_input_stream, event_deserializer, event_response_deserializer, + has_input_stream: bool = False, + event_deserializer: Callable[[ShapeDeserializer], Any] | None = None, + event_response_deserializer: DeserializeableShape | None = None, ${/hasEventStream} - ) - except Exception as e: - # Make sure every exception that we throw is an instance of $4T so - # customers can reliably catch everything we throw. - if not isinstance(e, $4T): - raise $4T(e) from e - raise e - - async def _handle_execution( - self, - input: Input, - plugins: list[$1T], - serialize: Callable[[Input, $5T], Awaitable[$2T]], - deserialize: Callable[[$3T, $5T], Awaitable[Output]], - config: $5T, - operation_name: str, - ${?hasEventStream} - has_input_stream: bool = False, - event_deserializer: Callable[[ShapeDeserializer], Any] | None = None, - event_response_deserializer: DeserializeableShape | None = None, - ${/hasEventStream} - ) -> Output: - logger.debug('Making request for operation "%s" with parameters: %s', operation_name, input) - context: InterceptorContext[Input, None, None, None] = InterceptorContext( - request=input, - response=None, - transport_request=None, - transport_response=None, - ) - _client_interceptors = config.interceptors - client_interceptors = cast( - list[Interceptor[Input, Output, $2T, $3T]], _client_interceptors - ) - interceptors = client_interceptors - - try: - # Step 1a: Invoke read_before_execution on client-level interceptors - for interceptor in client_interceptors: - interceptor.read_before_execution(context) - - # Step 1b: Run operation-level plugins - config = deepcopy(config) - for plugin in plugins: - plugin(config) - - _client_interceptors = config.interceptors - interceptors = cast( - list[Interceptor[Input, Output, $2T, $3T]], - _client_interceptors, - ) + ) -> Output: + try: + return await self._handle_execution( + input, plugins, serialize, deserialize, config, operation_name, + ${?hasEventStream} + has_input_stream, event_deserializer, event_response_deserializer, + ${/hasEventStream} + ) + except Exception as e: + # Make sure every exception that we throw is an instance of $4T so + # customers can reliably catch everything we throw. + if not isinstance(e, $4T): + raise $4T(e) from e + raise e + + async def _handle_execution( + self, + input: Input, + plugins: list[$1T], + serialize: Callable[[Input, $5T], Awaitable[$2T]], + deserialize: Callable[[$3T, $5T], Awaitable[Output]], + config: $5T, + operation_name: str, + ${?hasEventStream} + has_input_stream: bool = False, + event_deserializer: Callable[[ShapeDeserializer], Any] | None = None, + event_response_deserializer: DeserializeableShape | None = None, + ${/hasEventStream} + ) -> Output: + logger.debug('Making request for operation "%s" with parameters: %s', operation_name, input) + context: InterceptorContext[Input, None, None, None] = InterceptorContext( + request=input, + response=None, + transport_request=None, + transport_response=None, + ) + _client_interceptors = config.interceptors + client_interceptors = cast( + list[Interceptor[Input, Output, $2T, $3T]], _client_interceptors + ) + interceptors = client_interceptors + + try: + # Step 1a: Invoke read_before_execution on client-level interceptors + for interceptor in client_interceptors: + interceptor.read_before_execution(context) + + # Step 1b: Run operation-level plugins + config = deepcopy(config) + for plugin in plugins: + plugin(config) + + _client_interceptors = config.interceptors + interceptors = cast( + list[Interceptor[Input, Output, $2T, $3T]], + _client_interceptors, + ) - # Step 1c: Invoke the read_before_execution hooks on newly added - # interceptors. - for interceptor in interceptors: - if interceptor not in client_interceptors: - interceptor.read_before_execution(context) + # Step 1c: Invoke the read_before_execution hooks on newly added + # interceptors. + for interceptor in interceptors: + if interceptor not in client_interceptors: + interceptor.read_before_execution(context) - # Step 2: Invoke the modify_before_serialization hooks - for interceptor in interceptors: - context._request = interceptor.modify_before_serialization(context) + # Step 2: Invoke the modify_before_serialization hooks + for interceptor in interceptors: + context._request = interceptor.modify_before_serialization(context) - # Step 3: Invoke the read_before_serialization hooks - for interceptor in interceptors: - interceptor.read_before_serialization(context) + # Step 3: Invoke the read_before_serialization hooks + for interceptor in interceptors: + interceptor.read_before_serialization(context) - # Step 4: Serialize the request - context_with_transport_request = cast( - InterceptorContext[Input, None, $2T, None], context - ) - logger.debug("Serializing request for: %s", context_with_transport_request.request) - context_with_transport_request._transport_request = await serialize( - context_with_transport_request.request, config - ) - logger.debug("Serialization complete. Transport request: %s", context_with_transport_request._transport_request) + # Step 4: Serialize the request + context_with_transport_request = cast( + InterceptorContext[Input, None, $2T, None], context + ) + logger.debug("Serializing request for: %s", context_with_transport_request.request) + context_with_transport_request._transport_request = await serialize( + context_with_transport_request.request, config + ) + logger.debug("Serialization complete. Transport request: %s", context_with_transport_request._transport_request) - # Step 5: Invoke read_after_serialization - for interceptor in interceptors: - interceptor.read_after_serialization(context_with_transport_request) + # Step 5: Invoke read_after_serialization + for interceptor in interceptors: + interceptor.read_after_serialization(context_with_transport_request) - # Step 6: Invoke modify_before_retry_loop - for interceptor in interceptors: - context_with_transport_request._transport_request = ( - interceptor.modify_before_retry_loop(context_with_transport_request) - ) + # Step 6: Invoke modify_before_retry_loop + for interceptor in interceptors: + context_with_transport_request._transport_request = ( + interceptor.modify_before_retry_loop(context_with_transport_request) + ) - # Step 7: Acquire the retry token. - retry_strategy = config.retry_strategy - retry_token = retry_strategy.acquire_initial_retry_token() - - while True: - # Make an attempt, creating a copy of the context so we don't pass - # around old data. - context_with_response = await self._handle_attempt( - deserialize, - interceptors, - context_with_transport_request.copy(), - config, - operation_name, - ) + # Step 7: Acquire the retry token. + retry_strategy = config.retry_strategy + retry_token = retry_strategy.acquire_initial_retry_token() + + while True: + # Make an attempt, creating a copy of the context so we don't pass + # around old data. + context_with_response = await self._handle_attempt( + deserialize, + interceptors, + context_with_transport_request.copy(), + config, + operation_name, + ) - # We perform this type-ignored re-assignment because `context` needs - # to point at the latest context so it can be generically handled - # later on. This is only an issue here because we've created a copy, - # so we're no longer simply pointing at the same object in memory - # with different names and type hints. It is possible to address this - # without having to fall back to the type ignore, but it would impose - # unnecessary runtime costs. - context = context_with_response # type: ignore - - if isinstance(context_with_response.response, Exception): - # Step 7u: Reacquire retry token if the attempt failed - try: - retry_token = retry_strategy.refresh_retry_token_for_retry( - token_to_renew=retry_token, - error_info=self._classify_error( - error=context_with_response.response, - context=context_with_response, + # We perform this type-ignored re-assignment because `context` needs + # to point at the latest context so it can be generically handled + # later on. This is only an issue here because we've created a copy, + # so we're no longer simply pointing at the same object in memory + # with different names and type hints. It is possible to address this + # without having to fall back to the type ignore, but it would impose + # unnecessary runtime costs. + context = context_with_response # type: ignore + + if isinstance(context_with_response.response, Exception): + # Step 7u: Reacquire retry token if the attempt failed + try: + retry_token = retry_strategy.refresh_retry_token_for_retry( + token_to_renew=retry_token, + error_info=self._classify_error( + error=context_with_response.response, + context=context_with_response, + ) + ) + except SmithyRetryException: + raise context_with_response.response + logger.debug( + "Retry needed. Attempting request #%s in %.4f seconds.", + retry_token.retry_count + 1, + retry_token.retry_delay ) - ) - except SmithyRetryException: - raise context_with_response.response - logger.debug( - "Retry needed. Attempting request #%s in %.4f seconds.", - retry_token.retry_count + 1, - retry_token.retry_delay - ) - await sleep(retry_token.retry_delay) - current_body = context_with_transport_request.transport_request.body - if (seek := getattr(current_body, "seek", None)) is not None: - await seek(0) + await sleep(retry_token.retry_delay) + current_body = context_with_transport_request.transport_request.body + if (seek := getattr(current_body, "seek", None)) is not None: + await seek(0) + else: + # Step 8: Invoke record_success + retry_strategy.record_success(token=retry_token) + break + except Exception as e: + if context.response is not None: + logger.exception("Exception occurred while handling: %s", context.response) + pass + context._response = e + + # At this point, the context's request will have been definitively set, and + # The response will be set either with the modeled output or an exception. The + # transport_request and transport_response may be set or None. + execution_context = cast( + InterceptorContext[Input, Output, $2T | None, $3T | None], context + ) + ${^hasEventStream} + return await self._finalize_execution(interceptors, execution_context) + ${/hasEventStream} + ${?hasEventStream} + operation_output = await self._finalize_execution(interceptors, execution_context) + if has_input_stream or event_deserializer is not None: + ${6C|} else: - # Step 8: Invoke record_success - retry_strategy.record_success(token=retry_token) - break - except Exception as e: - if context.response is not None: - logger.exception("Exception occurred while handling: %s", context.response) - pass - context._response = e + return operation_output + ${/hasEventStream} - # At this point, the context's request will have been definitively set, and - # The response will be set either with the modeled output or an exception. The - # transport_request and transport_response may be set or None. - execution_context = cast( - InterceptorContext[Input, Output, $2T | None, $3T | None], context - ) - ${^hasEventStream} - return await self._finalize_execution(interceptors, execution_context) - ${/hasEventStream} - ${?hasEventStream} - operation_output = await self._finalize_execution(interceptors, execution_context) - if has_input_stream or event_deserializer is not None: - ${6C|} - else: - return operation_output - ${/hasEventStream} - - async def _handle_attempt( - self, - deserialize: Callable[[$3T, $5T], Awaitable[Output]], - interceptors: list[Interceptor[Input, Output, $2T, $3T]], - context: InterceptorContext[Input, None, $2T, None], - config: $5T, - operation_name: str, - ) -> InterceptorContext[Input, Output, $2T, $3T | None]: - try: - # assert config.interceptors is not None - # Step 7a: Invoke read_before_attempt - for interceptor in interceptors: - interceptor.read_before_attempt(context) + async def _handle_attempt( + self, + deserialize: Callable[[$3T, $5T], Awaitable[Output]], + interceptors: list[Interceptor[Input, Output, $2T, $3T]], + context: InterceptorContext[Input, None, $2T, None], + config: $5T, + operation_name: str, + ) -> InterceptorContext[Input, Output, $2T, $3T | None]: + try: + # assert config.interceptors is not None + # Step 7a: Invoke read_before_attempt + for interceptor in interceptors: + interceptor.read_before_attempt(context) - """, + """, pluginSymbol, transportRequest, transportResponse, @@ -442,16 +443,10 @@ async def _handle_attempt( if (context.applicationProtocol().isHttpProtocol()) { writer.addDependency(SmithyPythonDependency.SMITHY_CORE); writer.addDependency(SmithyPythonDependency.SMITHY_HTTP); - // TODO: implement the endpoints 2.0 spec and remove the hard-coded handling of static params. - writer.addImport("smithy_http.endpoints", "StaticEndpointParams"); writer.addImport("smithy_core", "URI"); writer.write(""" # Step 7f: Invoke endpoint_resolver.resolve_endpoint - if config.endpoint_uri is None: - raise $1T( - "No endpoint_uri found on the operation config." - ) - endpoint_resolver_parameters = StaticEndpointParams(uri=config.endpoint_uri) + endpoint_resolver_parameters = $T.build(config=config) logger.debug("Calling endpoint resolver with parameters: %s", endpoint_resolver_parameters) endpoint = await config.endpoint_resolver.resolve_endpoint( endpoint_resolver_parameters @@ -474,7 +469,7 @@ async def _handle_attempt( ) context._transport_request.fields.extend(endpoint.headers) - """, errorSymbol); + """, CodegenUtils.getEndpointParametersSymbol(context.settings())); } writer.popState(); diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/CodegenUtils.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/CodegenUtils.java index 39379536f..6660e1851 100644 --- a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/CodegenUtils.java +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/CodegenUtils.java @@ -140,10 +140,38 @@ public static Symbol getUnknownApiError(PythonSettings settings) { } /** - * Gets the symbol for the http auth parameters object. + * Gets the symbol for the endpoint parameters object. * * @param settings The client settings, used to account for module configuration. - * @return Returns the symbol for http auth params. + * @return Returns the symbol for endpoint parameters. + */ + public static Symbol getEndpointParametersSymbol(PythonSettings settings) { + return Symbol.builder() + .name("EndpointParameters") + .namespace(String.format("%s.endpoints", settings.moduleName()), ".") + .definitionFile(String.format("./%s/endpoints.py", settings.moduleName())) + .build(); + } + + /** + * Gets the symbol for the endpoint resolver object. + * + * @param settings The client settings, used to account for module configuration. + * @return Returns the symbol for endpoint resolver. + */ + public static Symbol getEndpointResolverSymbol(PythonSettings settings) { + return Symbol.builder() + .name("EndpointResolver") + .namespace(String.format("%s.endpoints", settings.moduleName()), ".") + .definitionFile(String.format("./%s/endpoints.py", settings.moduleName())) + .build(); + } + + /** + * Gets the symbol for the http auth params. + * + * @param settings The client settings, used to account for module configuration. + * @return Returns the http auth params symbol. */ public static Symbol getHttpAuthParamsSymbol(PythonSettings settings) { return Symbol.builder() diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/DirectedPythonCodegen.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/DirectedPythonCodegen.java index 5a161912c..2cebe7f5b 100644 --- a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/DirectedPythonCodegen.java +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/DirectedPythonCodegen.java @@ -40,6 +40,7 @@ import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.python.codegen.generators.ConfigGenerator; +import software.amazon.smithy.python.codegen.generators.EndpointsGenerator; import software.amazon.smithy.python.codegen.generators.EnumGenerator; import software.amazon.smithy.python.codegen.generators.IntEnumGenerator; import software.amazon.smithy.python.codegen.generators.ListGenerator; @@ -123,6 +124,7 @@ public void customizeBeforeShapeGeneration(CustomizeDirective { - writer.addImport("smithy_http.aio.endpoints", "StaticEndpointResolver"); - writer.write("self.endpoint_resolver = endpoint_resolver or StaticEndpointResolver()"); - }) - .build(), ConfigProperty.builder() .name("endpoint_uri") .type(Symbol.builder() @@ -124,7 +101,7 @@ public ConfigGenerator(PythonSettings settings, GenerationContext context) { } private static List getHttpProperties(GenerationContext context) { - var properties = new ArrayList(HTTP_PROPERTIES.size() + 1); + var properties = new ArrayList(HTTP_PROPERTIES.size() + 2); var clientBuilder = ConfigProperty.builder() .name("http_client") .type(Symbol.builder() @@ -153,6 +130,29 @@ private static List getHttpProperties(GenerationContext context) } properties.add(clientBuilder.build()); + var endpointResolver = CodegenUtils.getEndpointResolverSymbol(context.settings()); + properties.add(ConfigProperty.builder() + .name("endpoint_resolver") + .type(Symbol.builder() + .name("EndpointResolverProtocol[EndpointParameters]") + .addReference(CodegenUtils.getEndpointParametersSymbol(context.settings())) + .addReference(Symbol.builder() + .name("EndpointResolverProtocol") + .namespace("smithy_http.aio.interfaces", ".") + .addDependency(SmithyPythonDependency.SMITHY_HTTP) + .build()) + .build()) + .documentation(""" + The endpoint resolver used to resolve the final endpoint per-operation based on the \ + configuration.""") + .nullable(false) + .initialize(writer -> { + writer.pushState(new InitDefaultEndpointResolverSection()); + writer.write("self.endpoint_resolver = endpoint_resolver or $T()", endpointResolver); + writer.popState(); + }) + .build()); + properties.addAll(HTTP_PROPERTIES); return List.copyOf(properties); } diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/EndpointsGenerator.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/EndpointsGenerator.java new file mode 100644 index 000000000..a913ec800 --- /dev/null +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/EndpointsGenerator.java @@ -0,0 +1,59 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.python.codegen.generators; + +import software.amazon.smithy.codegen.core.Symbol; +import software.amazon.smithy.python.codegen.CodegenUtils; +import software.amazon.smithy.python.codegen.GenerationContext; +import software.amazon.smithy.python.codegen.PythonSettings; +import software.amazon.smithy.python.codegen.SmithyPythonDependency; +import software.amazon.smithy.python.codegen.sections.EndpointParametersSection; +import software.amazon.smithy.python.codegen.sections.EndpointResolverSection; +import software.amazon.smithy.python.codegen.writer.PythonWriter; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * This class is responsible for generating the endpoint resolver and its parameters. + */ +@SmithyInternalApi +public final class EndpointsGenerator implements Runnable { + + private final PythonSettings settings; + private final GenerationContext context; + + public EndpointsGenerator(GenerationContext context, PythonSettings settings) { + this.context = context; + this.settings = settings; + } + + @Override + public void run() { + var params = CodegenUtils.getEndpointParametersSymbol(settings); + context.writerDelegator().useFileWriter(params.getDefinitionFile(), params.getNamespace(), writer -> { + generateEndpointParameters(writer, params); + }); + + var resolver = CodegenUtils.getEndpointResolverSymbol(settings); + context.writerDelegator().useFileWriter(resolver.getDefinitionFile(), resolver.getNamespace(), writer -> { + generateEndpointResolver(writer, resolver); + }); + } + + private void generateEndpointParameters(PythonWriter writer, Symbol params) { + writer.pushState(new EndpointParametersSection()); + writer.addDependency(SmithyPythonDependency.SMITHY_HTTP); + writer.write("from smithy_http.endpoints import StaticEndpointParams"); + writer.write("$L = StaticEndpointParams", params.getName()); + writer.popState(); + } + + private void generateEndpointResolver(PythonWriter writer, Symbol resolver) { + writer.pushState(new EndpointResolverSection()); + writer.addDependency(SmithyPythonDependency.SMITHY_HTTP); + writer.write("from smithy_http.aio.endpoints import StaticEndpointResolver"); + writer.write("$L = StaticEndpointResolver", resolver.getName()); + writer.popState(); + } +} diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/sections/EndpointParametersSection.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/sections/EndpointParametersSection.java new file mode 100644 index 000000000..b70a0b111 --- /dev/null +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/sections/EndpointParametersSection.java @@ -0,0 +1,14 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.python.codegen.sections; + +import software.amazon.smithy.utils.CodeSection; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * A section that controls generating the EndpointParameters class. + */ +@SmithyInternalApi +public record EndpointParametersSection() implements CodeSection {} diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/sections/EndpointResolverSection.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/sections/EndpointResolverSection.java new file mode 100644 index 000000000..77b8472ac --- /dev/null +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/sections/EndpointResolverSection.java @@ -0,0 +1,14 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.python.codegen.sections; + +import software.amazon.smithy.utils.CodeSection; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * A section that controls generating the EndpointResolver class. + */ +@SmithyInternalApi +public record EndpointResolverSection() implements CodeSection {} diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/sections/InitDefaultEndpointResolverSection.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/sections/InitDefaultEndpointResolverSection.java new file mode 100644 index 000000000..5614c7212 --- /dev/null +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/sections/InitDefaultEndpointResolverSection.java @@ -0,0 +1,14 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.python.codegen.sections; + +import software.amazon.smithy.utils.CodeSection; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * A section that controls generating the EndpointParameters class. + */ +@SmithyInternalApi +public record InitDefaultEndpointResolverSection() implements CodeSection {} 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 index 6064d6315..20216d1ae 100644 --- 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 @@ -1,26 +1,41 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 from dataclasses import dataclass +from typing import Protocol, Self from urllib.parse import urlparse import smithy_core from smithy_core import URI -from smithy_http.aio.interfaces import EndpointResolver +from smithy_http.aio.interfaces import ( + EndpointResolverProtocol, + EndpointParametersProtocol, +) from smithy_http.endpoints import Endpoint, EndpointResolutionError +class _RegionUriConfig(Protocol): + endpoint_uri: str | smithy_core.interfaces.URI | None + region: str | None + + @dataclass(kw_only=True) -class RegionalEndpointParameters: +class RegionalEndpointParameters(EndpointParametersProtocol[_RegionUriConfig]): """Endpoint parameters for services with standard regional endpoints.""" sdk_endpoint: str | smithy_core.interfaces.URI | None region: str | None + @classmethod + def build(cls, config: _RegionUriConfig) -> Self: + return cls(sdk_endpoint=config.endpoint_uri, region=config.region) + -class StandardRegionalEndpointsResolver(EndpointResolver[RegionalEndpointParameters]): +class StandardRegionalEndpointsResolver( + EndpointResolverProtocol[RegionalEndpointParameters] +): """Resolves endpoints for services with standard regional endpoints.""" - def __init__(self, endpoint_prefix: str): + def __init__(self, endpoint_prefix: str = "bedrock-runtime"): self._endpoint_prefix = endpoint_prefix async def resolve_endpoint(self, params: RegionalEndpointParameters) -> Endpoint: diff --git a/packages/smithy-http/src/smithy_http/aio/endpoints.py b/packages/smithy-http/src/smithy_http/aio/endpoints.py index 9ce4ae34d..7f2c362b2 100644 --- a/packages/smithy-http/src/smithy_http/aio/endpoints.py +++ b/packages/smithy-http/src/smithy_http/aio/endpoints.py @@ -3,15 +3,13 @@ from urllib.parse import urlparse from smithy_core import URI +from .interfaces import EndpointResolverProtocol from .. import interfaces as http_interfaces from ..endpoints import Endpoint, StaticEndpointParams, EndpointResolutionError -from . import interfaces as http_aio_interfaces -class StaticEndpointResolver( - http_aio_interfaces.EndpointResolver[StaticEndpointParams] -): +class StaticEndpointResolver(EndpointResolverProtocol[StaticEndpointParams]): """A basic endpoint resolver that forwards a static URI.""" async def resolve_endpoint( diff --git a/packages/smithy-http/src/smithy_http/aio/interfaces/__init__.py b/packages/smithy-http/src/smithy_http/aio/interfaces/__init__.py index fd112ef38..f7ba35f85 100644 --- a/packages/smithy-http/src/smithy_http/aio/interfaces/__init__.py +++ b/packages/smithy-http/src/smithy_http/aio/interfaces/__init__.py @@ -1,6 +1,6 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -from typing import Protocol, TypeVar +from typing import Protocol, TypeVar, Self from smithy_core.aio.interfaces import Request, Response from smithy_core.aio.utils import read_streaming_blob, read_streaming_blob_async @@ -12,6 +12,15 @@ HTTPRequestConfiguration, ) +Config = TypeVar("Config", contravariant=True) + + +class EndpointParametersProtocol(Protocol[Config]): + @classmethod + def build(cls, config: Config) -> Self: + raise NotImplementedError() + + # EndpointParams are defined in the generated client, so we use a TypeVar here. # More specific EndpointParams implementations are subtypes of less specific ones. But # consumers of less specific EndpointParams implementations are subtypes of consumers @@ -19,7 +28,7 @@ EndpointParams = TypeVar("EndpointParams", contravariant=True) -class EndpointResolver(Protocol[EndpointParams]): +class EndpointResolverProtocol(Protocol[EndpointParams]): """Resolves an operation's endpoint based given parameters.""" async def resolve_endpoint(self, params: EndpointParams) -> Endpoint: diff --git a/packages/smithy-http/src/smithy_http/endpoints.py b/packages/smithy-http/src/smithy_http/endpoints.py index afe501ef0..67c6b5a73 100644 --- a/packages/smithy-http/src/smithy_http/endpoints.py +++ b/packages/smithy-http/src/smithy_http/endpoints.py @@ -1,11 +1,13 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 from dataclasses import dataclass, field +from typing import Protocol, Self from smithy_core import SmithyException from smithy_core.interfaces import URI from . import Fields, interfaces +from .aio.interfaces import EndpointParametersProtocol @dataclass @@ -17,11 +19,20 @@ class Endpoint(interfaces.Endpoint): class EndpointResolutionError(SmithyException): """Exception type for all exceptions raised by endpoint resolution.""" + +class _UriConfig(Protocol): + endpoint_uri: str | URI | None + + @dataclass -class StaticEndpointParams: +class StaticEndpointParams(EndpointParametersProtocol[_UriConfig]): """Static endpoint params. :param uri: A static URI to route requests to. """ uri: str | URI | None + + @classmethod + def build(cls, config: _UriConfig) -> Self: + return cls(uri=config.endpoint_uri) From 87b0781d3ec7df23090bf1da796032ff17cad5e1 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Mon, 10 Mar 2025 09:50:55 -0700 Subject: [PATCH 3/3] PR cleanups --- .../python/codegen/ClientGenerator.java | 5 +++-- .../codegen/generators/ConfigGenerator.java | 8 ++------ .../endpoints/standard_regional.py | 13 ++++++------- .../unit/endpoints/test_standard_regional.py | 3 ++- .../src/smithy_http/aio/endpoints.py | 7 ++++--- .../smithy_http/aio/interfaces/__init__.py | 19 +++++-------------- .../smithy-http/src/smithy_http/endpoints.py | 9 ++------- .../smithy-http/src/smithy_http/exceptions.py | 4 ++++ 8 files changed, 28 insertions(+), 40 deletions(-) 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 2382619cd..35f32b30b 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 @@ -446,7 +446,7 @@ async def _handle_attempt( writer.addImport("smithy_core", "URI"); writer.write(""" # Step 7f: Invoke endpoint_resolver.resolve_endpoint - endpoint_resolver_parameters = $T.build(config=config) + endpoint_resolver_parameters = $1T.build(config=config) logger.debug("Calling endpoint resolver with parameters: %s", endpoint_resolver_parameters) endpoint = await config.endpoint_resolver.resolve_endpoint( endpoint_resolver_parameters @@ -469,7 +469,8 @@ async def _handle_attempt( ) context._transport_request.fields.extend(endpoint.headers) - """, CodegenUtils.getEndpointParametersSymbol(context.settings())); + """, + CodegenUtils.getEndpointParametersSymbol(context.settings())); } writer.popState(); 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 ef36f9f8f..3eccb95df 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 @@ -136,19 +136,15 @@ private static List getHttpProperties(GenerationContext context) properties.add(ConfigProperty.builder() .name("endpoint_resolver") .type(Symbol.builder() - .name("EndpointResolverProtocol[EndpointParameters]") + .name("_EndpointResolver[EndpointParameters]") .addReference(CodegenUtils.getEndpointParametersSymbol(context.settings())) - .addReference(Symbol.builder() - .name("EndpointResolverProtocol") - .namespace("smithy_http.aio.interfaces", ".") - .addDependency(SmithyPythonDependency.SMITHY_HTTP) - .build()) .build()) .documentation(""" The endpoint resolver used to resolve the final endpoint per-operation based on the \ configuration.""") .nullable(false) .initialize(writer -> { + writer.addImport("smithy_http.aio.interfaces", "EndpointResolver", "_EndpointResolver"); writer.pushState(new InitDefaultEndpointResolverSection()); writer.write("self.endpoint_resolver = endpoint_resolver or $T()", endpointResolver); writer.popState(); 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 index 20216d1ae..c6b37df2d 100644 --- 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 @@ -7,10 +7,11 @@ import smithy_core from smithy_core import URI from smithy_http.aio.interfaces import ( - EndpointResolverProtocol, - EndpointParametersProtocol, + EndpointResolver, + EndpointParameters, ) -from smithy_http.endpoints import Endpoint, EndpointResolutionError +from smithy_http.endpoints import Endpoint +from smithy_http.exceptions import EndpointResolutionError class _RegionUriConfig(Protocol): @@ -19,7 +20,7 @@ class _RegionUriConfig(Protocol): @dataclass(kw_only=True) -class RegionalEndpointParameters(EndpointParametersProtocol[_RegionUriConfig]): +class RegionalEndpointParameters(EndpointParameters[_RegionUriConfig]): """Endpoint parameters for services with standard regional endpoints.""" sdk_endpoint: str | smithy_core.interfaces.URI | None @@ -30,9 +31,7 @@ def build(cls, config: _RegionUriConfig) -> Self: return cls(sdk_endpoint=config.endpoint_uri, region=config.region) -class StandardRegionalEndpointsResolver( - EndpointResolverProtocol[RegionalEndpointParameters] -): +class StandardRegionalEndpointsResolver(EndpointResolver[RegionalEndpointParameters]): """Resolves endpoints for services with standard regional endpoints.""" def __init__(self, endpoint_prefix: str = "bedrock-runtime"): diff --git a/packages/smithy-aws-core/tests/unit/endpoints/test_standard_regional.py b/packages/smithy-aws-core/tests/unit/endpoints/test_standard_regional.py index 332d8969d..0ae20ac3b 100644 --- a/packages/smithy-aws-core/tests/unit/endpoints/test_standard_regional.py +++ b/packages/smithy-aws-core/tests/unit/endpoints/test_standard_regional.py @@ -6,10 +6,11 @@ ) from smithy_core import URI -from smithy_http.endpoints import EndpointResolutionError import pytest +from smithy_http.exceptions import EndpointResolutionError + async def test_resolve_endpoint_with_valid_sdk_endpoint_string(): resolver = StandardRegionalEndpointsResolver(endpoint_prefix="service") diff --git a/packages/smithy-http/src/smithy_http/aio/endpoints.py b/packages/smithy-http/src/smithy_http/aio/endpoints.py index 7f2c362b2..47ebbbae2 100644 --- a/packages/smithy-http/src/smithy_http/aio/endpoints.py +++ b/packages/smithy-http/src/smithy_http/aio/endpoints.py @@ -3,13 +3,14 @@ from urllib.parse import urlparse from smithy_core import URI -from .interfaces import EndpointResolverProtocol +from .interfaces import EndpointResolver from .. import interfaces as http_interfaces -from ..endpoints import Endpoint, StaticEndpointParams, EndpointResolutionError +from ..endpoints import Endpoint, StaticEndpointParams +from ..exceptions import EndpointResolutionError -class StaticEndpointResolver(EndpointResolverProtocol[StaticEndpointParams]): +class StaticEndpointResolver(EndpointResolver[StaticEndpointParams]): """A basic endpoint resolver that forwards a static URI.""" async def resolve_endpoint( diff --git a/packages/smithy-http/src/smithy_http/aio/interfaces/__init__.py b/packages/smithy-http/src/smithy_http/aio/interfaces/__init__.py index f7ba35f85..699fcf373 100644 --- a/packages/smithy-http/src/smithy_http/aio/interfaces/__init__.py +++ b/packages/smithy-http/src/smithy_http/aio/interfaces/__init__.py @@ -1,6 +1,6 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -from typing import Protocol, TypeVar, Self +from typing import Protocol, Self from smithy_core.aio.interfaces import Request, Response from smithy_core.aio.utils import read_streaming_blob, read_streaming_blob_async @@ -12,26 +12,17 @@ HTTPRequestConfiguration, ) -Config = TypeVar("Config", contravariant=True) - -class EndpointParametersProtocol(Protocol[Config]): +class EndpointParameters[C](Protocol): @classmethod - def build(cls, config: Config) -> Self: + def build(cls, config: C) -> Self: raise NotImplementedError() -# EndpointParams are defined in the generated client, so we use a TypeVar here. -# More specific EndpointParams implementations are subtypes of less specific ones. But -# consumers of less specific EndpointParams implementations are subtypes of consumers -# of more specific ones. -EndpointParams = TypeVar("EndpointParams", contravariant=True) - - -class EndpointResolverProtocol(Protocol[EndpointParams]): +class EndpointResolver[T](Protocol): """Resolves an operation's endpoint based given parameters.""" - async def resolve_endpoint(self, params: EndpointParams) -> Endpoint: + async def resolve_endpoint(self, params: T) -> Endpoint: raise NotImplementedError() diff --git a/packages/smithy-http/src/smithy_http/endpoints.py b/packages/smithy-http/src/smithy_http/endpoints.py index 67c6b5a73..8f2945744 100644 --- a/packages/smithy-http/src/smithy_http/endpoints.py +++ b/packages/smithy-http/src/smithy_http/endpoints.py @@ -3,11 +3,10 @@ from dataclasses import dataclass, field from typing import Protocol, Self -from smithy_core import SmithyException from smithy_core.interfaces import URI from . import Fields, interfaces -from .aio.interfaces import EndpointParametersProtocol +from .aio.interfaces import EndpointParameters @dataclass @@ -16,16 +15,12 @@ class Endpoint(interfaces.Endpoint): headers: interfaces.Fields = field(default_factory=Fields) -class EndpointResolutionError(SmithyException): - """Exception type for all exceptions raised by endpoint resolution.""" - - class _UriConfig(Protocol): endpoint_uri: str | URI | None @dataclass -class StaticEndpointParams(EndpointParametersProtocol[_UriConfig]): +class StaticEndpointParams(EndpointParameters[_UriConfig]): """Static endpoint params. :param uri: A static URI to route requests to. diff --git a/packages/smithy-http/src/smithy_http/exceptions.py b/packages/smithy-http/src/smithy_http/exceptions.py index 08ced3c39..2b27c2942 100644 --- a/packages/smithy-http/src/smithy_http/exceptions.py +++ b/packages/smithy-http/src/smithy_http/exceptions.py @@ -5,3 +5,7 @@ class SmithyHTTPException(SmithyException): """Base exception type for all exceptions raised in HTTP clients.""" + + +class EndpointResolutionError(SmithyException): + """Exception type for all exceptions raised by endpoint resolution."""