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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ A high-performance caching extension for FastAPI: a server-side response cache w
- **HTTP caching** — a `@cache` decorator for GET routes with `Cache-Control`,
`ETag` / `If-None-Match` (304) and per-route invalidation.
- **Application cache** — `CacheManager` for caching arbitrary JSON values in
your own code, with compute-on-miss `get_or_set()` and atomic store-if-absent `add()`.
your own code, with compute-on-miss `get_or_set()` and atomic store-if-absent `add()`,
and `@cached` for a plain function, keyed on its arguments.
- **Backends** — in-memory, Redis and Memcached, with atomic counters,
one-shot values and locks.
- **Sessions and OAuth state (deprecated)**: signed session tokens and one-time
Expand Down
9 changes: 9 additions & 0 deletions changelog.d/248.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
**`@cached` caches a plain function's result, keyed on its arguments.**
`@cached(ttl=60)` on an `async` or sync function stores its result through
`CacheManager.get_or_set()`, so concurrent misses run it once; the decorated
function is always awaited. The default key is `module.qualname:` plus a
SHA-256 of the JSON-serialized arguments, bound to the signature with defaults
applied; `key="user:{user_id}"` or `key=lambda ...: ...` names it instead, for
a method or a non-JSON argument. `fn.cache_key(...)` returns the key a call
uses and `await fn.invalidate(...)` drops its value. `manager=` picks the
`CacheManager`; without it the application's (`AppCache`) is used.
37 changes: 37 additions & 0 deletions docs/APP_CACHE.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,43 @@ await manager.clear_pattern("user:*") # matches "myapp:user:*"

Complete runnable example: [`examples/app_cache.py`](https://github.com/allen0099/FastAPI-CacheX/blob/master/examples/app_cache.py).

## Caching a function

`@cached` does what `get_or_set()` does for a plain function, keyed on its
arguments, so a loader or a call to another service is written once and
cached wherever it is called:

<!-- fmt:off -->
```python
--8<-- "examples/app_cache.py:cached"
```
<!-- fmt:on -->

- The decorated function is always `await`-ed, even if it was `def` (a sync
function then runs on the event loop, as a `get_or_set()` factory does).
Its result goes through the manager's [JSON round-trip](#json-round-trip),
so it must be JSON-serializable and comes back as JSON gives it, on the
first call too.
- `ttl` takes seconds or a `timedelta`; without it the manager's
`default_ttl` applies. `manager=` names the `CacheManager` to store
through; without it the application's is used (what `AppCache` returns),
resolved on each call, so one registered with `CacheManagerProxy.set()` at
startup is picked up. `lock=False` skips the manager's
[stampede protection](#stampede-protection) for that function.
- Without `key=` the key is `module.qualname:` followed by a SHA-256 of the
arguments, bound to the signature with defaults applied, so `load(1)`,
`load(user_id=1)` and `load(1, locale="en")` share one entry. The
arguments are hashed as JSON, so they must be JSON-serializable; a call
with one that is not raises `CacheXError`. `key="user:{user_id}"` is a
`str.format` template over the arguments by name (a plain string is a fixed
key), and `key=lambda self, user_id: f"user:{user_id}"` is called with
them: use either for a method, whose `self` cannot be hashed, or for a
model. The manager's `key_prefix` goes in front, as for every key it stores.
- `fn.cache_key(*args, **kwargs)` is the key a call uses (without the
prefix) and `await fn.invalidate(*args, **kwargs)` drops its value,
returning whether one was cached. On a method, `obj.load.invalidate(1)`
binds `obj` as on a call.

## Behavior

- `get()` returns `None` (or a supplied `default=`) on a cache miss — it never
Expand Down
4 changes: 4 additions & 0 deletions docs/api/cache-manager.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ Application-level caching of arbitrary JSON-serializable values.

::: fastapi_cachex.manager.CacheManager

::: fastapi_cachex.cached.cached

::: fastapi_cachex.cached.CachedFunction

::: fastapi_cachex.manager_proxy.CacheManagerProxy
options:
inherited_members: true
Expand Down
29 changes: 28 additions & 1 deletion examples/app_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

``get_or_set`` computes a value once and serves it from the cache until it
expires; ``add`` stores a key only if it is absent, which makes a simple
idempotency check. Both come through the ``AppCache`` dependency.
idempotency check. Both come through the ``AppCache`` dependency. ``@cached``
does the same for a plain function, keyed on its arguments.

Run it from a checkout (see ``examples/README.md``)::

Expand All @@ -22,6 +23,7 @@
from fastapi_cachex import BackendProxy
from fastapi_cachex import CacheManager
from fastapi_cachex import CacheManagerProxy
from fastapi_cachex import cached
from fastapi_cachex.backends import MemoryBackend

backend = MemoryBackend()
Expand Down Expand Up @@ -77,3 +79,28 @@ async def create_order(
async def forget_rates(app_cache: AppCache) -> dict[str, bool]:
"""Drop the cached rates; the next read calls the upstream again."""
return {"deleted": await app_cache.delete("rates")}


# --8<-- [start:cached]
# Cache a plain function on its arguments. The value goes through the
# registered CacheManager, so it gets its prefix, TTL default and lock.
@cached(ttl=60, key="rate:{currency}")
async def fetch_rate(currency: str) -> float:
"""Stand-in for a slow call to another service, once per currency."""
upstream_calls["rates"] += 1
return {"EUR": 0.92, "JPY": 151.3}.get(currency.upper(), 1.0)


@app.get("/rates/{currency}")
async def read_rate(currency: str) -> dict[str, float]:
"""``fetch_rate`` runs once per currency until its value expires."""
return {currency: await fetch_rate(currency)}


@app.delete("/rates/{currency}")
async def forget_rate(currency: str) -> dict[str, bool]:
"""Drop the value cached for this currency: ``invalidate`` takes the same arguments."""
return {"deleted": await fetch_rate.invalidate(currency)}


# --8<-- [end:cached]
4 changes: 4 additions & 0 deletions fastapi_cachex/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
from .cache import default_key_builder as default_key_builder
from .cache import invalidate as invalidate
from .cache_key import CacheKey as CacheKey
from .cached import CachedFunction as CachedFunction
from .cached import cached as cached
from .dependencies import AppCache as AppCache
from .dependencies import CacheBackend as CacheBackend
from .dependencies import get_app_cache as get_app_cache
Expand Down Expand Up @@ -135,13 +137,15 @@ def __getattr__(name: str) -> object:
"CacheManager",
"CacheManagerProxy",
"CacheXError",
"CachedFunction",
"LockTimeoutError",
"ProxyNotSetError",
"RequestNotFoundError",
"__version__",
"add_routes",
"build_cache_key",
"cache",
"cached",
"default_key_builder",
"get_app_cache",
"get_cache_backend",
Expand Down
Loading