4747)
4848from redis .utils import (
4949 deprecated_function ,
50+ experimental_args ,
51+ experimental_method ,
5052 extract_expire_flags ,
5153)
5254
53- from .helpers import list_or_args
55+ from .helpers import at_most_one_value_set , list_or_args
5456
5557if TYPE_CHECKING :
5658 import redis .asyncio .client
@@ -1729,6 +1731,57 @@ def delete(self, *names: KeyT) -> ResponseT:
17291731 def __delitem__ (self , name : KeyT ):
17301732 self .delete (name )
17311733
1734+ @experimental_method ()
1735+ def delex (
1736+ self ,
1737+ name : KeyT ,
1738+ ifeq : Optional [Union [bytes , str ]] = None ,
1739+ ifne : Optional [Union [bytes , str ]] = None ,
1740+ ifdeq : Optional [str ] = None , # hex digest
1741+ ifdne : Optional [str ] = None , # hex digest
1742+ ) -> int :
1743+ """
1744+ Conditionally removes the specified key.
1745+
1746+ Warning:
1747+ **Experimental** since 7.1.
1748+ This API may change or be removed without notice.
1749+ The API may change based on feedback.
1750+
1751+ Arguments:
1752+ name: KeyT - the key to delete
1753+ ifeq match-valu: Optional[Union[bytes, str]] - Delete the key only if its value is equal to match-value
1754+ ifne match-value: Optional[Union[bytes, str]] - Delete the key only if its value is not equal to match-value
1755+ ifdeq match-digest: Optional[str] - Delete the key only if the digest of its value is equal to match-digest
1756+ ifdne match-digest: Optional[str] - Delete the key only if the digest of its value is not equal to match-digest
1757+
1758+ Returns:
1759+ int: 1 if the key was deleted, 0 otherwise.
1760+ Raises:
1761+ redis.exceptions.ResponseError: if key exists but is not a string
1762+ and a condition is specified.
1763+ ValueError: if more than one condition is provided.
1764+
1765+
1766+ Requires Redis 8.4 or greater.
1767+ For more information, see https://redis.io/commands/delex
1768+ """
1769+ conds = [x is not None for x in (ifeq , ifne , ifdeq , ifdne )]
1770+ if sum (conds ) > 1 :
1771+ raise ValueError ("Only one of IFEQ/IFNE/IFDEQ/IFDNE may be specified" )
1772+
1773+ pieces = ["DELEX" , name ]
1774+ if ifeq is not None :
1775+ pieces += ["IFEQ" , ifeq ]
1776+ elif ifne is not None :
1777+ pieces += ["IFNE" , ifne ]
1778+ elif ifdeq is not None :
1779+ pieces += ["IFDEQ" , ifdeq ]
1780+ elif ifdne is not None :
1781+ pieces += ["IFDNE" , ifdne ]
1782+
1783+ return self .execute_command (* pieces )
1784+
17321785 def dump (self , name : KeyT ) -> ResponseT :
17331786 """
17341787 Return a serialized version of the value stored at the specified key.
@@ -1835,6 +1888,32 @@ def expiretime(self, key: str) -> int:
18351888 """
18361889 return self .execute_command ("EXPIRETIME" , key )
18371890
1891+ @experimental_method ()
1892+ def digest (self , name : KeyT ) -> Optional [str ]:
1893+ """
1894+ Return the digest of the value stored at the specified key.
1895+
1896+ Warning:
1897+ **Experimental** since 7.1.
1898+ This API may change or be removed without notice.
1899+ The API may change based on feedback.
1900+
1901+ Arguments:
1902+ - name: KeyT - the key to get the digest of
1903+
1904+ Returns:
1905+ - None if the key does not exist
1906+ - (bulk string) the XXH3 digest of the value as a hex string
1907+ Raises:
1908+ - ResponseError if key exists but is not a string
1909+
1910+
1911+ Requires Redis 8.4 or greater.
1912+ For more information, see https://redis.io/commands/digest
1913+ """
1914+ # Bulk string response is already handled (bytes/str based on decode_responses)
1915+ return self .execute_command ("DIGEST" , name )
1916+
18381917 def get (self , name : KeyT ) -> ResponseT :
18391918 """
18401919 Return the value at key ``name``, or None if the key doesn't exist
@@ -1883,8 +1962,7 @@ def getex(
18831962
18841963 For more information, see https://redis.io/commands/getex
18851964 """
1886- opset = {ex , px , exat , pxat }
1887- if len (opset ) > 2 or len (opset ) > 1 and persist :
1965+ if not at_most_one_value_set ((ex , px , exat , pxat , persist )):
18881966 raise DataError (
18891967 "``ex``, ``px``, ``exat``, ``pxat``, "
18901968 "and ``persist`` are mutually exclusive."
@@ -2072,8 +2150,7 @@ def msetex(
20722150 Available since Redis 8.4
20732151 For more information, see https://redis.io/commands/msetex
20742152 """
2075- opset = {ex , px , exat , pxat }
2076- if len (opset ) > 2 or len (opset ) > 1 and keepttl :
2153+ if not at_most_one_value_set ((ex , px , exat , pxat , keepttl )):
20772154 raise DataError (
20782155 "``ex``, ``px``, ``exat``, ``pxat``, "
20792156 "and ``keepttl`` are mutually exclusive."
@@ -2327,6 +2404,7 @@ def restore(
23272404
23282405 return self .execute_command ("RESTORE" , * params )
23292406
2407+ @experimental_args (["ifeq" , "ifne" , "ifdeq" , "ifdne" ])
23302408 def set (
23312409 self ,
23322410 name : KeyT ,
@@ -2339,10 +2417,20 @@ def set(
23392417 get : bool = False ,
23402418 exat : Optional [AbsExpiryT ] = None ,
23412419 pxat : Optional [AbsExpiryT ] = None ,
2420+ ifeq : Optional [Union [bytes , str ]] = None ,
2421+ ifne : Optional [Union [bytes , str ]] = None ,
2422+ ifdeq : Optional [str ] = None , # hex digest of current value
2423+ ifdne : Optional [str ] = None , # hex digest of current value
23422424 ) -> ResponseT :
23432425 """
23442426 Set the value at key ``name`` to ``value``
23452427
2428+ Warning:
2429+ **Experimental** since 7.1.
2430+ The usage of the arguments ``ifeq``, ``ifne``, ``ifdeq``, and ``ifdne``
2431+ is experimental. The API or returned results when those parameters are used
2432+ may change based on feedback.
2433+
23462434 ``ex`` sets an expire flag on key ``name`` for ``ex`` seconds.
23472435
23482436 ``px`` sets an expire flag on key ``name`` for ``px`` milliseconds.
@@ -2366,35 +2454,67 @@ def set(
23662454 ``pxat`` sets an expire flag on key ``name`` for ``ex`` milliseconds,
23672455 specified in unix time.
23682456
2457+ ``ifeq`` set the value at key ``name`` to ``value`` only if the current
2458+ value exactly matches the argument.
2459+ If key doesn’t exist - it won’t be created.
2460+ (Requires Redis 8.4 or greater)
2461+
2462+ ``ifne`` set the value at key ``name`` to ``value`` only if the current
2463+ value does not exactly match the argument.
2464+ If key doesn’t exist - it will be created.
2465+ (Requires Redis 8.4 or greater)
2466+
2467+ ``ifdeq`` set the value at key ``name`` to ``value`` only if the current
2468+ value XXH3 hex digest exactly matches the argument.
2469+ If key doesn’t exist - it won’t be created.
2470+ (Requires Redis 8.4 or greater)
2471+
2472+ ``ifdne`` set the value at key ``name`` to ``value`` only if the current
2473+ value XXH3 hex digest does not exactly match the argument.
2474+ If key doesn’t exist - it will be created.
2475+ (Requires Redis 8.4 or greater)
2476+
23692477 For more information, see https://redis.io/commands/set
23702478 """
2371- opset = { ex , px , exat , pxat }
2372- if len ( opset ) > 2 or len ( opset ) > 1 and keepttl :
2479+
2480+ if not at_most_one_value_set (( ex , px , exat , pxat , keepttl )) :
23732481 raise DataError (
23742482 "``ex``, ``px``, ``exat``, ``pxat``, "
23752483 "and ``keepttl`` are mutually exclusive."
23762484 )
23772485
2378- if nx and xx :
2379- raise DataError ("``nx`` and ``xx`` are mutually exclusive." )
2486+ # Enforce mutual exclusivity among all conditional switches.
2487+ if not at_most_one_value_set ((nx , xx , ifeq , ifne , ifdeq , ifdne )):
2488+ raise DataError (
2489+ "``nx``, ``xx``, ``ifeq``, ``ifne``, ``ifdeq``, ``ifdne`` are mutually exclusive."
2490+ )
23802491
23812492 pieces : list [EncodableT ] = [name , value ]
23822493 options = {}
23832494
2384- pieces .extend (extract_expire_flags (ex , px , exat , pxat ))
2385-
2386- if keepttl :
2387- pieces .append ("KEEPTTL" )
2388-
2495+ # Conditional modifier (exactly one at most)
23892496 if nx :
23902497 pieces .append ("NX" )
2391- if xx :
2498+ elif xx :
23922499 pieces .append ("XX" )
2500+ elif ifeq is not None :
2501+ pieces .extend (("IFEQ" , ifeq ))
2502+ elif ifne is not None :
2503+ pieces .extend (("IFNE" , ifne ))
2504+ elif ifdeq is not None :
2505+ pieces .extend (("IFDEQ" , ifdeq ))
2506+ elif ifdne is not None :
2507+ pieces .extend (("IFDNE" , ifdne ))
23932508
23942509 if get :
23952510 pieces .append ("GET" )
23962511 options ["get" ] = True
23972512
2513+ pieces .extend (extract_expire_flags (ex , px , exat , pxat ))
2514+
2515+ if keepttl :
2516+ pieces .append ("KEEPTTL" )
2517+
23982518 return self .execute_command ("SET" , * pieces , ** options )
23992519
24002520 def __setitem__ (self , name : KeyT , value : EncodableT ):
@@ -5201,8 +5321,7 @@ def hgetex(
52015321 if not keys :
52025322 raise DataError ("'hgetex' should have at least one key provided" )
52035323
5204- opset = {ex , px , exat , pxat }
5205- if len (opset ) > 2 or len (opset ) > 1 and persist :
5324+ if not at_most_one_value_set ((ex , px , exat , pxat , persist )):
52065325 raise DataError (
52075326 "``ex``, ``px``, ``exat``, ``pxat``, "
52085327 "and ``persist`` are mutually exclusive."
@@ -5347,8 +5466,7 @@ def hsetex(
53475466 "'items' must contain a list of key/value pairs."
53485467 )
53495468
5350- opset = {ex , px , exat , pxat }
5351- if len (opset ) > 2 or len (opset ) > 1 and keepttl :
5469+ if not at_most_one_value_set ((ex , px , exat , pxat , keepttl )):
53525470 raise DataError (
53535471 "``ex``, ``px``, ``exat``, ``pxat``, "
53545472 "and ``keepttl`` are mutually exclusive."
0 commit comments