Skip to content

Commit 052e8ee

Browse files
authored
Adding new hash commands with expiration options - HGETDEL, HGETEX, HSETEX (#3570)
1 parent 57a95cf commit 052e8ee

File tree

8 files changed

+788
-92
lines changed

8 files changed

+788
-92
lines changed

Diff for: redis/commands/core.py

+204-68
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import datetime
44
import hashlib
55
import warnings
6+
from enum import Enum
67
from typing import (
78
TYPE_CHECKING,
89
Any,
@@ -44,6 +45,10 @@
4445
TimeoutSecT,
4546
ZScoreBoundT,
4647
)
48+
from redis.utils import (
49+
deprecated_function,
50+
extract_expire_flags,
51+
)
4752

4853
from .helpers import list_or_args
4954

@@ -1837,10 +1842,10 @@ def getdel(self, name: KeyT) -> ResponseT:
18371842
def getex(
18381843
self,
18391844
name: KeyT,
1840-
ex: Union[ExpiryT, None] = None,
1841-
px: Union[ExpiryT, None] = None,
1842-
exat: Union[AbsExpiryT, None] = None,
1843-
pxat: Union[AbsExpiryT, None] = None,
1845+
ex: Optional[ExpiryT] = None,
1846+
px: Optional[ExpiryT] = None,
1847+
exat: Optional[AbsExpiryT] = None,
1848+
pxat: Optional[AbsExpiryT] = None,
18441849
persist: bool = False,
18451850
) -> ResponseT:
18461851
"""
@@ -1863,41 +1868,19 @@ def getex(
18631868
18641869
For more information see https://redis.io/commands/getex
18651870
"""
1866-
18671871
opset = {ex, px, exat, pxat}
18681872
if len(opset) > 2 or len(opset) > 1 and persist:
18691873
raise DataError(
18701874
"``ex``, ``px``, ``exat``, ``pxat``, "
18711875
"and ``persist`` are mutually exclusive."
18721876
)
18731877

1874-
pieces: list[EncodableT] = []
1875-
# similar to set command
1876-
if ex is not None:
1877-
pieces.append("EX")
1878-
if isinstance(ex, datetime.timedelta):
1879-
ex = int(ex.total_seconds())
1880-
pieces.append(ex)
1881-
if px is not None:
1882-
pieces.append("PX")
1883-
if isinstance(px, datetime.timedelta):
1884-
px = int(px.total_seconds() * 1000)
1885-
pieces.append(px)
1886-
# similar to pexpireat command
1887-
if exat is not None:
1888-
pieces.append("EXAT")
1889-
if isinstance(exat, datetime.datetime):
1890-
exat = int(exat.timestamp())
1891-
pieces.append(exat)
1892-
if pxat is not None:
1893-
pieces.append("PXAT")
1894-
if isinstance(pxat, datetime.datetime):
1895-
pxat = int(pxat.timestamp() * 1000)
1896-
pieces.append(pxat)
1878+
exp_options: list[EncodableT] = extract_expire_flags(ex, px, exat, pxat)
1879+
18971880
if persist:
1898-
pieces.append("PERSIST")
1881+
exp_options.append("PERSIST")
18991882

1900-
return self.execute_command("GETEX", name, *pieces)
1883+
return self.execute_command("GETEX", name, *exp_options)
19011884

19021885
def __getitem__(self, name: KeyT):
19031886
"""
@@ -2255,14 +2238,14 @@ def set(
22552238
self,
22562239
name: KeyT,
22572240
value: EncodableT,
2258-
ex: Union[ExpiryT, None] = None,
2259-
px: Union[ExpiryT, None] = None,
2241+
ex: Optional[ExpiryT] = None,
2242+
px: Optional[ExpiryT] = None,
22602243
nx: bool = False,
22612244
xx: bool = False,
22622245
keepttl: bool = False,
22632246
get: bool = False,
2264-
exat: Union[AbsExpiryT, None] = None,
2265-
pxat: Union[AbsExpiryT, None] = None,
2247+
exat: Optional[AbsExpiryT] = None,
2248+
pxat: Optional[AbsExpiryT] = None,
22662249
) -> ResponseT:
22672250
"""
22682251
Set the value at key ``name`` to ``value``
@@ -2292,36 +2275,21 @@ def set(
22922275
22932276
For more information see https://redis.io/commands/set
22942277
"""
2278+
opset = {ex, px, exat, pxat}
2279+
if len(opset) > 2 or len(opset) > 1 and keepttl:
2280+
raise DataError(
2281+
"``ex``, ``px``, ``exat``, ``pxat``, "
2282+
"and ``keepttl`` are mutually exclusive."
2283+
)
2284+
2285+
if nx and xx:
2286+
raise DataError("``nx`` and ``xx`` are mutually exclusive.")
2287+
22952288
pieces: list[EncodableT] = [name, value]
22962289
options = {}
2297-
if ex is not None:
2298-
pieces.append("EX")
2299-
if isinstance(ex, datetime.timedelta):
2300-
pieces.append(int(ex.total_seconds()))
2301-
elif isinstance(ex, int):
2302-
pieces.append(ex)
2303-
elif isinstance(ex, str) and ex.isdigit():
2304-
pieces.append(int(ex))
2305-
else:
2306-
raise DataError("ex must be datetime.timedelta or int")
2307-
if px is not None:
2308-
pieces.append("PX")
2309-
if isinstance(px, datetime.timedelta):
2310-
pieces.append(int(px.total_seconds() * 1000))
2311-
elif isinstance(px, int):
2312-
pieces.append(px)
2313-
else:
2314-
raise DataError("px must be datetime.timedelta or int")
2315-
if exat is not None:
2316-
pieces.append("EXAT")
2317-
if isinstance(exat, datetime.datetime):
2318-
exat = int(exat.timestamp())
2319-
pieces.append(exat)
2320-
if pxat is not None:
2321-
pieces.append("PXAT")
2322-
if isinstance(pxat, datetime.datetime):
2323-
pxat = int(pxat.timestamp() * 1000)
2324-
pieces.append(pxat)
2290+
2291+
pieces.extend(extract_expire_flags(ex, px, exat, pxat))
2292+
23252293
if keepttl:
23262294
pieces.append("KEEPTTL")
23272295

@@ -4940,6 +4908,16 @@ def pfmerge(self, dest: KeyT, *sources: KeyT) -> ResponseT:
49404908
AsyncHyperlogCommands = HyperlogCommands
49414909

49424910

4911+
class HashDataPersistOptions(Enum):
4912+
# set the value for each provided key to each
4913+
# provided value only if all do not already exist.
4914+
FNX = "FNX"
4915+
4916+
# set the value for each provided key to each
4917+
# provided value only if all already exist.
4918+
FXX = "FXX"
4919+
4920+
49434921
class HashCommands(CommandsProtocol):
49444922
"""
49454923
Redis commands for Hash data type.
@@ -4980,6 +4958,80 @@ def hgetall(self, name: str) -> Union[Awaitable[dict], dict]:
49804958
"""
49814959
return self.execute_command("HGETALL", name, keys=[name])
49824960

4961+
def hgetdel(
4962+
self, name: str, *keys: str
4963+
) -> Union[
4964+
Awaitable[Optional[List[Union[str, bytes]]]], Optional[List[Union[str, bytes]]]
4965+
]:
4966+
"""
4967+
Return the value of ``key`` within the hash ``name`` and
4968+
delete the field in the hash.
4969+
This command is similar to HGET, except for the fact that it also deletes
4970+
the key on success from the hash with the provided ```name```.
4971+
4972+
Available since Redis 8.0
4973+
For more information see https://redis.io/commands/hgetdel
4974+
"""
4975+
if len(keys) == 0:
4976+
raise DataError("'hgetdel' should have at least one key provided")
4977+
4978+
return self.execute_command("HGETDEL", name, "FIELDS", len(keys), *keys)
4979+
4980+
def hgetex(
4981+
self,
4982+
name: KeyT,
4983+
*keys: str,
4984+
ex: Optional[ExpiryT] = None,
4985+
px: Optional[ExpiryT] = None,
4986+
exat: Optional[AbsExpiryT] = None,
4987+
pxat: Optional[AbsExpiryT] = None,
4988+
persist: bool = False,
4989+
) -> Union[
4990+
Awaitable[Optional[List[Union[str, bytes]]]], Optional[List[Union[str, bytes]]]
4991+
]:
4992+
"""
4993+
Return the values of ``key`` and ``keys`` within the hash ``name``
4994+
and optionally set their expiration.
4995+
4996+
``ex`` sets an expire flag on ``kyes`` for ``ex`` seconds.
4997+
4998+
``px`` sets an expire flag on ``keys`` for ``px`` milliseconds.
4999+
5000+
``exat`` sets an expire flag on ``keys`` for ``ex`` seconds,
5001+
specified in unix time.
5002+
5003+
``pxat`` sets an expire flag on ``keys`` for ``ex`` milliseconds,
5004+
specified in unix time.
5005+
5006+
``persist`` remove the time to live associated with the ``keys``.
5007+
5008+
Available since Redis 8.0
5009+
For more information see https://redis.io/commands/hgetex
5010+
"""
5011+
if not keys:
5012+
raise DataError("'hgetex' should have at least one key provided")
5013+
5014+
opset = {ex, px, exat, pxat}
5015+
if len(opset) > 2 or len(opset) > 1 and persist:
5016+
raise DataError(
5017+
"``ex``, ``px``, ``exat``, ``pxat``, "
5018+
"and ``persist`` are mutually exclusive."
5019+
)
5020+
5021+
exp_options: list[EncodableT] = extract_expire_flags(ex, px, exat, pxat)
5022+
5023+
if persist:
5024+
exp_options.append("PERSIST")
5025+
5026+
return self.execute_command(
5027+
"HGETEX",
5028+
name,
5029+
*exp_options,
5030+
"FIELDS",
5031+
len(keys),
5032+
*keys,
5033+
)
5034+
49835035
def hincrby(
49845036
self, name: str, key: str, amount: int = 1
49855037
) -> Union[Awaitable[int], int]:
@@ -5034,8 +5086,10 @@ def hset(
50345086
50355087
For more information see https://redis.io/commands/hset
50365088
"""
5089+
50375090
if key is None and not mapping and not items:
50385091
raise DataError("'hset' with no key value pairs")
5092+
50395093
pieces = []
50405094
if items:
50415095
pieces.extend(items)
@@ -5047,6 +5101,89 @@ def hset(
50475101

50485102
return self.execute_command("HSET", name, *pieces)
50495103

5104+
def hsetex(
5105+
self,
5106+
name: str,
5107+
key: Optional[str] = None,
5108+
value: Optional[str] = None,
5109+
mapping: Optional[dict] = None,
5110+
items: Optional[list] = None,
5111+
ex: Optional[ExpiryT] = None,
5112+
px: Optional[ExpiryT] = None,
5113+
exat: Optional[AbsExpiryT] = None,
5114+
pxat: Optional[AbsExpiryT] = None,
5115+
data_persist_option: Optional[HashDataPersistOptions] = None,
5116+
keepttl: bool = False,
5117+
) -> Union[Awaitable[int], int]:
5118+
"""
5119+
Set ``key`` to ``value`` within hash ``name``
5120+
5121+
``mapping`` accepts a dict of key/value pairs that will be
5122+
added to hash ``name``.
5123+
5124+
``items`` accepts a list of key/value pairs that will be
5125+
added to hash ``name``.
5126+
5127+
``ex`` sets an expire flag on ``keys`` for ``ex`` seconds.
5128+
5129+
``px`` sets an expire flag on ``keys`` for ``px`` milliseconds.
5130+
5131+
``exat`` sets an expire flag on ``keys`` for ``ex`` seconds,
5132+
specified in unix time.
5133+
5134+
``pxat`` sets an expire flag on ``keys`` for ``ex`` milliseconds,
5135+
specified in unix time.
5136+
5137+
``data_persist_option`` can be set to ``FNX`` or ``FXX`` to control the
5138+
behavior of the command.
5139+
``FNX`` will set the value for each provided key to each
5140+
provided value only if all do not already exist.
5141+
``FXX`` will set the value for each provided key to each
5142+
provided value only if all already exist.
5143+
5144+
``keepttl`` if True, retain the time to live associated with the keys.
5145+
5146+
Returns the number of fields that were added.
5147+
5148+
Available since Redis 8.0
5149+
For more information see https://redis.io/commands/hsetex
5150+
"""
5151+
if key is None and not mapping and not items:
5152+
raise DataError("'hsetex' with no key value pairs")
5153+
5154+
if items and len(items) % 2 != 0:
5155+
raise DataError(
5156+
"'hsetex' with odd number of items. "
5157+
"'items' must contain a list of key/value pairs."
5158+
)
5159+
5160+
opset = {ex, px, exat, pxat}
5161+
if len(opset) > 2 or len(opset) > 1 and keepttl:
5162+
raise DataError(
5163+
"``ex``, ``px``, ``exat``, ``pxat``, "
5164+
"and ``keepttl`` are mutually exclusive."
5165+
)
5166+
5167+
exp_options: list[EncodableT] = extract_expire_flags(ex, px, exat, pxat)
5168+
if data_persist_option:
5169+
exp_options.append(data_persist_option.value)
5170+
5171+
if keepttl:
5172+
exp_options.append("KEEPTTL")
5173+
5174+
pieces = []
5175+
if items:
5176+
pieces.extend(items)
5177+
if key is not None:
5178+
pieces.extend((key, value))
5179+
if mapping:
5180+
for pair in mapping.items():
5181+
pieces.extend(pair)
5182+
5183+
return self.execute_command(
5184+
"HSETEX", name, *exp_options, "FIELDS", int(len(pieces) / 2), *pieces
5185+
)
5186+
50505187
def hsetnx(self, name: str, key: str, value: str) -> Union[Awaitable[bool], bool]:
50515188
"""
50525189
Set ``key`` to ``value`` within hash ``name`` if ``key`` does not
@@ -5056,19 +5193,18 @@ def hsetnx(self, name: str, key: str, value: str) -> Union[Awaitable[bool], bool
50565193
"""
50575194
return self.execute_command("HSETNX", name, key, value)
50585195

5196+
@deprecated_function(
5197+
version="4.0.0",
5198+
reason="Use 'hset' instead.",
5199+
name="hmset",
5200+
)
50595201
def hmset(self, name: str, mapping: dict) -> Union[Awaitable[str], str]:
50605202
"""
50615203
Set key to value within hash ``name`` for each corresponding
50625204
key and value from the ``mapping`` dict.
50635205
50645206
For more information see https://redis.io/commands/hmset
50655207
"""
5066-
warnings.warn(
5067-
f"{self.__class__.__name__}.hmset() is deprecated. "
5068-
f"Use {self.__class__.__name__}.hset() instead.",
5069-
DeprecationWarning,
5070-
stacklevel=2,
5071-
)
50725208
if not mapping:
50735209
raise DataError("'hmset' with 'mapping' of length 0")
50745210
items = []

0 commit comments

Comments
 (0)