From 191524ef1f46f73e53bc1628243ae0803676d0ef Mon Sep 17 00:00:00 2001 From: dmaier-redislabs Date: Mon, 15 Dec 2025 15:31:20 +0100 Subject: [PATCH 1/2] Added a locl digest command to the client to execute the XXH3 locally rather than asking the server for it --- pyproject.toml | 5 ++++- redis/commands/core.py | 23 +++++++++++++++++++++++ tests/test_commands.py | 3 +++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f1cedefead..85e25059d5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,10 @@ classifiers = [ "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", ] -dependencies = ['async-timeout>=4.0.3; python_full_version<"3.11.3"'] +dependencies = [ + 'async-timeout>=4.0.3; python_full_version<"3.11.3"', + 'xxhash~=3.6.0', +] [project.optional-dependencies] hiredis = [ diff --git a/redis/commands/core.py b/redis/commands/core.py index 525b31c99d..3760669777 100644 --- a/redis/commands/core.py +++ b/redis/commands/core.py @@ -2,6 +2,7 @@ import datetime import hashlib +import xxhash import warnings from enum import Enum from typing import ( @@ -23,6 +24,7 @@ Union, ) +from docs.conf import version from redis.exceptions import ConnectionError, DataError, NoScriptError, RedisError from redis.typing import ( AbsExpiryT, @@ -1888,6 +1890,27 @@ def expiretime(self, key: str) -> int: """ return self.execute_command("EXPIRETIME", key) + @experimental_method() + def digest_local(self, value: Union[bytes, str]) -> str: + """ + Compute the hexadecimal digest of the value locally, without sending it to the server. + + This is useful for conditional operations like IFDEQ/IFDNE where you need to + compute the digest client-side before sending a command. + + Warning: + **Experimental** - This API may change or be removed without notice. + + Arguments: + - value: Union[bytes, str] - the value to compute the digest of. + + Returns: + - (str) the XXH3 digest of the value as a hex string (16 hex characters) + + For more information, see https://redis.io/commands/digest + """ + return xxhash.xxh3_64(value).hexdigest() + @experimental_method() def digest(self, name: KeyT) -> Optional[str]: """ diff --git a/tests/test_commands.py b/tests/test_commands.py index d7b56ca32f..61b1674e8b 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -2575,6 +2575,9 @@ def test_set_ifdeq_and_ifdne(self, r, val): d = self._server_xxh3_digest(r, "k") assert d is not None + # sanity check: local digest matches server's + assert d == r.digest_local(val) + # IFDEQ must match to set; if key missing => won't create assert r.set("k", b"X", ifdeq=d) is True assert r.get("k") == b"X" From 3a58af2fc89a7cf0ba47d20d9a39e0946d0a8058 Mon Sep 17 00:00:00 2001 From: dmaier-redislabs Date: Mon, 15 Dec 2025 15:57:01 +0100 Subject: [PATCH 2/2] Removed an unnecessary import --- redis/commands/core.py | 1 - 1 file changed, 1 deletion(-) diff --git a/redis/commands/core.py b/redis/commands/core.py index 3760669777..857f29a5f2 100644 --- a/redis/commands/core.py +++ b/redis/commands/core.py @@ -24,7 +24,6 @@ Union, ) -from docs.conf import version from redis.exceptions import ConnectionError, DataError, NoScriptError, RedisError from redis.typing import ( AbsExpiryT,