From cb47f65a56f588dfca5f7fe705395927cdcb655e Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Tue, 25 Feb 2025 10:48:22 -0800 Subject: [PATCH 1/4] 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 2/4] 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 3/4] 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 4/4] 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