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
39 changes: 11 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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.
4 changes: 2 additions & 2 deletions cachium/_decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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))
Expand Down
22 changes: 21 additions & 1 deletion cachium/storages/ttl_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 `TTLMapStorage` 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)
Expand Down Expand Up @@ -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)
Expand Down
8 changes: 4 additions & 4 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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}"
Expand Down Expand Up @@ -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...")
Expand Down Expand Up @@ -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}...")
Expand Down
6 changes: 3 additions & 3 deletions docs/guides/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

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

Expand Down
4 changes: 2 additions & 2 deletions docs/guides/tutorials.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
```
Expand All @@ -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
```
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
18 changes: 9 additions & 9 deletions tests/test__decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -226,8 +226,8 @@ def _f() -> None:
@pytest.mark.parametrize(
("func", "storage"),
[
(_af, lambda: TTLMapStorage(max_size=1000, ttl=timedelta(seconds=1))),
(_f, lambda: TTLMapAsyncStorage(max_size=1000, ttl=timedelta(seconds=1))),
(_af, TTLMapStorage.create_with(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:
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion todo
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
- 0.1.0 release (choose name for the package)
- Refactor key builder
- Redis
- Invalidation
- get CacheWith annotation from nested decorators
Expand Down