From a837cafcabcf55bbcef1d3720978a9aee8f5b9b3 Mon Sep 17 00:00:00 2001 From: Guionardo Furlan Date: Mon, 27 Jun 2022 15:11:32 -0300 Subject: [PATCH 1/2] Added Redis Cache --- docker-compose.yaml | 13 +++++++++++ gs/cache/__init__.py | 5 +++-- gs/cache/redis_cache.py | 40 +++++++++++++++++++++++++++++++++ tests/cache/mock_cache_redis.py | 17 ++++++++++++++ tests/cache/test_cache.py | 11 ++++++++- 5 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 docker-compose.yaml create mode 100644 gs/cache/redis_cache.py create mode 100644 tests/cache/mock_cache_redis.py diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..e2212f0 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,13 @@ +version: '3.8' +services: + cache: + image: redis:6.2-alpine + restart: always + ports: + - '6379:6379' + command: redis-server --save 20 1 --loglevel warning + volumes: + - cache:/data +volumes: + cache: + driver: local \ No newline at end of file diff --git a/gs/cache/__init__.py b/gs/cache/__init__.py index ad2bd2f..f3b8524 100644 --- a/gs/cache/__init__.py +++ b/gs/cache/__init__.py @@ -1,13 +1,14 @@ """Generic cache module.""" +from .cache_protocol import Cache from .file_cache import FileCache from .memory_cache import MemoryCache -from .cache_protocol import Cache +from .redis_cache import RedisCache def get_cache(connection_string: str) -> Cache: """Get cache instance from connection string.""" - for cache in [MemoryCache, FileCache]: + for cache in [MemoryCache, FileCache, RedisCache]: instance = cache().parse(connection_string) if instance: return instance diff --git a/gs/cache/redis_cache.py b/gs/cache/redis_cache.py new file mode 100644 index 0000000..d02e52a --- /dev/null +++ b/gs/cache/redis_cache.py @@ -0,0 +1,40 @@ +"""Redis Cache""" +import datetime +import logging +from typing import Union + +import redis.utils + +from .cache_protocol import Cache + + +class RedisCache(Cache): + """Redis Cache""" + + def __init__(self): + self.log = logging.getLogger(self.__class__.__name__) + self.redis: redis.Redis = None + + def get(self, key: str) -> Union[str, None]: + """Return value or None if not found.""" + value = self.redis.get(key) + if isinstance(value, bytes): + value = value.decode('utf-8') + return value + + def set(self, key: str, value: str, + ttl: datetime.timedelta = datetime.timedelta(seconds=0)) -> None: + """Set value with time to live""" + if ttl.total_seconds() > 0: + self.redis.set(key, value, ex=int(ttl.total_seconds())) + elif ttl.total_seconds() == 0: + self.redis.set(key, value) + + def parse(self, connection_string: str) -> 'Cache': + """Parse connection string and return instance of Cache if valid.""" + if not connection_string.startswith('redis://'): + return None + + self.redis = redis.utils.from_url(connection_string) + self.log.info('Initialized') + return self diff --git a/tests/cache/mock_cache_redis.py b/tests/cache/mock_cache_redis.py new file mode 100644 index 0000000..0bb7304 --- /dev/null +++ b/tests/cache/mock_cache_redis.py @@ -0,0 +1,17 @@ +"""Mocking redis""" + + +class FakeRedis(): + """Fake redis class""" + + def get(self, key: str): + return {'test_key': b'test_value', + 'test_key2': None}.get(key) + + def set(self, *args, **kwargs): + ... + + +def fake_from_url(*args, **kwargs): + """Fake from_url function""" + return FakeRedis() diff --git a/tests/cache/test_cache.py b/tests/cache/test_cache.py index 3a44581..a2f64c1 100644 --- a/tests/cache/test_cache.py +++ b/tests/cache/test_cache.py @@ -2,8 +2,11 @@ import datetime import tempfile import unittest +from unittest.mock import patch -from gs.cache import Cache, FileCache, MemoryCache, get_cache +from gs.cache import Cache, FileCache, MemoryCache, RedisCache, get_cache + +from .mock_cache_redis import fake_from_url class TestCache(unittest.TestCase): @@ -25,6 +28,12 @@ def test_file_cache(self): cache = get_cache(f'path://{tmpdir}') self._test_cache(cache, FileCache) + @patch('redis.utils.from_url', fake_from_url) + def test_redis_cache(self): + """Test redis cache""" + cache = get_cache('redis://localhost:6379/0') + self._test_cache(cache, RedisCache) + def _test_cache(self, cache: Cache, class_type): self.assertIsInstance(cache, class_type) cache.set('test_key', 'test_value') From 9e8f4ddebdb93300627419d9fe80b12323333968 Mon Sep 17 00:00:00 2001 From: Guionardo Furlan Date: Mon, 27 Jun 2022 15:12:02 -0300 Subject: [PATCH 2/2] Changed version to 0.1.5 --- gs/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gs/__init__.py b/gs/__init__.py index 85d5eae..1fff4ce 100644 --- a/gs/__init__.py +++ b/gs/__init__.py @@ -1,5 +1,5 @@ """Package data""" -__version__ = '0.1.4' +__version__ = '0.1.5' __tool_name__ = 'py-gstools' __description__ = 'Tool classes and functions for Guiosoft projects' __author__ = 'Guionardo Furlan'