diff --git a/CHANGELOG.md b/CHANGELOG.md index bd15282fbd..bbf9e3cfd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ * Node: Added XLEN command ([#1555](https://github.com/aws/glide-for-redis/pull/1555)) * Node: Added ZINTERCARD command ([#1553](https://github.com/aws/glide-for-redis/pull/1553)) * Python: Added LMPOP and BLMPOP commands ([#1547](https://github.com/aws/glide-for-redis/pull/1547)) +* Python: Added MSETNX command ([#1565](https://github.com/aws/glide-for-redis/pull/1565)) ### Breaking Changes * Node: Update XREAD to return a Map of Map ([#1494](https://github.com/aws/glide-for-redis/pull/1494)) diff --git a/python/python/glide/async_commands/core.py b/python/python/glide/async_commands/core.py index 8f5bd5f192..cfdd9a9bee 100644 --- a/python/python/glide/async_commands/core.py +++ b/python/python/glide/async_commands/core.py @@ -733,6 +733,36 @@ async def mset(self, key_value_map: Mapping[str, str]) -> TOK: parameters.extend(pair) return cast(TOK, await self._execute_command(RequestType.MSet, parameters)) + async def msetnx(self, key_value_map: Mapping[str, str]) -> bool: + """ + Sets multiple keys to values if the key does not exist. The operation is atomic, and if one or + more keys already exist, the entire operation fails. + + Note: + When in cluster mode, all `keys` must map to the same hash slot. + + See https://valkey.io/commands/msetnx/ for more details. + + Args: + key_value_map (Mapping[str, str]): A key-value map consisting of keys and their respective values to set. + + Returns: + bool: True if all keys were set. False if no key was set. + + Examples: + >>> await client.msetnx({"key1": "value1", "key2": "value2"}) + True + >>> await client.msetnx({"key2": "value4", "key3": "value5"}) + False + """ + parameters: List[str] = [] + for pair in key_value_map.items(): + parameters.extend(pair) + return cast( + bool, + await self._execute_command(RequestType.MSetNX, parameters), + ) + async def mget(self, keys: List[str]) -> List[Optional[str]]: """ Retrieve the values of multiple keys. diff --git a/python/python/glide/async_commands/transaction.py b/python/python/glide/async_commands/transaction.py index 7204bf10c7..939b0c6f9f 100644 --- a/python/python/glide/async_commands/transaction.py +++ b/python/python/glide/async_commands/transaction.py @@ -309,6 +309,24 @@ def mset(self: TTransaction, key_value_map: Mapping[str, str]) -> TTransaction: parameters.extend(pair) return self.append_command(RequestType.MSet, parameters) + def msetnx(self, key_value_map: Mapping[str, str]) -> bool: + """ + Sets multiple keys to values if the key does not exist. The operation is atomic, and if one or + more keys already exist, the entire operation fails. + + See https://valkey.io/commands/msetnx/ for more details. + + Args: + key_value_map (Mapping[str, str]): A key-value map consisting of keys and their respective values to set. + + Commands response: + bool: True if all keys were set. False if no key was set. + """ + parameters: List[str] = [] + for pair in key_value_map.items(): + parameters.extend(pair) + return self.append_command(RequestType.MSetNX, parameters) + def mget(self: TTransaction, keys: List[str]) -> TTransaction: """ Retrieve the values of multiple keys. diff --git a/python/python/tests/test_async_client.py b/python/python/tests/test_async_client.py index 45d7865087..c2143edae9 100644 --- a/python/python/tests/test_async_client.py +++ b/python/python/tests/test_async_client.py @@ -524,6 +524,25 @@ async def test_mset_mget(self, redis_client: TRedisClient): keys[-1] = None assert mget_res == keys + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_msetnx(self, redis_client: TRedisClient): + key1 = f"{{key}}-1{get_random_string(5)}" + key2 = f"{{key}}-2{get_random_string(5)}" + key3 = f"{{key}}-3{get_random_string(5)}" + non_existing = get_random_string(5) + value = get_random_string(5) + key_value_map1 = {key1: value, key2: value} + key_value_map2 = {key2: get_random_string(5), key3: value} + + assert await redis_client.msetnx(key_value_map1) is True + mget_res = await redis_client.mget([key1, key2, non_existing]) + assert mget_res == [value, value, None] + + assert await redis_client.msetnx(key_value_map2) is False + assert await redis_client.get(key3) is None + assert await redis_client.get(key2) == value + @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) async def test_ping(self, redis_client: TRedisClient): @@ -4183,6 +4202,7 @@ async def test_multi_key_command_returns_cross_slot_error( redis_client.blmove( "abc", "zxy", ListDirection.LEFT, ListDirection.LEFT, 1 ), + redis_client.msetnx({"abc": "abc", "zxy": "zyx"}), ] 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 1d753658e3..458844a593 100644 --- a/python/python/tests/test_transaction.py +++ b/python/python/tests/test_transaction.py @@ -103,6 +103,8 @@ async def transaction_test( transaction.mset({key: value, key2: value2}) args.append(OK) + transaction.msetnx({key: value, key2: value2}) + args.append(False) transaction.mget([key, key2]) args.append([value, value2])