diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bf984c3b0..e06cb2ffc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ - Node: Allow routing Cluster requests by address. ([#1021](https://github.com/aws/glide-for-redis/pull/1021)) * Python: Added HSETNX command. ([#954](https://github.com/aws/glide-for-redis/pull/954)) * Python: Added SISMEMBER command ([#971](https://github.com/aws/glide-for-redis/pull/971)) +* Python: Added ZRANGE command ([#906](https://github.com/aws/glide-for-redis/pull/906)) ## 0.2.0 (2024-02-11) diff --git a/python/python/glide/__init__.py b/python/python/glide/__init__.py index b4448ffad2..978706bc63 100644 --- a/python/python/glide/__init__.py +++ b/python/python/glide/__init__.py @@ -5,11 +5,18 @@ ExpireOptions, ExpirySet, ExpiryType, - InfBound, InfoSection, - ScoreLimit, UpdateOptions, ) +from glide.async_commands.sorted_set import ( + InfBound, + LexBoundary, + Limit, + RangeByIndex, + RangeByLex, + RangeByScore, + ScoreBoundary, +) from glide.async_commands.transaction import ClusterTransaction, Transaction from glide.config import ( BaseClientConfiguration, @@ -45,13 +52,18 @@ "BaseClientConfiguration", "ClusterClientConfiguration", "RedisClientConfiguration", - "ScoreLimit", + "ScoreBoundary", "ConditionalChange", "ExpireOptions", "ExpirySet", "ExpiryType", "InfBound", "InfoSection", + "LexBoundary", + "Limit", + "RangeByIndex", + "RangeByLex", + "RangeByScore", "UpdateOptions", "Logger", "LogLevel", diff --git a/python/python/glide/async_commands/core.py b/python/python/glide/async_commands/core.py index fa51137e66..4b8efe787c 100644 --- a/python/python/glide/async_commands/core.py +++ b/python/python/glide/async_commands/core.py @@ -16,6 +16,14 @@ get_args, ) +from glide.async_commands.sorted_set import ( + InfBound, + RangeByIndex, + RangeByLex, + RangeByScore, + ScoreBoundary, + _create_zrange_args, +) from glide.constants import TOK, TResult from glide.protobuf.redis_request_pb2 import RequestType from glide.routes import Route @@ -123,29 +131,6 @@ class UpdateOptions(Enum): GREATER_THAN = "GT" -class InfBound(Enum): - """ - Enumeration representing positive and negative infinity bounds for sorted set scores. - """ - - POS_INF = "+inf" - NEG_INF = "-inf" - - -class ScoreLimit: - """ - Represents a score limit in a sorted set. - - Args: - value (float): The score value. - is_inclusive (bool): Whether the score value is inclusive. Defaults to False. - """ - - def __init__(self, value: float, is_inclusive: bool = True): - """Convert the score limit to the Redis protocol format.""" - self.value = str(value) if is_inclusive else f"({value}" - - class ExpirySet: """SET option: Represents the expiry type and value to be executed with "SET" command.""" @@ -1253,9 +1238,9 @@ async def zadd( If `changed` is set, returns the number of elements updated in the sorted set. Examples: - >>> await zadd("my_sorted_set", {"member1": 10.5, "member2": 8.2}) + >>> await client.zadd("my_sorted_set", {"member1": 10.5, "member2": 8.2}) 2 # Indicates that two elements have been added or updated in the sorted set "my_sorted_set." - >>> await zadd("existing_sorted_set", {"member1": 15.0, "member2": 5.5}, existing_options=ConditionalChange.XX) + >>> await client.zadd("existing_sorted_set", {"member1": 15.0, "member2": 5.5}, existing_options=ConditionalChange.XX) 2 # Updates the scores of two existing members in the sorted set "existing_sorted_set." """ args = [key] @@ -1316,9 +1301,9 @@ async def zadd_incr( If there was a conflict with choosing the XX/NX/LT/GT options, the operation aborts and None is returned. Examples: - >>> await zaddIncr("my_sorted_set", member , 5.0) + >>> await client.zaddIncr("my_sorted_set", member , 5.0) 5.0 - >>> await zaddIncr("existing_sorted_set", member , "3.0" , UpdateOptions.LESS_THAN) + >>> await client.zaddIncr("existing_sorted_set", member , "3.0" , UpdateOptions.LESS_THAN) None """ args = [key] @@ -1357,9 +1342,9 @@ async def zcard(self, key: str) -> int: If `key` does not exist, it is treated as an empty sorted set, and the command returns 0. Examples: - >>> await zcard("my_sorted_set") + >>> await client.zcard("my_sorted_set") 3 # Indicates that there are 3 elements in the sorted set "my_sorted_set". - >>> await zcard("non_existing_key") + >>> await client.zcard("non_existing_key") 0 """ return cast(int, await self._execute_command(RequestType.Zcard, [key])) @@ -1367,8 +1352,8 @@ async def zcard(self, key: str) -> int: async def zcount( self, key: str, - min_score: Union[InfBound, ScoreLimit], - max_score: Union[InfBound, ScoreLimit], + min_score: Union[InfBound, ScoreBoundary], + max_score: Union[InfBound, ScoreBoundary], ) -> int: """ Returns the number of members in the sorted set stored at `key` with scores between `min_score` and `max_score`. @@ -1377,12 +1362,12 @@ async def zcount( Args: key (str): The key of the sorted set. - min_score (Union[InfBound, ScoreLimit]): The minimum score to count from. + min_score (Union[InfBound, ScoreBoundary]): The minimum score to count from. Can be an instance of InfBound representing positive/negative infinity, - or ScoreLimit representing a specific score and inclusivity. - max_score (Union[InfBound, ScoreLimit]): The maximum score to count up to. + or ScoreBoundary representing a specific score and inclusivity. + max_score (Union[InfBound, ScoreBoundary]): The maximum score to count up to. Can be an instance of InfBound representing positive/negative infinity, - or ScoreLimit representing a specific score and inclusivity. + or ScoreBoundary representing a specific score and inclusivity. Returns: int: The number of members in the specified score range. @@ -1390,15 +1375,25 @@ async def zcount( If `max_score` < `min_score`, 0 is returned. Examples: - >>> await client.zcount("my_sorted_set", ScoreLimit(5.0 , is_inclusive=true) , InfBound.POS_INF) + >>> await client.zcount("my_sorted_set", ScoreBoundary(5.0 , is_inclusive=true) , InfBound.POS_INF) 2 # Indicates that there are 2 members with scores between 5.0 (not exclusive) and +inf in the sorted set "my_sorted_set". - >>> await client.zcount("my_sorted_set", ScoreLimit(5.0 , is_inclusive=true) , ScoreLimit(10.0 , is_inclusive=false)) - 1 # Indicates that there is one ScoreLimit with 5.0 < score <= 10.0 in the sorted set "my_sorted_set". + >>> await client.zcount("my_sorted_set", ScoreBoundary(5.0 , is_inclusive=true) , ScoreBoundary(10.0 , is_inclusive=false)) + 1 # Indicates that there is one ScoreBoundary with 5.0 < score <= 10.0 in the sorted set "my_sorted_set". """ + score_min = ( + min_score.value["score_arg"] + if type(min_score) == InfBound + else min_score.value + ) + score_max = ( + max_score.value["score_arg"] + if type(max_score) == InfBound + else max_score.value + ) return cast( int, await self._execute_command( - RequestType.Zcount, [key, min_score.value, max_score.value] + RequestType.Zcount, [key, score_min, score_max] ), ) @@ -1466,6 +1461,78 @@ async def zpopmin( ), ) + async def zrange( + self, + key: str, + range_query: Union[RangeByIndex, RangeByLex, RangeByScore], + reverse: bool = False, + ) -> List[str]: + """ + Returns the specified range of elements in the sorted set stored at `key`. + + ZRANGE can perform different types of range queries: by index (rank), by the score, or by lexicographical order. + + See https://redis.io/commands/zrange/ for more details. + + To get the elements with their scores, see zrange_withscores. + + Args: + key (str): The key of the sorted set. + range_query (Union[RangeByIndex, RangeByLex, RangeByScore]): The range query object representing the type of range query to perform. + - For range queries by index (rank), use RangeByIndex. + - For range queries by lexicographical order, use RangeByLex. + - For range queries by score, use RangeByScore. + reverse (bool): If True, reverses the sorted set, with index 0 as the element with the highest score. + + Returns: + List[str]: A list of elements within the specified range. + If `key` does not exist, it is treated as an empty sorted set, and the command returns an empty array. + + Examples: + >>> await client.zrange("my_sorted_set", RangeByIndex(0, -1)) + ['member1', 'member2', 'member3'] # Returns all members in ascending order. + >>> await client.zrange("my_sorted_set", RangeByScore(start=InfBound.NEG_INF, stop=ScoreBoundary(3))) + ['member2', 'member3'] # Returns members with scores within the range of negative infinity to 3, in ascending order. + """ + args = _create_zrange_args(key, range_query, reverse, with_scores=False) + + return cast(List[str], await self._execute_command(RequestType.Zrange, args)) + + async def zrange_withscores( + self, + key: str, + range_query: Union[RangeByIndex, RangeByScore], + reverse: bool = False, + ) -> Mapping[str, float]: + """ + Returns the specified range of elements with their scores in the sorted set stored at `key`. + Similar to ZRANGE but with a WITHSCORE flag. + + See https://redis.io/commands/zrange/ for more details. + + Args: + key (str): The key of the sorted set. + range_query (Union[RangeByIndex, RangeByScore]): The range query object representing the type of range query to perform. + - For range queries by index (rank), use RangeByIndex. + - For range queries by score, use RangeByScore. + reverse (bool): If True, reverses the sorted set, with index 0 as the element with the highest score. + + Returns: + Mapping[str , float]: A map of elements and their scores within the specified range. + If `key` does not exist, it is treated as an empty sorted set, and the command returns an empty map. + + Examples: + >>> await client.zrange_withscores("my_sorted_set", RangeByScore(ScoreBoundary(10), ScoreBoundary(20))) + {'member1': 10.5, 'member2': 15.2} # Returns members with scores between 10 and 20 with their scores. + >>> await client.zrange("my_sorted_set", RangeByScore(start=InfBound.NEG_INF, stop=ScoreBoundary(3))) + {'member4': -2.0, 'member7': 1.5} # Returns members with with scores within the range of negative infinity to 3, with their scores. + """ + args = _create_zrange_args(key, range_query, reverse, with_scores=True) + + return cast( + Mapping[str, float], await self._execute_command(RequestType.Zrange, args) + ) + async def zrem( self, key: str, diff --git a/python/python/glide/async_commands/sorted_set.py b/python/python/glide/async_commands/sorted_set.py new file mode 100644 index 0000000000..83c6037341 --- /dev/null +++ b/python/python/glide/async_commands/sorted_set.py @@ -0,0 +1,160 @@ +# Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + +from enum import Enum +from typing import List, Optional, Union + + +class InfBound(Enum): + """ + Enumeration representing numeric and lexicographic positive and negative infinity bounds for sorted set. + """ + + POS_INF = {"score_arg": "+inf", "lex_arg": "+"} + """ + Positive infinity bound for sorted set. + score_arg: represents numeric positive infinity (+inf). + lex_arg: represents lexicographic positive infinity (+). + """ + NEG_INF = {"score_arg": "-inf", "lex_arg": "-"} + """ + Negative infinity bound for sorted set. + score_arg: represents numeric negative infinity (-inf). + lex_arg: represents lexicographic negative infinity (-). + """ + + +class ScoreBoundary: + """ + Represents a specific numeric score boundary in a sorted set. + + Args: + value (float): The score value. + is_inclusive (bool): Whether the score value is inclusive. Defaults to True. + """ + + def __init__(self, value: float, is_inclusive: bool = True): + # Convert the score boundary to the Redis protocol format + self.value = str(value) if is_inclusive else f"({value}" + + +class LexBoundary: + """ + Represents a specific lexicographic boundary in a sorted set. + + Args: + value (str): The lex value. + is_inclusive (bool): Whether the score value is inclusive. Defaults to True. + """ + + def __init__(self, value: str, is_inclusive: bool = True): + # Convert the lexicographic boundary to the Redis protocol format + self.value = f"[{value}" if is_inclusive else f"({value}" + + +class Limit: + """ + Represents a limit argument for a range query in a sorted set to be used in [ZRANGE](https://redis.io/commands/zrange) command. + + The optional LIMIT argument can be used to obtain a sub-range from the matching elements + (similar to SELECT LIMIT offset, count in SQL). + Args: + offset (int): The offset from the start of the range. + count (int): The number of elements to include in the range. + A negative count returns all elements from the offset. + """ + + def __init__(self, offset: int, count: int): + self.offset = offset + self.count = count + + +class RangeByIndex: + """ + Represents a range by index (rank) in a sorted set. + + The `start` and `stop` arguments represent zero-based indexes. + + Args: + start (int): The start index of the range. + stop (int): The stop index of the range. + """ + + def __init__(self, start: int, stop: int): + self.start = start + self.stop = stop + + +class RangeByScore: + """ + Represents a range by score in a sorted set. + + The `start` and `stop` arguments represent score boundaries. + + Args: + start (Union[InfBound, ScoreBoundary]): The start score boundary. + stop (Union[InfBound, ScoreBoundary]): The stop score boundary. + limit (Optional[Limit]): The limit argument for a range query. Defaults to None. See `Limit` class for more information. + """ + + def __init__( + self, + start: Union[InfBound, ScoreBoundary], + stop: Union[InfBound, ScoreBoundary], + limit: Optional[Limit] = None, + ): + self.start = ( + start.value["score_arg"] if type(start) == InfBound else start.value + ) + self.stop = stop.value["score_arg"] if type(stop) == InfBound else stop.value + self.limit = limit + + +class RangeByLex: + """ + Represents a range by lexicographical order in a sorted set. + + The `start` and `stop` arguments represent lexicographical boundaries. + + Args: + start (Union[InfBound, LexBoundary]): The start lexicographic boundary. + stop (Union[InfBound, LexBoundary]): The stop lexicographic boundary. + limit (Optional[Limit]): The limit argument for a range query. Defaults to None. See `Limit` class for more information. + """ + + def __init__( + self, + start: Union[InfBound, LexBoundary], + stop: Union[InfBound, LexBoundary], + limit: Optional[Limit] = None, + ): + self.start = start.value["lex_arg"] if type(start) == InfBound else start.value + self.stop = stop.value["lex_arg"] if type(stop) == InfBound else stop.value + self.limit = limit + + +def _create_zrange_args( + key: str, + range_query: Union[RangeByLex, RangeByScore, RangeByIndex], + reverse: bool, + with_scores: bool, +) -> List[str]: + args = [key, str(range_query.start), str(range_query.stop)] + + if isinstance(range_query, RangeByScore): + args.append("BYSCORE") + elif isinstance(range_query, RangeByLex): + args.append("BYLEX") + if reverse: + args.append("REV") + if hasattr(range_query, "limit") and range_query.limit is not None: + args.extend( + [ + "LIMIT", + str(range_query.limit.offset), + str(range_query.limit.count), + ] + ) + if with_scores: + args.append("WITHSCORES") + + return args diff --git a/python/python/glide/async_commands/transaction.py b/python/python/glide/async_commands/transaction.py index ed3ba10172..58cfab4ac8 100644 --- a/python/python/glide/async_commands/transaction.py +++ b/python/python/glide/async_commands/transaction.py @@ -7,11 +7,17 @@ ConditionalChange, ExpireOptions, ExpirySet, - InfBound, InfoSection, - ScoreLimit, UpdateOptions, ) +from glide.async_commands.sorted_set import ( + InfBound, + RangeByIndex, + RangeByLex, + RangeByScore, + ScoreBoundary, + _create_zrange_args, +) from glide.protobuf.redis_request_pb2 import RequestType TTransaction = TypeVar("TTransaction", bound="BaseTransaction") @@ -1082,8 +1088,8 @@ def zcard(self: TTransaction, key: str) -> TTransaction: def zcount( self: TTransaction, key: str, - min_score: Union[InfBound, ScoreLimit], - max_score: Union[InfBound, ScoreLimit], + min_score: Union[InfBound, ScoreBoundary], + max_score: Union[InfBound, ScoreBoundary], ) -> TTransaction: """ Returns the number of members in the sorted set stored at `key` with scores between `min_score` and `max_score`. @@ -1092,21 +1098,29 @@ def zcount( Args: key (str): The key of the sorted set. - min_score (Union[InfBound, ScoreLimit]): The minimum score to count from. + min_score (Union[InfBound, ScoreBoundary]): The minimum score to count from. Can be an instance of InfBound representing positive/negative infinity, - or ScoreLimit representing a specific score and inclusivity. - max_score (Union[InfBound, ScoreLimit]): The maximum score to count up to. + or ScoreBoundary representing a specific score and inclusivity. + max_score (Union[InfBound, ScoreBoundary]): The maximum score to count up to. Can be an instance of InfBound representing positive/negative infinity, - or ScoreLimit representing a specific score and inclusivity. + or ScoreBoundary representing a specific score and inclusivity. Commands response: int: The number of members in the specified score range. If key does not exist, 0 is returned. If `max_score` < `min_score`, 0 is returned. """ - return self.append_command( - RequestType.Zcount, [key, min_score.value, max_score.value] + score_min = ( + min_score.value["score_arg"] + if type(min_score) == InfBound + else min_score.value + ) + score_max = ( + max_score.value["score_arg"] + if type(max_score) == InfBound + else max_score.value ) + return self.append_command(RequestType.Zcount, [key, score_min, score_max]) def zpopmax( self: TTransaction, key: str, count: Optional[int] = None @@ -1154,6 +1168,62 @@ def zpopmin( RequestType.ZPopMin, [key, str(count)] if count else [key] ) + def zrange( + self: TTransaction, + key: str, + range_query: Union[RangeByIndex, RangeByLex, RangeByScore], + reverse: bool = False, + ) -> TTransaction: + """ + Returns the specified range of elements in the sorted set stored at `key`. + + ZRANGE can perform different types of range queries: by index (rank), by the score, or by lexicographical order. + + See https://redis.io/commands/zrange/ for more details. + + Args: + key (str): The key of the sorted set. + range_query (Union[RangeByIndex, RangeByLex, RangeByScore]): The range query object representing the type of range query to perform. + - For range queries by index (rank), use RangeByIndex. + - For range queries by lexicographical order, use RangeByLex. + - For range queries by score, use RangeByScore. + reverse (bool): If True, reverses the sorted set, with index 0 as the element with the highest score. + + Commands response: + List[str]: A list of elements within the specified range. + If `key` does not exist, it is treated as an empty sorted set, and the command returns an empty array. + """ + args = _create_zrange_args(key, range_query, reverse, with_scores=False) + + return self.append_command(RequestType.Zrange, args) + + def zrange_withscores( + self: TTransaction, + key: str, + range_query: Union[RangeByIndex, RangeByScore], + reverse: bool = False, + ) -> TTransaction: + """ + Returns the specified range of elements with their scores in the sorted set stored at `key`. + Similar to ZRANGE but with a WITHSCORE flag. + + See https://redis.io/commands/zrange/ for more details. + + Args: + key (str): The key of the sorted set. + range_query (Union[RangeByIndex, RangeByScore]): The range query object representing the type of range query to perform. + - For range queries by index (rank), use RangeByIndex. + - For range queries by score, use RangeByScore. + reverse (bool): If True, reverses the sorted set, with index 0 as the element with the highest score. + + Commands response: + Mapping[str , float]: A map of elements and their scores within the specified range. + If `key` does not exist, it is treated as an empty sorted set, and the command returns an empty map. + """ + args = _create_zrange_args(key, range_query, reverse, with_scores=True) + + return self.append_command(RequestType.Zrange, args) + def zrem( self: TTransaction, key: str, diff --git a/python/python/tests/test_async_client.py b/python/python/tests/test_async_client.py index 5fff8c965e..217920710b 100644 --- a/python/python/tests/test_async_client.py +++ b/python/python/tests/test_async_client.py @@ -18,9 +18,17 @@ ExpiryType, InfBound, InfoSection, - ScoreLimit, UpdateOptions, ) +from glide.async_commands.sorted_set import ( + InfBound, + LexBoundary, + Limit, + RangeByIndex, + RangeByLex, + RangeByScore, + ScoreBoundary, +) from glide.config import ProtocolVersion, RedisCredentials from glide.constants import OK from glide.redis_client import RedisClient, RedisClusterClient, TRedisClient @@ -1170,18 +1178,32 @@ async def test_zcount(self, redis_client: TRedisClient): assert await redis_client.zcount(key, InfBound.NEG_INF, InfBound.POS_INF) == 3 assert ( - await redis_client.zcount(key, ScoreLimit(1, False), ScoreLimit(3, False)) + await redis_client.zcount( + key, + ScoreBoundary(1, is_inclusive=False), + ScoreBoundary(3, is_inclusive=False), + ) == 1 ) assert ( - await redis_client.zcount(key, ScoreLimit(1, False), ScoreLimit(3, True)) + await redis_client.zcount( + key, + ScoreBoundary(1, is_inclusive=False), + ScoreBoundary(3, is_inclusive=True), + ) == 2 ) assert ( - await redis_client.zcount(key, InfBound.NEG_INF, ScoreLimit(3, True)) == 3 + await redis_client.zcount( + key, InfBound.NEG_INF, ScoreBoundary(3, is_inclusive=True) + ) + == 3 ) assert ( - await redis_client.zcount(key, InfBound.POS_INF, ScoreLimit(3, True)) == 0 + await redis_client.zcount( + key, InfBound.POS_INF, ScoreBoundary(3, is_inclusive=True) + ) + == 0 ) assert ( await redis_client.zcount( @@ -1234,6 +1256,194 @@ async def test_zpopmax(self, redis_client: TRedisClient): assert await redis_client.zpopmax("non_exisitng_key") == {} + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_zrange_by_index(self, redis_client: TRedisClient): + key = get_random_string(10) + members_scores = {"one": 1, "two": 2, "three": 3} + assert await redis_client.zadd(key, members_scores=members_scores) == 3 + + assert await redis_client.zrange(key, RangeByIndex(start=0, stop=1)) == [ + "one", + "two", + ] + + assert ( + await redis_client.zrange_withscores(key, RangeByIndex(start=0, stop=-1)) + ) == {"one": 1.0, "two": 2.0, "three": 3.0} + + assert await redis_client.zrange( + key, RangeByIndex(start=0, stop=1), reverse=True + ) == [ + "three", + "two", + ] + + assert await redis_client.zrange(key, RangeByIndex(start=3, stop=1)) == [] + assert ( + await redis_client.zrange_withscores(key, RangeByIndex(start=3, stop=1)) + == {} + ) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_zrange_byscore(self, redis_client: TRedisClient): + key = get_random_string(10) + members_scores = {"one": 1, "two": 2, "three": 3} + assert await redis_client.zadd(key, members_scores=members_scores) == 3 + + assert await redis_client.zrange( + key, + RangeByScore( + start=InfBound.NEG_INF, stop=ScoreBoundary(3, is_inclusive=False) + ), + ) == ["one", "two"] + + assert ( + await redis_client.zrange_withscores( + key, + RangeByScore(start=InfBound.NEG_INF, stop=InfBound.POS_INF), + ) + ) == {"one": 1.0, "two": 2.0, "three": 3.0} + + assert await redis_client.zrange( + key, + RangeByScore( + start=ScoreBoundary(3, is_inclusive=False), stop=InfBound.NEG_INF + ), + reverse=True, + ) == ["two", "one"] + + assert ( + await redis_client.zrange( + key, + RangeByScore( + start=InfBound.NEG_INF, + stop=InfBound.POS_INF, + limit=Limit(offset=1, count=2), + ), + ) + ) == ["two", "three"] + + assert ( + await redis_client.zrange( + key, + RangeByScore( + start=InfBound.NEG_INF, stop=ScoreBoundary(3, is_inclusive=False) + ), + reverse=True, + ) + == [] + ) # stop is greater than start with reverse set to True + + assert ( + await redis_client.zrange( + key, + RangeByScore( + start=InfBound.POS_INF, stop=ScoreBoundary(3, is_inclusive=False) + ), + ) + == [] + ) # start is greater than stop + + assert ( + await redis_client.zrange_withscores( + key, + RangeByScore( + start=InfBound.POS_INF, stop=ScoreBoundary(3, is_inclusive=False) + ), + ) + == {} + ) # start is greater than stop + + assert ( + await redis_client.zrange_withscores( + key, + RangeByScore( + start=InfBound.NEG_INF, stop=ScoreBoundary(3, is_inclusive=False) + ), + reverse=True, + ) + == {} + ) # stop is greater than start with reverse set to True + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_zrange_bylex(self, redis_client: TRedisClient): + key = get_random_string(10) + members_scores = {"a": 1, "b": 2, "c": 3} + assert await redis_client.zadd(key, members_scores=members_scores) == 3 + + assert await redis_client.zrange( + key, + RangeByLex( + start=InfBound.NEG_INF, stop=LexBoundary("c", is_inclusive=False) + ), + ) == ["a", "b"] + + assert ( + await redis_client.zrange( + key, + RangeByLex( + start=InfBound.NEG_INF, + stop=InfBound.POS_INF, + limit=Limit(offset=1, count=2), + ), + ) + ) == ["b", "c"] + + assert await redis_client.zrange( + key, + RangeByLex( + start=LexBoundary("c", is_inclusive=False), stop=InfBound.NEG_INF + ), + reverse=True, + ) == ["b", "a"] + + assert ( + await redis_client.zrange( + key, + RangeByLex( + start=InfBound.NEG_INF, stop=LexBoundary("c", is_inclusive=False) + ), + reverse=True, + ) + == [] + ) # stop is greater than start with reverse set to True + + assert ( + await redis_client.zrange( + key, + RangeByLex( + start=InfBound.POS_INF, stop=LexBoundary("c", is_inclusive=False) + ), + ) + == [] + ) # start is greater than stop + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_zrange_different_types_of_keys(self, redis_client: TRedisClient): + key = get_random_string(10) + + assert ( + await redis_client.zrange("non_existing_key", RangeByIndex(start=0, stop=1)) + == [] + ) + + assert ( + await redis_client.zrange_withscores( + "non_existing_key", RangeByIndex(start=0, stop=-1) + ) + ) == {} + + assert await redis_client.set(key, "value") == OK + with pytest.raises(RequestError): + await redis_client.zrange(key, RangeByIndex(start=0, stop=1)) + + with pytest.raises(RequestError): + await redis_client.zrange_withscores(key, RangeByIndex(start=0, stop=1)) + @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) async def test_type(self, redis_client: TRedisClient): diff --git a/python/python/tests/test_transaction.py b/python/python/tests/test_transaction.py index 621e2b1751..b3e7838ede 100644 --- a/python/python/tests/test_transaction.py +++ b/python/python/tests/test_transaction.py @@ -5,7 +5,7 @@ import pytest from glide import RequestError -from glide.async_commands.core import InfBound, ScoreLimit +from glide.async_commands.sorted_set import InfBound, RangeByIndex, ScoreBoundary from glide.async_commands.transaction import ( BaseTransaction, ClusterTransaction, @@ -146,10 +146,14 @@ def transaction_test( args.append(1) transaction.zcard(key8) args.append(2) - transaction.zcount(key8, ScoreLimit(2, True), InfBound.POS_INF) + transaction.zcount(key8, ScoreBoundary(2, is_inclusive=True), InfBound.POS_INF) args.append(2) transaction.zscore(key8, "two") args.append(2.0) + transaction.zrange(key8, RangeByIndex(start=0, stop=-1)) + args.append(["two", "three"]) + transaction.zrange_withscores(key8, RangeByIndex(start=0, stop=-1)) + args.append({"two": 2, "three": 3}) transaction.zpopmin(key8) args.append({"two": 2.0}) transaction.zpopmax(key8)