Reference

The following classes are the main API of the package.

Hermes([backend, mangler, cachedfactory, ttl])

Cache façade.

hermes.backend.redis.Backend(mangler, *[, ...])

Redis backend implementation.

hermes.backend.memcached.Backend(mangler, *)

Memcached backend implementation.

hermes.backend.inprocess.Backend(mangler, *)

Simple in-process dictionary-based backend implementation.

hermes.backend.inprocess.AsyncBackend(mangler, *)

Simple in-process dictionary-based backend for asyncio programs.

Facade

class hermes.Hermes(backend=AbstractBackend, *, mangler=None, cachedfactory=cachedfactory, ttl=3600, **backendconf)

Cache façade.

Parameters:
  • backend (AbstractBackend) –

    Class or instance of cache backend. If a class is passed, keyword arguments of passed to Hermes constructor will be bypassed to the class’ constructor.

    If the argument is omitted no-op backend will be be used.

  • mangler (Mangler) – Optional, typically of a subclass, mangler instance.

  • cachedfactory (Callable[[...], Cached]) – Optional, a cache-point factory for functions and coroutines.

  • ttl (int) – Default cache entry time-to-live.

  • backend

  • mangler

  • cachedfactory

  • ttl

Usage:

import hermes.backend.redis


cache = hermes.Hermes(
  hermes.backend.redis.Backend, ttl = 600, host = 'localhost', db = 1
)

@cache
def foo(a, b):
  return a * b

class Example:

  @cache(tags = ('math', 'power'), ttl = 1200)
  def bar(self, a, b):
    return a ** b

  @cache(
    tags = ('math', 'avg'),
    key = lambda fn, *args, **kwargs: 'avg:{0}:{1}'.format(*args),
  )
  def baz(self, a, b):
    return (a + b) / 2.0

print(foo(2, 333))

example = Example()
print(example.bar(2, 10))
print(example.baz(2, 10))

foo.invalidate(2, 333)
example.bar.invalidate(2, 10)
example.baz.invalidate(2, 10)

cache.clean(['math']) # invalidate entries tagged 'math'
cache.clean()         # flush cache
ttl: int

Default cache entry time-to-live.

mangler: Mangler

Key manager responsible for creating keys, hashing and serialisation.

backend: AbstractBackend

Cache backend.

cachedfactory: Callable[[...], Cached]

Cache-point callable object factory.

__call__(*args, ttl=None, tags=(), key=None)

Wrap the callable in a cache-point instance.

Decorator that caches method or function result. The following key arguments are optional:

Bare decorator, @cache, is supported as well as a call with keyword arguments @cache(ttl = 7200).

Parameters:
  • ttl (int | None) – Seconds until entry expiration, otherwise instance’s default is used.

  • tags (Sequence[str]) – Cache entry tag list.

  • key (Callable | None) – Lambda that provides custom key, otherwise Mangler.nameEntry is used.

  • ttl

  • tags

  • key

clean(tags=())

Clean all, or tagged with given tags, cache entries.

Parameters:
  • tags (Sequence[str]) – If this argument is omitted the call flushes all cache entries, otherwise only the entries tagged by given tags are flushed.

  • tags

class hermes.HermesError

Generic Hermes error.

Redis backend

class hermes.backend.redis.Backend(mangler, *, host='localhost', password=None, port=6379, db=0, lockconf=None, **kwargs)

Bases: AbstractBackend

Redis backend implementation.

Parameters:
  • host (str) –

  • password (str | None) –

  • port (int) –

  • db (int) –

  • lockconf (dict | None) –

client: Redis

Redis client.

lock(key)

Create lock object for the key.

Parameters:

key (str) –

Return type:

Lock

save(mapping, *, ttl=None)

Save cache entry.

Parameters:
  • mapping (Dict[str, Any]) – key-value mapping for a bulk save.

  • ttl (int | None) – Cache entry time-to-live .

  • mapping

  • ttl

load(keys)

Load cache entry(ies).

Note, when handling a multiple key call, absent value keys should be excluded from resulting dictionary.

Parameters:

keys (str | Iterable[str]) –

Return type:

Any | Dict[str, Any] | None

remove(keys)

Remove given keys.

Parameters:

keys (str | Iterable[str]) –

clean()

Purge the backend storage.

class hermes.backend.redis.Lock(key, client, *, sleep=0.1, timeout=900)

Bases: AbstractLock

