Skip to content

Commit 14432d4

Browse files
authored
DX-1780: add HEXPIRE command (#60)
* feat: add HEXPIRE * fix: fmt * fix: review * fix: fmt
1 parent f3f2a51 commit 14432d4

File tree

3 files changed

+226
-0
lines changed

3 files changed

+226
-0
lines changed
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import pytest
2+
import time
3+
from upstash_redis import Redis
4+
5+
6+
@pytest.fixture(autouse=True)
7+
def flush_hash(redis: Redis):
8+
hash_name = "myhash"
9+
redis.delete(hash_name)
10+
11+
12+
def test_hexpire_expires_hash_key(redis: Redis):
13+
hash_name = "myhash"
14+
field = "field1"
15+
value = "value1"
16+
17+
redis.hset(hash_name, field, value)
18+
assert redis.hexpire(hash_name, field, 1) == [1]
19+
20+
time.sleep(2)
21+
assert redis.hget(hash_name, field) is None
22+
23+
24+
def test_hexpire_nx_sets_expiry_if_no_expiry(redis: Redis):
25+
hash_name = "myhash"
26+
field = "field1"
27+
value = "value1"
28+
29+
redis.hset(hash_name, field, value)
30+
assert redis.hexpire(hash_name, field, 1, nx=True) == [1]
31+
32+
time.sleep(2)
33+
assert redis.hget(hash_name, field) is None
34+
35+
36+
def test_hexpire_nx_does_not_set_expiry_if_already_exists(redis: Redis):
37+
hash_name = "myhash"
38+
field = "field1"
39+
value = "value1"
40+
41+
redis.hset(hash_name, field, value)
42+
redis.hexpire(hash_name, field, 1000)
43+
assert redis.hexpire(hash_name, field, 1, nx=True) == [0]
44+
45+
46+
def test_hexpire_xx_sets_expiry_if_exists(redis: Redis):
47+
hash_name = "myhash"
48+
field = "field1"
49+
value = "value1"
50+
51+
redis.hset(hash_name, field, value)
52+
redis.hexpire(hash_name, field, 1)
53+
assert redis.hexpire(hash_name, [field], 5, xx=True) == [1]
54+
55+
time.sleep(6)
56+
assert redis.hget(hash_name, field) is None
57+
58+
59+
def test_hexpire_xx_does_not_set_expiry_if_not_exists(redis: Redis):
60+
hash_name = "myhash"
61+
field = "field1"
62+
value = "value1"
63+
64+
redis.hset(hash_name, field, value)
65+
assert redis.hexpire(hash_name, field, 5, xx=True) == [0]
66+
67+
68+
def test_hexpire_gt_sets_expiry_if_new_greater(redis: Redis):
69+
hash_name = "myhash"
70+
field = "field1"
71+
value = "value1"
72+
73+
redis.hset(hash_name, field, value)
74+
redis.hexpire(hash_name, field, 1)
75+
assert redis.hexpire(hash_name, field, 5, gt=True) == [1]
76+
77+
time.sleep(6)
78+
assert redis.hget(hash_name, field) is None
79+
80+
81+
def test_hexpire_gt_does_not_set_if_new_not_greater(redis: Redis):
82+
hash_name = "myhash"
83+
field = "field1"
84+
value = "value1"
85+
86+
redis.hset(hash_name, field, value)
87+
redis.hexpire(hash_name, field, 10)
88+
assert redis.hexpire(hash_name, [field], 5, gt=True) == [0]
89+
90+
91+
def test_hexpire_lt_sets_expiry_if_new_less(redis: Redis):
92+
hash_name = "myhash"
93+
field = "field1"
94+
value = "value1"
95+
96+
redis.hset(hash_name, field, value)
97+
redis.hexpire(hash_name, [field], 5)
98+
assert redis.hexpire(hash_name, field, 3, lt=True) == [1]
99+
100+
time.sleep(4)
101+
assert redis.hget(hash_name, field) is None
102+
103+
104+
def test_hexpire_lt_does_not_set_if_new_not_less(redis: Redis):
105+
hash_name = "myhash"
106+
field = "field1"
107+
value = "value1"
108+
109+
redis.hset(hash_name, field, value)
110+
redis.hexpire(hash_name, field, 10)
111+
assert redis.hexpire(hash_name, [field], 20, lt=True) == [0]
112+
113+
114+
def test_hexpire_returns_minus2_if_field_does_not_exist(redis: Redis):
115+
hash_name = "myhash"
116+
field = "field1"
117+
field2 = "field2"
118+
redis.hset(hash_name, field, "10")
119+
assert redis.hexpire(hash_name, field2, 1) == [-2]
120+
121+
122+
def test_hexpire_returns_minus2_if_hash_does_not_exist(redis: Redis):
123+
assert redis.hexpire("nonexistent_hash", "field1", 1) == [-2]
124+
125+
126+
def test_hexpire_returns_2_when_called_with_zero_seconds(redis: Redis):
127+
hash_name = "myhash"
128+
field = "field1"
129+
value = "value1"
130+
131+
redis.hset(hash_name, field, value)
132+
assert redis.hexpire(hash_name, field, 0) == [2]
133+
assert redis.hget(hash_name, field) is None

upstash_redis/commands.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,69 @@ def expire(
364364

365365
return self.execute(command)
366366

367+
def hexpire(
368+
self,
369+
key: str,
370+
fields: Union[str, List[str]],
371+
seconds: Union[int, datetime.timedelta],
372+
nx: bool = False,
373+
xx: bool = False,
374+
gt: bool = False,
375+
lt: bool = False,
376+
) -> ResponseT:
377+
"""
378+
Sets a timeout on a hash field in seconds.
379+
After the timeout has expired, the hash field will automatically be deleted.
380+
381+
:param key: The key of the hash.
382+
:param field: The field within the hash to set the expiry for.
383+
:param seconds: The timeout in seconds as an int or a datetime.timedelta object.
384+
:param nx: Set expiry only when the field has no expiry.
385+
:param xx: Set expiry only when the field has an existing expiry.
386+
:param gt: Set expiry only when the new expiry is greater than the current one.
387+
:param lt: Set expiry only when the new expiry is less than the current one.
388+
389+
Example:
390+
```python
391+
# With seconds
392+
redis.hset("myhash", "field1", "value1")
393+
redis.hexpire("myhash", "field1", 5)
394+
395+
assert redis.hget("myhash", "field1") == "value1"
396+
397+
time.sleep(5)
398+
399+
assert redis.hget("myhash", "field1") is None
400+
401+
# With a timedelta
402+
redis.hset("myhash", "field1", "value1")
403+
redis.hexpire("myhash", "field1", datetime.timedelta(seconds=5))
404+
```
405+
406+
See https://redis.io/commands/hexpire for more details on expiration behavior.
407+
"""
408+
409+
if isinstance(seconds, datetime.timedelta):
410+
seconds = int(seconds.total_seconds())
411+
412+
command: List = ["HEXPIRE", key, seconds]
413+
414+
if nx:
415+
command.append("NX")
416+
if xx:
417+
command.append("XX")
418+
if gt:
419+
command.append("GT")
420+
if lt:
421+
command.append("LT")
422+
423+
if isinstance(fields, str):
424+
fields = [fields]
425+
426+
command.extend(["FIELDS", len(fields), *fields])
427+
428+
return self.execute(command)
429+
367430
def expireat(
368431
self,
369432
key: str,

upstash_redis/commands.pyi

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,16 @@ class Commands:
192192
) -> int: ...
193193
def hdel(self, key: str, *fields: str) -> int: ...
194194
def hexists(self, key: str, field: str) -> bool: ...
195+
def hexpire(
196+
self,
197+
key: str,
198+
fields: Union[str, List[str]],
199+
seconds: Union[int, datetime.timedelta],
200+
nx: bool = False,
201+
xx: bool = False,
202+
gt: bool = False,
203+
lt: bool = False,
204+
) -> List[int]: ...
195205
def hget(self, key: str, field: str) -> Optional[str]: ...
196206
def hgetall(self, key: str) -> Dict[str, str]: ...
197207
def hincrby(self, key: str, field: str, increment: int) -> int: ...
@@ -691,6 +701,16 @@ class AsyncCommands:
691701
) -> int: ...
692702
async def hdel(self, key: str, *fields: str) -> int: ...
693703
async def hexists(self, key: str, field: str) -> bool: ...
704+
async def hexpire(
705+
self,
706+
key: str,
707+
fields: Union[str, List[str]],
708+
seconds: Union[int, datetime.timedelta],
709+
nx: bool = False,
710+
xx: bool = False,
711+
gt: bool = False,
712+
lt: bool = False,
713+
) -> int: ...
694714
async def hget(self, key: str, field: str) -> Optional[str]: ...
695715
async def hgetall(self, key: str) -> Dict[str, str]: ...
696716
async def hincrby(self, key: str, field: str, increment: int) -> int: ...
@@ -1233,6 +1253,16 @@ class PipelineCommands:
12331253
) -> PipelineCommands: ...
12341254
def hdel(self, key: str, *fields: str) -> PipelineCommands: ...
12351255
def hexists(self, key: str, field: str) -> PipelineCommands: ...
1256+
def hexpire(
1257+
self,
1258+
key: str,
1259+
fields: Union[str, List[str]],
1260+
seconds: Union[int, datetime.timedelta],
1261+
nx: bool = False,
1262+
xx: bool = False,
1263+
gt: bool = False,
1264+
lt: bool = False,
1265+
) -> PipelineCommands: ...
12361266
def hget(self, key: str, field: str) -> PipelineCommands: ...
12371267
def hgetall(self, key: str) -> PipelineCommands: ...
12381268
def hincrby(self, key: str, field: str, increment: int) -> PipelineCommands: ...

0 commit comments

Comments
 (0)