From e22b3cffed833a37570ab81d0e1f6d846d04c5b9 Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Fri, 13 Dec 2024 15:03:55 +0100 Subject: [PATCH] Use CRT by default when h2 is needed This updates the code generator to set the default http client to the CRT when the protocol configures it or when bidirectional event streaming is needed. --- .../model/main.smithy | 5 +- .../python/codegen/ApplicationProtocol.java | 23 +++++- .../python/codegen/ConfigGenerator.java | 82 +++++++++++++++---- .../python/codegen/GenerationContext.java | 2 +- .../python/codegen/PythonDependency.java | 4 + .../codegen/SmithyPythonDependency.java | 4 +- .../HttpBindingProtocolGenerator.java | 2 +- .../integration/ProtocolGenerator.java | 2 +- .../RestJsonProtocolGenerator.java | 14 ++++ 9 files changed, 110 insertions(+), 28 deletions(-) diff --git a/codegen/smithy-python-codegen-test/model/main.smithy b/codegen/smithy-python-codegen-test/model/main.smithy index 39ee49502..d79556b83 100644 --- a/codegen/smithy-python-codegen-test/model/main.smithy +++ b/codegen/smithy-python-codegen-test/model/main.smithy @@ -8,7 +8,10 @@ use smithy.test#httpResponseTests use smithy.waiters#waitable /// Provides weather forecasts. -@restJson1 +@restJson1( + http: ["h2", "http/1.1"] + eventStreamHttp: ["h2"] +) @fakeProtocol @paginated(inputToken: "nextToken", outputToken: "nextToken", pageSize: "pageSize") @httpApiKeyAuth(name: "weather-auth", in: "header") diff --git a/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/ApplicationProtocol.java b/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/ApplicationProtocol.java index a1c7ae8d2..42a321d3f 100644 --- a/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/ApplicationProtocol.java +++ b/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/ApplicationProtocol.java @@ -17,6 +17,7 @@ import software.amazon.smithy.codegen.core.Symbol; import software.amazon.smithy.codegen.core.SymbolReference; +import software.amazon.smithy.model.node.ObjectNode; import software.amazon.smithy.utils.SmithyUnstableApi; /** @@ -24,8 +25,12 @@ * application protocol (e.g., "http", "mqtt", etc). */ @SmithyUnstableApi -public record ApplicationProtocol(String name, SymbolReference requestType, SymbolReference responseType) { - +public record ApplicationProtocol( + String name, + SymbolReference requestType, + SymbolReference responseType, + ObjectNode configuration +) { /** * Checks if the protocol is an HTTP based protocol. * @@ -40,7 +45,7 @@ public boolean isHttpProtocol() { * * @return Returns the created application protocol. */ - public static ApplicationProtocol createDefaultHttpApplicationProtocol() { + public static ApplicationProtocol createDefaultHttpApplicationProtocol(ObjectNode config) { return new ApplicationProtocol( "http", SymbolReference.builder() @@ -48,10 +53,20 @@ public static ApplicationProtocol createDefaultHttpApplicationProtocol() { .build(), SymbolReference.builder() .symbol(createHttpSymbol("HTTPResponse")) - .build() + .build(), + config ); } + /** + * Creates a default HTTP application protocol. + * + * @return Returns the created application protocol. + */ + public static ApplicationProtocol createDefaultHttpApplicationProtocol() { + return createDefaultHttpApplicationProtocol(ObjectNode.objectNode()); + } + private static Symbol createHttpSymbol(String symbolName) { PythonDependency dependency = SmithyPythonDependency.SMITHY_HTTP; return Symbol.builder() diff --git a/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/ConfigGenerator.java b/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/ConfigGenerator.java index fb54c2757..8f847e1c7 100644 --- a/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/ConfigGenerator.java +++ b/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/ConfigGenerator.java @@ -20,9 +20,14 @@ import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; import software.amazon.smithy.codegen.core.Symbol; +import software.amazon.smithy.model.knowledge.EventStreamIndex; import software.amazon.smithy.model.knowledge.ServiceIndex; import software.amazon.smithy.model.knowledge.TopDownIndex; +import software.amazon.smithy.model.node.ArrayNode; +import software.amazon.smithy.model.node.StringNode; +import software.amazon.smithy.model.shapes.OperationShape; import software.amazon.smithy.python.codegen.integration.PythonIntegration; import software.amazon.smithy.python.codegen.integration.RuntimeClientPlugin; import software.amazon.smithy.python.codegen.sections.ConfigSection; @@ -65,23 +70,8 @@ final class ConfigGenerator implements Runnable { ); // This list contains any properties that must be added to any http-based - // service client. + // service client, except for the http client itself. private static final List HTTP_PROPERTIES = Arrays.asList( - ConfigProperty.builder() - .name("http_client") - .type(Symbol.builder() - .name("HTTPClient") - .namespace("smithy_http.aio.interfaces", ".") - .addDependency(SmithyPythonDependency.SMITHY_HTTP) - .build()) - .documentation("The HTTP client used to make requests.") - .nullable(false) - .initialize(writer -> { - writer.addDependency(SmithyPythonDependency.SMITHY_HTTP); - writer.addImport("smithy_http.aio.aiohttp", "AIOHTTPClient"); - writer.write("self.http_client = http_client or AIOHTTPClient()"); - }) - .build(), ConfigProperty.builder() .name("http_request_config") .type(Symbol.builder() @@ -137,6 +127,64 @@ final class ConfigGenerator implements Runnable { this.settings = settings; } + private static List getHttpProperties(GenerationContext context) { + var properties = new ArrayList(HTTP_PROPERTIES.size() + 1); + var clientBuilder = ConfigProperty.builder() + .name("http_client") + .type(Symbol.builder() + .name("HTTPClient") + .namespace("smithy_http.aio.interfaces", ".") + .addDependency(SmithyPythonDependency.SMITHY_HTTP) + .build()) + .documentation("The HTTP client used to make requests.") + .nullable(false); + + if (usesHttp2(context)) { + clientBuilder + .initialize(writer -> { + writer.addDependency(SmithyPythonDependency.SMITHY_HTTP.withOptionalDependencies("awscrt")); + writer.addImport("smithy_http.aio.crt", "AWSCRTHTTPClient"); + writer.write("self.http_client = http_client or AWSCRTHTTPClient()"); + }); + + } else { + clientBuilder + .initialize(writer -> { + writer.addDependency(SmithyPythonDependency.SMITHY_HTTP.withOptionalDependencies("aiohttp")); + writer.addImport("smithy_http.aio.aiohttp", "AIOHTTPClient"); + writer.write("self.http_client = http_client or AIOHTTPClient()"); + }); + } + properties.add(clientBuilder.build()); + + properties.addAll(HTTP_PROPERTIES); + return List.copyOf(properties); + } + + private static boolean usesHttp2(GenerationContext context) { + var configuration = context.applicationProtocol().configuration(); + var httpVersions = configuration.getArrayMember("http") + .orElse(ArrayNode.arrayNode()) + .getElementsAs(StringNode.class) + .stream().map(node -> node.getValue().toLowerCase(Locale.ENGLISH)).toList(); + + // An explicit http2 configuration + if (httpVersions.contains("h2")) { + return true; + } + + // Bidirectional streaming REQUIRES h2 inherently + var eventIndex = EventStreamIndex.of(context.model()); + var topDownIndex = TopDownIndex.of(context.model()); + for (OperationShape operation : topDownIndex.getContainedOperations(context.settings().service())) { + if (eventIndex.getInputInfo(operation).isPresent() && eventIndex.getOutputInfo(operation).isPresent()) { + return true; + } + } + + return false; + } + private static List getHttpAuthProperties(GenerationContext context) { return List.of( ConfigProperty.builder() @@ -254,7 +302,7 @@ private void generateConfig(GenerationContext context, PythonWriter writer) { // and add them in if the protocol is going to need them. var serviceIndex = ServiceIndex.of(context.model()); if (context.applicationProtocol().isHttpProtocol()) { - properties.addAll(HTTP_PROPERTIES); + properties.addAll(getHttpProperties(context)); if (!serviceIndex.getAuthSchemes(settings.service()).isEmpty()) { properties.addAll(getHttpAuthProperties(context)); writer.onSection(new AddAuthHelper()); diff --git a/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/GenerationContext.java b/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/GenerationContext.java index feecfb72b..0315e056b 100644 --- a/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/GenerationContext.java +++ b/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/GenerationContext.java @@ -96,7 +96,7 @@ public ProtocolGenerator protocolGenerator() { */ public ApplicationProtocol applicationProtocol() { return protocolGenerator != null - ? protocolGenerator.getApplicationProtocol() + ? protocolGenerator.getApplicationProtocol(this) : ApplicationProtocol.createDefaultHttpApplicationProtocol(); } diff --git a/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/PythonDependency.java b/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/PythonDependency.java index b62bd2a94..2e0fb9fa3 100644 --- a/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/PythonDependency.java +++ b/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/PythonDependency.java @@ -56,6 +56,10 @@ public SymbolDependency getDependency() { .build(); } + public PythonDependency withOptionalDependencies(String... optionalDependencies) { + return new PythonDependency(packageName, version, type, isLink, List.of(optionalDependencies)); + } + /** * An enum of valid dependency types. */ diff --git a/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/SmithyPythonDependency.java b/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/SmithyPythonDependency.java index 954c207d0..26a34ca17 100644 --- a/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/SmithyPythonDependency.java +++ b/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/SmithyPythonDependency.java @@ -49,9 +49,7 @@ public final class SmithyPythonDependency { // You'll need to locally install this before we publish "==0.0.1", Type.DEPENDENCY, - false, - // TODO: make this configurable - List.of("aiohttp") + false ); /** diff --git a/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/integration/HttpBindingProtocolGenerator.java b/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/integration/HttpBindingProtocolGenerator.java index e552eee7e..b34d45a68 100644 --- a/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/integration/HttpBindingProtocolGenerator.java +++ b/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/integration/HttpBindingProtocolGenerator.java @@ -92,7 +92,7 @@ public abstract class HttpBindingProtocolGenerator implements ProtocolGenerator private final Set deserializingDocumentShapes = new TreeSet<>(); @Override - public ApplicationProtocol getApplicationProtocol() { + public ApplicationProtocol getApplicationProtocol(GenerationContext context) { return ApplicationProtocol.createDefaultHttpApplicationProtocol(); } diff --git a/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/integration/ProtocolGenerator.java b/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/integration/ProtocolGenerator.java index 78257ec2e..e6ab93e37 100644 --- a/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/integration/ProtocolGenerator.java +++ b/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/integration/ProtocolGenerator.java @@ -55,7 +55,7 @@ default String getName() { * * @return Returns the created application protocol. */ - ApplicationProtocol getApplicationProtocol(); + ApplicationProtocol getApplicationProtocol(GenerationContext context); /** diff --git a/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/integration/RestJsonProtocolGenerator.java b/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/integration/RestJsonProtocolGenerator.java index 39484cd84..5246a031b 100644 --- a/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/integration/RestJsonProtocolGenerator.java +++ b/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/integration/RestJsonProtocolGenerator.java @@ -19,6 +19,8 @@ import java.util.Set; import software.amazon.smithy.aws.traits.protocols.RestJson1Trait; import software.amazon.smithy.model.knowledge.HttpBinding; +import software.amazon.smithy.model.node.ArrayNode; +import software.amazon.smithy.model.node.ObjectNode; import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.OperationShape; import software.amazon.smithy.model.shapes.Shape; @@ -27,6 +29,7 @@ import software.amazon.smithy.model.traits.StreamingTrait; import software.amazon.smithy.model.traits.TimestampFormatTrait.Format; import software.amazon.smithy.protocoltests.traits.HttpMessageTestCase; +import software.amazon.smithy.python.codegen.ApplicationProtocol; import software.amazon.smithy.python.codegen.CodegenUtils; import software.amazon.smithy.python.codegen.GenerationContext; import software.amazon.smithy.python.codegen.HttpProtocolTestGenerator; @@ -90,6 +93,17 @@ public ShapeId getProtocol() { return RestJson1Trait.ID; } + @Override + public ApplicationProtocol getApplicationProtocol(GenerationContext context) { + var service = context.settings().service(context.model()); + var trait = service.expectTrait(RestJson1Trait.class); + var config = ObjectNode.builder() + .withMember("http", ArrayNode.fromStrings(trait.getHttp())) + .withMember("eventStreamHttp", ArrayNode.fromStrings(trait.getEventStreamHttp())) + .build(); + return ApplicationProtocol.createDefaultHttpApplicationProtocol(config); + } + @Override protected Format getDocumentTimestampFormat() { return Format.EPOCH_SECONDS;