From da961d1cf1224f08833552e3342315ffa2872488 Mon Sep 17 00:00:00 2001 From: Alexander Dmitriev Date: Thu, 28 Aug 2025 10:32:28 +0400 Subject: [PATCH 1/2] refactor: update cache storage initialization to use create_with method --- README.md | 39 +++++++++++-------------------------- cachium/_decorators.py | 2 +- cachium/storages/ttl_map.py | 22 ++++++++++++++++++++- docs/examples.md | 8 ++++---- docs/guides/quickstart.md | 4 ++-- docs/guides/tutorials.md | 4 ++-- docs/index.md | 2 +- tests/test__decorator.py | 10 +++++----- todo | 2 +- 9 files changed, 48 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index bb9c4db..628d84a 100644 --- a/README.md +++ b/README.md @@ -18,12 +18,14 @@ pip install cachium ## Quick Start ```python +import asyncio + from cachium import cache +from cachium.storages.ttl_map import TTLMapStorage, TTLMapAsyncStorage # Simple function caching -@cache() +@cache(storage=TTLMapStorage.create_with(max_size=100, ttl=None)) def expensive_calculation(x: int, y: int) -> int: - print(f"Calculating {x} + {y}") return x + y # First call performs the calculation @@ -35,19 +37,17 @@ result2 = expensive_calculation(1, 2) # No calculation performed print(result2) # Output: 3 # Async function caching works too -@cache() -async def async_expensive_calculation(x: int, y: int) -> int: - print(f"Calculating {x} + {y} asynchronously") +@cache(storage=TTLMapAsyncStorage.create_with(max_size=100, ttl=None)) +async def async_io_operation(x: int, y: int) -> int: + await asyncio.sleep(1) # Simulate an I/O-bound operation return x + y -# Usage with async functions -import asyncio async def main(): # First call performs the calculation - result1 = await async_expensive_calculation(1, 2) + result1 = await async_io_operation(1, 2) # Second call uses cached result - result2 = await async_expensive_calculation(1, 2) + result2 = await async_io_operation(1, 2) print(result1, result2) # Output: 3 3 @@ -63,28 +63,11 @@ from datetime import timedelta from cachium import cache from cachium.storages.ttl_map import TTLMapStorage -@cache(storage=lambda: TTLMapStorage(max_size=100, ttl=timedelta(hours=1))) +@cache(storage=TTLMapStorage.create_with(max_size=100, ttl=timedelta(hours=1))) def long_lived_cache_function(x): return x * 2 ``` -### Custom Key Builders - -```python -from cachium import cache, DefaultKeyBuilder - -# Create a custom key builder -key_builder = DefaultKeyBuilder( - prefix="custom_prefix", - func=lambda x, y: x + y, - delimiter=":" -) - -@cache(key_builder=key_builder) -def my_function(x, y): - return x + y -``` - ## License -This project is licensed under the Apache License 2.0 - see the LICENSE file for details. +See the LICENSE file for details. diff --git a/cachium/_decorators.py b/cachium/_decorators.py index 1d9981f..83f5f7d 100644 --- a/cachium/_decorators.py +++ b/cachium/_decorators.py @@ -62,7 +62,7 @@ def cache( Minimal usage: >>> from cachium import cache >>> from cachium.storages.ttl_map import TTLMapStorage - >>> @cache(storage=lambda: TTLMapStorage()) + >>> @cache(storage=TTLMapStorage.create_with()) ... def add(a: int, b: int) -> int: ... return a + b >>> add(1, 2) diff --git a/cachium/storages/ttl_map.py b/cachium/storages/ttl_map.py index 9e0cdd6..71f8153 100644 --- a/cachium/storages/ttl_map.py +++ b/cachium/storages/ttl_map.py @@ -6,7 +6,7 @@ from asyncio import Condition as AsyncCondition from datetime import timedelta from threading import Condition -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Callable from ttlru_map import TTLMap from typing_extensions import override @@ -146,6 +146,16 @@ def __init__( self._lock_storage = LockStorage() self._storage: TTLMap[TCacheKey, TValue] = TTLMap(max_size=max_size, ttl=ttl) + @classmethod + def create_with( + cls, + *, + max_size: int | None = 1000, + ttl: timedelta | None = timedelta(minutes=1), + ) -> Callable[..., Self]: + """Return a callable that creates a new instance of `TTLMapAsyncStorage` with the specified parameters.""" + return lambda: cls(max_size=max_size, ttl=ttl) + @override def lock(self, key: TCacheKey, *, timeout: timedelta | None = None) -> SimpleLock: return SimpleLock(self._lock_storage, key, timeout) @@ -173,6 +183,16 @@ def __init__( self._lock_storage = AsyncLockStorage() self._storage: TTLMap[TCacheKey, TValue] = TTLMap(max_size=max_size, ttl=ttl) + @classmethod + def create_with( + cls, + *, + max_size: int | None = 1000, + ttl: timedelta | None = timedelta(minutes=1), + ) -> Callable[..., Self]: + """Return a callable that creates a new instance of `TTLMapAsyncStorage` with the specified parameters.""" + return lambda: cls(max_size=max_size, ttl=ttl) + @override def lock(self, key: TCacheKey, *, timeout: timedelta | None = None) -> SimpleAsyncLock: return SimpleAsyncLock(self._lock_storage, key, timeout) diff --git a/docs/examples.md b/docs/examples.md index 89d61df..36823be 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -63,7 +63,7 @@ from cachium import cache from cachium.storages.ttl_map import TTLMapStorage import time -@cache(storage=lambda: TTLMapStorage(ttl=timedelta(seconds=5))) +@cache(storage=TTLMapStorage.create_with(ttl=timedelta(seconds=5))) def get_timestamp() -> float: """Return the current timestamp.""" return time.time() @@ -92,7 +92,7 @@ print(f"Different timestamp: {ts1 != ts3}") # Output: True from cachium import cache from cachium.storages.ttl_map import TTLMapStorage -@cache(storage=lambda: TTLMapStorage(max_size=2)) # Only store the 2 most recently used results +@cache(storage=TTLMapStorage.create_with(max_size=2)) # Only store the 2 most recently used results def process_data(data_id: int) -> str: print(f"Processing data {data_id}...") return f"Processed {data_id}" @@ -194,7 +194,7 @@ cursor.execute('INSERT INTO users VALUES (1, "Alice")') cursor.execute('INSERT INTO users VALUES (2, "Bob")') conn.commit() -@cache(storage=lambda: TTLMapStorage(ttl=timedelta(minutes=5))) +@cache(storage=TTLMapStorage.create_with(ttl=timedelta(minutes=5))) def get_user(user_id: int) -> dict: """Get a user from the database by ID.""" print(f"Fetching user {user_id} from database...") @@ -226,7 +226,7 @@ from datetime import timedelta from cachium import cache from cachium.storages.ttl_map import TTLMapStorage -@cache(storage=lambda: TTLMapStorage(ttl=timedelta(minutes=5))) +@cache(storage=TTLMapStorage.create_with(ttl=timedelta(minutes=5))) async def fetch_weather(city: str) -> dict: """Simulate fetching weather data from an API.""" print(f"Fetching weather data for {city}...") diff --git a/docs/guides/quickstart.md b/docs/guides/quickstart.md index 4dc9b18..cc1c965 100644 --- a/docs/guides/quickstart.md +++ b/docs/guides/quickstart.md @@ -15,7 +15,7 @@ from py_cashier import cache from py_cashier.storages.ttl_map import TTLMapStorage # Configure TTL and max size -@cache(storage=lambda: TTLMapStorage(max_size=512, ttl=timedelta(seconds=30))) +@cache(storage=TTLMapStorage.create_with(max_size=512, ttl=timedelta(seconds=30))) def add(a: int, b: int) -> int: return a + b @@ -53,7 +53,7 @@ from py_cashier import cache, CacheWith from py_cashier.storages.ttl_map import TTLMapStorage # Only `x` participates in the cache key; calls differing only by `y` share the cached result -@cache(storage=lambda: TTLMapStorage()) +@cache(storage=TTLMapStorage.create_with()) def compute(x: Annotated[int, CacheWith()], y: int) -> int: return x + y diff --git a/docs/guides/tutorials.md b/docs/guides/tutorials.md index 7ca0178..d9746d8 100644 --- a/docs/guides/tutorials.md +++ b/docs/guides/tutorials.md @@ -9,7 +9,7 @@ from datetime import timedelta from py_cashier import cache from py_cashier.storages.ttl_map import TTLMapStorage -@cache(storage=lambda: TTLMapStorage(max_size=1000, ttl=timedelta(minutes=10))) +@cache(storage=TTLMapStorage.create_with(max_size=1000, ttl=timedelta(minutes=10))) def get_item(key: str) -> str: return f"value:{key}" ``` @@ -22,7 +22,7 @@ from py_cashier import cache, CacheWith from py_cashier.storages.ttl_map import TTLMapStorage # Cache only by `x`, ignore `y` in the cache key -@cache(storage=lambda: TTLMapStorage()) +@cache(storage=TTLMapStorage.create_with()) def f_cached(x: Annotated[int, CacheWith()], y: int) -> int: return x + y ``` diff --git a/docs/index.md b/docs/index.md index 8a625b1..0ffe0bf 100644 --- a/docs/index.md +++ b/docs/index.md @@ -28,7 +28,7 @@ from datetime import timedelta from cachium import cache from cachium.storages.ttl_map import TTLMapStorage -@cache(storage=lambda: TTLMapStorage(max_size=1024, ttl=timedelta(minutes=1))) +@cache(storage=TTLMapStorage.create_with(max_size=1024, ttl=timedelta(minutes=1))) def add(a: int, b: int) -> int: return a + b diff --git a/tests/test__decorator.py b/tests/test__decorator.py index 7df3fdd..7c4b468 100644 --- a/tests/test__decorator.py +++ b/tests/test__decorator.py @@ -30,7 +30,7 @@ def test_cache(a: int, b: int, expected: int) -> None: """Test that the cache decorator works correctly for synchronous functions.""" calls: dict[tuple[int, int], int] = defaultdict(int) - @cache(storage=lambda: TTLMapStorage(max_size=1000, ttl=timedelta(seconds=1))) + @cache(storage=TTLMapStorage.create_with(max_size=1000, ttl=timedelta(seconds=1))) def func(x: int, y: int) -> int: nonlocal calls calls[(x, y)] += 1 @@ -120,7 +120,7 @@ def test_cache_dog_piling_sync(a: int, b: int, expected: int, workers_count: int """ calls = 0 - @cache(storage=lambda: TTLMapStorage(max_size=1000, ttl=timedelta(seconds=1))) + @cache(storage=TTLMapStorage.create_with(max_size=1000, ttl=timedelta(seconds=1))) def func(x: int, y: int) -> int: time.sleep(0.1) nonlocal calls @@ -153,7 +153,7 @@ def test_cache_failing_func_sync(a: int, b: int, workers_count: int) -> None: """Test that failing functions are not cached in synchronous context.""" calls = 0 - @cache(storage=lambda: TTLMapStorage(max_size=1000, ttl=timedelta(seconds=1))) + @cache(storage=TTLMapStorage.create_with(max_size=1000, ttl=timedelta(seconds=1))) def func(x: int, y: int) -> int: time.sleep(0.03) nonlocal calls @@ -226,7 +226,7 @@ def _f() -> None: @pytest.mark.parametrize( ("func", "storage"), [ - (_af, lambda: TTLMapStorage(max_size=1000, ttl=timedelta(seconds=1))), + (_af, TTLMapStorage.create_with(max_size=1000, ttl=timedelta(seconds=1))), (_f, lambda: TTLMapAsyncStorage(max_size=1000, ttl=timedelta(seconds=1))), ], ) @@ -239,7 +239,7 @@ def test__decorator__invalid_storage(func: Callable[..., Any], storage: PStorage def test__decorator__cache_only_by_chosen_args(): calls: dict[tuple[str, int, float], int] = defaultdict(int) - @cache(storage=lambda: TTLMapStorage(max_size=1000, ttl=timedelta(seconds=1))) + @cache(storage=TTLMapStorage.create_with(max_size=1000, ttl=timedelta(seconds=1))) def func(a: Annotated[str, CacheWith()], b: int, c: Annotated[float, CacheWith]) -> str: nonlocal calls calls[(a, b, c)] += 1 diff --git a/todo b/todo index d887e35..dc4eb56 100644 --- a/todo +++ b/todo @@ -1,4 +1,4 @@ -- 0.1.0 release (choose name for the package) +- Refactor key builder - Redis - Invalidation - get CacheWith annotation from nested decorators From acd2cc239504f7b0c920512cd66c61b2906262d4 Mon Sep 17 00:00:00 2001 From: Alexander Dmitriev Date: Thu, 28 Aug 2025 10:43:15 +0400 Subject: [PATCH 2/2] refactor: update cache decorator to use create_with method for TTLMapAsyncStorage --- cachium/_decorators.py | 2 +- cachium/storages/ttl_map.py | 2 +- docs/guides/quickstart.md | 2 +- tests/test__decorator.py | 8 ++++---- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cachium/_decorators.py b/cachium/_decorators.py index 83f5f7d..937d381 100644 --- a/cachium/_decorators.py +++ b/cachium/_decorators.py @@ -74,7 +74,7 @@ def cache( >>> import asyncio >>> from cachium import cache >>> from cachium.storages.ttl_map import TTLMapAsyncStorage - >>> @cache(storage=lambda: TTLMapAsyncStorage()) + >>> @cache(storage=TTLMapAsyncStorage.create_with()) ... async def add_async(a: int, b: int) -> int: ... return a + b >>> asyncio.run(add_async(1, 2)) diff --git a/cachium/storages/ttl_map.py b/cachium/storages/ttl_map.py index 71f8153..cad1299 100644 --- a/cachium/storages/ttl_map.py +++ b/cachium/storages/ttl_map.py @@ -153,7 +153,7 @@ def create_with( max_size: int | None = 1000, ttl: timedelta | None = timedelta(minutes=1), ) -> Callable[..., Self]: - """Return a callable that creates a new instance of `TTLMapAsyncStorage` with the specified parameters.""" + """Return a callable that creates a new instance of `TTLMapStorage` with the specified parameters.""" return lambda: cls(max_size=max_size, ttl=ttl) @override diff --git a/docs/guides/quickstart.md b/docs/guides/quickstart.md index cc1c965..c3459a2 100644 --- a/docs/guides/quickstart.md +++ b/docs/guides/quickstart.md @@ -32,7 +32,7 @@ from py_cashier import cache from py_cashier.storages.ttl_map import TTLMapAsyncStorage # Configure TTL and max size for async storage -@cache(storage=lambda: TTLMapAsyncStorage(max_size=512, ttl=timedelta(seconds=30))) +@cache(storage=TTLMapAsyncStorage.create_with(max_size=512, ttl=timedelta(seconds=30))) async def add_async(a: int, b: int) -> int: # Simulate I/O await asyncio.sleep(0.1) diff --git a/tests/test__decorator.py b/tests/test__decorator.py index 7c4b468..c484b76 100644 --- a/tests/test__decorator.py +++ b/tests/test__decorator.py @@ -58,7 +58,7 @@ async def test_cache_async(a: int, b: int, expected: int) -> None: """Test that the cache decorator works correctly for asynchronous functions.""" calls: dict[tuple[int, int], int] = defaultdict(int) - @cache(storage=lambda: TTLMapAsyncStorage(max_size=1000, ttl=timedelta(seconds=1))) + @cache(storage=TTLMapAsyncStorage.create_with(max_size=1000, ttl=timedelta(seconds=1))) async def func(x: int, y: int) -> int: nonlocal calls calls[(x, y)] += 1 @@ -91,7 +91,7 @@ async def test_cache_dog_piling_async(a: int, b: int, expected: int, tasks_count """ calls = 0 - @cache(storage=lambda: TTLMapAsyncStorage(max_size=1000, ttl=timedelta(seconds=1))) + @cache(storage=TTLMapAsyncStorage.create_with(max_size=1000, ttl=timedelta(seconds=1))) async def func(x: int, y: int) -> int: await asyncio.sleep(0.1) nonlocal calls @@ -192,7 +192,7 @@ async def test_cache_failing_func_async(a: int, b: int, tasks_count: int) -> Non """Test that failing functions are not cached in asynchronous context.""" calls = 0 - @cache(storage=lambda: TTLMapAsyncStorage(max_size=1000, ttl=timedelta(seconds=1))) + @cache(storage=TTLMapAsyncStorage.create_with(max_size=1000, ttl=timedelta(seconds=1))) async def func(x: int, y: int) -> int: await asyncio.sleep(0.1) nonlocal calls @@ -227,7 +227,7 @@ def _f() -> None: ("func", "storage"), [ (_af, TTLMapStorage.create_with(max_size=1000, ttl=timedelta(seconds=1))), - (_f, lambda: TTLMapAsyncStorage(max_size=1000, ttl=timedelta(seconds=1))), + (_f, TTLMapAsyncStorage.create_with(max_size=1000, ttl=timedelta(seconds=1))), ], ) def test__decorator__invalid_storage(func: Callable[..., Any], storage: PStorage | PAsyncStorage) -> None: