API reference

Everything public is importable from the package root:

from action0.django_acache import (
    PoolRegistry,
    RedisCache,
    RedisCacheClient,
    aclose_pools,
    registry,
)

Backend

The cache backend: Django’s Redis cache, with native async methods.

class action0.django_acache.backend.RedisCache(server, params)[source]

Django’s RedisCache, with async methods that talk to the server through redis.asyncio instead of running the sync ones in a worker thread.

Configured like Django’s backend, plus an optional ASYNC_OPTIONS setting that overrides OPTIONS keys for the async pools only:

CACHES = {
    "default": {
        "BACKEND": "action0.django_acache.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379",
    },
}

The sync methods are Django’s own. The async ones mirror them one by one, including aincr() staying a single INCR on the server; the remaining ones (adecr, aget_or_set, aincr_version, …) are Django’s generic implementations, built on these.

Parameters:
  • server (str | list[str])

  • params (dict[str, Any])

  • server – LOCATION: one server URL, several separated by , or ;, or a list of them

  • params – the rest of the cache’s settings

get_backend_timeout(timeout=<object object>)[source]

Django’s timeout conversion, typed as what it returns for Redis: whole seconds.

Parameters:

timeout (float | None, default: <object object at 0x7f8cd54285d0>) – seconds, None for no expiry, or the default timeout

Return type:

int | None

async aadd(key, value, timeout=<object object>, version=None)[source]

The async counterpart of add().

Parameters:
  • key (Any)

  • value (Any)

  • timeout (float | None, default: <object object at 0x7f8cd54285d0>)

  • version (int | None, default: None)

Return type:

bool

async aget(key, default=None, version=None)[source]

The async counterpart of get().

Parameters:
Return type:

Any

async aset(key, value, timeout=<object object>, version=None)[source]

The async counterpart of set().

Parameters:
  • key (Any)

  • value (Any)

  • timeout (float | None, default: <object object at 0x7f8cd54285d0>)

  • version (int | None, default: None)

Return type:

None

async atouch(key, timeout=<object object>, version=None)[source]

The async counterpart of touch().

Parameters:
  • key (Any)

  • timeout (float | None, default: <object object at 0x7f8cd54285d0>)

  • version (int | None, default: None)

Return type:

bool

async adelete(key, version=None)[source]

The async counterpart of delete().

Parameters:
Return type:

bool

async aget_many(keys, version=None)[source]

The async counterpart of get_many().

Parameters:
Return type:

dict[Any, Any]

async ahas_key(key, version=None)[source]

The async counterpart of has_key().

Parameters:
Return type:

bool

async aincr(key, delta=1, version=None)[source]

The async counterpart of incr().

Parameters:
  • key (Any)

  • delta (int, default: 1)

  • version (int | None, default: None)

Return type:

int

async aset_many(data, timeout=<object object>, version=None)[source]

The async counterpart of set_many().

Parameters:
Return type:

list[Any]

async adelete_many(keys, version=None)[source]

The async counterpart of delete_many().

Parameters:
Return type:

None

async aclear()[source]

The async counterpart of clear().

Return type:

None

async aclose(**kwargs)[source]

Do nothing, just like close() — but without a detour through a worker thread.

Django closes its caches after every request, and the async pools are shared by all requests on the event loop, so they are closed at loop shutdown instead (or explicitly, by aclose_pools()).

Parameters:

kwargs (Any)

Return type:

None

Client

The cache client: Django’s, plus async twins of its methods.

class action0.django_acache.client.RedisCacheClient(servers, async_options=None, **options)[source]

Django’s Redis cache client, with an async method for each of its sync ones.

The async methods are line-by-line translations of Django’s sync ones onto redis.asyncio, with the same serializer and the same server selection (writes go to the first server, reads to a random other one), so the two sides read each other’s values. Their pools come from the shared registry.

Parameters:
  • servers (list[str])

  • async_options (Mapping[str, Any] | None, default: None)

  • options (Any)

  • servers – the server URLs, as Django splits them out of LOCATION

  • async_options – the cache’s ASYNC_OPTIONS, overrides for the async pools

  • options – the cache’s OPTIONS, as Django’s client takes them

async aget_client(key=None, *, write=False)[source]

The async counterpart of get_client(): the running loop’s client for a server.

Parameters:
  • key (str | None, default: None) – unused, like in get_client(); there for clients that pick a server by key

  • write (bool, default: False) – whether the client is used to write

Return type:

Redis

async aadd(key, value, timeout)[source]

The async counterpart of add().

Parameters:
Return type:

bool

async aget(key, default)[source]

The async counterpart of get().

Parameters:
Return type:

Any

async aset(key, value, timeout)[source]

The async counterpart of set().

Parameters:
Return type:

None

async atouch(key, timeout)[source]

The async counterpart of touch().

Parameters:
Return type:

bool

async adelete(key)[source]

The async counterpart of delete().

Parameters:

key (str)

Return type:

bool

async aget_many(keys)[source]

The async counterpart of get_many().

Parameters:

keys (Iterable[str])

Return type:

dict[str, Any]

async ahas_key(key)[source]

The async counterpart of has_key().

Parameters:

key (str)

Return type:

bool

async aincr(key, delta)[source]

The async counterpart of incr().

Parameters:
Return type:

int

async aset_many(data, timeout)[source]

The async counterpart of set_many().

Parameters:
Return type:

None

async adelete_many(keys)[source]

The async counterpart of delete_many().

Parameters:

keys (Iterable[str])

Return type:

None

async aclear()[source]

The async counterpart of clear().

Return type:

bool

Pools

Async connection pools, shared per event loop.

