Skip to content

Commit e4d3c76

Browse files
gerzsevladvildanov
authored andcommitted
Hash field expiration commands (#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 df6de3f commit e4d3c76

File tree

9 files changed

+1086
-7
lines changed

9 files changed

+1086
-7
lines changed

redis/commands/core.py

+368
Original file line numberDiff line numberDiff line change
@@ -5094,6 +5094,374 @@ def hstrlen(self, name: str, key: str) -> Union[Awaitable[int], int]:
50945094
"""
50955095
return self.execute_command("HSTRLEN", name, key, keys=[name])
50965096

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

50985466
AsyncHashCommands = HashCommands
50995467

tests/test_asyncio/test_cluster.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1453,7 +1453,7 @@ async def test_memory_stats(self, r: RedisCluster) -> None:
14531453
assert isinstance(stats, dict)
14541454
for key, value in stats.items():
14551455
if key.startswith("db."):
1456-
assert isinstance(value, dict)
1456+
assert not isinstance(value, list)
14571457

14581458
@skip_if_server_version_lt("4.0.0")
14591459
async def test_memory_help(self, r: RedisCluster) -> None:

0 commit comments

Comments
 (0)