Skip to content
Merged
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
133 changes: 133 additions & 0 deletions tests/commands/hash/test_hexpire.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import pytest
import time
from upstash_redis import Redis


@pytest.fixture(autouse=True)
def flush_hash(redis: Redis):
hash_name = "myhash"
redis.delete(hash_name)


def test_hexpire_expires_hash_key(redis: Redis):
hash_name = "myhash"
field = "field1"
value = "value1"

redis.hset(hash_name, field, value)
assert redis.hexpire(hash_name, field, 1) == [1]

time.sleep(2)
assert redis.hget(hash_name, field) is None


def test_hexpire_nx_sets_expiry_if_no_expiry(redis: Redis):
hash_name = "myhash"
field = "field1"
value = "value1"

redis.hset(hash_name, field, value)
assert redis.hexpire(hash_name, field, 1, nx=True) == [1]

time.sleep(2)
assert redis.hget(hash_name, field) is None


def test_hexpire_nx_does_not_set_expiry_if_already_exists(redis: Redis):
hash_name = "myhash"
field = "field1"
value = "value1"

redis.hset(hash_name, field, value)
redis.hexpire(hash_name, field, 1000)
assert redis.hexpire(hash_name, field, 1, nx=True) == [0]


def test_hexpire_xx_sets_expiry_if_exists(redis: Redis):
hash_name = "myhash"
field = "field1"
value = "value1"

redis.hset(hash_name, field, value)
redis.hexpire(hash_name, field, 1)
assert redis.hexpire(hash_name, [field], 5, xx=True) == [1]

time.sleep(6)
assert redis.hget(hash_name, field) is None


def test_hexpire_xx_does_not_set_expiry_if_not_exists(redis: Redis):
hash_name = "myhash"
field = "field1"
value = "value1"

redis.hset(hash_name, field, value)
assert redis.hexpire(hash_name, field, 5, xx=True) == [0]


def test_hexpire_gt_sets_expiry_if_new_greater(redis: Redis):
hash_name = "myhash"
field = "field1"
value = "value1"

redis.hset(hash_name, field, value)
redis.hexpire(hash_name, field, 1)
assert redis.hexpire(hash_name, field, 5, gt=True) == [1]

time.sleep(6)
assert redis.hget(hash_name, field) is None


def test_hexpire_gt_does_not_set_if_new_not_greater(redis: Redis):
hash_name = "myhash"
field = "field1"
value = "value1"

redis.hset(hash_name, field, value)
redis.hexpire(hash_name, field, 10)
assert redis.hexpire(hash_name, [field], 5, gt=True) == [0]


def test_hexpire_lt_sets_expiry_if_new_less(redis: Redis):
hash_name = "myhash"
field = "field1"
value = "value1"

redis.hset(hash_name, field, value)
redis.hexpire(hash_name, [field], 5)
assert redis.hexpire(hash_name, field, 3, lt=True) == [1]

time.sleep(4)
assert redis.hget(hash_name, field) is None


def test_hexpire_lt_does_not_set_if_new_not_less(redis: Redis):
hash_name = "myhash"
field = "field1"
value = "value1"

redis.hset(hash_name, field, value)
redis.hexpire(hash_name, field, 10)
assert redis.hexpire(hash_name, [field], 20, lt=True) == [0]


def test_hexpire_returns_minus2_if_field_does_not_exist(redis: Redis):
hash_name = "myhash"
field = "field1"
field2 = "field2"
redis.hset(hash_name, field, "10")
assert redis.hexpire(hash_name, field2, 1) == [-2]


def test_hexpire_returns_minus2_if_hash_does_not_exist(redis: Redis):
assert redis.hexpire("nonexistent_hash", "field1", 1) == [-2]


def test_hexpire_returns_2_when_called_with_zero_seconds(redis: Redis):
hash_name = "myhash"
field = "field1"
value = "value1"

