diff --git a/litellm-rust/Cargo.lock b/litellm-rust/Cargo.lock index febcd1da759..0b49deedd0f 100644 --- a/litellm-rust/Cargo.lock +++ b/litellm-rust/Cargo.lock @@ -4465,6 +4465,7 @@ dependencies = [ "litellm-redis-types", "litellm-secret-value", "rstest", + "serde", "serde_json", "strum", "thiserror 2.0.19", diff --git a/litellm-rust/crates/redis-resolve/AGENTS.md b/litellm-rust/crates/redis-resolve/AGENTS.md index 246e2158b44..078d168d2ce 100644 --- a/litellm-rust/crates/redis-resolve/AGENTS.md +++ b/litellm-rust/crates/redis-resolve/AGENTS.md @@ -3,7 +3,8 @@ - The boundary is merged redis-py kwargs in, `litellm_redis_types::Resolution` out. `interpret` takes the dict a host already merged the way `_merge_redis_kwargs` in `litellm/_redis.py` does, with its `REDIS_*` values and `get_secret` fallbacks filled, and stands in for redis-py's constructors and `from_url`. It never reads the environment or a secret, so both hosts share it. Every `Entry` keeps the source a warning or error names, which `kwargs` sets to the kwarg name and the gateway merge sets to `REDIS_` or the fallback name - The `Resolution` holds the target, the auth and the timeouts Rust models, every other plain setting verbatim in `passthrough`, the names of object-valued settings in `host_objects`, and every setting the Rust client ignores as an `Ignored` with its source and consequence, which the host logs. A host passes its `credential_provider` object as a `ProviderHandle`, and a `credential_provider` key without one is `RedisError::ProviderWithoutHandle`. Azure's username is read from `os.environ` by Python, so `interpret` leaves `AzureAdAuth::username` unset and the host fills it in - `options.rs` reads the kwargs into the typed `Options` and splits the unmodeled settings into passthrough and host objects and collects ignored ones. `endpoint.rs`, `auth.rs` and `tls.rs` pick the target, the credentials and TLS from `Options` alone -- Rust never introspects redis-py. Every Redis setting Python reads is one row in `SETTINGS` in `src/keys.rs`: how Rust handles it (`Read`, `Warn` or `Reject`), whether `_get_redis_env_kwarg_mapping` reads it as `REDIS_`, and the names `_merge_redis_kwargs` falls back to. The last two columns are only read by the gateway merge, and they stay on the row so there is one table. A `Read` row is also a field of `Options`. A kwarg with no row goes to `passthrough`, or to `host_objects` if `HOST_OBJECTS` lists it, without a warning. `redis-py-kwargs.json` records which redis-py version accepts each kwarg next to its row, so check it when adding or changing a row +- Rust never introspects redis-py. Every Redis kwarg redis-py or `litellm/_redis.py` accepts is one row in `SETTINGS` in `src/keys.rs`, and that row is the whole answer for the kwarg: its `Handling` (`Read`, `Warn` with the consequence the warning names, or `Reject`), its `Shape` (plain data lands in `passthrough`, a Python object in `host_objects`), whether a url query carries it (`FromUrl`), whether `_get_redis_env_kwarg_mapping` reads it as `REDIS_`, and the names `_merge_redis_kwargs` falls back to. The last two columns are only read by the gateway merge, and they stay on the row so there is one table. A `Read` row is a field of `Options`, which an inline test pins. A kwarg with no row is not a Redis setting, so it passes through without a warning +- `redis-py-kwargs.json` holds the Python facts each row answers: which redis-py versions name the kwarg in which signature, and whether Python reads its `REDIS_*` name under the locked redis-py. `test_redis_py_kwargs_catalog_matches_the_installed_redis_py` in `tests/unit/test_redis.py` checks it against each version the `redis-compat` CI matrix installs, and `tests/catalog.rs` fails until `SETTINGS` has exactly one row per kwarg in it with the same `environment`. A redis-py bump that adds a kwarg therefore cannot land without a row deciding how Rust handles it - The contract is `resolves_like_python_redis` in `tests/interpret.rs` and its mirror `test_redis_settings_resolve_like_the_rust_resolver_expects` in `tests/unit/test_redis.py`. Each case holds the merged dict, the sources the merge gave it, and the expected view written next to it. The Python side asserts that `_merge_redis_kwargs` returns that merged dict and reads the effective redis-py 5.3.1 connection objects without connecting, and `merges_like_python_redis` in `gateway/src/redis/surface_tests.rs` pins the same merged dicts from the Rust merge. A new rule is a new case in every file, never a generated fixture. Python and Rust pods share one Redis during the switch, so a change here is a change to a wire contract - A url carries its own TLS. redis-py's url allowlist is the plain `Connection` signature, so every `ssl_*` setting outside the url is dropped, and a credential provider strips the url's userinfo. A `rediss://` url's `ssl_cert_reqs`, `ssl_ca_certs`, `ssl_ca_data`, `ssl_ca_path`, `ssl_certfile`, `ssl_keyfile`, `ssl_password` and `ssl_check_hostname` query parameters are read like `parse_url` reads them - TLS options and `db` are only read for the endpoint that uses them, so a bad `ssl_cert_reqs` never fails a url target. A cluster with a non-zero `db`, and Sentinel with a `host` or `port`, are errors because redis-py raises building those clients @@ -11,12 +12,13 @@ - Precedence matches Python. The target is cluster, then URL, then Sentinel (`sentinel_nodes` plus `service_name`), then host. Auth is a `credential_provider` handle, then GCP, then Azure, then AWS, and the first one wins with a warning - Numbers are read with `litellm_python_compat::number`, so whitespace and digit underscores parse like Python's `int()` and `float()`. A value they cannot parse (`port: abc`, `socket_timeout: slow`) is dropped with a warning and the default is used, like `_coerce_redis_kwargs_types`. A number that parses but is out of range (negative seconds, a port above 65535) fails with `RedisError::Invalid` - Flags (`ssl`, `ssl_check_hostname`, `aws_iam_auth`, `aws_iam_serverless`) are read one way on every surface. `true`, `1` and `yes` are true and `false`, `0`, `no`, `off`, `f` and `n` are false, the text redis-py's `_str_to_bool` and the proxy's pydantic models agree on. Any other text is `RedisError::Invalid`, so a TLS setting is never guessed -- The resolver fails on a setting it cannot honor only when that setting is a security check. A `Warn` row is dropped and returned as an `Ignored` naming its source (`ssl_ciphers` or `REDIS_SSL_CIPHERS`) and the consequence from `keys::consequence`, because a Python deployment that worked must keep working when its stores move to Rust. Only a value that is malformed (`RedisError::Invalid`) or a target that is missing fails. Promoting a `Warn` row to `Read` is then a pure improvement with no behavior cliff - - `ssl_ciphers`, `ssl_min_version` above TLS 1.2, `ssl_ocsp_context`, `ssl_ocsp_expected_cert`, `connection_pool`, `event_dispatcher`, `redis_connect_func` and `retry` are ignored +- The resolver fails on a setting it cannot honor only when that setting is a security check. A `Warn` row is dropped and returned as an `Ignored` naming its source (`ssl_ciphers` or `REDIS_SSL_CIPHERS`) and the consequence on its row, because a Python deployment that worked must keep working when its stores move to Rust. Only a value that is malformed (`RedisError::Invalid`) or a target that is missing fails. Promoting a `Warn` row to `Read` is then a pure improvement with no behavior cliff + - A `Warn` row with a `met` check is dropped silently when its value asks for nothing more than Rust does, so `ssl_min_version` of TLS 1.2 or lower never warns - A `Reject` row fails closed. `ssl_validate_ocsp` and `ssl_validate_ocsp_stapled` set to true, from the kwargs, `REDIS_*` or a url query, are `RedisError::Unenforceable`, because rustls has no client-side OCSP and connecting without the check would silently drop it. redis-py 5.3.1's async client refuses both with a `TypeError`, so this matches the proxy's path. Set to false they ask for nothing and are dropped without a warning - - `ssl_ca_data` and `ssl_ca_path` are read into `TlsOptions` and trusted next to `ssl_ca_certs`. `ssl_password` is read into `TlsOptions::key_password`, which `redis-client` uses to decrypt a PKCS#8 key like the sync client's `load_cert_chain(password=...)`. redis-py 5.3.1's async client refuses it, so this is a superset of the path the proxy uses, like `ssl_ca_path` `unix_socket_path` resolves to `Target::Unix`, which like redis-py's `UnixDomainSocketConnection` replaces host, port and TLS. Python still requires `host` next to it, so Rust does too - - A url's `?db=`, `?username=` and `?password=` are read like `parse_url` reads them, so `?db=` wins over the path and the userinfo wins over `?username=` and `?password=`. Any other query parameter except the TLS ones on a `rediss://` url is ignored naming `url ?`. redis-py turns each one into a client kwarg - - `retry_on_timeout`, `retry_on_error`, `protocol`, `cache`, `cache_config`, `socket_keepalive` and `max_connections` are ignored. Cluster clients still turn TCP keepalive on, like Python's async cluster default + - `ssl_ca_data` and `ssl_ca_path` are read into `TlsOptions` and trusted next to `ssl_ca_certs`. `ssl_password` is read into `TlsOptions::key_password`, which `redis-client` uses to decrypt a PKCS#8 key like the sync client's `load_cert_chain(password=...)`. redis-py 5.3.1's async client refuses it, so this is a superset of the path the proxy uses, like `ssl_ca_path` + - `unix_socket_path` resolves to `Target::Unix`, which like redis-py's `UnixDomainSocketConnection` replaces host, port and TLS. Python still requires `host` next to it, so Rust does too + - A url's `?db=`, `?username=` and `?password=` are read like `parse_url` reads them, so `?db=` wins over the path and the userinfo wins over `?username=` and `?password=`. Any other query parameter, including one with no row, is ignored naming `url ?`, because redis-py turns each one into a client kwarg. Which ones a url carries is the `FromUrl` column + - `socket_keepalive` is warned, but cluster clients still turn TCP keepalive on, like Python's async cluster default - `health_check_interval` is read as an `int` of seconds like `_coerce_redis_kwargs_types` into `HealthCheck`. Zero is `Disabled` and stays off on every target, a negative interval checks before every command like redis-py, and only `Unset` takes a default. `redis-connect` gives an unset cluster Python's 25s default and `redis-client` pings every node before the next command once the client sat idle that long, outside the breaker and the call events - Cases where Rust and Python disagree on purpose are cases in both contract tests with a different expected outcome on each side - `ssl_ca_path`: redis-py 5.3.1's async `Redis` has no such parameter and raises `TypeError` building the client, while Rust trusts the directory like `load_verify_locations(capath=...)` diff --git a/litellm-rust/crates/redis-resolve/Cargo.toml b/litellm-rust/crates/redis-resolve/Cargo.toml index 7125af6451d..b3d3fc01e44 100644 --- a/litellm-rust/crates/redis-resolve/Cargo.toml +++ b/litellm-rust/crates/redis-resolve/Cargo.toml @@ -17,3 +17,4 @@ url.workspace = true [dev-dependencies] rstest.workspace = true +serde.workspace = true diff --git a/litellm-rust/crates/redis-resolve/redis-py-kwargs.json b/litellm-rust/crates/redis-resolve/redis-py-kwargs.json index 11da8806354..ae0eff87ad9 100644 --- a/litellm-rust/crates/redis-resolve/redis-py-kwargs.json +++ b/litellm-rust/crates/redis-resolve/redis-py-kwargs.json @@ -1,6 +1,6 @@ { "meta": { - "description": "Every kwarg litellm/_redis.py can pass to redis-py, by tested redis-py version, next to how SETTINGS in litellm-rust/crates/redis-resolve/src/keys.rs handles it", + "description": "What redis-py and litellm/_redis.py accept for every Redis kwarg the Rust resolver can be handed. SETTINGS in src/keys.rs has exactly one row per arg here", "generated": "2026-11-05", "redis_py_versions": [ "5.3.1", @@ -17,2849 +17,103 @@ "connection": "redis.Connection or redis.asyncio.Connection MRO, the url allowlist from _get_redis_url_kwargs", "cluster": "redis.asyncio.RedisCluster.__init__, the allowlist from _get_redis_cluster_kwargs" }, - "rust": { - "supported": "true when the Rust client honors the setting, which is only the Read rows", - "handling": { - "Read": "modeled by the resolver", - "Warn": "dropped and returned as an Ignored warning naming the consequence", - "Reject": "fails with RedisError::Unenforceable when set to true", - "unlisted": "no SETTINGS row, so it lands in passthrough for a redis-py host and the Rust client never uses it, without a warning", - "unlisted_host_object": "no SETTINGS row and listed in HOST_OBJECTS, so it lands in host_objects and the Rust client never uses it, without a warning" - }, - "field": "the Options field in litellm-rust/crates/redis-resolve/src/options.rs that a Read row fills, null otherwise" - }, - "versions": "each entry is a redis-py version whose signatures name the arg, with which ones. An empty array means no signature names it, so it only reaches Python as a litellm key" + "environment": "true when _get_redis_env_kwarg_mapping reads the arg as REDIS_ under the locked redis-py", + "redis_py": "each redis-py version whose signatures name the arg, with which ones. An empty object means only litellm reads it", + "checked_by": "test_redis_py_kwargs_catalog_matches_the_installed_redis_py in tests/unit/test_redis.py checks the locked version against the installed redis-py, and tests/catalog.rs checks SETTINGS against this file" }, "args": { - "address_remap": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "cluster" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "cluster" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "cluster" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "cluster" - ] - } - ], - "rust": { - "supported": false, - "handling": "unlisted_host_object", - "field": null - }, - "note": "Cluster-only and no SETTINGS row, so a Rust cluster store drops it without a warning" - }, - "auto_close_connection_pool": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "async" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "async" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "async" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "async" - ] - } - ], - "rust": { - "supported": false, - "handling": "unlisted", - "field": null - }, - "note": "Async-only and outside the config allowlist, so Python never passes it from config" - }, - "aws_iam_auth": { - "versions": [], - "rust": { - "supported": true, - "handling": "Read", - "field": "Options.aws.auth" - } - }, - "aws_iam_cache_name": { - "versions": [], - "rust": { - "supported": true, - "handling": "Read", - "field": "Options.aws.cache_name" - } - }, - "aws_iam_region": { - "versions": [], - "rust": { - "supported": true, - "handling": "Read", - "field": "Options.aws.region" - } - }, - "aws_iam_serverless": { - "versions": [], - "rust": { - "supported": true, - "handling": "Read", - "field": "Options.aws.serverless" - } - }, - "aws_iam_user_name": { - "versions": [], - "rust": { - "supported": true, - "handling": "Read", - "field": "Options.aws.user_name" - } - }, - "azure_client_id": { - "versions": [], - "rust": { - "supported": true, - "handling": "Read", - "field": "Options.azure.client_id" - } - }, - "azure_client_secret": { - "versions": [], - "rust": { - "supported": true, - "handling": "Read", - "field": "Options.azure.client_secret" - } - }, - "azure_redis_ad_token": { - "versions": [], - "rust": { - "supported": true, - "handling": "Read", - "field": "Options.azure.ad_token" - } - }, - "azure_tenant_id": { - "versions": [], - "rust": { - "supported": true, - "handling": "Read", - "field": "Options.azure.tenant_id" - } - }, - "cache": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "sync" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "sync" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "sync" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync" - ] - } - ], - "rust": { - "supported": false, - "handling": "Warn", - "field": null - }, - "note": "Sync-only on every tested version, so the proxy's async path raises TypeError on it in Python too" - }, - "cache_config": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "sync" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "sync" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "sync" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync" - ] - } - ], - "rust": { - "supported": false, - "handling": "Warn", - "field": null - }, - "note": "Sync-only on every tested version, so the proxy's async path raises TypeError on it in Python too" - }, - "charset": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "sync" - ] - } - ], - "rust": { - "supported": false, - "handling": "Warn", - "field": null - }, - "note": "Removed in redis-py 6.0, so the Warn row only matters for 5.3.1 configs" - }, - "client_name": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - } - ], - "rust": { - "supported": false, - "handling": "Warn", - "field": null - } - }, - "cluster_error_retry_attempts": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "cluster" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "cluster" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "cluster" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "cluster" - ] - } - ], - "rust": { - "supported": false, - "handling": "unlisted", - "field": null - }, - "note": "Cluster-only and no SETTINGS row, so a Rust cluster store drops it without a warning" - }, - "command_packer": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "connection" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "connection" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "connection" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "connection" - ] - } - ], - "rust": { - "supported": false, - "handling": "unlisted", - "field": null - }, - "note": "Connection internal that only the url allowlist admits, not a setting anyone configures" - }, - "connection_error_retry_attempts": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "cluster" - ] - } - ], - "rust": { - "supported": false, - "handling": "unlisted", - "field": null - }, - "note": "Cluster-only and no SETTINGS row, so a Rust cluster store drops it without a warning. Only exists in 5.3.1" - }, - "connection_pool": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "sync", - "async" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "sync", - "async" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "sync", - "async" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync", - "async" - ] - } - ], - "rust": { - "supported": false, - "handling": "Warn", - "field": null - } - }, - "credential_provider": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - } - ], - "rust": { - "supported": true, - "handling": "Read", - "field": "Options.credential_provider" - } - }, - "db": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - } - ], - "rust": { - "supported": true, - "handling": "Read", - "field": "Options.db" - } - }, - "decode_responses": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - } - ], - "rust": { - "supported": false, - "handling": "Warn", - "field": null - } - }, - "driver_info": { - "versions": [ - { - "redis_py": "7.4.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - } - ], - "rust": { - "supported": false, - "handling": "unlisted_host_object", - "field": null - }, - "note": "No SETTINGS row, so a Rust store drops it without a warning" - }, - "dynamic_startup_nodes": { - "versions": [ - { - "redis_py": "6.4.0", - "signatures": [ - "cluster" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "cluster" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "cluster" - ] - } - ], - "rust": { - "supported": false, - "handling": "unlisted", - "field": null - }, - "note": "Cluster-only and no SETTINGS row, so a Rust cluster store drops it without a warning" - }, - "encoder_class": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "connection" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "connection" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "connection" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "connection" - ] - } - ], - "rust": { - "supported": false, - "handling": "unlisted", - "field": null - }, - "note": "Connection internal that only the url allowlist admits, not a setting anyone configures" - }, - "encoding": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - } - ], - "rust": { - "supported": false, - "handling": "Warn", - "field": null - } - }, - "encoding_errors": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - } - ], - "rust": { - "supported": false, - "handling": "Warn", - "field": null - } - }, - "errors": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "sync" - ] - } - ], - "rust": { - "supported": false, - "handling": "Warn", - "field": null - }, - "note": "Removed in redis-py 6.0, so the Warn row only matters for 5.3.1 configs" - }, - "event_dispatcher": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - } - ], - "rust": { - "supported": false, - "handling": "Warn", - "field": null - } - }, - "gcp_service_account": { - "versions": [], - "rust": { - "supported": true, - "handling": "Read", - "field": "Options.gcp.service_account" - } - }, - "gcp_ssl_ca_certs": { - "versions": [], - "rust": { - "supported": true, - "handling": "Read", - "field": "Options.gcp.ssl_ca_certs" - } - }, - "health_check_interval": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - } - ], - "rust": { - "supported": true, - "handling": "Read", - "field": "Options.health_check_interval" - } - }, - "host": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - } - ], - "rust": { - "supported": true, - "handling": "Read", - "field": "Options.host" - } - }, - "legacy_responses": { - "versions": [ - { - "redis_py": "8.0.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - } - ], - "rust": { - "supported": false, - "handling": "unlisted", - "field": null - }, - "note": "No SETTINGS row, so a Rust store drops it without a warning" - }, - "lib_name": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - } - ], - "rust": { - "supported": false, - "handling": "Warn", - "field": null - } - }, - "lib_version": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - } - ], - "rust": { - "supported": false, - "handling": "Warn", - "field": null - } - }, - "load_balancing_strategy": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "cluster" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "cluster" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "cluster" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "cluster" - ] - } - ], - "rust": { - "supported": false, - "handling": "unlisted", - "field": null - }, - "note": "Cluster-only and no SETTINGS row, so a Rust cluster store drops it without a warning. It changes which node serves a read" - }, - "maint_notifications_config": { - "versions": [ - { - "redis_py": "7.4.1", - "signatures": [ - "sync", - "connection" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync", - "connection" - ] - } - ], - "rust": { - "supported": false, - "handling": "unlisted_host_object", - "field": null - }, - "note": "No SETTINGS row, so a Rust store drops it without a warning" - }, - "maint_notifications_pool_handler": { - "versions": [ - { - "redis_py": "7.4.1", - "signatures": [ - "connection" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "connection" - ] - } - ], - "rust": { - "supported": false, - "handling": "unlisted", - "field": null - }, - "note": "Connection internal that only the url allowlist admits, not a setting anyone configures" - }, - "maintenance_notification_hash": { - "versions": [ - { - "redis_py": "7.4.1", - "signatures": [ - "connection" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "connection" - ] - } - ], - "rust": { - "supported": false, - "handling": "unlisted", - "field": null - }, - "note": "Connection internal that only the url allowlist admits, not a setting anyone configures" - }, - "maintenance_state": { - "versions": [ - { - "redis_py": "7.4.1", - "signatures": [ - "connection" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "connection" - ] - } - ], - "rust": { - "supported": false, - "handling": "unlisted", - "field": null - }, - "note": "Connection internal that only the url allowlist admits, not a setting anyone configures" - }, - "max_connections": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "sync", - "async", - "cluster" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "sync", - "async", - "cluster" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "sync", - "async", - "cluster" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync", - "async", - "cluster" - ] - } - ], - "rust": { - "supported": false, - "handling": "Warn", - "field": null - }, - "note": "Warned with the generic consequence, though Rust having no connection cap can push a low maxclients server over its limit" - }, - "namespace": { - "versions": [], - "rust": { - "supported": true, - "handling": "Read", - "field": "Options.namespace" - } - }, - "orig_host_address": { - "versions": [ - { - "redis_py": "7.4.1", - "signatures": [ - "connection" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "connection" - ] - } - ], - "rust": { - "supported": false, - "handling": "unlisted", - "field": null - }, - "note": "Connection internal that only the url allowlist admits, not a setting anyone configures" - }, - "orig_socket_connect_timeout": { - "versions": [ - { - "redis_py": "7.4.1", - "signatures": [ - "connection" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "connection" - ] - } - ], - "rust": { - "supported": false, - "handling": "unlisted", - "field": null - }, - "note": "Connection internal that only the url allowlist admits, not a setting anyone configures" - }, - "orig_socket_timeout": { - "versions": [ - { - "redis_py": "7.4.1", - "signatures": [ - "connection" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "connection" - ] - } - ], - "rust": { - "supported": false, - "handling": "unlisted", - "field": null - }, - "note": "Connection internal that only the url allowlist admits, not a setting anyone configures" - }, - "oss_cluster_maint_notifications_handler": { - "versions": [ - { - "redis_py": "7.4.1", - "signatures": [ - "sync", - "connection" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync", - "connection" - ] - } - ], - "rust": { - "supported": false, - "handling": "unlisted", - "field": null - }, - "note": "No SETTINGS row, so a Rust store drops it without a warning. It takes an object but is missing from HOST_OBJECTS" - }, - "parser": { - "versions": [ - { - "redis_py": "7.4.1", - "signatures": [ - "connection" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "connection" - ] - } - ], - "rust": { - "supported": false, - "handling": "unlisted", - "field": null - }, - "note": "Connection internal that only the url allowlist admits, not a setting anyone configures" - }, - "parser_class": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "connection" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "connection" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "connection" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "connection" - ] - } - ], - "rust": { - "supported": false, - "handling": "unlisted_host_object", - "field": null - }, - "note": "Connection internal that only the url allowlist admits, not a setting anyone configures" - }, - "password": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - } - ], - "rust": { - "supported": true, - "handling": "Read", - "field": "Options.password" - } - }, - "path": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "cluster" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "cluster" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "cluster" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "cluster" - ] - } - ], - "rust": { - "supported": false, - "handling": "unlisted", - "field": null - }, - "note": "Cluster-only and no SETTINGS row, so a Rust cluster store drops it without a warning" - }, - "policy_resolver": { - "versions": [ - { - "redis_py": "7.4.1", - "signatures": [ - "cluster" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "cluster" - ] - } - ], - "rust": { - "supported": false, - "handling": "unlisted", - "field": null - }, - "note": "Cluster-only and no SETTINGS row, so a Rust cluster store drops it without a warning" - }, - "port": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - } - ], - "rust": { - "supported": true, - "handling": "Read", - "field": "Options.port" - } - }, - "protocol": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - } - ], - "rust": { - "supported": false, - "handling": "Warn", - "field": null - } - }, - "read_from_replicas": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "cluster" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "cluster" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "cluster" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "cluster" - ] - } - ], - "rust": { - "supported": false, - "handling": "unlisted", - "field": null - }, - "note": "Cluster-only and no SETTINGS row, so a Rust cluster store drops it without a warning. It changes which node serves a read" - }, - "redis_connect_func": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "sync", - "async", - "connection" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "sync", - "async", - "connection" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "sync", - "async", - "connection" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync", - "async", - "connection" - ] - } - ], - "rust": { - "supported": false, - "handling": "Warn", - "field": null - } - }, - "reinitialize_steps": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "cluster" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "cluster" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "cluster" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "cluster" - ] - } - ], - "rust": { - "supported": false, - "handling": "unlisted", - "field": null - }, - "note": "Cluster-only and no SETTINGS row, so a Rust cluster store drops it without a warning" - }, - "require_full_coverage": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "cluster" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "cluster" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "cluster" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "cluster" - ] - } - ], - "rust": { - "supported": false, - "handling": "unlisted", - "field": null - }, - "note": "Cluster-only and no SETTINGS row, so a Rust cluster store drops it without a warning" - }, - "retry": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - } - ], - "rust": { - "supported": false, - "handling": "Warn", - "field": null - } - }, - "retry_on_error": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - } - ], - "rust": { - "supported": false, - "handling": "Warn", - "field": null - } - }, - "retry_on_timeout": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "sync", - "async", - "connection" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "sync", - "async", - "connection" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "sync", - "async", - "connection" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync", - "async", - "connection" - ] - } - ], - "rust": { - "supported": false, - "handling": "Warn", - "field": null - } - }, - "sentinel_nodes": { - "versions": [], - "rust": { - "supported": true, - "handling": "Read", - "field": "Options.sentinel.nodes" - } - }, - "sentinel_password": { - "versions": [], - "rust": { - "supported": true, - "handling": "Read", - "field": "Options.sentinel.password" - } - }, - "service_name": { - "versions": [], - "rust": { - "supported": true, - "handling": "Read", - "field": "Options.sentinel.service_name" - } - }, - "single_connection_client": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "sync", - "async" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "sync", - "async" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "sync", - "async" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync", - "async" - ] - } - ], - "rust": { - "supported": false, - "handling": "Warn", - "field": null - } - }, - "socket_connect_timeout": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - } - ], - "rust": { - "supported": true, - "handling": "Read", - "field": "Options.socket_connect_timeout" - } - }, - "socket_keepalive": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - } - ], - "rust": { - "supported": false, - "handling": "Warn", - "field": null - } - }, - "socket_keepalive_options": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - } - ], - "rust": { - "supported": false, - "handling": "Warn", - "field": null - } - }, - "socket_read_size": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "connection" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "connection" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "connection" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - } - ], - "rust": { - "supported": false, - "handling": "unlisted", - "field": null - }, - "note": "No SETTINGS row, so a Rust store drops it without a warning" - }, - "socket_timeout": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - } - ], - "rust": { - "supported": true, - "handling": "Read", - "field": "Options.socket_timeout" - } - }, - "socket_type": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "connection" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "connection" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "connection" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "connection" - ] - } - ], - "rust": { - "supported": false, - "handling": "unlisted", - "field": null - }, - "note": "Connection internal that only the url allowlist admits, not a setting anyone configures" - }, - "ssl": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "sync", - "async", - "cluster" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "sync", - "async", - "cluster" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "sync", - "async", - "cluster" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync", - "async", - "cluster" - ] - } - ], - "rust": { - "supported": true, - "handling": "Read", - "field": "Options.ssl" - } - }, - "ssl_ca_certs": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "sync", - "async", - "cluster" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "sync", - "async", - "cluster" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "sync", - "async", - "cluster" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync", - "async", - "cluster" - ] - } - ], - "rust": { - "supported": true, - "handling": "Read", - "field": "Options.tls.ca_certs" - } - }, - "ssl_ca_data": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "sync", - "async", - "cluster" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "sync", - "async", - "cluster" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "sync", - "async", - "cluster" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync", - "async", - "cluster" - ] - } - ], - "rust": { - "supported": true, - "handling": "Read", - "field": "Options.tls.ca_data" - } - }, - "ssl_ca_path": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "sync" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "sync" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "sync", - "async" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync", - "async" - ] - } - ], - "rust": { - "supported": true, - "handling": "Read", - "field": "Options.tls.ca_path" - }, - "note": "Python's async client raises TypeError on it before 7.4.1, so Rust reading it is a superset with the pinned 5.3.1" - }, - "ssl_cert_reqs": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "sync", - "async", - "cluster" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "sync", - "async", - "cluster" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "sync", - "async", - "cluster" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync", - "async", - "cluster" - ] - } - ], - "rust": { - "supported": true, - "handling": "Read", - "field": "Options.tls.cert_reqs" - } - }, - "ssl_certfile": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "sync", - "async", - "cluster" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "sync", - "async", - "cluster" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "sync", - "async", - "cluster" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync", - "async", - "cluster" - ] - } - ], - "rust": { - "supported": true, - "handling": "Read", - "field": "Options.tls.certfile" - } - }, - "ssl_check_hostname": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "sync", - "async", - "cluster" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "sync", - "async", - "cluster" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "sync", - "async", - "cluster" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync", - "async", - "cluster" - ] - } - ], - "rust": { - "supported": true, - "handling": "Read", - "field": "Options.tls.check_hostname" - } - }, - "ssl_ciphers": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "sync", - "async", - "cluster" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "sync", - "async", - "cluster" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "sync", - "async", - "cluster" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync", - "async", - "cluster" - ] - } - ], - "rust": { - "supported": false, - "handling": "Warn", - "field": null - } - }, - "ssl_exclude_verify_flags": { - "versions": [ - { - "redis_py": "7.4.1", - "signatures": [ - "sync", - "async", - "cluster" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync", - "async", - "cluster" - ] - } - ], - "rust": { - "supported": false, - "handling": "unlisted", - "field": null - }, - "note": "No SETTINGS row, so a Rust store drops it without a warning" - }, - "ssl_include_verify_flags": { - "versions": [ - { - "redis_py": "7.4.1", - "signatures": [ - "sync", - "async", - "cluster" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync", - "async", - "cluster" - ] - } - ], - "rust": { - "supported": false, - "handling": "unlisted", - "field": null - }, - "note": "No SETTINGS row, so a Rust store drops it without a warning" - }, - "ssl_keyfile": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "sync", - "async", - "cluster" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "sync", - "async", - "cluster" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "sync", - "async", - "cluster" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync", - "async", - "cluster" - ] - } - ], - "rust": { - "supported": true, - "handling": "Read", - "field": "Options.tls.keyfile" - } - }, - "ssl_min_version": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "sync", - "async", - "cluster" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "sync", - "async", - "cluster" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "sync", - "async", - "cluster" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync", - "async", - "cluster" - ] - } - ], - "rust": { - "supported": false, - "handling": "Warn", - "field": null - } - }, - "ssl_ocsp_context": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "sync" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "sync" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "sync" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync" - ] - } - ], - "rust": { - "supported": false, - "handling": "Warn", - "field": null - }, - "note": "Sync-only on every tested version, so the proxy's async path raises TypeError on it in Python too" - }, - "ssl_ocsp_expected_cert": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "sync" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "sync" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "sync" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync" - ] - } - ], - "rust": { - "supported": false, - "handling": "Warn", - "field": null - }, - "note": "Sync-only on every tested version, so the proxy's async path raises TypeError on it in Python too" - }, - "ssl_password": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "sync" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "sync" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "sync", - "async" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync", - "async" - ] - } - ], - "rust": { - "supported": true, - "handling": "Read", - "field": "Options.tls.key_password" - }, - "note": "Python's async client raises TypeError on it before 7.4.1, so Rust reading it is a superset with the pinned 5.3.1" - }, - "ssl_validate_ocsp": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "sync" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "sync" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "sync" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync" - ] - } - ], - "rust": { - "supported": false, - "handling": "Reject", - "field": null - }, - "note": "Sync-only on every tested version, so the proxy's async path raises TypeError on it in Python too" - }, - "ssl_validate_ocsp_stapled": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "sync" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "sync" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "sync" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync" - ] - } - ], - "rust": { - "supported": false, - "handling": "Reject", - "field": null - }, - "note": "Sync-only on every tested version, so the proxy's async path raises TypeError on it in Python too" - }, - "startup_nodes": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "cluster" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "cluster" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "cluster" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "cluster" - ] - } - ], - "rust": { - "supported": true, - "handling": "Read", - "field": "Options.startup_nodes" - } - }, - "unix_socket_path": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "sync", - "async" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "sync", - "async" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "sync", - "async" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync", - "async" - ] - } - ], - "rust": { - "supported": true, - "handling": "Read", - "field": "Options.unix_socket_path" - } - }, - "url": { - "versions": [], - "rust": { - "supported": true, - "handling": "Read", - "field": "Options.url" - } - }, - "username": { - "versions": [ - { - "redis_py": "5.3.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "6.4.0", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "7.4.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - }, - { - "redis_py": "8.0.1", - "signatures": [ - "sync", - "async", - "connection", - "cluster" - ] - } - ], - "rust": { - "supported": true, - "handling": "Read", - "field": "Options.username" - } - } + "address_remap": {"environment": false, "redis_py": {"5.3.1": ["cluster"], "6.4.0": ["cluster"], "7.4.1": ["cluster"], "8.0.1": ["cluster"]}}, + "auto_close_connection_pool": {"environment": false, "redis_py": {"5.3.1": ["async"], "6.4.0": ["async"], "7.4.1": ["async"], "8.0.1": ["async"]}}, + "aws_iam_auth": {"environment": true, "redis_py": {}}, + "aws_iam_cache_name": {"environment": true, "redis_py": {}}, + "aws_iam_region": {"environment": true, "redis_py": {}}, + "aws_iam_serverless": {"environment": true, "redis_py": {}}, + "aws_iam_user_name": {"environment": true, "redis_py": {}}, + "azure_client_id": {"environment": true, "redis_py": {}}, + "azure_client_secret": {"environment": true, "redis_py": {}}, + "azure_redis_ad_token": {"environment": true, "redis_py": {}}, + "azure_tenant_id": {"environment": true, "redis_py": {}}, + "cache": {"environment": true, "redis_py": {"5.3.1": ["sync"], "6.4.0": ["sync"], "7.4.1": ["sync"], "8.0.1": ["sync"]}}, + "cache_config": {"environment": true, "redis_py": {"5.3.1": ["sync"], "6.4.0": ["sync"], "7.4.1": ["sync"], "8.0.1": ["sync"]}}, + "charset": {"environment": true, "redis_py": {"5.3.1": ["sync"]}}, + "client_name": {"environment": true, "redis_py": {"5.3.1": ["sync", "async", "connection", "cluster"], "6.4.0": ["sync", "async", "connection", "cluster"], "7.4.1": ["sync", "async", "connection", "cluster"], "8.0.1": ["sync", "async", "connection", "cluster"]}}, + "cluster_error_retry_attempts": {"environment": false, "redis_py": {"5.3.1": ["cluster"], "6.4.0": ["cluster"], "7.4.1": ["cluster"], "8.0.1": ["cluster"]}}, + "command_packer": {"environment": false, "redis_py": {"5.3.1": ["connection"], "6.4.0": ["connection"], "7.4.1": ["connection"], "8.0.1": ["connection"]}}, + "connection_class": {"environment": false, "redis_py": {}}, + "connection_error_retry_attempts": {"environment": false, "redis_py": {"5.3.1": ["cluster"]}}, + "connection_pool": {"environment": false, "redis_py": {"5.3.1": ["sync", "async"], "6.4.0": ["sync", "async"], "7.4.1": ["sync", "async"], "8.0.1": ["sync", "async"]}}, + "credential_provider": {"environment": false, "redis_py": {"5.3.1": ["sync", "async", "connection", "cluster"], "6.4.0": ["sync", "async", "connection", "cluster"], "7.4.1": ["sync", "async", "connection", "cluster"], "8.0.1": ["sync", "async", "connection", "cluster"]}}, + "db": {"environment": true, "redis_py": {"5.3.1": ["sync", "async", "connection", "cluster"], "6.4.0": ["sync", "async", "connection", "cluster"], "7.4.1": ["sync", "async", "connection", "cluster"], "8.0.1": ["sync", "async", "connection", "cluster"]}}, + "decode_responses": {"environment": true, "redis_py": {"5.3.1": ["sync", "async", "connection", "cluster"], "6.4.0": ["sync", "async", "connection", "cluster"], "7.4.1": ["sync", "async", "connection", "cluster"], "8.0.1": ["sync", "async", "connection", "cluster"]}}, + "driver_info": {"environment": false, "redis_py": {"7.4.1": ["sync", "async", "connection", "cluster"], "8.0.1": ["sync", "async", "connection", "cluster"]}}, + "dynamic_startup_nodes": {"environment": false, "redis_py": {"6.4.0": ["cluster"], "7.4.1": ["cluster"], "8.0.1": ["cluster"]}}, + "encoder_class": {"environment": false, "redis_py": {"5.3.1": ["connection"], "6.4.0": ["connection"], "7.4.1": ["connection"], "8.0.1": ["connection"]}}, + "encoding": {"environment": true, "redis_py": {"5.3.1": ["sync", "async", "connection", "cluster"], "6.4.0": ["sync", "async", "connection", "cluster"], "7.4.1": ["sync", "async", "connection", "cluster"], "8.0.1": ["sync", "async", "connection", "cluster"]}}, + "encoding_errors": {"environment": true, "redis_py": {"5.3.1": ["sync", "async", "connection", "cluster"], "6.4.0": ["sync", "async", "connection", "cluster"], "7.4.1": ["sync", "async", "connection", "cluster"], "8.0.1": ["sync", "async", "connection", "cluster"]}}, + "errors": {"environment": true, "redis_py": {"5.3.1": ["sync"]}}, + "event_dispatcher": {"environment": true, "redis_py": {"5.3.1": ["sync", "async", "connection", "cluster"], "6.4.0": ["sync", "async", "connection", "cluster"], "7.4.1": ["sync", "async", "connection", "cluster"], "8.0.1": ["sync", "async", "connection", "cluster"]}}, + "gcp_service_account": {"environment": true, "redis_py": {}}, + "gcp_ssl_ca_certs": {"environment": true, "redis_py": {}}, + "health_check_interval": {"environment": true, "redis_py": {"5.3.1": ["sync", "async", "connection", "cluster"], "6.4.0": ["sync", "async", "connection", "cluster"], "7.4.1": ["sync", "async", "connection", "cluster"], "8.0.1": ["sync", "async", "connection", "cluster"]}}, + "host": {"environment": true, "redis_py": {"5.3.1": ["sync", "async", "connection", "cluster"], "6.4.0": ["sync", "async", "connection", "cluster"], "7.4.1": ["sync", "async", "connection", "cluster"], "8.0.1": ["sync", "async", "connection", "cluster"]}}, + "legacy_responses": {"environment": false, "redis_py": {"8.0.1": ["sync", "async", "connection", "cluster"]}}, + "lib_name": {"environment": true, "redis_py": {"5.3.1": ["sync", "async", "connection", "cluster"], "6.4.0": ["sync", "async", "connection", "cluster"], "7.4.1": ["sync", "async", "connection", "cluster"], "8.0.1": ["sync", "async", "connection", "cluster"]}}, + "lib_version": {"environment": true, "redis_py": {"5.3.1": ["sync", "async", "connection", "cluster"], "6.4.0": ["sync", "async", "connection", "cluster"], "7.4.1": ["sync", "async", "connection", "cluster"], "8.0.1": ["sync", "async", "connection", "cluster"]}}, + "load_balancing_strategy": {"environment": false, "redis_py": {"5.3.1": ["cluster"], "6.4.0": ["cluster"], "7.4.1": ["cluster"], "8.0.1": ["cluster"]}}, + "maint_notifications_config": {"environment": false, "redis_py": {"7.4.1": ["sync", "connection"], "8.0.1": ["sync", "connection"]}}, + "maint_notifications_pool_handler": {"environment": false, "redis_py": {"7.4.1": ["connection"], "8.0.1": ["connection"]}}, + "maintenance_notification_hash": {"environment": false, "redis_py": {"7.4.1": ["connection"], "8.0.1": ["connection"]}}, + "maintenance_state": {"environment": false, "redis_py": {"7.4.1": ["connection"], "8.0.1": ["connection"]}}, + "max_connections": {"environment": true, "redis_py": {"5.3.1": ["sync", "async", "cluster"], "6.4.0": ["sync", "async", "cluster"], "7.4.1": ["sync", "async", "cluster"], "8.0.1": ["sync", "async", "cluster"]}}, + "namespace": {"environment": false, "redis_py": {}}, + "orig_host_address": {"environment": false, "redis_py": {"7.4.1": ["connection"], "8.0.1": ["connection"]}}, + "orig_socket_connect_timeout": {"environment": false, "redis_py": {"7.4.1": ["connection"], "8.0.1": ["connection"]}}, + "orig_socket_timeout": {"environment": false, "redis_py": {"7.4.1": ["connection"], "8.0.1": ["connection"]}}, + "oss_cluster_maint_notifications_handler": {"environment": false, "redis_py": {"7.4.1": ["sync", "connection"], "8.0.1": ["sync", "connection"]}}, + "parser": {"environment": false, "redis_py": {"7.4.1": ["connection"], "8.0.1": ["connection"]}}, + "parser_class": {"environment": false, "redis_py": {"5.3.1": ["connection"], "6.4.0": ["connection"], "7.4.1": ["connection"], "8.0.1": ["connection"]}}, + "password": {"environment": true, "redis_py": {"5.3.1": ["sync", "async", "connection", "cluster"], "6.4.0": ["sync", "async", "connection", "cluster"], "7.4.1": ["sync", "async", "connection", "cluster"], "8.0.1": ["sync", "async", "connection", "cluster"]}}, + "path": {"environment": false, "redis_py": {"5.3.1": ["cluster"], "6.4.0": ["cluster"], "7.4.1": ["cluster"], "8.0.1": ["cluster"]}}, + "policy_resolver": {"environment": false, "redis_py": {"7.4.1": ["cluster"], "8.0.1": ["cluster"]}}, + "port": {"environment": true, "redis_py": {"5.3.1": ["sync", "async", "connection", "cluster"], "6.4.0": ["sync", "async", "connection", "cluster"], "7.4.1": ["sync", "async", "connection", "cluster"], "8.0.1": ["sync", "async", "connection", "cluster"]}}, + "protocol": {"environment": true, "redis_py": {"5.3.1": ["sync", "async", "connection", "cluster"], "6.4.0": ["sync", "async", "connection", "cluster"], "7.4.1": ["sync", "async", "connection", "cluster"], "8.0.1": ["sync", "async", "connection", "cluster"]}}, + "read_from_replicas": {"environment": false, "redis_py": {"5.3.1": ["cluster"], "6.4.0": ["cluster"], "7.4.1": ["cluster"], "8.0.1": ["cluster"]}}, + "redis_connect_func": {"environment": true, "redis_py": {"5.3.1": ["sync", "async", "connection"], "6.4.0": ["sync", "async", "connection"], "7.4.1": ["sync", "async", "connection"], "8.0.1": ["sync", "async", "connection"]}}, + "reinitialize_steps": {"environment": false, "redis_py": {"5.3.1": ["cluster"], "6.4.0": ["cluster"], "7.4.1": ["cluster"], "8.0.1": ["cluster"]}}, + "require_full_coverage": {"environment": false, "redis_py": {"5.3.1": ["cluster"], "6.4.0": ["cluster"], "7.4.1": ["cluster"], "8.0.1": ["cluster"]}}, + "retry": {"environment": false, "redis_py": {"5.3.1": ["sync", "async", "connection", "cluster"], "6.4.0": ["sync", "async", "connection", "cluster"], "7.4.1": ["sync", "async", "connection", "cluster"], "8.0.1": ["sync", "async", "connection", "cluster"]}}, + "retry_on_error": {"environment": true, "redis_py": {"5.3.1": ["sync", "async", "connection", "cluster"], "6.4.0": ["sync", "async", "connection", "cluster"], "7.4.1": ["sync", "async", "connection", "cluster"], "8.0.1": ["sync", "async", "connection", "cluster"]}}, + "retry_on_timeout": {"environment": true, "redis_py": {"5.3.1": ["sync", "async", "connection"], "6.4.0": ["sync", "async", "connection"], "7.4.1": ["sync", "async", "connection"], "8.0.1": ["sync", "async", "connection"]}}, + "sentinel_nodes": {"environment": false, "redis_py": {}}, + "sentinel_password": {"environment": false, "redis_py": {}}, + "service_name": {"environment": false, "redis_py": {}}, + "single_connection_client": {"environment": true, "redis_py": {"5.3.1": ["sync", "async"], "6.4.0": ["sync", "async"], "7.4.1": ["sync", "async"], "8.0.1": ["sync", "async"]}}, + "socket_connect_timeout": {"environment": true, "redis_py": {"5.3.1": ["sync", "async", "connection", "cluster"], "6.4.0": ["sync", "async", "connection", "cluster"], "7.4.1": ["sync", "async", "connection", "cluster"], "8.0.1": ["sync", "async", "connection", "cluster"]}}, + "socket_keepalive": {"environment": true, "redis_py": {"5.3.1": ["sync", "async", "connection", "cluster"], "6.4.0": ["sync", "async", "connection", "cluster"], "7.4.1": ["sync", "async", "connection", "cluster"], "8.0.1": ["sync", "async", "connection", "cluster"]}}, + "socket_keepalive_options": {"environment": true, "redis_py": {"5.3.1": ["sync", "async", "connection", "cluster"], "6.4.0": ["sync", "async", "connection", "cluster"], "7.4.1": ["sync", "async", "connection", "cluster"], "8.0.1": ["sync", "async", "connection", "cluster"]}}, + "socket_read_size": {"environment": false, "redis_py": {"5.3.1": ["connection"], "6.4.0": ["connection"], "7.4.1": ["connection"], "8.0.1": ["sync", "async", "connection", "cluster"]}}, + "socket_timeout": {"environment": true, "redis_py": {"5.3.1": ["sync", "async", "connection", "cluster"], "6.4.0": ["sync", "async", "connection", "cluster"], "7.4.1": ["sync", "async", "connection", "cluster"], "8.0.1": ["sync", "async", "connection", "cluster"]}}, + "socket_type": {"environment": false, "redis_py": {"5.3.1": ["connection"], "6.4.0": ["connection"], "7.4.1": ["connection"], "8.0.1": ["connection"]}}, + "ssl": {"environment": true, "redis_py": {"5.3.1": ["sync", "async", "cluster"], "6.4.0": ["sync", "async", "cluster"], "7.4.1": ["sync", "async", "cluster"], "8.0.1": ["sync", "async", "cluster"]}}, + "ssl_ca_certs": {"environment": true, "redis_py": {"5.3.1": ["sync", "async", "cluster"], "6.4.0": ["sync", "async", "cluster"], "7.4.1": ["sync", "async", "cluster"], "8.0.1": ["sync", "async", "cluster"]}}, + "ssl_ca_data": {"environment": true, "redis_py": {"5.3.1": ["sync", "async", "cluster"], "6.4.0": ["sync", "async", "cluster"], "7.4.1": ["sync", "async", "cluster"], "8.0.1": ["sync", "async", "cluster"]}}, + "ssl_ca_path": {"environment": true, "redis_py": {"5.3.1": ["sync"], "6.4.0": ["sync"], "7.4.1": ["sync", "async"], "8.0.1": ["sync", "async"]}}, + "ssl_cert_reqs": {"environment": true, "redis_py": {"5.3.1": ["sync", "async", "cluster"], "6.4.0": ["sync", "async", "cluster"], "7.4.1": ["sync", "async", "cluster"], "8.0.1": ["sync", "async", "cluster"]}}, + "ssl_certfile": {"environment": true, "redis_py": {"5.3.1": ["sync", "async", "cluster"], "6.4.0": ["sync", "async", "cluster"], "7.4.1": ["sync", "async", "cluster"], "8.0.1": ["sync", "async", "cluster"]}}, + "ssl_check_hostname": {"environment": true, "redis_py": {"5.3.1": ["sync", "async", "cluster"], "6.4.0": ["sync", "async", "cluster"], "7.4.1": ["sync", "async", "cluster"], "8.0.1": ["sync", "async", "cluster"]}}, + "ssl_ciphers": {"environment": true, "redis_py": {"5.3.1": ["sync", "async", "cluster"], "6.4.0": ["sync", "async", "cluster"], "7.4.1": ["sync", "async", "cluster"], "8.0.1": ["sync", "async", "cluster"]}}, + "ssl_exclude_verify_flags": {"environment": false, "redis_py": {"7.4.1": ["sync", "async", "cluster"], "8.0.1": ["sync", "async", "cluster"]}}, + "ssl_include_verify_flags": {"environment": false, "redis_py": {"7.4.1": ["sync", "async", "cluster"], "8.0.1": ["sync", "async", "cluster"]}}, + "ssl_keyfile": {"environment": true, "redis_py": {"5.3.1": ["sync", "async", "cluster"], "6.4.0": ["sync", "async", "cluster"], "7.4.1": ["sync", "async", "cluster"], "8.0.1": ["sync", "async", "cluster"]}}, + "ssl_min_version": {"environment": true, "redis_py": {"5.3.1": ["sync", "async", "cluster"], "6.4.0": ["sync", "async", "cluster"], "7.4.1": ["sync", "async", "cluster"], "8.0.1": ["sync", "async", "cluster"]}}, + "ssl_ocsp_context": {"environment": true, "redis_py": {"5.3.1": ["sync"], "6.4.0": ["sync"], "7.4.1": ["sync"], "8.0.1": ["sync"]}}, + "ssl_ocsp_expected_cert": {"environment": true, "redis_py": {"5.3.1": ["sync"], "6.4.0": ["sync"], "7.4.1": ["sync"], "8.0.1": ["sync"]}}, + "ssl_password": {"environment": true, "redis_py": {"5.3.1": ["sync"], "6.4.0": ["sync"], "7.4.1": ["sync", "async"], "8.0.1": ["sync", "async"]}}, + "ssl_validate_ocsp": {"environment": true, "redis_py": {"5.3.1": ["sync"], "6.4.0": ["sync"], "7.4.1": ["sync"], "8.0.1": ["sync"]}}, + "ssl_validate_ocsp_stapled": {"environment": true, "redis_py": {"5.3.1": ["sync"], "6.4.0": ["sync"], "7.4.1": ["sync"], "8.0.1": ["sync"]}}, + "startup_nodes": {"environment": false, "redis_py": {"5.3.1": ["cluster"], "6.4.0": ["cluster"], "7.4.1": ["cluster"], "8.0.1": ["cluster"]}}, + "unix_socket_path": {"environment": true, "redis_py": {"5.3.1": ["sync", "async"], "6.4.0": ["sync", "async"], "7.4.1": ["sync", "async"], "8.0.1": ["sync", "async"]}}, + "url": {"environment": true, "redis_py": {}}, + "username": {"environment": true, "redis_py": {"5.3.1": ["sync", "async", "connection", "cluster"], "6.4.0": ["sync", "async", "connection", "cluster"], "7.4.1": ["sync", "async", "connection", "cluster"], "8.0.1": ["sync", "async", "connection", "cluster"]}} } } diff --git a/litellm-rust/crates/redis-resolve/src/keys.rs b/litellm-rust/crates/redis-resolve/src/keys.rs index 863770c3191..b1d04356aee 100644 --- a/litellm-rust/crates/redis-resolve/src/keys.rs +++ b/litellm-rust/crates/redis-resolve/src/keys.rs @@ -1,184 +1,265 @@ use litellm_redis_types::Ignored; use serde_json::Value; -// Every Redis setting litellm/_redis.py reads, with how Rust handles it and where else Python looks for it. -// Warn rows are read by Python and dropped here with a warning naming the consequence, never an error. -// Reject rows are security checks Rust cannot perform, so asking for one fails instead of connecting without it. -// environment marks the kwargs _get_redis_env_kwarg_mapping reads as REDIS_, and fallback lists the names -// _get_redis_client_logic consults with get_secret when the merged kwargs lack the setting -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +/// How the Rust client honors a redis-py kwarg. +#[derive(Clone, Copy, Debug)] pub enum Handling { + /// Modeled by the resolver into a field of `Options`. Read, - Warn, + /// Dropped and returned as an `Ignored` naming `consequence`, unless `met` says the value asks for nothing the + /// Rust client does not already do. + Warn { + consequence: &'static str, + met: Option bool>, + }, + /// A security check the Rust client cannot perform, so setting it to true fails instead of connecting without it. Reject, } +/// What the value is, which decides where an unread setting lands in a `Resolution`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Shape { + /// Plain data, kept verbatim in `passthrough`. + Plain, + /// A Python object (a class, callable or instance), named in `host_objects` for the host to reattach. + Object, +} + +/// Whether redis-py's `parse_url` reads the setting from a url's query and the Rust resolver does too. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FromUrl { + No, + Any, + Rediss, +} + +#[derive(Clone, Copy, Debug)] pub struct Setting { pub kwarg: &'static str, pub handling: Handling, + pub shape: Shape, + pub from_url: FromUrl, + /// `_get_redis_env_kwarg_mapping` reads it as `REDIS_`. pub environment: bool, + /// The names `_merge_redis_kwargs` reads with `get_secret` when the merged kwargs lack the setting. pub fallback: &'static [&'static str], } impl Setting { - const fn new( - kwarg: &'static str, - handling: Handling, - environment: bool, - fallback: &'static [&'static str], - ) -> Self { + const fn new(kwarg: &'static str, handling: Handling) -> Self { Self { kwarg, handling, - environment, - fallback, + shape: Shape::Plain, + from_url: FromUrl::No, + environment: false, + fallback: &[], } } + const fn env(self) -> Self { + Self { + environment: true, + ..self + } + } + + const fn object(self) -> Self { + Self { + shape: Shape::Object, + ..self + } + } + + const fn in_url(self, from_url: FromUrl) -> Self { + Self { from_url, ..self } + } + + const fn or_secrets(self, fallback: &'static [&'static str]) -> Self { + Self { fallback, ..self } + } + pub fn environment_name(&self) -> Option { self.environment .then(|| format!("REDIS_{}", self.kwarg.to_ascii_uppercase())) } -} -pub const SETTINGS: &[Setting] = &[ - Setting::new("aws_iam_auth", Handling::Read, true, &[]), - Setting::new("aws_iam_cache_name", Handling::Read, true, &[]), - Setting::new( - "aws_iam_region", - Handling::Read, - true, - &["AWS_REGION", "AWS_DEFAULT_REGION"], - ), - Setting::new("aws_iam_serverless", Handling::Read, true, &[]), - Setting::new("aws_iam_user_name", Handling::Read, true, &[]), - Setting::new( - "azure_client_id", - Handling::Read, - true, - &["AZURE_CLIENT_ID"], - ), - Setting::new( - "azure_client_secret", - Handling::Read, - true, - &["AZURE_CLIENT_SECRET"], - ), - Setting::new( - "azure_redis_ad_token", - Handling::Read, - true, - &["REDIS_AZURE_AD_TOKEN"], - ), - Setting::new( - "azure_tenant_id", - Handling::Read, - true, - &["AZURE_TENANT_ID"], - ), - Setting::new("cache", Handling::Warn, true, &[]), - Setting::new("cache_config", Handling::Warn, true, &[]), - Setting::new("charset", Handling::Warn, true, &[]), - Setting::new("client_name", Handling::Warn, true, &[]), - Setting::new("connection_pool", Handling::Warn, false, &[]), - Setting::new("credential_provider", Handling::Read, false, &[]), - Setting::new("db", Handling::Read, true, &[]), - Setting::new("decode_responses", Handling::Warn, true, &[]), - Setting::new("encoding", Handling::Warn, true, &[]), - Setting::new("encoding_errors", Handling::Warn, true, &[]), - Setting::new("errors", Handling::Warn, true, &[]), - Setting::new("event_dispatcher", Handling::Warn, true, &[]), - Setting::new( - "gcp_service_account", - Handling::Read, - true, - &["REDIS_GCP_SERVICE_ACCOUNT"], - ), - Setting::new( - "gcp_ssl_ca_certs", - Handling::Read, - true, - &["REDIS_GCP_SSL_CA_CERTS"], - ), - Setting::new("health_check_interval", Handling::Read, true, &[]), - Setting::new("host", Handling::Read, true, &[]), - Setting::new("lib_name", Handling::Warn, true, &[]), - Setting::new("lib_version", Handling::Warn, true, &[]), - Setting::new("max_connections", Handling::Warn, true, &[]), - Setting::new("namespace", Handling::Read, false, &[]), - Setting::new("password", Handling::Read, true, &[]), - Setting::new("port", Handling::Read, true, &[]), - Setting::new("protocol", Handling::Warn, true, &[]), - Setting::new("redis_connect_func", Handling::Warn, true, &[]), - Setting::new("retry", Handling::Warn, false, &[]), - Setting::new("retry_on_error", Handling::Warn, true, &[]), - Setting::new("retry_on_timeout", Handling::Warn, true, &[]), - Setting::new( - "sentinel_nodes", - Handling::Read, - false, - &["REDIS_SENTINEL_NODES"], - ), - Setting::new( - "sentinel_password", - Handling::Read, - false, - &["REDIS_SENTINEL_PASSWORD"], - ), - Setting::new( - "service_name", - Handling::Read, - false, - &["REDIS_SERVICE_NAME"], - ), - Setting::new("single_connection_client", Handling::Warn, true, &[]), - Setting::new("socket_connect_timeout", Handling::Read, true, &[]), - Setting::new("socket_keepalive", Handling::Warn, true, &[]), - Setting::new("socket_keepalive_options", Handling::Warn, true, &[]), - Setting::new("socket_timeout", Handling::Read, true, &[]), - Setting::new("ssl", Handling::Read, true, &[]), - Setting::new("ssl_ca_certs", Handling::Read, true, &[]), - Setting::new("ssl_ca_data", Handling::Read, true, &[]), - Setting::new("ssl_ca_path", Handling::Read, true, &[]), - Setting::new("ssl_cert_reqs", Handling::Read, true, &[]), - Setting::new("ssl_certfile", Handling::Read, true, &[]), - Setting::new("ssl_check_hostname", Handling::Read, true, &[]), - Setting::new("ssl_ciphers", Handling::Warn, true, &[]), - Setting::new("ssl_keyfile", Handling::Read, true, &[]), - Setting::new("ssl_min_version", Handling::Warn, true, &[]), - Setting::new("ssl_ocsp_context", Handling::Warn, true, &[]), - Setting::new("ssl_ocsp_expected_cert", Handling::Warn, true, &[]), - Setting::new("ssl_password", Handling::Read, true, &[]), - Setting::new("ssl_validate_ocsp", Handling::Reject, true, &[]), - Setting::new("ssl_validate_ocsp_stapled", Handling::Reject, true, &[]), - Setting::new( - "startup_nodes", - Handling::Read, - false, - &["REDIS_CLUSTER_NODES"], - ), - Setting::new("unix_socket_path", Handling::Read, true, &[]), - Setting::new("url", Handling::Read, true, &[]), - Setting::new("username", Handling::Read, true, &[]), -]; + pub fn is_read(&self) -> bool { + matches!(self.handling, Handling::Read) + } -pub(crate) fn is_refused_when_set(kwarg: &str) -> bool { - setting(kwarg).is_some_and(|setting| setting.handling == Handling::Reject) -} - -pub(crate) fn ignored(setting: String, kwarg: &str) -> Ignored { - Ignored { - setting, - consequence: consequence(kwarg), + /// The warning a set value earns, or `None` when the setting is read, refused or already met. + pub(crate) fn warning(&self, value: &Value) -> Option<&'static str> { + match self.handling { + Handling::Warn { consequence, met } if !met.is_some_and(|met| met(value)) => { + Some(consequence) + } + _ => None, + } } } +const fn read(kwarg: &'static str) -> Setting { + Setting::new(kwarg, Handling::Read) +} + +const fn warn(kwarg: &'static str, consequence: &'static str) -> Setting { + Setting::new( + kwarg, + Handling::Warn { + consequence, + met: None, + }, + ) +} + +const fn warn_unless( + kwarg: &'static str, + consequence: &'static str, + met: fn(&Value) -> bool, +) -> Setting { + Setting::new( + kwarg, + Handling::Warn { + consequence, + met: Some(met), + }, + ) +} + +const fn reject(kwarg: &'static str) -> Setting { + Setting::new(kwarg, Handling::Reject) +} + +const NO_EQUIVALENT: &str = "the Rust client has no equivalent"; +const OWN_STACK: &str = "the Rust client uses its own pool, retries and events"; +const OWN_CONNECTION: &str = "it tunes redis-py's own connection, and the Rust client has its own"; +const OWN_CLUSTER: &str = "the Rust cluster client discovers, routes and retries on its own"; +const MAINTENANCE: &str = "the Rust client does not follow server maintenance notifications"; +const RUSTLS: &str = "rustls negotiates TLS 1.2+ with its own ECDHE AEAD suites"; +const OCSP: &str = "it only configures ssl_validate_ocsp, which the Rust client refuses when set"; +const VERIFY_FLAGS: &str = "rustls verifies certificates with its own fixed rules, so no extra X.509 flag such as CRL checking applies"; + +/// Every Redis kwarg redis-py or `litellm/_redis.py` accepts, one row each. `redis-py-kwargs.json` records which +/// redis-py versions and `REDIS_*` names accept them, and `tests/catalog.rs` keeps the two in step. +pub const SETTINGS: &[Setting] = &[ + warn("address_remap", OWN_CLUSTER).object(), + warn("auto_close_connection_pool", OWN_STACK), + read("aws_iam_auth").env(), + read("aws_iam_cache_name").env(), + read("aws_iam_region") + .env() + .or_secrets(&["AWS_REGION", "AWS_DEFAULT_REGION"]), + read("aws_iam_serverless").env(), + read("aws_iam_user_name").env(), + read("azure_client_id") + .env() + .or_secrets(&["AZURE_CLIENT_ID"]), + read("azure_client_secret") + .env() + .or_secrets(&["AZURE_CLIENT_SECRET"]), + read("azure_redis_ad_token") + .env() + .or_secrets(&["REDIS_AZURE_AD_TOKEN"]), + read("azure_tenant_id") + .env() + .or_secrets(&["AZURE_TENANT_ID"]), + warn("cache", NO_EQUIVALENT).env().object(), + warn("cache_config", NO_EQUIVALENT).env().object(), + warn("charset", NO_EQUIVALENT).env(), + warn("client_name", NO_EQUIVALENT).env(), + warn("cluster_error_retry_attempts", OWN_CLUSTER), + warn("command_packer", OWN_CONNECTION).object(), + warn("connection_class", OWN_STACK).object(), + warn("connection_error_retry_attempts", OWN_CLUSTER), + warn("connection_pool", OWN_STACK).object(), + read("credential_provider"), + read("db").env().in_url(FromUrl::Any), + warn("decode_responses", NO_EQUIVALENT).env(), + warn("driver_info", NO_EQUIVALENT).object(), + warn("dynamic_startup_nodes", OWN_CLUSTER), + warn("encoder_class", OWN_CONNECTION).object(), + warn("encoding", NO_EQUIVALENT).env(), + warn("encoding_errors", NO_EQUIVALENT).env(), + warn("errors", NO_EQUIVALENT).env(), + warn("event_dispatcher", OWN_STACK).env().object(), + read("gcp_service_account") + .env() + .or_secrets(&["REDIS_GCP_SERVICE_ACCOUNT"]), + read("gcp_ssl_ca_certs") + .env() + .or_secrets(&["REDIS_GCP_SSL_CA_CERTS"]), + read("health_check_interval").env(), + read("host").env(), + warn("legacy_responses", NO_EQUIVALENT), + warn("lib_name", NO_EQUIVALENT).env(), + warn("lib_version", NO_EQUIVALENT).env(), + warn("load_balancing_strategy", OWN_CLUSTER), + warn("maint_notifications_config", MAINTENANCE).object(), + warn("maint_notifications_pool_handler", MAINTENANCE).object(), + warn("maintenance_notification_hash", MAINTENANCE), + warn("maintenance_state", MAINTENANCE), + warn("max_connections", "the Rust client has no connection cap").env(), + read("namespace"), + warn("orig_host_address", MAINTENANCE), + warn("orig_socket_connect_timeout", MAINTENANCE), + warn("orig_socket_timeout", MAINTENANCE), + warn("oss_cluster_maint_notifications_handler", MAINTENANCE).object(), + warn("parser", OWN_CONNECTION), + warn("parser_class", OWN_CONNECTION).object(), + read("password").env().in_url(FromUrl::Any), + warn("path", OWN_CLUSTER), + warn("policy_resolver", OWN_CLUSTER).object(), + read("port").env(), + warn("protocol", NO_EQUIVALENT).env(), + warn("read_from_replicas", OWN_CLUSTER), + warn( + "redis_connect_func", + "the Python object is not callable from Rust, so static username and password are used", + ) + .env() + .object(), + warn("reinitialize_steps", OWN_CLUSTER), + warn("require_full_coverage", OWN_CLUSTER), + warn("retry", OWN_STACK).object(), + warn("retry_on_error", NO_EQUIVALENT).env().object(), + warn("retry_on_timeout", NO_EQUIVALENT).env(), + read("sentinel_nodes").or_secrets(&["REDIS_SENTINEL_NODES"]), + read("sentinel_password").or_secrets(&["REDIS_SENTINEL_PASSWORD"]), + read("service_name").or_secrets(&["REDIS_SERVICE_NAME"]), + warn("single_connection_client", NO_EQUIVALENT).env(), + read("socket_connect_timeout").env(), + warn("socket_keepalive", NO_EQUIVALENT).env(), + warn("socket_keepalive_options", NO_EQUIVALENT).env(), + warn("socket_read_size", OWN_CONNECTION), + read("socket_timeout").env(), + warn("socket_type", OWN_CONNECTION), + read("ssl").env(), + read("ssl_ca_certs").env().in_url(FromUrl::Rediss), + read("ssl_ca_data").env().in_url(FromUrl::Rediss), + read("ssl_ca_path").env().in_url(FromUrl::Rediss), + read("ssl_cert_reqs").env().in_url(FromUrl::Rediss), + read("ssl_certfile").env().in_url(FromUrl::Rediss), + read("ssl_check_hostname").env().in_url(FromUrl::Rediss), + warn("ssl_ciphers", RUSTLS).env(), + warn("ssl_exclude_verify_flags", VERIFY_FLAGS), + warn("ssl_include_verify_flags", VERIFY_FLAGS), + read("ssl_keyfile").env().in_url(FromUrl::Rediss), + warn_unless("ssl_min_version", RUSTLS, is_met_by_rustls).env(), + warn("ssl_ocsp_context", OCSP).env().object(), + warn("ssl_ocsp_expected_cert", OCSP).env(), + read("ssl_password").env().in_url(FromUrl::Rediss), + reject("ssl_validate_ocsp").env(), + reject("ssl_validate_ocsp_stapled").env(), + read("startup_nodes").or_secrets(&["REDIS_CLUSTER_NODES"]), + read("unix_socket_path").env(), + read("url").env(), + read("username").env().in_url(FromUrl::Any), +]; + // rustls only speaks TLS 1.2 and 1.3, so a minimum of 1.2 or lower is already met. redis-py takes an // ssl.TLSVersion, which reaches Rust as its name or its value -pub(crate) fn is_met_by_rustls(kwarg: &str, value: &Value) -> bool { - if kwarg != "ssl_min_version" { - return false; - } +fn is_met_by_rustls(value: &Value) -> bool { match value { Value::Number(number) => matches!(number.as_i64(), Some(-2 | 769 | 770 | 771)), Value::String(text) => matches!( @@ -192,48 +273,36 @@ pub(crate) fn is_met_by_rustls(kwarg: &str, value: &Value) -> bool { } } -// redis-py parameters whose value is a Python object rather than plain data. credential_provider is modeled as -// Auth::Provider instead -const HOST_OBJECTS: &[&str] = &[ - "address_remap", - "cache", - "cache_config", - "connection_class", - "connection_pool", - "driver_info", - "event_dispatcher", - "maint_notifications_config", - "parser_class", - "redis_connect_func", - "retry", - "retry_on_error", - "ssl_ocsp_context", -]; - -pub(crate) fn is_host_object(kwarg: &str) -> bool { - HOST_OBJECTS.contains(&kwarg) -} - -/// What ignoring a warned setting means for the connection, so the operator reads the consequence -/// rather than only the key name. -fn consequence(kwarg: &str) -> &'static str { - match kwarg { - "ssl_ocsp_context" | "ssl_ocsp_expected_cert" => { - "it only configures ssl_validate_ocsp, which the Rust client refuses when set" - } - "ssl_ciphers" | "ssl_min_version" => { - "rustls negotiates TLS 1.2+ with its own ECDHE AEAD suites" - } - "credential_provider" | "redis_connect_func" => { - "the Python object is not callable from Rust, so static username and password are used" - } - "connection_pool" | "retry" | "event_dispatcher" => { - "the Rust client uses its own pool, retries and events" - } - _ => "the Rust client has no equivalent", - } -} - pub fn setting(kwarg: &str) -> Option<&'static Setting> { SETTINGS.iter().find(|setting| setting.kwarg == kwarg) } + +pub(crate) fn is_refused_when_set(kwarg: &str) -> bool { + setting(kwarg).is_some_and(|setting| matches!(setting.handling, Handling::Reject)) +} + +/// A warned setting the kwargs set, with what dropping it means. A kwarg with no row is not a Redis setting, so it +/// passes through without a warning. +pub(crate) fn ignored(source: &str, kwarg: &str, value: &Value) -> Option { + let consequence = setting(kwarg)?.warning(value)?; + Some(Ignored { + setting: source.to_owned(), + consequence, + }) +} + +/// A url query parameter the resolver does not read. redis-py turns every one into a client kwarg, so even one with +/// no row is reported. +pub(crate) fn ignored_query(source: &str, name: &str, value: &Value) -> Option { + let consequence = match setting(name) { + None => NO_EQUIVALENT, + Some(setting) if setting.is_read() => { + "the Rust client does not read it from this url's query" + } + Some(setting) => setting.warning(value)?, + }; + Some(Ignored { + setting: source.to_owned(), + consequence, + }) +} diff --git a/litellm-rust/crates/redis-resolve/src/lib.rs b/litellm-rust/crates/redis-resolve/src/lib.rs index 1c68b98a7d4..0b8fb19b70b 100644 --- a/litellm-rust/crates/redis-resolve/src/lib.rs +++ b/litellm-rust/crates/redis-resolve/src/lib.rs @@ -11,7 +11,7 @@ mod options; mod tls; pub use error::{Expected, RedisError}; -pub use keys::{Handling, SETTINGS, Setting, setting}; +pub use keys::{FromUrl, Handling, SETTINGS, Setting, Shape, setting}; pub use options::is_present; use options::Options; diff --git a/litellm-rust/crates/redis-resolve/src/options.rs b/litellm-rust/crates/redis-resolve/src/options.rs index 819863d9edb..16dced92b5c 100644 --- a/litellm-rust/crates/redis-resolve/src/options.rs +++ b/litellm-rust/crates/redis-resolve/src/options.rs @@ -7,7 +7,7 @@ use serde_json::Value; use crate::{ Entry, Expected, Kwargs, RedisError, - keys::{self, Handling}, + keys::{self, Shape}, }; /// Every setting the resolver reads. A setting is `None` when it is unset, null or empty. @@ -84,10 +84,11 @@ impl Unmodeled { let unmodeled = || { kwargs .iter() - .filter(|(key, _)| handling(key) != Some(Handling::Read)) + .filter(|(key, _)| !keys::setting(key).is_some_and(keys::Setting::is_read)) }; - let (objects, plain): (Vec<_>, Vec<_>) = - unmodeled().partition(|(key, _)| keys::is_host_object(key)); + let (objects, plain): (Vec<_>, Vec<_>) = unmodeled().partition(|(key, _)| { + keys::setting(key).is_some_and(|setting| setting.shape == Shape::Object) + }); let passthrough = plain .into_iter() .map(|(key, entry)| (key.clone(), entry.value.clone())) @@ -96,11 +97,7 @@ impl Unmodeled { passthrough: Passthrough::new(passthrough), host_objects: objects.into_iter().map(|(key, _)| key.clone()).collect(), ignored: unmodeled() - .filter(|(key, entry)| { - handling(key) == Some(Handling::Warn) - && !keys::is_met_by_rustls(key, &entry.value) - }) - .map(|(key, entry)| keys::ignored(entry.source.clone(), key)) + .filter_map(|(key, entry)| keys::ignored(&entry.source, key, &entry.value)) .collect(), }) } @@ -110,13 +107,17 @@ impl Options { /// The typed settings and the ones the resolver leaves to the host. pub(super) fn read(kwargs: &Kwargs) -> Result<(Self, Unmodeled), RedisError> { let unmodeled = Unmodeled::read(kwargs)?; - let get = |key: &str| { + let options = Self::from_lookup(|key| { kwargs .get(key) .filter(|entry| is_present(&entry.value)) .cloned() - }; - let options = Self { + }); + Ok((options, unmodeled)) + } + + fn from_lookup(get: impl Fn(&'static str) -> Option) -> Self { + Self { url: get("url"), host: get("host"), port: get("port"), @@ -163,8 +164,7 @@ impl Options { socket_timeout: get("socket_timeout"), socket_connect_timeout: get("socket_connect_timeout"), health_check_interval: get("health_check_interval"), - }; - Ok((options, unmodeled)) + } } } @@ -183,10 +183,6 @@ pub(super) fn refuse_unenforceable<'a>( }) } -fn handling(key: &str) -> Option { - keys::setting(key).map(|setting| setting.handling) -} - // Python reads most of these with `x or fallback`, so an empty value counts as unset everywhere pub fn is_present(value: &Value) -> bool { match value { @@ -283,3 +279,30 @@ fn coerced( } } } + +#[cfg(test)] +mod tests { + use std::{cell::RefCell, collections::BTreeSet}; + + use rstest::rstest; + + use super::Options; + use crate::SETTINGS; + + // a Read row that Options never asks for would be neither modeled nor passed through nor warned about + #[rstest] + fn options_ask_for_exactly_the_read_rows() { + let asked = RefCell::new(BTreeSet::new()); + let _ = Options::from_lookup(|kwarg| { + asked.borrow_mut().insert(kwarg); + None + }); + + let read: BTreeSet<&str> = SETTINGS + .iter() + .filter(|setting| setting.is_read()) + .map(|setting| setting.kwarg) + .collect(); + assert_eq!(asked.into_inner(), read); + } +} diff --git a/litellm-rust/crates/redis-resolve/src/tls.rs b/litellm-rust/crates/redis-resolve/src/tls.rs index 113db93d38d..a55f1be4b88 100644 --- a/litellm-rust/crates/redis-resolve/src/tls.rs +++ b/litellm-rust/crates/redis-resolve/src/tls.rs @@ -5,25 +5,11 @@ use litellm_secret_value::SecretValue; use serde_json::Value; use crate::{ - Entry, Expected, RedisError, keys, + Entry, Expected, RedisError, + keys::{self, FromUrl}, options::{self, TlsInput, flag, parsed, secret, text}, }; -// redis-py's parse_url turns every query parameter into a client kwarg. Only these and, on a rediss:// url, the -// TLS ones have a redis-rs equivalent, so any other parameter is ignored -const URL_CONNECTION_SETTINGS: &[&str] = &["db", "username", "password"]; - -const URL_TLS_SETTINGS: &[&str] = &[ - "ssl_cert_reqs", - "ssl_ca_certs", - "ssl_ca_data", - "ssl_ca_path", - "ssl_certfile", - "ssl_keyfile", - "ssl_password", - "ssl_check_hostname", -]; - /// `gcp_ca_certs` replaces `ssl_ca_certs` when GCP IAM is on, as `_get_redis_client_logic` does. pub(super) fn tls_options( input: &TlsInput, @@ -85,8 +71,11 @@ pub(super) fn url_query(url: &str) -> Result { .collect(); options::refuse_unenforceable(query.iter().map(|(name, entry)| (name, entry)))?; let (query, ignored): (Vec<_>, Vec<_>) = query.into_iter().partition(|(name, _)| { - URL_CONNECTION_SETTINGS.contains(&name.as_str()) - || (tls_scheme && URL_TLS_SETTINGS.contains(&name.as_str())) + keys::setting(name).is_some_and(|setting| match setting.from_url { + FromUrl::Any => true, + FromUrl::Rediss => tls_scheme, + FromUrl::No => false, + }) }); let get = |key: &str| { query @@ -118,9 +107,8 @@ pub(super) fn url_query(url: &str) -> Result { username: secret(get("username").as_ref()), password: secret(get("password").as_ref()), ignored: ignored - .into_iter() - .filter(|(name, _)| !keys::is_refused_when_set(name)) - .map(|(name, entry)| keys::ignored(entry.source, &name)) + .iter() + .filter_map(|(name, entry)| keys::ignored_query(&entry.source, name, &entry.value)) .collect(), }) } diff --git a/litellm-rust/crates/redis-resolve/tests/catalog.rs b/litellm-rust/crates/redis-resolve/tests/catalog.rs new file mode 100644 index 00000000000..1ba97297329 --- /dev/null +++ b/litellm-rust/crates/redis-resolve/tests/catalog.rs @@ -0,0 +1,94 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use litellm_redis_resolve::{Handling, RedisError, SETTINGS, Shape, interpret, kwargs}; +use rstest::rstest; +use serde::Deserialize; +use serde_json::json; + +#[derive(Deserialize)] +struct Catalog { + args: BTreeMap, +} + +#[derive(Deserialize)] +struct Arg { + environment: bool, +} + +fn catalog() -> Catalog { + serde_json::from_str(include_str!("../redis-py-kwargs.json")).unwrap() +} + +fn host_with(kwarg: &str, value: serde_json::Value) -> litellm_redis_resolve::Kwargs { + kwargs([ + ("host".to_owned(), json!("redis")), + (kwarg.to_owned(), value), + ]) +} + +#[rstest] +fn every_catalogued_kwarg_has_exactly_one_settings_row() { + let rows: Vec<&str> = SETTINGS.iter().map(|setting| setting.kwarg).collect(); + let unique: BTreeSet<&str> = rows.iter().copied().collect(); + assert_eq!(rows.len(), unique.len(), "a kwarg has two SETTINGS rows"); + + let catalog = catalog(); + let catalogued: BTreeSet<&str> = catalog.args.keys().map(String::as_str).collect(); + assert_eq!(unique, catalogued); +} + +#[rstest] +fn settings_read_redis_environment_names_exactly_where_python_does() { + let catalog = catalog(); + + let differing: Vec<&str> = SETTINGS + .iter() + .filter(|setting| { + catalog + .args + .get(setting.kwarg) + .is_some_and(|arg| arg.environment != setting.environment) + }) + .map(|setting| setting.kwarg) + .collect(); + assert!(differing.is_empty(), "{differing:?}"); +} + +#[rstest] +fn every_warned_setting_is_reported_and_still_reaches_the_host() { + let warned = SETTINGS + .iter() + .filter(|setting| matches!(setting.handling, Handling::Warn { met: None, .. })); + + for setting in warned { + let resolved = interpret(&host_with(setting.kwarg, json!("set")), None).unwrap(); + + let reported: Vec<&str> = resolved + .ignored + .iter() + .map(|ignored| ignored.setting.as_str()) + .collect(); + assert_eq!(reported, vec![setting.kwarg]); + let reached = match setting.shape { + Shape::Plain => resolved.passthrough.get(setting.kwarg) == Some(&json!("set")), + Shape::Object => resolved.host_objects.contains(setting.kwarg), + }; + assert!(reached, "{} did not reach the host", setting.kwarg); + } +} + +#[rstest] +fn every_rejected_setting_fails_when_asked_for() { + let rejected = SETTINGS + .iter() + .filter(|setting| matches!(setting.handling, Handling::Reject)); + + for setting in rejected { + let error = interpret(&host_with(setting.kwarg, json!(true)), None).unwrap_err(); + + assert!( + matches!(&error, RedisError::Unenforceable { setting: source } if source == setting.kwarg), + "{error:?}" + ); + } +} diff --git a/tests/unit/test_redis.py b/tests/unit/test_redis.py index f85a7fcbec0..f6c5ebafb92 100644 --- a/tests/unit/test_redis.py +++ b/tests/unit/test_redis.py @@ -2,6 +2,7 @@ import inspect import json import os import ssl +from pathlib import Path from types import SimpleNamespace from typing import Final from unittest.mock import AsyncMock, MagicMock, patch @@ -3249,3 +3250,46 @@ def test_redis_settings_resolve_like_the_rust_resolver_expects(monkeypatch, kwar outcome = {"rejected": {"type": type(error).__name__, "message": str(error)}} assert outcome == expected + + +_REDIS_PY_KWARGS: Final = json.loads( + (Path(__file__).parents[2] / "litellm-rust/crates/redis-resolve/redis-py-kwargs.json").read_text() +) + + +def _named_args(*classes: type) -> frozenset[str]: + return frozenset( + name + for cls in classes + for spec in (inspect.getfullargspec(inspect.unwrap(cls.__init__)),) + for name in spec.args + spec.kwonlyargs + if name != "self" + ) + + +def test_redis_py_kwargs_catalog_matches_the_installed_redis_py(): + assert redis.__version__ in _REDIS_PY_KWARGS["meta"]["redis_py_versions"] + signatures: Final = { + "sync": _named_args(redis.Redis), + "async": _named_args(async_redis.Redis), + "connection": _named_args( + *(cls for cls in (*redis.Connection.__mro__, *async_redis.Connection.__mro__) if cls is not object) + ), + "cluster": _named_args(async_redis.RedisCluster), + } + args: Final = _REDIS_PY_KWARGS["args"] + names: Final = frozenset(args).union(*signatures.values()) + + catalogued: Final = {name: args.get(name, {}).get("redis_py", {}).get(redis.__version__, []) for name in names} + installed: Final = {name: [kind for kind, named in signatures.items() if name in named] for name in names} + assert catalogued == installed + + +@pytest.mark.skipif( + redis.__version__ != _REDIS_PY_KWARGS["meta"]["redis_py_locked"], + reason="the catalog records REDIS_* names for the locked redis-py only", +) +def test_redis_py_kwargs_catalog_names_the_environment_kwargs_python_reads(): + catalogued: Final = frozenset(name for name, arg in _REDIS_PY_KWARGS["args"].items() if arg["environment"]) + + assert catalogued == frozenset(_get_redis_env_kwarg_mapping().values())