diff --git a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsPythonDependency.java b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsPythonDependency.java new file mode 100644 index 000000000..d6d3563db --- /dev/null +++ b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsPythonDependency.java @@ -0,0 +1,26 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.python.aws.codegen; + +import software.amazon.smithy.python.codegen.PythonDependency; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * AWS Dependencies used in the smithy python generator. + */ +@SmithyUnstableApi +public class AwsPythonDependency { + /** + * The core aws smithy runtime python package. + * + *

While in development this will use the develop branch. + */ + public static final PythonDependency SMITHY_AWS_CORE = new PythonDependency( + "smithy_aws_core", + // You'll need to locally install this before we publish + "==0.0.1", + PythonDependency.Type.DEPENDENCY, + false); +} \ No newline at end of file diff --git a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsRegionalEndpointsGenerator.java b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsRegionalEndpointsGenerator.java new file mode 100644 index 000000000..47a49fa09 --- /dev/null +++ b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsRegionalEndpointsGenerator.java @@ -0,0 +1,82 @@ +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.SmithyPythonDependency; +import software.amazon.smithy.python.codegen.integrations.EndpointsGenerator; +import software.amazon.smithy.python.codegen.writer.PythonWriter; + +/** + * Generates endpoint config and resolution logic for standard regional endpoints. + */ +public class AwsRegionalEndpointsGenerator implements EndpointsGenerator { + @Override + public List endpointsConfig(GenerationContext context) { + return List.of(ConfigProperty.builder() + .name("endpoint_resolver") + .type(Symbol.builder() + .name("EndpointResolver[RegionalEndpointParameters]") + .addReference(Symbol.builder() + .name("RegionalEndpointParameters") + .namespace(AwsPythonDependency.SMITHY_AWS_CORE.packageName() + ".endpoints.standard_regional", ".") + .addDependency(AwsPythonDependency.SMITHY_AWS_CORE) + .build()) + .addReference(Symbol.builder() + .name("EndpointResolver") + .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_aws_core.endpoints.standard_regional", "StandardRegionalEndpointsResolver"); + 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 StandardRegionalEndpointsResolver(endpoint_prefix='$L')", + endpointPrefix); + }) + .build(), + ConfigProperty.builder() + .name("endpoint_uri") + .type(Symbol.builder() + .name("str | URI") + .addReference(Symbol.builder() + .name("URI") + .namespace("smithy_core.interfaces", ".") + .addDependency(SmithyPythonDependency.SMITHY_CORE) + .build()) + .build()) + .documentation("A static URI to route requests to.") + .build(), + 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()); + } + + @Override + public void renderEndpointParameterConstruction(GenerationContext context, PythonWriter writer) { + + writer.addDependency(AwsPythonDependency.SMITHY_AWS_CORE); + writer.addImport("smithy_aws_core.endpoints.standard_regional", "RegionalEndpointParameters"); + writer.write(""" + endpoint_parameters = RegionalEndpointParameters( + sdk_endpoint=config.endpoint_uri, + region=config.region + ) + """); + } +} diff --git a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsRegionalEndpointsIntegration.java b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsRegionalEndpointsIntegration.java new file mode 100644 index 000000000..298f88acc --- /dev/null +++ b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsRegionalEndpointsIntegration.java @@ -0,0 +1,14 @@ +package software.amazon.smithy.python.aws.codegen; + +import java.util.Optional; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.python.codegen.integrations.EndpointsGenerator; +import software.amazon.smithy.python.codegen.integrations.PythonIntegration; + +public class AwsRegionalEndpointsIntegration implements PythonIntegration { + @Override + public Optional getEndpointsGenerator(Model model, ServiceShape service) { + return Optional.of(new AwsRegionalEndpointsGenerator()); + } +} 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 5155ed74a..b4ae333c5 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 @@ -5,3 +5,4 @@ software.amazon.smithy.python.aws.codegen.AwsAuthIntegration software.amazon.smithy.python.aws.codegen.AwsProtocolsIntegration +software.amazon.smithy.python.aws.codegen.AwsRegionalEndpointsIntegration 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 5aa42d257..b2df848c5 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 @@ -442,20 +442,14 @@ 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"); + + context.endpointsGenerator().renderEndpointParameterConstruction(context, writer); + 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) - logger.debug("Calling endpoint resolver with parameters: %s", endpoint_resolver_parameters) - endpoint = await config.endpoint_resolver.resolve_endpoint( - endpoint_resolver_parameters - ) + logger.debug("Calling endpoint resolver with parameters: %s", endpoint_parameters) + endpoint = await config.endpoint_resolver.resolve_endpoint(endpoint_parameters) logger.debug("Endpoint resolver result: %s", endpoint) if not endpoint.uri.path: path = "" @@ -474,7 +468,7 @@ async def _handle_attempt( ) context._transport_request.fields.extend(endpoint.headers) - """, errorSymbol); + """); } writer.popState(); 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 a52cb082e..98270804e 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 @@ -47,8 +47,10 @@ import software.amazon.smithy.python.codegen.generators.ProtocolGenerator; import software.amazon.smithy.python.codegen.generators.SchemaGenerator; import software.amazon.smithy.python.codegen.generators.SetupGenerator; +import software.amazon.smithy.python.codegen.generators.StaticEndpointsGenerator; import software.amazon.smithy.python.codegen.generators.StructureGenerator; import software.amazon.smithy.python.codegen.generators.UnionGenerator; +import software.amazon.smithy.python.codegen.integrations.EndpointsGenerator; import software.amazon.smithy.python.codegen.integrations.PythonIntegration; import software.amazon.smithy.python.codegen.writer.PythonDelegator; import software.amazon.smithy.python.codegen.writer.PythonWriter; @@ -81,9 +83,20 @@ public GenerationContext createContext(CreateContextDirective directive) { + for (PythonIntegration integration : directive.integrations()) { + Optional generator = integration.getEndpointsGenerator(directive.model(), directive.service()); + if (generator.isPresent()) { + return generator.get(); + } + } + return new StaticEndpointsGenerator(); + } + private ProtocolGenerator resolveProtocolGenerator( Collection integrations, Model model, diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/GenerationContext.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/GenerationContext.java index 2f112e388..18fd2cd5e 100644 --- a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/GenerationContext.java +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/GenerationContext.java @@ -11,6 +11,7 @@ import software.amazon.smithy.codegen.core.WriterDelegator; import software.amazon.smithy.model.Model; import software.amazon.smithy.python.codegen.generators.ProtocolGenerator; +import software.amazon.smithy.python.codegen.integrations.EndpointsGenerator; import software.amazon.smithy.python.codegen.integrations.PythonIntegration; import software.amazon.smithy.python.codegen.writer.PythonDelegator; import software.amazon.smithy.python.codegen.writer.PythonWriter; @@ -32,6 +33,7 @@ public final class GenerationContext private final PythonDelegator delegator; private final List integrations; private final ProtocolGenerator protocolGenerator; + private final EndpointsGenerator endpointsGenerator; private GenerationContext(Builder builder) { model = builder.model; @@ -41,6 +43,7 @@ private GenerationContext(Builder builder) { delegator = builder.delegator; integrations = builder.integrations; protocolGenerator = builder.protocolGenerator; + endpointsGenerator = builder.endpointsGenerator; } @Override @@ -80,6 +83,13 @@ public ProtocolGenerator protocolGenerator() { return protocolGenerator; } + /** + * @return Returns the endpoints generator to use in code generation. + */ + public EndpointsGenerator endpointsGenerator () { + return endpointsGenerator; + } + /** * Gets the application protocol for the service protocol. * @@ -105,7 +115,8 @@ public SmithyBuilder toBuilder() { .settings(settings) .symbolProvider(symbolProvider) .fileManifest(fileManifest) - .writerDelegator(delegator); + .writerDelegator(delegator) + .endpointsGenerator(endpointsGenerator); } /** @@ -119,6 +130,7 @@ public static final class Builder implements SmithyBuilder { private PythonDelegator delegator; private List integrations; private ProtocolGenerator protocolGenerator; + private EndpointsGenerator endpointsGenerator; @Override public GenerationContext build() { @@ -187,5 +199,14 @@ public Builder protocolGenerator(ProtocolGenerator protocolGenerator) { this.protocolGenerator = protocolGenerator; return this; } + + /** + * @param endpointsGenerator The resolved endpoints generator to use. + * @return Returns the builder. + */ + public Builder endpointsGenerator(EndpointsGenerator endpointsGenerator) { + this.endpointsGenerator = endpointsGenerator; + return this; + } } } 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 fb01926d8..0a435bcd1 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 @@ -68,7 +68,7 @@ public final class ConfigGenerator implements Runnable { // This list contains any properties that must be added to any http-based // service client, except for the http client itself. - private static final List HTTP_PROPERTIES = Arrays.asList( + private static final List HTTP_PROPERTIES = List.of( ConfigProperty.builder() .name("http_request_config") .type(Symbol.builder() @@ -77,42 +77,6 @@ public final class ConfigGenerator implements Runnable { .addDependency(SmithyPythonDependency.SMITHY_HTTP) .build()) .documentation("Configuration for individual HTTP requests.") - .build(), - ConfigProperty.builder() - .name("endpoint_resolver") - .type(Symbol.builder() - .name("EndpointResolver[Any]") - .addReference(Symbol.builder() - .name("Any") - .namespace("typing", ".") - .putProperty(SymbolProperties.STDLIB, true) - .build()) - .addReference(Symbol.builder() - .name("EndpointResolver") - .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.endpoints", "StaticEndpointResolver"); - writer.write("self.endpoint_resolver = endpoint_resolver or StaticEndpointResolver()"); - }) - .build(), - ConfigProperty.builder() - .name("endpoint_uri") - .type(Symbol.builder() - .name("str | URI") - .addReference(Symbol.builder() - .name("URI") - .namespace("smithy_core.interfaces", ".") - .addDependency(SmithyPythonDependency.SMITHY_CORE) - .build()) - .build()) - .documentation("A static URI to route requests to.") .build()); private final PythonSettings settings; @@ -154,6 +118,7 @@ private static List getHttpProperties(GenerationContext context) properties.add(clientBuilder.build()); properties.addAll(HTTP_PROPERTIES); + properties.addAll(context.endpointsGenerator().endpointsConfig(context)); return List.copyOf(properties); } diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/StaticEndpointsGenerator.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/StaticEndpointsGenerator.java new file mode 100644 index 000000000..a4ef6c757 --- /dev/null +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/StaticEndpointsGenerator.java @@ -0,0 +1,63 @@ +package software.amazon.smithy.python.codegen.generators; + +import java.util.List; +import software.amazon.smithy.codegen.core.Symbol; +import software.amazon.smithy.python.codegen.ConfigProperty; +import software.amazon.smithy.python.codegen.GenerationContext; +import software.amazon.smithy.python.codegen.SmithyPythonDependency; +import software.amazon.smithy.python.codegen.SymbolProperties; +import software.amazon.smithy.python.codegen.integrations.EndpointsGenerator; +import software.amazon.smithy.python.codegen.writer.PythonWriter; + +public class StaticEndpointsGenerator implements EndpointsGenerator { + @Override + public List endpointsConfig(GenerationContext context) { + return List.of(ConfigProperty.builder() + .name("endpoint_resolver") + .type(Symbol.builder() + .name("EndpointResolver[Any]") + .addReference(Symbol.builder() + .name("Any") + .namespace("typing", ".") + .putProperty(SymbolProperties.STDLIB, true) + .build()) + .addReference(Symbol.builder() + .name("EndpointResolver") + .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.endpoints", "StaticEndpointResolver"); + writer.write("self.endpoint_resolver = endpoint_resolver or StaticEndpointResolver()"); + }) + .build(), + ConfigProperty.builder() + .name("endpoint_uri") + .type(Symbol.builder() + .name("str | URI") + .addReference(Symbol.builder() + .name("URI") + .namespace("smithy_core.interfaces", ".") + .addDependency(SmithyPythonDependency.SMITHY_CORE) + .build()) + .build()) + .documentation("A static URI to route requests to.") + .build()); + } + + @Override + public void renderEndpointParameterConstruction(GenerationContext context, PythonWriter writer) { + writer.addDependency(SmithyPythonDependency.SMITHY_HTTP); + writer.addImport("smithy_http.endpoints", "StaticEndpointParams"); + writer.write(""" + endpoint_parameters = StaticEndpointParams( + uri=config.endpoint_uri + ) + """); + } +} diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/integrations/EndpointsGenerator.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/integrations/EndpointsGenerator.java new file mode 100644 index 000000000..dfb349bd9 --- /dev/null +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/integrations/EndpointsGenerator.java @@ -0,0 +1,38 @@ +package software.amazon.smithy.python.codegen.integrations; + +import java.util.List; +import software.amazon.smithy.python.codegen.ConfigProperty; +import software.amazon.smithy.python.codegen.GenerationContext; +import software.amazon.smithy.python.codegen.writer.PythonWriter; + +/** + * Interface for Endpoints Generators. + * Endpoints Generators are responsible for defining the client configuration + * required for resolving endpoints, this includes an appropriately typed + * `endpoint_resolver` and any other configuration properties such as `endpoint_uri`. + * Endpoint generators are also responsible for rendering the logic in the client's + * request execution stack that sets the destination on the transport request. + * Endpoints Generators are only applied to services that use an HTTP transport. + */ +public interface EndpointsGenerator { + + /** + * Endpoints Generators are responsible for defining the client configuration + * required for resolving endpoints, this must include an appropriately typed + * `endpoint_resolver` and any other configuration properties such as `endpoint_uri` + * + * @param context generation context. + * @return list of client config required to resolve endpoints. + */ + List endpointsConfig(GenerationContext context); + + /** + * Render the logic in the client's request execution stack that constructs + * `endpoint_parameters`. Implementations must add required dependencies and import statements + * and must ensure the `endpoint_parameters` variable is set. + * + * @param context generation context + * @param writer writer to write out logic to construct `endpoint_parameters`. + */ + void renderEndpointParameterConstruction(GenerationContext context, PythonWriter writer); +} diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/integrations/PythonIntegration.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/integrations/PythonIntegration.java index a23cf237a..40b33e252 100644 --- a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/integrations/PythonIntegration.java +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/integrations/PythonIntegration.java @@ -6,7 +6,10 @@ import java.util.Collections; import java.util.List; +import java.util.Optional; import software.amazon.smithy.codegen.core.SmithyIntegration; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.ServiceShape; import software.amazon.smithy.python.codegen.GenerationContext; import software.amazon.smithy.python.codegen.PythonSettings; import software.amazon.smithy.python.codegen.generators.ProtocolGenerator; @@ -38,4 +41,13 @@ default List getProtocolGenerators() { default List getClientPlugins() { return Collections.emptyList(); } + + /** + * Get an EndpointsGenerator that will be used to generate endpoints related config and resolution logic. + * + * @return optional EndpointsGenerator. + */ + default Optional getEndpointsGenerator(Model model, ServiceShape service) { + return Optional.empty(); + } } 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..a1337734b 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,10 @@ 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 +25,4 @@ class StaticEndpointParams: :param uri: A static URI to route requests to. """ - uri: str | URI + uri: str | URI | None