Skip to content

Commit 736fd88

Browse files
committed
Adding new hash commands with expiration options - HGETDEL, HGETEX, HSETEX
1 parent 7a36e8b commit 736fd88

File tree

8 files changed

+708
-93
lines changed

8 files changed

+708
-93
lines changed

Diff for: redis/commands/core.py

+174-68
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@
4444
TimeoutSecT,
4545
ZScoreBoundT,
4646
)
47+
from redis.utils import (
48+
deprecated_function,
49+
extract_expire_flags,
50+
)
4751

4852
from .helpers import list_or_args
4953

@@ -1837,10 +1841,10 @@ def getdel(self, name: KeyT) -> ResponseT:
18371841
def getex(
18381842
self,
18391843
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,
1844+
ex: Optional[ExpiryT] = None,
1845+
px: Optional[ExpiryT] = None,
1846+
exat: Optional[AbsExpiryT] = None,
1847+
pxat: Optional[AbsExpiryT] = None,
18441848
persist: bool = False,
18451849
) -> ResponseT:
18461850
"""
@@ -1863,41 +1867,19 @@ def getex(
18631867
18641868
For more information see https://redis.io/commands/getex
18651869
"""
1866-
18671870
opset = {ex, px, exat, pxat}
18681871
if len(opset) > 2 or len(opset) > 1 and persist:
18691872
raise DataError(
18701873
"``ex``, ``px``, ``exat``, ``pxat``, "
18711874
"and ``persist`` are mutually exclusive."
18721875
)
18731876

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)
1877+
exp_options: list[EncodableT] = extract_expire_flags(ex, px, exat, pxat)
1878+
18971879
if persist:
1898-
pieces.append("PERSIST")
1880+
exp_options.append("PERSIST")
18991881

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

