From a075d0daabae3f5369aa51638a9962890dcfceaa Mon Sep 17 00:00:00 2001 From: argaen Date: Thu, 29 Dec 2016 23:25:15 +0100 Subject: [PATCH] Added expire command --- aiocache/backends/memcached.py | 10 ++++++++++ aiocache/backends/memory.py | 18 ++++++++++++++++++ aiocache/backends/redis.py | 11 +++++++++++ aiocache/cache.py | 20 ++++++++++++++++++++ aiocache/plugins.py | 2 ++ tests/integration/test_cache.py | 15 +++++++++++++-- tests/ut/conftest.py | 1 + tests/ut/test_cache.py | 22 ++++++++++++++++++++++ 8 files changed, 97 insertions(+), 2 deletions(-) diff --git a/aiocache/backends/memcached.py b/aiocache/backends/memcached.py index 6dacb2ab..02100b5b 100644 --- a/aiocache/backends/memcached.py +++ b/aiocache/backends/memcached.py @@ -105,6 +105,16 @@ async def _exists(self, key): """ return await self.client.append(key, b'') + async def _expire(self, key, ttl): + """ + Expire the given key in ttl seconds. If ttl is 0, remove the expiration + + :param key: str key to expire + :param ttl: int number of seconds for expiration. If 0, ttl is disabled + :returns: True if set, False if key is not found + """ + return await self.client.touch(key, ttl) + async def _delete(self, key): """ Deletes the given key. diff --git a/aiocache/backends/memory.py b/aiocache/backends/memory.py index 93361988..a02bdc99 100644 --- a/aiocache/backends/memory.py +++ b/aiocache/backends/memory.py @@ -90,6 +90,24 @@ async def _exists(self, key): """ return key in SimpleMemoryBackend._cache + async def _expire(self, key, ttl): + """ + Expire the given key in ttl seconds. If ttl is 0, remove the expiration + + :param key: str key to expire + :param ttl: int number of seconds for expiration. If 0, ttl is disabled + :returns: True if set, False if key is not found + """ + if key in SimpleMemoryBackend._cache: + handle = SimpleMemoryBackend._handlers.pop(key, None) + if handle: + handle.cancel() + loop = asyncio.get_event_loop() + SimpleMemoryBackend._handlers[key] = loop.call_later(ttl, self.__delete, key) + return True + + return False + async def _delete(self, key): """ Deletes the given key. diff --git a/aiocache/backends/redis.py b/aiocache/backends/redis.py index bfcae8ba..223bc1b9 100644 --- a/aiocache/backends/redis.py +++ b/aiocache/backends/redis.py @@ -118,6 +118,17 @@ async def _exists(self, key): exists = await redis.exists(key) return True if exists > 0 else False + async def _expire(self, key, ttl): + """ + Expire the given key in ttl seconds. If ttl is 0, remove the expiration + + :param key: str key to expire + :param ttl: int number of seconds for expiration. If 0, ttl is disabled + :returns: True if set, False if key is not found + """ + with await self._connect() as redis: + return await redis.expire(key, ttl) + async def _delete(self, key): """ Deletes the given key. diff --git a/aiocache/cache.py b/aiocache/cache.py index a46c7870..3bc8d1de 100644 --- a/aiocache/cache.py +++ b/aiocache/cache.py @@ -237,6 +237,26 @@ async def exists(self, key, namespace=None): async def _exists(self, key): raise NotImplementedError() + @plugin_pipeline + async def expire(self, key, ttl, namespace=None): + """ + Set the ttl to the given key. By setting it to 0, it will disable it + + :param key: str key to expire + :param ttl: int number of seconds for expiration. If 0, ttl is disabled + :param namespace: str alternative namespace to use + :returns: True if set, False if key is not found + """ + with Timeout(self._timeout): + start = time.time() + ns_key = self._build_key(key, namespace=namespace) + ret = await self._expire(ns_key, ttl) + logger.debug("EXPIRE %s %d (%.4f)s", ns_key, ret, time.time() - start) + return ret + + async def _expire(self, key, ttl): + raise NotImplementedError() + @plugin_pipeline async def clear(self, namespace=None): """ diff --git a/aiocache/plugins.py b/aiocache/plugins.py index cda0e6f2..59b16413 100644 --- a/aiocache/plugins.py +++ b/aiocache/plugins.py @@ -34,6 +34,7 @@ class BasePlugin: 'multi_set', 'delete', 'exists', + 'expire', 'clear', 'raw', ] @@ -46,6 +47,7 @@ class BasePlugin: 'add': True, 'delete': 0, 'exists': False, + 'expire': True, 'clear': True, 'raw': None, } diff --git a/tests/integration/test_cache.py b/tests/integration/test_cache.py index dfc021cf..2760098a 100644 --- a/tests/integration/test_cache.py +++ b/tests/integration/test_cache.py @@ -113,14 +113,14 @@ async def test_multi_set(self, cache): async def test_multi_set_with_ttl(self, cache): pairs = [(pytest.KEY, "value"), [pytest.KEY_1, "random_value"]] assert await cache.multi_set(pairs, ttl=1) is True - await asyncio.sleep(2) + await asyncio.sleep(1.1) assert await cache.multi_get([pytest.KEY, pytest.KEY_1]) == [None, None] @pytest.mark.asyncio async def test_set_with_ttl(self, cache): await cache.set(pytest.KEY, "value", ttl=1) - await asyncio.sleep(2) + await asyncio.sleep(1.1) assert await cache.get(pytest.KEY) is None @@ -178,6 +178,17 @@ async def test_exists_existing(self, cache): await cache.set(pytest.KEY, "value") assert await cache.exists(pytest.KEY) is True + @pytest.mark.asyncio + async def test_expire_existing(self, cache): + await cache.set(pytest.KEY, "value") + assert await cache.expire(pytest.KEY, 1) is True + await asyncio.sleep(1.1) + assert await cache.exists(pytest.KEY) is False + + @pytest.mark.asyncio + async def test_expire_missing(self, cache): + assert await cache.expire(pytest.KEY, 1) is False + @pytest.mark.asyncio async def test_clear(self, cache): await cache.set(pytest.KEY, "value") diff --git a/tests/ut/conftest.py b/tests/ut/conftest.py index b2de788a..68221279 100644 --- a/tests/ut/conftest.py +++ b/tests/ut/conftest.py @@ -49,6 +49,7 @@ class MockCache(BaseCache): _multi_set = asynctest.CoroutineMock() _delete = asynctest.CoroutineMock() _exists = asynctest.CoroutineMock() + _expire = asynctest.CoroutineMock() _clear = asynctest.CoroutineMock() _raw = asynctest.CoroutineMock() diff --git a/tests/ut/test_cache.py b/tests/ut/test_cache.py index e43cf591..56469fc2 100644 --- a/tests/ut/test_cache.py +++ b/tests/ut/test_cache.py @@ -48,6 +48,11 @@ async def test_exists(self, base_cache): with pytest.raises(NotImplementedError): await base_cache.exists(pytest.KEY) + @pytest.mark.asyncio + async def test_expire(self, base_cache): + with pytest.raises(NotImplementedError): + await base_cache.expire(pytest.KEY, 0) + @pytest.mark.asyncio async def test_clear(self, base_cache): with pytest.raises(NotImplementedError): @@ -175,6 +180,23 @@ async def test_delete_timeouts(self, mock_cache): with pytest.raises(asyncio.TimeoutError): await mock_cache.delete(pytest.KEY) + @pytest.mark.asyncio + async def test_expire(self, mock_cache): + await mock_cache.expire(pytest.KEY, 1) + mock_cache._expire.assert_called_with(mock_cache._build_key(pytest.KEY), 1) + + @pytest.mark.asyncio + async def test_expire_timeouts(self, mock_cache): + mock_cache._expire = asynctest.CoroutineMock(side_effect=asyncio.sleep(0.005)) + + with pytest.raises(asyncio.TimeoutError): + await mock_cache.expire(pytest.KEY, 0) + + @pytest.mark.asyncio + async def test_clear(self, mock_cache): + await mock_cache.clear(pytest.KEY) + mock_cache._clear.assert_called_with(mock_cache._build_key(pytest.KEY)) + @pytest.mark.asyncio async def test_clear_timeouts(self, mock_cache): mock_cache._clear = asynctest.CoroutineMock(side_effect=asyncio.sleep(0.005))