Framework-independent Cache System

(part of privex.helpers.cache)

Helper functions/classes related to caching.

This module acts as a singleton wrapper, allowing for easily setting a framework-independent global cache API.

To make the module easy to use, adapter_get() initialises an instance of MemoryCache if no global cache adapter instance has been setup. This means you can use the various alias functions in this module without having to configure a cache adapter.

Available Cache Adapters

Standard Synchronous Adapters

Four synchronous cache adapters are included by default - MemoryCache (dependency free), MemcachedCache (needs pylibmc library), SqliteCache (needs privex-db library), RedisCache (needs redis library).

While these synchronous classes don’t support coroutines for most methods, as of privex-helpers 2.7 the method privex.helpers.cache.CacheAdapter.CacheAdapter.get_or_set_async() is an async version of CacheAdapter.get_or_set(), and is available on all CacheAdapter sub-classes (both MemoryCache and RedisCache). get_or_set_async allows a coroutine or coroutine function/method reference to be passed as the fallback value.

Adapter

Description

CacheAdapter

This is the base class for all synchronous cache adapters (doesn’t do anything)

MemoryCache

A cache adapter which stores cached items in memory using a dict. Fully functional incl. timeout.

MemcachedCache

A cache adapter for Memcached using the synchronous python library pylibmc

RedisCache

A cache adapter for Redis using the python library redis

SqliteCache

A cache adapter for SQLite3 using the standard Python module sqlite3 + privex.db

Asynchronous (Python AsyncIO) Adapters

Over the past few years, Python’s AsyncIO has grown more mature and has gotten a lot of attention. Thankfully, whether you use AsyncIO or not, we’ve got you covered.

Four AsyncIO cache adapters are included by default - AsyncMemoryCache (dependency free), AsyncRedisCache (needs aioredis library), AsyncSqliteCache (needs aiosqlite library), and AsyncMemcachedCache (needs aiomcache library).

Adapter

Description

AsyncCacheAdapter

This is the base class for all AsyncIO cache adapters (abstract class, only implements get_or_set)

AsyncMemoryCache

A cache adapter which stores cached items in memory using a dict. Fully functional incl. timeout.

AsyncRedisCache

A cache adapter for Redis using the AsyncIO python library aioredis

AsyncMemcachedCache

A cache adapter for Memcached using the AsyncIO python library aiomcache

AsyncSqliteCache

A cache adapter for SQLite3 using the AsyncIO python library aiosqlite

Setting / updating the global cache adapter instance

First import the cache module.

>>> from privex.helpers import cache

When setting an adapter using adapter_set(), if your application has a user configurable cache adapter via a plain text configuration file (e.g. a .env file), or you simply don’t have any need to manually instantiate a cache adapter, then you can pass either an alias name (memory, redis, memcached, sqlite3), or a full adapter class name (such as MemoryCache, MemcachedCache, RedisCache, SqliteCache). Example usage:

>>> cache.adapter_set('memcached')
>>> cache.adapter_set('MemcachedCache')

Alternatively, you may instantiate your cache adapter of choice before passing it to adapter_set() - which updates the global cache adapter instance.

>>> my_adapter = cache.MemoryCache()
>>> cache.adapter_set(my_adapter)

Once you’ve set the adapter, you can use the module functions such as get() and set() - or you can import cached to enable dictionary-like cache item access.

>>> cache.set('hello', 'world')
>>> cache.get('hello')
'world'
>>> from privex.helpers import cached
>>> cached['hello']
'world'
>>> cached['otherkey'] = 'testing'

You can also use AsyncIO adapters with the global cache adapter wrapper. CacheWrapper uses awaitable() to ensure that AsyncIO adapters can work synchronously when being called from a synchronous function, while working asynchronously from a non-async function.

