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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions redis/commands/cluster.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"GEOPOS",
"GEORADIUS",
"GEORADIUSBYMEMBER",
"DIGEST",
"GET",
"GETBIT",
"GETRANGE",
Expand Down
130 changes: 111 additions & 19 deletions redis/commands/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
extract_expire_flags,
)

from .helpers import list_or_args
from .helpers import at_most_one_value_set, list_or_args

if TYPE_CHECKING:
import redis.asyncio.client
Expand Down Expand Up @@ -1729,6 +1729,49 @@ def delete(self, *names: KeyT) -> ResponseT:
def __delitem__(self, name: KeyT):
self.delete(name)

def delex(
self,
name: KeyT,
ifeq: Optional[Union[bytes, str]] = None,
ifne: Optional[Union[bytes, str]] = None,
ifdeq: Optional[str] = None, # hex digest
ifdne: Optional[str] = None, # hex digest
) -> int:
"""
Conditionally removes the specified key.

ifeq match-value - Delete the key only if its value is equal to match-value
ifne match-value - Delete the key only if its value is not equal to match-value
ifdeq match-digest - Delete the key only if the digest of its value is equal to match-digest
ifdne match-digest - Delete the key only if the digest of its value is not equal to match-digest

Returns:
int: 1 if the key was deleted, 0 otherwise.
Raises:
redis.exceptions.ResponseError: if key exists but is not a string
and a condition is specified.
ValueError: if more than one condition is provided.


Requires Redis 8.4 or greater.
For more information, see https://redis.io/commands/delex
"""
conds = [x is not None for x in (ifeq, ifne, ifdeq, ifdne)]
if sum(conds) > 1:
raise ValueError("Only one of IFEQ/IFNE/IFDEQ/IFDNE may be specified")

pieces = ["DELEX", name]
if ifeq is not None:
pieces += ["IFEQ", ifeq]
elif ifne is not None:
pieces += ["IFNE", ifne]
elif ifdeq is not None:
pieces += ["IFDEQ", ifdeq]
elif ifdne is not None:
pieces += ["IFDNE", ifdne]

return self.execute_command(*pieces)

def dump(self, name: KeyT) -> ResponseT:
"""
Return a serialized version of the value stored at the specified key.
Expand Down Expand Up @@ -1835,6 +1878,23 @@ def expiretime(self, key: str) -> int:
"""
return self.execute_command("EXPIRETIME", key)

def digest(self, name: KeyT) -> Optional[str]:
"""
Return the digest of the value stored at the specified key.

Returns:
- None if the key does not exist
- (bulk string) the XXH3 digest of the value as a hex string
Raises:
- ResponseError if key exists but is not a string


Requires Redis 8.4 or greater.
For more information, see https://redis.io/commands/digest
"""
# Bulk string response is already handled (bytes/str based on decode_responses)
return self.execute_command("DIGEST", name)

