From 98a2f6bdd97be255f37b37279d246f007c49be60 Mon Sep 17 00:00:00 2001 From: Shoham Elias Date: Mon, 17 Jun 2024 07:46:10 +0000 Subject: [PATCH 1/2] Python: add GETRANGE commands --- CHANGELOG.md | 1 + python/python/glide/async_commands/core.py | 37 +++++++++++++++++++ .../glide/async_commands/transaction.py | 21 +++++++++++ python/python/tests/test_async_client.py | 34 +++++++++++++++++ python/python/tests/test_transaction.py | 2 + 5 files changed, 95 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58b4263627..959acb2f1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ * Python: Added XLEN command ([#1503](https://github.com/aws/glide-for-redis/pull/1503)) * Python: Added LASTSAVE command ([#1509](https://github.com/aws/glide-for-redis/pull/1509)) * Python: Added GETDEL command ([#1514](https://github.com/aws/glide-for-redis/pull/1514)) +* Python: Added GETRANGE command ([#1585](https://github.com/aws/glide-for-redis/pull/1585)) * 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)) diff --git a/python/python/glide/async_commands/core.py b/python/python/glide/async_commands/core.py index 4e33247192..112d8706f1 100644 --- a/python/python/glide/async_commands/core.py +++ b/python/python/glide/async_commands/core.py @@ -471,6 +471,43 @@ async def getdel(self, key: str) -> Optional[str]: Optional[str], await self._execute_command(RequestType.GetDel, [key]) ) + async def getrange(self, key: str, start: int, end: int) -> str: + """ + Returns the substring of the string value stored at `key`, determined by the offsets `start` and `end` (both are inclusive). + Negative offsets can be used in order to provide an offset starting from the end of the string. + So `-1` means the last character, `-2` the penultimate and so forth. + + If `key` does not exist, an empty string is returned. If `start` or `end` + are out of range, it returns the substring within the valid range of the string. + + See https://valkey.io/commands/getrange/ for more details. + + Args: + key (str): The key of the string. + start (int): The starting offset. + end (int): The ending offset. + + Returns: + str: A substring extracted from the value stored at `key`. + + Examples: + >>> await client.set("mykey", "This is a string") + >>> await client.getrange("mykey", 0, 3) + "This" + >>> await client.getrange("mykey", -3, -1) + "ing" # extracted last 3 characters of a string + >>> await client.getrange("mykey", 0, 100) + "This is a string" + >>> await client.getrange("non_existing", 5, 6) + "" + """ + return cast( + str, + await self._execute_command( + RequestType.GetRange, [key, str(start), str(end)] + ), + ) + async def append(self, key: str, value: str) -> int: """ Appends a value to a key. diff --git a/python/python/glide/async_commands/transaction.py b/python/python/glide/async_commands/transaction.py index 743f3e8fe8..ed65ccef12 100644 --- a/python/python/glide/async_commands/transaction.py +++ b/python/python/glide/async_commands/transaction.py @@ -98,6 +98,27 @@ def getdel(self: TTransaction, key: str) -> TTransaction: """ return self.append_command(RequestType.GetDel, [key]) + def getrange(self: TTransaction, key: str, start: int, end: int) -> TTransaction: + """ + Returns the substring of the string value stored at `key`, determined by the offsets `start` and `end` (both are inclusive). + Negative offsets can be used in order to provide an offset starting from the end of the string. + So `-1` means the last character, `-2` the penultimate and so forth. + + If `key` does not exist, an empty string is returned. If `start` or `end` + are out of range, it returns the substring within the valid range of the string. + + See https://valkey.io/commands/getrange/ for more details. + + Args: + key (str): The key of the string. + start (int): The starting offset. + end (int): The ending offset. + + Commands response: + str: A substring extracted from the value stored at `key`. + """ + return self.append_command(RequestType.GetRange, [key, str(start), str(end)]) + def set( self: TTransaction, key: str, diff --git a/python/python/tests/test_async_client.py b/python/python/tests/test_async_client.py index 0f94589515..21b4509087 100644 --- a/python/python/tests/test_async_client.py +++ b/python/python/tests/test_async_client.py @@ -436,6 +436,40 @@ async def test_getdel(self, redis_client: TRedisClient): with pytest.raises(RequestError) as e: await redis_client.getdel(list_key) + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_getrange(self, redis_client: TRedisClient): + key = get_random_string(16) + value = get_random_string(10) + non_string_key = get_random_string(10) + + assert await redis_client.set(key, value) == OK + assert await redis_client.getrange(key, 0, 3) == value[:4] + assert await redis_client.getrange(key, -3, -1) == value[-3:] + assert await redis_client.getrange(key, 0, -1) == value + + # out of range + assert await redis_client.getrange(key, 10, 100) == value[10:] + assert await redis_client.getrange(key, -200, -3) == value[-200:-2] + assert await redis_client.getrange(key, 100, 200) == "" + + # incorrect range + assert await redis_client.getrange(key, -1, -3) == "" + + # a redis bug, fixed in version 8: https://github.com/redis/redis/issues/13207 + if await check_if_server_version_lt(redis_client, "8.0.0"): + assert await redis_client.getrange(key, -200, -100) == value[0] + else: + assert await redis_client.getrange(key, -200, -100) == "" + + if await check_if_server_version_lt(redis_client, "8.0.0"): + assert await redis_client.getrange(non_string_key, 0, -1) == "" + + # non-string key + assert await redis_client.lpush(non_string_key, ["_"]) == 1 + with pytest.raises(RequestError): + await redis_client.getrange(non_string_key, 0, -1) + @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) async def test_config_reset_stat(self, redis_client: TRedisClient): diff --git a/python/python/tests/test_transaction.py b/python/python/tests/test_transaction.py index bd547f6410..5a4966107a 100644 --- a/python/python/tests/test_transaction.py +++ b/python/python/tests/test_transaction.py @@ -96,6 +96,8 @@ async def transaction_test( transaction.set(key, value) args.append(OK) + transaction.getrange(key, 0, -1) + args.append(value) transaction.getdel(key) args.append(value) transaction.getdel(key) From b88410fc2b9ed325fece20ae6983234411ed48fa Mon Sep 17 00:00:00 2001 From: Shoham Elias <116083498+shohamazon@users.noreply.github.com> Date: Tue, 18 Jun 2024 12:01:33 +0300 Subject: [PATCH 2/2] Apply suggestions from code review Co-authored-by: Andrew Carbonetto --- python/python/glide/async_commands/core.py | 2 +- python/python/glide/async_commands/transaction.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/python/python/glide/async_commands/core.py b/python/python/glide/async_commands/core.py index 112d8706f1..871a4c7556 100644 --- a/python/python/glide/async_commands/core.py +++ b/python/python/glide/async_commands/core.py @@ -478,7 +478,7 @@ async def getrange(self, key: str, start: int, end: int) -> str: So `-1` means the last character, `-2` the penultimate and so forth. If `key` does not exist, an empty string is returned. If `start` or `end` - are out of range, it returns the substring within the valid range of the string. + are out of range, returns the substring within the valid range of the string. See https://valkey.io/commands/getrange/ for more details. diff --git a/python/python/glide/async_commands/transaction.py b/python/python/glide/async_commands/transaction.py index ed65ccef12..d8ba40fc3b 100644 --- a/python/python/glide/async_commands/transaction.py +++ b/python/python/glide/async_commands/transaction.py @@ -105,7 +105,7 @@ def getrange(self: TTransaction, key: str, start: int, end: int) -> TTransaction So `-1` means the last character, `-2` the penultimate and so forth. If `key` does not exist, an empty string is returned. If `start` or `end` - are out of range, it returns the substring within the valid range of the string. + are out of range, returns the substring within the valid range of the string. See https://valkey.io/commands/getrange/ for more details.