Skip to content

Commit

Permalink
Hash field expiration commands (redis#3218)
Browse files Browse the repository at this point in the history
Support hash field expiration commands that become available with
Redis 7.4.

Adapt some tests to match recent server-side changes. Update tests
related to memory stats. Make CLIENT KILL test not run with cluster.
Disable tests related to Graph module. The Graph module is no longer
part of Redis Stack, so for the moment disable all related tests.

---------

Co-authored-by: Gabriel Erzse <[email protected]>
  • Loading branch information
gerzse and gerzse committed Jul 11, 2024
1 parent 153c310 commit 45ed240
Show file tree
Hide file tree
Showing 7 changed files with 1,056 additions and 5 deletions.
360 changes: 360 additions & 0 deletions redis/commands/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -5087,6 +5087,366 @@ def hstrlen(self, name: str, key: str) -> Union[Awaitable[int], int]:
"""
return self.execute_command("HSTRLEN", name, key)

def hexpire(
self,
name: KeyT,
seconds: ExpiryT,
*fields: str,
nx: bool = False,
xx: bool = False,
gt: bool = False,
lt: bool = False,
) -> ResponseT:
"""
Sets or updates the expiration time for fields within a hash key, using relative
time in seconds.
If a field already has an expiration time, the behavior of the update can be
controlled using the `nx`, `xx`, `gt`, and `lt` parameters.
The return value provides detailed information about the outcome for each field.
For more information, see https://redis.io/commands/hexpire
Args:
name: The name of the hash key.
seconds: Expiration time in seconds, relative. Can be an integer, or a
Python `timedelta` object.
fields: List of fields within the hash to apply the expiration time to.
nx: Set expiry only when the field has no expiry.
xx: Set expiry only when the field has an existing expiry.
gt: Set expiry only when the new expiry is greater than the current one.
lt: Set expiry only when the new expiry is less than the current one.
Returns:
If the key does not exist, returns an empty list. If the key exists, returns
a list which contains for each field in the request:
- `-2` if the field does not exist.
- `0` if the specified NX | XX | GT | LT condition was not met.
- `1` if the expiration time was set or updated.
- `2` if the field was deleted because the specified expiration time is
in the past.
"""
conditions = [nx, xx, gt, lt]
if sum(conditions) > 1:
raise ValueError("Only one of 'nx', 'xx', 'gt', 'lt' can be specified.")

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

options = []
if nx:
options.append("NX")
if xx:
options.append("XX")
if gt:
options.append("GT")
if lt:
options.append("LT")

return self.execute_command(
"HEXPIRE", name, seconds, *options, "FIELDS", len(fields), *fields
)

def hpexpire(
self,
name: KeyT,
milliseconds: ExpiryT,
*fields: str,
nx: bool = False,
xx: bool = False,
gt: bool = False,
lt: bool = False,
) -> ResponseT:
"""
Sets or updates the expiration time for fields within a hash key, using relative
time in milliseconds.
If a field already has an expiration time, the behavior of the update can be
controlled using the `nx`, `xx`, `gt`, and `lt` parameters.
The return value provides detailed information about the outcome for each field.
For more information, see https://redis.io/commands/hpexpire
Args:
name: The name of the hash key.
milliseconds: Expiration time in milliseconds, relative. Can be an integer,
or a Python `timedelta` object.
fields: List of fields within the hash to apply the expiration time to.
nx: Set expiry only when the field has no expiry.
xx: Set expiry only when the field has an existing expiry.
gt: Set expiry only when the new expiry is greater than the current one.
lt: Set expiry only when the new expiry is less than the current one.
Returns:
If the key does not exist, returns an empty list. If the key exists, returns
a list which contains for each field in the request:
- `-2` if the field does not exist.
- `0` if the specified NX | XX | GT | LT condition was not met.
- `1` if the expiration time was set or updated.
- `2` if the field was deleted because the specified expiration time is
in the past.
"""
conditions = [nx, xx, gt, lt]
if sum(conditions) > 1:
raise ValueError("Only one of 'nx', 'xx', 'gt', 'lt' can be specified.")

if isinstance(milliseconds, datetime.timedelta):
milliseconds = int(milliseconds.total_seconds() * 1000)

options = []
if nx:
options.append("NX")
if xx:
options.append("XX")
if gt:
options.append("GT")
if lt:
options.append("LT")

return self.execute_command(
"HPEXPIRE", name, milliseconds, *options, "FIELDS", len(fields), *fields
)

def hexpireat(
self,
name: KeyT,
unix_time_seconds: AbsExpiryT,
*fields: str,
nx: bool = False,
xx: bool = False,
gt: bool = False,
lt: bool = False,
) -> ResponseT:
"""
Sets or updates the expiration time for fields within a hash key, using an
absolute Unix timestamp in seconds.
If a field already has an expiration time, the behavior of the update can be
controlled using the `nx`, `xx`, `gt`, and `lt` parameters.
The return value provides detailed information about the outcome for each field.
For more information, see https://redis.io/commands/hexpireat
Args:
name: The name of the hash key.
unix_time_seconds: Expiration time as Unix timestamp in seconds. Can be an
integer or a Python `datetime` object.
fields: List of fields within the hash to apply the expiration time to.
nx: Set expiry only when the field has no expiry.
xx: Set expiry only when the field has an existing expiration time.
gt: Set expiry only when the new expiry is greater than the current one.
lt: Set expiry only when the new expiry is less than the current one.
Returns:
If the key does not exist, returns an empty list. If the key exists, returns
a list which contains for each field in the request:
- `-2` if the field does not exist.
- `0` if the specified NX | XX | GT | LT condition was not met.
- `1` if the expiration time was set or updated.
- `2` if the field was deleted because the specified expiration time is
in the past.
"""
conditions = [nx, xx, gt, lt]
if sum(conditions) > 1:
raise ValueError("Only one of 'nx', 'xx', 'gt', 'lt' can be specified.")

if isinstance(unix_time_seconds, datetime.datetime):
unix_time_seconds = int(unix_time_seconds.timestamp())

options = []
if nx:
options.append("NX")
if xx:
options.append("XX")
if gt:
options.append("GT")
if lt:
options.append("LT")

return self.execute_command(
"HEXPIREAT",
name,
unix_time_seconds,
*options,
"FIELDS",
len(fields),
*fields,
)

def hpexpireat(
self,
name: KeyT,
unix_time_milliseconds: AbsExpiryT,
*fields: str,
nx: bool = False,
xx: bool = False,
gt: bool = False,
lt: bool = False,
) -> ResponseT:
"""
Sets or updates the expiration time for fields within a hash key, using an
absolute Unix timestamp in milliseconds.
If a field already has an expiration time, the behavior of the update can be
controlled using the `nx`, `xx`, `gt`, and `lt` parameters.
The return value provides detailed information about the outcome for each field.
For more information, see https://redis.io/commands/hpexpireat
Args:
name: The name of the hash key.
unix_time_milliseconds: Expiration time as Unix timestamp in milliseconds.
Can be an integer or a Python `datetime` object.
fields: List of fields within the hash to apply the expiry.
nx: Set expiry only when the field has no expiry.
xx: Set expiry only when the field has an existing expiry.
gt: Set expiry only when the new expiry is greater than the current one.
lt: Set expiry only when the new expiry is less than the current one.
Returns:
If the key does not exist, returns an empty list. If the key exists, returns
a list which contains for each field in the request:
- `-2` if the field does not exist.
- `0` if the specified NX | XX | GT | LT condition was not met.
- `1` if the expiration time was set or updated.
- `2` if the field was deleted because the specified expiration time is
in the past.
"""
conditions = [nx, xx, gt, lt]
if sum(conditions) > 1:
raise ValueError("Only one of 'nx', 'xx', 'gt', 'lt' can be specified.")

if isinstance(unix_time_milliseconds, datetime.datetime):
unix_time_milliseconds = int(unix_time_milliseconds.timestamp() * 1000)

options = []
if nx:
options.append("NX")
if xx:
options.append("XX")
if gt:
options.append("GT")
if lt:
options.append("LT")

return self.execute_command(
"HPEXPIREAT",
name,
unix_time_milliseconds,
*options,
"FIELDS",
len(fields),
*fields,
)

def hpersist(self, name: KeyT, *fields: str) -> ResponseT:
"""
Removes the expiration time for each specified field in a hash.
For more information, see https://redis.io/commands/hpersist
Args:
name: The name of the hash key.
fields: A list of fields within the hash from which to remove the
expiration time.
Returns:
If the key does not exist, returns an empty list. If the key exists, returns
a list which contains for each field in the request:
- `-2` if the field does not exist.
- `-1` if the field exists but has no associated expiration time.
- `1` if the expiration time was successfully removed from the field.
"""
return self.execute_command("HPERSIST", name, "FIELDS", len(fields), *fields)

def hexpiretime(self, key: KeyT, *fields: str) -> ResponseT:
"""
Returns the expiration times of hash fields as Unix timestamps in seconds.
For more information, see https://redis.io/commands/hexpiretime
Args:
key: The hash key.
fields: A list of fields within the hash for which to get the expiration
time.
Returns:
If the key does not exist, returns an empty list. If the key exists, returns
a list which contains for each field in the request:
- `-2` if the field does not exist.
- `-1` if the field exists but has no associated expire time.
- A positive integer representing the expiration Unix timestamp in
seconds, if the field has an associated expiration time.
"""
return self.execute_command("HEXPIRETIME", key, "FIELDS", len(fields), *fields)

def hpexpiretime(self, key: KeyT, *fields: str) -> ResponseT:
"""
Returns the expiration times of hash fields as Unix timestamps in milliseconds.
For more information, see https://redis.io/commands/hpexpiretime
Args:
key: The hash key.
fields: A list of fields within the hash for which to get the expiration
time.
Returns:
If the key does not exist, returns an empty list. If the key exists, returns
a list which contains for each field in the request:
- `-2` if the field does not exist.
- `-1` if the field exists but has no associated expire time.
- A positive integer representing the expiration Unix timestamp in
milliseconds, if the field has an associated expiration time.
"""
return self.execute_command("HPEXPIRETIME", key, "FIELDS", len(fields), *fields)

def httl(self, key: KeyT, *fields: str) -> ResponseT:
"""
Returns the TTL (Time To Live) in seconds for each specified field within a hash
key.
For more information, see https://redis.io/commands/httl
Args:
key: The hash key.
fields: A list of fields within the hash for which to get the TTL.
Returns:
If the key does not exist, returns an empty list. If the key exists, returns
a list which contains for each field in the request:
- `-2` if the field does not exist.
- `-1` if the field exists but has no associated expire time.
- A positive integer representing the TTL in seconds if the field has
an associated expiration time.
"""
return self.execute_command("HTTL", key, "FIELDS", len(fields), *fields)

def hpttl(self, key: KeyT, *fields: str) -> ResponseT:
"""
Returns the TTL (Time To Live) in milliseconds for each specified field within a
hash key.
For more information, see https://redis.io/commands/hpttl
Args:
key: The hash key.
fields: A list of fields within the hash for which to get the TTL.
Returns:
If the key does not exist, returns an empty list. If the key exists, returns
a list which contains for each field in the request:
- `-2` if the field does not exist.
- `-1` if the field exists but has no associated expire time.
- A positive integer representing the TTL in milliseconds if the field
has an associated expiration time.
"""
return self.execute_command("HPTTL", key, "FIELDS", len(fields), *fields)


AsyncHashCommands = HashCommands

Expand Down
4 changes: 2 additions & 2 deletions tests/test_asyncio/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -1351,7 +1351,7 @@ async def test_hscan(self, r: redis.Redis):
_, dic = await r.hscan("a_notset", match="a")
assert dic == {}

@skip_if_server_version_lt("7.4.0")
@skip_if_server_version_lt("7.3.240")
async def test_hscan_novalues(self, r: redis.Redis):
await r.hset("a", mapping={"a": 1, "b": 2, "c": 3})
cursor, keys = await r.hscan("a", no_values=True)
Expand All @@ -1372,7 +1372,7 @@ async def test_hscan_iter(self, r: redis.Redis):
dic = {k: v async for k, v in r.hscan_iter("a_notset", match="a")}
assert dic == {}

@skip_if_server_version_lt("7.4.0")
@skip_if_server_version_lt("7.3.240")
async def test_hscan_iter_novalues(self, r: redis.Redis):
await r.hset("a", mapping={"a": 1, "b": 2, "c": 3})
keys = list([k async for k in r.hscan_iter("a", no_values=True)])
Expand Down
1 change: 1 addition & 0 deletions tests/test_asyncio/test_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,7 @@ async def test_list_keys(decoded_r: redis.Redis):
assert result == []


@pytest.mark.redismod
async def test_multi_label(decoded_r: redis.Redis):
redis_graph = decoded_r.graph("g")

Expand Down
Loading

0 comments on commit 45ed240

Please sign in to comment.