Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions fast_cache_middleware/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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
Expand Down Expand Up @@ -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)
35 changes: 24 additions & 11 deletions fast_cache_middleware/storages/base_storage.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import re
from abc import ABC, abstractmethod
from typing import Optional, Tuple, TypeAlias, Union

from starlette.requests import Request
Expand All @@ -10,7 +11,7 @@
StoredResponse: TypeAlias = Tuple[Response, Request, Metadata]


class BaseStorage:
class BaseStorage(ABC):
"""Base class for cache storage.

Args:
Expand All @@ -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.
"""
6 changes: 3 additions & 3 deletions fast_cache_middleware/storages/in_memory_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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:
Expand Down
6 changes: 3 additions & 3 deletions fast_cache_middleware/storages/redis_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
"""
Expand All @@ -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.
"""
Expand All @@ -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
"""
Expand Down
40 changes: 20 additions & 20 deletions tests/storages/test_in_memory_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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


Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
14 changes: 7 additions & 7 deletions tests/storages/test_redis_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"})

Expand All @@ -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)
Expand All @@ -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


Expand All @@ -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


Expand All @@ -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")
Expand All @@ -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()


Expand Down
17 changes: 9 additions & 8 deletions tests/test_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,39 +158,40 @@ 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(
self, controller: Controller, mock_storage: MagicMock, mock_request: Request
) -> 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:
Expand All @@ -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
Expand Down