From d1fcc943d91132362c17ccfcf8e7b08be484d2f3 Mon Sep 17 00:00:00 2001 From: Gilboab <97948000+GilboaAWS@users.noreply.github.com> Date: Wed, 5 Jun 2024 12:28:33 +0300 Subject: [PATCH] Python: Adds Sort command (#1439) * Adds sort command to python Co-authored-by: Yury-Fridlyand Co-authored-by: Shoham Elias Co-authored-by: Aaron Co-authored-by: ikolomi --- CHANGELOG.md | 1 + glide-core/src/protobuf/redis_request.proto | 1 + glide-core/src/request_type.rs | 3 + python/python/glide/__init__.py | 3 +- .../glide/async_commands/cluster_commands.py | 87 +++++++++- .../glide/async_commands/command_args.py | 45 +++++ python/python/glide/async_commands/core.py | 34 ++++ .../python/glide/async_commands/sorted_set.py | 19 +-- .../async_commands/standalone_commands.py | 125 +++++++++++++- .../glide/async_commands/transaction.py | 158 +++++++++++++++++- python/python/tests/test_async_client.py | 142 ++++++++++++++++ python/python/tests/test_transaction.py | 45 ++++- 12 files changed, 640 insertions(+), 23 deletions(-) create mode 100644 python/python/glide/async_commands/command_args.py diff --git a/CHANGELOG.md b/CHANGELOG.md index c4d6749293..3c0c9fdeb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ * Python: Added GETDEL command ([#1514](https://github.com/aws/glide-for-redis/pull/1514)) * Python: Added ZINTER, ZUNION commands ([#1478](https://github.com/aws/glide-for-redis/pull/1478)) * Python: Added SINTERCARD command ([#1511](https://github.com/aws/glide-for-redis/pull/1511)) +* Python: Added SORT command ([#1439](https://github.com/aws/glide-for-redis/pull/1439)) ### Breaking Changes * Node: Update XREAD to return a Map of Map ([#1494](https://github.com/aws/glide-for-redis/pull/1494)) diff --git a/glide-core/src/protobuf/redis_request.proto b/glide-core/src/protobuf/redis_request.proto index a5acd8e2d2..a54b13ef41 100644 --- a/glide-core/src/protobuf/redis_request.proto +++ b/glide-core/src/protobuf/redis_request.proto @@ -196,6 +196,7 @@ enum RequestType { PExpireTime = 157; BLMPop = 158; XLen = 159; + Sort = 160; LSet = 165; XDel = 166; XRange = 167; diff --git a/glide-core/src/request_type.rs b/glide-core/src/request_type.rs index ed86f72d51..d069336ede 100644 --- a/glide-core/src/request_type.rs +++ b/glide-core/src/request_type.rs @@ -166,6 +166,7 @@ pub enum RequestType { PExpireTime = 157, BLMPop = 158, XLen = 159, + Sort = 160, LSet = 165, XDel = 166, XRange = 167, @@ -357,6 +358,7 @@ impl From<::protobuf::EnumOrUnknown> for RequestType { ProtobufRequestType::BitFieldReadOnly => RequestType::BitFieldReadOnly, ProtobufRequestType::Move => RequestType::Move, ProtobufRequestType::SInterCard => RequestType::SInterCard, + ProtobufRequestType::Sort => RequestType::Sort, } } } @@ -533,6 +535,7 @@ impl RequestType { RequestType::BitFieldReadOnly => Some(cmd("BITFIELD_RO")), RequestType::Move => Some(cmd("MOVE")), RequestType::SInterCard => Some(cmd("SINTERCARD")), + RequestType::Sort => Some(cmd("SORT")), } } } diff --git a/python/python/glide/__init__.py b/python/python/glide/__init__.py index 8928249303..ebd86eca1c 100644 --- a/python/python/glide/__init__.py +++ b/python/python/glide/__init__.py @@ -1,5 +1,6 @@ # Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 +from glide.async_commands.command_args import Limit, OrderBy from glide.async_commands.core import ( ConditionalChange, ExpireOptions, @@ -20,7 +21,6 @@ AggregationType, InfBound, LexBoundary, - Limit, RangeByIndex, RangeByLex, RangeByScore, @@ -102,6 +102,7 @@ "RangeByLex", "RangeByScore", "ScoreFilter", + "OrderBy", "StreamAddOptions", "StreamTrimOptions", "TrimByMaxLen", diff --git a/python/python/glide/async_commands/cluster_commands.py b/python/python/glide/async_commands/cluster_commands.py index 872b4d12a4..e010f1f54b 100644 --- a/python/python/glide/async_commands/cluster_commands.py +++ b/python/python/glide/async_commands/cluster_commands.py @@ -4,7 +4,8 @@ from typing import Dict, List, Mapping, Optional, cast -from glide.async_commands.core import CoreCommands, InfoSection +from glide.async_commands.command_args import Limit, OrderBy +from glide.async_commands.core import CoreCommands, InfoSection, _build_sort_args from glide.async_commands.transaction import BaseTransaction, ClusterTransaction from glide.constants import TOK, TClusterResponse, TResult, TSingleNodeRoute from glide.protobuf.redis_request_pb2 import RequestType @@ -367,3 +368,87 @@ async def lastsave(self, route: Optional[Route] = None) -> TClusterResponse[int] TClusterResponse[int], await self._execute_command(RequestType.LastSave, [], route), ) + + async def sort( + self, + key: str, + limit: Optional[Limit] = None, + order: Optional[OrderBy] = None, + alpha: Optional[bool] = None, + ) -> List[str]: + """ + Sorts the elements in the list, set, or sorted set at `key` and returns the result. + To store the result into a new key, see `sort_store`. + + By default, sorting is numeric, and elements are compared by their value interpreted as double precision floating point numbers. + + See https://valkey.io/commands/sort for more details. + + Args: + key (str): The key of the list, set, or sorted set to be sorted. + limit (Optional[Limit]): Limiting the range of the query by setting offset and result count. See `Limit` class for more information. + order (Optional[OrderBy]): Specifies the order to sort the elements. + Can be `OrderBy.ASC` (ascending) or `OrderBy.DESC` (descending). + alpha (Optional[bool]): When `True`, sorts elements lexicographically. When `False` (default), sorts elements numerically. + Use this when the list, set, or sorted set contains string values that cannot be converted into double precision floating point numbers. + + Returns: + List[str]: A list of sorted elements. + + Examples: + >>> await client.lpush("mylist", '3', '1', '2') + >>> await client.sort("mylist") + ['1', '2', '3'] + + >>> await client.sort("mylist", order=OrderBy.DESC) + ['3', '2', '1'] + + >>> await client.lpush("mylist", '2', '1', '2', '3', '3', '1') + >>> await client.sort("mylist", limit=Limit(2, 3)) + ['1', '2', '2'] + + >>> await client.lpush("mylist", "a", "b", "c", "d") + >>> await client.sort("mylist", limit=Limit(2, 2), order=OrderBy.DESC, alpha=True) + ['b', 'a'] + """ + args = _build_sort_args(key, None, limit, None, order, alpha) + result = await self._execute_command(RequestType.Sort, args) + return cast(List[str], result) + + async def sort_store( + self, + key: str, + destination: str, + limit: Optional[Limit] = None, + order: Optional[OrderBy] = None, + alpha: Optional[bool] = None, + ) -> int: + """ + Sorts the elements in the list, set, or sorted set at `key` and stores the result in `store`. + When in cluster mode, `key` and `store` must map to the same hash slot. + To get the sort result without storing it into a key, see `sort`. + + See https://valkey.io/commands/sort for more details. + + Args: + key (str): The key of the list, set, or sorted set to be sorted. + destination (str): The key where the sorted result will be stored. + limit (Optional[Limit]): Limiting the range of the query by setting offset and result count. See `Limit` class for more information. + order (Optional[OrderBy]): Specifies the order to sort the elements. + Can be `OrderBy.ASC` (ascending) or `OrderBy.DESC` (descending). + alpha (Optional[bool]): When `True`, sorts elements lexicographically. When `False` (default), sorts elements numerically. + Use this when the list, set, or sorted set contains string values that cannot be converted into double precision floating point numbers. + + Returns: + int: The number of elements in the sorted key stored at `store`. + + Examples: + >>> await client.lpush("mylist", 3, 1, 2) + >>> await client.sort_store("mylist", "sorted_list") + 3 # Indicates that the sorted list "sorted_list" contains three elements. + >>> await client.lrange("sorted_list", 0, -1) + ['1', '2', '3'] + """ + args = _build_sort_args(key, None, limit, None, order, alpha, store=destination) + result = await self._execute_command(RequestType.Sort, args) + return cast(int, result) diff --git a/python/python/glide/async_commands/command_args.py b/python/python/glide/async_commands/command_args.py new file mode 100644 index 0000000000..d308ca9ed7 --- /dev/null +++ b/python/python/glide/async_commands/command_args.py @@ -0,0 +1,45 @@ +# Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + +from enum import Enum +from typing import List, Optional, Union + + +class Limit: + """ + Represents a limit argument for range queries in various Redis commands. + + The `LIMIT` argument is commonly used to specify a subset of results from the matching elements, + similar to the `LIMIT` clause in SQL (e.g., `SELECT LIMIT offset, count`). + + This class can be utilized in multiple Redis commands that support limit options, + such as [ZRANGE](https://valkey.io/commands/zrange), [SORT](https://valkey.io/commands/sort/), and others. + + Args: + offset (int): The starting position of the range, zero based. + count (int): The maximum number of elements to include in the range. + A negative count returns all elements from the offset. + + Examples: + >>> limit = Limit(0, 10) # Fetch the first 10 elements + >>> limit = Limit(5, -1) # Fetch all elements starting from the 5th element + """ + + def __init__(self, offset: int, count: int): + self.offset = offset + self.count = count + + +class OrderBy(Enum): + """ + SORT order options: options for sorting elements. + """ + + ASC = "ASC" + """ + ASC: Sort in ascending order. + """ + + DESC = "DESC" + """ + DESC: Sort in descending order. + """ diff --git a/python/python/glide/async_commands/core.py b/python/python/glide/async_commands/core.py index 5ea9aa1a6d..57d41fbb17 100644 --- a/python/python/glide/async_commands/core.py +++ b/python/python/glide/async_commands/core.py @@ -16,6 +16,7 @@ get_args, ) +from glide.async_commands.command_args import Limit, OrderBy from glide.async_commands.sorted_set import ( AggregationType, InfBound, @@ -361,6 +362,39 @@ class InsertPosition(Enum): AFTER = "AFTER" +def _build_sort_args( + key: str, + by_pattern: Optional[str] = None, + limit: Optional[Limit] = None, + get_patterns: Optional[List[str]] = None, + order: Optional[OrderBy] = None, + alpha: Optional[bool] = None, + store: Optional[str] = None, +) -> List[str]: + args = [key] + + if by_pattern: + args.extend(["BY", by_pattern]) + + if limit: + args.extend(["LIMIT", str(limit.offset), str(limit.count)]) + + if get_patterns: + for pattern in get_patterns: + args.extend(["GET", pattern]) + + if order: + args.append(order.value) + + if alpha: + args.append("ALPHA") + + if store: + args.extend(["STORE", store]) + + return args + + class CoreCommands(Protocol): async def _execute_command( self, diff --git a/python/python/glide/async_commands/sorted_set.py b/python/python/glide/async_commands/sorted_set.py index 7ac92f1e99..5e99da8631 100644 --- a/python/python/glide/async_commands/sorted_set.py +++ b/python/python/glide/async_commands/sorted_set.py @@ -3,6 +3,8 @@ from enum import Enum from typing import List, Optional, Tuple, Union +from glide.async_commands.command_args import Limit + class InfBound(Enum): """ @@ -88,23 +90,6 @@ def __init__(self, value: str, is_inclusive: bool = True): 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. diff --git a/python/python/glide/async_commands/standalone_commands.py b/python/python/glide/async_commands/standalone_commands.py index 83a99a2b8a..2b21fc9f1c 100644 --- a/python/python/glide/async_commands/standalone_commands.py +++ b/python/python/glide/async_commands/standalone_commands.py @@ -4,7 +4,8 @@ from typing import Dict, List, Mapping, Optional, cast -from glide.async_commands.core import CoreCommands, InfoSection +from glide.async_commands.command_args import Limit, OrderBy +from glide.async_commands.core import CoreCommands, InfoSection, _build_sort_args from glide.async_commands.transaction import BaseTransaction, Transaction from glide.constants import TOK, TResult from glide.protobuf.redis_request_pb2 import RequestType @@ -264,3 +265,125 @@ async def lastsave(self) -> int: int, await self._execute_command(RequestType.LastSave, []), ) + + async def sort( + self, + key: str, + by_pattern: Optional[str] = None, + limit: Optional[Limit] = None, + get_patterns: Optional[List[str]] = None, + order: Optional[OrderBy] = None, + alpha: Optional[bool] = None, + ) -> List[Optional[str]]: + """ + Sorts the elements in the list, set, or sorted set at `key` and returns the result. + The `sort` command can be used to sort elements based on different criteria and apply transformations on sorted elements. + To store the result into a new key, see `sort_store`. + + See https://valkey.io/commands/sort for more details. + + Args: + key (str): The key of the list, set, or sorted set to be sorted. + by_pattern (Optional[str]): A pattern to sort by external keys instead of by the elements stored at the key themselves. + The pattern should contain an asterisk (*) as a placeholder for the element values, where the value + from the key replaces the asterisk to create the key name. For example, if `key` contains IDs of objects, + `by_pattern` can be used to sort these IDs based on an attribute of the objects, like their weights or + timestamps. + E.g., if `by_pattern` is `weight_*`, the command will sort the elements by the values of the + keys `weight_`. + If not provided, elements are sorted by their value. + limit (Optional[Limit]): Limiting the range of the query by setting offset and result count. See `Limit` class for more information. + get_pattern (Optional[str]): A pattern used to retrieve external keys' values, instead of the elements at `key`. + The pattern should contain an asterisk (*) as a placeholder for the element values, where the value + from `key` replaces the asterisk to create the key name. This allows the sorted elements to be + transformed based on the related keys values. For example, if `key` contains IDs of users, `get_pattern` + can be used to retrieve specific attributes of these users, such as their names or email addresses. + E.g., if `get_pattern` is `name_*`, the command will return the values of the keys `name_` + for each sorted element. Multiple `get_pattern` arguments can be provided to retrieve multiple attributes. + The special value `#` can be used to include the actual element from `key` being sorted. + If not provided, only the sorted elements themselves are returned. + order (Optional[OrderBy]): Specifies the order to sort the elements. + Can be `OrderBy.ASC` (ascending) or `OrderBy.DESC` (descending). + alpha (Optional[bool]): When `True`, sorts elements lexicographically. When `False` (default), sorts elements numerically. + Use this when the list, set, or sorted set contains string values that cannot be converted into double precision floating point + + Returns: + List[Optional[str]]: Returns a list of sorted elements. + + Examples: + >>> await client.lpush("mylist", 3, 1, 2) + >>> await client.sort("mylist") + ['1', '2', '3'] + >>> await client.sort("mylist", order=OrderBy.DESC) + ['3', '2', '1'] + >>> await client.lpush("mylist2", 2, 1, 2, 3, 3, 1) + >>> await client.sort("mylist2", limit=Limit(2, 3)) + ['2', '2', '3'] + >>> await client.hset("user:1", "name", "Alice", "age", 30) + >>> await client.hset("user:2", "name", "Bob", "age", 25) + >>> await client.lpush("user_ids", 2, 1) + >>> await client.sort("user_ids", by_pattern="user:*->age", get_patterns=["user:*->name"]) + ['Bob', 'Alice'] + """ + args = _build_sort_args(key, by_pattern, limit, get_patterns, order, alpha) + result = await self._execute_command(RequestType.Sort, args) + return cast(List[Optional[str]], result) + + async def sort_store( + self, + key: str, + destination: str, + by_pattern: Optional[str] = None, + limit: Optional[Limit] = None, + get_patterns: Optional[List[str]] = None, + order: Optional[OrderBy] = None, + alpha: Optional[bool] = None, + ) -> int: + """ + Sorts the elements in the list, set, or sorted set at `key` and stores the result in `store`. + The `sort` command can be used to sort elements based on different criteria, apply transformations on sorted elements, and store the result in a new key. + To get the sort result without storing it into a key, see `sort`. + + See https://valkey.io/commands/sort for more details. + + Args: + key (str): The key of the list, set, or sorted set to be sorted. + destination (str): The key where the sorted result will be stored. + by_pattern (Optional[str]): A pattern to sort by external keys instead of by the elements stored at the key themselves. + The pattern should contain an asterisk (*) as a placeholder for the element values, where the value + from the key replaces the asterisk to create the key name. For example, if `key` contains IDs of objects, + `by_pattern` can be used to sort these IDs based on an attribute of the objects, like their weights or + timestamps. + E.g., if `by_pattern` is `weight_*`, the command will sort the elements by the values of the + keys `weight_`. + If not provided, elements are sorted by their value. + limit (Optional[Limit]): Limiting the range of the query by setting offset and result count. See `Limit` class for more information. + get_pattern (Optional[str]): A pattern used to retrieve external keys' values, instead of the elements at `key`. + The pattern should contain an asterisk (*) as a placeholder for the element values, where the value + from `key` replaces the asterisk to create the key name. This allows the sorted elements to be + transformed based on the related keys values. For example, if `key` contains IDs of users, `get_pattern` + can be used to retrieve specific attributes of these users, such as their names or email addresses. + E.g., if `get_pattern` is `name_*`, the command will return the values of the keys `name_` + for each sorted element. Multiple `get_pattern` arguments can be provided to retrieve multiple attributes. + The special value `#` can be used to include the actual element from `key` being sorted. + If not provided, only the sorted elements themselves are returned. + order (Optional[OrderBy]): Specifies the order to sort the elements. + Can be `OrderBy.ASC` (ascending) or `OrderBy.DESC` (descending). + alpha (Optional[bool]): When `True`, sorts elements lexicographically. When `False` (default), sorts elements numerically. + Use this when the list, set, or sorted set contains string values that cannot be converted into double precision floating point + + Returns: + int: The number of elements in the sorted key stored at `store`. + + Examples: + >>> await client.lpush("mylist", 3, 1, 2) + >>> await client.sort_store("mylist", "sorted_list") + 3 # Indicates that the sorted list "sorted_list" contains three elements. + >>> await client.lrange("sorted_list", 0, -1) + ['1', '2', '3'] + """ + args = _build_sort_args( + key, by_pattern, limit, get_patterns, order, alpha, store=destination + ) + result = await self._execute_command(RequestType.Sort, args) + return cast(int, result) diff --git a/python/python/glide/async_commands/transaction.py b/python/python/glide/async_commands/transaction.py index c5d4e3949e..c33497e7ed 100644 --- a/python/python/glide/async_commands/transaction.py +++ b/python/python/glide/async_commands/transaction.py @@ -3,6 +3,7 @@ import threading from typing import List, Mapping, Optional, Tuple, TypeVar, Union +from glide.async_commands.command_args import Limit, OrderBy from glide.async_commands.core import ( ConditionalChange, ExpireOptions, @@ -14,6 +15,7 @@ StreamAddOptions, StreamTrimOptions, UpdateOptions, + _build_sort_args, ) from glide.async_commands.sorted_set import ( AggregationType, @@ -2745,6 +2747,104 @@ def select(self, index: int) -> "Transaction": """ return self.append_command(RequestType.Select, [str(index)]) + def sort( + self: TTransaction, + key: str, + by_pattern: Optional[str] = None, + limit: Optional[Limit] = None, + get_patterns: Optional[List[str]] = None, + order: Optional[OrderBy] = None, + alpha: Optional[bool] = None, + ) -> TTransaction: + """ + Sorts the elements in the list, set, or sorted set at `key` and returns the result. + The `sort` command can be used to sort elements based on different criteria and apply transformations on sorted elements. + To store the result into a new key, see `sort_store`. + + See https://valkey.io/commands/sort for more details. + + Args: + key (str): The key of the list, set, or sorted set to be sorted. + by_pattern (Optional[str]): A pattern to sort by external keys instead of by the elements stored at the key themselves. + The pattern should contain an asterisk (*) as a placeholder for the element values, where the value + from the key replaces the asterisk to create the key name. For example, if `key` contains IDs of objects, + `by_pattern` can be used to sort these IDs based on an attribute of the objects, like their weights or + timestamps. + E.g., if `by_pattern` is `weight_*`, the command will sort the elements by the values of the + keys `weight_`. + If not provided, elements are sorted by their value. + limit (Optional[Limit]): Limiting the range of the query by setting offset and result count. See `Limit` class for more information. + get_pattern (Optional[str]): A pattern used to retrieve external keys' values, instead of the elements at `key`. + The pattern should contain an asterisk (*) as a placeholder for the element values, where the value + from `key` replaces the asterisk to create the key name. This allows the sorted elements to be + transformed based on the related keys values. For example, if `key` contains IDs of users, `get_pattern` + can be used to retrieve specific attributes of these users, such as their names or email addresses. + E.g., if `get_pattern` is `name_*`, the command will return the values of the keys `name_` + for each sorted element. Multiple `get_pattern` arguments can be provided to retrieve multiple attributes. + The special value `#` can be used to include the actual element from `key` being sorted. + If not provided, only the sorted elements themselves are returned. + order (Optional[OrderBy]): Specifies the order to sort the elements. + Can be `OrderBy.ASC` (ascending) or `OrderBy.DESC` (descending). + alpha (Optional[bool]): When `True`, sorts elements lexicographically. When `False` (default), sorts elements numerically. + Use this when the list, set, or sorted set contains string values that cannot be converted into double precision floating point numbers. + + Command response: + List[Optional[str]]: Returns a list of sorted elements. + """ + args = _build_sort_args(key, by_pattern, limit, get_patterns, order, alpha) + return self.append_command(RequestType.Sort, args) + + def sort_store( + self: TTransaction, + key: str, + destination: str, + by_pattern: Optional[str] = None, + limit: Optional[Limit] = None, + get_patterns: Optional[List[str]] = None, + order: Optional[OrderBy] = None, + alpha: Optional[bool] = None, + ) -> TTransaction: + """ + Sorts the elements in the list, set, or sorted set at `key` and stores the result in `store`. + The `sort` command can be used to sort elements based on different criteria, apply transformations on sorted elements, and store the result in a new key. + To get the sort result without storing it into a key, see `sort`. + + See https://valkey.io/commands/sort for more details. + + Args: + key (str): The key of the list, set, or sorted set to be sorted. + destination (str): The key where the sorted result will be stored. + by_pattern (Optional[str]): A pattern to sort by external keys instead of by the elements stored at the key themselves. + The pattern should contain an asterisk (*) as a placeholder for the element values, where the value + from the key replaces the asterisk to create the key name. For example, if `key` contains IDs of objects, + `by_pattern` can be used to sort these IDs based on an attribute of the objects, like their weights or + timestamps. + E.g., if `by_pattern` is `weight_*`, the command will sort the elements by the values of the + keys `weight_`. + If not provided, elements are sorted by their value. + limit (Optional[Limit]): Limiting the range of the query by setting offset and result count. See `Limit` class for more information. + get_pattern (Optional[str]): A pattern used to retrieve external keys' values, instead of the elements at `key`. + The pattern should contain an asterisk (*) as a placeholder for the element values, where the value + from `key` replaces the asterisk to create the key name. This allows the sorted elements to be + transformed based on the related keys values. For example, if `key` contains IDs of users, `get_pattern` + can be used to retrieve specific attributes of these users, such as their names or email addresses. + E.g., if `get_pattern` is `name_*`, the command will return the values of the keys `name_` + for each sorted element. Multiple `get_pattern` arguments can be provided to retrieve multiple attributes. + The special value `#` can be used to include the actual element from `key` being sorted. + If not provided, only the sorted elements themselves are returned. + order (Optional[OrderBy]): Specifies the order to sort the elements. + Can be `OrderBy.ASC` (ascending) or `OrderBy.DESC` (descending). + alpha (Optional[bool]): When `True`, sorts elements lexicographically. When `False` (default), sorts elements numerically. + Use this when the list, set, or sorted set contains string values that cannot be converted into double precision floating point numbers. + + Command response: + int: The number of elements in the sorted key stored at `store`. + """ + args = _build_sort_args( + key, by_pattern, limit, get_patterns, order, alpha, store=destination + ) + return self.append_command(RequestType.Sort, args) + class ClusterTransaction(BaseTransaction): """ @@ -2755,5 +2855,61 @@ class ClusterTransaction(BaseTransaction): are documented alongside each method. """ + def sort( + self: TTransaction, + key: str, + limit: Optional[Limit] = None, + order: Optional[OrderBy] = None, + alpha: Optional[bool] = None, + ) -> TTransaction: + """ + Sorts the elements in the list, set, or sorted set at `key` and returns the result. + To store the result into a new key, see `sort_store`. + + See https://valkey.io/commands/sort for more details. + + Args: + key (str): The key of the list, set, or sorted set to be sorted. + limit (Optional[Limit]): Limiting the range of the query by setting offset and result count. See `Limit` class for more information. + order (Optional[OrderBy]): Specifies the order to sort the elements. + Can be `OrderBy.ASC` (ascending) or `OrderBy.DESC` (descending). + alpha (Optional[bool]): When `True`, sorts elements lexicographically. When `False` (default), sorts elements numerically. + Use this when the list, set, or sorted set contains string values that cannot be converted into double precision floating point numbers. + + Command response: + List[str]: A list of sorted elements. + """ + args = _build_sort_args(key, None, limit, None, order, alpha) + return self.append_command(RequestType.Sort, args) + + def sort_store( + self: TTransaction, + key: str, + destination: str, + limit: Optional[Limit] = None, + order: Optional[OrderBy] = None, + alpha: Optional[bool] = None, + ) -> TTransaction: + """ + Sorts the elements in the list, set, or sorted set at `key` and stores the result in `store`. + When in cluster mode, `key` and `store` must map to the same hash slot. + To get the sort result without storing it into a key, see `sort`. + + See https://valkey.io/commands/sort for more details. + + Args: + key (str): The key of the list, set, or sorted set to be sorted. + destination (str): The key where the sorted result will be stored. + limit (Optional[Limit]): Limiting the range of the query by setting offset and result count. See `Limit` class for more information. + order (Optional[OrderBy]): Specifies the order to sort the elements. + Can be `OrderBy.ASC` (ascending) or `OrderBy.DESC` (descending). + alpha (Optional[bool]): When `True`, sorts elements lexicographically. When `False` (default), sorts elements numerically. + Use this when the list, set, or sorted set contains string values that cannot be converted into double precision floating point numbers. + + Command response: + int: The number of elements in the sorted key stored at `store`. + """ + args = _build_sort_args(key, None, limit, None, order, alpha, store=destination) + return self.append_command(RequestType.Sort, args) + # TODO: add all CLUSTER commands - pass diff --git a/python/python/tests/test_async_client.py b/python/python/tests/test_async_client.py index bd1a51366a..48abd147fa 100644 --- a/python/python/tests/test_async_client.py +++ b/python/python/tests/test_async_client.py @@ -11,6 +11,7 @@ import pytest from glide import ClosingError, RequestError, Script +from glide.async_commands.command_args import Limit, OrderBy from glide.async_commands.core import ( ConditionalChange, ExpireOptions, @@ -3416,6 +3417,146 @@ async def test_type(self, redis_client: TRedisClient): assert (await redis_client.type(key)).lower() == "none" + @pytest.mark.parametrize("cluster_mode", [False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_sort_and_sort_store_with_get_or_by_args( + self, redis_client: RedisClient + ): + key = "{SameSlotKey}" + get_random_string(10) + store = "{SameSlotKey}" + get_random_string(10) + user_key1, user_key2, user_key3, user_key4, user_key5 = ( + "user:1", + "user:2", + "user:3", + "user:4", + "user:5", + ) + + # Prepare some data + assert await redis_client.hset(user_key1, {"name": "Alice", "age": "30"}) == 2 + assert await redis_client.hset(user_key2, {"name": "Bob", "age": "25"}) == 2 + assert await redis_client.hset(user_key3, {"name": "Charlie", "age": "35"}) == 2 + assert await redis_client.hset(user_key4, {"name": "Dave", "age": "20"}) == 2 + assert await redis_client.hset(user_key5, {"name": "Eve", "age": "40"}) == 2 + assert await redis_client.lpush("user_ids", ["5", "4", "3", "2", "1"]) == 5 + + # Test sort with all arguments + assert await redis_client.lpush(key, ["3", "1", "2"]) == 3 + result = await redis_client.sort( + key, + limit=Limit(0, 2), + get_patterns=["user:*->name"], + order=OrderBy.ASC, + alpha=True, + ) + assert result == ["Alice", "Bob"] + + # Test sort_store with all arguments + sort_store_result = await redis_client.sort_store( + key, + store, + limit=Limit(0, 2), + get_patterns=["user:*->name"], + order=OrderBy.ASC, + alpha=True, + ) + assert sort_store_result == 2 + sorted_list = await redis_client.lrange(store, 0, -1) + assert sorted_list == ["Alice", "Bob"] + + # Test sort with `by` argument + result = await redis_client.sort( + "user_ids", + by_pattern="user:*->age", + get_patterns=["user:*->name"], + alpha=True, + ) + assert result == ["Dave", "Bob", "Alice", "Charlie", "Eve"] + + # Test sort with `by` argument with missing keys to sort by + assert await redis_client.lpush("user_ids", ["a"]) == 6 + result = await redis_client.sort( + "user_ids", + by_pattern="user:*->age", + get_patterns=["user:*->name"], + alpha=True, + ) + assert result == [None, "Dave", "Bob", "Alice", "Charlie", "Eve"] + + # Test sort with `by` argument with missing keys to sort by + result = await redis_client.sort( + "user_ids", + by_pattern="user:*->name", + get_patterns=["user:*->age"], + alpha=True, + ) + assert result == [None, "30", "25", "35", "20", "40"] + + # Test Limit with count 0 + result = await redis_client.sort( + "user_ids", + limit=Limit(0, 0), + alpha=True, + ) + assert result == [] + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_sort_and_sort_store_without_get_or_by_args( + self, redis_client: TRedisClient + ): + key = "{SameSlotKey}" + get_random_string(10) + store = "{SameSlotKey}" + get_random_string(10) + + # Test sort with non-existing key + result = await redis_client.sort("non_existing_key") + assert result == [] + + # Test sort_store with non-existing key + sort_store_result = await redis_client.sort_store( + "{SameSlotKey}:non_existing_key", store + ) + assert sort_store_result == 0 + + # Test each argument separately + assert await redis_client.lpush(key, ["5", "2", "4", "1", "3"]) == 5 + + # Test w/o flags + result = await redis_client.sort(key) + assert result == ["1", "2", "3", "4", "5"] + + # limit argument + result = await redis_client.sort(key, limit=Limit(1, 3)) + assert result == ["2", "3", "4"] + + # order argument + result = await redis_client.sort(key, order=OrderBy.DESC) + assert result == ["5", "4", "3", "2", "1"] + + assert await redis_client.lpush(key, ["a"]) == 6 + + with pytest.raises(RequestError) as e: + await redis_client.sort(key) + assert "can't be converted into double" in str(e).lower() + + # alpha argument + result = await redis_client.sort(key, alpha=True) + assert result == ["1", "2", "3", "4", "5", "a"] + + # Combining multiple arguments + result = await redis_client.sort( + key, limit=Limit(1, 3), order=OrderBy.DESC, alpha=True + ) + assert result == ["5", "4", "3"] + + # Test sort_store with combined arguments + sort_store_result = await redis_client.sort_store( + key, store, limit=Limit(1, 3), order=OrderBy.DESC, alpha=True + ) + assert sort_store_result == 3 + sorted_list = await redis_client.lrange(store, 0, -1) + assert sorted_list == ["5", "4", "3"] + @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) async def test_echo(self, redis_client: TRedisClient): @@ -3791,6 +3932,7 @@ async def test_multi_key_command_returns_cross_slot_error( redis_client.zinter_withscores(["def", "ghi"]), redis_client.zunion(["def", "ghi"]), redis_client.zunion_withscores(["def", "ghi"]), + redis_client.sort_store("abc", "zxy"), ] if not await check_if_server_version_lt(redis_client, "7.0.0"): diff --git a/python/python/tests/test_transaction.py b/python/python/tests/test_transaction.py index 3891c97b28..3ce0a3acf5 100644 --- a/python/python/tests/test_transaction.py +++ b/python/python/tests/test_transaction.py @@ -6,6 +6,7 @@ import pytest from glide import RequestError +from glide.async_commands.command_args import Limit, OrderBy from glide.async_commands.core import ( GeospatialData, InsertPosition, @@ -53,6 +54,8 @@ async def transaction_test( key14 = "{{{}}}:{}".format(keyslot, get_random_string(3)) # sorted set key15 = "{{{}}}:{}".format(keyslot, get_random_string(3)) # sorted set key16 = "{{{}}}:{}".format(keyslot, get_random_string(3)) # sorted set + key17 = "{{{}}}:{}".format(keyslot, get_random_string(3)) # sort + key18 = "{{{}}}:{}".format(keyslot, get_random_string(3)) # sort value = datetime.now(timezone.utc).strftime("%m/%d/%Y, %H:%M:%S") value2 = get_random_string(5) @@ -362,6 +365,24 @@ async def transaction_test( transaction.xtrim(key11, TrimByMinId(threshold="0-2", exact=True)) args.append(1) + transaction.lpush(key17, ["2", "1", "4", "3", "a"]) + args.append(5) + transaction.sort( + key17, + limit=Limit(1, 4), + order=OrderBy.ASC, + alpha=True, + ) + args.append(["2", "3", "4", "a"]) + transaction.sort_store( + key17, + key18, + limit=Limit(1, 4), + order=OrderBy.ASC, + alpha=True, + ) + args.append(4) + min_version = "7.0.0" if not await check_if_server_version_lt(redis_client, min_version): transaction.zadd(key16, {"a": 1, "b": 2, "c": 3, "d": 4}) @@ -496,12 +517,31 @@ async def test_standalone_transaction(self, redis_client: RedisClient): assert await redis_client.custom_command(["FLUSHALL"]) == OK keyslot = get_random_string(3) key = "{{{}}}:{}".format(keyslot, get_random_string(3)) # to get the same slot + key1 = "{{{}}}:{}".format(keyslot, get_random_string(3)) # to get the same slot value = get_random_string(5) transaction = Transaction() transaction.info() transaction.select(1) transaction.set(key, value) transaction.get(key) + transaction.hset("user:1", {"name": "Alice", "age": "30"}) + transaction.hset("user:2", {"name": "Bob", "age": "25"}) + transaction.lpush(key1, ["2", "1"]) + transaction.sort( + key1, + by_pattern="user:*->age", + get_patterns=["user:*->name"], + order=OrderBy.ASC, + alpha=True, + ) + transaction.sort_store( + key1, + "newSortedKey", + by_pattern="user:*->age", + get_patterns=["user:*->name"], + order=OrderBy.ASC, + alpha=True, + ) transaction.select(0) transaction.get(key) expected = await transaction_test(transaction, keyslot, redis_client) @@ -509,8 +549,9 @@ async def test_standalone_transaction(self, redis_client: RedisClient): assert isinstance(result, list) assert isinstance(result[0], str) assert "# Memory" in result[0] - assert result[1:6] == [OK, OK, value, OK, None] - assert result[6:] == expected + assert result[1:4] == [OK, OK, value] + assert result[4:11] == [2, 2, 2, ["Bob", "Alice"], 2, OK, None] + assert result[11:] == expected def test_transaction_clear(self): transaction = Transaction()