Small logged cachetools wrappers with cache managers, stats, and decorators.
Project description
logged-cache
logged-cache is a small Python package around
cachetools for applications that want
cache hit/miss logs without rewriting their caching code.
It provides:
LoggedCache, a mutable mapping wrapper that logs cache activity.LRUCacheManagerandTTLCacheManager, with a shared lock forcachetools.cached.CacheStats, lightweight counters for hits, misses, writes, deletes, and clears.cachedandcachedmethod, convenience decorators for functions and methods.- Synchronous and asynchronous function caching with the same decorator.
- Hooks and key formatters for metrics, redaction, and structured logging.
Installation
pip install logged-cache
For local development with uv:
uv sync --extra dev --extra docs
uv run pytest
If you do not use uv, the project is still standard PEP 621 Python packaging:
python -m venv .venv
. .venv/bin/activate
python -m pip install -e ".[dev,docs]"
pytest
Quick Start
import logging
from logged_cache import (
LRUCacheManager,
TTLCacheManager,
cached,
cachedmethod,
)
logger = logging.getLogger("myapp.cache")
# Use LRU when you want a bounded cache.
users = LRUCacheManager(maxsize=256, logger=logger, name="users")
@cached(users)
def load_user(user_id: int) -> dict[str, int | str]:
return {"id": user_id, "name": "Ada"}
# Use TTL when values should expire.
tokens = TTLCacheManager(maxsize=1000, ttl=300, logger=logger, name="tokens")
@cached(tokens)
def fetch_token(account_id: str) -> str:
return f"token-for-{account_id}"
# Use cachedmethod for instance or class methods.
class ProfileService:
cache = LRUCacheManager(maxsize=256, logger=logger, name="profiles")
@cachedmethod(cache)
def load_profile(self, user_id: int) -> dict[str, int | str]:
return {"id": user_id, "name": "Ada"}
# The same decorators work with async functions and methods.
@cached(users)
async def load_user_async(user_id: int) -> dict[str, int | str]:
return {"id": user_id, "name": "Ada"}
load_user(1) # miss, then set.
load_user(1) # hit.
fetch_token("main") # TTL miss, then set.
ProfileService().load_profile(1) # method cache.
print(users.stats.hits, users.stats.misses, users.stats.hit_rate)
With a standard logging formatter, cache events look like this:
DEBUG:myapp.cache:[users] Cache miss: function=load_user key=(1,)
DEBUG:myapp.cache:[users] Cache set: function=load_user key=(1,)
DEBUG:myapp.cache:[users] Cache hit: function=load_user key=(1,)
If logger is not provided, logging.getLogger() is used. Each log record also
contains cache_name, cache_function, cache_event, and cache_key
attributes for structured logging. When a cache is used through cached,
cachedmethod, async_cached, or async_cachedmethod, log messages also show
the function or method name.
If name is omitted, decorators set a readable fallback name from the decorated
callable, such as myapp.services.UserService.load_user.
Keys are formatted defensively before they are written to logs. Very large keys are truncated, and plain object instances are shown as readable class names instead of memory-address-heavy default representations:
DEBUG:myapp.cache:[users] Cache miss: key=<myapp.queries.UserQuery>
DEBUG:myapp.cache:[users] Cache miss: key=('xxxxxxxxxxxxxxxxxxxx...<truncated>)
LRU Cache
Use LRUCacheManager when you want a fixed-size cache where the least recently
used entries are evicted first.
import logging
from logged_cache import LRUCacheManager, cached
logger = logging.getLogger("myapp.cache")
users = LRUCacheManager(maxsize=256, logger=logger, name="users")
@cached(users)
def load_user(user_id: int) -> dict[str, int | str]:
return {"id": user_id, "name": "Ada"}
load_user(1)
load_user(1)
print(users.stats.hits, users.stats.misses, users.stats.hit_rate)
TTL Cache
from logged_cache import TTLCacheManager, cached
tokens = TTLCacheManager(maxsize=1000, ttl=300)
@cached(tokens)
def fetch_token(account_id: str) -> str:
return f"token-for-{account_id}"
Async Functions
Use the same cached decorator for async def. The awaited result is cached,
not the coroutine object.
from logged_cache import LRUCacheManager, cached
profiles = LRUCacheManager(maxsize=256)
@cached(profiles)
async def fetch_profile(user_id: int) -> dict[str, int | str]:
return {"id": user_id, "name": "Ada"}
profile = await fetch_profile(1)
For async-only applications, use AsyncLRUCacheManager or
AsyncTTLCacheManager with async_cached to protect cache operations with
asyncio.Lock.
from logged_cache import AsyncLRUCacheManager, async_cached
profiles = AsyncLRUCacheManager(maxsize=256, name="profiles")
@async_cached(profiles)
async def fetch_profile(user_id: int) -> dict[str, int | str]:
return {"id": user_id, "name": "Ada"}
Methods
Use cachedmethod for instance methods and class methods. By default, it uses
cachetools.keys.methodkey, so the first method argument (self or cls) is
not included in the cache key.
from logged_cache import LRUCacheManager, cachedmethod
class ProfileService:
cache = LRUCacheManager(maxsize=256)
@cachedmethod(cache)
def load_profile(self, user_id: int) -> dict[str, int | str]:
return {"id": user_id, "name": "Ada"}
Async methods work the same way:
from logged_cache import AsyncLRUCacheManager, async_cachedmethod
class AsyncProfileService:
cache = AsyncLRUCacheManager(maxsize=256)
@async_cachedmethod(cache)
async def load_profile(self, user_id: int) -> dict[str, int | str]:
return {"id": user_id, "name": "Ada"}
Direct Mapping Usage
from cachetools import LRUCache
from logged_cache import LoggedCache
cache = LoggedCache(LRUCache(maxsize=2))
cache["a"] = 1
if "a" in cache:
print(cache["a"])
Direct mapping operations emit the same event names:
DEBUG:myapp.cache:[logged-cache] Cache set: key='a'
DEBUG:myapp.cache:[logged-cache] Cache hit: key='a'
DEBUG:myapp.cache:[logged-cache] Cache delete: key='a'
DEBUG:myapp.cache:[logged-cache] Cache cleared
Redacting Sensitive Keys
By default, keys are logged with a safe formatter that truncates large values and describes plain object instances by class. For user IDs, tokens, emails, or other sensitive values, pass a formatter:
from logged_cache import LRUCacheManager
def redact_key(key: object) -> str:
return "<redacted>"
cache = LRUCacheManager(maxsize=128, key_formatter=redact_key)
To keep the default behavior but change the maximum key length, use
format_cache_key:
from functools import partial
from logged_cache import LRUCacheManager, format_cache_key
cache = LRUCacheManager(
maxsize=128,
key_formatter=partial(format_cache_key, max_length=80),
)
Naming Caches
Pass name when the same logger receives events from several caches.
users = LRUCacheManager(maxsize=256, name="users")
tokens = TTLCacheManager(maxsize=1000, ttl=300, name="tokens")
If name is omitted and the manager is used with cached or cachedmethod,
the package sets a fallback from the decorated callable's module and qualified
name. Explicit names are never replaced by decorators.
Exporting Metrics
Use on_event to bridge cache events into your metrics stack.
from logged_cache import CacheEvent, LRUCacheManager
def record_metric(event: CacheEvent, key: object) -> None:
print(f"cache.{event}")
cache = LRUCacheManager(maxsize=128, on_event=record_metric)
API Overview
LoggedCache(inner, logger=None, log_level=logging.DEBUG, stats=None, key_formatter=repr, on_event=None, name=None) wraps any mutable mapping, including
cachetools caches. If logger is None, the root logger from
logging.getLogger() is used.
LRUCacheManager(maxsize, ...) creates an LRUCache, a LoggedCache, an
RLock, and a shared CacheStats object.
TTLCacheManager(maxsize, ttl, ...) does the same for TTLCache.
AsyncLRUCacheManager(maxsize, ...) and AsyncTTLCacheManager(maxsize, ttl, ...)
use asyncio.Lock for async-only applications.
cached(manager, key=cachetools.keys.hashkey, info=False) returns a decorator
compatible with regular and async functions.
cachedmethod(manager, key=cachetools.keys.methodkey, info=False) returns a
decorator compatible with regular and async methods.
async_cached(...) and async_cachedmethod(...) are async-only variants for
async cache managers.
Development
uv sync --extra dev --extra docs
uv run ruff check .
uv run pylint src/logged_cache tests
uv run mypy
uv run pytest
uv run python -m build
uv run twine check dist/*
Install pre-commit hooks with:
uv run pre-commit install
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file logged_cache-0.0.0.tar.gz.
File metadata
- Download URL: logged_cache-0.0.0.tar.gz
- Upload date:
- Size: 18.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6f81d23c1d1f85185ab585ff9a00dc52cf3f01d3277f52e1675efbd49ffdf20f
|
|
| MD5 |
d6d701b2a3af2dd26573a8c31960eb55
|
|
| BLAKE2b-256 |
998d16ab8a48032ed1a8660f6434779a159441f49e1161b3724b61555a12e025
|
Provenance
The following attestation bundles were made for logged_cache-0.0.0.tar.gz:
Publisher:
release.yml on 51n91n51nk1n/logged_cache
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
logged_cache-0.0.0.tar.gz -
Subject digest:
6f81d23c1d1f85185ab585ff9a00dc52cf3f01d3277f52e1675efbd49ffdf20f - Sigstore transparency entry: 1615836611
- Sigstore integration time:
-
Permalink:
51n91n51nk1n/logged_cache@7e13a639388ab6b35a16280980ae9e95571fb9d2 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/51n91n51nk1n
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@7e13a639388ab6b35a16280980ae9e95571fb9d2 -
Trigger Event:
push
-
Statement type:
File details
Details for the file logged_cache-0.0.0-py3-none-any.whl.
File metadata
- Download URL: logged_cache-0.0.0-py3-none-any.whl
- Upload date:
- Size: 13.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
88daf31a735a2beaeb6150861e6460365eb7c45ccb03ba561934add98b87fcda
|
|
| MD5 |
dc976f8823ce87a53ea91c98aef19921
|
|
| BLAKE2b-256 |
a22d1d0211d03d3c83d553c15ce9577e6fc1fc7c9a7cd8158673800530e76ffb
|
Provenance
The following attestation bundles were made for logged_cache-0.0.0-py3-none-any.whl:
Publisher:
release.yml on 51n91n51nk1n/logged_cache
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
logged_cache-0.0.0-py3-none-any.whl -
Subject digest:
88daf31a735a2beaeb6150861e6460365eb7c45ccb03ba561934add98b87fcda - Sigstore transparency entry: 1615836635
- Sigstore integration time:
-
Permalink:
51n91n51nk1n/logged_cache@7e13a639388ab6b35a16280980ae9e95571fb9d2 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/51n91n51nk1n
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@7e13a639388ab6b35a16280980ae9e95571fb9d2 -
Trigger Event:
push
-
Statement type: