Metadata-Version: 2.4
Name: logged-cache
Version: 0.0.0
Summary: Small logged cachetools wrappers with cache managers, stats, and decorators.
Project-URL: Homepage, https://github.com/51n91n51nk1n/logged_cache
Project-URL: Documentation, https://github.com/51n91n51nk1n/logged_cache#readme
Project-URL: Issues, https://github.com/51n91n51nk1n/logged_cache/issues
Project-URL: Source, https://github.com/51n91n51nk1n/logged_cache
Author: 51n91n51nk1n
License-Expression: MIT
License-File: LICENSE
Keywords: cache,cachetools,logging,lru,ttl
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: cachetools<7,>=5.3
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == 'dev'
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: packaging>=25; extra == 'dev'
Requires-Dist: pkginfo>=1.12; extra == 'dev'
Requires-Dist: pre-commit>=4.0; extra == 'dev'
Requires-Dist: pylint>=3.3; extra == 'dev'
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: python-semantic-release>=10.5; extra == 'dev'
Requires-Dist: ruff>=0.5; extra == 'dev'
Requires-Dist: twine>=5.0; extra == 'dev'
Requires-Dist: types-cachetools>=6.2; extra == 'dev'
Provides-Extra: docs
Requires-Dist: mkdocs>=1.6; extra == 'docs'
Description-Content-Type: text/markdown

# logged-cache

`logged-cache` is a small Python package around
[`cachetools`](https://cachetools.readthedocs.io/) for applications that want
cache hit/miss logs without rewriting their caching code.

It provides:

- `LoggedCache`, a mutable mapping wrapper that logs cache activity.
- `LRUCacheManager` and `TTLCacheManager`, with a shared lock for
  `cachetools.cached`.
- `CacheStats`, lightweight counters for hits, misses, writes, deletes, and
  clears.
- `cached` and `cachedmethod`, 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

```bash
pip install logged-cache
```

For local development with `uv`:

```bash
uv sync --extra dev --extra docs
uv run pytest
```

If you do not use `uv`, the project is still standard PEP 621 Python packaging:

```bash
python -m venv .venv
. .venv/bin/activate
python -m pip install -e ".[dev,docs]"
pytest
```

## Quick Start

```python
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:

```text
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:

```text
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.

```python
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

```python
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.

```python
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`.

```python
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.

```python
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:

```python
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

```python
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:

```text
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:

```python
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`:

```python
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.

```python
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.

```python
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

```bash
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:

```bash
uv run pre-commit install
```

## License

MIT
