diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d6001c78b..0b9e7c14f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ * Python: Added BZMPOP command ([#1412](https://github.com/aws/glide-for-redis/pull/1412)) * Python: Added ZINTERCARD command ([#1418](https://github.com/aws/glide-for-redis/pull/1418)) * Python: Added ZMPOP command ([#1417](https://github.com/aws/glide-for-redis/pull/1417)) +* Python: Added SMOVE command ([#1421](https://github.com/aws/glide-for-redis/pull/1421)) #### Fixes diff --git a/python/python/glide/async_commands/core.py b/python/python/glide/async_commands/core.py index de8483c98e..78410ed4cf 100644 --- a/python/python/glide/async_commands/core.py +++ b/python/python/glide/async_commands/core.py @@ -1536,6 +1536,40 @@ async def sismember( await self._execute_command(RequestType.SIsMember, [key, member]), ) + async def smove( + self, + source: str, + destination: str, + member: str, + ) -> bool: + """ + Moves `member` from the set at `source` to the set at `destination`, removing it from the source set. Creates a + new destination set if needed. The operation is atomic. + + See https://valkey.io/commands/smove for more details. + + Note: + When in cluster mode, `source` and `destination` must map to the same hash slot. + + Args: + source (str): The key of the set to remove the element from. + destination (str): The key of the set to add the element to. + member (str): The set element to move. + + Returns: + bool: True on success, or False if the `source` set does not exist or the element is not a member of the source set. + + Examples: + >>> await client.smove("set1", "set2", "member1") + True # "member1" was moved from "set1" to "set2". + """ + return cast( + bool, + await self._execute_command( + RequestType.SMove, [source, destination, member] + ), + ) + async def ltrim(self, key: str, start: int, end: int) -> TOK: """ Trim an existing list so that it will contain only the specified range of elements specified. diff --git a/python/python/glide/async_commands/transaction.py b/python/python/glide/async_commands/transaction.py index 754dcf8851..d9e44f10c9 100644 --- a/python/python/glide/async_commands/transaction.py +++ b/python/python/glide/async_commands/transaction.py @@ -1011,6 +1011,28 @@ def sismember( """ return self.append_command(RequestType.SIsMember, [key, member]) + def smove( + self: TTransaction, + source: str, + destination: str, + member: str, + ) -> TTransaction: + """ + Moves `member` from the set at `source` to the set at `destination`, removing it from the source set. Creates a + new destination set if needed. The operation is atomic. + + See https://valkey.io/commands/smove for more details. + + Args: + source (str): The key of the set to remove the element from. + destination (str): The key of the set to add the element to. + member (str): The set element to move. + + Command response: + bool: True on success, or False if the `source` set does not exist or the element is not a member of the source set. + """ + return self.append_command(RequestType.SMove, [source, destination, member]) + def ltrim(self: TTransaction, key: str, start: int, end: int) -> TTransaction: """ Trim an existing list so that it will contain only the specified range of elements specified. diff --git a/python/python/tests/test_async_client.py b/python/python/tests/test_async_client.py index d5c144f0cd..370f62d97e 100644 --- a/python/python/tests/test_async_client.py +++ b/python/python/tests/test_async_client.py @@ -1096,6 +1096,53 @@ async def test_spop(self, redis_client: TRedisClient): assert await redis_client.spop("non_existing_key") == None assert await redis_client.spop_count("non_existing_key", 3) == set() + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_smove(self, redis_client: TRedisClient): + key1 = f"{{testKey}}:1-{get_random_string(10)}" + key2 = f"{{testKey}}:2-{get_random_string(10)}" + key3 = f"{{testKey}}:3-{get_random_string(10)}" + key4 = f"{{testKey}}:4-{get_random_string(10)}" + string_key = f"{{testKey}}:4-{get_random_string(10)}" + non_existing_key = f"{{testKey}}:5-{get_random_string(10)}" + + assert await redis_client.sadd(key1, ["1", "2", "3"]) == 3 + assert await redis_client.sadd(key2, ["2", "3"]) == 2 + + # move an element + assert await redis_client.smove(key1, key2, "1") is True + assert await redis_client.smembers(key1) == {"2", "3"} + assert await redis_client.smembers(key2) == {"1", "2", "3"} + + # moved element already exists in the destination set + assert await redis_client.smove(key2, key1, "2") is True + assert await redis_client.smembers(key1) == {"2", "3"} + assert await redis_client.smembers(key2) == {"1", "3"} + + # attempt to move from a non-existing key + assert await redis_client.smove(non_existing_key, key1, "4") is False + assert await redis_client.smembers(key1) == {"2", "3"} + + # move to a new set + assert await redis_client.smove(key1, key3, "2") + assert await redis_client.smembers(key1) == {"3"} + assert await redis_client.smembers(key3) == {"2"} + + # attempt to move a missing element + assert await redis_client.smove(key1, key3, "42") is False + assert await redis_client.smembers(key1) == {"3"} + assert await redis_client.smembers(key3) == {"2"} + + # move missing element to missing key + assert await redis_client.smove(key1, non_existing_key, "42") is False + assert await redis_client.smembers(key1) == {"3"} + assert await redis_client.type(non_existing_key) == "none" + + # key exists, but it is not a set + assert await redis_client.set(string_key, "value") == OK + with pytest.raises(RequestError): + await redis_client.smove(string_key, key1, "_") + @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) async def test_ltrim(self, redis_client: TRedisClient): @@ -3033,6 +3080,7 @@ async def test_multi_key_command_returns_cross_slot_error( redis_client.zunionstore("{xyz}", ["{abc}", "{def}"]), redis_client.bzpopmin(["abc", "zxy", "lkn"], 0.5), redis_client.bzpopmax(["abc", "zxy", "lkn"], 0.5), + redis_client.smove("abc", "def", "_"), ] if not 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 c64ea6490a..341450247a 100644 --- a/python/python/tests/test_transaction.py +++ b/python/python/tests/test_transaction.py @@ -205,6 +205,8 @@ async def transaction_test( args.append(2) transaction.spop_count(key7, 4) args.append({"foo", "bar"}) + transaction.smove(key7, key7, "non_existing_member") + args.append(False) transaction.zadd(key8, {"one": 1, "two": 2, "three": 3, "four": 4}) args.append(4)