Skip to content

Commit 45ed240

Browse files
committed
Hash field expiration commands (redis#3218)
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]>
1 parent 153c310 commit 45ed240

File tree

7 files changed

+1056
-5
lines changed

7 files changed

+1056
-5
lines changed

redis/commands/core.py

+360
Original file line numberDiff line numberDiff line change
@@ -5087,6 +5087,366 @@ def hstrlen(self, name: str, key: str) -> Union[Awaitable[int], int]:
50875087
"""
50885088
return self.execute_command("HSTRLEN", name, key)
50895089

5090+
def hexpire(
5091+
self,
5092+
name: KeyT,
5093+
seconds: ExpiryT,
5094+
*fields: str,
5095+
nx: bool = False,
5096+
xx: bool = False,
5097+
gt: bool = False,
5098+
lt: bool = False,
5099+
) -> ResponseT:
5100+
"""
5101+
Sets or updates the expiration time for fields within a hash key, using relative
5102+
time in seconds.
5103+
5104+
If a field already has an expiration time, the behavior of the update can be
5105+
controlled using the `nx`, `xx`, `gt`, and `lt` parameters.
5106+
5107+
The return value provides detailed information about the outcome for each field.
5108+
5109+
For more information, see https://redis.io/commands/hexpire
5110+
5111+
Args:
5112+
name: The name of the hash key.
5113+
seconds: Expiration time in seconds, relative. Can be an integer, or a
5114+
Python `timedelta` object.
5115+
fields: List of fields within the hash to apply the expiration time to.
5116+
nx: Set expiry only when the field has no expiry.
5117+
xx: Set expiry only when the field has an existing expiry.
5118+
gt: Set expiry only when the new expiry is greater than the current one.
5119+
lt: Set expiry only when the new expiry is less than the current one.
5120+
5121+
Returns:
5122+
If the key does not exist, returns an empty list. If the key exists, returns
5123+
a list which contains for each field in the request:
5124+
- `-2` if the field does not exist.
5125+
- `0` if the specified NX | XX | GT | LT condition was not met.
5126+
- `1` if the expiration time was set or updated.
5127+
- `2` if the field was deleted because the specified expiration time is
5128+
in the past.
5129+
"""
5130+
conditions = [nx, xx, gt, lt]
5131+
if sum(conditions) > 1:
5132+
raise ValueError("Only one of 'nx', 'xx', 'gt', 'lt' can be specified.")
5133+
5134+
if isinstance(seconds, datetime.timedelta):
5135+
seconds = int(seconds.total_seconds())
5136+
5137+
options = []
5138+
if nx:
5139+
options.append("NX")
5140+
if xx:
5141+
options.append("XX")
5142+
if gt:
5143+
options.append("GT")
5144+
if lt:
5145+
options.append("LT")
5146+
5147+
return self.execute_command(
5148+
"HEXPIRE", name, seconds, *options, "FIELDS", len(fields), *fields
5149+
)
5150+
5151+
def hpexpire(
5152+
self,
5153+
name: KeyT,
5154+
milliseconds: ExpiryT,
5155+
*fields: str,
5156+
nx: bool = False,
5157+
xx: bool = False,
5158+
gt: bool = False,
5159+
lt: bool = False,
5160+
) -> ResponseT:
5161+
"""
5162+
Sets or updates the expiration time for fields within a hash key, using relative
5163+
time in milliseconds.
5164+
5165+
If a field already has an expiration time, the behavior of the update can be
5166+
controlled using the `nx`, `xx`, `gt`, and `lt` parameters.
5167+
5168+
The return value provides detailed information about the outcome for each field.
5169+
5170+
For more information, see https://redis.io/commands/hpexpire
5171+
5172+
Args:
5173+
name: The name of the hash key.
5174+
milliseconds: Expiration time in milliseconds, relative. Can be an integer,
5175+
or a Python `timedelta` object.
5176+
fields: List of fields within the hash to apply the expiration time to.
5177+
nx: Set expiry only when the field has no expiry.
5178+
xx: Set expiry only when the field has an existing expiry.
5179+
gt: Set expiry only when the new expiry is greater than the current one.
5180+
lt: Set expiry only when the new expiry is less than the current one.
5181+
5182+
Returns:
5183+
If the key does not exist, returns an empty list. If the key exists, returns
5184+
a list which contains for each field in the request:
5185+
- `-2` if the field does not exist.
5186+
- `0` if the specified NX | XX | GT | LT condition was not met.
5187+
- `1` if the expiration time was set or updated.
5188+
- `2` if the field was deleted because the specified expiration time is
5189+
in the past.
5190+
"""
5191+
conditions = [nx, xx, gt, lt]
5192+
if sum(conditions) > 1:
5193+
raise ValueError("Only one of 'nx', 'xx', 'gt', 'lt' can be specified.")
5194+
5195+
if isinstance(milliseconds, datetime.timedelta):
5196+
milliseconds = int(milliseconds.total_seconds() * 1000)
5197+
5198+
options = []
5199+
if nx:
5200+
options.append("NX")
5201+
if xx:
5202+
options.append("XX")
5203+
if gt:
5204+
options.append("GT")
5205+
if lt:
5206+
options.append("LT")
5207+
5208+
return self.execute_command(
5209+
"HPEXPIRE", name, milliseconds, *options, "FIELDS", len(fields), *fields
5210+
)
5211+
5212+
def hexpireat(
5213+
self,
5214+
name: KeyT,
5215+
unix_time_seconds: AbsExpiryT,
5216+
*fields: str,
5217+
nx: bool = False,
5218+
xx: bool = False,
5219+
gt: bool = False,
5220+
lt: bool = False,
5221+
) -> ResponseT:
5222+
"""
5223+
Sets or updates the expiration time for fields within a hash key, using an
5224+
absolute Unix timestamp in seconds.
5225+
5226+
If a field already has an expiration time, the behavior of the update can be
5227+
controlled using the `nx`, `xx`, `gt`, and `lt` parameters.
5228+
5229+
The return value provides detailed information about the outcome for each field.
5230+
5231+
For more information, see https://redis.io/commands/hexpireat
5232+
5233+
Args:
5234+
name: The name of the hash key.
5235+
unix_time_seconds: Expiration time as Unix timestamp in seconds. Can be an
5236+
integer or a Python `datetime` object.
5237+
fields: List of fields within the hash to apply the expiration time to.
5238+
nx: Set expiry only when the field has no expiry.
5239+
xx: Set expiry only when the field has an existing expiration time.
5240+
gt: Set expiry only when the new expiry is greater than the current one.
5241+
lt: Set expiry only when the new expiry is less than the current one.
5242+
5243+
Returns:
5244+
If the key does not exist, returns an empty list. If the key exists, returns
5245+
a list which contains for each field in the request:
5246+
- `-2` if the field does not exist.
5247+
- `0` if the specified NX | XX | GT | LT condition was not met.
5248+
- `1` if the expiration time was set or updated.
5249+
- `2` if the field was deleted because the specified expiration time is
5250+
in the past.
5251+
"""
5252+
conditions = [nx, xx, gt, lt]
5253+
if sum(conditions) > 1:
5254+
raise ValueError("Only one of 'nx', 'xx', 'gt', 'lt' can be specified.")
5255+
5256+
if isinstance(unix_time_seconds, datetime.datetime):
5257+
unix_time_seconds = int(unix_time_seconds.timestamp())
5258+
5259+
options = []
5260+
if nx:
5261+
options.append("NX")
5262+
if xx:
5263+
options.append("XX")
5264+
if gt:
5265+
options.append("GT")
5266+
if lt:
5267+
options.append("LT")
5268+
5269+
return self.execute_command(
5270+
"HEXPIREAT",
5271+
name,
5272+
unix_time_seconds,
5273+
*options,
5274+
"FIELDS",
5275+
len(fields),
5276+
*fields,
5277+
)
5278+
5279+
def hpexpireat(
5280+
self,
5281+
name: KeyT,
5282+
unix_time_milliseconds: AbsExpiryT,
5283+
*fields: str,
5284+
nx: bool = False,
5285+
xx: bool = False,
5286+
gt: bool = False,
5287+
lt: bool = False,
5288+
) -> ResponseT:
5289+
"""
5290+
Sets or updates the expiration time for fields within a hash key, using an
5291+
absolute Unix timestamp in milliseconds.
5292+
5293+
If a field already has an expiration time, the behavior of the update can be
5294+
controlled using the `nx`, `xx`, `gt`, and `lt` parameters.
5295+
5296+
The return value provides detailed information about the outcome for each field.
5297+
5298+
For more information, see https://redis.io/commands/hpexpireat
5299+
5300+
Args:
5301+
name: The name of the hash key.
5302+
unix_time_milliseconds: Expiration time as Unix timestamp in milliseconds.
5303+
Can be an integer or a Python `datetime` object.
5304+
fields: List of fields within the hash to apply the expiry.
5305+
nx: Set expiry only when the field has no expiry.
5306+
xx: Set expiry only when the field has an existing expiry.
5307+
gt: Set expiry only when the new expiry is greater than the current one.
5308+
lt: Set expiry only when the new expiry is less than the current one.
5309+
5310+
Returns:
5311+
If the key does not exist, returns an empty list. If the key exists, returns
5312+
a list which contains for each field in the request:
5313+
- `-2` if the field does not exist.
5314+
- `0` if the specified NX | XX | GT | LT condition was not met.
5315+
- `1` if the expiration time was set or updated.
5316+
- `2` if the field was deleted because the specified expiration time is
5317+
in the past.
5318+
"""
5319+
conditions = [nx, xx, gt, lt]
5320+
if sum(conditions) > 1:
5321+
raise ValueError("Only one of 'nx', 'xx', 'gt', 'lt' can be specified.")
5322+
5323+
if isinstance(unix_time_milliseconds, datetime.datetime):
5324+
unix_time_milliseconds = int(unix_time_milliseconds.timestamp() * 1000)
5325+
5326+
options = []
5327+
if nx:
5328+
options.append("NX")
5329+
if xx:
5330+
options.append("XX")
5331+
if gt:
5332+
options.append("GT")
5333+
if lt:
5334+
options.append("LT")
5335+
5336+
return self.execute_command(
5337+
"HPEXPIREAT",
5338+
name,
5339+
unix_time_milliseconds,
5340+
*options,
5341+
"FIELDS",
5342+
len(fields),
5343+
*fields,
5344+
)
5345+
5346+
def hpersist(self, name: KeyT, *fields: str) -> ResponseT:
5347+
"""
5348+
Removes the expiration time for each specified field in a hash.
5349+
5350+
For more information, see https://redis.io/commands/hpersist
5351+
5352+
Args:
5353+
name: The name of the hash key.
5354+
fields: A list of fields within the hash from which to remove the
5355+
expiration time.
5356+
5357+
Returns:
5358+
If the key does not exist, returns an empty list. If the key exists, returns
5359+
a list which contains for each field in the request:
5360+
- `-2` if the field does not exist.
5361+
- `-1` if the field exists but has no associated expiration time.
5362+
- `1` if the expiration time was successfully removed from the field.
5363+
"""
5364+
return self.execute_command("HPERSIST", name, "FIELDS", len(fields), *fields)
5365+
5366+
def hexpiretime(self, key: KeyT, *fields: str) -> ResponseT:
5367+
"""
5368+
Returns the expiration times of hash fields as Unix timestamps in seconds.
5369+
5370+
For more information, see https://redis.io/commands/hexpiretime
5371+
5372+
Args:
5373+
key: The hash key.
5374+
fields: A list of fields within the hash for which to get the expiration
5375+
time.
5376+
5377+
Returns:
5378+
If the key does not exist, returns an empty list. If the key exists, returns
5379+
a list which contains for each field in the request:
5380+
- `-2` if the field does not exist.
5381+
- `-1` if the field exists but has no associated expire time.
5382+
- A positive integer representing the expiration Unix timestamp in
5383+
seconds, if the field has an associated expiration time.
5384+
"""
5385+
return self.execute_command("HEXPIRETIME", key, "FIELDS", len(fields), *fields)
5386+
5387+
def hpexpiretime(self, key: KeyT, *fields: str) -> ResponseT:
5388+
"""
5389+
Returns the expiration times of hash fields as Unix timestamps in milliseconds.
5390+
5391+
For more information, see https://redis.io/commands/hpexpiretime
5392+
5393+
Args:
5394+
key: The hash key.
5395+
fields: A list of fields within the hash for which to get the expiration
5396+
time.
5397+
5398+
Returns:
5399+
If the key does not exist, returns an empty list. If the key exists, returns
5400+
a list which contains for each field in the request:
5401+
- `-2` if the field does not exist.
5402+
- `-1` if the field exists but has no associated expire time.
5403+
- A positive integer representing the expiration Unix timestamp in
5404+
milliseconds, if the field has an associated expiration time.
5405+
"""
5406+
return self.execute_command("HPEXPIRETIME", key, "FIELDS", len(fields), *fields)
5407+
5408+
def httl(self, key: KeyT, *fields: str) -> ResponseT:
5409+
"""
5410+
Returns the TTL (Time To Live) in seconds for each specified field within a hash
5411+
key.
5412+
5413+
For more information, see https://redis.io/commands/httl
5414+
5415+
Args:
5416+
key: The hash key.
5417+
fields: A list of fields within the hash for which to get the TTL.
5418+
5419+
Returns:
5420+
If the key does not exist, returns an empty list. If the key exists, returns
5421+
a list which contains for each field in the request:
5422+
- `-2` if the field does not exist.
5423+
- `-1` if the field exists but has no associated expire time.
5424+
- A positive integer representing the TTL in seconds if the field has
5425+
an associated expiration time.
5426+
"""
5427+
return self.execute_command("HTTL", key, "FIELDS", len(fields), *fields)
5428+
5429+
def hpttl(self, key: KeyT, *fields: str) -> ResponseT:
5430+
"""
5431+
Returns the TTL (Time To Live) in milliseconds for each specified field within a
5432+
hash key.
5433+
5434+
For more information, see https://redis.io/commands/hpttl
5435+
5436+
Args:
5437+
key: The hash key.
5438+
fields: A list of fields within the hash for which to get the TTL.
5439+
5440+
Returns:
5441+
If the key does not exist, returns an empty list. If the key exists, returns
5442+
a list which contains for each field in the request:
5443+
- `-2` if the field does not exist.
5444+
- `-1` if the field exists but has no associated expire time.
5445+
- A positive integer representing the TTL in milliseconds if the field
5446+
has an associated expiration time.
5447+
"""
5448+
return self.execute_command("HPTTL", key, "FIELDS", len(fields), *fields)
5449+
50905450

50915451
AsyncHashCommands = HashCommands
50925452

tests/test_asyncio/test_commands.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1351,7 +1351,7 @@ async def test_hscan(self, r: redis.Redis):
13511351
_, dic = await r.hscan("a_notset", match="a")
13521352
assert dic == {}
13531353

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

1375-
@skip_if_server_version_lt("7.4.0")
1375+
@skip_if_server_version_lt("7.3.240")
13761376
async def test_hscan_iter_novalues(self, r: redis.Redis):
13771377
await r.hset("a", mapping={"a": 1, "b": 2, "c": 3})
13781378
keys = list([k async for k in r.hscan_iter("a", no_values=True)])

tests/test_asyncio/test_graph.py

+1
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,7 @@ async def test_list_keys(decoded_r: redis.Redis):
373373
assert result == []
374374

375375

376+
@pytest.mark.redismod
376377
async def test_multi_label(decoded_r: redis.Redis):
377378
redis_graph = decoded_r.graph("g")
378379

0 commit comments

Comments
 (0)