redis.hset(hash_name, field, value)
assert redis.hexpire(hash_name, field, 0) == [2]
assert redis.hget(hash_name, field) is None
63 changes: 63 additions & 0 deletions upstash_redis/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,69 @@ def expire(

return self.execute(command)

def hexpire(
self,
key: str,
fields: Union[str, List[str]],
seconds: Union[int, datetime.timedelta],
nx: bool = False,
xx: bool = False,
gt: bool = False,
lt: bool = False,
) -> ResponseT:
"""
Sets a timeout on a hash field in seconds.
After the timeout has expired, the hash field will automatically be deleted.

:param key: The key of the hash.
:param field: The field within the hash to set the expiry for.
:param seconds: The timeout in seconds as an int or a datetime.timedelta object.
:param nx: Set expiry only when the field has no expiry.
:param xx: Set expiry only when the field has an existing expiry.
:param gt: Set expiry only when the new expiry is greater than the current one.
:param lt: Set expiry only when the new expiry is less than the current one.

Example:
```python
# With seconds
redis.hset("myhash", "field1", "value1")
redis.hexpire("myhash", "field1", 5)

assert redis.hget("myhash", "field1") == "value1"

time.sleep(5)

assert redis.hget("myhash", "field1") is None

# With a timedelta
redis.hset("myhash", "field1", "value1")
redis.hexpire("myhash", "field1", datetime.timedelta(seconds=5))
```

See https://redis.io/commands/hexpire for more details on expiration behavior.
"""

if isinstance(seconds, datetime.timedelta):
seconds = int(seconds.total_seconds())

command: List = ["HEXPIRE", key, seconds]

if nx:
command.append("NX")
if xx:
command.append("XX")
if gt:
command.append("GT")
if lt:
command.append("LT")

if isinstance(fields, str):
fields = [fields]

command.extend(["FIELDS", len(fields), *fields])

return self.execute(command)

def expireat(
self,
key: str,
Expand Down
30 changes: 30 additions & 0 deletions upstash_redis/commands.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,16 @@ class Commands:
) -> int: ...
def hdel(self, key: str, *fields: str) -> int: ...
def hexists(self, key: str, field: str) -> bool: ...
def hexpire(
self,
key: str,
fields: Union[str, List[str]],
seconds: Union[int, datetime.timedelta],
nx: bool = False,
xx: bool = False,
gt: bool = False,
lt: bool = False,
) -> List[int]: ...
def hget(self, key: str, field: str) -> Optional[str]: ...
def hgetall(self, key: str) -> Dict[str, str]: ...
def hincrby(self, key: str, field: str, increment: int) -> int: ...
Expand Down Expand Up @@ -691,6 +701,16 @@ class AsyncCommands:
) -> int: ...
async def hdel(self, key: str, *fields: str) -> int: ...
async def hexists(self, key: str, field: str) -> bool: ...
async def hexpire(
self,
key: str,
fields: Union[str, List[str]],
seconds: Union[int, datetime.timedelta],
nx: bool = False,
xx: bool = False,
gt: bool = False,
lt: bool = False,
) -> int: ...
async def hget(self, key: str, field: str) -> Optional[str]: ...
async def hgetall(self, key: str) -> Dict[str, str]: ...
async def hincrby(self, key: str, field: str, increment: int) -> int: ...
Expand Down Expand Up @@ -1233,6 +1253,16 @@ class PipelineCommands:
) -> PipelineCommands: ...
def hdel(self, key: str, *fields: str) -> PipelineCommands: ...
def hexists(self, key: str, field: str) -> PipelineCommands: ...
def hexpire(
self,
key: str,
fields: Union[str, List[str]],
seconds: Union[int, datetime.timedelta],
nx: bool = False,
xx: bool = False,
gt: bool = False,
lt: bool = False,
) -> PipelineCommands: ...
def hget(self, key: str, field: str) -> PipelineCommands: ...
def hgetall(self, key: str) -> PipelineCommands: ...
def hincrby(self, key: str, field: str, increment: int) -> PipelineCommands: ...
Expand Down