A redis.asyncio connection belongs to the event loop it was opened on, so every loop needs pools of its own. And under ASGI, Django usually creates a new cache backend instance for every request: the cache handler keeps its instances in a context-local, each request runs in a task of its own, and a task only sees the instances its parent context had already created. Pools kept on the backend instance would mean a new pool, and a new connection to the server, for every request.

The pools therefore live in one process-wide PoolRegistry, per running event loop and per configuration: all cache instances with equal settings share them, whichever request created them. Every loop’s pools are closed when the loop shuts down. asyncio.run(), asyncio.Runner, ASGI servers and asgiref’s async_to_sync all call shutdown_asyncgens() before they close a loop, and the registry leaves an async generator on every loop it serves whose finally clause closes that loop’s pools.

class action0.django_acache.pools.PoolRegistry[source]

The async clients and their pools, per event loop and configuration.

Thread-safe: every thread may run a loop of its own. The lock only guards the mapping of loops, and is never held across an await.

async client(pool_class, url, options)[source]

The running loop’s client for this configuration.

The pool is created on first use, and closed when the loop shuts down; its connections are opened on demand.

Parameters:
  • pool_class (type[ConnectionPool]) – the pool class

  • url (str) – the server URL

  • options (Mapping[str, Any]) – the keyword arguments for the pool’s from_url(); they are kept, not copied, and must not be changed afterwards

Return type:

Redis

pools(loop=None)[source]

The open pools of a loop, in the order they were created.

Parameters:

loop (AbstractEventLoop | None, default: None) – the loop; the running one by default

Return type:

tuple[ConnectionPool, ...]

async aclose()[source]

Close the running loop’s pools now rather than at loop shutdown.

Return type:

None

action0.django_acache.pools.registry = <action0.django_acache.pools.PoolRegistry object>

the registry that all cache backends of the process share

async action0.django_acache.pools.aclose_pools()[source]

Close the running event loop’s async connection pools now.

They are closed at loop shutdown anyway; this is for when that is too late, e.g. in a test that checks for leaked connections before its loop ends. The pools are shared by all cache backends, so don’t call it while other tasks on the loop still use the cache. Using a cache afterwards simply creates new pools.

Return type:

None

Options

The configuration of the async connection pools.

Django’s RedisCacheClient turns the cache’s OPTIONS into keyword arguments for its connection pools. Nearly all of them (db, password, socket_timeout, …) mean the same to redis.asyncio, so the async pools start out with the very same ones. A few name redis-py classes that come in a sync and an async variant, though: pool_class, parser_class, connection_class, retry. The cache setting ASYNC_OPTIONS overrides keys for the async pools only, and a sync class or object that would still reach them is an error (see validation).

action0.django_acache.options.SHARED_ONLY = frozenset({'serializer'})

OPTIONS keys that must stay the same for the sync and the async side: values written by one side have to be readable by the other

action0.django_acache.options.async_pool_config(pool_options, overrides)[source]

Derive the async pool class and pool options from the sync ones.

pool_class and parser_class default to their redis.asyncio variants — never to the sync ones configured in OPTIONS — and, like in OPTIONS, both may be given as dotted import paths.

>>> from redis.connection import DefaultParser as SyncParser
>>> pool_class, options = async_pool_config(
...     {"db": 1, "parser_class": SyncParser},
...     {"pool_class": "redis.asyncio.BlockingConnectionPool"},
... )
>>> pool_class.__name__, options["db"], options["parser_class"] is DefaultParser
('BlockingConnectionPool', 1, True)
Parameters:
  • pool_options (Mapping[str, Any]) – the keyword arguments Django built for its sync pools

  • overrides (Mapping[str, Any]) – the cache’s ASYNC_OPTIONS

Return type:

tuple[type[ConnectionPool], dict[str, Any]]

Returns:

the pool class and the keyword arguments for its from_url()

Raises:

ImproperlyConfigured – if overrides contains a key that must be shared, or if a sync redis-py class or object would reach the async pools

Checks that the async pools get async redis-py classes and objects.

The async pools inherit OPTIONS, so an option naming a sync redis-py class or object reaches them unless ASYNC_OPTIONS overrides it. Some of those would only fail on first use; a sync redis.retry.Retry even fails silently: the async connection accepts it, and it never retries — its call_with_retry() returns the connect coroutine instead of awaiting it. So these options are checked when the cache client is created, and a wrong one is an error that names the fix.

action0.django_acache.validation.RULES: dict[str, Rule] = {'connection_class': (<function _subclass_of.<locals>.<lambda>>, 'a redis.asyncio connection class'), 'pool_class': (<function _subclass_of.<locals>.<lambda>>, 'a redis.asyncio connection pool class'), 'retry': (<function <lambda>>, 'a redis.asyncio.retry.Retry')}

the options that come in a sync and an async variant, and what the async pools need

action0.django_acache.validation.check_async_options(options, overrides)[source]

Raise if an option of the async pools is a sync redis-py class or object.

>>> from redis.backoff import NoBackoff
>>> from redis.retry import Retry
>>> check_async_options({"retry": Retry(NoBackoff(), 3)}, {})
Traceback (most recent call last):
...
django.core.exceptions.ImproperlyConfigured: OPTIONS['retry'] is a redis.retry.Retry, which the async side cannot use: set ASYNC_OPTIONS['retry'] to a redis.asyncio.retry.Retry
Parameters:
  • options (Mapping[str, Any]) – the async pool options, pool_class included

  • overrides (Mapping[str, Any]) – the cache’s ASYNC_OPTIONS, which tell where a value came from

Raises:

ImproperlyConfigured – naming the option, where it was set, and what it should be

Return type:

None