>>> my_adapter = cache.AsyncRedisCache()
>>> cache.adapter_set(my_adapter)
>>>
>>> # get_hello_async() is async, so @awaitable returns the normal .get() coroutine for awaiting
>>> async def get_hello_async():
...     result = await cached.get('hello')
...     return result
...
>>> # get_hello() is synchronous, so @awaitable seamlessly runs .get() in an event loop and returns
>>> # the result - get_hello() can treat it as if it were just a normal synchronous function.
>>> def get_hello():
...     return cached.get('hello')
...
>>> get_hello()
'world'
>>> await get_hello_async()
'world'

Important info about using the cache abstraction layer with AsyncIO

While cached usually works in AsyncIO contexts, due to the synchronous backwards compatibility wrappers, the standard CacheWrapper can behave strangely in complex AsyncIO applications.

If your application / library is primarily AsyncIO, you should use async_cached ( AsyncCacheWrapper ) instead of the standard cached - and similarly, the adapter management functions async_adapter_get() and async_adapter_set().

The AsyncCacheWrapper adapter wrapper class - as the name implies - is generally only usable from async functions/methods, and maintains a separate cache adapter instance than CacheWrapper ( cached ), which must be an AsyncIO adapter such as AsyncRedisCache

The AsyncIO cache abstraction layer / wrapper works pretty much exactly the same as the “hybrid” CacheWrapper, but to make it clear how it can be used, here’s some example code which shows setting/getting the global Async adapter, and using the cache abstraction layer.

Examples

First we’ll import the three main AsyncIO cache abstraction functions, plus AsyncRedisCache

>>> from privex.helpers.cache import async_cached, async_adapter_get, async_adapter_set, AsyncRedisCache

We can use the global AsyncIO cache adapter instance (defaults to AsyncMemoryCache) via async_cached:

>>> await async_cached.set('hello', 'world')
>>> await async_cached.get('hello')
'world'

Much like the standard CacheWrapper, you can get and set keys using dict-like syntax, however since __getitem__ and __setitem__ can’t be natively async without Python complaining, they use the wrapper decorator awaitable() for getting, and the synchronous async wrapper function loop_run() for setting. The use of these wrappers may cause problems in certain scenarios, so it’s recommended to avoid using the dict-like cache syntax within AsyncIO code:

>>> await async_cached['hello']
'world'
>>> async_cached['lorem'] = 'ipsum'

To set / replace the global AsyncIO cache adapter, use async_adapter_set() - similarly, you can use async_adapter_get() to get the current adapter instance (e.g. a direct instance of AsyncRedisCache if that’s the current adapter):

>>> async_adapter_set(AsyncRedisCache())    # Either pass an instance of an async cache adapter class
>>> async_adapter_set('redis')              # Or pass a simple string alias name, such as: redis, memcached, memory, sqlite3
>>> adp = async_adapter_get()
>>> await adp.set('lorem', 'test')  # Set 'lorem' using the AsyncRedisCache instance directly
>>> await adp.get('lorem')          # Get 'lorem' using the AsyncRedisCache instance directly
'test'
>>> await async_cached.get('lorem') # Get 'lorem' using the global AsyncIO cache wrapper
'test'

Plug-n-play usage

As explained near the start of this module’s documentation, you don’t have to set the global adapter if you only plan on using the simple MemoryCache adapter.

Just start using the global cache API via either privex.helpers.cache or privex.helpers.cache.cached and MemoryCache will automatically be instantiated as the global adapter as soon as something attempts to access the global instance.

We recommend importing cached rather than cache, as it acts as a wrapper that allows dictionary-like cache key getting/setting, and is also immediately aware when the global cache adapter is set/replaced.

>>> from privex.helpers import cached

You can access cached like a dictionary to get and set cache keys (they will use the default expiry time of privex.helpers.settings.DEFAULT_CACHE_TIMEOUT)

>>> cached['testing'] = 123
>>> cached['testing']
123

