From 1c0febf183741b9c6d3c7d78aca05eef62fe5604 Mon Sep 17 00:00:00 2001 From: ubaskota <19787410+ubaskota@users.noreply.github.com> Date: Tue, 10 Mar 2026 22:09:06 -0400 Subject: [PATCH 1/4] Add support for endpoint_url, user_agent_extra, and sdk_ua_app_id --- .../python/aws/codegen/AwsConfiguration.java | 26 ++++++++++++ ...sStandardRegionalEndpointsIntegration.java | 2 + .../aws/codegen/AwsUserAgentIntegration.java | 23 +++-------- .../codegen/generators/ConfigGenerator.java | 16 ++++++++ .../tests/unit/config/test_property.py | 41 +++++++++++++++++++ 5 files changed, 90 insertions(+), 18 deletions(-) diff --git a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsConfiguration.java b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsConfiguration.java index 01529a4ff..c82f078e1 100644 --- a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsConfiguration.java +++ b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsConfiguration.java @@ -60,4 +60,30 @@ private AwsConfiguration() {} .build()) .defaultValue("RetryStrategyOptions(retry_mode=\"standard\")") .build(); + + public static final ConfigProperty USER_AGENT_EXTRA = ConfigProperty.builder() + .name("user_agent_extra") + .type(Symbol.builder().name("str").build()) + .documentation("Additional suffix to be appended to the User-Agent header.") + .nullable(false) + .useDescriptor(true) + .defaultValue("''") + .build(); + + public static final ConfigProperty SDK_UA_APP_ID = ConfigProperty.builder() + .name("sdk_ua_app_id") + .type(Symbol.builder().name("str").build()) + .documentation("A unique and opaque application ID that is appended to the User-Agent header.") + .nullable(false) + .useDescriptor(true) + .defaultValue("''") + .build(); + + public static final ConfigProperty ENDPOINT_URL = ConfigProperty.builder() + .name("endpoint_url") + .type(Symbol.builder().name("str").build()) + .documentation("The endpoint URL to use for requests. If not set, the standard endpoint for the service and region will be used.") + .nullable(true) + .useDescriptor(true) + .build(); } diff --git a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsStandardRegionalEndpointsIntegration.java b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsStandardRegionalEndpointsIntegration.java index c290468b4..e89b640fd 100644 --- a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsStandardRegionalEndpointsIntegration.java +++ b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsStandardRegionalEndpointsIntegration.java @@ -4,6 +4,7 @@ */ package software.amazon.smithy.python.aws.codegen; +import static software.amazon.smithy.python.aws.codegen.AwsConfiguration.ENDPOINT_URL; import static software.amazon.smithy.python.aws.codegen.AwsConfiguration.REGION; import java.util.List; @@ -23,6 +24,7 @@ public List getClientPlugins(GenerationContext context) { return List.of( RuntimeClientPlugin.builder() .addConfigProperty(REGION) + .addConfigProperty(ENDPOINT_URL) .build()); } else { return List.of(); 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 423296913..9dc1d72cb 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 @@ -9,12 +9,14 @@ import software.amazon.smithy.codegen.core.Symbol; import software.amazon.smithy.codegen.core.SymbolReference; import software.amazon.smithy.python.codegen.CodegenUtils; -import software.amazon.smithy.python.codegen.ConfigProperty; import software.amazon.smithy.python.codegen.GenerationContext; import software.amazon.smithy.python.codegen.integrations.PythonIntegration; import software.amazon.smithy.python.codegen.integrations.RuntimeClientPlugin; import software.amazon.smithy.utils.SmithyInternalApi; +import static software.amazon.smithy.python.aws.codegen.AwsConfiguration.SDK_UA_APP_ID; +import static software.amazon.smithy.python.aws.codegen.AwsConfiguration.USER_AGENT_EXTRA; + /** * Adds a runtime plugin to set user agent. */ @@ -36,21 +38,6 @@ def aws_user_agent_plugin(config: $1T): @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(); @@ -86,8 +73,8 @@ public List getClientPlugins(GenerationContext context) { return List.of( RuntimeClientPlugin.builder() - .addConfigProperty(userAgentExtra) - .addConfigProperty(uaAppId) + .addConfigProperty(USER_AGENT_EXTRA) + .addConfigProperty(SDK_UA_APP_ID) .pythonPlugin(userAgentPlugin) .writeAdditionalFiles((c) -> { String filename = "src/%s/%s.py".formatted(moduleName, user_agent_plugin_file); 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 92aa857e5..270795dc3 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 @@ -97,6 +97,22 @@ public final class ConfigGenerator implements Runnable { .build()) .build()) .documentation("The retry strategy or options for configuring retry behavior. Can be either a configured RetryStrategy or RetryStrategyOptions to create one.") + .build(), + ConfigProperty.builder() + .name("user_agent_extra") + .type(Symbol.builder().name("str").build()) + .documentation("Additional suffix to be appended to the User-Agent header.") + .build(), + ConfigProperty.builder() + .name("sdk_ua_app_id") + .type(Symbol.builder().name("str").build()) + .documentation("A unique and opaque application ID that is appended to the User-Agent header.") + .build(), + ConfigProperty.builder() + .name("endpoint_url") + .type(Symbol.builder().name("str").build()) + .documentation("The endpoint URL to use for requests. If not set, the standard endpoint for the service and region will be used.") + .nullable(true) .build()); // This list contains any properties that must be added to any http-based diff --git a/packages/smithy-aws-core/tests/unit/config/test_property.py b/packages/smithy-aws-core/tests/unit/config/test_property.py index 42ea31291..1bdd87db5 100644 --- a/packages/smithy-aws-core/tests/unit/config/test_property.py +++ b/packages/smithy-aws-core/tests/unit/config/test_property.py @@ -96,6 +96,47 @@ def test_different_properties_resolve_independently(self) -> None: assert region == "us-west-2" assert retry_mode == "adaptive" + def test_unresolved_ua_extra_defaults_to_empty_string(self) -> None: + class ConfigWithEmptyDefault: + user_agent_extra = ConfigProperty( + "user_agent_extra", + default_value="", + ) + + def __init__(self, resolver: ConfigResolver) -> None: + self._resolver = resolver + + source = StubSource("environment", {}) + resolver = ConfigResolver(sources=[source]) + config = ConfigWithEmptyDefault(resolver) + + result = config.user_agent_extra + + assert result == "" + assert getattr(config, "_cache_user_agent_extra") == ( + "", + SimpleSource("default"), + ) + + def test_endpoint_url_defaults_to_none(self) -> None: + class ConfigWithNoDefault: + endpoint_url = ConfigProperty("endpoint_url") + + def __init__(self, resolver: ConfigResolver) -> None: + self._resolver = resolver + + source = StubSource("environment", {}) + resolver = ConfigResolver(sources=[source]) + config = ConfigWithNoDefault(resolver) + + result = config.endpoint_url + + assert result is None + assert getattr(config, "_cache_endpoint_url") == ( + None, + SimpleSource("default"), + ) + class TestConfigPropertyValidation: """Test suite for ConfigProperty validation behavior.""" From 7146d4504d27e2bb93128e8ab562469bee805826 Mon Sep 17 00:00:00 2001 From: ubaskota <19787410+ubaskota@users.noreply.github.com> Date: Fri, 13 Mar 2026 01:22:23 -0400 Subject: [PATCH 2/4] Address reviews --- .../python/aws/codegen/AwsConfiguration.java | 26 ------------- ...sStandardRegionalEndpointsIntegration.java | 2 - .../aws/codegen/AwsUserAgentIntegration.java | 29 ++++++++++++--- .../codegen/generators/ConfigGenerator.java | 16 -------- .../src/smithy_aws_core/config/validators.py | 30 +++++++++++++++ .../tests/unit/config/test_property.py | 37 +++---------------- .../tests/unit/config/test_validators.py | 25 +++++++++++++ 7 files changed, 85 insertions(+), 80 deletions(-) diff --git a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsConfiguration.java b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsConfiguration.java index c82f078e1..01529a4ff 100644 --- a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsConfiguration.java +++ b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsConfiguration.java @@ -60,30 +60,4 @@ private AwsConfiguration() {} .build()) .defaultValue("RetryStrategyOptions(retry_mode=\"standard\")") .build(); - - public static final ConfigProperty USER_AGENT_EXTRA = ConfigProperty.builder() - .name("user_agent_extra") - .type(Symbol.builder().name("str").build()) - .documentation("Additional suffix to be appended to the User-Agent header.") - .nullable(false) - .useDescriptor(true) - .defaultValue("''") - .build(); - - public static final ConfigProperty SDK_UA_APP_ID = ConfigProperty.builder() - .name("sdk_ua_app_id") - .type(Symbol.builder().name("str").build()) - .documentation("A unique and opaque application ID that is appended to the User-Agent header.") - .nullable(false) - .useDescriptor(true) - .defaultValue("''") - .build(); - - public static final ConfigProperty ENDPOINT_URL = ConfigProperty.builder() - .name("endpoint_url") - .type(Symbol.builder().name("str").build()) - .documentation("The endpoint URL to use for requests. If not set, the standard endpoint for the service and region will be used.") - .nullable(true) - .useDescriptor(true) - .build(); } diff --git a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsStandardRegionalEndpointsIntegration.java b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsStandardRegionalEndpointsIntegration.java index e89b640fd..c290468b4 100644 --- a/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsStandardRegionalEndpointsIntegration.java +++ b/codegen/aws/core/src/main/java/software/amazon/smithy/python/aws/codegen/AwsStandardRegionalEndpointsIntegration.java @@ -4,7 +4,6 @@ */ package software.amazon.smithy.python.aws.codegen; -import static software.amazon.smithy.python.aws.codegen.AwsConfiguration.ENDPOINT_URL; import static software.amazon.smithy.python.aws.codegen.AwsConfiguration.REGION; import java.util.List; @@ -24,7 +23,6 @@ public List getClientPlugins(GenerationContext context) { return List.of( RuntimeClientPlugin.builder() .addConfigProperty(REGION) - .addConfigProperty(ENDPOINT_URL) .build()); } else { return List.of(); 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 9dc1d72cb..aaf0ad0ca 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 @@ -9,14 +9,12 @@ import software.amazon.smithy.codegen.core.Symbol; import software.amazon.smithy.codegen.core.SymbolReference; import software.amazon.smithy.python.codegen.CodegenUtils; +import software.amazon.smithy.python.codegen.ConfigProperty; import software.amazon.smithy.python.codegen.GenerationContext; import software.amazon.smithy.python.codegen.integrations.PythonIntegration; import software.amazon.smithy.python.codegen.integrations.RuntimeClientPlugin; import software.amazon.smithy.utils.SmithyInternalApi; -import static software.amazon.smithy.python.aws.codegen.AwsConfiguration.SDK_UA_APP_ID; -import static software.amazon.smithy.python.aws.codegen.AwsConfiguration.USER_AGENT_EXTRA; - /** * Adds a runtime plugin to set user agent. */ @@ -38,6 +36,27 @@ def aws_user_agent_plugin(config: $1T): @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) + .useDescriptor(true) + .validator(Symbol.builder() + .name("validate_and_sanitize_ua_string") + .namespace("smithy_aws_core.config.validators", ".") + .addDependency(AwsPythonDependency.SMITHY_AWS_CORE) + .build()) + .build(); + final String user_agent_plugin_file = "user_agent"; final String moduleName = context.settings().moduleName(); @@ -73,8 +92,8 @@ public List getClientPlugins(GenerationContext context) { return List.of( RuntimeClientPlugin.builder() - .addConfigProperty(USER_AGENT_EXTRA) - .addConfigProperty(SDK_UA_APP_ID) + .addConfigProperty(userAgentExtra) + .addConfigProperty(uaAppId) .pythonPlugin(userAgentPlugin) .writeAdditionalFiles((c) -> { String filename = "src/%s/%s.py".formatted(moduleName, user_agent_plugin_file); 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 270795dc3..92aa857e5 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 @@ -97,22 +97,6 @@ public final class ConfigGenerator implements Runnable { .build()) .build()) .documentation("The retry strategy or options for configuring retry behavior. Can be either a configured RetryStrategy or RetryStrategyOptions to create one.") - .build(), - ConfigProperty.builder() - .name("user_agent_extra") - .type(Symbol.builder().name("str").build()) - .documentation("Additional suffix to be appended to the User-Agent header.") - .build(), - ConfigProperty.builder() - .name("sdk_ua_app_id") - .type(Symbol.builder().name("str").build()) - .documentation("A unique and opaque application ID that is appended to the User-Agent header.") - .build(), - ConfigProperty.builder() - .name("endpoint_url") - .type(Symbol.builder().name("str").build()) - .documentation("The endpoint URL to use for requests. If not set, the standard endpoint for the service and region will be used.") - .nullable(true) .build()); // This list contains any properties that must be added to any http-based diff --git a/packages/smithy-aws-core/src/smithy_aws_core/config/validators.py b/packages/smithy-aws-core/src/smithy_aws_core/config/validators.py index 41b670e02..3410bc8fe 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/config/validators.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/config/validators.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 import re +from string import ascii_letters, digits from typing import Any, get_args from smithy_core.interfaces.retries import RetryStrategy @@ -9,6 +10,8 @@ from smithy_aws_core.config.source_info import SourceInfo +_USERAGENT_ALLOWED_CHARACTERS = ascii_letters + digits + "!#$%&'*+-.^_`|~/" + class ConfigValidationError(ValueError): """Raised when a configuration value fails validation.""" @@ -144,3 +147,30 @@ def validate_retry_strategy( f"retry_strategy must be RetryStrategy or RetryStrategyOptions, got {type(value).__name__}", source, ) + + +def validate_and_sanitize_ua_string( + value: Any, source: SourceInfo | None = None +) -> str | None: + """Validate and sanitize a User-Agent string component. + + Replaces all disallowed characters with a hyphen. Allowed characters are + ASCII alphanumerics and "!#$%&'*+-.^_`|~/". + + :param value: The UA string value to sanitize + :param source: The source that provided this value + + :returns: The sanitized UA string or None if value is None + + :raises ConfigValidationError: If the value is not a string + """ + if value is None: + return None + if not isinstance(value, str): + raise ConfigValidationError( + "sdk_ua_app_id", + value, + f"UA string must be a string, got {type(value).__name__}", + source, + ) + return "".join(c if c in _USERAGENT_ALLOWED_CHARACTERS else "-" for c in value) diff --git a/packages/smithy-aws-core/tests/unit/config/test_property.py b/packages/smithy-aws-core/tests/unit/config/test_property.py index 1bdd87db5..6fe1c4946 100644 --- a/packages/smithy-aws-core/tests/unit/config/test_property.py +++ b/packages/smithy-aws-core/tests/unit/config/test_property.py @@ -96,31 +96,9 @@ def test_different_properties_resolve_independently(self) -> None: assert region == "us-west-2" assert retry_mode == "adaptive" - def test_unresolved_ua_extra_defaults_to_empty_string(self) -> None: - class ConfigWithEmptyDefault: - user_agent_extra = ConfigProperty( - "user_agent_extra", - default_value="", - ) - - def __init__(self, resolver: ConfigResolver) -> None: - self._resolver = resolver - - source = StubSource("environment", {}) - resolver = ConfigResolver(sources=[source]) - config = ConfigWithEmptyDefault(resolver) - - result = config.user_agent_extra - - assert result == "" - assert getattr(config, "_cache_user_agent_extra") == ( - "", - SimpleSource("default"), - ) - - def test_endpoint_url_defaults_to_none(self) -> None: + def test_unresolved_ua_app_id_defaults_to_none(self) -> None: class ConfigWithNoDefault: - endpoint_url = ConfigProperty("endpoint_url") + sdk_ua_app_id = ConfigProperty("sdk_ua_app_id") def __init__(self, resolver: ConfigResolver) -> None: self._resolver = resolver @@ -129,10 +107,10 @@ def __init__(self, resolver: ConfigResolver) -> None: resolver = ConfigResolver(sources=[source]) config = ConfigWithNoDefault(resolver) - result = config.endpoint_url + result = config.sdk_ua_app_id assert result is None - assert getattr(config, "_cache_endpoint_url") == ( + assert getattr(config, "_cache_sdk_ua_app_id") == ( None, SimpleSource("default"), ) @@ -148,7 +126,6 @@ def _create_config_with_validator( class ConfigWithValidator: region = ConfigProperty("region", validator=validator) - retry_strategy = ConfigProperty("retry_strategy", validator=validator) def __init__(self, resolver: ConfigResolver) -> None: self._resolver = resolver @@ -194,9 +171,7 @@ class ConfigWithComplexResolver: retry_strategy = ConfigProperty( "retry_strategy", resolver_func=mock_resolver, - default_value=RetryStrategyOptions( - retry_mode="standard", max_attempts=3 - ), + default_value=RetryStrategyOptions(retry_mode="standard"), ) def __init__(self, resolver: ConfigResolver) -> None: @@ -212,7 +187,7 @@ def __init__(self, resolver: ConfigResolver) -> None: assert isinstance(result, RetryStrategyOptions) assert result.retry_mode == "standard" - assert result.max_attempts == 3 + assert result.max_attempts is None assert source_info == SimpleSource("default") def test_validator_not_called_on_cached_access(self) -> None: diff --git a/packages/smithy-aws-core/tests/unit/config/test_validators.py b/packages/smithy-aws-core/tests/unit/config/test_validators.py index 546af5365..85b03d0b6 100644 --- a/packages/smithy-aws-core/tests/unit/config/test_validators.py +++ b/packages/smithy-aws-core/tests/unit/config/test_validators.py @@ -5,6 +5,7 @@ import pytest from smithy_aws_core.config.validators import ( ConfigValidationError, + validate_and_sanitize_ua_string, validate_max_attempts, validate_region, validate_retry_mode, @@ -49,3 +50,27 @@ def test_invalid_retry_mode_error_message(self) -> None: "Invalid value for 'retry_mode': 'random_mode'. retry_mode must be one " "of ('standard',), got random_mode" in str(exc_info.value) ) + + +class TestValidateUaString: + def test_allows_alphanumeric(self) -> None: + assert validate_and_sanitize_ua_string("abc123") == "abc123" + + def test_none_returns_none(self) -> None: + assert validate_and_sanitize_ua_string(None) is None + + def test_allows_spec_special_chars(self) -> None: + assert validate_and_sanitize_ua_string("!#$%&'*+-.^_`|~/") == "!#$%&'*+-.^_`|~/" + + def test_sanitizes_parentheses(self) -> None: + result = validate_and_sanitize_ua_string("Java_HotSpot_(TM)_64-Bit_Server_VM") + assert result == "Java_HotSpot_-TM-_64-Bit_Server_VM" + + def test_empty_string_passthrough(self) -> None: + assert validate_and_sanitize_ua_string("") == "" + + def test_rejects_non_string(self) -> None: + with pytest.raises(ConfigValidationError) as exc_info: + validate_and_sanitize_ua_string(123) + + assert exc_info.value.key == "sdk_ua_app_id" From f55cdbfa67bfb8dd27f5ee78155050710fb407b3 Mon Sep 17 00:00:00 2001 From: ubaskota <19787410+ubaskota@users.noreply.github.com> Date: Fri, 13 Mar 2026 15:29:33 -0400 Subject: [PATCH 3/4] Address reviews --- .../aws/codegen/AwsUserAgentIntegration.java | 2 +- .../src/smithy_aws_core/config/validators.py | 18 +++++------------- .../tests/unit/config/test_validators.py | 19 ++++++------------- 3 files changed, 12 insertions(+), 27 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 aaf0ad0ca..df703730a 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 @@ -51,7 +51,7 @@ public List getClientPlugins(GenerationContext context) { .nullable(true) .useDescriptor(true) .validator(Symbol.builder() - .name("validate_and_sanitize_ua_string") + .name("validate_ua_string") .namespace("smithy_aws_core.config.validators", ".") .addDependency(AwsPythonDependency.SMITHY_AWS_CORE) .build()) diff --git a/packages/smithy-aws-core/src/smithy_aws_core/config/validators.py b/packages/smithy-aws-core/src/smithy_aws_core/config/validators.py index 3410bc8fe..a2766ecbe 100644 --- a/packages/smithy-aws-core/src/smithy_aws_core/config/validators.py +++ b/packages/smithy-aws-core/src/smithy_aws_core/config/validators.py @@ -2,7 +2,6 @@ # SPDX-License-Identifier: Apache-2.0 import re -from string import ascii_letters, digits from typing import Any, get_args from smithy_core.interfaces.retries import RetryStrategy @@ -10,8 +9,6 @@ from smithy_aws_core.config.source_info import SourceInfo -_USERAGENT_ALLOWED_CHARACTERS = ascii_letters + digits + "!#$%&'*+-.^_`|~/" - class ConfigValidationError(ValueError): """Raised when a configuration value fails validation.""" @@ -149,18 +146,13 @@ def validate_retry_strategy( ) -def validate_and_sanitize_ua_string( - value: Any, source: SourceInfo | None = None -) -> str | None: - """Validate and sanitize a User-Agent string component. - - Replaces all disallowed characters with a hyphen. Allowed characters are - ASCII alphanumerics and "!#$%&'*+-.^_`|~/". +def validate_ua_string(value: Any, source: SourceInfo | None = None) -> str | None: + """Validate a User-Agent string component. - :param value: The UA string value to sanitize + :param value: The UA string value to validate :param source: The source that provided this value - :returns: The sanitized UA string or None if value is None + :returns: The UA string or None if value is None :raises ConfigValidationError: If the value is not a string """ @@ -173,4 +165,4 @@ def validate_and_sanitize_ua_string( f"UA string must be a string, got {type(value).__name__}", source, ) - return "".join(c if c in _USERAGENT_ALLOWED_CHARACTERS else "-" for c in value) + return value diff --git a/packages/smithy-aws-core/tests/unit/config/test_validators.py b/packages/smithy-aws-core/tests/unit/config/test_validators.py index 85b03d0b6..dbdbb7b60 100644 --- a/packages/smithy-aws-core/tests/unit/config/test_validators.py +++ b/packages/smithy-aws-core/tests/unit/config/test_validators.py @@ -5,10 +5,10 @@ import pytest from smithy_aws_core.config.validators import ( ConfigValidationError, - validate_and_sanitize_ua_string, validate_max_attempts, validate_region, validate_retry_mode, + validate_ua_string, ) @@ -53,24 +53,17 @@ def test_invalid_retry_mode_error_message(self) -> None: class TestValidateUaString: - def test_allows_alphanumeric(self) -> None: - assert validate_and_sanitize_ua_string("abc123") == "abc123" + def test_allows_string(self) -> None: + assert validate_ua_string("abc123") == "abc123" def test_none_returns_none(self) -> None: - assert validate_and_sanitize_ua_string(None) is None - - def test_allows_spec_special_chars(self) -> None: - assert validate_and_sanitize_ua_string("!#$%&'*+-.^_`|~/") == "!#$%&'*+-.^_`|~/" - - def test_sanitizes_parentheses(self) -> None: - result = validate_and_sanitize_ua_string("Java_HotSpot_(TM)_64-Bit_Server_VM") - assert result == "Java_HotSpot_-TM-_64-Bit_Server_VM" + assert validate_ua_string(None) is None def test_empty_string_passthrough(self) -> None: - assert validate_and_sanitize_ua_string("") == "" + assert validate_ua_string("") == "" def test_rejects_non_string(self) -> None: with pytest.raises(ConfigValidationError) as exc_info: - validate_and_sanitize_ua_string(123) + validate_ua_string(123) assert exc_info.value.key == "sdk_ua_app_id" From 10926727f3164226a11f7c5c662f9468068a788f Mon Sep 17 00:00:00 2001 From: ubaskota <19787410+ubaskota@users.noreply.github.com> Date: Fri, 13 Mar 2026 17:02:17 -0400 Subject: [PATCH 4/4] Address reviews --- .../tests/unit/config/test_property.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/packages/smithy-aws-core/tests/unit/config/test_property.py b/packages/smithy-aws-core/tests/unit/config/test_property.py index 6fe1c4946..4246d0602 100644 --- a/packages/smithy-aws-core/tests/unit/config/test_property.py +++ b/packages/smithy-aws-core/tests/unit/config/test_property.py @@ -96,25 +96,6 @@ def test_different_properties_resolve_independently(self) -> None: assert region == "us-west-2" assert retry_mode == "adaptive" - def test_unresolved_ua_app_id_defaults_to_none(self) -> None: - class ConfigWithNoDefault: - sdk_ua_app_id = ConfigProperty("sdk_ua_app_id") - - def __init__(self, resolver: ConfigResolver) -> None: - self._resolver = resolver - - source = StubSource("environment", {}) - resolver = ConfigResolver(sources=[source]) - config = ConfigWithNoDefault(resolver) - - result = config.sdk_ua_app_id - - assert result is None - assert getattr(config, "_cache_sdk_ua_app_id") == ( - None, - SimpleSource("default"), - ) - class TestConfigPropertyValidation: """Test suite for ConfigProperty validation behavior."""