Key-aware distributed lock. “Distributed” is in sense of clients, not Redis instances. Implemented as described in Correct implementation with a single instance, but without setting unique value to the lock entry and later checking it, because it is expected for a cached function to complete before lock timeout.

Parameters:
  • key (str) –

  • client (Redis) –

  • sleep (float) –

  • timeout (int) –

client: Redis

Redis client.

sleep: float

Amount of time to sleep per while True iteration when waiting.

timeout: int

Maximum TTL of lock.

acquire(wait=True)

Acquire distributed lock.

Parameters:

wait – Whether to wait for the lock.

Returns:

Whether the lock was acquired.

release()

Release distributed lock.

Memcached backend

class hermes.backend.memcached.Backend(mangler, *, server='localhost:11211', lockconf=None, **kwargs)

Bases: AbstractBackend

Memcached backend implementation.

Parameters:
  • server (str) –

  • lockconf (dict | None) –

client: PooledClient

Memcached client.

lock(key)

Create lock object for the key.

Return type:

Lock

save(mapping, *, ttl=None)

Save cache entry.

Parameters:
  • mapping (Dict[str, Any]) – key-value mapping for a bulk save.

  • ttl (int | None) – Cache entry time-to-live .

  • mapping

  • ttl

load(keys)

Load cache entry(ies).

Note, when handling a multiple key call, absent value keys should be excluded from resulting dictionary.

Parameters:

keys (str | Iterable[str]) –

Return type:

Any | Dict[str, Any] | None

remove(keys)

Remove given keys.

Parameters:

keys (str | Iterable[str]) –

clean()

Purge the backend storage.

class hermes.backend.memcached.Lock(key, client, *, sleep=0.1, timeout=900)

Bases: AbstractLock

Key-aware distributed lock.

Parameters:
  • key (str) –

  • client (PooledClient) –

  • sleep (float) –

  • timeout (int) –

client: PooledClient

Memcached client.

sleep: float

Amount of time to sleep per while True iteration when waiting.

timeout: int

Maximum TTL of lock, can be up to 30 days, otherwise memcached will treated it as a UNIX timestamp of an exact date.

acquire(wait=True)

Acquire distributed lock.

Parameters:

wait – Whether to wait for the lock.

Returns:

Whether the lock was acquired.

release()

Release distributed lock.

In-process backend

class hermes.backend.inprocess.BaseBackend(mangler)

Bases: AbstractBackend

Base dictionary backend without key expiration.

cache: dict

A dict used to store cache entries.

save(mapping, *, ttl=None)

Save cache entry.

Parameters:
  • mapping (Dict[str, Any]) – key-value mapping for a bulk save.

  • ttl (int | None) – Cache entry time-to-live .

  • mapping

  • ttl

load(keys)

Load cache entry(ies).

Note, when handling a multiple key call, absent value keys should be excluded from resulting dictionary.

Parameters:

keys (str | Iterable[str]) –

Return type:

Any | Dict[str, Any] | None

remove(keys)

Remove given keys.

Parameters:

keys (str | Iterable[str]) –

clean()

Purge the backend storage.

dump()

Dump the cache entries. Sorry, Barbara.

Return type:

Dict[str, Any]

class hermes.backend.inprocess.Backend(mangler, *, ttlWatchSleep=1)

Bases: BaseBackend

Simple in-process dictionary-based backend implementation.

In-process in-memory cache without memory limit, but with expiration. Besides testing, it may be suitable for limited number of real-world use-cases with a priori small cached data.

Parameters:

ttlWatchSleep (float) –

lock(key)

Create lock object for the key.

Parameters:

key (str) –

Return type:

Lock

save(mapping, *, ttl=None)

Save cache entry.

Parameters:
  • mapping (Dict[str, Any]) – key-value mapping for a bulk save.

  • ttl (int | None) – Cache entry time-to-live .

  • mapping

  • ttl

clean()

Purge the backend storage.

stopWatch()

Ask TTL watch thread to stop and join it.

dump()

Dump the cache entries. Sorry, Barbara.

Return type:

Dict[str, Any]

class hermes.backend.inprocess.Lock(key=None)

Bases: AbstractLock

Key-unaware reentrant thread lock.

Parameters:

key (str) –

acquire(wait=True)

Acquire the RLock.

release()

Release the RLock.

class hermes.backend.inprocess.AsyncBackend(mangler, *, ttlWatchSleep=1)

Bases: BaseBackend

