From cb47f65a56f588dfca5f7fe705395927cdcb655e Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Tue, 25 Feb 2025 10:48:22 -0800 Subject: [PATCH 01/20] WIP - add user agent interceptor/plugin --- .../aws/codegen/AwsPythonDependency.java | 26 +++++++++++ .../aws/codegen/AwsUserAgentIntegration.java | 45 +++++++++++++++++++ ...hon.codegen.integrations.PythonIntegration | 1 + .../smithy_aws_core/interceptors/__init__.py | 12 +++++ .../interceptors/user_agent.py | 24 ++++++++++ .../src/smithy_aws_core/plugins/__init__.py | 16 +++++++ 6 files changed, 124 insertions(+) create mode 100644 codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsPythonDependency.java create mode 100644 codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsUserAgentIntegration.java create mode 100644 packages/smithy-aws-core/src/smithy_aws_core/interceptors/__init__.py create mode 100644 packages/smithy-aws-core/src/smithy_aws_core/interceptors/user_agent.py create mode 100644 packages/smithy-aws-core/src/smithy_aws_core/plugins/__init__.py 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..b8d0dc2dc --- /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); +} diff --git a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsUserAgentIntegration.java b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsUserAgentIntegration.java new file mode 100644 index 000000000..87bb0a4e3 --- /dev/null +++ b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsUserAgentIntegration.java @@ -0,0 +1,45 @@ +/* + * 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.Collections; +import java.util.List; +import software.amazon.smithy.codegen.core.Symbol; +import software.amazon.smithy.codegen.core.SymbolReference; +import software.amazon.smithy.python.codegen.ConfigProperty; +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 { + @Override + public List getClientPlugins() { + return List.of( + RuntimeClientPlugin.builder() + .addConfigProperty(ConfigProperty.builder() + // TODO: This is the name used in boto, but potentially could be user_agent_prefix. Depends on backwards compat strategy. + .name("user_agent_extra") + .documentation("Additional suffix to be added to the user agent") + .type(Symbol.builder().name("str").build()) // TODO: Should common types like this be defined as constants somewhere? + .nullable(true) + .build()) + .pythonPlugin( + SymbolReference.builder() + .symbol(Symbol.builder() + .namespace(AwsPythonDependency.SMITHY_AWS_CORE.packageName() + ".plugins", ".") + .name("user_agent_plugin") + .addDependency(AwsPythonDependency.SMITHY_AWS_CORE) + .build()) + .build() + ) + .build() + ); + } + +} 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..0375294ba 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.AwsUserAgentIntegration diff --git a/packages/smithy-aws-core/src/smithy_aws_core/interceptors/__init__.py b/packages/smithy-aws-core/src/smithy_aws_core/interceptors/__init__.py new file mode 100644 index 000000000..654905217 --- /dev/null +++ b/packages/smithy-aws-core/src/smithy_aws_core/interceptors/__init__.py @@ -0,0 +1,12 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. diff --git a/packages/smithy-aws-core/src/smithy_aws_core/interceptors/user_agent.py b/packages/smithy-aws-core/src/smithy_aws_core/interceptors/user_agent.py new file mode 100644 index 000000000..1cfd48249 --- /dev/null +++ b/packages/smithy-aws-core/src/smithy_aws_core/interceptors/user_agent.py @@ -0,0 +1,24 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +from smithy_core.interceptors import Interceptor, InterceptorContext, Request, TransportRequest +from smithy_http.aio import HTTPRequest + + +class UserAgentInterceptor(Interceptor): + """Adds UserAgent header to the Request before signing. + """ + def modify_before_signing( + self, context: InterceptorContext[Request, None, HTTPRequest, None] + ) -> HTTPRequest: + print("Oh Hello here I am!") + return context.transport_request \ No newline at end of file diff --git a/packages/smithy-aws-core/src/smithy_aws_core/plugins/__init__.py b/packages/smithy-aws-core/src/smithy_aws_core/plugins/__init__.py new file mode 100644 index 000000000..2302e957f --- /dev/null +++ b/packages/smithy-aws-core/src/smithy_aws_core/plugins/__init__.py @@ -0,0 +1,16 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. + +# TODO: Define a Protocol for Config w/ interceptor method? +def user_agent_plugin(config: any) -> None: + config.interceptors.append() \ No newline at end of file From 91e82083c4811fc742101c7d0cdbd3ca5afdffe2 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Tue, 25 Feb 2025 14:39:16 -0800 Subject: [PATCH 02/20] Working implementation with basic user agent --- .../aws/codegen/AwsUserAgentIntegration.java | 13 +- .../interceptors/user_agent.py | 39 ++- .../src/smithy_aws_core/plugins/__init__.py | 13 +- .../src/smithy_aws_core/user_agent.py | 273 ++++++++++++++++++ 4 files changed, 328 insertions(+), 10 deletions(-) create mode 100644 packages/smithy-aws-core/src/smithy_aws_core/user_agent.py diff --git a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsUserAgentIntegration.java b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsUserAgentIntegration.java index 87bb0a4e3..4434f0c23 100644 --- a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsUserAgentIntegration.java +++ b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsUserAgentIntegration.java @@ -22,13 +22,22 @@ public class AwsUserAgentIntegration implements PythonIntegration { public List getClientPlugins() { return List.of( RuntimeClientPlugin.builder() - .addConfigProperty(ConfigProperty.builder() + .addConfigProperty( + ConfigProperty.builder() // TODO: This is the name used in boto, but potentially could be user_agent_prefix. Depends on backwards compat strategy. .name("user_agent_extra") - .documentation("Additional suffix to be added to the user agent") + .documentation("Additional suffix to be added to the User-Agent header.") .type(Symbol.builder().name("str").build()) // TODO: Should common types like this be defined as constants somewhere? .nullable(true) .build()) + .addConfigProperty( + 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() + ) .pythonPlugin( SymbolReference.builder() .symbol(Symbol.builder() diff --git a/packages/smithy-aws-core/src/smithy_aws_core/interceptors/user_agent.py b/packages/smithy-aws-core/src/smithy_aws_core/interceptors/user_agent.py index 1cfd48249..eeef90c45 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/interceptors/user_agent.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/interceptors/user_agent.py @@ -10,15 +10,42 @@ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. -from smithy_core.interceptors import Interceptor, InterceptorContext, Request, TransportRequest + +from smithy_aws_core.user_agent import UserAgent +from smithy_core.interceptors import Interceptor, InterceptorContext, Request +from smithy_http import Field from smithy_http.aio import HTTPRequest class UserAgentInterceptor(Interceptor): - """Adds UserAgent header to the Request before signing. - """ + """Adds UserAgent header to the Request before signing.""" + + def __init__( + self, + ua_suffix: str | None = None, + ua_app_id: str | None = None, + sdk_version: str | None = "0.0.1", + ) -> None: + """Initialize the UserAgentInterceptor. + + :ua_suffix: Additional suffix to be added to the UserAgent header. :ua_app_id: + User defined and opaque application ID to be added to the UserAgent header. + """ + super().__init__() + self._ua_suffix = ua_suffix + self._ua_app_id = ua_app_id + self._sdk_version = sdk_version + def modify_before_signing( - self, context: InterceptorContext[Request, None, HTTPRequest, None] + self, context: InterceptorContext[Request, None, HTTPRequest, None] ) -> HTTPRequest: - print("Oh Hello here I am!") - return context.transport_request \ No newline at end of file + user_agent = UserAgent.from_environment().with_config( + ua_suffix=self._ua_suffix, + ua_app_id=self._ua_app_id, + sdk_version=self._sdk_version, + ) + request = context.transport_request + request.fields.set_field( + Field(name="User-Agent", values=[user_agent.to_string()]) + ) + return context.transport_request diff --git a/packages/smithy-aws-core/src/smithy_aws_core/plugins/__init__.py b/packages/smithy-aws-core/src/smithy_aws_core/plugins/__init__.py index 2302e957f..583cede6f 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/plugins/__init__.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/plugins/__init__.py @@ -10,7 +10,16 @@ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. +from typing import Any + +from smithy_aws_core.interceptors.user_agent import UserAgentInterceptor + # TODO: Define a Protocol for Config w/ interceptor method? -def user_agent_plugin(config: any) -> None: - config.interceptors.append() \ No newline at end of file +def user_agent_plugin(config: Any) -> None: + config.interceptors.append( + UserAgentInterceptor( + ua_suffix=config.user_agent_extra, + ua_app_id=config.sdk_ua_app_id, + ) + ) diff --git a/packages/smithy-aws-core/src/smithy_aws_core/user_agent.py b/packages/smithy-aws-core/src/smithy_aws_core/user_agent.py new file mode 100644 index 000000000..1cc352f02 --- /dev/null +++ b/packages/smithy-aws-core/src/smithy_aws_core/user_agent.py @@ -0,0 +1,273 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. + +import os +import platform +from string import ascii_letters, digits +from typing import NamedTuple, Optional, Self + +from smithy_http.aio.crt import HAS_CRT + +_USERAGENT_ALLOWED_CHARACTERS = ascii_letters + digits + "!$%&'*+-.^_`|~" +_USERAGENT_ALLOWED_OS_NAMES = ( + "windows", + "linux", + "macos", + "android", + "ios", + "watchos", + "tvos", + "other", +) +_USERAGENT_PLATFORM_NAME_MAPPINGS = {"darwin": "macos"} +_USERAGENT_SDK_NAME = "aws-sdk-python" + + +class UserAgent: + def __init__( + self, + platform_name, + platform_version, + platform_machine, + python_version, + python_implementation, + execution_env, + crt_version=None, + ) -> None: + self._platform_name = platform_name + self._platform_version = platform_version + self._platform_machine = platform_machine + self._python_version = python_version + self._python_implementation = python_implementation + self._execution_env = execution_env + self._crt_version = crt_version + + # Components that can be added with ``set_config`` + self._user_agent_suffix = None + self._user_agent_app_id = None + self._sdk_version = None + + @classmethod + def from_environment(cls) -> Self: + crt_version = None + if HAS_CRT: + crt_version = _get_crt_version() or "Unknown" + return cls( + platform_name=platform.system(), + platform_version=platform.release(), + platform_machine=platform.machine(), + python_version=platform.python_version(), + python_implementation=platform.python_implementation(), + execution_env=os.environ.get("AWS_EXECUTION_ENV"), + crt_version=crt_version, + ) + + def with_config( + self, + ua_suffix: str | None = None, + ua_app_id: str | None = None, + sdk_version: str | None = None, + ) -> Self: + self._user_agent_suffix = ua_suffix + self._user_agent_app_id = ua_app_id + self._sdk_version = sdk_version + return self + + def to_string(self): + """Build User-Agent header string from the object's properties.""" + components = [ + *self._build_sdk_metadata(), + UserAgentComponent("ua", "2.0"), + *self._build_os_metadata(), + *self._build_architecture_metadata(), + *self._build_language_metadata(), + *self._build_execution_env_metadata(), + *self._build_feature_metadata(), + *self._build_app_id(), + *self._build_suffix(), + ] + + return " ".join([comp.to_string() for comp in components]) + + def _build_sdk_metadata(self): + """Build the SDK name and version component of the User-Agent header. + + Includes CRT version if available. + """ + sdk_md = [] + sdk_md.append(UserAgentComponent(_USERAGENT_SDK_NAME, self._sdk_version)) + + if self._crt_version is not None: + sdk_md.append(UserAgentComponent("md", "awscrt", self._crt_version)) + + return sdk_md + + def _build_os_metadata(self): + """Build the OS/platform components of the User-Agent header string. + + For recognized platform names that match or map to an entry in the list + of standardized OS names, a single component with prefix "os" is + returned. Otherwise, one component "os/other" is returned and a second + with prefix "md" and the raw platform name. + + String representations of example return values: + * ``os/macos#10.13.6`` + * ``os/linux`` + * ``os/other`` + * ``os/other md/foobar#1.2.3`` + """ + if self._platform_name is None: + return [UserAgentComponent("os", "other")] + + plt_name_lower = self._platform_name.lower() + if plt_name_lower in _USERAGENT_ALLOWED_OS_NAMES: + os_family = plt_name_lower + elif plt_name_lower in _USERAGENT_PLATFORM_NAME_MAPPINGS: + os_family = _USERAGENT_PLATFORM_NAME_MAPPINGS[plt_name_lower] + else: + os_family = None + + if os_family is not None: + return [UserAgentComponent("os", os_family, self._platform_version)] + else: + return [ + UserAgentComponent("os", "other"), + UserAgentComponent("md", self._platform_name, self._platform_version), + ] + + def _build_architecture_metadata(self): + """Build architecture component of the User-Agent header string. + + Returns the machine type with prefix "md" and name "arch", if one is available. + Common values include "x86_64", "arm64", "i386". + """ + if self._platform_machine: + return [UserAgentComponent("md", "arch", self._platform_machine.lower())] + return [] + + def _build_language_metadata(self): + """Build the language components of the User-Agent header string. + + Returns the Python version in a component with prefix "lang" and name + "python". The Python implementation (e.g. CPython, PyPy) is returned as + separate metadata component with prefix "md" and name "pyimpl". + + String representation of an example return value: + ``lang/python#3.10.4 md/pyimpl#CPython`` + """ + lang_md = [ + UserAgentComponent("lang", "python", self._python_version), + ] + if self._python_implementation: + lang_md.append( + UserAgentComponent("md", "pyimpl", self._python_implementation) + ) + return lang_md + + def _build_execution_env_metadata(self): + """Build the execution environment component of the User-Agent header. + + Returns a single component prefixed with "exec-env", usually sourced from the + environment variable AWS_EXECUTION_ENV. + """ + if self._execution_env: + return [UserAgentComponent("exec-env", self._execution_env)] + else: + return [] + + def _build_feature_metadata(self): + """Build the features components of the User-Agent header string. + + TODO: These should be sourced from property bag set on context. + """ + return [] + + def _build_app_id(self): + """Build app component of the User-Agent header string.""" + if self._user_agent_app_id: + return [UserAgentComponent("app", self._user_agent_app_id)] + else: + return [] + + def _build_suffix(self): + if self._user_agent_suffix: + return [RawStringUserAgentComponent(self._user_agent_suffix)] + else: + return [] + + +def sanitize_user_agent_string_component(raw_str, allow_hash): + """Replaces all not allowed characters in the string with a dash ("-"). + + Allowed characters are ASCII alphanumerics and ``!$%&'*+-.^_`|~``. If + ``allow_hash`` is ``True``, "#"``" is also allowed. + + :type raw_str: str + :param raw_str: The input string to be sanitized. + + :type allow_hash: bool + :param allow_hash: Whether "#" is considered an allowed character. + """ + return "".join( + c if c in _USERAGENT_ALLOWED_CHARACTERS or (allow_hash and c == "#") else "-" + for c in raw_str + ) + + +class UserAgentComponent(NamedTuple): + """Component of a User-Agent header string in the standard format. + + Each component consists of a prefix, a name, and a value. In the string + representation these are combined in the format ``prefix/name#value``. + + This class is considered private and is subject to abrupt breaking changes. + """ + + prefix: str + name: str + value: Optional[str] = None + + def to_string(self): + """Create string like 'prefix/name#value' from a UserAgentComponent.""" + clean_prefix = sanitize_user_agent_string_component( + self.prefix, allow_hash=True + ) + clean_name = sanitize_user_agent_string_component(self.name, allow_hash=False) + if self.value is None or self.value == "": + return f"{clean_prefix}/{clean_name}" + clean_value = sanitize_user_agent_string_component(self.value, allow_hash=True) + return f"{clean_prefix}/{clean_name}#{clean_value}" + + +class RawStringUserAgentComponent: + """UserAgentComponent interface wrapper around ``str``. + + Use for User-Agent header components that are not constructed from prefix+name+value + but instead are provided as strings. No sanitization is performed. + """ + + def __init__(self, value): + self._value = value + + def to_string(self): + return self._value + + +def _get_crt_version(): + """This function is considered private and is subject to abrupt breaking changes.""" + try: + import awscrt + + return awscrt.__version__ + except AttributeError: + return None From d3b1d159ec78580307106f7e9dccf6cbd2cf8316 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Tue, 25 Feb 2025 15:29:44 -0800 Subject: [PATCH 03/20] Fix pyright errors --- .../interceptors/user_agent.py | 2 +- .../src/smithy_aws_core/user_agent.py | 131 +++++++++--------- pyproject.toml | 1 + 3 files changed, 70 insertions(+), 64 deletions(-) diff --git a/packages/smithy-aws-core/src/smithy_aws_core/interceptors/user_agent.py b/packages/smithy-aws-core/src/smithy_aws_core/interceptors/user_agent.py index eeef90c45..3a3c196c8 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/interceptors/user_agent.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/interceptors/user_agent.py @@ -17,7 +17,7 @@ from smithy_http.aio import HTTPRequest -class UserAgentInterceptor(Interceptor): +class UserAgentInterceptor(Interceptor[Request, None, HTTPRequest, None]): """Adds UserAgent header to the Request before signing.""" def __init__( diff --git a/packages/smithy-aws-core/src/smithy_aws_core/user_agent.py b/packages/smithy-aws-core/src/smithy_aws_core/user_agent.py index 1cc352f02..24b0e26e9 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/user_agent.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/user_agent.py @@ -14,7 +14,7 @@ import os import platform from string import ascii_letters, digits -from typing import NamedTuple, Optional, Self +from typing import NamedTuple, Optional, Self, Union, List from smithy_http.aio.crt import HAS_CRT @@ -33,16 +33,58 @@ _USERAGENT_SDK_NAME = "aws-sdk-python" +class UserAgentComponent(NamedTuple): + """Component of a User-Agent header string in the standard format. + + Each component consists of a prefix, a name, and a value. In the string + representation these are combined in the format ``prefix/name#value``. + + This class is considered private and is subject to abrupt breaking changes. + """ + + prefix: str + name: str + value: Optional[str] = None + + def to_string(self): + """Create string like 'prefix/name#value' from a UserAgentComponent.""" + clean_prefix = sanitize_user_agent_string_component( + self.prefix, allow_hash=True + ) + clean_name = sanitize_user_agent_string_component(self.name, allow_hash=False) + if self.value is None or self.value == "": + return f"{clean_prefix}/{clean_name}" + clean_value = sanitize_user_agent_string_component(self.value, allow_hash=True) + return f"{clean_prefix}/{clean_name}#{clean_value}" + + +class RawStringUserAgentComponent: + """UserAgentComponent interface wrapper around ``str``. + + Use for User-Agent header components that are not constructed from prefix+name+value + but instead are provided as strings. No sanitization is performed. + """ + + def __init__(self, value: str): + self._value = value + + def to_string(self) -> str: + return self._value + + +_UAComponent = Union[UserAgentComponent, RawStringUserAgentComponent] + + class UserAgent: def __init__( self, - platform_name, - platform_version, - platform_machine, - python_version, - python_implementation, - execution_env, - crt_version=None, + platform_name: str | None, + platform_version: str | None, + platform_machine: str | None, + python_version: str | None, + python_implementation: str | None, + execution_env: str | None, + crt_version: str | None, ) -> None: self._platform_name = platform_name self._platform_version = platform_version @@ -74,16 +116,16 @@ def from_environment(cls) -> Self: def with_config( self, - ua_suffix: str | None = None, - ua_app_id: str | None = None, - sdk_version: str | None = None, + ua_suffix: str | None, + ua_app_id: str | None, + sdk_version: str | None, ) -> Self: self._user_agent_suffix = ua_suffix self._user_agent_app_id = ua_app_id self._sdk_version = sdk_version return self - def to_string(self): + def to_string(self) -> str: """Build User-Agent header string from the object's properties.""" components = [ *self._build_sdk_metadata(), @@ -99,20 +141,22 @@ def to_string(self): return " ".join([comp.to_string() for comp in components]) - def _build_sdk_metadata(self): + def _build_sdk_metadata(self) -> List[UserAgentComponent]: """Build the SDK name and version component of the User-Agent header. Includes CRT version if available. """ - sdk_md = [] - sdk_md.append(UserAgentComponent(_USERAGENT_SDK_NAME, self._sdk_version)) + sdk_version = self._sdk_version if self._sdk_version else "Unknown" + sdk_md: List[UserAgentComponent] = [ + UserAgentComponent(_USERAGENT_SDK_NAME, sdk_version) + ] if self._crt_version is not None: sdk_md.append(UserAgentComponent("md", "awscrt", self._crt_version)) return sdk_md - def _build_os_metadata(self): + def _build_os_metadata(self) -> List[UserAgentComponent]: """Build the OS/platform components of the User-Agent header string. For recognized platform names that match or map to an entry in the list @@ -145,7 +189,7 @@ def _build_os_metadata(self): UserAgentComponent("md", self._platform_name, self._platform_version), ] - def _build_architecture_metadata(self): + def _build_architecture_metadata(self) -> List[UserAgentComponent]: """Build architecture component of the User-Agent header string. Returns the machine type with prefix "md" and name "arch", if one is available. @@ -155,7 +199,7 @@ def _build_architecture_metadata(self): return [UserAgentComponent("md", "arch", self._platform_machine.lower())] return [] - def _build_language_metadata(self): + def _build_language_metadata(self) -> List[UserAgentComponent]: """Build the language components of the User-Agent header string. Returns the Python version in a component with prefix "lang" and name @@ -174,7 +218,7 @@ def _build_language_metadata(self): ) return lang_md - def _build_execution_env_metadata(self): + def _build_execution_env_metadata(self) -> List[UserAgentComponent]: """Build the execution environment component of the User-Agent header. Returns a single component prefixed with "exec-env", usually sourced from the @@ -185,28 +229,28 @@ def _build_execution_env_metadata(self): else: return [] - def _build_feature_metadata(self): + def _build_feature_metadata(self) -> List[UserAgentComponent]: """Build the features components of the User-Agent header string. TODO: These should be sourced from property bag set on context. """ return [] - def _build_app_id(self): + def _build_app_id(self) -> List[UserAgentComponent]: """Build app component of the User-Agent header string.""" if self._user_agent_app_id: return [UserAgentComponent("app", self._user_agent_app_id)] else: return [] - def _build_suffix(self): + def _build_suffix(self) -> List[_UAComponent]: if self._user_agent_suffix: return [RawStringUserAgentComponent(self._user_agent_suffix)] else: return [] -def sanitize_user_agent_string_component(raw_str, allow_hash): +def sanitize_user_agent_string_component(raw_str: str, allow_hash: bool = False) -> str: """Replaces all not allowed characters in the string with a dash ("-"). Allowed characters are ASCII alphanumerics and ``!$%&'*+-.^_`|~``. If @@ -224,46 +268,7 @@ def sanitize_user_agent_string_component(raw_str, allow_hash): ) -class UserAgentComponent(NamedTuple): - """Component of a User-Agent header string in the standard format. - - Each component consists of a prefix, a name, and a value. In the string - representation these are combined in the format ``prefix/name#value``. - - This class is considered private and is subject to abrupt breaking changes. - """ - - prefix: str - name: str - value: Optional[str] = None - - def to_string(self): - """Create string like 'prefix/name#value' from a UserAgentComponent.""" - clean_prefix = sanitize_user_agent_string_component( - self.prefix, allow_hash=True - ) - clean_name = sanitize_user_agent_string_component(self.name, allow_hash=False) - if self.value is None or self.value == "": - return f"{clean_prefix}/{clean_name}" - clean_value = sanitize_user_agent_string_component(self.value, allow_hash=True) - return f"{clean_prefix}/{clean_name}#{clean_value}" - - -class RawStringUserAgentComponent: - """UserAgentComponent interface wrapper around ``str``. - - Use for User-Agent header components that are not constructed from prefix+name+value - but instead are provided as strings. No sanitization is performed. - """ - - def __init__(self, value): - self._value = value - - def to_string(self): - return self._value - - -def _get_crt_version(): +def _get_crt_version() -> str | None: """This function is considered private and is subject to abrupt breaking changes.""" try: import awscrt diff --git a/pyproject.toml b/pyproject.toml index 6bdb5f1b1..3efbce818 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ aws_event_stream = { workspace = true } [tool.pyright] typeCheckingMode = "strict" +reportMissingTypeStubs = false # TODO: Remove once awscrt supplies stubs/types [tool.pytest.ini_options] asyncio_mode = "auto" # makes pytest run async tests without having to be marked with the @pytest.mark.asyncio decorator From 203193392b4f21891e6a6194f81c66ca7b0e0a54 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Tue, 25 Feb 2025 15:34:47 -0800 Subject: [PATCH 04/20] Use short form license --- CONTRIBUTING.md | 2 +- .../src/smithy_aws_core/interceptors/__init__.py | 14 ++------------ .../smithy_aws_core/interceptors/user_agent.py | 14 ++------------ .../src/smithy_aws_core/plugins/__init__.py | 15 +++------------ .../src/smithy_aws_core/user_agent.py | 15 +++------------ pyproject.toml | 1 - 6 files changed, 11 insertions(+), 50 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0bb6185d8..36eb193e4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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. diff --git a/packages/smithy-aws-core/src/smithy_aws_core/interceptors/__init__.py b/packages/smithy-aws-core/src/smithy_aws_core/interceptors/__init__.py index 654905217..33cbe867a 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/interceptors/__init__.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/interceptors/__init__.py @@ -1,12 +1,2 @@ -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). You -# may not use this file except in compliance with the License. A copy of -# the License is located at -# -# http://aws.amazon.com/apache2.0/ -# -# or in the "license" file accompanying this file. This file is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF -# ANY KIND, either express or implied. See the License for the specific -# language governing permissions and limitations under the License. +# 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/interceptors/user_agent.py b/packages/smithy-aws-core/src/smithy_aws_core/interceptors/user_agent.py index 3a3c196c8..b7826fc99 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/interceptors/user_agent.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/interceptors/user_agent.py @@ -1,15 +1,5 @@ -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). You -# may not use this file except in compliance with the License. A copy of -# the License is located at -# -# http://aws.amazon.com/apache2.0/ -# -# or in the "license" file accompanying this file. This file is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF -# ANY KIND, either express or implied. See the License for the specific -# language governing permissions and limitations under the License. +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 from smithy_aws_core.user_agent import UserAgent from smithy_core.interceptors import Interceptor, InterceptorContext, Request diff --git a/packages/smithy-aws-core/src/smithy_aws_core/plugins/__init__.py b/packages/smithy-aws-core/src/smithy_aws_core/plugins/__init__.py index 583cede6f..a0829f930 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/plugins/__init__.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/plugins/__init__.py @@ -1,15 +1,6 @@ -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). You -# may not use this file except in compliance with the License. A copy of -# the License is located at -# -# http://aws.amazon.com/apache2.0/ -# -# or in the "license" file accompanying this file. This file is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF -# ANY KIND, either express or implied. See the License for the specific -# language governing permissions and limitations under the License. +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + from typing import Any from smithy_aws_core.interceptors.user_agent import UserAgentInterceptor diff --git a/packages/smithy-aws-core/src/smithy_aws_core/user_agent.py b/packages/smithy-aws-core/src/smithy_aws_core/user_agent.py index 24b0e26e9..c15fc25ca 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/user_agent.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/user_agent.py @@ -1,15 +1,6 @@ -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). You -# may not use this file except in compliance with the License. A copy of -# the License is located at -# -# http://aws.amazon.com/apache2.0/ -# -# or in the "license" file accompanying this file. This file is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF -# ANY KIND, either express or implied. See the License for the specific -# language governing permissions and limitations under the License. +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +# pyright: reportMissingTypeStubs=false,reportUnknownMemberType=false import os import platform diff --git a/pyproject.toml b/pyproject.toml index 3efbce818..6bdb5f1b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,6 @@ aws_event_stream = { workspace = true } [tool.pyright] typeCheckingMode = "strict" -reportMissingTypeStubs = false # TODO: Remove once awscrt supplies stubs/types [tool.pytest.ini_options] asyncio_mode = "auto" # makes pytest run async tests without having to be marked with the @pytest.mark.asyncio decorator From d5473b56a47638ae421ddd413d9ad123843588f9 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Wed, 26 Feb 2025 09:04:06 -0800 Subject: [PATCH 05/20] Run pyupgrade --- .../src/smithy_aws_core/user_agent.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/smithy-aws-core/src/smithy_aws_core/user_agent.py b/packages/smithy-aws-core/src/smithy_aws_core/user_agent.py index c15fc25ca..b2603ce22 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/user_agent.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/user_agent.py @@ -35,7 +35,7 @@ class UserAgentComponent(NamedTuple): prefix: str name: str - value: Optional[str] = None + value: str | None = None def to_string(self): """Create string like 'prefix/name#value' from a UserAgentComponent.""" @@ -132,13 +132,13 @@ def to_string(self) -> str: return " ".join([comp.to_string() for comp in components]) - def _build_sdk_metadata(self) -> List[UserAgentComponent]: + def _build_sdk_metadata(self) -> list[UserAgentComponent]: """Build the SDK name and version component of the User-Agent header. Includes CRT version if available. """ sdk_version = self._sdk_version if self._sdk_version else "Unknown" - sdk_md: List[UserAgentComponent] = [ + sdk_md: list[UserAgentComponent] = [ UserAgentComponent(_USERAGENT_SDK_NAME, sdk_version) ] @@ -147,7 +147,7 @@ def _build_sdk_metadata(self) -> List[UserAgentComponent]: return sdk_md - def _build_os_metadata(self) -> List[UserAgentComponent]: + def _build_os_metadata(self) -> list[UserAgentComponent]: """Build the OS/platform components of the User-Agent header string. For recognized platform names that match or map to an entry in the list @@ -180,7 +180,7 @@ def _build_os_metadata(self) -> List[UserAgentComponent]: UserAgentComponent("md", self._platform_name, self._platform_version), ] - def _build_architecture_metadata(self) -> List[UserAgentComponent]: + def _build_architecture_metadata(self) -> list[UserAgentComponent]: """Build architecture component of the User-Agent header string. Returns the machine type with prefix "md" and name "arch", if one is available. @@ -190,7 +190,7 @@ def _build_architecture_metadata(self) -> List[UserAgentComponent]: return [UserAgentComponent("md", "arch", self._platform_machine.lower())] return [] - def _build_language_metadata(self) -> List[UserAgentComponent]: + def _build_language_metadata(self) -> list[UserAgentComponent]: """Build the language components of the User-Agent header string. Returns the Python version in a component with prefix "lang" and name @@ -209,7 +209,7 @@ def _build_language_metadata(self) -> List[UserAgentComponent]: ) return lang_md - def _build_execution_env_metadata(self) -> List[UserAgentComponent]: + def _build_execution_env_metadata(self) -> list[UserAgentComponent]: """Build the execution environment component of the User-Agent header. Returns a single component prefixed with "exec-env", usually sourced from the @@ -220,21 +220,21 @@ def _build_execution_env_metadata(self) -> List[UserAgentComponent]: else: return [] - def _build_feature_metadata(self) -> List[UserAgentComponent]: + def _build_feature_metadata(self) -> list[UserAgentComponent]: """Build the features components of the User-Agent header string. TODO: These should be sourced from property bag set on context. """ return [] - def _build_app_id(self) -> List[UserAgentComponent]: + def _build_app_id(self) -> list[UserAgentComponent]: """Build app component of the User-Agent header string.""" if self._user_agent_app_id: return [UserAgentComponent("app", self._user_agent_app_id)] else: return [] - def _build_suffix(self) -> List[_UAComponent]: + def _build_suffix(self) -> list[_UAComponent]: if self._user_agent_suffix: return [RawStringUserAgentComponent(self._user_agent_suffix)] else: From 83a0a8ff8e1ccedb40442b2e085f6f646ae56fc1 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Wed, 26 Feb 2025 09:36:30 -0800 Subject: [PATCH 06/20] Cleanups from PR (still todo: seperate into generic user agent and aws user agent). --- .../aws/codegen/AwsUserAgentIntegration.java | 12 ++++------ .../interceptors/user_agent.py | 7 ++++-- .../src/smithy_aws_core/user_agent.py | 23 +++++++++---------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsUserAgentIntegration.java b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsUserAgentIntegration.java index 4434f0c23..dd52540d6 100644 --- a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsUserAgentIntegration.java +++ b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsUserAgentIntegration.java @@ -4,7 +4,6 @@ */ package software.amazon.smithy.python.aws.codegen; -import java.util.Collections; import java.util.List; import software.amazon.smithy.codegen.core.Symbol; import software.amazon.smithy.codegen.core.SymbolReference; @@ -24,12 +23,11 @@ public List getClientPlugins() { RuntimeClientPlugin.builder() .addConfigProperty( ConfigProperty.builder() - // TODO: This is the name used in boto, but potentially could be user_agent_prefix. Depends on backwards compat strategy. - .name("user_agent_extra") - .documentation("Additional suffix to be added to the User-Agent header.") - .type(Symbol.builder().name("str").build()) // TODO: Should common types like this be defined as constants somewhere? - .nullable(true) - .build()) + .name("user_agent_extra") + .documentation("Additional suffix to be added to the User-Agent header.") + .type(Symbol.builder().name("str").build()) + .nullable(true) + .build()) .addConfigProperty( ConfigProperty.builder() .name("sdk_ua_app_id") diff --git a/packages/smithy-aws-core/src/smithy_aws_core/interceptors/user_agent.py b/packages/smithy-aws-core/src/smithy_aws_core/interceptors/user_agent.py index b7826fc99..f6a2a76d7 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/interceptors/user_agent.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/interceptors/user_agent.py @@ -12,14 +12,17 @@ class UserAgentInterceptor(Interceptor[Request, None, HTTPRequest, None]): def __init__( self, + *, ua_suffix: str | None = None, ua_app_id: str | None = None, sdk_version: str | None = "0.0.1", ) -> None: """Initialize the UserAgentInterceptor. - :ua_suffix: Additional suffix to be added to the UserAgent header. :ua_app_id: - User defined and opaque application ID to be added to the UserAgent header. + :param ua_suffix: Additional suffix to be added to the UserAgent header. + :param ua_app_id: User defined and opaque application ID to be added to the + UserAgent header. + :param sdk_version: SDK version to be added to the UserAgent header. """ super().__init__() self._ua_suffix = ua_suffix diff --git a/packages/smithy-aws-core/src/smithy_aws_core/user_agent.py b/packages/smithy-aws-core/src/smithy_aws_core/user_agent.py index b2603ce22..138afa47b 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/user_agent.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/user_agent.py @@ -4,8 +4,9 @@ import os import platform +from dataclasses import dataclass from string import ascii_letters, digits -from typing import NamedTuple, Optional, Self, Union, List +from typing import Self from smithy_http.aio.crt import HAS_CRT @@ -24,7 +25,8 @@ _USERAGENT_SDK_NAME = "aws-sdk-python" -class UserAgentComponent(NamedTuple): +@dataclass(frozen=True, slots=True) +class UserAgentComponent: """Component of a User-Agent header string in the standard format. Each component consists of a prefix, a name, and a value. In the string @@ -37,7 +39,7 @@ class UserAgentComponent(NamedTuple): name: str value: str | None = None - def to_string(self): + def __str__(self): """Create string like 'prefix/name#value' from a UserAgentComponent.""" clean_prefix = sanitize_user_agent_string_component( self.prefix, allow_hash=True @@ -59,11 +61,11 @@ class RawStringUserAgentComponent: def __init__(self, value: str): self._value = value - def to_string(self) -> str: + def __str__(self) -> str: return self._value -_UAComponent = Union[UserAgentComponent, RawStringUserAgentComponent] +_UAComponent = UserAgentComponent | RawStringUserAgentComponent class UserAgent: @@ -92,9 +94,6 @@ def __init__( @classmethod def from_environment(cls) -> Self: - crt_version = None - if HAS_CRT: - crt_version = _get_crt_version() or "Unknown" return cls( platform_name=platform.system(), platform_version=platform.release(), @@ -102,7 +101,7 @@ def from_environment(cls) -> Self: python_version=platform.python_version(), python_implementation=platform.python_implementation(), execution_env=os.environ.get("AWS_EXECUTION_ENV"), - crt_version=crt_version, + crt_version=_get_crt_version(), ) def with_config( @@ -130,7 +129,7 @@ def to_string(self) -> str: *self._build_suffix(), ] - return " ".join([comp.to_string() for comp in components]) + return " ".join([str(comp) for comp in components]) def _build_sdk_metadata(self) -> list[UserAgentComponent]: """Build the SDK name and version component of the User-Agent header. @@ -261,9 +260,9 @@ def sanitize_user_agent_string_component(raw_str: str, allow_hash: bool = False) def _get_crt_version() -> str | None: """This function is considered private and is subject to abrupt breaking changes.""" - try: + if HAS_CRT: import awscrt return awscrt.__version__ - except AttributeError: + else: return None From b7e15d1e5215a2b36e6fc093b2458d86d6c990a0 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Wed, 26 Feb 2025 12:55:48 -0800 Subject: [PATCH 07/20] Add Config/HttpConfig Protocols + generate __version__ --- .../aws/codegen/AwsUserAgentIntegration.java | 16 ++++++++-------- .../python/codegen/DirectedPythonCodegen.java | 15 +++++++++++++++ .../codegen/generators/ConfigGenerator.java | 19 ++++++++++++++++--- .../src/smithy_aws_core/user_agent.py | 7 ++++--- .../src/smithy_core/interfaces/config.py | 11 +++++++++++ .../src/smithy_http/interfaces/config.py | 15 +++++++++++++++ 6 files changed, 69 insertions(+), 14 deletions(-) create mode 100644 packages/smithy-core/src/smithy_core/interfaces/config.py create mode 100644 packages/smithy-http/src/smithy_http/interfaces/config.py diff --git a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsUserAgentIntegration.java b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsUserAgentIntegration.java index dd52540d6..9648f0dac 100644 --- a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsUserAgentIntegration.java +++ b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsUserAgentIntegration.java @@ -31,22 +31,22 @@ public List getClientPlugins() { .addConfigProperty( ConfigProperty.builder() .name("sdk_ua_app_id") - .documentation("A unique and opaque application ID that is appended to the User-Agent header.") + .documentation( + "A unique and opaque application ID that is appended to the User-Agent header.") .type(Symbol.builder().name("str").build()) .nullable(true) - .build() - ) + .build()) .pythonPlugin( SymbolReference.builder() .symbol(Symbol.builder() - .namespace(AwsPythonDependency.SMITHY_AWS_CORE.packageName() + ".plugins", ".") + .namespace( + AwsPythonDependency.SMITHY_AWS_CORE.packageName() + ".plugins", + ".") .name("user_agent_plugin") .addDependency(AwsPythonDependency.SMITHY_AWS_CORE) .build()) - .build() - ) - .build() - ); + .build()) + .build()); } } 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..ac726a4f7 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 @@ -273,9 +273,24 @@ public void generateIntEnumShape(GenerateIntEnumDirective directive) { + generateServiceModuleInit(directive); generateInits(directive); } + /** + * Creates top level __init__.py file. + */ + private void generateServiceModuleInit(CustomizeDirective directive) { + directive.context() + .writerDelegator() + .useFileWriter( + "%s/__init__.py".formatted(directive.context().settings().moduleName()), + writer -> { + writer + .write("__version__ = '$L'", directive.context().settings().moduleVersion()); + }); + } + /** * Creates __init__.py files where not already present. */ 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..7e6677f27 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 @@ -289,7 +289,19 @@ private void writeInterceptorsType(PythonWriter writer) { } private void generateConfig(GenerationContext context, PythonWriter writer) { - var symbol = CodegenUtils.getConfigSymbol(context.settings()); + var configSymbol = CodegenUtils.getConfigSymbol(context.settings()); + Symbol configProtocolSymbol = null; + if (context.applicationProtocol().isHttpProtocol()) { + configProtocolSymbol = Symbol.builder() + .name("HttpConfig") + .namespace("smithy_http.interfaces.config", ".") + .build(); + } else { + configProtocolSymbol = Symbol.builder() + .name("Config") + .namespace("smithy_core.interfaces.config", ".") + .build(); + } // Initialize the list of config properties with our base properties. Here a new // list is constructed because that base list is immutable. @@ -324,7 +336,7 @@ private void generateConfig(GenerationContext context, PythonWriter writer) { writer.addStdlibImport("dataclasses", "dataclass"); writer.write(""" @dataclass(init=False) - class $L: + class $T($T): \"""Configuration for $L.\""" ${C|} @@ -340,7 +352,8 @@ def __init__( \""" ${C|} """, - symbol.getName(), + configSymbol, + configProtocolSymbol, context.settings().service().getName(), writer.consumer(w -> writePropertyDeclarations(w, finalProperties)), writer.consumer(w -> writeInitParams(w, finalProperties)), diff --git a/packages/smithy-aws-core/src/smithy_aws_core/user_agent.py b/packages/smithy-aws-core/src/smithy_aws_core/user_agent.py index 138afa47b..fb2d9b1eb 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/user_agent.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/user_agent.py @@ -51,6 +51,7 @@ def __str__(self): return f"{clean_prefix}/{clean_name}#{clean_value}" +@dataclass(frozen=True, slots=True) class RawStringUserAgentComponent: """UserAgentComponent interface wrapper around ``str``. @@ -58,11 +59,10 @@ class RawStringUserAgentComponent: but instead are provided as strings. No sanitization is performed. """ - def __init__(self, value: str): - self._value = value + value: str def __str__(self) -> str: - return self._value + return self.value _UAComponent = UserAgentComponent | RawStringUserAgentComponent @@ -71,6 +71,7 @@ def __str__(self) -> str: class UserAgent: def __init__( self, + *, platform_name: str | None, platform_version: str | None, platform_machine: str | None, diff --git a/packages/smithy-core/src/smithy_core/interfaces/config.py b/packages/smithy-core/src/smithy_core/interfaces/config.py new file mode 100644 index 000000000..b246668a3 --- /dev/null +++ b/packages/smithy-core/src/smithy_core/interfaces/config.py @@ -0,0 +1,11 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +from typing import Protocol, Any + +from smithy_core.interceptors import Interceptor +from smithy_core.interfaces.retries import RetryStrategy + + +class Config(Protocol): + interceptors: list[Interceptor[Any, Any, Any, Any]] + retry_strategy: RetryStrategy diff --git a/packages/smithy-http/src/smithy_http/interfaces/config.py b/packages/smithy-http/src/smithy_http/interfaces/config.py new file mode 100644 index 000000000..e3a2a302e --- /dev/null +++ b/packages/smithy-http/src/smithy_http/interfaces/config.py @@ -0,0 +1,15 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +from typing import Any + +from smithy_core.interfaces import URI +from smithy_core.interfaces.config import Config +from smithy_http.aio.interfaces import HTTPClient, EndpointResolver +from smithy_http.interfaces import HTTPRequestConfiguration + + +class HttpConfig(Config): + http_client: HTTPClient + http_request_config: HTTPRequestConfiguration | None + endpoint_resolver: EndpointResolver[Any] + endpoint_uri: str | URI | None From 8afb134d3e42cb1317131e85e977f1a939330949 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Wed, 26 Feb 2025 13:18:47 -0800 Subject: [PATCH 08/20] Add __version__ to smithy and aws core --- .../src/smithy_aws_core/__init__.py | 16 ++++------------ packages/smithy-core/src/smithy_core/__init__.py | 2 ++ 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/packages/smithy-aws-core/src/smithy_aws_core/__init__.py b/packages/smithy-aws-core/src/smithy_aws_core/__init__.py index 654905217..ce8f9b1b1 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/__init__.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/__init__.py @@ -1,12 +1,4 @@ -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). You -# may not use this file except in compliance with the License. A copy of -# the License is located at -# -# http://aws.amazon.com/apache2.0/ -# -# or in the "license" file accompanying this file. This file is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF -# ANY KIND, either express or implied. See the License for the specific -# language governing permissions and limitations under the License. +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +__version__ = "0.1.0" diff --git a/packages/smithy-core/src/smithy_core/__init__.py b/packages/smithy-core/src/smithy_core/__init__.py index 310a1e210..05a322331 100644 --- a/packages/smithy-core/src/smithy_core/__init__.py +++ b/packages/smithy-core/src/smithy_core/__init__.py @@ -8,6 +8,8 @@ from . import interfaces, rfc3986 from .exceptions import SmithyException +__version__ = "0.1.0" + class HostType(Enum): """Enumeration of possible host types.""" From a7475d74331708926df734fe36bbb9e3981ecee3 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Wed, 26 Feb 2025 16:04:21 -0800 Subject: [PATCH 09/20] WIP - generic useragent refactoring. --- .../aws/codegen/AwsUserAgentIntegration.java | 64 +++-- .../python/codegen/ClientGenerator.java | 6 +- .../python/codegen/HttpAuthGenerator.java | 2 +- .../codegen/generators/ConfigGenerator.java | 4 +- .../codegen/integrations/HttpApiKeyAuth.java | 2 +- .../integrations/PythonIntegration.java | 2 +- .../integrations/UserAgentIntegration.java | 40 +++ ...hon.codegen.integrations.PythonIntegration | 1 + .../interceptors/user_agent.py | 42 +-- .../src/smithy_aws_core/plugins/__init__.py | 11 +- .../src/smithy_aws_core/user_agent.py | 269 ------------------ .../smithy-http/src/smithy_http/__init__.py | 1 - .../src/smithy_http/interceptors/__init__.py | 2 + .../smithy_http/interceptors/user_agent.py | 141 +++++++++ .../src/smithy_http/plugins/__init__.py | 8 + .../smithy-http/src/smithy_http/user_agent.py | 95 +++++++ 16 files changed, 335 insertions(+), 355 deletions(-) create mode 100644 codegen/core/src/main/java/software/amazon/smithy/python/codegen/integrations/UserAgentIntegration.java delete mode 100644 packages/smithy-aws-core/src/smithy_aws_core/user_agent.py create mode 100644 packages/smithy-http/src/smithy_http/interceptors/__init__.py create mode 100644 packages/smithy-http/src/smithy_http/interceptors/user_agent.py create mode 100644 packages/smithy-http/src/smithy_http/plugins/__init__.py create mode 100644 packages/smithy-http/src/smithy_http/user_agent.py diff --git a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsUserAgentIntegration.java b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsUserAgentIntegration.java index 9648f0dac..113275044 100644 --- a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsUserAgentIntegration.java +++ b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsUserAgentIntegration.java @@ -8,6 +8,7 @@ import software.amazon.smithy.codegen.core.Symbol; import software.amazon.smithy.codegen.core.SymbolReference; 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; @@ -18,35 +19,40 @@ @SmithyInternalApi public class AwsUserAgentIntegration implements PythonIntegration { @Override - public List getClientPlugins() { - return List.of( - RuntimeClientPlugin.builder() - .addConfigProperty( - 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()) - .addConfigProperty( - 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()) - .pythonPlugin( - SymbolReference.builder() - .symbol(Symbol.builder() - .namespace( - AwsPythonDependency.SMITHY_AWS_CORE.packageName() + ".plugins", - ".") - .name("user_agent_plugin") - .addDependency(AwsPythonDependency.SMITHY_AWS_CORE) - .build()) - .build()) - .build()); + public List getClientPlugins(GenerationContext context) { + if (context.applicationProtocol().isHttpProtocol()) { + return List.of( + RuntimeClientPlugin.builder() + .addConfigProperty( + 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()) + .addConfigProperty( + 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()) + .pythonPlugin( + SymbolReference.builder() + .symbol(Symbol.builder() + .namespace( + AwsPythonDependency.SMITHY_AWS_CORE.packageName() + + ".plugins", + ".") + .name("aws_user_agent_plugin") + .addDependency(AwsPythonDependency.SMITHY_AWS_CORE) + .build()) + .build()) + .build()); + } else { + return List.of(); + } } } 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 e24305226..4b01fc5a8 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 @@ -76,7 +76,7 @@ private void generateService(PythonWriter writer) { var defaultPlugins = new LinkedHashSet(); 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); } @@ -657,7 +657,7 @@ private boolean hasEventStream() { private void initializeHttpAuthParameters(PythonWriter writer) { var derived = new LinkedHashSet(); 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()) { @@ -746,7 +746,7 @@ private void writeSharedOperationInit(PythonWriter writer, OperationShape operat var defaultPlugins = new LinkedHashSet(); 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); } diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/HttpAuthGenerator.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/HttpAuthGenerator.java index 5500d0849..843196d85 100644 --- a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/HttpAuthGenerator.java +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/HttpAuthGenerator.java @@ -43,7 +43,7 @@ public void run() { var properties = new ArrayList(); 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()) { 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 7e6677f27..bbf7a49b5 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 @@ -219,7 +219,7 @@ private static void writeDefaultHttpAuthSchemes(GenerationContext context, Pytho var supportedAuthSchemes = new LinkedHashMap(); 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()) { @@ -324,7 +324,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()); } diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/integrations/HttpApiKeyAuth.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/integrations/HttpApiKeyAuth.java index f8034d0ab..f7b292d85 100644 --- a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/integrations/HttpApiKeyAuth.java +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/integrations/HttpApiKeyAuth.java @@ -24,7 +24,7 @@ public final class HttpApiKeyAuth implements PythonIntegration { private static final String OPTION_GENERATOR_NAME = "_generate_api_key_option"; @Override - public List getClientPlugins() { + public List getClientPlugins(GenerationContext context) { return List.of( RuntimeClientPlugin.builder() .servicePredicate((model, service) -> service.hasTrait(HttpApiKeyAuthTrait.class)) 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..6a6f37338 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 @@ -35,7 +35,7 @@ default List getProtocolGenerators() { * * @return Returns the list of RuntimePlugins to apply to the client. */ - default List getClientPlugins() { + default List getClientPlugins(GenerationContext context) { return Collections.emptyList(); } } diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/integrations/UserAgentIntegration.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/integrations/UserAgentIntegration.java new file mode 100644 index 000000000..f61947548 --- /dev/null +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/integrations/UserAgentIntegration.java @@ -0,0 +1,40 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.python.codegen.integrations; + +import java.util.List; +import software.amazon.smithy.codegen.core.Symbol; +import software.amazon.smithy.codegen.core.SymbolReference; +import software.amazon.smithy.python.codegen.GenerationContext; +import software.amazon.smithy.python.codegen.SmithyPythonDependency; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Adds a runtime plugin to set generic user agent. + */ +@SmithyInternalApi +public class UserAgentIntegration implements PythonIntegration { + @Override + public List getClientPlugins(GenerationContext context) { + if (context.applicationProtocol().isHttpProtocol()) { + return List.of( + RuntimeClientPlugin.builder() + .pythonPlugin( + SymbolReference.builder() + .symbol(Symbol.builder() + .namespace( + SmithyPythonDependency.SMITHY_HTTP.packageName() + + ".plugins", + ".") + .name("user_agent_plugin") + .addDependency(SmithyPythonDependency.SMITHY_HTTP) + .build()) + .build()) + .build()); + } else { + return List.of(); + } + } +} diff --git a/codegen/core/src/main/resources/META-INF/services/software.amazon.smithy.python.codegen.integrations.PythonIntegration b/codegen/core/src/main/resources/META-INF/services/software.amazon.smithy.python.codegen.integrations.PythonIntegration index e8dc8f92c..154d4fc1f 100644 --- a/codegen/core/src/main/resources/META-INF/services/software.amazon.smithy.python.codegen.integrations.PythonIntegration +++ b/codegen/core/src/main/resources/META-INF/services/software.amazon.smithy.python.codegen.integrations.PythonIntegration @@ -5,3 +5,4 @@ software.amazon.smithy.python.codegen.integrations.RestJsonIntegration software.amazon.smithy.python.codegen.integrations.HttpApiKeyAuth +software.amazon.smithy.python.codegen.integrations.UserAgentIntegration diff --git a/packages/smithy-aws-core/src/smithy_aws_core/interceptors/user_agent.py b/packages/smithy-aws-core/src/smithy_aws_core/interceptors/user_agent.py index f6a2a76d7..8b59e4208 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/interceptors/user_agent.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/interceptors/user_agent.py @@ -1,44 +1,8 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 - -from smithy_aws_core.user_agent import UserAgent -from smithy_core.interceptors import Interceptor, InterceptorContext, Request -from smithy_http import Field -from smithy_http.aio import HTTPRequest +from smithy_core.interceptors import Interceptor, Request +from smithy_http.aio.interfaces import HTTPRequest class UserAgentInterceptor(Interceptor[Request, None, HTTPRequest, None]): - """Adds UserAgent header to the Request before signing.""" - - def __init__( - self, - *, - ua_suffix: str | None = None, - ua_app_id: str | None = None, - sdk_version: str | None = "0.0.1", - ) -> None: - """Initialize the UserAgentInterceptor. - - :param ua_suffix: Additional suffix to be added to the UserAgent header. - :param ua_app_id: User defined and opaque application ID to be added to the - UserAgent header. - :param sdk_version: SDK version to be added to the UserAgent header. - """ - super().__init__() - self._ua_suffix = ua_suffix - self._ua_app_id = ua_app_id - self._sdk_version = sdk_version - - def modify_before_signing( - self, context: InterceptorContext[Request, None, HTTPRequest, None] - ) -> HTTPRequest: - user_agent = UserAgent.from_environment().with_config( - ua_suffix=self._ua_suffix, - ua_app_id=self._ua_app_id, - sdk_version=self._sdk_version, - ) - request = context.transport_request - request.fields.set_field( - Field(name="User-Agent", values=[user_agent.to_string()]) - ) - return context.transport_request + """Adds UserAgent header to the Request before signing.""" \ No newline at end of file diff --git a/packages/smithy-aws-core/src/smithy_aws_core/plugins/__init__.py b/packages/smithy-aws-core/src/smithy_aws_core/plugins/__init__.py index a0829f930..ade462782 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/plugins/__init__.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/plugins/__init__.py @@ -5,12 +5,5 @@ from smithy_aws_core.interceptors.user_agent import UserAgentInterceptor - -# TODO: Define a Protocol for Config w/ interceptor method? -def user_agent_plugin(config: Any) -> None: - config.interceptors.append( - UserAgentInterceptor( - ua_suffix=config.user_agent_extra, - ua_app_id=config.sdk_ua_app_id, - ) - ) +def aws_user_agent_plugin(config: Any) -> None: + config.interceptors.append(UserAgentInterceptor()) diff --git a/packages/smithy-aws-core/src/smithy_aws_core/user_agent.py b/packages/smithy-aws-core/src/smithy_aws_core/user_agent.py deleted file mode 100644 index fb2d9b1eb..000000000 --- a/packages/smithy-aws-core/src/smithy_aws_core/user_agent.py +++ /dev/null @@ -1,269 +0,0 @@ -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: Apache-2.0 -# pyright: reportMissingTypeStubs=false,reportUnknownMemberType=false - -import os -import platform -from dataclasses import dataclass -from string import ascii_letters, digits -from typing import Self - -from smithy_http.aio.crt import HAS_CRT - -_USERAGENT_ALLOWED_CHARACTERS = ascii_letters + digits + "!$%&'*+-.^_`|~" -_USERAGENT_ALLOWED_OS_NAMES = ( - "windows", - "linux", - "macos", - "android", - "ios", - "watchos", - "tvos", - "other", -) -_USERAGENT_PLATFORM_NAME_MAPPINGS = {"darwin": "macos"} -_USERAGENT_SDK_NAME = "aws-sdk-python" - - -@dataclass(frozen=True, slots=True) -class UserAgentComponent: - """Component of a User-Agent header string in the standard format. - - Each component consists of a prefix, a name, and a value. In the string - representation these are combined in the format ``prefix/name#value``. - - This class is considered private and is subject to abrupt breaking changes. - """ - - prefix: str - name: str - value: str | None = None - - def __str__(self): - """Create string like 'prefix/name#value' from a UserAgentComponent.""" - clean_prefix = sanitize_user_agent_string_component( - self.prefix, allow_hash=True - ) - clean_name = sanitize_user_agent_string_component(self.name, allow_hash=False) - if self.value is None or self.value == "": - return f"{clean_prefix}/{clean_name}" - clean_value = sanitize_user_agent_string_component(self.value, allow_hash=True) - return f"{clean_prefix}/{clean_name}#{clean_value}" - - -@dataclass(frozen=True, slots=True) -class RawStringUserAgentComponent: - """UserAgentComponent interface wrapper around ``str``. - - Use for User-Agent header components that are not constructed from prefix+name+value - but instead are provided as strings. No sanitization is performed. - """ - - value: str - - def __str__(self) -> str: - return self.value - - -_UAComponent = UserAgentComponent | RawStringUserAgentComponent - - -class UserAgent: - def __init__( - self, - *, - platform_name: str | None, - platform_version: str | None, - platform_machine: str | None, - python_version: str | None, - python_implementation: str | None, - execution_env: str | None, - crt_version: str | None, - ) -> None: - self._platform_name = platform_name - self._platform_version = platform_version - self._platform_machine = platform_machine - self._python_version = python_version - self._python_implementation = python_implementation - self._execution_env = execution_env - self._crt_version = crt_version - - # Components that can be added with ``set_config`` - self._user_agent_suffix = None - self._user_agent_app_id = None - self._sdk_version = None - - @classmethod - def from_environment(cls) -> Self: - return cls( - platform_name=platform.system(), - platform_version=platform.release(), - platform_machine=platform.machine(), - python_version=platform.python_version(), - python_implementation=platform.python_implementation(), - execution_env=os.environ.get("AWS_EXECUTION_ENV"), - crt_version=_get_crt_version(), - ) - - def with_config( - self, - ua_suffix: str | None, - ua_app_id: str | None, - sdk_version: str | None, - ) -> Self: - self._user_agent_suffix = ua_suffix - self._user_agent_app_id = ua_app_id - self._sdk_version = sdk_version - return self - - def to_string(self) -> str: - """Build User-Agent header string from the object's properties.""" - components = [ - *self._build_sdk_metadata(), - UserAgentComponent("ua", "2.0"), - *self._build_os_metadata(), - *self._build_architecture_metadata(), - *self._build_language_metadata(), - *self._build_execution_env_metadata(), - *self._build_feature_metadata(), - *self._build_app_id(), - *self._build_suffix(), - ] - - return " ".join([str(comp) for comp in components]) - - def _build_sdk_metadata(self) -> list[UserAgentComponent]: - """Build the SDK name and version component of the User-Agent header. - - Includes CRT version if available. - """ - sdk_version = self._sdk_version if self._sdk_version else "Unknown" - sdk_md: list[UserAgentComponent] = [ - UserAgentComponent(_USERAGENT_SDK_NAME, sdk_version) - ] - - if self._crt_version is not None: - sdk_md.append(UserAgentComponent("md", "awscrt", self._crt_version)) - - return sdk_md - - def _build_os_metadata(self) -> list[UserAgentComponent]: - """Build the OS/platform components of the User-Agent header string. - - For recognized platform names that match or map to an entry in the list - of standardized OS names, a single component with prefix "os" is - returned. Otherwise, one component "os/other" is returned and a second - with prefix "md" and the raw platform name. - - String representations of example return values: - * ``os/macos#10.13.6`` - * ``os/linux`` - * ``os/other`` - * ``os/other md/foobar#1.2.3`` - """ - if self._platform_name is None: - return [UserAgentComponent("os", "other")] - - plt_name_lower = self._platform_name.lower() - if plt_name_lower in _USERAGENT_ALLOWED_OS_NAMES: - os_family = plt_name_lower - elif plt_name_lower in _USERAGENT_PLATFORM_NAME_MAPPINGS: - os_family = _USERAGENT_PLATFORM_NAME_MAPPINGS[plt_name_lower] - else: - os_family = None - - if os_family is not None: - return [UserAgentComponent("os", os_family, self._platform_version)] - else: - return [ - UserAgentComponent("os", "other"), - UserAgentComponent("md", self._platform_name, self._platform_version), - ] - - def _build_architecture_metadata(self) -> list[UserAgentComponent]: - """Build architecture component of the User-Agent header string. - - Returns the machine type with prefix "md" and name "arch", if one is available. - Common values include "x86_64", "arm64", "i386". - """ - if self._platform_machine: - return [UserAgentComponent("md", "arch", self._platform_machine.lower())] - return [] - - def _build_language_metadata(self) -> list[UserAgentComponent]: - """Build the language components of the User-Agent header string. - - Returns the Python version in a component with prefix "lang" and name - "python". The Python implementation (e.g. CPython, PyPy) is returned as - separate metadata component with prefix "md" and name "pyimpl". - - String representation of an example return value: - ``lang/python#3.10.4 md/pyimpl#CPython`` - """ - lang_md = [ - UserAgentComponent("lang", "python", self._python_version), - ] - if self._python_implementation: - lang_md.append( - UserAgentComponent("md", "pyimpl", self._python_implementation) - ) - return lang_md - - def _build_execution_env_metadata(self) -> list[UserAgentComponent]: - """Build the execution environment component of the User-Agent header. - - Returns a single component prefixed with "exec-env", usually sourced from the - environment variable AWS_EXECUTION_ENV. - """ - if self._execution_env: - return [UserAgentComponent("exec-env", self._execution_env)] - else: - return [] - - def _build_feature_metadata(self) -> list[UserAgentComponent]: - """Build the features components of the User-Agent header string. - - TODO: These should be sourced from property bag set on context. - """ - return [] - - def _build_app_id(self) -> list[UserAgentComponent]: - """Build app component of the User-Agent header string.""" - if self._user_agent_app_id: - return [UserAgentComponent("app", self._user_agent_app_id)] - else: - return [] - - def _build_suffix(self) -> list[_UAComponent]: - if self._user_agent_suffix: - return [RawStringUserAgentComponent(self._user_agent_suffix)] - else: - return [] - - -def sanitize_user_agent_string_component(raw_str: str, allow_hash: bool = False) -> str: - """Replaces all not allowed characters in the string with a dash ("-"). - - Allowed characters are ASCII alphanumerics and ``!$%&'*+-.^_`|~``. If - ``allow_hash`` is ``True``, "#"``" is also allowed. - - :type raw_str: str - :param raw_str: The input string to be sanitized. - - :type allow_hash: bool - :param allow_hash: Whether "#" is considered an allowed character. - """ - return "".join( - c if c in _USERAGENT_ALLOWED_CHARACTERS or (allow_hash and c == "#") else "-" - for c in raw_str - ) - - -def _get_crt_version() -> str | None: - """This function is considered private and is subject to abrupt breaking changes.""" - if HAS_CRT: - import awscrt - - return awscrt.__version__ - else: - return None diff --git a/packages/smithy-http/src/smithy_http/__init__.py b/packages/smithy-http/src/smithy_http/__init__.py index 90869db71..8ec658ea1 100644 --- a/packages/smithy-http/src/smithy_http/__init__.py +++ b/packages/smithy-http/src/smithy_http/__init__.py @@ -6,7 +6,6 @@ from . import interfaces from .interfaces import FieldPosition - class Field(interfaces.Field): """A name-value pair representing a single field in an HTTP Request or Response. diff --git a/packages/smithy-http/src/smithy_http/interceptors/__init__.py b/packages/smithy-http/src/smithy_http/interceptors/__init__.py new file mode 100644 index 000000000..33cbe867a --- /dev/null +++ b/packages/smithy-http/src/smithy_http/interceptors/__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-http/src/smithy_http/interceptors/user_agent.py b/packages/smithy-http/src/smithy_http/interceptors/user_agent.py new file mode 100644 index 000000000..b59c7c879 --- /dev/null +++ b/packages/smithy-http/src/smithy_http/interceptors/user_agent.py @@ -0,0 +1,141 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +import platform +from typing import Self + +import smithy_core +import smithy_http +from smithy_core.aio.interfaces import Request +from smithy_core.interceptors import Interceptor, InterceptorContext +from smithy_http import Field +from smithy_http.aio.interfaces import HTTPRequest +from smithy_http.user_agent import UserAgent, UserAgentComponent + + +class UserAgentInterceptor(Interceptor[Request, None, HTTPRequest, None]): + """Adds interceptors that initialize UserAgent in the context and add the user-agent header""" + + def read_before_execution(self, context: InterceptorContext[Request, None, None, None]) -> None: + context.properties['user_agent'] = _UserAgentBuilder.from_environment().build() + + def modify_before_signing( + self, context: InterceptorContext[Request, None, HTTPRequest, None] + ) -> HTTPRequest: + user_agent = context.properties['user_agent'] + request = context.transport_request + request.fields.set_field( + Field(name="User-Agent", values=[str(user_agent)]) + ) + return context.transport_request + + +_USERAGENT_ALLOWED_OS_NAMES = ( + "windows", + "linux", + "macos", + "android", + "ios", + "watchos", + "tvos", + "other", +) +_USERAGENT_PLATFORM_NAME_MAPPINGS = {"darwin": "macos"} +_USERAGENT_SDK_NAME = "python" + +class _UserAgentBuilder: + def __init__( + self, + *, + platform_name: str | None, + platform_version: str | None, + platform_machine: str | None, + python_version: str | None, + python_implementation: str | None, + sdk_version: str | None = None, + ) -> None: + self._platform_name = platform_name + self._platform_version = platform_version + self._platform_machine = platform_machine + self._python_version = python_version + self._python_implementation = python_implementation + # TODO: Allow configuration through context + self._sdk_version = smithy_core.__version__ + + @classmethod + def from_environment(cls) -> Self: + return cls( + platform_name=platform.system(), + platform_version=platform.release(), + platform_machine=platform.machine(), + python_version=platform.python_version(), + python_implementation=platform.python_implementation(), + ) + + def build(self) -> UserAgent: + user_agent = UserAgent() + user_agent.sdk_metadata.append( + UserAgentComponent(prefix=_USERAGENT_SDK_NAME, name=self._sdk_version)) + user_agent.ua_metadata.append(UserAgentComponent(prefix="ua", name="2.1")) + return user_agent + + def _build_os_metadata(self) -> list[UserAgentComponent]: + """Build the OS/platform components of the User-Agent header string. + + For recognized platform names that match or map to an entry in the list + of standardized OS names, a single component with prefix "os" is + returned. Otherwise, one component "os/other" is returned and a second + with prefix "md" and the raw platform name. + + String representations of example return values: + * ``os/macos#10.13.6`` + * ``os/linux`` + * ``os/other`` + * ``os/other md/foobar#1.2.3`` + """ + if self._platform_name is None: + return [UserAgentComponent("os", "other")] + + plt_name_lower = self._platform_name.lower() + if plt_name_lower in _USERAGENT_ALLOWED_OS_NAMES: + os_family = plt_name_lower + elif plt_name_lower in _USERAGENT_PLATFORM_NAME_MAPPINGS: + os_family = _USERAGENT_PLATFORM_NAME_MAPPINGS[plt_name_lower] + else: + os_family = None + + if os_family is not None: + return [UserAgentComponent("os", os_family, self._platform_version)] + else: + return [ + UserAgentComponent("os", "other"), + UserAgentComponent("md", self._platform_name, self._platform_version), + ] + + def _build_architecture_metadata(self) -> list[UserAgentComponent]: + """Build architecture component of the User-Agent header string. + + Returns the machine type with prefix "md" and name "arch", if one is available. + Common values include "x86_64", "arm64", "i386". + """ + if self._platform_machine: + return [UserAgentComponent("md", "arch", self._platform_machine.lower())] + return [] + + def _build_language_metadata(self) -> list[UserAgentComponent]: + """Build the language components of the User-Agent header string. + + Returns the Python version in a component with prefix "lang" and name + "python". The Python implementation (e.g. CPython, PyPy) is returned as + separate metadata component with prefix "md" and name "pyimpl". + + String representation of an example return value: + ``lang/python#3.10.4 md/pyimpl#CPython`` + """ + lang_md = [ + UserAgentComponent("lang", "python", self._python_version), + ] + if self._python_implementation: + lang_md.append( + UserAgentComponent("md", "pyimpl", self._python_implementation) + ) + return lang_md \ No newline at end of file diff --git a/packages/smithy-http/src/smithy_http/plugins/__init__.py b/packages/smithy-http/src/smithy_http/plugins/__init__.py new file mode 100644 index 000000000..417b28eb9 --- /dev/null +++ b/packages/smithy-http/src/smithy_http/plugins/__init__.py @@ -0,0 +1,8 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +from smithy_core.interfaces.config import Config +from smithy_http.interceptors.user_agent import UserAgentInterceptor + +def user_agent_plugin(config: Config) -> None: + config.interceptors.append(UserAgentInterceptor()) diff --git a/packages/smithy-http/src/smithy_http/user_agent.py b/packages/smithy-http/src/smithy_http/user_agent.py new file mode 100644 index 000000000..48b96a784 --- /dev/null +++ b/packages/smithy-http/src/smithy_http/user_agent.py @@ -0,0 +1,95 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +# pyright: reportMissingTypeStubs=false,reportUnknownMemberType=false + +from dataclasses import dataclass, field +from string import ascii_letters, digits + +_USERAGENT_ALLOWED_CHARACTERS = ascii_letters + digits + "!$%&'*+-.^_`|~" + + +@dataclass(frozen=True, slots=True) +class UserAgentComponent: + """Component of a User-Agent header string in the standard format. + + Each component consists of a prefix, a name, and a value. In the string + representation these are combined in the format ``prefix/name#value``. + + This class is considered private and is subject to abrupt breaking changes. + """ + + prefix: str + name: str + value: str | None = None + + def __str__(self): + """Create string like 'prefix/name#value' from a UserAgentComponent.""" + clean_prefix = sanitize_user_agent_string_component( + self.prefix, allow_hash=True + ) + clean_name = sanitize_user_agent_string_component(self.name, allow_hash=False) + if self.value is None or self.value == "": + return f"{clean_prefix}/{clean_name}" + clean_value = sanitize_user_agent_string_component(self.value, allow_hash=True) + return f"{clean_prefix}/{clean_name}#{clean_value}" + + +@dataclass(frozen=True, slots=True) +class RawStringUserAgentComponent: + """UserAgentComponent interface wrapper around ``str``. + + Use for User-Agent header components that are not constructed from prefix+name+value + but instead are provided as strings. No sanitization is performed. + """ + + value: str + + def __str__(self) -> str: + return self.value + + +@dataclass(kw_only=True, slots=True) +class UserAgent: + sdk_metadata: list[UserAgentComponent] = field(default_factory=list) + internal_metadata: list[UserAgentComponent] = field(default_factory=list) + ua_metadata: list[UserAgentComponent] = field(default_factory=list) + api_metadata: list[UserAgentComponent] = field(default_factory=list) + os_metadata: list[UserAgentComponent] = field(default_factory=list) + language_metadata: list[UserAgentComponent] = field(default_factory=list) + env_metadata: list[UserAgentComponent] = field(default_factory=list) + config_metadata: list[UserAgentComponent] = field(default_factory=list) + feat_metadata: list[UserAgentComponent] = field(default_factory=list) + additional_metadata: list[RawStringUserAgentComponent] = field(default_factory=list) + + def __str__(self) -> str: + components = [ + *self.sdk_metadata, + *self.internal_metadata, + *self.ua_metadata, + *self.api_metadata, + *self.os_metadata, + *self.language_metadata, + *self.env_metadata, + *self.config_metadata, + *self.feat_metadata, + *self.additional_metadata, + ] + return " ".join([str(comp) for comp in components]) + + +def sanitize_user_agent_string_component(raw_str: str, allow_hash: bool = False) -> str: + """Replaces all not allowed characters in the string with a dash ("-"). + + Allowed characters are ASCII alphanumerics and ``!$%&'*+-.^_`|~``. If + ``allow_hash`` is ``True``, "#"``" is also allowed. + + :type raw_str: str + :param raw_str: The input string to be sanitized. + + :type allow_hash: bool + :param allow_hash: Whether "#" is considered an allowed character. + """ + return "".join( + c if c in _USERAGENT_ALLOWED_CHARACTERS or (allow_hash and c == "#") else "-" + for c in raw_str + ) From 296811b8d49fe37e76bfa5c78c25755e7c77a2b0 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Thu, 27 Feb 2025 09:11:21 -0800 Subject: [PATCH 10/20] Remove explicit Config protocol --- .../codegen/generators/ConfigGenerator.java | 15 +-------------- .../src/smithy_core/interfaces/config.py | 11 ----------- .../src/smithy_http/interceptors/user_agent.py | 10 ++++------ .../src/smithy_http/interfaces/config.py | 15 --------------- .../src/smithy_http/plugins/__init__.py | 8 ++++++-- 5 files changed, 11 insertions(+), 48 deletions(-) delete mode 100644 packages/smithy-core/src/smithy_core/interfaces/config.py delete mode 100644 packages/smithy-http/src/smithy_http/interfaces/config.py 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 bbf7a49b5..cd2989d22 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 @@ -290,18 +290,6 @@ private void writeInterceptorsType(PythonWriter writer) { private void generateConfig(GenerationContext context, PythonWriter writer) { var configSymbol = CodegenUtils.getConfigSymbol(context.settings()); - Symbol configProtocolSymbol = null; - if (context.applicationProtocol().isHttpProtocol()) { - configProtocolSymbol = Symbol.builder() - .name("HttpConfig") - .namespace("smithy_http.interfaces.config", ".") - .build(); - } else { - configProtocolSymbol = Symbol.builder() - .name("Config") - .namespace("smithy_core.interfaces.config", ".") - .build(); - } // Initialize the list of config properties with our base properties. Here a new // list is constructed because that base list is immutable. @@ -336,7 +324,7 @@ private void generateConfig(GenerationContext context, PythonWriter writer) { writer.addStdlibImport("dataclasses", "dataclass"); writer.write(""" @dataclass(init=False) - class $T($T): + class $T: \"""Configuration for $L.\""" ${C|} @@ -353,7 +341,6 @@ def __init__( ${C|} """, configSymbol, - configProtocolSymbol, context.settings().service().getName(), writer.consumer(w -> writePropertyDeclarations(w, finalProperties)), writer.consumer(w -> writeInitParams(w, finalProperties)), diff --git a/packages/smithy-core/src/smithy_core/interfaces/config.py b/packages/smithy-core/src/smithy_core/interfaces/config.py deleted file mode 100644 index b246668a3..000000000 --- a/packages/smithy-core/src/smithy_core/interfaces/config.py +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: Apache-2.0 -from typing import Protocol, Any - -from smithy_core.interceptors import Interceptor -from smithy_core.interfaces.retries import RetryStrategy - - -class Config(Protocol): - interceptors: list[Interceptor[Any, Any, Any, Any]] - retry_strategy: RetryStrategy diff --git a/packages/smithy-http/src/smithy_http/interceptors/user_agent.py b/packages/smithy-http/src/smithy_http/interceptors/user_agent.py index b59c7c879..3511e5a55 100644 --- a/packages/smithy-http/src/smithy_http/interceptors/user_agent.py +++ b/packages/smithy-http/src/smithy_http/interceptors/user_agent.py @@ -1,25 +1,23 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import platform -from typing import Self +from typing import Self, Any import smithy_core -import smithy_http -from smithy_core.aio.interfaces import Request from smithy_core.interceptors import Interceptor, InterceptorContext from smithy_http import Field from smithy_http.aio.interfaces import HTTPRequest from smithy_http.user_agent import UserAgent, UserAgentComponent -class UserAgentInterceptor(Interceptor[Request, None, HTTPRequest, None]): +class UserAgentInterceptor(Interceptor[Any, None, HTTPRequest, None]): """Adds interceptors that initialize UserAgent in the context and add the user-agent header""" - def read_before_execution(self, context: InterceptorContext[Request, None, None, None]) -> None: + def read_before_execution(self, context: InterceptorContext[Any, None, None, None]) -> None: context.properties['user_agent'] = _UserAgentBuilder.from_environment().build() def modify_before_signing( - self, context: InterceptorContext[Request, None, HTTPRequest, None] + self, context: InterceptorContext[Any, None, HTTPRequest, None] ) -> HTTPRequest: user_agent = context.properties['user_agent'] request = context.transport_request diff --git a/packages/smithy-http/src/smithy_http/interfaces/config.py b/packages/smithy-http/src/smithy_http/interfaces/config.py deleted file mode 100644 index e3a2a302e..000000000 --- a/packages/smithy-http/src/smithy_http/interfaces/config.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: Apache-2.0 -from typing import Any - -from smithy_core.interfaces import URI -from smithy_core.interfaces.config import Config -from smithy_http.aio.interfaces import HTTPClient, EndpointResolver -from smithy_http.interfaces import HTTPRequestConfiguration - - -class HttpConfig(Config): - http_client: HTTPClient - http_request_config: HTTPRequestConfiguration | None - endpoint_resolver: EndpointResolver[Any] - endpoint_uri: str | URI | None diff --git a/packages/smithy-http/src/smithy_http/plugins/__init__.py b/packages/smithy-http/src/smithy_http/plugins/__init__.py index 417b28eb9..55e93f2a0 100644 --- a/packages/smithy-http/src/smithy_http/plugins/__init__.py +++ b/packages/smithy-http/src/smithy_http/plugins/__init__.py @@ -1,8 +1,12 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 +from typing import Protocol, Any -from smithy_core.interfaces.config import Config +from smithy_core.interceptors import Interceptor from smithy_http.interceptors.user_agent import UserAgentInterceptor -def user_agent_plugin(config: Config) -> None: +class _InterceptorConfig(Protocol): + interceptors: list[Interceptor[Any, Any, Any, Any]] + +def user_agent_plugin(config: _InterceptorConfig) -> None: config.interceptors.append(UserAgentInterceptor()) From f449114e21e842f55d2cfc47b635a1f23f988378 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Thu, 27 Feb 2025 12:00:47 -0800 Subject: [PATCH 11/20] Major refactor - generic and aws specific user agent. Codegen useragent plugin per service. --- .../aws/codegen/AwsUserAgentIntegration.java | 98 ++++++++++++++----- .../python/codegen/DirectedPythonCodegen.java | 18 +++- .../integrations/RuntimeClientPlugin.java | 42 +++++++- .../codegen/writer/ImportDeclarations.java | 7 +- .../src/smithy_aws_core/__init__.py | 2 +- .../interceptors/user_agent.py | 73 +++++++++++++- .../src/smithy_aws_core/plugins/__init__.py | 9 -- .../smithy-core/src/smithy_core/__init__.py | 2 +- .../smithy-http/src/smithy_http/__init__.py | 1 + .../smithy_http/interceptors/user_agent.py | 29 +++--- .../src/smithy_http/plugins/__init__.py | 2 + .../smithy-http/src/smithy_http/user_agent.py | 4 +- 12 files changed, 231 insertions(+), 56 deletions(-) delete mode 100644 packages/smithy-aws-core/src/smithy_aws_core/plugins/__init__.py diff --git a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsUserAgentIntegration.java b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsUserAgentIntegration.java index 113275044..e1f166516 100644 --- a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsUserAgentIntegration.java +++ b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsUserAgentIntegration.java @@ -7,6 +7,7 @@ import java.util.List; 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; @@ -21,34 +22,79 @@ public class AwsUserAgentIntegration implements PythonIntegration { @Override public List 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(); + return List.of( RuntimeClientPlugin.builder() - .addConfigProperty( - 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()) - .addConfigProperty( - 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()) - .pythonPlugin( - SymbolReference.builder() - .symbol(Symbol.builder() - .namespace( - AwsPythonDependency.SMITHY_AWS_CORE.packageName() - + ".plugins", - ".") - .name("aws_user_agent_plugin") - .addDependency(AwsPythonDependency.SMITHY_AWS_CORE) - .build()) - .build()) + .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(""" + 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' + ) + ) + """, + CodegenUtils.getConfigSymbol(c.settings()), + userAgentInterceptor, + versionSymbol, + c.settings().service().getName() + ); + + }); + return List.of(filename); + }) .build()); } else { return List.of(); 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 ac726a4f7..5a161912c 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 @@ -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; @@ -274,9 +275,24 @@ public void generateIntEnumShape(GenerateIntEnumDirective directive) { generateServiceModuleInit(directive); + generatePluginFiles(directive); generateInits(directive); } + /** + * Writes out all extra files required by runtime plugins. + */ + private void generatePluginFiles(CustomizeDirective 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. */ @@ -287,7 +303,7 @@ private void generateServiceModuleInit(CustomizeDirective { writer - .write("__version__ = '$L'", directive.context().settings().moduleVersion()); + .write("__version__: str = '$L'", directive.context().settings().moduleVersion()); }); } diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/integrations/RuntimeClientPlugin.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/integrations/RuntimeClientPlugin.java index 88ac5ec4f..540cdf50b 100644 --- a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/integrations/RuntimeClientPlugin.java +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/integrations/RuntimeClientPlugin.java @@ -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; @@ -38,6 +39,7 @@ public final class RuntimeClientPlugin implements ToSmithyBuilder configProperties; private final SymbolReference pythonPlugin; + private final WriteAdditionalFiles writeAdditionalFiles; private final AuthScheme authScheme; @@ -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; } /** @@ -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 writeAdditionalFiles(GenerationContext context); + } + /** * Returns true if this plugin applies to the given service. * @@ -120,6 +137,16 @@ public Optional 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 writeAdditionalFiles(GenerationContext context) { + return writeAdditionalFiles.writeAdditionalFiles(context); + } + /** * @return Returns a new builder for a {@link RuntimeClientPlugin}. */ @@ -132,7 +159,8 @@ public SmithyBuilder toBuilder() { var builder = builder() .pythonPlugin(pythonPlugin) .authScheme(authScheme) - .configProperties(configProperties); + .configProperties(configProperties) + .writeAdditionalFiles(writeAdditionalFiles); if (operationPredicate == OPERATION_ALWAYS_FALSE) { builder.servicePredicate(servicePredicate); @@ -152,6 +180,7 @@ public static final class Builder implements SmithyBuilder private List configProperties = new ArrayList<>(); private SymbolReference pythonPlugin = null; private AuthScheme authScheme = null; + private WriteAdditionalFiles writeAdditionalFiles = (context) -> Collections.emptyList(); Builder() {} @@ -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; + } } } diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/writer/ImportDeclarations.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/writer/ImportDeclarations.java index 799b64912..13df49e9d 100644 --- a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/writer/ImportDeclarations.java +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/writer/ImportDeclarations.java @@ -70,7 +70,12 @@ private String relativize(String namespace) { } } var prefix = StringUtils.repeat(".", localParts.length - commonSegments); - return prefix + namespace.split("\\.", commonSegments + 1)[commonSegments]; + String[] segments = namespace.split("\\.", commonSegments + 1); + if (commonSegments >= segments.length) { + return "."; + } else { + return prefix + segments[commonSegments]; + } } ImportDeclarations addStdlibImport(String namespace) { diff --git a/packages/smithy-aws-core/src/smithy_aws_core/__init__.py b/packages/smithy-aws-core/src/smithy_aws_core/__init__.py index ce8f9b1b1..5fddba84a 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/__init__.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/__init__.py @@ -1,4 +1,4 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -__version__ = "0.1.0" +__version__: str = "0.1.0" diff --git a/packages/smithy-aws-core/src/smithy_aws_core/interceptors/user_agent.py b/packages/smithy-aws-core/src/smithy_aws_core/interceptors/user_agent.py index 8b59e4208..4b8834ca4 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/interceptors/user_agent.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/interceptors/user_agent.py @@ -1,8 +1,73 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -from smithy_core.interceptors import Interceptor, Request -from smithy_http.aio.interfaces import HTTPRequest +# pyright: reportMissingTypeStubs=false +from typing import Any +import smithy_aws_core +import smithy_core +from smithy_core.interceptors import Interceptor, InterceptorContext +from smithy_http.user_agent import UserAgentComponent, RawStringUserAgentComponent -class UserAgentInterceptor(Interceptor[Request, None, HTTPRequest, None]): - """Adds UserAgent header to the Request before signing.""" \ No newline at end of file +_USERAGENT_SDK_NAME = "python" + + +class UserAgentInterceptor(Interceptor[Any, Any, Any, Any]): + """Adds AWS fields to the UserAgent.""" + + def __init__( + self, + *, + ua_suffix: str | None, + ua_app_id: str | None, + sdk_version: str, + service_id: str, + ) -> None: + """Initialize the UserAgentInterceptor. + + :param ua_suffix: Additional suffix to be added to the UserAgent header. + :param ua_app_id: User defined and opaque application ID to be added to the + UserAgent header. + :param sdk_version: SDK version to be added to the UserAgent header. + :param service_id: ServiceId to be added to the UserAgent header. + """ + super().__init__() + self._ua_suffix = ua_suffix + self._ua_app_id = ua_app_id + self._sdk_version = sdk_version + self._service_id = service_id + + def read_after_serialization( + self, context: InterceptorContext[Any, Any, Any, Any] + ) -> None: + if "user_agent" in context.properties: + user_agent = context.properties["user_agent"] + user_agent.sdk_metadata = self._build_sdk_metadata() + user_agent.api_metadata.append( + UserAgentComponent("api", self._service_id, self._sdk_version) + ) + + if self._ua_app_id is not None: + user_agent.additional_metadata.append( + UserAgentComponent("app", self._ua_app_id) + ) + + if self._ua_suffix is not None: + user_agent.additional_metadata.append( + RawStringUserAgentComponent(self._ua_suffix) + ) + + def _build_sdk_metadata(self) -> list[UserAgentComponent]: + return [ + UserAgentComponent(_USERAGENT_SDK_NAME, self._sdk_version), + UserAgentComponent("md", "smithy-aws-core", smithy_aws_core.__version__), + UserAgentComponent("md", "smithy-core", smithy_core.__version__), + *self._crt_version(), + ] + + def _crt_version(self) -> list[UserAgentComponent]: + try: + import awscrt + + return [UserAgentComponent("md", "awscrt", awscrt.__version__)] + except AttributeError: + return [] diff --git a/packages/smithy-aws-core/src/smithy_aws_core/plugins/__init__.py b/packages/smithy-aws-core/src/smithy_aws_core/plugins/__init__.py deleted file mode 100644 index ade462782..000000000 --- a/packages/smithy-aws-core/src/smithy_aws_core/plugins/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: Apache-2.0 - -from typing import Any - -from smithy_aws_core.interceptors.user_agent import UserAgentInterceptor - -def aws_user_agent_plugin(config: Any) -> None: - config.interceptors.append(UserAgentInterceptor()) diff --git a/packages/smithy-core/src/smithy_core/__init__.py b/packages/smithy-core/src/smithy_core/__init__.py index 05a322331..96e888f3e 100644 --- a/packages/smithy-core/src/smithy_core/__init__.py +++ b/packages/smithy-core/src/smithy_core/__init__.py @@ -8,7 +8,7 @@ from . import interfaces, rfc3986 from .exceptions import SmithyException -__version__ = "0.1.0" +__version__: str = "0.1.0" class HostType(Enum): diff --git a/packages/smithy-http/src/smithy_http/__init__.py b/packages/smithy-http/src/smithy_http/__init__.py index 8ec658ea1..90869db71 100644 --- a/packages/smithy-http/src/smithy_http/__init__.py +++ b/packages/smithy-http/src/smithy_http/__init__.py @@ -6,6 +6,7 @@ from . import interfaces from .interfaces import FieldPosition + class Field(interfaces.Field): """A name-value pair representing a single field in an HTTP Request or Response. diff --git a/packages/smithy-http/src/smithy_http/interceptors/user_agent.py b/packages/smithy-http/src/smithy_http/interceptors/user_agent.py index 3511e5a55..6c1b5f335 100644 --- a/packages/smithy-http/src/smithy_http/interceptors/user_agent.py +++ b/packages/smithy-http/src/smithy_http/interceptors/user_agent.py @@ -11,19 +11,20 @@ class UserAgentInterceptor(Interceptor[Any, None, HTTPRequest, None]): - """Adds interceptors that initialize UserAgent in the context and add the user-agent header""" + """Adds interceptors that initialize UserAgent in the context and add the user-agent + header.""" - def read_before_execution(self, context: InterceptorContext[Any, None, None, None]) -> None: - context.properties['user_agent'] = _UserAgentBuilder.from_environment().build() + def read_before_execution( + self, context: InterceptorContext[Any, None, None, None] + ) -> None: + context.properties["user_agent"] = _UserAgentBuilder.from_environment().build() def modify_before_signing( - self, context: InterceptorContext[Any, None, HTTPRequest, None] - ) -> HTTPRequest: - user_agent = context.properties['user_agent'] + self, context: InterceptorContext[Any, None, HTTPRequest, None] + ) -> HTTPRequest: + user_agent = context.properties["user_agent"] request = context.transport_request - request.fields.set_field( - Field(name="User-Agent", values=[str(user_agent)]) - ) + request.fields.set_field(Field(name="User-Agent", values=[str(user_agent)])) return context.transport_request @@ -40,6 +41,7 @@ def modify_before_signing( _USERAGENT_PLATFORM_NAME_MAPPINGS = {"darwin": "macos"} _USERAGENT_SDK_NAME = "python" + class _UserAgentBuilder: def __init__( self, @@ -72,8 +74,13 @@ def from_environment(cls) -> Self: def build(self) -> UserAgent: user_agent = UserAgent() user_agent.sdk_metadata.append( - UserAgentComponent(prefix=_USERAGENT_SDK_NAME, name=self._sdk_version)) + UserAgentComponent(prefix=_USERAGENT_SDK_NAME, name=self._sdk_version) + ) user_agent.ua_metadata.append(UserAgentComponent(prefix="ua", name="2.1")) + user_agent.os_metadata.extend(self._build_os_metadata()) + user_agent.os_metadata.extend(self._build_architecture_metadata()) + user_agent.language_metadata.extend(self._build_language_metadata()) + return user_agent def _build_os_metadata(self) -> list[UserAgentComponent]: @@ -136,4 +143,4 @@ def _build_language_metadata(self) -> list[UserAgentComponent]: lang_md.append( UserAgentComponent("md", "pyimpl", self._python_implementation) ) - return lang_md \ No newline at end of file + return lang_md diff --git a/packages/smithy-http/src/smithy_http/plugins/__init__.py b/packages/smithy-http/src/smithy_http/plugins/__init__.py index 55e93f2a0..2d2ab0574 100644 --- a/packages/smithy-http/src/smithy_http/plugins/__init__.py +++ b/packages/smithy-http/src/smithy_http/plugins/__init__.py @@ -5,8 +5,10 @@ from smithy_core.interceptors import Interceptor from smithy_http.interceptors.user_agent import UserAgentInterceptor + class _InterceptorConfig(Protocol): interceptors: list[Interceptor[Any, Any, Any, Any]] + def user_agent_plugin(config: _InterceptorConfig) -> None: config.interceptors.append(UserAgentInterceptor()) diff --git a/packages/smithy-http/src/smithy_http/user_agent.py b/packages/smithy-http/src/smithy_http/user_agent.py index 48b96a784..e44e3ca11 100644 --- a/packages/smithy-http/src/smithy_http/user_agent.py +++ b/packages/smithy-http/src/smithy_http/user_agent.py @@ -59,7 +59,9 @@ class UserAgent: env_metadata: list[UserAgentComponent] = field(default_factory=list) config_metadata: list[UserAgentComponent] = field(default_factory=list) feat_metadata: list[UserAgentComponent] = field(default_factory=list) - additional_metadata: list[RawStringUserAgentComponent] = field(default_factory=list) + additional_metadata: list[UserAgentComponent | RawStringUserAgentComponent] = field( + default_factory=list + ) def __str__(self) -> str: components = [ From 6a497289115951de4f1e3d2d85bc43e43d2a6f48 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Fri, 28 Feb 2025 09:15:32 -0800 Subject: [PATCH 12/20] Use aws-sdk-python as sdk name + correctly use sdkId for serviceId --- .../aws/codegen/AwsUserAgentIntegration.java | 44 ++++++++++++------- .../interceptors/user_agent.py | 2 +- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsUserAgentIntegration.java b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsUserAgentIntegration.java index e1f166516..f51d0ec6e 100644 --- a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsUserAgentIntegration.java +++ b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsUserAgentIntegration.java @@ -5,6 +5,7 @@ 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; @@ -19,6 +20,19 @@ */ @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 getClientPlugins(GenerationContext context) { if (context.applicationProtocol().isHttpProtocol()) { @@ -58,10 +72,17 @@ public List getClientPlugins(GenerationContext context) { .build(); final SymbolReference versionSymbol = SymbolReference.builder() .symbol(Symbol.builder() - .namespace(moduleName, ".") - .name("__version__") - .build() - ).build(); + .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() @@ -75,22 +96,11 @@ public List getClientPlugins(GenerationContext context) { filename, moduleName + ".", writer -> { - writer.write(""" - 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' - ) - ) - """, + writer.write(USER_AGENT_PLUGIN, CodegenUtils.getConfigSymbol(c.settings()), userAgentInterceptor, versionSymbol, - c.settings().service().getName() - ); + serviceId); }); return List.of(filename); diff --git a/packages/smithy-aws-core/src/smithy_aws_core/interceptors/user_agent.py b/packages/smithy-aws-core/src/smithy_aws_core/interceptors/user_agent.py index 4b8834ca4..42d9b2bcb 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/interceptors/user_agent.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/interceptors/user_agent.py @@ -8,7 +8,7 @@ from smithy_core.interceptors import Interceptor, InterceptorContext from smithy_http.user_agent import UserAgentComponent, RawStringUserAgentComponent -_USERAGENT_SDK_NAME = "python" +_USERAGENT_SDK_NAME = "aws-sdk-python" class UserAgentInterceptor(Interceptor[Any, Any, Any, Any]): From b88dfc26aac5ae5f24a2796669d0867e4e61f30a Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Fri, 28 Feb 2025 10:49:43 -0800 Subject: [PATCH 13/20] Use aws core version as the "main" sdk version. client library version is still captured on `api` field as expected by ua/2.0 --- .../src/smithy_aws_core/interceptors/user_agent.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/smithy-aws-core/src/smithy_aws_core/interceptors/user_agent.py b/packages/smithy-aws-core/src/smithy_aws_core/interceptors/user_agent.py index 42d9b2bcb..6acccdeff 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/interceptors/user_agent.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/interceptors/user_agent.py @@ -58,8 +58,7 @@ def read_after_serialization( def _build_sdk_metadata(self) -> list[UserAgentComponent]: return [ - UserAgentComponent(_USERAGENT_SDK_NAME, self._sdk_version), - UserAgentComponent("md", "smithy-aws-core", smithy_aws_core.__version__), + UserAgentComponent(_USERAGENT_SDK_NAME, smithy_aws_core.__version__), UserAgentComponent("md", "smithy-core", smithy_core.__version__), *self._crt_version(), ] From 6c6458ba4dd4b649dbdfd744534deecc44dfa8ff Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Mon, 3 Mar 2025 09:42:27 -0800 Subject: [PATCH 14/20] User agent tests --- packages/smithy-http/tests/unit/user_agent.py | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 packages/smithy-http/tests/unit/user_agent.py diff --git a/packages/smithy-http/tests/unit/user_agent.py b/packages/smithy-http/tests/unit/user_agent.py new file mode 100644 index 000000000..dcf135807 --- /dev/null +++ b/packages/smithy-http/tests/unit/user_agent.py @@ -0,0 +1,121 @@ +import pytest + +from smithy_http.user_agent import ( + sanitize_user_agent_string_component, + UserAgentComponent, + RawStringUserAgentComponent, + UserAgent, +) + + +@pytest.mark.parametrize( + "raw_str, allow_hash, expected_str", + [ + ("foo", False, "foo"), + ("foo", True, "foo"), + ("ExampleFramework (1.2.3)", False, "ExampleFramework--1.2.3-"), + ("foo#1.2.3", False, "foo-1.2.3"), + ("foo#1.2.3", True, "foo#1.2.3"), + ("", False, ""), + ("", True, ""), + ("", False, ""), + ("#", False, "-"), + ("#", True, "#"), + (" ", False, "-"), + (" ", False, "--"), + ("@=[]{ }/\\øß©", True, "------------"), + ( + "Java_HotSpot_(TM)_64-Bit_Server_VM/25.151-b12", + True, + "Java_HotSpot_-TM-_64-Bit_Server_VM-25.151-b12", + ), + ], +) +def test_sanitize_ua_string_component( + raw_str: str, allow_hash: bool, expected_str: str +): + actual_str = sanitize_user_agent_string_component(raw_str, allow_hash) + assert actual_str == expected_str + + +# Test cases for UserAgentComponent +def test_user_agent_component_without_value(): + component = UserAgentComponent(prefix="md", name="test") + assert str(component) == "md/test" + + +def test_user_agent_component_with_value(): + component = UserAgentComponent(prefix="md", name="test", value="123") + assert str(component) == "md/test#123" + + +def test_user_agent_component_with_empty_value(): + component = UserAgentComponent(prefix="md", name="test", value="") + assert str(component) == "md/test" + + +def test_user_agent_component_sanitization(): + component = UserAgentComponent(prefix="md@", name="test!", value="123#") + assert str(component) == "md-/test!#123#" + + +# Test cases for RawStringUserAgentComponent +def test_raw_string_user_agent_component(): + component = RawStringUserAgentComponent(value="raw/string#123") + assert str(component) == "raw/string#123" + + +# Test cases for UserAgent +def test_user_agent_with_multiple_components(): + sdk_component = UserAgentComponent(prefix="sdk", name="python", value="1.0") + os_component = UserAgentComponent(prefix="os", name="linux", value="5.4") + raw_component = RawStringUserAgentComponent(value="raw/string#123") + + user_agent = UserAgent( + sdk_metadata=[sdk_component], + os_metadata=[os_component], + additional_metadata=[raw_component], + ) + + expected_output = "sdk/python#1.0 os/linux#5.4 raw/string#123" + assert str(user_agent) == expected_output + + +def test_user_agent_with_empty_metadata(): + user_agent = UserAgent() + assert str(user_agent) == "" + + +def test_user_agent_with_all_metadata_types(): + sdk_component = UserAgentComponent(prefix="sdk", name="python", value="1.0") + internal_component = UserAgentComponent(prefix="md", name="internal") + ua_component = UserAgentComponent(prefix="ua", name="2.0") + api_component = UserAgentComponent(prefix="api", name="operation", value="1.1") + os_component = UserAgentComponent(prefix="os", name="linux", value="5.4") + language_component = UserAgentComponent(prefix="lang", name="python", value="3.12") + env_component = UserAgentComponent(prefix="exec-env", name="prod") + config_component = UserAgentComponent( + prefix="cfg", name="retry-mode", value="standard" + ) + feat_component = UserAgentComponent(prefix="ft", name="paginator") + raw_component = RawStringUserAgentComponent(value="raw/string#123") + + user_agent = UserAgent( + sdk_metadata=[sdk_component], + internal_metadata=[internal_component], + ua_metadata=[ua_component], + api_metadata=[api_component], + os_metadata=[os_component], + language_metadata=[language_component], + env_metadata=[env_component], + config_metadata=[config_component], + feat_metadata=[feat_component], + additional_metadata=[raw_component], + ) + + expected_output = ( + "sdk/python#1.0 md/internal ua/2.0 api/operation#1.1 os/linux#5.4 " + "lang/python#3.12 exec-env/prod cfg/retry-mode#standard ft/paginator " + "raw/string#123" + ) + assert str(user_agent) == expected_output From 7aac81ce4c2ba601d3e33c3708c81554a764e3c7 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Mon, 3 Mar 2025 11:40:09 -0800 Subject: [PATCH 15/20] Add user_agent interceptor test --- .../smithy_http/interceptors/user_agent.py | 31 ++++---- .../unit/interceptors/test_user_agent.py | 71 +++++++++++++++++++ .../{user_agent.py => test_user_agent.py} | 0 3 files changed, 89 insertions(+), 13 deletions(-) create mode 100644 packages/smithy-http/tests/unit/interceptors/test_user_agent.py rename packages/smithy-http/tests/unit/{user_agent.py => test_user_agent.py} (100%) diff --git a/packages/smithy-http/src/smithy_http/interceptors/user_agent.py b/packages/smithy-http/src/smithy_http/interceptors/user_agent.py index 6c1b5f335..012fc8133 100644 --- a/packages/smithy-http/src/smithy_http/interceptors/user_agent.py +++ b/packages/smithy-http/src/smithy_http/interceptors/user_agent.py @@ -17,7 +17,7 @@ class UserAgentInterceptor(Interceptor[Any, None, HTTPRequest, None]): def read_before_execution( self, context: InterceptorContext[Any, None, None, None] ) -> None: - context.properties["user_agent"] = _UserAgentBuilder.from_environment().build() + context.properties["user_agent"] = UserAgentBuilder.from_environment().build() def modify_before_signing( self, context: InterceptorContext[Any, None, HTTPRequest, None] @@ -42,15 +42,15 @@ def modify_before_signing( _USERAGENT_SDK_NAME = "python" -class _UserAgentBuilder: +class UserAgentBuilder: def __init__( self, *, - platform_name: str | None, - platform_version: str | None, - platform_machine: str | None, - python_version: str | None, - python_implementation: str | None, + platform_name: str | None = None, + platform_version: str | None = None, + platform_machine: str | None = None, + python_version: str | None = None, + python_implementation: str | None = None, sdk_version: str | None = None, ) -> None: self._platform_name = platform_name @@ -58,8 +58,7 @@ def __init__( self._platform_machine = platform_machine self._python_version = python_version self._python_implementation = python_implementation - # TODO: Allow configuration through context - self._sdk_version = smithy_core.__version__ + self._sdk_version = sdk_version @classmethod def from_environment(cls) -> Self: @@ -69,13 +68,12 @@ def from_environment(cls) -> Self: platform_machine=platform.machine(), python_version=platform.python_version(), python_implementation=platform.python_implementation(), + sdk_version=smithy_core.__version__, ) def build(self) -> UserAgent: user_agent = UserAgent() - user_agent.sdk_metadata.append( - UserAgentComponent(prefix=_USERAGENT_SDK_NAME, name=self._sdk_version) - ) + user_agent.sdk_metadata.extend(self._build_sdk_metadata()) user_agent.ua_metadata.append(UserAgentComponent(prefix="ua", name="2.1")) user_agent.os_metadata.extend(self._build_os_metadata()) user_agent.os_metadata.extend(self._build_architecture_metadata()) @@ -83,6 +81,13 @@ def build(self) -> UserAgent: return user_agent + def _build_sdk_metadata(self) -> list[UserAgentComponent]: + if self._sdk_version: + return [ + UserAgentComponent(prefix=_USERAGENT_SDK_NAME, name=self._sdk_version) + ] + return [] + def _build_os_metadata(self) -> list[UserAgentComponent]: """Build the OS/platform components of the User-Agent header string. @@ -97,7 +102,7 @@ def _build_os_metadata(self) -> list[UserAgentComponent]: * ``os/other`` * ``os/other md/foobar#1.2.3`` """ - if self._platform_name is None: + if self._platform_name is None or self._platform_name == "": return [UserAgentComponent("os", "other")] plt_name_lower = self._platform_name.lower() diff --git a/packages/smithy-http/tests/unit/interceptors/test_user_agent.py b/packages/smithy-http/tests/unit/interceptors/test_user_agent.py new file mode 100644 index 000000000..6a4a14a66 --- /dev/null +++ b/packages/smithy-http/tests/unit/interceptors/test_user_agent.py @@ -0,0 +1,71 @@ +import platform + +import smithy_core +from smithy_http.interceptors.user_agent import UserAgentBuilder + + +def test_from_environment(monkeypatch): # type: ignore + monkeypatch.setattr(platform, "system", lambda: "Linux") # type: ignore + monkeypatch.setattr(platform, "release", lambda: "5.4.228-131.415.AMZN2.X86_64") # type: ignore + monkeypatch.setattr(platform, "python_version", lambda: "4.3.2") # type: ignore + monkeypatch.setattr(platform, "python_implementation", lambda: "Cpython") # type: ignore + monkeypatch.setattr(smithy_core, "__version__", "1.2.3") # type: ignore + + user_agent = str(UserAgentBuilder.from_environment().build()) + assert "python/1.2.3" in user_agent + assert "os/linux#5.4.228-131.415.AMZN2.X86_64" in user_agent + assert "md/arch#arm64" in user_agent + assert "lang/python#4.3.2" in user_agent + assert "md/pyimpl#Cpython" in user_agent + + +def test_build_adds_sdk_metadata(): + user_agent = UserAgentBuilder(sdk_version="1.2.3").build() + assert "python/1.2.3" in str(user_agent) + + +def test_build_adds_ua_metadata(): + user_agent = UserAgentBuilder().build() + assert "ua/2.1" in str(user_agent) + + +def test_build_os_defaults_to_other(): + user_agent = UserAgentBuilder().build() + assert "os/other" in str(user_agent) + + +def test_build_os_lowercases_platform(): + user_agent = UserAgentBuilder(platform_name="LINUX").build() + assert "os/linux" in str(user_agent) + + +def test_build_os_maps_platform_names(): + user_agent = UserAgentBuilder(platform_name="darwin").build() + assert "os/macos" in str(user_agent) + + +def test_build_os_includes_version(): + user_agent = UserAgentBuilder(platform_name="linux", platform_version="5.4").build() + assert "os/linux#5.4" in str(user_agent) + + +def test_build_os_other_platform(): + user_agent = UserAgentBuilder( + platform_name="myos", platform_version="0.0.1" + ).build() + assert "os/other md/myos#0.0.1" in str(user_agent) + + +def test_build_arch_adds_md(): + user_agent = UserAgentBuilder(platform_machine="x86_64").build() + assert "md/arch#x86_64" in str(user_agent) + + +def test_build_language_version(): + user_agent = UserAgentBuilder(python_version="3.12").build() + assert "lang/python#3.12" in str(user_agent) + + +def test_build_language_implementation(): + user_agent = UserAgentBuilder(python_implementation="CPython").build() + assert "md/pyimpl#CPython" in str(user_agent) diff --git a/packages/smithy-http/tests/unit/user_agent.py b/packages/smithy-http/tests/unit/test_user_agent.py similarity index 100% rename from packages/smithy-http/tests/unit/user_agent.py rename to packages/smithy-http/tests/unit/test_user_agent.py From 86e461c7220de94b6c7133bf7b53d44055714631 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Mon, 3 Mar 2025 12:18:28 -0800 Subject: [PATCH 16/20] Update packages/smithy-http/src/smithy_http/interceptors/user_agent.py Co-authored-by: Nate Prewitt --- packages/smithy-http/src/smithy_http/interceptors/user_agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/smithy-http/src/smithy_http/interceptors/user_agent.py b/packages/smithy-http/src/smithy_http/interceptors/user_agent.py index 012fc8133..ce61c0b6b 100644 --- a/packages/smithy-http/src/smithy_http/interceptors/user_agent.py +++ b/packages/smithy-http/src/smithy_http/interceptors/user_agent.py @@ -102,7 +102,7 @@ def _build_os_metadata(self) -> list[UserAgentComponent]: * ``os/other`` * ``os/other md/foobar#1.2.3`` """ - if self._platform_name is None or self._platform_name == "": + if self._platform_name in (None, ""): return [UserAgentComponent("os", "other")] plt_name_lower = self._platform_name.lower() From 001313923320b06abdc424131fb7d6d61a85e94a Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Mon, 3 Mar 2025 12:25:08 -0800 Subject: [PATCH 17/20] Update packages/smithy-http/tests/unit/interceptors/test_user_agent.py Co-authored-by: Nate Prewitt --- packages/smithy-http/tests/unit/interceptors/test_user_agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/smithy-http/tests/unit/interceptors/test_user_agent.py b/packages/smithy-http/tests/unit/interceptors/test_user_agent.py index 6a4a14a66..f90b26e00 100644 --- a/packages/smithy-http/tests/unit/interceptors/test_user_agent.py +++ b/packages/smithy-http/tests/unit/interceptors/test_user_agent.py @@ -8,7 +8,7 @@ def test_from_environment(monkeypatch): # type: ignore monkeypatch.setattr(platform, "system", lambda: "Linux") # type: ignore monkeypatch.setattr(platform, "release", lambda: "5.4.228-131.415.AMZN2.X86_64") # type: ignore monkeypatch.setattr(platform, "python_version", lambda: "4.3.2") # type: ignore - monkeypatch.setattr(platform, "python_implementation", lambda: "Cpython") # type: ignore + monkeypatch.setattr(platform, "python_implementation", lambda: "CPython") # type: ignore monkeypatch.setattr(smithy_core, "__version__", "1.2.3") # type: ignore user_agent = str(UserAgentBuilder.from_environment().build()) From 00ce11f274a4dd0b615b06e15cb3ec2d5903b768 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Mon, 3 Mar 2025 12:50:56 -0800 Subject: [PATCH 18/20] PR cleanups --- .../smithy_http/interceptors/user_agent.py | 4 +-- .../unit/interceptors/test_user_agent.py | 31 ++++++++++--------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/packages/smithy-http/src/smithy_http/interceptors/user_agent.py b/packages/smithy-http/src/smithy_http/interceptors/user_agent.py index ce61c0b6b..a7a4feb33 100644 --- a/packages/smithy-http/src/smithy_http/interceptors/user_agent.py +++ b/packages/smithy-http/src/smithy_http/interceptors/user_agent.py @@ -17,7 +17,7 @@ class UserAgentInterceptor(Interceptor[Any, None, HTTPRequest, None]): def read_before_execution( self, context: InterceptorContext[Any, None, None, None] ) -> None: - context.properties["user_agent"] = UserAgentBuilder.from_environment().build() + context.properties["user_agent"] = _UserAgentBuilder.from_environment().build() def modify_before_signing( self, context: InterceptorContext[Any, None, HTTPRequest, None] @@ -42,7 +42,7 @@ def modify_before_signing( _USERAGENT_SDK_NAME = "python" -class UserAgentBuilder: +class _UserAgentBuilder: def __init__( self, *, diff --git a/packages/smithy-http/tests/unit/interceptors/test_user_agent.py b/packages/smithy-http/tests/unit/interceptors/test_user_agent.py index f90b26e00..ec14cbc2c 100644 --- a/packages/smithy-http/tests/unit/interceptors/test_user_agent.py +++ b/packages/smithy-http/tests/unit/interceptors/test_user_agent.py @@ -1,71 +1,74 @@ import platform import smithy_core -from smithy_http.interceptors.user_agent import UserAgentBuilder +from smithy_http.interceptors.user_agent import _UserAgentBuilder # type: ignore def test_from_environment(monkeypatch): # type: ignore monkeypatch.setattr(platform, "system", lambda: "Linux") # type: ignore monkeypatch.setattr(platform, "release", lambda: "5.4.228-131.415.AMZN2.X86_64") # type: ignore + monkeypatch.setattr(platform, "machine", lambda: "x86_64") # type: ignore monkeypatch.setattr(platform, "python_version", lambda: "4.3.2") # type: ignore monkeypatch.setattr(platform, "python_implementation", lambda: "CPython") # type: ignore monkeypatch.setattr(smithy_core, "__version__", "1.2.3") # type: ignore - user_agent = str(UserAgentBuilder.from_environment().build()) + user_agent = str(_UserAgentBuilder.from_environment().build()) assert "python/1.2.3" in user_agent assert "os/linux#5.4.228-131.415.AMZN2.X86_64" in user_agent - assert "md/arch#arm64" in user_agent + assert "md/arch#x86_64" in user_agent assert "lang/python#4.3.2" in user_agent - assert "md/pyimpl#Cpython" in user_agent + assert "md/pyimpl#CPython" in user_agent def test_build_adds_sdk_metadata(): - user_agent = UserAgentBuilder(sdk_version="1.2.3").build() + user_agent = _UserAgentBuilder(sdk_version="1.2.3").build() assert "python/1.2.3" in str(user_agent) def test_build_adds_ua_metadata(): - user_agent = UserAgentBuilder().build() + user_agent = _UserAgentBuilder().build() assert "ua/2.1" in str(user_agent) def test_build_os_defaults_to_other(): - user_agent = UserAgentBuilder().build() + user_agent = _UserAgentBuilder().build() assert "os/other" in str(user_agent) def test_build_os_lowercases_platform(): - user_agent = UserAgentBuilder(platform_name="LINUX").build() + user_agent = _UserAgentBuilder(platform_name="LINUX").build() assert "os/linux" in str(user_agent) def test_build_os_maps_platform_names(): - user_agent = UserAgentBuilder(platform_name="darwin").build() + user_agent = _UserAgentBuilder(platform_name="darwin").build() assert "os/macos" in str(user_agent) def test_build_os_includes_version(): - user_agent = UserAgentBuilder(platform_name="linux", platform_version="5.4").build() + user_agent = _UserAgentBuilder( + platform_name="linux", platform_version="5.4" + ).build() assert "os/linux#5.4" in str(user_agent) def test_build_os_other_platform(): - user_agent = UserAgentBuilder( + user_agent = _UserAgentBuilder( platform_name="myos", platform_version="0.0.1" ).build() assert "os/other md/myos#0.0.1" in str(user_agent) def test_build_arch_adds_md(): - user_agent = UserAgentBuilder(platform_machine="x86_64").build() + user_agent = _UserAgentBuilder(platform_machine="x86_64").build() assert "md/arch#x86_64" in str(user_agent) def test_build_language_version(): - user_agent = UserAgentBuilder(python_version="3.12").build() + user_agent = _UserAgentBuilder(python_version="3.12").build() assert "lang/python#3.12" in str(user_agent) def test_build_language_implementation(): - user_agent = UserAgentBuilder(python_implementation="CPython").build() + user_agent = _UserAgentBuilder(python_implementation="CPython").build() assert "md/pyimpl#CPython" in str(user_agent) From 8ea3bf249f45fd1efc772e8a29788208e055988f Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Mon, 3 Mar 2025 13:05:58 -0800 Subject: [PATCH 19/20] PR updates --- .../smithy_http/interceptors/user_agent.py | 12 +++--- .../unit/interceptors/test_user_agent.py | 43 +++++++++++++------ 2 files changed, 35 insertions(+), 20 deletions(-) diff --git a/packages/smithy-http/src/smithy_http/interceptors/user_agent.py b/packages/smithy-http/src/smithy_http/interceptors/user_agent.py index a7a4feb33..ad58ddcaa 100644 --- a/packages/smithy-http/src/smithy_http/interceptors/user_agent.py +++ b/packages/smithy-http/src/smithy_http/interceptors/user_agent.py @@ -46,12 +46,12 @@ class _UserAgentBuilder: def __init__( self, *, - platform_name: str | None = None, - platform_version: str | None = None, - platform_machine: str | None = None, - python_version: str | None = None, - python_implementation: str | None = None, - sdk_version: str | None = None, + platform_name: str | None, + platform_version: str | None, + platform_machine: str | None, + python_version: str | None, + python_implementation: str | None, + sdk_version: str | None, ) -> None: self._platform_name = platform_name self._platform_version = platform_version diff --git a/packages/smithy-http/tests/unit/interceptors/test_user_agent.py b/packages/smithy-http/tests/unit/interceptors/test_user_agent.py index ec14cbc2c..6f73e887d 100644 --- a/packages/smithy-http/tests/unit/interceptors/test_user_agent.py +++ b/packages/smithy-http/tests/unit/interceptors/test_user_agent.py @@ -20,55 +20,70 @@ def test_from_environment(monkeypatch): # type: ignore assert "md/pyimpl#CPython" in user_agent +defaults = { + "platform_name": None, + "platform_version": None, + "platform_machine": None, + "python_version": None, + "python_implementation": None, + "sdk_version": None, +} + + def test_build_adds_sdk_metadata(): - user_agent = _UserAgentBuilder(sdk_version="1.2.3").build() + args = {"sdk_version": "1.2.3"} + user_agent = _UserAgentBuilder(**{**defaults, **args}).build() assert "python/1.2.3" in str(user_agent) def test_build_adds_ua_metadata(): - user_agent = _UserAgentBuilder().build() + user_agent = _UserAgentBuilder(**defaults).build() assert "ua/2.1" in str(user_agent) def test_build_os_defaults_to_other(): - user_agent = _UserAgentBuilder().build() + user_agent = _UserAgentBuilder(**defaults).build() assert "os/other" in str(user_agent) def test_build_os_lowercases_platform(): - user_agent = _UserAgentBuilder(platform_name="LINUX").build() + args = {"platform_name": "LINUX"} + user_agent = _UserAgentBuilder(**{**defaults, **args}).build() assert "os/linux" in str(user_agent) def test_build_os_maps_platform_names(): - user_agent = _UserAgentBuilder(platform_name="darwin").build() + args = {"platform_name": "darwin"} + user_agent = _UserAgentBuilder(**{**defaults, **args}).build() + assert "os/macos" in str(user_agent) def test_build_os_includes_version(): - user_agent = _UserAgentBuilder( - platform_name="linux", platform_version="5.4" - ).build() + args = {"platform_name": "linux", "platform_version": "5.4"} + user_agent = _UserAgentBuilder(**{**defaults, **args}).build() assert "os/linux#5.4" in str(user_agent) def test_build_os_other_platform(): - user_agent = _UserAgentBuilder( - platform_name="myos", platform_version="0.0.1" - ).build() + args = {"platform_name": "myos", "platform_version": "0.0.1"} + user_agent = _UserAgentBuilder(**{**defaults, **args}).build() assert "os/other md/myos#0.0.1" in str(user_agent) def test_build_arch_adds_md(): - user_agent = _UserAgentBuilder(platform_machine="x86_64").build() + args = {"platform_machine": "x86_64"} + user_agent = _UserAgentBuilder(**{**defaults, **args}).build() assert "md/arch#x86_64" in str(user_agent) def test_build_language_version(): - user_agent = _UserAgentBuilder(python_version="3.12").build() + args = {"python_version": "3.12"} + user_agent = _UserAgentBuilder(**{**defaults, **args}).build() assert "lang/python#3.12" in str(user_agent) def test_build_language_implementation(): - user_agent = _UserAgentBuilder(python_implementation="CPython").build() + args = {"python_implementation": "CPython"} + user_agent = _UserAgentBuilder(**{**defaults, **args}).build() assert "md/pyimpl#CPython" in str(user_agent) From 20b97ed82f4c5379fd1018b46484f91cefed9d6a Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Wed, 5 Mar 2025 09:16:49 -0800 Subject: [PATCH 20/20] PR Cleanups --- .../codegen/generators/ConfigGenerator.java | 4 +-- .../integrations/UserAgentIntegration.java | 26 +++++++++---------- 2 files changed, 14 insertions(+), 16 deletions(-) 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 cd2989d22..1d120d6d9 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 @@ -324,7 +324,7 @@ private void generateConfig(GenerationContext context, PythonWriter writer) { writer.addStdlibImport("dataclasses", "dataclass"); writer.write(""" @dataclass(init=False) - class $T: + class $L: \"""Configuration for $L.\""" ${C|} @@ -340,7 +340,7 @@ def __init__( \""" ${C|} """, - configSymbol, + configSymbol.getName(), context.settings().service().getName(), writer.consumer(w -> writePropertyDeclarations(w, finalProperties)), writer.consumer(w -> writeInitParams(w, finalProperties)), diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/integrations/UserAgentIntegration.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/integrations/UserAgentIntegration.java index f61947548..5d1df673c 100644 --- a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/integrations/UserAgentIntegration.java +++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/integrations/UserAgentIntegration.java @@ -19,20 +19,18 @@ public class UserAgentIntegration implements PythonIntegration { @Override public List getClientPlugins(GenerationContext context) { if (context.applicationProtocol().isHttpProtocol()) { - return List.of( - RuntimeClientPlugin.builder() - .pythonPlugin( - SymbolReference.builder() - .symbol(Symbol.builder() - .namespace( - SmithyPythonDependency.SMITHY_HTTP.packageName() - + ".plugins", - ".") - .name("user_agent_plugin") - .addDependency(SmithyPythonDependency.SMITHY_HTTP) - .build()) - .build()) - .build()); + var userAgentPlugin = SymbolReference + .builder() + .symbol( + Symbol.builder() + .namespace( + SmithyPythonDependency.SMITHY_HTTP.packageName() + ".plugins", + ".") + .name("user_agent_plugin") + .addDependency(SmithyPythonDependency.SMITHY_HTTP) + .build()) + .build(); + return List.of(RuntimeClientPlugin.builder().pythonPlugin(userAgentPlugin).build()); } else { return List.of(); }