From 09e53a15a2e5b1bebda2b57d2b9e9c7e848bbffa Mon Sep 17 00:00:00 2001 From: kasra Date: Sat, 27 Jan 2024 02:17:25 +0330 Subject: [PATCH 1/3] Add support for hashmaps --- changelog.d/598.feature | 1 + django_redis/cache.py | 20 +++++++++ django_redis/client/default.py | 76 ++++++++++++++++++++++++++++++++++ tests/test_backend.py | 38 +++++++++++++++++ 4 files changed, 135 insertions(+) create mode 100644 changelog.d/598.feature diff --git a/changelog.d/598.feature b/changelog.d/598.feature new file mode 100644 index 00000000..dfae9a2a --- /dev/null +++ b/changelog.d/598.feature @@ -0,0 +1 @@ +Support HashMaps \ No newline at end of file diff --git a/django_redis/cache.py b/django_redis/cache.py index 732ca9df..9c270f1d 100644 --- a/django_redis/cache.py +++ b/django_redis/cache.py @@ -184,3 +184,23 @@ def close(self, **kwargs): @omit_exception def touch(self, *args, **kwargs): return self.client.touch(*args, **kwargs) + + @omit_exception + def hset(self, *args, **kwargs): + return self.client.hset(*args, **kwargs) + + @omit_exception + def hdel(self, *args, **kwargs): + return self.client.hdel(*args, **kwargs) + + @omit_exception + def hlen(self, *args, **kwargs): + return self.client.hlen(*args, **kwargs) + + @omit_exception + def hkeys(self, *args, **kwargs): + return self.client.hkeys(*args, **kwargs) + + @omit_exception + def hexists(self, *args, **kwargs): + return self.client.hexists(*args, **kwargs) diff --git a/django_redis/client/default.py b/django_redis/client/default.py index 9485627f..07b49f9d 100644 --- a/django_redis/client/default.py +++ b/django_redis/client/default.py @@ -813,3 +813,79 @@ def touch( # Convert to milliseconds timeout = int(timeout * 1000) return bool(client.pexpire(key, timeout)) + + def hset( + self, + name: str, + key: KeyT, + value: EncodableT, + version: Optional[int] = None, + client: Optional[Redis] = None, + ) -> int: + """ + Set the value of hash name at key to value. + Returns the number of fields added to the hash. + """ + if client is None: + client = self.get_client(write=True) + nkey = self.make_key(key, version=version) + nvalue = self.encode(value) + return int(client.hset(name, nkey, nvalue)) + + def hdel( + self, + name: str, + key: KeyT, + version: Optional[int] = None, + client: Optional[Redis] = None, + ) -> int: + """ + Remove keys from hash name. + Returns the number of fields deleted from the hash. + """ + if client is None: + client = self.get_client(write=True) + nkey = self.make_key(key, version=version) + return int(client.hdel(name, nkey)) + + def hlen( + self, + name: str, + client: Optional[Redis] = None, + ) -> int: + """ + Return the number of items in hash name. + """ + if client is None: + client = self.get_client(write=False) + return int(client.hlen(name)) + + def hkeys( + self, + name: str, + client: Optional[Redis] = None, + ) -> List: + """ + Return a list of keys in hash name. + """ + if client is None: + client = self.get_client(write=False) + try: + return [self.reverse_key(k.decode()) for k in client.hkeys(name)] + except _main_exceptions as e: + raise ConnectionInterrupted(connection=client) from e + + def hexists( + self, + name: str, + key: KeyT, + version: Optional[int] = None, + client: Optional[Redis] = None, + ) -> bool: + """ + Return True if key exists in hash name, else False. + """ + if client is None: + client = self.get_client(write=False) + nkey = self.make_key(key, version=version) + return bool(client.hexists(name, nkey)) diff --git a/tests/test_backend.py b/tests/test_backend.py index 58d6b92c..d654a1b7 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -797,3 +797,41 @@ def test_clear(self, cache: RedisCache): cache.clear() value_from_cache_after_clear = cache.get("foo") assert value_from_cache_after_clear is None + + def test_hset(self, cache: RedisCache): + cache.hset("foo_hash1", "foo1", "bar1") + cache.hset("foo_hash1", "foo2", "bar2") + assert cache.hlen("foo_hash1") == 2 + assert cache.hexists("foo_hash1", "foo1") + assert cache.hexists("foo_hash1", "foo2") + + def test_hdel(self, cache: RedisCache): + cache.hset("foo_hash2", "foo1", "bar1") + cache.hset("foo_hash2", "foo2", "bar2") + assert cache.hlen("foo_hash2") == 2 + deleted_count = cache.hdel("foo_hash2", "foo1") + assert deleted_count == 1 + assert cache.hlen("foo_hash2") == 1 + assert not cache.hexists("foo_hash2", "foo1") + assert cache.hexists("foo_hash2", "foo2") + + def test_hlen(self, cache: RedisCache): + assert cache.hlen("foo_hash3") == 0 + cache.hset("foo_hash3", "foo1", "bar1") + assert cache.hlen("foo_hash3") == 1 + cache.hset("foo_hash3", "foo2", "bar2") + assert cache.hlen("foo_hash3") == 2 + + def test_hkeys(self, cache: RedisCache): + cache.hset("foo_hash4", "foo1", "bar1") + cache.hset("foo_hash4", "foo2", "bar2") + cache.hset("foo_hash4", "foo3", "bar3") + keys = cache.hkeys("foo_hash4") + assert len(keys) == 3 + for i in range(len(keys)): + assert keys[i] == f"foo{i + 1}" + + def test_hexists(self, cache: RedisCache): + cache.hset("foo_hash5", "foo1", "bar1") + assert cache.hexists("foo_hash5", "foo1") + assert not cache.hexists("foo_hash5", "foo") From d08e9225c1fadd58eb09bc031c23123967814b5e Mon Sep 17 00:00:00 2001 From: kasra Date: Mon, 29 Jan 2024 19:02:37 +0330 Subject: [PATCH 2/3] Skip sharded client in hashmap tests --- tests/test_backend.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_backend.py b/tests/test_backend.py index d654a1b7..550ce79c 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -799,6 +799,8 @@ def test_clear(self, cache: RedisCache): assert value_from_cache_after_clear is None def test_hset(self, cache: RedisCache): + if isinstance(cache.client, ShardClient): + pytest.skip("ShardClient doesn't support get_client") cache.hset("foo_hash1", "foo1", "bar1") cache.hset("foo_hash1", "foo2", "bar2") assert cache.hlen("foo_hash1") == 2 @@ -806,6 +808,8 @@ def test_hset(self, cache: RedisCache): assert cache.hexists("foo_hash1", "foo2") def test_hdel(self, cache: RedisCache): + if isinstance(cache.client, ShardClient): + pytest.skip("ShardClient doesn't support get_client") cache.hset("foo_hash2", "foo1", "bar1") cache.hset("foo_hash2", "foo2", "bar2") assert cache.hlen("foo_hash2") == 2 @@ -816,6 +820,8 @@ def test_hdel(self, cache: RedisCache): assert cache.hexists("foo_hash2", "foo2") def test_hlen(self, cache: RedisCache): + if isinstance(cache.client, ShardClient): + pytest.skip("ShardClient doesn't support get_client") assert cache.hlen("foo_hash3") == 0 cache.hset("foo_hash3", "foo1", "bar1") assert cache.hlen("foo_hash3") == 1 @@ -823,6 +829,8 @@ def test_hlen(self, cache: RedisCache): assert cache.hlen("foo_hash3") == 2 def test_hkeys(self, cache: RedisCache): + if isinstance(cache.client, ShardClient): + pytest.skip("ShardClient doesn't support get_client") cache.hset("foo_hash4", "foo1", "bar1") cache.hset("foo_hash4", "foo2", "bar2") cache.hset("foo_hash4", "foo3", "bar3") @@ -832,6 +840,8 @@ def test_hkeys(self, cache: RedisCache): assert keys[i] == f"foo{i + 1}" def test_hexists(self, cache: RedisCache): + if isinstance(cache.client, ShardClient): + pytest.skip("ShardClient doesn't support get_client") cache.hset("foo_hash5", "foo1", "bar1") assert cache.hexists("foo_hash5", "foo1") assert not cache.hexists("foo_hash5", "foo") From d2ce2fa3735fc1c1a233d246b8a577354defdf8c Mon Sep 17 00:00:00 2001 From: kasra Date: Mon, 29 Jan 2024 19:12:29 +0330 Subject: [PATCH 3/3] Refactor code --- django_redis/client/default.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_redis/client/default.py b/django_redis/client/default.py index 07b49f9d..4742b1b7 100644 --- a/django_redis/client/default.py +++ b/django_redis/client/default.py @@ -864,7 +864,7 @@ def hkeys( self, name: str, client: Optional[Redis] = None, - ) -> List: + ) -> List[Any]: """ Return a list of keys in hash name. """