Simple in-process dictionary-based backend for asyncio programs.

For cache entries to expire according to their TTL, startWatch() must be awaited manually when the IO loop is already running.

Parameters:

ttlWatchSleep (float) –

lock(key)

Create lock object for the key.

Parameters:

key (str) –

Return type:

AsyncLock

save(mapping, *, ttl=None)

Save cache entry.

Parameters:
  • mapping (Dict[str, Any]) – key-value mapping for a bulk save.

  • ttl (int | None) – Cache entry time-to-live .

  • mapping

  • ttl

clean()

Purge the backend storage.

startWatch()

Start TTL watching task.

It must be called when asyncio IO loop is running.

stopWatch()

Stop TTL watching task.

class hermes.backend.inprocess.AsyncLock(key)

Bases: AbstractLock

Key-aware asynchronous lock.

Note that instances of this class are used for both synchronous and asynchronous cases. For asynchronous cases asyncio.Lock is used per key. When a synchronous callable is cached in an asynchronous application, synchronous code is by definition executed serially in single-threaded Python process running an asyncio IO loop. Hence, for synchronous code this class does nothing.

The trick that makes it work for the both cases is that hermes.Cached uses the context manager protocol, and hermes.CachedCoro uses acquire and release directly.

Parameters:

key (str) –

__enter__()

No-op context manager implementation.

Used by hermes.Cached for synchronous code.

__exit__(*args)

No-op context manager implementation.

Used by hermes.Cached for synchronous code.

async acquire(wait=True)

Acquire the asynchronous lock.

Used by CachedCoro for asynchronous code.

Return type:

bool

async release()

Release the asynchronous lock.

Used by CachedCoro for asynchronous code.

This method does not have to a coroutine itself, because underlying release is synchronous. But because hermes.CachedCoro runs regular synchronous callables in a thread pool, and the thread won’t have running IO loop, making this a coroutine lead to desired behaviour.

Extension & customisation

class hermes.Mangler

Key manager responsible for creating keys, hashing and serialisation.

prefix = 'cache'

Prefix for cache and tag entries.

serialiser = (<built-in function dumps>, <built-in function loads>)

Serialisation delegate.

compressor = (<built-in function compress>, <built-in function decompress>, <class 'zlib.error'>, 100)

Optional compression delegate.

hash(value)

Hash value.

Returns:

base64 encoded MD5 hash of the value.

Parameters:

value (bytes) –

Return type:

str

dumps(value)

Serialise and conditionally compress value.

Return type:

bytes

loads(value)

Conditionally decompress and deserialise value.

Parameters:

value (bytes) –

nameEntry(fn, *args, **kwargs)

Return cache key for given callable and its positional and keyword arguments.

Note how callable, fn, is represented in the cache key:

  1. a types.MethodType instance -> names of (module, class, method)

  2. a types.FunctionType instance -> names of (module, function)

  3. other callalbe objects with __name__ -> name of (module, object)

This means that if two function are defined dynamically in the same module with same names, like:

def createF1():
    @cache
    def f(a, b):
        return a + b
    return f

def createF2():
    @cache
    def f(a, b):
        return a * b
    return f

print(createF1()(1, 2))
print(createF2()(1, 2))

Both will return 3, because cache keys will clash. In such cases you need to pass key with custom key function.

It can also be that an object in case 3 doesn’t have name, or its name isn’t unique, then a nameEntry should be overridden with something that represents it uniquely, like repr(fn).rsplit(' at 0x', 1)[0] (address should be stripped so after Python process restart the cache can still be valid and usable).

Parameters:

fn (Callable) –

Return type:

str

nameTag(tag)

Build fully qualified backend tag name.

Parameters:

tag (str) –

Return type:

str

mapTags(tagKeys)

Map tags to random values for seeding.

Parameters:

tagKeys (Iterable[str]) –

Return type:

Dict[str, str]

hashTags(tagMap)

Hash tags of a cache entry for the entry key,

Parameters:

tagMap (Dict[str, str]) –

Return type:

str

nameLock(entryKey)

Create fully qualified backend lock key for the entry key.

Parameters:

entryKey (str) –

Entry key to create a lock key for. If given entry key is already a colon-separated key name with first component equal to prefix, first to components are dropped. For instance:

  • foocache:lock:foo

  • cache:entry:fn:tagged:78d64ea049a57494cache:lock:fn:tagged:78d64ea049a57494

Return type:

str

