diff --git a/CHANGELOG.md b/CHANGELOG.md index 8322ff2c7c..66e2a3d2e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ #### Changes * Python: Added OBJECT ENCODING command ([#1471](https://github.com/aws/glide-for-redis/pull/1471)) * Python: Added OBJECT FREQ command ([#1472](https://github.com/aws/glide-for-redis/pull/1472)) +* Python: Added OBJECT IDLETIME command ([#1474](https://github.com/aws/glide-for-redis/pull/1474)) ## 0.4.0 (2024-05-26) diff --git a/python/python/glide/async_commands/core.py b/python/python/glide/async_commands/core.py index cd86ef09fb..0dd027f4f0 100644 --- a/python/python/glide/async_commands/core.py +++ b/python/python/glide/async_commands/core.py @@ -3545,7 +3545,7 @@ async def object_encoding(self, key: str) -> Optional[str]: await self._execute_command(RequestType.ObjectEncoding, [key]), ) - async def object_freq(self, key: str) -> int: + async def object_freq(self, key: str) -> Optional[int]: """ Returns the logarithmic access frequency counter of a Redis object stored at `key`. @@ -3555,7 +3555,7 @@ async def object_freq(self, key: str) -> int: key (str): The key of the object to get the logarithmic access frequency counter of. Returns: - int: If `key` exists, returns the logarithmic access frequency counter of the object stored at `key` as an + Optional[int]: If `key` exists, returns the logarithmic access frequency counter of the object stored at `key` as an integer. Otherwise, returns None. Examples: @@ -3563,6 +3563,27 @@ async def object_freq(self, key: str) -> int: 2 # The logarithmic access frequency counter of "my_hash" has a value of 2. """ return cast( - int, + Optional[int], await self._execute_command(RequestType.ObjectFreq, [key]), ) + + async def object_idletime(self, key: str) -> Optional[int]: + """ + Returns the time in seconds since the last access to the value stored at `key`. + + See https://valkey.io/commands/object-idletime for more details. + + Args: + key (str): The key of the object to get the idle time of. + + Returns: + Optional[int]: If `key` exists, returns the idle time in seconds. Otherwise, returns None. + + Examples: + >>> await client.object_idletime("my_hash") + 13 # "my_hash" was last accessed 13 seconds ago. + """ + return cast( + Optional[int], + await self._execute_command(RequestType.ObjectIdleTime, [key]), + ) diff --git a/python/python/glide/async_commands/transaction.py b/python/python/glide/async_commands/transaction.py index 80130da109..d16101f92c 100644 --- a/python/python/glide/async_commands/transaction.py +++ b/python/python/glide/async_commands/transaction.py @@ -2485,11 +2485,25 @@ def object_freq(self: TTransaction, key: str) -> TTransaction: key (str): The key of the object to get the logarithmic access frequency counter of. Command response: - int: If `key` exists, returns the logarithmic access frequency counter of the object stored at `key` as an + Optional[int]: If `key` exists, returns the logarithmic access frequency counter of the object stored at `key` as an integer. Otherwise, returns None. """ return self.append_command(RequestType.ObjectFreq, [key]) + def object_idletime(self: TTransaction, key: str) -> TTransaction: + """ + Returns the time in seconds since the last access to the value stored at `key`. + + See https://valkey.io/commands/object-idletime for more details. + + Args: + key (str): The key of the object to get the idle time of. + + Command response: + Optional[int]: If `key` exists, returns the idle time in seconds. Otherwise, returns None. + """ + return self.append_command(RequestType.ObjectIdleTime, [key]) + class Transaction(BaseTransaction): """ diff --git a/python/python/tests/test_async_client.py b/python/python/tests/test_async_client.py index 0e639a4afe..4a2213bf21 100644 --- a/python/python/tests/test_async_client.py +++ b/python/python/tests/test_async_client.py @@ -3409,10 +3409,23 @@ async def test_object_freq(self, redis_client: TRedisClient): ) assert await redis_client.object_freq(non_existing_key) is None assert await redis_client.set(key, "") == OK - assert await redis_client.object_freq(key) >= 0 + freq = await redis_client.object_freq(key) + assert freq is not None and freq >= 0 finally: await redis_client.config_set({maxmemory_policy_key: maxmemory_policy}) + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_object_idletime(self, redis_client: TRedisClient): + string_key = get_random_string(10) + non_existing_key = get_random_string(10) + + assert await redis_client.object_idletime(non_existing_key) is None + assert await redis_client.set(string_key, "foo") == OK + time.sleep(1) + idletime = await redis_client.object_idletime(string_key) + assert idletime is not None and idletime > 0 + class TestMultiKeyCommandCrossSlot: @pytest.mark.parametrize("cluster_mode", [True]) diff --git a/python/python/tests/test_transaction.py b/python/python/tests/test_transaction.py index a8cb937a88..f90d336c28 100644 --- a/python/python/tests/test_transaction.py +++ b/python/python/tests/test_transaction.py @@ -63,8 +63,6 @@ async def transaction_test( transaction.set(key, value) args.append(OK) - transaction.object_encoding(key) - args.append("embstr") transaction.setrange(key, 0, value) args.append(len(value)) transaction.get(key) @@ -502,10 +500,13 @@ async def test_transaction_chaining_calls(self, redis_client: TRedisClient): assert await redis_client.exec(transaction) == [OK, "value", 1] - # object_freq is not tested in transaction_test as it requires that we set and later restore the max memory policy + # The object commands are tested here instead of transaction_test because they have special requirements: + # - OBJECT FREQ and OBJECT IDLETIME require specific maxmemory policies to be set on the config + # - we cannot reliably predict the exact response values for OBJECT FREQ, OBJECT IDLETIME, and OBJECT REFCOUNT + # - OBJECT ENCODING is tested here since all the other OBJECT commands are tested here @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_transaction_object_freq( + async def test_transaction_object_commands( self, redis_client: TRedisClient, cluster_mode: bool ): string_key = get_random_string(10) @@ -515,16 +516,28 @@ async def test_transaction_object_freq( try: transaction = ClusterTransaction() if cluster_mode else Transaction() - transaction.config_set({maxmemory_policy_key: "allkeys-lfu"}) transaction.set(string_key, "foo") + transaction.object_encoding(string_key) + # OBJECT FREQ requires a LFU maxmemory-policy + transaction.config_set({maxmemory_policy_key: "allkeys-lfu"}) transaction.object_freq(string_key) + # OBJECT IDLETIME requires a non-LFU maxmemory-policy + transaction.config_set({maxmemory_policy_key: "allkeys-random"}) + transaction.object_idletime(string_key) response = await redis_client.exec(transaction) assert response is not None - assert len(response) == 3 - assert response[0] == OK - assert response[1] == OK - frequency = cast(int, response[2]) - assert frequency >= 0 + assert response[0] == OK # transaction.set(string_key, "foo") + assert response[1] == "embstr" # transaction.object_encoding(string_key) + assert ( + response[2] == OK + ) # transaction.config_set({maxmemory_policy_key: "allkeys-lfu"}) + assert cast(int, response[3]) >= 0 # transaction.object_freq(string_key) + assert ( + response[4] == OK + ) # transaction.config_set({maxmemory_policy_key: "allkeys-random"}) + assert ( + cast(int, response[5]) >= 0 + ) # transaction.object_idletime(string_key) finally: await redis_client.config_set({maxmemory_policy_key: maxmemory_policy})