def get(self, name: KeyT) -> ResponseT:
"""
Return the value at key ``name``, or None if the key doesn't exist
Expand Down Expand Up @@ -1883,8 +1943,7 @@ def getex(

For more information, see https://redis.io/commands/getex
"""
opset = {ex, px, exat, pxat}
if len(opset) > 2 or len(opset) > 1 and persist:
if not at_most_one_value_set((ex, px, exat, pxat, persist)):
raise DataError(
"``ex``, ``px``, ``exat``, ``pxat``, "
"and ``persist`` are mutually exclusive."
Expand Down Expand Up @@ -2072,8 +2131,7 @@ def msetex(
Available since Redis 8.4
For more information, see https://redis.io/commands/msetex
"""
opset = {ex, px, exat, pxat}
if len(opset) > 2 or len(opset) > 1 and keepttl:
if not at_most_one_value_set((ex, px, exat, pxat, keepttl)):
raise DataError(
"``ex``, ``px``, ``exat``, ``pxat``, "
"and ``keepttl`` are mutually exclusive."
Expand Down Expand Up @@ -2339,6 +2397,10 @@ def set(
get: bool = False,
exat: Optional[AbsExpiryT] = None,
pxat: Optional[AbsExpiryT] = None,
ifeq: Optional[Union[bytes, str]] = None,
ifne: Optional[Union[bytes, str]] = None,
ifdeq: Optional[str] = None, # hex digest of current value
ifdne: Optional[str] = None, # hex digest of current value
) -> ResponseT:
"""
Set the value at key ``name`` to ``value``
Expand Down Expand Up @@ -2366,35 +2428,67 @@ def set(
``pxat`` sets an expire flag on key ``name`` for ``ex`` milliseconds,
specified in unix time.

``ifeq`` set the value at key ``name`` to ``value`` only if the current
value exactly matches the argument.
If key doesn’t exist - it won’t be created.
(Requires Redis 8.4 or greater)

``ifne`` set the value at key ``name`` to ``value`` only if the current
value does not exactly match the argument.
If key doesn’t exist - it will be created.
(Requires Redis 8.4 or greater)

``ifdeq`` set the value at key ``name`` to ``value`` only if the current
value XXH3 hex digest exactly matches the argument.
If key doesn’t exist - it won’t be created.
(Requires Redis 8.4 or greater)

``ifdne`` set the value at key ``name`` to ``value`` only if the current
value XXH3 hex digest does not exactly match the argument.
If key doesn’t exist - it will be created.
(Requires Redis 8.4 or greater)

For more information, see https://redis.io/commands/set
"""
opset = {ex, px, exat, pxat}
if len(opset) > 2 or len(opset) > 1 and keepttl:

if not at_most_one_value_set((ex, px, exat, pxat, keepttl)):
raise DataError(
"``ex``, ``px``, ``exat``, ``pxat``, "
"and ``keepttl`` are mutually exclusive."
)

if nx and xx:
raise DataError("``nx`` and ``xx`` are mutually exclusive.")
# Enforce mutual exclusivity among all conditional switches.
if not at_most_one_value_set((nx, xx, ifeq, ifne, ifdeq, ifdne)):
raise DataError(
"``nx``, ``xx``, ``ifeq``, ``ifne``, ``ifdeq``, ``ifdne`` are mutually exclusive."
)

pieces: list[EncodableT] = [name, value]
options = {}

pieces.extend(extract_expire_flags(ex, px, exat, pxat))

if keepttl:
pieces.append("KEEPTTL")

# Conditional modifier (exactly one at most)
if nx:
pieces.append("NX")
if xx:
elif xx:
pieces.append("XX")
elif ifeq is not None:
pieces.extend(("IFEQ", ifeq))
elif ifne is not None:
pieces.extend(("IFNE", ifne))
elif ifdeq is not None:
pieces.extend(("IFDEQ", ifdeq))
elif ifdne is not None:
pieces.extend(("IFDNE", ifdne))

if get:
pieces.append("GET")
options["get"] = True

pieces.extend(extract_expire_flags(ex, px, exat, pxat))

if keepttl:
pieces.append("KEEPTTL")

return self.execute_command("SET", *pieces, **options)

def __setitem__(self, name: KeyT, value: EncodableT):
Expand Down Expand Up @@ -5201,8 +5295,7 @@ def hgetex(
if not keys:
raise DataError("'hgetex' should have at least one key provided")

opset = {ex, px, exat, pxat}
if len(opset) > 2 or len(opset) > 1 and persist:
if not at_most_one_value_set((ex, px, exat, pxat, persist)):
raise DataError(
"``ex``, ``px``, ``exat``, ``pxat``, "
"and ``persist`` are mutually exclusive."
Expand Down Expand Up @@ -5347,8 +5440,7 @@ def hsetex(
"'items' must contain a list of key/value pairs."
)

opset = {ex, px, exat, pxat}
if len(opset) > 2 or len(opset) > 1 and keepttl:
if not at_most_one_value_set((ex, px, exat, pxat, keepttl)):
raise DataError(
"``ex``, ``px``, ``exat``, ``pxat``, "
"and ``keepttl`` are mutually exclusive."
Expand Down
21 changes: 20 additions & 1 deletion redis/commands/helpers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import copy
import random
import string
from typing import List, Tuple
from typing import Any, Iterable, List, Tuple

import redis
from redis.typing import KeysT, KeyT
Expand Down Expand Up @@ -96,3 +96,22 @@ def get_protocol_version(client):
return client.connection_pool.connection_kwargs.get("protocol")
elif isinstance(client, redis.cluster.AbstractRedisCluster):
return client.nodes_manager.connection_kwargs.get("protocol")


def at_most_one_value_set(iterable: Iterable[Any]):
"""
Checks that at most one of the values in the iterable is truthy.

Args:
iterable: An iterable of values to check.

Returns:
True if at most one value is truthy, False otherwise.

Raises:
Might raise an error if the values in iterable are not boolean-compatible.
For example if the type of the values implement
__len__ or __bool__ methods and they raise an error.
"""
values = (bool(x) for x in iterable)
return sum(values) <= 1
Loading
Loading