diff --git a/README.md b/README.md index d5a6bd9..feec191 100644 --- a/README.md +++ b/README.md @@ -214,7 +214,6 @@ config = ( | `timeouts` | `TimeoutConfig` | discovery 5s, connect 5s, read 30s | Discovery and SDK per-attempt timeouts | | `aws_region` | `str` | `"us-east-1"` | Region placeholder required by the SDK | | `user_agent` | str, callable, or `None` | `alternator-client-python/` | Final User-Agent; `None` omits the wire header | -| `sdk_config_customizer` | callable | `None` | Callback for safe SDK config kwargs adjustments | | `active_refresh_interval_ms` | `int` | `1000` | Node refresh interval when active | | `idle_refresh_interval_ms` | `int` | `60000` | Node refresh interval when idle | @@ -779,9 +778,13 @@ client = create_client(config) ``` The client still owns the SDK config object, endpoint routing, and the final -wire `User-Agent` header. Python botocore does not expose direct knobs for max -idle connections, max idle connections per host, or idle connection timeout; -tune `max_pool_connections`, retries, and timeouts instead. +wire `User-Agent` header. Use typed Alternator config fields for SDK transport +settings: `RetryConfig` for retry behavior, `TimeoutConfig` for connect/read +timeouts, `max_pool_connections` for pool sizing, `aws_region` for the SDK +region placeholder, and `TLS` for client certificates. Python botocore does not +expose direct knobs for max idle connections, max idle connections per host, or +idle connection timeout; tune `max_pool_connections`, retries, and timeouts +instead. ## Production Recommendations diff --git a/alternator/__init__.py b/alternator/__init__.py index 0e8d452..2cf21a1 100644 --- a/alternator/__init__.py +++ b/alternator/__init__.py @@ -106,7 +106,6 @@ ResponseCompression, RetryConfig, RetryMode, - SDKConfigCustomizer, TimeoutConfig, TlsConfig, TlsSessionCacheConfig, @@ -160,7 +159,6 @@ "NodeListPollingConfig", "RetryConfig", "RetryMode", - "SDKConfigCustomizer", "TimeoutConfig", "TlsConfig", "TlsSessionCacheConfig", diff --git a/alternator/config.py b/alternator/config.py index d443d54..fc98a8a 100644 --- a/alternator/config.py +++ b/alternator/config.py @@ -17,7 +17,6 @@ logger = logging.getLogger("alternator") -SDKConfigCustomizer = Callable[[dict[str, Any]], None] SDKClientCert = str | tuple[str, str] UserAgentCustomizer = Callable[[str], str] UserAgent = str | UserAgentCustomizer @@ -403,7 +402,6 @@ class Config: # SDK settings aws_region: str = "us-east-1" user_agent: UserAgent | None = DEFAULT_USER_AGENT - sdk_config_customizer: SDKConfigCustomizer | None = None def __post_init__(self) -> None: """Validate configuration values.""" @@ -473,7 +471,6 @@ def __init__(self) -> None: self._timeouts = TimeoutConfig() self._aws_region = "us-east-1" self._user_agent: UserAgent | None = DEFAULT_USER_AGENT - self._sdk_config_customizer: SDKConfigCustomizer | None = None def with_seeds(self, *hosts: str) -> AlternatorConfigBuilder: """Add seed hosts for node discovery.""" @@ -628,14 +625,6 @@ def with_user_agent( self._user_agent = user_agent return self - def with_sdk_config_customizer( - self, - customizer: SDKConfigCustomizer | None, - ) -> AlternatorConfigBuilder: - """Set a callback that can adjust generated SDK config kwargs.""" - self._sdk_config_customizer = customizer - return self - def build(self) -> Config: """Build the configuration object.""" from alternator.core.routing_scope import ClusterScope @@ -656,7 +645,6 @@ def build(self) -> Config: timeouts=self._timeouts, aws_region=self._aws_region, user_agent=self._user_agent, - sdk_config_customizer=self._sdk_config_customizer, ) @@ -673,8 +661,6 @@ def build_sdk_config_kwargs(config: Config) -> dict[str, Any]: } if config.scheme == "https" and config.tls.sdk_client_cert is not None: kwargs["client_cert"] = config.tls.sdk_client_cert - if config.sdk_config_customizer is not None: - config.sdk_config_customizer(kwargs) if config.user_agent is not None: kwargs["user_agent"] = _resolve_user_agent(config.user_agent) else: diff --git a/docs/CAPABILITY_MATRIX.md b/docs/CAPABILITY_MATRIX.md index 1d0fcb0..7021633 100644 --- a/docs/CAPABILITY_MATRIX.md +++ b/docs/CAPABILITY_MATRIX.md @@ -20,7 +20,7 @@ and intentionally deferred behavior. | TLS server trust | Supported | Existing API | System CA, custom CA, hostname verification, and trust-all mode exist. | | TLS client certificates | Supported | [#38](https://github.com/scylladb/alternator-client-python/issues/38) | mTLS certificate/key paths are loaded into discovery SSL contexts and SDK `client_cert` config. | | TLS key log file | Supported | [#38](https://github.com/scylladb/alternator-client-python/issues/38) | Debug-only key log file paths are applied to SSL contexts when supported by the runtime. | -| Transport and SDK config knobs | Supported | [#34](https://github.com/scylladb/alternator-client-python/issues/34) | Retry, pool, connect/read timeout, region, and SDK config customizer settings are wired into sync and async SDK clients. | +| Transport and SDK config knobs | Supported | [#34](https://github.com/scylladb/alternator-client-python/issues/34), [#89](https://github.com/scylladb/alternator-client-python/issues/89) | Retry, pool, connect/read timeout, region, TLS client certificates, and User-Agent settings have typed Alternator config fields. The client does not expose raw SDK config mutation. | | Key route affinity | Supported | [#23](https://github.com/scylladb/alternator-client-python/issues/23) | RMW detection, single-write affinity, and BatchWriteItem preferred-node voting are implemented with fallback on missing or ambiguous routing data. | | Helper lifecycle facade | Supported | [#33](https://github.com/scylladb/alternator-client-python/issues/33) | `Helper` and `AsyncHelper` expose lifecycle, node inspection, topology checks, and partition-key diagnostics. | | Compatibility and release decisions | Supported | [#39](https://github.com/scylladb/alternator-client-python/issues/39) | Decision record lives in [docs/COMPATIBILITY_AND_RELEASE.md](COMPATIBILITY_AND_RELEASE.md). | diff --git a/docs/RELEASE_NOTES.md b/docs/RELEASE_NOTES.md index 6d5e787..a818c07 100644 --- a/docs/RELEASE_NOTES.md +++ b/docs/RELEASE_NOTES.md @@ -15,7 +15,8 @@ authority for compatibility decisions. `without_fallback(...)`. - Added topology validation helpers for configured datacenter/rack scopes. - Added transport configuration for SDK retries, connect/read timeouts, - connection pool size, region placeholder, and SDK config customization. + connection pool size, region placeholder, TLS client certificates, and + User-Agent customization. - Added Alternator-specific `User-Agent` control with `Config.user_agent`. By default, requests send the Alternator Python client identity; callers can pass `None` to remove the wire header, a string to set a final value, or a @@ -51,8 +52,9 @@ authority for compatibility decisions. For `BatchWriteItem`, valid put/delete entries vote for their preferred node. Tied votes, missing partition-key metadata, unsupported key values, no active nodes, or no eligible votes fall back to normal routing. -- `sdk_config_customizer` can adjust supported SDK config kwargs, but the client - still owns endpoint routing and reapplies auth-managed signature settings. +- The client owns the SDK config object, endpoint routing, auth-managed + signature settings, TLS client certificate settings, retries, timeouts, and + final wire `User-Agent` handling. - TLS key logs contain traffic decryption material and should only be used in protected debugging environments. diff --git a/tests/unit/test_boto_config.py b/tests/unit/test_boto_config.py index 8295306..7924095 100644 --- a/tests/unit/test_boto_config.py +++ b/tests/unit/test_boto_config.py @@ -142,61 +142,6 @@ def test_user_agent_string_must_be_non_empty(self) -> None: with pytest.raises(ValueError, match="user_agent"): _create_boto_config(config, auth_enabled=False) - def test_sdk_config_customizer_can_adjust_safe_fields(self) -> None: - """SDK config customizer can adjust generated config kwargs.""" - - def customize(kwargs: dict[str, object]) -> None: - kwargs["connect_timeout"] = 9.0 - - config = self._make_config(sdk_config_customizer=customize) - boto_config = _create_boto_config(config, auth_enabled=False) - - assert boto_config.connect_timeout == 9.0 - assert boto_config.user_agent == DEFAULT_USER_AGENT - - def test_user_agent_none_overrides_sdk_config_customizer(self) -> None: - """The typed user-agent API removes generic customizer user-agent output.""" - - def customize(kwargs: dict[str, object]) -> None: - kwargs["user_agent"] = "legacy-customizer/1.0" - - config = self._make_config( - sdk_config_customizer=customize, - user_agent=None, - ) - boto_config = _create_boto_config(config, auth_enabled=False) - - assert boto_config.user_agent is None - assert "user_agent" not in boto_config._user_provided_options - - def test_user_agent_callback_overrides_sdk_config_customizer(self) -> None: - """The typed user-agent API wins over the generic SDK customizer.""" - - def customize(kwargs: dict[str, object]) -> None: - kwargs["user_agent"] = "legacy-customizer/1.0" - - config = self._make_config( - sdk_config_customizer=customize, - user_agent=lambda default: f"orders-service {default}", - ) - boto_config = _create_boto_config(config, auth_enabled=False) - - assert boto_config.user_agent == f"orders-service {DEFAULT_USER_AGENT}" - - def test_sdk_config_customizer_cannot_override_signature(self) -> None: - """Auth-managed signature settings override customizer changes.""" - - def customize(kwargs: dict[str, object]) -> None: - kwargs["signature_version"] = "v4" - - config = self._make_config(sdk_config_customizer=customize) - - unsigned_config = _create_boto_config(config, auth_enabled=False) - signed_config = _create_boto_config(config, auth_enabled=True) - - assert unsigned_config.signature_version is UNSIGNED - assert "signature_version" not in signed_config._user_provided_options - def test_client_cert_propagated_for_https(self) -> None: """TLS client certificate settings flow into BotoConfig.""" config = self._make_config( diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index a1a6ce6..9a5f945 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -94,7 +94,6 @@ def test_default_values(self) -> None: assert config.node_list_polling.idle_interval_ms == 60000 assert config.aws_region == "us-east-1" assert config.user_agent.startswith("alternator-client-python/") - assert config.sdk_config_customizer is None assert isinstance(config.routing_scope, ClusterScope) @@ -437,21 +436,6 @@ def test_build_with_user_agent_none(self) -> None: ) assert config.user_agent is None - def test_build_with_sdk_config_customizer(self) -> None: - """Test building config with SDK config customizer.""" - - def customize(kwargs: dict[str, object]) -> None: - kwargs["user_agent"] = "test" - - config = ( - AlternatorConfigBuilder() - .with_seeds("localhost") - .with_port(8000) - .with_sdk_config_customizer(customize) - .build() - ) - assert config.sdk_config_customizer is customize - class TestTlsConfig: """Tests for TLS factory methods."""