From 887e9fa59b9d66d27beb7cb53451dcc18d37e5b8 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Sun, 6 Jul 2025 19:35:46 +0300 Subject: [PATCH 1/3] refactor basic storage methods --- fast_cache_middleware/controller.py | 6 +-- .../storages/base_storage.py | 35 +++++++++++----- .../storages/in_memory_storage.py | 6 +-- .../storages/redis_storage.py | 6 +-- tests/storages/test_in_memory_storage.py | 40 +++++++++---------- tests/storages/test_redis_storage.py | 14 +++---- 6 files changed, 60 insertions(+), 47 deletions(-) diff --git a/fast_cache_middleware/controller.py b/fast_cache_middleware/controller.py index cf78285..bb5dbf5 100644 --- a/fast_cache_middleware/controller.py +++ b/fast_cache_middleware/controller.py @@ -175,7 +175,7 @@ async def cache_response( """ if await self.is_cachable_response(response): response.headers["X-Cache-Status"] = "HIT" - await storage.store(cache_key, response, request, {"ttl": ttl}) + await storage.set(cache_key, response, request, {"ttl": ttl}) else: logger.debug("Skip caching for response: %s", response.status_code) @@ -191,7 +191,7 @@ async def get_cached_response( Returns: Response or None if cache is invalid/missing """ - result = await storage.retrieve(cache_key) + result = await storage.get(cache_key) if result is None: return None response, _, _ = result @@ -228,5 +228,5 @@ async def invalidate_cache( and their joint invalidation """ for path in invalidate_paths: - await storage.remove(path) + await storage.delete(path) logger.info("Invalidated cache for pattern: %s", path.pattern) diff --git a/fast_cache_middleware/storages/base_storage.py b/fast_cache_middleware/storages/base_storage.py index 8da4f8c..0944b47 100644 --- a/fast_cache_middleware/storages/base_storage.py +++ b/fast_cache_middleware/storages/base_storage.py @@ -1,5 +1,6 @@ import re from typing import Optional, Tuple, TypeAlias, Union +from abc import ABC, abstractmethod from starlette.requests import Request from starlette.responses import Response @@ -10,7 +11,7 @@ StoredResponse: TypeAlias = Tuple[Response, Request, Metadata] -class BaseStorage: +class BaseStorage(ABC): """Base class for cache storage. Args: @@ -30,16 +31,28 @@ def __init__( self._ttl = ttl - async def store( + @abstractmethod + async def set( self, key: str, response: Response, request: Request, metadata: Metadata ) -> None: - raise NotImplementedError() - - async def retrieve(self, key: str) -> Optional[StoredResponse]: - raise NotImplementedError() - - async def remove(self, path: re.Pattern) -> None: - raise NotImplementedError() - + """ + Add data: response, request, metadata to the cache storage. + """ + + @abstractmethod + async def get(self, key: str) -> Optional[StoredResponse]: + """ + Get data from the cache. + """ + + @abstractmethod + async def delete(self, path: re.Pattern) -> None: + """ + Delete data from the cache. + """ + + @abstractmethod async def close(self) -> None: - raise NotImplementedError() + """ + Clear all data from the cache. + """ diff --git a/fast_cache_middleware/storages/in_memory_storage.py b/fast_cache_middleware/storages/in_memory_storage.py index 07eae3d..6fdfaef 100644 --- a/fast_cache_middleware/storages/in_memory_storage.py +++ b/fast_cache_middleware/storages/in_memory_storage.py @@ -53,7 +53,7 @@ def __init__( self._last_expiry_check_time: float = 0 self._expiry_check_interval: float = 60 - async def store( + async def set( self, key: str, response: Response, request: Request, metadata: Metadata ) -> None: """Saves response to cache with TTL and LRU eviction support. @@ -88,7 +88,7 @@ async def store( self._cleanup_lru_items() - async def retrieve(self, key: str) -> Optional[StoredResponse]: + async def get(self, key: str) -> Optional[StoredResponse]: """Gets response from cache with lazy TTL checking. Element moves to the end to update LRU position. @@ -113,7 +113,7 @@ async def retrieve(self, key: str) -> Optional[StoredResponse]: return self._storage[key] - async def remove(self, path: re.Pattern) -> None: + async def delete(self, path: re.Pattern) -> None: """Removes responses from cache by request path pattern. Args: diff --git a/fast_cache_middleware/storages/redis_storage.py b/fast_cache_middleware/storages/redis_storage.py index 6af7c5f..8bc4629 100644 --- a/fast_cache_middleware/storages/redis_storage.py +++ b/fast_cache_middleware/storages/redis_storage.py @@ -37,7 +37,7 @@ def __init__( self._storage = redis_client self._namespace = namespace - async def store( + async def set( self, key: str, response: Response, request: Request, metadata: Metadata ) -> None: """ @@ -62,7 +62,7 @@ async def store( await self._storage.set(full_key, value, ex=ttl) logger.info("Data written to Redis") - async def retrieve(self, key: str) -> Optional[StoredResponse]: + async def get(self, key: str) -> Optional[StoredResponse]: """ Get response from Redis. If TTL expired returns None. """ @@ -82,7 +82,7 @@ async def retrieve(self, key: str) -> Optional[StoredResponse]: ) return None - async def remove(self, path: re.Pattern) -> None: + async def delete(self, path: re.Pattern) -> None: """ Deleting the cache using the specified path """ diff --git a/tests/storages/test_in_memory_storage.py b/tests/storages/test_in_memory_storage.py index 58b9c95..075f910 100644 --- a/tests/storages/test_in_memory_storage.py +++ b/tests/storages/test_in_memory_storage.py @@ -54,12 +54,12 @@ async def test_store_and_retrieve_with_ttl( response = Response(content="test", status_code=200) metadata = {"key": "value"} - await storage.store("test_key", response, request, metadata) + await storage.set("test_key", response, request, metadata) if should_expire: await asyncio.sleep(wait_time) - result = await storage.retrieve("test_key") + result = await storage.get("test_key") if should_expire: assert result is None @@ -92,16 +92,16 @@ async def test_expired_items_cleanup( metadata = {"key": "value"} # Добавляем элемент - await storage.store("test_key", response, request, metadata) + await storage.set("test_key", response, request, metadata) # Ждем await asyncio.sleep(wait_time) # Добавляем еще один элемент, который может вызвать очистку - await storage.store("test_key2", response, request, metadata) + await storage.set("test_key2", response, request, metadata) # Проверяем результат - result = await storage.retrieve("test_key") + result = await storage.get("test_key") if expected_cleanup_calls > 0: assert result is None # Элемент должен быть удален else: @@ -149,14 +149,14 @@ async def test_lru_eviction( # Добавляем элементы for i in range(num_items): - await storage.store(f"key_{i}", response, request, metadata) + await storage.set(f"key_{i}", response, request, metadata) # Проверяем размер assert len(storage) == expected_final_size # Проверяем, что последние добавленные элементы остались for i in range(max(0, num_items - max_size), num_items): - result = await storage.retrieve(f"key_{i}") + result = await storage.get(f"key_{i}") assert result is not None @@ -181,21 +181,21 @@ async def test_retrieve_updates_lru_position( storage = InMemoryStorage(max_size=len(keys)) for key in keys: - await storage.store(key, *mock_store_data) + await storage.set(key, *mock_store_data) # Добавляем элементы чтобы заполнить хранилище и получаем то что должно остаться for i in range(storage._cleanup_threshold): - await storage.store(f"key_{i}", *mock_store_data) + await storage.set(f"key_{i}", *mock_store_data) for key in retrive_keys: - await storage.retrieve(key) + await storage.get(key) # Проверяем, какой элемент остался for key in expected_keys: - assert await storage.retrieve(key) is not None + assert await storage.get(key) is not None for key in expire_keys: - assert await storage.retrieve(key) is None + assert await storage.get(key) is None @pytest.mark.asyncio @@ -227,11 +227,11 @@ async def test_remove_by_path_pattern( "headers": [("host", "test.com")], } ) - await storage.store(key, mock_response, request, mock_metadata) + await storage.set(key, mock_response, request, mock_metadata) # Удаляем по паттерну pattern = re.compile(path_pattern) - await storage.remove(pattern) + await storage.delete(pattern) # Проверяем количество оставшихся элементов assert len(storage) == expected_remaining @@ -252,9 +252,9 @@ async def test_retrieve_nonexistent_key( storage = InMemoryStorage() if should_exist: - await storage.store(key, *mock_store_data) + await storage.set(key, *mock_store_data) - result = await storage.retrieve(key) + result = await storage.get(key) if should_exist: assert result is not None @@ -282,7 +282,7 @@ async def test_close_storage(num_items: int, expected_size_after_close: int) -> # Добавляем элементы for i in range(num_items): - await storage.store(f"key_{i}", response, request, metadata) + await storage.set(f"key_{i}", response, request, metadata) assert len(storage) == num_items @@ -313,15 +313,15 @@ async def test_store_overwrite_existing_key( # Добавляем исходный элемент original_metadata = {"original": "value"} - await storage.store("test_key", response, request, original_metadata) + await storage.set("test_key", response, request, original_metadata) if overwrite_key: # Перезаписываем элемент new_metadata = {"new": "value"} - await storage.store("test_key", response, request, new_metadata) + await storage.set("test_key", response, request, new_metadata) # Получаем элемент - result = await storage.retrieve("test_key") + result = await storage.get("test_key") assert result is not None _, _, stored_metadata = result diff --git a/tests/storages/test_redis_storage.py b/tests/storages/test_redis_storage.py index e10b5cf..f504e00 100644 --- a/tests/storages/test_redis_storage.py +++ b/tests/storages/test_redis_storage.py @@ -52,11 +52,11 @@ async def test_store_and_retrieve_works(): mock_redis.exists.return_value = False - await storage.store("key1", response, request, metadata) + await storage.set("key1", response, request, metadata) mock_redis.set.assert_awaited_with("cache:key1", serialized_value, ex=1) mock_redis.get.return_value = serialized_value - result = await storage.retrieve("key1") + result = await storage.get("key1") assert result == ("deserialized_response", "req", {"meta": "data"}) @@ -77,7 +77,7 @@ async def test_store_overwrites_existing_key(): mock_redis.exists.return_value = True - await storage.store("existing_key", response, request, metadata) + await storage.set("existing_key", response, request, metadata) mock_redis.delete.assert_awaited_with("cache:existing_key") mock_redis.set.assert_awaited_with("cache:existing_key", serialized_value, ex=10) @@ -89,7 +89,7 @@ async def test_retrieve_returns_none_on_missing_key(): storage = RedisStorage(redis_client=mock_redis) mock_redis.get.return_value = None - result = await storage.retrieve("missing") + result = await storage.get("missing") assert result is None @@ -109,7 +109,7 @@ def raise_error(_): mock_redis.get.return_value = b"invalid" - result = await storage.retrieve("corrupt") + result = await storage.get("corrupt") assert result is None @@ -121,7 +121,7 @@ async def test_remove_by_regex(): pattern = re.compile(r"^/api/.*") mock_redis.scan.return_value = (0, ["myspace:/api/test1", "myspace:/api/test2"]) - await storage.remove(pattern) + await storage.delete(pattern) mock_redis.delete.assert_any_await("myspace:/api/test1") mock_redis.delete.assert_any_await("myspace:/api/test2") @@ -136,7 +136,7 @@ async def test_remove_with_no_matches_logs_warning(): pattern = re.compile(r"^/nothing.*") mock_redis.scan.return_value = (0, []) - await storage.remove(pattern) + await storage.delete(pattern) mock_redis.delete.assert_not_called() From 8157be4cb4ca0a76cf807a92bd59f6a287e4cb8b Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Sun, 6 Jul 2025 19:40:42 +0300 Subject: [PATCH 2/3] fix isort --- fast_cache_middleware/storages/base_storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fast_cache_middleware/storages/base_storage.py b/fast_cache_middleware/storages/base_storage.py index 0944b47..a87f7d3 100644 --- a/fast_cache_middleware/storages/base_storage.py +++ b/fast_cache_middleware/storages/base_storage.py @@ -1,6 +1,6 @@ import re -from typing import Optional, Tuple, TypeAlias, Union from abc import ABC, abstractmethod +from typing import Optional, Tuple, TypeAlias, Union from starlette.requests import Request from starlette.responses import Response From 8c96f22b197e542fbee22c3e90b962e2e1ddf5f2 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Sun, 6 Jul 2025 19:48:31 +0300 Subject: [PATCH 3/3] edit mocks methods --- tests/test_controller.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/tests/test_controller.py b/tests/test_controller.py index c6fdbb5..99e2a7b 100644 --- a/tests/test_controller.py +++ b/tests/test_controller.py @@ -158,26 +158,27 @@ async def test_get_cached_response_success( "ttl": 300, "etag": "test-etag", } - mock_storage.retrieve.return_value = (cached_response, mock_request, metadata) + mock_storage.get.return_value = (cached_response, mock_request, metadata) result = await controller.get_cached_response("test_key", mock_storage) + print(result) assert result is not None assert result.body == b"cached" assert result.status_code == 200 - mock_storage.retrieve.assert_called_once_with("test_key") + mock_storage.get.assert_called_once_with("test_key") @pytest.mark.asyncio async def test_get_cached_response_not_found( self, controller: Controller, mock_storage: MagicMock, mock_request: Request ) -> None: """Тестирует получение несуществующего кеша.""" - mock_storage.retrieve.return_value = None + mock_storage.get.return_value = None result = await controller.get_cached_response("test_key", mock_storage) assert result is None - mock_storage.retrieve.assert_called_once_with("test_key") + mock_storage.get.assert_called_once_with("test_key") @pytest.mark.asyncio async def test_get_cached_response_expired( @@ -185,12 +186,12 @@ async def test_get_cached_response_expired( ) -> None: """Тестирует получение истекшего кеша.""" cached_response = Response(content="cached", status_code=200) - mock_storage.retrieve.return_value = None + mock_storage.get.return_value = None result = await controller.get_cached_response("test_key", mock_storage) assert result is None - mock_storage.retrieve.assert_called_once_with("test_key") + mock_storage.get.assert_called_once_with("test_key") class TestCacheResponse: @@ -209,8 +210,8 @@ async def test_cache_response_success( "test_key", mock_request, mock_response, mock_storage, 600 ) - mock_storage.store.assert_called_once() - call_args = mock_storage.store.call_args + mock_storage.set.assert_called_once() + call_args = mock_storage.set.call_args assert call_args[0][0] == "test_key" # key assert call_args[0][1] == mock_response # response assert call_args[0][2] == mock_request # request