Cache

from craft.facades import Cache

Cache.put("stats", value, ttl=300)
Cache.get("stats", default=None)
Cache.has("stats")
Cache.forget("stats")
Cache.flush()

Stores

| Driver | Where | Use it for | |---|---|---| | array | Process memory | Default; tests and single-process development | | file | storage/framework/cache | Survives a restart, no extra service | | redis | Redis | Shared across processes and machines |

CACHE_DRIVER=array

Redis needs the extra:

pip install -e ".[redis]"

If Redis is configured but unreachable, the manager degrades to the array store rather than failing the request. Cache misses are cheaper than downtime — but it does mean a misconfigured Redis looks like a very cold cache.

remember

Compute a value once and reuse it until it expires:

def dashboard_stats():
    return Cache.remember("dashboard.stats", 300, lambda: expensive_query())

The callback runs only on a miss. remember_forever skips the TTL.

A callback that returns None is cached too — remember stores a sentinel in its place, so an expensive lookup that legitimately produced None is not re-run on every call.

Counters

Cache.increment("page.views")        # 1
Cache.increment("page.views", 5)     # 6
Cache.decrement("page.views", 2)     # 4

A missing key starts at zero. Incrementing or decrementing an existing entry keeps whatever TTL it already had — a counter stored with a 60-second TTL does not become eternal just because it was bumped.

pull

Read and forget in one step — handy for one-shot values:

token = Cache.pull("one_time_token")

TTL

ttl is in seconds. Omit it, or use forever, to store without expiry:

Cache.put("key", value, ttl=60)
Cache.forever("key", value)

Expired entries are removed on read, so an expired key behaves exactly like a missing one.

What can be cached

The array store keeps the object as-is. The file and Redis stores serialise to JSON, so values must be JSON-serialisable — dicts, lists, strings, numbers, booleans and None. Model instances are not; cache model.to_dict() instead.

Clearing

python dev.py cache clear

In tests

The array store is the default under pytest, and each CacheManager holds its own store, so tests do not leak cached values into each other. To be explicit:

from craft.cache.manager import ArrayStore, CacheManager

cache = CacheManager()
cache._store = ArrayStore()

Atomic put-if-absent

Cache.add(key, value, ttl) stores only when the key is absent and returns whether this caller won. Reach for it instead of has() then put() whenever the answer decides who does the work: the pair is a race, and every guarantee built on it silently does not hold.