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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def expensive_calculation(x: int, y: int) -> int:
return x + y

# First call performs the calculation
result1 = expensive_calculation(1, 2) # Output: Calculating 1 + 2
result1 = expensive_calculation(1, 2)
print(result1) # Output: 3

# Second call uses cached result
Expand All @@ -39,7 +39,7 @@ print(result2) # Output: 3
# Async function caching works too
@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
await asyncio.sleep(0.01) # Simulate an I/O-bound operation
return x + y


Expand Down
7 changes: 5 additions & 2 deletions docs/api/builders.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
# Key Builders

Key builders construct the cache key for a given function call. The DefaultKeyBuilder uses the function path and selected arguments (by default all args/kwargs) serialized via a serializer (repr by default). Choose a custom builder when you need to exclude framework objects, add prefixes/versions, or change serialization.
Key builders construct the cache key for a given function call. The `DefaultKeyBuilder` uses the function file path, function name, and selected arguments (by default all args/kwargs) serialized via a serializer (`repr` by default). Choose a custom builder when you need to exclude framework objects, add prefixes/versions, or change serialization.

The `cache` decorator accepts a key-builder factory: a callable that returns a `KeyBuilder` instance. Classes with no required constructor arguments work directly, for example `key_builder=MyKeyBuilder`.

Common parameters (DefaultKeyBuilder):
- func: the original function; used to extract signature and names.
- key_serializer: Serializer type used to turn values into stable strings (e.g., ReprSerializer, Md5Serializer).
- key_serializer: `Serializer` type used to turn values into stable strings (e.g., `ReprSerializer`, `Md5Serializer`).
- prefix: optional string to namespace or version keys.
- delimiter: string used between argument key/value pairs.

Related pages: Concepts (key model), Examples (custom builders), and Serializers. Full reference below.

Expand Down
6 changes: 4 additions & 2 deletions docs/api/cache.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ When to use:
- You want to avoid thundering herds via per-key locks.

Common parameters:
- storage: callable returning the storage instance (TTLMapStorage for sync, TTLMapAsyncStorage for async).
- key_builder: optional callable returning a KeyBuilder to customize how keys are built (which args participate, serialization, prefixes, etc.).
- storage: required callable returning the storage instance (`TTLMapStorage` for sync, `TTLMapAsyncStorage` for async).
- key_builder: optional callable returning a `KeyBuilder` to customize how keys are built (which args participate, serialization, prefixes, etc.).

The storage factory is called once at decoration time, so each decorated function gets its own storage instance. Passing a sync storage to an async function, or an async storage to a sync function, raises `TypeError`.

See also: Quickstart, Concepts, and Examples pages. Full reference below.

Expand Down
4 changes: 4 additions & 0 deletions docs/api/serializers.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,7 @@ Related pages: Key Builders and Concepts. Full reference below.
---

::: cachium.serializers._std_hash.StdHashSerializer

---

::: cachium.serializers._md5.Md5Serializer
46 changes: 46 additions & 0 deletions docs/api/storages.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Storages

Storages keep cached values and provide the per-key locks used by the decorator to prevent dog-piling.

Built-in storages:
- `TTLMapStorage`: synchronous in-memory storage with TTL expiration and LRU eviction.
- `TTLMapAsyncStorage`: asynchronous counterpart for `async def` functions.

Usage:

```python
from datetime import timedelta
from cachium import cache
from cachium.storages.ttl_map import TTLMapStorage

@cache(storage=TTLMapStorage.create_with(max_size=1000, ttl=timedelta(minutes=5)))
def get_value(key: str) -> str:
return f"value:{key}"
```

Important details:
- `cache` expects a storage factory, not a storage instance.
- The factory is called once at decoration time, giving each decorated function its own storage.
- Use `TTLMapStorage` for regular functions and `TTLMapAsyncStorage` for async functions.
- `max_size` limits entry count; `ttl` controls age-based expiration. Use `ttl=None` to disable age-based expiration.
- Built-in storages are process-local. Use a custom storage backend for shared multi-process or cross-machine caching.

Full reference below.

::: cachium.storages._abc.Result

---

::: cachium.storages._abc.BaseStorage

---

::: cachium.storages._abc.BaseAsyncStorage

---

::: cachium.storages.ttl_map.TTLMapStorage

---

::: cachium.storages.ttl_map.TTLMapAsyncStorage
2 changes: 1 addition & 1 deletion docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

See the project changelog on GitHub:

- https://github.com/zoola969/py-cashier/blob/main/CHANGELOG.md
- https://github.com/zoola969/cachium/blob/main/CHANGELOG.md
8 changes: 4 additions & 4 deletions docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,21 @@ This page combines our previous Development Guide and contribution guidelines.
Install the docs toolchain:

```bash
pip install mkdocs mkdocs-material "mkdocstrings[python]"
uv sync --group docs
```

Preview documentation locally:

```bash
mkdocs serve
uv run mkdocs serve
```

