Cashews
Cache with async power
Install / Use
/learn @Krukov/CashewsREADME
pip install cashews
pip install cashews[redis]
pip install cashews[diskcache]
pip install cashews[dill] # can cache in redis more types of objects
pip install cashews[speedup] # for bloom filters
Why
Cache plays a significant role in modern applications and everybody wants to use all the power of async programming and cache. There are a few advanced techniques with cache and async programming that can help you build simple, fast, scalable and reliable applications. This library intends to make it easy to implement such techniques.
Features
- Easy to configure and use
- Decorator-based API, decorate and play
- Different cache strategies out-of-the-box
- Support for multiple storage backends (In-memory, Redis, DiskCache)
- Set TTL as a string ("2h5m"), as
timedeltaor use a function in case TTL depends on key parameters - Transactionality
- Middlewares
- Client-side cache (10x faster than simple cache with redis)
- Bloom filters
- Different cache invalidation techniques (time-based or tags)
- Cache any objects securely with pickle (use secret)
- Save memory size with compression
- 2x faster than
aiocache(with client side caching)
Usage Example
from cashews import cache
cache.setup("mem://") # configure as in-memory cache, but redis/diskcache is also supported
# use a decorator-based API
@cache(ttl="3h", key="user:{request.user.uid}")
async def long_running_function(request):
...
# or for fine-grained control, use it directly in a function
async def cache_using_function(request):
await cache.set(key=request.user.uid, value=request.user, expire="20h")
...
More examples here
Table of Contents
- Configuration
- Available Backends
- Basic API
- Disable Cache
- Strategies
- Cache Invalidation
- Detect the source of a result
- Middleware
- Callbacks
- Transactional mode
- Contrib
Configuration
cashews provides a default cache, that you can setup in two different ways:
from cashews import cache
# via url
cache.setup("redis://0.0.0.0/?db=1&socket_connect_timeout=0.5&suppress=0&secret=my_secret&enable=1")
# or via kwargs
cache.setup("redis://0.0.0.0/", db=1, wait_for_connection_timeout=0.5, suppress=False, secret=b"my_key", enable=True)
Alternatively, you can create a cache instance yourself:
from cashews import Cache
cache = Cache()
cache.setup(...)
Optionally, you can disable cache with disable/enable parameter (see Disable Cache):
cache.setup("redis://redis/0?enable=1")
cache.setup("mem://?size=500", disable=True)
cache.setup("mem://?size=500", enable=False)
You can setup different Backends based on a prefix:
cache.setup("redis://redis/0")
cache.setup("mem://?size=500", prefix="user")
await cache.get("accounts") # will use the redis backend
await cache.get("user:1") # will use the memory backend
Available Backends
In-memory
The in-memory cache uses fixed-sized LRU dict to store values. It checks expiration on get
and periodically purge expired keys.
cache.setup("mem://")
cache.setup("mem://?check_interval=10&size=10000")
Redis
Requires redis package.\
This will use Redis as a storage.
This backend uses pickle module to serialize values, but the cashes can store values with md5-keyed hash.
Use secret and digestmod parameters to protect your application from security vulnerabilities.
The digestmod is a hashing algorithm that can be used: sum, md5 (default), sha1 and sha256.
To use xxhash algorithms (digestmods: xxh3_64, xxh3_128, xxh32 and xxh64) install xxhash package or setup cashews with speedup requirements (pip install cashews[speedup]))
The secret is a salt for a hash.
Pickle can't serialize any type of object. In case you need to store more complex types
you can use dill - set pickle_type="dill". Dill is great, but less performance.
If you need complex serializer for sqlalchemy objects you can set pickle_type="sqlalchemy"
Use json also an option to serialize/deserialize an object, but it very limited (pickle_type="json")
Any connection errors are suppressed, to disable it use suppress=False - a CacheBackendInteractionError will be raised
For some data, it may be useful to use compression. Gzip and zlib compression are available;
you can use the compress_type parameter to configure it.
If you would like to use client-side cache set client_side=True. Client side cache will add cashews: prefix for each key, to customize it use client_side_prefix option.
If you would like to use RedisCluster set cluster=True.
cache.setup("redis://0.0.0.0/?db=1&minsize=10&suppress=false&secret=my_secret", prefix="func")
cache.setup("redis://0.0.0.0/2", password="my_pass", socket_connect_timeout=0.1, retry_on_timeout=True, secret="my_secret")
cache.setup("redis://0.0.0.0", client_side=True, client_side_prefix="my_prefix:", pickle_type="dill", compress_type="gzip")
cache.setup("redis://0.0.0.0:6379", cluster=True)
For using secure connections to redis (over ssl) uri should have rediss as schema
cache.setup("rediss://0.0.0.0/", ssl_ca_certs="path/to/ca.crt", ssl_keyfile="path/to/client.key",ssl_certfile="path/to/client.crt",)
DiskCache
Requires diskcache package.
This will use local sqlite databases (with shards) as storage.
It is a good choice if you don't want to use redis, but you need a shared storage, or your cache takes a lot of local memory. Also, it is a good choice for client side local storage.
You can setup disk cache with Cache parameters
** Warning ** cache.scan and cache.get_match does not work with this storage (works only if shards are disabled)
** Warning ** Be careful with the default settings as they contain parameters such as size_limit
cache.setup("disk://")
cache.setup("disk://?directory=/tmp/cache&timeout=1&shards=0") # disable shards
Gb = 1073741824
cache.setup("disk://", size_limit=3 * Gb, shards=12)
Basic API
There are a few basic methods to work with cache:
from cashews import cache
cache.setup("mem://") # configure as in-memory cache
await cache.set(key="key", value=90, expire="2h", exist=None) # -> bool
await cache.set_raw(key="key", value="str") # -> bool
await cache.set_many({"key1": value, "key2": value}) # -> None
await cache.get("key", default=None) # -> Any
await cache.get_or_set("key", default=awaitable_or_callable, expire="1h") # -> Any
await cache.get_raw("key") # -> Any
await cache.get_many("key1", "key2", default=None) # -> tuple[Any]
async for key, value in cache.get_match("pattern:*", batch_size=100):
...
await cache.incr("key") # -> int
await cache.exists("key") # -> bool
await cache.delete("key")
await cache.delete_many("key1", "key2")
await cache.delete_match("pattern:*")
async for key in cache.scan("pattern:*"):
...
await cache.expire("key", timeout=10)
await cache.get_expire("key") # -> int seconds to expire
await cache.ping(message=None) # -> bytes
await cache.clear()
await cache.is_locked("key", wait=60) # -> bool
async with cache.lock("key", expire=10):
...
await cache.set_lock("key", value="value", expire=60) # -> bool
await cache.unlock("key", "value") # -> bool
await cache.get_keys_count() # -> int - total number of keys in cache
await cache.close()
Disable Cache
Cache can be disabled not only at setup, but also in runtime. Cashews allow you to disable/enable any call of cache or specific commands:
from cashews import cache, Command
cache.setup("mem://") # configure as in-memory cache
cache.disable(Command.DELETE)
cache.disable()
cache.enable(Command.GET, Command.SET)
cache.enable()
with cache.disabling():
...
Strategies
- Simple cache
- Fail cache (Failover cache)
- Hit cache
- Early
- Soft
- Async Iterators
- Locked
- Rate limit
- Circuit breaker
Simple cache
This is a typical cache strategy: execute, store and return from cache until it expires.
from datetime import timedelta
from cashews import cache
cache.setup("mem://")
@cache(ttl=timedelta(hours=3), key="user:{request.user.uid}")
async def long_running_function(request):
...
Fail cache (Failover cache)
Return cache result, if one of the given exceptions is raised (at least one function call should succeed prior to that).
from cashews import cache
cache.setup("mem://")
# note: the key will be "__module__.get_status:name:{name}"
@cache.failover(ttl="2h", exceptions=(Valu