You can also call methods such as get() and set() for getting/setting cache items with more control, for example:

  1. Setting a custom expiration, or disabling expiration by setting timeout to None

    >>> cached.set('example', 'test', timeout=30)   # Drop 'example' from the cache after 30 seconds from now.
    >>> cached.set('this key', 'is forever!', timeout=None) # A timeout of ``None`` disables automatic expiration.
    
  2. Fallback values when a key isn’t found, or have it throw an exception if it’s not found instead.

    >>> cached.get('example', 'NOT FOUND')          # If the key 'example' doesn't exist, return 'NOT FOUND'
    'test'
    
    >>> try:   # By setting ``fail`` to True, ``get`` raises ``CacheNotFound`` if the key doesn't exist / is expired
    ...     cached.get('nonexistent', fail=True)
    ... except CacheNotFound:
    ...     log.error('The cache key "nonexistent" does not exist!')
    >>>
    
  3. Using get_or_set() you can specify either a standard type (e.g. str, int, dict), or even a custom function to call to obtain the value to set and return.

    >>> cached.get_or_set('hello', lambda key: 'world', timeout=60)
    >>> cached['hello']
    'world'
    

Copyright:

    +===================================================+
    |                 © 2019 Privex Inc.                |
    |               https://www.privex.io               |
    +===================================================+
    |                                                   |
    |        Originally Developed by Privex Inc.        |
    |        License: X11 / MIT                         |
    |                                                   |
    |        Core Developer(s):                         |
    |                                                   |
    |          (+)  Chris (@someguy123) [Privex]        |
    |          (+)  Kale (@kryogenic) [Privex]          |
    |                                                   |
    +===================================================+

