From c9fa80dbedbbcc2b43c01852bb1aa19c0820a0f8 Mon Sep 17 00:00:00 2001 From: Shoham Elias Date: Tue, 13 Feb 2024 17:06:18 +0000 Subject: [PATCH] Python: adds HSETNX command --- CHANGELOG.md | 1 + glide-core/src/client/value_conversion.rs | 2 +- glide-core/src/protobuf/redis_request.proto | 1 + glide-core/src/socket_listener.rs | 1 + python/python/glide/async_commands/core.py | 37 +++++++++++++++++-- .../glide/async_commands/transaction.py | 22 +++++++++++ python/python/tests/test_async_client.py | 14 +++++++ python/python/tests/test_transaction.py | 3 +- 8 files changed, 76 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b2a4b4025..7cc795c97f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ #### Changes - 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)) ## 0.2.0 (2024-02-11) diff --git a/glide-core/src/client/value_conversion.rs b/glide-core/src/client/value_conversion.rs index c4864310d0..2aca949d34 100644 --- a/glide-core/src/client/value_conversion.rs +++ b/glide-core/src/client/value_conversion.rs @@ -201,7 +201,7 @@ pub(crate) fn expected_type_for_cmd(cmd: &Cmd) -> Option { Some(ExpectedReturnType::Map) } b"INCRBYFLOAT" | b"HINCRBYFLOAT" => Some(ExpectedReturnType::Double), - b"HEXISTS" | b"EXPIRE" | b"EXPIREAT" | b"PEXPIRE" | b"PEXPIREAT" => { + b"HEXISTS" | b"HSETNX" | b"EXPIRE" | b"EXPIREAT" | b"PEXPIRE" | b"PEXPIREAT" => { Some(ExpectedReturnType::Boolean) } b"SMEMBERS" => Some(ExpectedReturnType::Set), diff --git a/glide-core/src/protobuf/redis_request.proto b/glide-core/src/protobuf/redis_request.proto index 0389ce82d3..7b413654d4 100644 --- a/glide-core/src/protobuf/redis_request.proto +++ b/glide-core/src/protobuf/redis_request.proto @@ -123,6 +123,7 @@ enum RequestType { XTrim = 79; XGroupCreate = 80; XGroupDestroy = 81; + HSetNX = 82; } message Command { diff --git a/glide-core/src/socket_listener.rs b/glide-core/src/socket_listener.rs index d925c80172..de9ce806dc 100644 --- a/glide-core/src/socket_listener.rs +++ b/glide-core/src/socket_listener.rs @@ -360,6 +360,7 @@ fn get_command(request: &Command) -> Option { RequestType::XGroupCreate => Some(get_two_word_command("XGROUP", "CREATE")), RequestType::XGroupDestroy => Some(get_two_word_command("XGROUP", "DESTROY")), RequestType::XTrim => Some(cmd("XTRIM")), + RequestType::HSetNX => Some(cmd("HSETNX")), } } diff --git a/python/python/glide/async_commands/core.py b/python/python/glide/async_commands/core.py index a0d4c71e25..7ddfededa3 100644 --- a/python/python/glide/async_commands/core.py +++ b/python/python/glide/async_commands/core.py @@ -419,7 +419,7 @@ async def hset(self, key: str, field_value_map: Mapping[str, str]) -> int: int: The number of fields that were added to the hash. Example: - >>> hset("my_hash", {"field": "value", "field2": "value2"}) + >>> await client.hset("my_hash", {"field": "value", "field2": "value2"}) 2 """ field_value_list: List[str] = [key] @@ -444,9 +444,9 @@ async def hget(self, key: str, field: str) -> Optional[str]: Returns None if `field` is not presented in the hash or `key` does not exist. Examples: - >>> hget("my_hash", "field") + >>> await client.hget("my_hash", "field") "value" - >>> hget("my_hash", "nonexistent_field") + >>> await client.hget("my_hash", "nonexistent_field") None """ return cast( @@ -454,6 +454,37 @@ async def hget(self, key: str, field: str) -> Optional[str]: await self._execute_command(RequestType.HashGet, [key, field]), ) + async def hsetnx( + self, + key: str, + field: str, + value: str, + ) -> bool: + """ + Sets `field` in the hash stored at `key` to `value`, only if `field` does not yet exist. + If `key` does not exist, a new key holding a hash is created. + If `field` already exists, this operation has no effect. + See https://redis.io/commands/hsetnx/ for more details. + + Args: + key (str): The key of the hash. + field (str): The field to set the value for. + value (str): The value to set. + + Returns: + bool: True if the field was set, False if the field already existed and was not set. + + Examples: + >>> await client.hsetnx("my_hash", "field", "value") + True # Indicates that the field "field" was set successfully in the hash "my_hash". + >>> await client.hsetnx("my_hash", "field", "new_value") + False # Indicates that the field "field" already existed in the hash "my_hash" and was not set again. + """ + return cast( + bool, + await self._execute_command(RequestType.HSetNX, [key, field, value]), + ) + async def hincrby(self, key: str, field: str, amount: int) -> int: """ Increment or decrement the value of a `field` in the hash stored at `key` by the specified amount. diff --git a/python/python/glide/async_commands/transaction.py b/python/python/glide/async_commands/transaction.py index 89550c5bee..ee52e5cb94 100644 --- a/python/python/glide/async_commands/transaction.py +++ b/python/python/glide/async_commands/transaction.py @@ -373,6 +373,28 @@ def hget(self: TTransaction, key: str, field: str) -> TTransaction: """ return self.append_command(RequestType.HashGet, [key, field]) + def hsetnx( + self: TTransaction, + key: str, + field: str, + value: str, + ) -> TTransaction: + """ + Sets `field` in the hash stored at `key` to `value`, only if `field` does not yet exist. + If `key` does not exist, a new key holding a hash is created. + If `field` already exists, this operation has no effect. + See https://redis.io/commands/hsetnx/ for more details. + + Args: + key (str): The key of the hash. + field (str): The field to set the value for. + value (str): The value to set. + + Commands response: + bool: True if the field was set, False if the field already existed and was not set. + """ + return self.append_command(RequestType.HSetNX, [key, field, value]) + def hincrby(self: TTransaction, key: str, field: str, amount: int) -> TTransaction: """ Increment or decrement the value of a `field` in the hash stored at `key` by the specified amount. diff --git a/python/python/tests/test_async_client.py b/python/python/tests/test_async_client.py index 029e73e510..c17f1fde94 100644 --- a/python/python/tests/test_async_client.py +++ b/python/python/tests/test_async_client.py @@ -634,6 +634,20 @@ async def test_hdel(self, redis_client: TRedisClient): assert await redis_client.hdel(key, ["nonExistingField"]) == 0 assert await redis_client.hdel("nonExistingKey", [field3]) == 0 + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_hsetnx(self, redis_client: TRedisClient): + key = get_random_string(10) + field = get_random_string(5) + + assert await redis_client.hsetnx(key, field, "value") == True + assert await redis_client.hsetnx(key, field, "new value") == False + assert await redis_client.hget(key, field) == "value" + key = get_random_string(5) + assert await redis_client.set(key, "value") == OK + with pytest.raises(RequestError): + await redis_client.hsetnx(key, field, "value") + @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) async def test_hmget(self, redis_client: TRedisClient): diff --git a/python/python/tests/test_transaction.py b/python/python/tests/test_transaction.py index 2e0bef1f74..ee242962dc 100644 --- a/python/python/tests/test_transaction.py +++ b/python/python/tests/test_transaction.py @@ -86,7 +86,8 @@ def transaction_test( args.append(value2) transaction.hlen(key4) args.append(2) - + transaction.hsetnx(key4, key, value) + args.append(False) transaction.hincrby(key4, key3, 5) args.append(5) transaction.hincrbyfloat(key4, key3, 5.5)