19021884
def __getitem__(self, name: KeyT):
19031885
"""
@@ -2255,14 +2237,14 @@ def set(
22552237
self,
22562238
name: KeyT,
22572239
value: EncodableT,
2258-
ex: Union[ExpiryT, None] = None,
2259-
px: Union[ExpiryT, None] = None,
2240+
ex: Optional[ExpiryT] = None,
2241+
px: Optional[ExpiryT] = None,
22602242
nx: bool = False,
22612243
xx: bool = False,
22622244
keepttl: bool = False,
22632245
get: bool = False,
2264-
exat: Union[AbsExpiryT, None] = None,
2265-
pxat: Union[AbsExpiryT, None] = None,
2246+
exat: Optional[AbsExpiryT] = None,
2247+
pxat: Optional[AbsExpiryT] = None,
22662248
) -> ResponseT:
22672249
"""
22682250
Set the value at key ``name`` to ``value``
@@ -2292,36 +2274,21 @@ def set(
22922274
22932275
For more information see https://redis.io/commands/set
22942276
"""
2277+
opset = {ex, px, exat, pxat}
2278+
if len(opset) > 2 or len(opset) > 1 and keepttl:
2279+
raise DataError(
2280+
"``ex``, ``px``, ``exat``, ``pxat``, "
2281+
"and ``keepttl`` are mutually exclusive."
2282+
)
2283+
2284+
if nx and xx:
2285+
raise DataError("``nx`` and ``xx`` are mutually exclusive.")
2286+
22952287
pieces: list[EncodableT] = [name, value]
22962288
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)
2289+
2290+
pieces.extend(extract_expire_flags(ex, px, exat, pxat))
2291+
23252292
if keepttl:
23262293
pieces.append("KEEPTTL")
23272294

@@ -4980,6 +4947,65 @@ def hgetall(self, name: str) -> Union[Awaitable[dict], dict]:
49804947
"""
49814948
return self.execute_command("HGETALL", name, keys=[name])
49824949

4950+
def hgetdel(
4951+
self, name: str, *keys: str
4952+
) -> Union[Awaitable[Optional[str]], Optional[str]]:
4953+
"""
4954+
Return the value of ``key`` within the hash ``name`` and
4955+
delete the field in the hash.
4956+
This command is similar to HGET, except for the fact that it also deletes
4957+
the key on success from the hash with the provided ```name```.
4958+
4959+
Available since Redis 8.0
4960+
For more information see https://redis.io/commands/hgetdel
4961+
"""
4962+
return self.execute_command("HGETDEL", name, "FIELDS", len(keys), *keys)
4963+
4964+
def hgetex(
4965+
self,
4966+
name: KeyT,
4967+
*keys: str,
4968+
ex: Optional[ExpiryT] = None,
4969+
px: Optional[ExpiryT] = None,
4970+
exat: Optional[AbsExpiryT] = None,
4971+
pxat: Optional[AbsExpiryT] = None,
4972+
persist: bool = False,
4973+
) -> Union[Awaitable[Optional[str]], Optional[str]]:
4974+
"""
4975+
Return the values of ``keys`` within the hash ``name``
4976+
and optionally set their expiration.
4977+
4978+
``ex`` sets an expire flag on ``kyes`` for ``ex`` seconds.
4979+
4980+
``px`` sets an expire flag on ``keys`` for ``px`` milliseconds.
4981+
4982+
``exat`` sets an expire flag on ``keys`` for ``ex`` seconds,
4983+
specified in unix time.
4984+
4985+
``pxat`` sets an expire flag on ``keys`` for ``ex`` milliseconds,
4986+
specified in unix time.
4987+
4988+
``persist`` remove the time to live associated with the ``keys``.
4989+
4990+
Available since Redis 8.0
4991+
For more information see https://redis.io/commands/hgetex
4992+
"""
4993+
opset = {ex, px, exat, pxat}
4994+
if len(opset) > 2 or len(opset) > 1 and persist:
4995+
raise DataError(
4996+
"``ex``, ``px``, ``exat``, ``pxat``, "
4997+
"and ``persist`` are mutually exclusive."
4998+
)
4999+
5000+
exp_options: list[EncodableT] = extract_expire_flags(ex, px, exat, pxat)
5001+
5002+
if persist:
5003+
exp_options.append("PERSIST")
5004+
5005+
return self.execute_command(
5006+
"HGETEX", name, *exp_options, "FIELDS", len(keys), *keys
5007+
)
5008+
49835009
def hincrby(
49845010
self, name: str, key: str, amount: int = 1
49855011
) -> Union[Awaitable[int], int]:
@@ -5047,6 +5073,87 @@ def hset(
50475073

50485074
return self.execute_command("HSET", name, *pieces)
50495075

5076+
def hsetex(
5077+
self,
5078+
name: str,
5079+
key: Optional[str] = None,
5080+
value: Optional[str] = None,
5081+
mapping: Optional[dict] = None,
5082+
items: Optional[list] = None,
5083+
ex: Optional[ExpiryT] = None,
5084+
px: Optional[ExpiryT] = None,
5085+
exat: Optional[AbsExpiryT] = None,
5086+
pxat: Optional[AbsExpiryT] = None,
5087+
fnx: bool = False,
5088+
fxx: bool = False,
5089+
keepttl: bool = False,
5090+
) -> Union[Awaitable[int], int]:
5091+
"""
5092+
Set ``key`` to ``value`` within hash ``name``
5093+
5094+
``mapping`` accepts a dict of key/value pairs that will be
5095+
added to hash ``name``.
5096+
5097+
``items`` accepts a list of key/value pairs that will be
5098+
added to hash ``name``.
5099+
5100+
``ex`` sets an expire flag on ``keys`` for ``ex`` seconds.
5101+
5102+
``px`` sets an expire flag on ``keys`` for ``px`` milliseconds.
5103+
5104+
``exat`` sets an expire flag on ``keys`` for ``ex`` seconds,
5105+
specified in unix time.
5106+
5107+
``pxat`` sets an expire flag on ``keys`` for ``ex`` milliseconds,
5108+
specified in unix time.
5109+
5110+
``fnx`` if set to True, set the value for each provided key to each
5111+
provided value only if all do not already exist.
5112+
5113+
``fxx`` if set to True, set the value for each provided key to each
5114+
provided value only if all already exist.
5115+
5116+
``keepttl`` if True, retain the time to live associated with the keys.
5117+
5118+
Returns the number of fields that were added.
5119+
5120+
Available since Redis 8.0
5121+
For more information see https://redis.io/commands/hsetex
5122+
"""
5123+
if key is None and not mapping and not items:
5124+
raise DataError("'hsetex' with no key value pairs")
5125+
5126+
opset = {ex, px, exat, pxat}
5127+
if len(opset) > 2 or len(opset) > 1 and keepttl:
5128+
raise DataError(
5129+
"``ex``, ``px``, ``exat``, ``pxat``, "
5130+
"and ``keepttl`` are mutually exclusive."
5131+
)
5132+
5133+
if fnx and fxx:
5134+
raise DataError("``fnx`` and ``fxx`` are mutually exclusive.")
5135+
5136+
exp_options: list[EncodableT] = extract_expire_flags(ex, px, exat, pxat)
5137+
if fnx:
5138+
exp_options.append("FNX")
5139+
if fxx:
5140+
exp_options.append("FXX")
5141+
if keepttl:
5142+
exp_options.append("KEEPTTL")
5143+
5144+
pieces = []
5145+
if items:
5146+
pieces.extend(items)
5147+
if key is not None:
5148+
pieces.extend((key, value))
5149+
if mapping:
5150+
for pair in mapping.items():
5151+
pieces.extend(pair)
5152+
5153+
return self.execute_command(
5154+
"HSETEX", name, *exp_options, "FIELDS", int(len(pieces) / 2), *pieces
5155+
)
5156+
50505157
def hsetnx(self, name: str, key: str, value: str) -> Union[Awaitable[bool], bool]:
50515158
"""
50525159
Set ``key`` to ``value`` within hash ``name`` if ``key`` does not
@@ -5056,19 +5163,18 @@ def hsetnx(self, name: str, key: str, value: str) -> Union[Awaitable[bool], bool
50565163
"""
50575164
return self.execute_command("HSETNX", name, key, value)
50585165

5166+
@deprecated_function(
5167+
version="4.0.0",
5168+
reason="Use 'hset' instead.",
5169+
name="hmset",
5170+
)
50595171
def hmset(self, name: str, mapping: dict) -> Union[Awaitable[str], str]:
50605172
"""
50615173
Set key to value within hash ``name`` for each corresponding
50625174
key and value from the ``mapping`` dict.
50635175
50645176
For more information see https://redis.io/commands/hmset
50655177
"""
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-
)
50725178
if not mapping:
50735179
raise DataError("'hmset' with 'mapping' of length 0")
50745180
items = []

Diff for: redis/utils.py

+42-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
import datetime
12
import logging
23
from contextlib import contextmanager
34
from functools import wraps
4-
from typing import Any, Dict, Mapping, Union
5+
from typing import Any, Dict, List, Mapping, Optional, Union
6+
7+
from redis.exceptions import DataError
8+
from redis.typing import AbsExpiryT, EncodableT, ExpiryT
59

610
try:
711
import hiredis # noqa
@@ -257,3 +261,40 @@ def ensure_string(key):
257261
return key
258262
else:
259263
raise TypeError("Key must be either a string or bytes")
264+
265+
266+
def extract_expire_flags(
267+
ex: Optional[ExpiryT] = None,
268+
px: Optional[ExpiryT] = None,
269+
exat: Optional[AbsExpiryT] = None,
270+
pxat: Optional[AbsExpiryT] = None,
271+
) -> List[EncodableT]:
272+
exp_options: list[EncodableT] = []
273+
if ex is not None:
274+
exp_options.append("EX")
275+
if isinstance(ex, datetime.timedelta):
276+
exp_options.append(int(ex.total_seconds()))
277+
elif isinstance(ex, int):
278+
exp_options.append(ex)
279+
elif isinstance(ex, str) and ex.isdigit():
280+
exp_options.append(int(ex))
281+
else:
282+
raise DataError("ex must be datetime.timedelta or int")
283+
elif px is not None:
284+
exp_options.append("PX")
285+
if isinstance(px, datetime.timedelta):
286+
exp_options.append(int(px.total_seconds() * 1000))
287+
elif isinstance(px, int):
288+
exp_options.append(px)
289+
else:
290+
raise DataError("px must be datetime.timedelta or int")
291+
elif exat is not None:
292+
if isinstance(exat, datetime.datetime):
293+
exat = int(exat.timestamp())
294+
exp_options.extend(["EXAT", exat])
295+
elif pxat is not None:
296+
if isinstance(pxat, datetime.datetime):
297+
pxat = int(pxat.timestamp() * 1000)
298+
exp_options.extend(["PXAT", pxat])
299+
300+
return exp_options

0 commit comments

Comments
 (0)