Copyright 2020     Privex Inc.   ( https://www.privex.io )

Cache API Docs

(the above heading is for sidebar display purposes on the docs)

Base/Core Caching System

Below are functions and classes contained within privex.helpers.cache (inside the module init file __init__.py) - these functions/classes provide a high-level entry point into the Privex Helper’s Framework Independent Caching System

Functions

adapter_get([default])

Get the global cache adapter instance.

adapter_set(adapter)

Set the global cache adapter instance to adapter - which should be an instantiated adapter class which implements CacheAdapter

get(key[, default, fail])

Return the value of cache key key.

get_or_set(key, value[, timeout])

Attempt to return the value of key in the cache.

remove(*key)

Remove one or more keys from the cache.

set(key, value[, timeout])

Set the cache key key to the value value, and automatically expire the key after timeout seconds from now.

update_timeout(key[, timeout])

Update the timeout for a given key to datetime.utcnow() + timedelta(seconds=timeout)

Attributes

cached

This module attribute acts as a singleton, containing an instance of CacheWrapper which is designed to allow painless usage of this caching module.

async_cached

For applications/packages which are primarily AsyncIO, CacheWrapper can cause problems such as the common event loop is already running - and unfortunately, nest_asyncio isn’t always able to fix it.

Classes

CacheWrapper()

CacheWrapper is a small class designed to wrap an instance of CacheAdapter and allow the adapter to be switched out at any time, using the static class attribute cache_instance.

AsyncCacheWrapper()

For applications/packages which are primarily AsyncIO, CacheWrapper can cause problems such as the common event loop is already running - and unfortunately, nest_asyncio isn’t always able to fix it.

Extra Modules

extras

post_deps

Synchronous Cache Adapters

Currently, Privex Helper’s caching system includes two synchronous cache adapters for use by the user, along with the base adapter class CacheAdapter

In-memory Cache

MemoryCache is the cache adapter used by default by the cache abstraction system, if you don’t change the adapter in your application using adapter_set(). MemoryCache requires no additional dependencies to use, and can handle caching practically anything you throw at it, since it simply stores the cache in a python dict dictionary within your applications memory.

Redis Cache

RedisCache is the second pre-included synchronous cache adapter, unlike MemoryCache, it requires the package redis to function. The redis package is included in the privex-helpers’ extra cache.

To use the cache extra, simply change privex-helpers in your requirements.txt to: privex-helpers[cache]

Similarly, if installing the package on the command line, you can run: pip3 install "privex-helpers[cache]" and then Pip will install both privex-helpers, along with some additional dependencies to be able to use all of the available cache adapters.

Class List

Classes

CacheAdapter(*args, enter_reconnect, …)

CacheAdapter is an abstract base class which scaffolds methods for implementing a Cache, allowing for consistent methods and method signatures across all child classes which implement it.

MemoryCache(*args, enter_reconnect, …)

A very basic cache adapter which implements CacheAdapter - stores the cache in memory using the static attribute __CACHE

RedisCache(use_pickle, redis_instance, …)

A Redis backed implementation of CacheAdapter.

MemcachedCache(use_pickle, mcache_instance, …)

A Memcached backed implementation of CacheAdapter.

SqliteCache(db_file[, memory_persist])

An SQLite3 backed implementation of CacheAdapter.

AsyncIO Cache Adapters

AsyncIO-oriented cache adapters for Privex Helpers’ caching API.

All cache adapters in this module are designed to be used within AsyncIO functions/methods, as most methods (apart from the constructor) must be await’ed.

The AsyncIO cache adapters can be used with both sync/async code if they’re configured as the global cache adapter with privex.helpers.cache.adapter_set() - as the cache wrapper privex.helpers.cache.cached uses the decorator awaitable() to transparently run async functions/methods in an event loop when methods are called from non-async code, while returning the original async coroutine when called from asynchronous code.

Using AsyncIO cache adapters with global cache adapter

First import cached, adapter_set(), and the AsyncIO cache adapter(s) you want to set as the shared global adapter.

Create an instance of the cache adapter you want to use, and pass it to adapter_set like so:

>>> from privex.helpers.cache import cached, adapter_set, AsyncMemoryCache
>>>
>>> aio_mcache = AsyncMemoryCache()
>>> adapter_set(aio_mcache)         # Set the shared global adapter (cached) to an instance of AsyncMemoryCache
>>>

When using privex.helpers.cache.cached from a non-async context with an async adapter, you can call methods such as get and set as if they were normal synchronous methods - thanks to the decorator awaitable(). Example:

>>> # The variable 'cached' is a reference to a global shared instance of CacheWrapper, which proxies method calls
>>> # to the current global adapter set using 'adapter_set' (currently AsyncMemoryCache).
>>> # Thanks to '@awaitable' we can call the async method .set() from a non-async context without needing await
>>> cached.set('example', 'hello world')
>>> cached['example'] = 'hello world'
>>> print('synchronous REPL (cache "example" after):', cached['example'])
synchronous REPL (cache "example" after): hello world

When using cached from an asynchronous context (e.g. an async function/method), you should make sure to await any method calls - since when an asynchronous context is detected, the awaitable() decorator will return async co-routines which must be awaited, just like any async function:

>>> # While 'some_async_func' is in an async context, thus it await's method calls as they're plain co-routines
>>> async def some_async_func():
...     print('some_async_func (cache "example" before):', await cached.get('example'))
...     await cached.set('example', 'lorem ipsum')
...     print('some_async_func (cache "example" after):', await cached.get('example'))
...
>>> await some_async_func()
some_async_func (cache "example" before): hello world
some_async_func (cache "example" after): lorem ipsum

Available Cache Adapters

  • AsyncMemoryCache - Stores cache entries in your application’s memory using a plain dict. Dependency free.

  • AsyncRedisCache - Stores cache entries using a Redis server. Depends on the package aioredis

  • AsyncMemcachedCache - Stores cache entries using a Memcached server. Depends on the package aiomcache

AsyncRedisCache(use_pickle, redis_instance, …)

A Redis backed implementation of AsyncCacheAdapter.

AsyncMemoryCache(*args, enter_reconnect, …)

A very basic cache adapter which implements AsyncCacheAdapter - stores the cache in memory using the static attribute __CACHE

AsyncMemcachedCache(use_pickle, …)

A Memcached backed implementation of AsyncCacheAdapter.

AsyncSqliteCache(db_file[, memory_persist])

An SQLite3 backed implementation of AsyncCacheAdapter.

base

Core classes/functions used by AsyncIO Cache Adapters, including the base class AsyncCacheAdapter