Skip to content

Commit

Permalink
Python: adds ZRANGE command
Browse files Browse the repository at this point in the history
  • Loading branch information
shohamazon committed Feb 13, 2024
1 parent 396bb6d commit cf5cff3
Show file tree
Hide file tree
Showing 6 changed files with 567 additions and 25 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* Python, Node: When recieving LPOP/RPOP with count, convert result to Array. ([#811](https://github.com/aws/glide-for-redis/pull/811))
* Python: Added TYPE command ([#945](https://github.com/aws/glide-for-redis/pull/945))
* Python: Added HLEN command ([#944](https://github.com/aws/glide-for-redis/pull/944))
* Python: Added ZRANGE command ([#906](https://github.com/aws/glide-for-redis/pull/906))

#### Features
* Python, Node: Added support in Lua Scripts ([#775](https://github.com/aws/glide-for-redis/pull/775), [#860](https://github.com/aws/glide-for-redis/pull/860))
Expand Down
14 changes: 12 additions & 2 deletions python/python/glide/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@
ExpiryType,
InfBound,
InfoSection,
ScoreLimit,
LexBoundary,
LexInfBound,
RangeByIndex,
RangeByLex,
RangeByScore,
ScoreBoundary,
UpdateOptions,
)
from glide.async_commands.transaction import ClusterTransaction, Transaction
Expand Down Expand Up @@ -45,13 +50,18 @@
"BaseClientConfiguration",
"ClusterClientConfiguration",
"RedisClientConfiguration",
"ScoreLimit",
"ScoreBoundary",
"ConditionalChange",
"ExpireOptions",
"ExpirySet",
"ExpiryType",
"InfBound",
"InfoSection",
"LexBoundary",
"LexInfBound",
"RangeByIndex",
"RangeByLex",
"RangeByScore",
"UpdateOptions",
"Logger",
"LogLevel",
Expand Down
248 changes: 235 additions & 13 deletions python/python/glide/async_commands/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,20 +132,124 @@ class InfBound(Enum):
NEG_INF = "-inf"


class ScoreLimit:
class LexInfBound(Enum):
"""
Represents a score limit in a sorted set.
Enumeration representing lexicographic positive and negative infinity bounds for sorted set scores.
"""

POS_INF = "+"
NEG_INF = "-"


class ScoreBoundary:
"""
Represents a score boundary in a sorted set.
Args:
value (float): The score value.
is_inclusive (bool): Whether the score value is inclusive. Defaults to False.
is_inclusive (bool): Whether the score value is inclusive. Defaults to True.
"""

def __init__(self, value: float, is_inclusive: bool = True):
"""Convert the score limit to the Redis protocol format."""
"""Convert the score boundary to the Redis protocol format."""
self.value = str(value) if is_inclusive else f"({value}"


class LexBoundary:
"""
Represents a 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 command.
The optional LIMIT argument can be used to only get a range of 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 query by index (rank) in a sorted set.
The `start` and `stop` arguments represent zero-based indexes.
If `start` is greater than either the end index of the sorted set or `stop`, an empty list is returned.
If `stop` is greater than the end index of the sorted set, Redis will use the last element of the sorted set.
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 query 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.
"""

def __init__(
self,
start: Union[InfBound, ScoreBoundary],
stop: Union[InfBound, ScoreBoundary],
limit: Optional[Limit] = None,
):
self.start = start.value
self.stop = stop.value
self.limit = limit


class RangeByLex:
"""
Represents a range query by lexicographical order in a sorted set.
The `start` and `stop` arguments represent lexicographical boundaries.
Args:
start (Union[LexInfBound, LexBoundary]): The start lexicographic boundary.
stop (Union[LexInfBound, LexBoundary]): The stop lexicographic boundary.
limit (Optional[Limit]): The limit argument for a range query. Defaults to None.
"""

def __init__(
self,
start: Union[LexInfBound, LexBoundary],
stop: Union[LexInfBound, LexBoundary],
limit: Optional[Limit] = None,
):
self.start = start.value
self.stop = stop.value
self.limit = limit


class ExpirySet:
"""SET option: Represents the expiry type and value to be executed with "SET" command."""

Expand Down Expand Up @@ -1288,8 +1392,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`.
Expand All @@ -1298,12 +1402,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.
Expand All @@ -1312,10 +1416,10 @@ async def zcount(
If `key` holds a value that is not a sorted set, an error is returned.
Examples:
>>> await zcount("my_sorted_set", ScoreLimit(5.0 , is_inclusive=true) , InfBound.POS_INF)
>>> await 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 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 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".
"""
return cast(
int,
Expand All @@ -1324,6 +1428,124 @@ async def zcount(
),
)

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.
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
By default, `start` must be less than or equal to `stop` to return anything.
However, if range_query is not an index query, the `start` is the highest score to consider, and `stop` is the lowest score to consider,
therefore `start` must be greater than or equal to `stop` in order to return anything.
Returns:
List[str]: A list of elements within the specified range.
For idex queries, If `start` is greater than either the end index of the sorted set or `stop`, an empty list is returned.
For lex/score queries:
- If `start` is greater than either the end index of the sorted set or `stop` and `reverse` is set to False,
an empty list is returned.
- If `stop` is greater than either the end index of the sorted set or `start` and `reverse` is set to True,
an empty list is returned.
If `key` does not exist, it is treated as an empty sorted set, and the command returns an empty array.
If `key` holds a value that is not a sorted set, an error is returned.
Examples:
>>> await zrange("my_sorted_set", RangeByIndex(0, -1))
['member1', 'member2', 'member3'] # Returns all members in ascending order.
>>> await zrange("non_existing_key", RangeByScore(ScoreBoundary(0), ScoreBoundary(-1)))
[]
"""
args = [key, str(range_query.start), str(range_query.stop)]
if reverse:
args.append("REV")
if not isinstance(range_query, RangeByIndex):
args.append("BYSCORE" if type(range_query) is RangeByScore else "BYLEX")
if range_query.limit:
args.extend(
[
"LIMIT",
str(range_query.limit.offset),
str(range_query.limit.count),
]
)

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 WTHISCORE 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
By default, `start` must be less than or equal to `stop` to return anything.
However, if range_query is not an index query, the `start` is the highest score to consider, and `stop` is the lowest score to consider,
therefore `start` must be greater than or equal to `stop` in order to return anything.
Returns:
Map[str , float]: A map of elements and their scores within the specified range.
For idex queries, If `start` is greater than either the end index of the sorted set or `stop`, an empty map is returned.
For score queries:
- If `start` is greater than either the end index of the sorted set or `stop` and `reverse` is set to False,
an empty map is returned.
- If `stop` is greater than either the end index of the sorted set or `start` and `reverse` is set to True,
an empty map is returned.
If `key` does not exist, it is treated as an empty sorted set, and the command returns an empty map.
If `key` holds a value that is not a sorted set, an error is returned.
Examples:
>>> await 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 zrange_withscores("non_existing_key", RangeByScore(ScoreBoundary(0), ScoreBoundary(-1)))
{}
"""
args = [key, str(range_query.start), str(range_query.stop)]
if reverse:
args.append("REV")
if isinstance(range_query, RangeByScore):
args.append("BYSCORE")
if range_query.limit:
args.extend(
[
"LIMIT",
str(range_query.limit.offset),
str(range_query.limit.count),
]
)

args.append("WITHSCORES")

return cast(
Mapping[str, float], await self._execute_command(RequestType.Zrange, args)
)

async def zrem(
self,
key: str,
Expand Down
Loading

0 comments on commit cf5cff3

Please sign in to comment.