Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
cb47f65
WIP - add user agent interceptor/plugin
alextwoods Feb 25, 2025
91e8208
Working implementation with basic user agent
alextwoods Feb 25, 2025
d3b1d15
Fix pyright errors
alextwoods Feb 25, 2025
2031933
Use short form license
alextwoods Feb 25, 2025
d5473b5
Run pyupgrade
alextwoods Feb 26, 2025
83a0a8f
Cleanups from PR (still todo: seperate into generic user agent and aw…
alextwoods Feb 26, 2025
b7e15d1
Add Config/HttpConfig Protocols + generate __version__
alextwoods Feb 26, 2025
8afb134
Add __version__ to smithy and aws core
alextwoods Feb 26, 2025
a7475d7
WIP - generic useragent refactoring.
alextwoods Feb 27, 2025
296811b
Remove explicit Config protocol
alextwoods Feb 27, 2025
f449114
Major refactor - generic and aws specific user agent. Codegen userage…
alextwoods Feb 27, 2025
7b7573d
Merge branch 'develop' into user_agent
alextwoods Feb 28, 2025
6a49728
Use aws-sdk-python as sdk name + correctly use sdkId for serviceId
alextwoods Feb 28, 2025
b727753
Merge branch 'develop' into user_agent
alextwoods Feb 28, 2025
b88dfc2
Use aws core version as the "main" sdk version. client library versio…
alextwoods Feb 28, 2025
6c6458b
User agent tests
alextwoods Mar 3, 2025
7aac81c
Add user_agent interceptor test
alextwoods Mar 3, 2025
86e461c
Update packages/smithy-http/src/smithy_http/interceptors/user_agent.py
alextwoods Mar 3, 2025
0013139
Update packages/smithy-http/tests/unit/interceptors/test_user_agent.py
alextwoods Mar 3, 2025
00ce11f
PR cleanups
alextwoods Mar 3, 2025
8ea3bf2
PR updates
alextwoods Mar 3, 2025
a35f607
Merge branch 'develop' into user_agent
alextwoods Mar 4, 2025
5f128d4
Merge branch 'smithy-lang:develop' into user_agent
alextwoods Mar 5, 2025
20b97ed
PR Cleanups
alextwoods Mar 5, 2025
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
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ To send us a pull request, please:
1. Fork the repository.
2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change.
3. Ensure local tests pass (`make test-py` and `make test-protocols`).
4. Run `make lint-py` if you've changed any python sources.
4. Run `make lint-py` and `make check-py` if you've changed any python sources.
4. Commit to your fork using clear commit messages.
5. Send us a pull request, answering any default questions in the pull request interface.
6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation.
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* 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.codegen.core.SymbolReference;
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.utils.SmithyInternalApi;

/**
* Adds a runtime plugin to set user agent.
*/
@SmithyInternalApi
public class AwsUserAgentIntegration implements PythonIntegration {

public static final String USER_AGENT_PLUGIN = """
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='$4L'
)
)
""";

@Override
public List<RuntimeClientPlugin> getClientPlugins(GenerationContext context) {
if (context.applicationProtocol().isHttpProtocol()) {
final ConfigProperty userAgentExtra = ConfigProperty.builder()
.name("user_agent_extra")
.documentation("Additional suffix to be added to the User-Agent header.")
.type(Symbol.builder().name("str").build())
.nullable(true)
.build();

final ConfigProperty uaAppId = ConfigProperty.builder()
.name("sdk_ua_app_id")
.documentation(
"A unique and opaque application ID that is appended to the User-Agent header.")
.type(Symbol.builder().name("str").build())
.nullable(true)
.build();

final String user_agent_plugin_file = "user_agent";

final String moduleName = context.settings().moduleName();
final SymbolReference userAgentPlugin = SymbolReference.builder()
.symbol(Symbol.builder()
.namespace(String.format("%s.%s",
moduleName,
user_agent_plugin_file.replace('/', '.')), ".")
.definitionFile(String
.format("./%s/%s.py", moduleName, user_agent_plugin_file))
.name("aws_user_agent_plugin")
.build())
.build();
final SymbolReference userAgentInterceptor = SymbolReference.builder()
.symbol(Symbol.builder()
.namespace("smithy_aws_core.interceptors.user_agent", ".")
.name("UserAgentInterceptor")
.build())
.build();
final SymbolReference versionSymbol = SymbolReference.builder()
.symbol(Symbol.builder()
.namespace(moduleName, ".")
.name("__version__")
.build())
.build();

final String serviceId = context.settings()
.service(context.model())
.getTrait(ServiceTrait.class)
.map(ServiceTrait::getSdkId)
.orElse(context.settings().service().getName())
.replace(' ', '_');

return List.of(
RuntimeClientPlugin.builder()
.addConfigProperty(userAgentExtra)
.addConfigProperty(uaAppId)
.pythonPlugin(userAgentPlugin)
.writeAdditionalFiles((c) -> {
String filename = "%s/%s.py".formatted(moduleName, user_agent_plugin_file);
c.writerDelegator()
.useFileWriter(
filename,
moduleName + ".",
writer -> {
writer.write(USER_AGENT_PLUGIN,
CodegenUtils.getConfigSymbol(c.settings()),
userAgentInterceptor,
versionSymbol,
serviceId);

});
return List.of(filename);
})
.build());
} else {
return List.of();
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@

software.amazon.smithy.python.aws.codegen.AwsAuthIntegration
software.amazon.smithy.python.aws.codegen.AwsProtocolsIntegration
software.amazon.smithy.python.aws.codegen.AwsUserAgentIntegration
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ private void generateService(PythonWriter writer) {
var defaultPlugins = new LinkedHashSet<SymbolReference>();

for (PythonIntegration integration : context.integrations()) {
for (RuntimeClientPlugin runtimeClientPlugin : integration.getClientPlugins()) {
for (RuntimeClientPlugin runtimeClientPlugin : integration.getClientPlugins(context)) {
if (runtimeClientPlugin.matchesService(context.model(), service)) {
runtimeClientPlugin.getPythonPlugin().ifPresent(defaultPlugins::add);
}
Expand Down Expand Up @@ -657,7 +657,7 @@ private boolean hasEventStream() {
private void initializeHttpAuthParameters(PythonWriter writer) {
var derived = new LinkedHashSet<DerivedProperty>();
for (PythonIntegration integration : context.integrations()) {
for (RuntimeClientPlugin plugin : integration.getClientPlugins()) {
for (RuntimeClientPlugin plugin : integration.getClientPlugins(context)) {
if (plugin.matchesService(context.model(), service)
&& plugin.getAuthScheme().isPresent()
&& plugin.getAuthScheme().get().getApplicationProtocol().isHttpProtocol()) {
Expand Down Expand Up @@ -746,7 +746,7 @@ private void writeSharedOperationInit(PythonWriter writer, OperationShape operat

var defaultPlugins = new LinkedHashSet<SymbolReference>();
for (PythonIntegration integration : context.integrations()) {
for (RuntimeClientPlugin runtimeClientPlugin : integration.getClientPlugins()) {
for (RuntimeClientPlugin runtimeClientPlugin : integration.getClientPlugins(context)) {
if (runtimeClientPlugin.matchesOperation(context.model(), service, operation)) {
runtimeClientPlugin.getPythonPlugin().ifPresent(defaultPlugins::add);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
import software.amazon.smithy.python.codegen.generators.StructureGenerator;
import software.amazon.smithy.python.codegen.generators.UnionGenerator;
import software.amazon.smithy.python.codegen.integrations.PythonIntegration;
import software.amazon.smithy.python.codegen.integrations.RuntimeClientPlugin;
import software.amazon.smithy.python.codegen.writer.PythonDelegator;
import software.amazon.smithy.python.codegen.writer.PythonWriter;
import software.amazon.smithy.utils.SmithyUnstableApi;
Expand Down Expand Up @@ -273,9 +274,39 @@ public void generateIntEnumShape(GenerateIntEnumDirective<GenerationContext, Pyt

@Override
public void customizeBeforeIntegrations(CustomizeDirective<GenerationContext, PythonSettings> directive) {
generateServiceModuleInit(directive);
generatePluginFiles(directive);
generateInits(directive);
}

/**
* Writes out all extra files required by runtime plugins.
*/
private void generatePluginFiles(CustomizeDirective<GenerationContext, PythonSettings> directive) {
GenerationContext context = directive.context();
for (PythonIntegration integration : context.integrations()) {
for (RuntimeClientPlugin runtimeClientPlugin : integration.getClientPlugins(context)) {
if (runtimeClientPlugin.matchesService(context.model(), directive.service())) {
runtimeClientPlugin.writeAdditionalFiles(context);
}
}
}
}

/**
* Creates top level __init__.py file.
*/
private void generateServiceModuleInit(CustomizeDirective<GenerationContext, PythonSettings> directive) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This could be done directly in generateInits. I'm refactoring this in a separate PR though so it's fine to have it here for now.

directive.context()
.writerDelegator()
.useFileWriter(
"%s/__init__.py".formatted(directive.context().settings().moduleName()),
writer -> {
writer
.write("__version__: str = '$L'", directive.context().settings().moduleVersion());
});
}

/**
* Creates __init__.py files where not already present.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public void run() {
var properties = new ArrayList<DerivedProperty>();
var service = context.settings().service(context.model());
for (PythonIntegration integration : context.integrations()) {
for (RuntimeClientPlugin plugin : integration.getClientPlugins()) {
for (RuntimeClientPlugin plugin : integration.getClientPlugins(context)) {
if (plugin.matchesService(context.model(), service)
&& plugin.getAuthScheme().isPresent()
&& plugin.getAuthScheme().get().getApplicationProtocol().isHttpProtocol()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ private static void writeDefaultHttpAuthSchemes(GenerationContext context, Pytho
var supportedAuthSchemes = new LinkedHashMap<String, Symbol>();
var service = context.settings().service(context.model());
for (PythonIntegration integration : context.integrations()) {
for (RuntimeClientPlugin plugin : integration.getClientPlugins()) {
for (RuntimeClientPlugin plugin : integration.getClientPlugins(context)) {
if (plugin.matchesService(context.model(), service)
&& plugin.getAuthScheme().isPresent()
&& plugin.getAuthScheme().get().getApplicationProtocol().isHttpProtocol()) {
Expand Down Expand Up @@ -289,7 +289,7 @@ private void writeInterceptorsType(PythonWriter writer) {
}

private void generateConfig(GenerationContext context, PythonWriter writer) {
var symbol = CodegenUtils.getConfigSymbol(context.settings());
var configSymbol = CodegenUtils.getConfigSymbol(context.settings());

// Initialize the list of config properties with our base properties. Here a new
// list is constructed because that base list is immutable.
Expand All @@ -312,7 +312,7 @@ private void generateConfig(GenerationContext context, PythonWriter writer) {

// Add any relevant config properties from plugins.
for (PythonIntegration integration : context.integrations()) {
for (RuntimeClientPlugin plugin : integration.getClientPlugins()) {
for (RuntimeClientPlugin plugin : integration.getClientPlugins(context)) {
if (plugin.matchesService(model, service)) {
properties.addAll(plugin.getConfigProperties());
}
Expand All @@ -324,7 +324,7 @@ private void generateConfig(GenerationContext context, PythonWriter writer) {
writer.addStdlibImport("dataclasses", "dataclass");
writer.write("""
@dataclass(init=False)
class $L:
class $T:
Copy link
Contributor

Choose a reason for hiding this comment

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

T is intended for references, not declarations. Why was this change made?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Reverted to $L for the decleration - Had done it without thinking since its using the symbol as an argument.

\"""Configuration for $L.\"""

${C|}
Expand All @@ -340,7 +340,7 @@ def __init__(
\"""
${C|}
""",
symbol.getName(),
configSymbol,
context.settings().service().getName(),
writer.consumer(w -> writePropertyDeclarations(w, finalProperties)),
writer.consumer(w -> writeInitParams(w, finalProperties)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public final class HttpApiKeyAuth implements PythonIntegration {
private static final String OPTION_GENERATOR_NAME = "_generate_api_key_option";

@Override
public List<RuntimeClientPlugin> getClientPlugins() {
public List<RuntimeClientPlugin> getClientPlugins(GenerationContext context) {
return List.of(
RuntimeClientPlugin.builder()
.servicePredicate((model, service) -> service.hasTrait(HttpApiKeyAuthTrait.class))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ default List<ProtocolGenerator> getProtocolGenerators() {
*
* @return Returns the list of RuntimePlugins to apply to the client.
*/
default List<RuntimeClientPlugin> getClientPlugins() {
default List<RuntimeClientPlugin> getClientPlugins(GenerationContext context) {
return Collections.emptyList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import software.amazon.smithy.model.shapes.OperationShape;
import software.amazon.smithy.model.shapes.ServiceShape;
import software.amazon.smithy.python.codegen.ConfigProperty;
import software.amazon.smithy.python.codegen.GenerationContext;
import software.amazon.smithy.utils.SmithyBuilder;
import software.amazon.smithy.utils.SmithyUnstableApi;
import software.amazon.smithy.utils.ToSmithyBuilder;
Expand All @@ -38,6 +39,7 @@ public final class RuntimeClientPlugin implements ToSmithyBuilder<RuntimeClientP
private final OperationPredicate operationPredicate;
private final List<ConfigProperty> configProperties;
private final SymbolReference pythonPlugin;
private final WriteAdditionalFiles writeAdditionalFiles;

private final AuthScheme authScheme;

Expand All @@ -47,6 +49,7 @@ private RuntimeClientPlugin(Builder builder) {
configProperties = Collections.unmodifiableList(builder.configProperties);
this.pythonPlugin = builder.pythonPlugin;
this.authScheme = builder.authScheme;
this.writeAdditionalFiles = builder.writeAdditionalFiles;
}

/**
Expand All @@ -65,6 +68,20 @@ public interface OperationPredicate {
boolean test(Model model, ServiceShape service, OperationShape operation);
}

@FunctionalInterface
/**
* Called to write out additional files.
*/
public interface WriteAdditionalFiles {
/**
* Called to write out additional files needed by a generator.
*
* @param context GenerationContext - allows access to file manifest and symbol providers
* @return List of the relative paths of files written.
*/
List<String> writeAdditionalFiles(GenerationContext context);
}

/**
* Returns true if this plugin applies to the given service.
*
Expand Down Expand Up @@ -120,6 +137,16 @@ public Optional<AuthScheme> getAuthScheme() {
return Optional.ofNullable(authScheme);
}

/**
* Write additional files required by this plugin.
*
* @param context generation context
* @return relative paths of additional files written.
*/
public List<String> writeAdditionalFiles(GenerationContext context) {
return writeAdditionalFiles.writeAdditionalFiles(context);
}

/**
* @return Returns a new builder for a {@link RuntimeClientPlugin}.
*/
Expand All @@ -132,7 +159,8 @@ public SmithyBuilder<RuntimeClientPlugin> toBuilder() {
var builder = builder()
.pythonPlugin(pythonPlugin)
.authScheme(authScheme)
.configProperties(configProperties);
.configProperties(configProperties)
.writeAdditionalFiles(writeAdditionalFiles);

if (operationPredicate == OPERATION_ALWAYS_FALSE) {
builder.servicePredicate(servicePredicate);
Expand All @@ -152,6 +180,7 @@ public static final class Builder implements SmithyBuilder<RuntimeClientPlugin>
private List<ConfigProperty> configProperties = new ArrayList<>();
private SymbolReference pythonPlugin = null;
private AuthScheme authScheme = null;
private WriteAdditionalFiles writeAdditionalFiles = (context) -> Collections.emptyList();

Builder() {}

Expand Down Expand Up @@ -264,5 +293,16 @@ public Builder authScheme(AuthScheme authScheme) {
this.authScheme = authScheme;
return this;
}

/**
* Write additional files required by this plugin.
*
* @param writeAdditionalFiles additional files to write.
* @return Returns the builder.
*/
public Builder writeAdditionalFiles(WriteAdditionalFiles writeAdditionalFiles) {
this.writeAdditionalFiles = writeAdditionalFiles;
return this;
}
}
}
Loading
Loading