Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,298 @@
/*
* 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.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import software.amazon.smithy.aws.traits.ServiceTrait;
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.CodegenUtils;
import software.amazon.smithy.python.codegen.ConfigProperty;
import software.amazon.smithy.python.codegen.GenerationContext;
import software.amazon.smithy.python.codegen.RuntimeTypes;
import software.amazon.smithy.python.codegen.SmithyPythonDependency;
import software.amazon.smithy.python.codegen.integrations.PythonIntegration;
import software.amazon.smithy.python.codegen.integrations.RuntimeClientPlugin;
import software.amazon.smithy.python.codegen.sections.AsyncConfigSection;
import software.amazon.smithy.python.codegen.writer.PythonWriter;
import software.amazon.smithy.utils.CodeInterceptor;
import software.amazon.smithy.utils.CodeSection;
import software.amazon.smithy.utils.SmithyInternalApi;

/**
* AWS integration that generates the async config subclass (e.g., AsyncBedrockRuntimeConfig)
* inheriting from AsyncAwsConfig with service-specific fields and defaults.
*/
@SmithyInternalApi
public class AwsAsyncConfigIntegration implements PythonIntegration {

@Override
public List<? extends CodeInterceptor<? extends CodeSection, PythonWriter>> interceptors(
GenerationContext context
) {
return List.of(new AsyncConfigInterceptor(context));
}

private static final class AsyncConfigInterceptor
implements CodeInterceptor<AsyncConfigSection, PythonWriter> {

private final GenerationContext context;

AsyncConfigInterceptor(GenerationContext context) {
this.context = context;
}

@Override
public Class<AsyncConfigSection> sectionType() {
return AsyncConfigSection.class;
}

@Override
public void write(PythonWriter writer, String previousText, AsyncConfigSection section) {
// Write any previous content first
writer.write(previousText);

var model = context.model();
var service = context.settings().service(model);

// Gate on the same source of truth the core generators use to decide whether
// to emit references to these classes. If it says no symbol is generated, we
// must not define one, or the two would disagree.
var maybeAsyncConfigSymbol = CodegenUtils.getAsyncConfigSymbol(context.settings(), model);
if (maybeAsyncConfigSymbol.isEmpty()) {
return;
}
var asyncConfigSymbol = maybeAsyncConfigSymbol.get();

final String serviceId = service.getTrait(ServiceTrait.class)
.map(ServiceTrait::getSdkId)
.orElse(context.settings().service().getName());

// Import AsyncAwsConfig base class
var asyncAwsConfigSymbol = Symbol.builder()
.name("AsyncAwsConfig")
.namespace("smithy_aws_core.config.aws_config", ".")
.addDependency(AwsPythonDependency.SMITHY_AWS_CORE)
.build();

// Import FieldSpec and ClassVar
var fieldSpecSymbol = Symbol.builder()
.name("FieldSpec")
.namespace("smithy_aws_core.config.types", ".")
.addDependency(AwsPythonDependency.SMITHY_AWS_CORE)
.build();
writer.addStdlibImport("typing", "ClassVar");
writer.addStdlibImport("typing", "Any");
writer.addStdlibImport("dataclasses", "dataclass");

writer.write("");
writer.write("");
// repr=False is required: AsyncAwsConfig defines a __repr__ that filters out
// credential fields, and a generated __repr__ on this subclass would shadow it
// and leak secrets.
writer.write("@dataclass(kw_only=True, repr=False)");
writer.openBlock("class $L($T):", asyncConfigSymbol.getName(), asyncAwsConfigSymbol);
writer.writeDocs(serviceId + " configuration (async-resolved).", context);
writer.write("");

// Write service-specific field declarations
writer.write("endpoint_resolver: $T | None = None", RuntimeTypes.ENDPOINT_RESOLVER);
writer.writeDocs("The endpoint resolver used to resolve the final endpoint per-operation "
+ "based on the configuration.", context);
writer.write("");

writer.write("protocol: $T | None = None",
Symbol.builder()
.name("ClientProtocol[Any, Any]")
.addReference(Symbol.builder()
.name("ClientProtocol")
.namespace("smithy_core.aio.interfaces", ".")
.addDependency(SmithyPythonDependency.SMITHY_CORE)
.build())
.build());
writer.writeDocs("The protocol to serialize and deserialize requests with.", context);
writer.write("");
Comment on lines +122 to +123

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are multiple config options that get generated with trailing whitespace in their docstrings:

"""The protocol to serialize and deserialize requests with.    """

This should be:

"""The protocol to serialize and deserialize requests with.    """

Can you investigate this bug and compare with the existing Config object to see why there is this difference?


var serviceIndex = ServiceIndex.of(context.model());
var hasAuth = !serviceIndex.getAuthSchemes(context.settings().service()).isEmpty();

if (hasAuth) {
writer.write("auth_schemes: dict[$T, $T] | None = None",
RuntimeTypes.SHAPE_ID,
Symbol.builder()
.name("AuthScheme[Any, Any, Any, Any]")
.addReference(Symbol.builder()
.name("AuthScheme")
.namespace("smithy_core.aio.interfaces.auth", ".")
.addDependency(SmithyPythonDependency.SMITHY_CORE)
.build())
.build());
writer.writeDocs("A map of auth scheme ids to auth schemes.", context);
writer.write("");

writer.write("auth_scheme_resolver: $T | None = None",
CodegenUtils.getHttpAuthSchemeResolverSymbol(context.settings()));
writer.writeDocs("An auth scheme resolver that determines the auth scheme "
+ "for each operation.", context);
writer.write("");
}

// Plugin-contributed field declarations (e.g., api_key for @httpApiKeyAuth).
//
// More than one plugin can contribute the same property — region, for
// instance, comes from both the auth and regional-endpoints integrations
// — so track the names already written and emit each only once.
var writtenProperties = new LinkedHashSet<String>();
for (PythonIntegration integration : context.integrations()) {
for (RuntimeClientPlugin plugin : integration.getClientPlugins(context)) {
if (plugin.matchesService(model, service)) {
for (ConfigProperty property : plugin.getConfigProperties()) {
if (!writtenProperties.add(property.name())) {
continue;
}
writer.write("$L: $T | None = None", property.name(), property.type());
writer.writeDocs(property.documentation(), context);
writer.write("");
}
}
}
}

// Write _FIELDS class variable with service-specific defaults
writer.openBlock("_FIELDS: ClassVar[dict[str, $T]] = {", fieldSpecSymbol);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This emits something like below:

    _FIELDS: ClassVar[dict[str, FieldSpec]] = {
        "aws_credentials_identity_resolver": FieldSpec(default=None),
        "region": FieldSpec(default=None),
        "aws_access_key_id": FieldSpec(default=None),
        "aws_secret_access_key": FieldSpec(default=None),
        "aws_session_token": FieldSpec(default=None),
        "user_agent_extra": FieldSpec(default=None),
        "sdk_ua_app_id": FieldSpec(default=None),
        **AsyncAwsConfig._FIELDS,
        "endpoint_uri": FieldSpec(
            default=None, resolver=EndpointUriResolver("bedrock_runtime")
        ),
        "endpoint_resolver": FieldSpec(
            default_factory=lambda: StandardRegionalEndpointsResolver(
                endpoint_prefix="bedrock-runtime"
            )
        ),
        "protocol": FieldSpec(
            default_factory=lambda: RestJsonClientProtocol(
                _SCHEMA_AMAZON_BEDROCK_FRONTEND_SERVICE
            )
        ),
        "auth_schemes": FieldSpec(
            default_factory=lambda: {
                ShapeID("aws.auth#sigv4"): SigV4AuthScheme(service="bedrock")
            }
        ),
        "auth_scheme_resolver": FieldSpec(default_factory=HTTPAuthSchemeResolver),
        "transport": FieldSpec(default_factory=lambda: AWSCRTHTTPClient()),
    }

It's not clean to my why we're emitting inherited fields here that I though would be covered by **AsyncAwsConfig._FIELDS,.

I was expecting to see something closer to:

 _FIELDS = {
      **AsyncAwsConfig._FIELDS,
      "endpoint_uri": ...,
      "endpoint_resolver": ...,
      "protocol": ...,
      "auth_schemes": ...,
      "auth_scheme_resolver": ...,
      "transport": ...,
  }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intentional. The base class (AsyncAwsConfig in aws_config.py) declares common config fields with their resolvers and validators. When AwsAsyncConfigIntegration.java generates the service-specific config, it iterates all registered integration plugins (like AwsAuthIntegration,AwsUserAgentIntegration) to emit config fields they contribute. Some of those plugins contribute fields that already exist in the base class, just like the duplicates here.

Functionally, these duplicates are harmless because if a config var is declared in the base class it will overwrite the duplicate ones with the proper specs (resolvers and validators). Fields that are unique to a service are not overwritten and will be used in config resolution. One option to prevent this was by filtering based on the config vars that are already in the base class, but that'd require us to hardcode the list of those config vars in codegen. That meant creating a second source of truth that needs to be in sync with the variables in the base class. For now, I chose to keep duplicates rather than have two sources of truth.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I may be missing something here, but why can't we just strip out the config property declarations from the runtime plugins in these aws integrations? We shouldn't keep the old codegen-defined properties if we are moving to maintaining these in our runtime libraries. We're effectively maintaining two sources of truth for AWS-specific config options now.


// Plugin-contributed FieldSpec entries are emitted before the base class
// spread. Some duplicate fields already in AsyncAwsConfig._FIELDS (e.g.,
// region, sdk_ua_app_id) — these are harmlessly overwritten by the spread
// below. Fields unique to this service (e.g., api_key from @httpApiKeyAuth)
// survive and participate in the resolution pipeline.
for (String propertyName : writtenProperties) {
writer.write("\"$L\": $T(default=None),", propertyName, fieldSpecSymbol);
}

writer.write("**$T._FIELDS,", asyncAwsConfigSymbol);

// Everything below deliberately overrides the base class and so must
// stay after the spread.

// endpoint_uri FieldSpec — overrides base class with service-aware resolver
var endpointUriResolverSymbol = Symbol.builder()
.name("EndpointUriResolver")
.namespace("smithy_aws_core.config.resolvers", ".")
.addDependency(AwsPythonDependency.SMITHY_AWS_CORE)
.build();
var snakeCaseServiceId = serviceId.replace(" ", "_").toLowerCase();
writer.write("\"endpoint_uri\": $T(", fieldSpecSymbol);
writer.indent();
writer.write("default=None,");
writer.write("resolver=$T($S),", endpointUriResolverSymbol, snakeCaseServiceId);
writer.dedent();
writer.write("),");

// endpoint_resolver FieldSpec
var endpointPrefix = service.getTrait(ServiceTrait.class)
.map(ServiceTrait::getEndpointPrefix)
.orElse(context.settings().service().getName());
writer.write("\"endpoint_resolver\": $T(", fieldSpecSymbol);
writer.indent();
writer.write("default_factory=lambda: $T(endpoint_prefix=$S),",
AwsRuntimeTypes.STANDARD_REGIONAL_ENDPOINTS_RESOLVER,
endpointPrefix);
writer.dedent();
writer.write("),");

// protocol FieldSpec
writer.write("\"protocol\": $T(", fieldSpecSymbol);
writer.indent();
writer.write("default_factory=lambda: ${C|},",
writer.consumer(w -> context.protocolGenerator().initializeProtocol(context, w)));
writer.dedent();
writer.write("),");

// auth_schemes FieldSpec
if (hasAuth) {
writer.write("\"auth_schemes\": $T(", fieldSpecSymbol);
writer.indent();
writer.write("default_factory=lambda: ${C|},",
writer.consumer(w -> writeAsyncDefaultAuthSchemes(context, w)));
writer.dedent();
writer.write("),");

// auth_scheme_resolver FieldSpec
writer.write("\"auth_scheme_resolver\": $T(", fieldSpecSymbol);
writer.indent();
writer.write("default_factory=$T,",
CodegenUtils.getHttpAuthSchemeResolverSymbol(context.settings()));
writer.dedent();
writer.write("),");
}

// transport FieldSpec
writer.write("\"transport\": $T(", fieldSpecSymbol);
writer.indent();
if (usesHttp2(context)) {
writer.addDependency(SmithyPythonDependency.SMITHY_HTTP.withOptionalDependencies("awscrt"));
writer.write("default_factory=lambda: $T(),", RuntimeTypes.AWS_CRT_HTTP_CLIENT);
} else {
writer.addDependency(SmithyPythonDependency.SMITHY_HTTP.withOptionalDependencies("aiohttp"));
writer.write("default_factory=lambda: $T(),", RuntimeTypes.AIOHTTP_CLIENT);
}
writer.dedent();
writer.write("),");

writer.closeBlock("}");
writer.closeBlock("");
}

private static void writeAsyncDefaultAuthSchemes(GenerationContext context, PythonWriter writer) {
var service = context.settings().service(context.model());
writer.openBlock("{");
for (PythonIntegration integration : context.integrations()) {
for (RuntimeClientPlugin plugin : integration.getClientPlugins(context)) {
if (plugin.matchesService(context.model(), service) && plugin.getAuthScheme().isPresent()) {
var scheme = plugin.getAuthScheme().get();
writer.write("$T($S): ${C|},",
RuntimeTypes.SHAPE_ID,
scheme.getAuthTrait(),
writer.consumer(w -> scheme.initializeScheme(context, writer, service)));
}
}
}
writer.closeBlock("}");
}

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();

if (httpVersions.contains("h2")) {
return true;
}

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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,19 @@ def aws_user_agent_plugin(config: $1T):
)
""";

// Variant for services without a generated async config, which uses old Config.
private static final String USER_AGENT_PLUGIN_SYNC_ONLY = """

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which services won't have an async config? Shouldn't they all have the async config right now?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All AWS services have async config. This is for non-AWS Smithy services (those without @aws.api#service), which don't get an async config generated, getAsyncConfigSymbol returns Optional.empty() for them.

def aws_user_agent_plugin(config: $1T):
config.interceptors.append(
$2T(
ua_suffix=config.user_agent_extra,
ua_app_id=config.sdk_ua_app_id,
sdk_version=$3T,
service_id=$4S,
)
)
""";

@Override
public List<RuntimeClientPlugin> getClientPlugins(GenerationContext context) {
if (context.applicationProtocol().isHttpProtocol()) {
Expand Down Expand Up @@ -96,12 +109,22 @@ public List<RuntimeClientPlugin> getClientPlugins(GenerationContext context) {
filename,
moduleName + ".",
writer -> {
writer.write(USER_AGENT_PLUGIN,
CodegenUtils.getConfigSymbol(c.settings()),
userAgentInterceptor,
versionSymbol,
serviceId);

var asyncConfig = CodegenUtils.getAsyncConfigSymbol(
c.settings(),
c.model());
if (asyncConfig.isPresent()) {
writer.write(USER_AGENT_PLUGIN,
asyncConfig.get(),
userAgentInterceptor,
versionSymbol,
serviceId);
} else {
writer.write(USER_AGENT_PLUGIN_SYNC_ONLY,
CodegenUtils.getConfigSymbol(c.settings()),
userAgentInterceptor,
versionSymbol,
serviceId);
}
});
return List.of(filename);
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package software.amazon.smithy.python.aws.codegen;
package software.amazon.smithy.python.aws.codegen.customizations.dynamodb;

import java.util.List;
import java.util.Set;
import software.amazon.smithy.aws.traits.ServiceTrait;
import software.amazon.smithy.codegen.core.Symbol;
import software.amazon.smithy.codegen.core.SymbolReference;
import software.amazon.smithy.python.aws.codegen.AwsPythonDependency;
import software.amazon.smithy.python.codegen.GenerationContext;
import software.amazon.smithy.python.codegen.SmithyPythonDependency;
import software.amazon.smithy.python.codegen.integrations.PythonIntegration;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ software.amazon.smithy.python.aws.codegen.AwsProtocolsIntegration
software.amazon.smithy.python.aws.codegen.AwsServiceIdIntegration
software.amazon.smithy.python.aws.codegen.AwsUserAgentIntegration
software.amazon.smithy.python.aws.codegen.AwsStandardRegionalEndpointsIntegration
software.amazon.smithy.python.aws.codegen.AwsDynamoDbRetryIntegration
software.amazon.smithy.python.aws.codegen.customizations.dynamodb.AwsDynamoDbRetryIntegration
software.amazon.smithy.python.aws.codegen.AwsAsyncConfigIntegration
Loading
Loading