This will start a local server (usually http://127.0.0.1:8000/).

## Running tests

```bash
pytest -q
uv run pytest
```

## Linting and typing
Expand All @@ -36,4 +36,4 @@ pytest -q

## Opening issues and PRs

Open issues and PRs on GitHub: https://github.com/zoola969/py-cashier
Open issues and PRs on GitHub: https://github.com/zoola969/cachium
56 changes: 32 additions & 24 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ This page provides examples of how to use cachium in various scenarios.
from cachium import cache
from cachium.storages.ttl_map import TTLMapStorage

@cache()
@cache(storage=TTLMapStorage.create_with())
def fibonacci(n: int) -> int:
"""Calculate the nth Fibonacci number."""
if n <= 1:
Expand All @@ -33,12 +33,12 @@ import asyncio
from cachium import cache
from cachium.storages.ttl_map import TTLMapAsyncStorage

@cache()
@cache(storage=TTLMapAsyncStorage.create_with())
async def fetch_data(user_id: int) -> dict:
"""Simulate fetching user data from a database."""
print(f"Fetching data for user {user_id}...")
# Simulate network delay
await asyncio.sleep(1)
await asyncio.sleep(0.01)
return {"id": user_id, "name": f"User {user_id}"}

async def main():
Expand All @@ -63,7 +63,7 @@ from cachium import cache
from cachium.storages.ttl_map import TTLMapStorage
import time

@cache(storage=TTLMapStorage.create_with(ttl=timedelta(seconds=5)))
@cache(storage=TTLMapStorage.create_with(ttl=timedelta(milliseconds=10)))
def get_timestamp() -> float:
"""Return the current timestamp."""
return time.time()
Expand All @@ -78,7 +78,7 @@ print(f"Timestamp 2: {ts2}")
print(f"Same timestamp: {ts1 == ts2}") # Output: True

# Wait for TTL to expire
time.sleep(6)
time.sleep(0.02)

# Third call (after TTL expired) - returns new value
ts3 = get_timestamp()
Expand Down Expand Up @@ -118,15 +118,19 @@ process_data(2) # No output (cached)

```python
from cachium import cache
from cachium.key_builders import DefaultKeyBuilder
from cachium.key_builders import KeyBuilder
from cachium.storages.ttl_map import TTLMapStorage

# Only consider the first argument for caching
key_builder = DefaultKeyBuilder(
func=lambda x, *args, **kwargs: x,
prefix="first_arg_only"
)
class FirstArgumentKeyBuilder(KeyBuilder):
"""Build keys from only the first positional argument."""

@cache(key_builder=key_builder)
def build_key(self, *args, **kwargs) -> str:
return f"first_arg_only:{args[0]}"

@cache(
storage=TTLMapStorage.create_with(),
key_builder=FirstArgumentKeyBuilder,
)
def add_numbers(a: int, b: int) -> int:
print(f"Adding {a} + {b}")
return a + b
Expand All @@ -149,17 +153,21 @@ print(result3) # Output: 17

```python
from cachium import cache
from cachium.key_builders import DefaultKeyBuilder
from cachium.key_builders import KeyBuilder
from cachium.serializers import Md5Serializer
from cachium.storages.ttl_map import TTLMapStorage

# Use MD5 serializer for consistent hashing across processes
key_builder = DefaultKeyBuilder(
func=lambda x, y: x + y,
key_serializer=Md5Serializer
)
class HashedArgsKeyBuilder(KeyBuilder):
"""Build compact keys from args and kwargs."""

def build_key(self, *args, **kwargs) -> str:
return Md5Serializer.serialize((args, sorted(kwargs.items())))


@cache(key_builder=key_builder)
@cache(
storage=TTLMapStorage.create_with(),
key_builder=HashedArgsKeyBuilder,
)
def compute_value(x: int, y: int) -> int:
print(f"Computing {x} * {y}")
return x * y
Expand All @@ -179,8 +187,8 @@ print(result_again) # Output: 50
### Caching Database Queries

```python
from py_cashier import cache
from py_cashier._storages import TTLMapStorage
from cachium import cache
from cachium.storages.ttl_map import TTLMapStorage
from datetime import timedelta
import sqlite3

Expand Down Expand Up @@ -224,14 +232,14 @@ import asyncio
import random
from datetime import timedelta
from cachium import cache
from cachium.storages.ttl_map import TTLMapStorage
from cachium.storages.ttl_map import TTLMapAsyncStorage

@cache(storage=TTLMapStorage.create_with(ttl=timedelta(minutes=5)))
@cache(storage=TTLMapAsyncStorage.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}...")
# Simulate API request delay
await asyncio.sleep(1)
await asyncio.sleep(0.01)
# Return simulated weather data
return {
"city": city,
Expand Down
15 changes: 8 additions & 7 deletions docs/guides/concepts.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
# Concepts

This page explains the core concepts behind py-cashier so you can reason about cache hits, misses, and correctness in your application.
This page explains the core concepts behind cachium so you can reason about cache hits, misses, and correctness in your application.

## Cache key model
- What becomes the key: By default, keys are built from the fully qualified function name and its arguments. The default builder serializes each participating argument using a stable serializer (repr-based by default) to form a deterministic string key.
- Selective arguments: Use the CacheWith type annotation to mark which parameters should participate in the key. This helps avoid including non-deterministic or heavy framework objects (e.g., requests, DB sessions) that would explode key cardinality.
- Custom builders and serializers: Provide your own KeyBuilder to control which args/kwargs and in what order are included, and plug different serializers (e.g., MD5) when you need shorter or cross-process-stable keys.
- What becomes the key: By default, keys are built from the function file path, function name, and arguments. The default builder serializes each participating argument using `repr()` to form a deterministic string key.
- Selective arguments: Use the `CacheWith` type annotation to mark which parameters should participate in the key. If any parameter is marked, only marked parameters are included. This helps avoid non-deterministic or heavy framework objects such as requests or DB sessions.
- Custom builders and serializers: Provide your own `KeyBuilder` factory to control which args/kwargs are included and how they are serialized. Use serializers such as `Md5Serializer` when you need compact keys.

## Storage model
- In-memory TTL + LRU: The built-in TTLMapStorage/TTLMapAsyncStorage keep values in-memory with time-to-live expiration and a least-recently-used eviction policy when max_size is reached.
- Sizing: max_size bounds the number of cached entries. When the cache is full, the least recently used entries are evicted first. TTL expiration removes entries after their time window passes.
- In-memory TTL + LRU: The built-in `TTLMapStorage` and `TTLMapAsyncStorage` keep values in-memory with time-to-live expiration and a least-recently-used eviction policy when `max_size` is reached.
- Sizing: `max_size` bounds the number of cached entries. When the cache is full, the least recently used entries are evicted first. TTL expiration removes entries after their time window passes. Set `ttl=None` for entries that do not expire by age.
- Storage factories: The `cache` decorator expects a callable that creates storage, for example `TTLMapStorage.create_with(...)`. The factory is called once at decoration time, so each decorated function gets its own storage instance.
- Fit for purpose: In-memory storages are ideal for function-level caching within a single process. For multi-process or cross-machine caches, use an external store (e.g., Redis) — planned for future releases.

## Concurrency model
- Per-key locking: py-cashier prevents dog-piling by ensuring only one caller computes a missing value per key. Others wait and reuse the result. This applies to both sync and async flows with appropriate lock types.
- Per-key locking: cachium prevents dog-piling by ensuring only one caller computes a missing value per key. Others wait and reuse the result. This applies to both sync and async flows with appropriate lock types.
- Safety: The decorator verifies that sync functions use a sync storage and async functions use an async storage to avoid accidental cross-usage.
- Granularity: Locks are per-key, so independent keys proceed in parallel.

Expand Down
14 changes: 7 additions & 7 deletions docs/guides/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ Notes:

```python
from datetime import timedelta
from py_cashier import cache
from py_cashier.storages.ttl_map import TTLMapStorage
from cachium import cache
from cachium.storages.ttl_map import TTLMapStorage

# Configure TTL and max size
@cache(storage=TTLMapStorage.create_with(max_size=512, ttl=timedelta(seconds=30)))
Expand All @@ -28,14 +28,14 @@ print(add(1, 2)) # cached
```python
import asyncio
from datetime import timedelta
from py_cashier import cache
from py_cashier.storages.ttl_map import TTLMapAsyncStorage
from cachium import cache
from cachium.storages.ttl_map import TTLMapAsyncStorage

# Configure TTL and max size for async storage
@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)
await asyncio.sleep(0.01)
return a + b

async def main():
Expand All @@ -49,8 +49,8 @@ asyncio.run(main())

```python
from typing import Annotated
from py_cashier import cache, CacheWith
from py_cashier.storages.ttl_map import TTLMapStorage
from cachium import cache, CacheWith
from cachium.storages.ttl_map import TTLMapStorage

# Only `x` participates in the cache key; calls differing only by `y` share the cached result
@cache(storage=TTLMapStorage.create_with())
Expand Down
8 changes: 4 additions & 4 deletions docs/guides/tutorials.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ This section contains step-by-step guides for common workflows.

```python
from datetime import timedelta
from py_cashier import cache
from py_cashier.storages.ttl_map import TTLMapStorage
from cachium import cache
from cachium.storages.ttl_map import TTLMapStorage

@cache(storage=TTLMapStorage.create_with(max_size=1000, ttl=timedelta(minutes=10)))
def get_item(key: str) -> str:
Expand All @@ -18,8 +18,8 @@ def get_item(key: str) -> str:

```python
from typing import Annotated
from py_cashier import cache, CacheWith
from py_cashier.storages.ttl_map import TTLMapStorage
from cachium import cache, CacheWith
from cachium.storages.ttl_map import TTLMapStorage

# Cache only by `x`, ignore `y` in the cache key
@cache(storage=TTLMapStorage.create_with())
Expand Down
Loading