class hermes.Cached(frontend, callable, *, ttl=None, key=None, tags=())

Cache-point wrapper for callables and descriptors.

Parameters:
  • frontend (Hermes) –

  • callable (Callable) –

  • ttl (int | None) –

  • key (Callable | None) –

  • tags (Sequence[str]) –

invalidate(*args, **kwargs)

Invalidate the cache entry.

Invalidated entry corresponds to the wrapped callable called with given args and kwargs.

__call__(*args, **kwargs)

Get the value of the wrapped callable.

__get__(instance, type)

Implements non-data descriptor protocol.

The invocation happens only when instance method is decorated, so we can distinguish between decorated types.MethodType and types.FunctionType. Python class declaration mechanics prevent a decorator from having awareness of the class type, as the function is received by the decorator before it becomes an instance method.

How it works:

cache = hermes.Hermes()

class Model:

  @cache
  def calc(self):
    return 42

m = Model()
m.calc

Last attribute access results in the call, calc.__get__(m, Model), where calc is instance of Cached which decorates the original Model.calc.

Note, initially Cached is created on decoration per class method, when class type is created by the interpreter, and is shared among all instances. Later, on attribute access, a copy is returned with bound _callable, just like ordinary Python method descriptor works.

For more details, descriptor-protocol.

class hermes.CachedCoro(frontend, callable, *, ttl=None, key=None, tags=())

Cache-point wrapper for coroutine functions.

The implementation uses the default thread pool of asyncio to execute synchronous functions of the cache backend, and manage their (distributed) locks.

Parameters:
  • frontend (Hermes) –

  • callable (Callable) –

  • ttl (int | None) –

  • key (Callable | None) –

  • tags (Sequence[str]) –

async invalidate(*args, **kwargs)

Invalidate the cache entry.

Invalidated entry corresponds to the wrapped coroutine function called with given args and kwargs.

async __call__(*args, **kwargs)

Get the value of the wrapped coroutine function’s coroutine.

class hermes.Serialiser(dumps, loads)

Serialisation delegate.

Parameters:
  • dumps (Callable[[Any], bytes]) –

  • loads (Callable[[bytes], Any]) –

dumps: Callable[[Any], bytes]

Serialise cache value.

loads: Callable[[bytes], Any]

Deserialise cache value.

class hermes.Compressor(compress, decompress, decompressError, compressMinLength=0)

Compression delegate.

Parameters:
  • compress (Callable[[bytes], bytes]) –

  • decompress (Callable[[bytes], bytes]) –

  • decompressError (Type[Exception] | Tuple[Type[Exception], ...]) –

  • compressMinLength (int) –

compress: Callable[[bytes], bytes]

Compress serialised cache value.

decompress: Callable[[bytes], bytes]

Decompress serialised cache value.

decompressError: Type[Exception] | Tuple[Type[Exception], ...]

Decompression error(s) that indicate uncompressed payload.

compressMinLength: int

Minimal length of payload in bytes to trigger compression.

class hermes.backend.AbstractBackend(mangler)

Base backend class. It’s also the no-op implementation.

Parameters:

mangler (Mangler) –

mangler: Mangler

Key manager responsible for creating keys, hashing and serialisation.

lock(key)

Create lock object for the key.

Parameters:

key (str) –

Return type:

AbstractLock

save(mapping, *, ttl=None)

Save cache entry.

Parameters:
  • mapping (Dict[str, Any]) – key-value mapping for a bulk save.

  • ttl (int | None) – Cache entry time-to-live .

  • mapping

  • ttl

load(keys)

Load cache entry(ies).

Note, when handling a multiple key call, absent value keys should be excluded from resulting dictionary.

Parameters:

keys (str | Iterable[str]) –

Return type:

Any | Dict[str, Any] | None

remove(keys)

Remove given keys.

Parameters:

keys (str | Iterable[str]) –

clean()

Purge the backend storage.

class hermes.backend.AbstractLock(key)

Base locking class. Implements context manger protocol. Mocks acquire and release i.e. it always acquires.

Parameters:

key (str) –

key: str

Implementation may be key-aware.

__enter__()

Enter context manager by acquiring the distributed lock.

__exit__(type, value, traceback)

Exit context manager by releasing the distributed lock.

acquire(wait=True)

Acquire distributed lock.

Parameters:

wait – Whether to wait for the lock.

Returns:

Whether the lock was acquired.

Return type:

bool

release()

Release distributed lock.