Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<version>` | 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 |

Expand Down Expand Up @@ -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

Expand Down
2 changes: 0 additions & 2 deletions alternator/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,6 @@
ResponseCompression,
RetryConfig,
RetryMode,
SDKConfigCustomizer,
TimeoutConfig,
TlsConfig,
TlsSessionCacheConfig,
Expand Down Expand Up @@ -160,7 +159,6 @@
"NodeListPollingConfig",
"RetryConfig",
"RetryMode",
"SDKConfigCustomizer",
"TimeoutConfig",
"TlsConfig",
"TlsSessionCacheConfig",
Expand Down
14 changes: 0 additions & 14 deletions alternator/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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
Expand All @@ -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,
)


Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion docs/CAPABILITY_MATRIX.md
Original file line number Diff line number Diff line change
Expand Up @@ -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). |
Expand Down
8 changes: 5 additions & 3 deletions docs/RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand Down
55 changes: 0 additions & 55 deletions tests/unit/test_boto_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
16 changes: 0 additions & 16 deletions tests/unit/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down Expand Up @@ -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."""
Expand Down
Loading