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
This is the base class for all synchronous cache adapters (doesn’t do anything)
A cache adapter which stores cached items in memory using a dict. Fully functional incl. timeout.
A cache adapter for Memcached using the synchronous python library
pylibmc
A cache adapter for Redis using the python library
redis
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
This is the base class for all AsyncIO cache adapters (abstract class, only implements get_or_set)
A cache adapter which stores cached items in memory using a dict. Fully functional incl. timeout.
A cache adapter for Redis using the AsyncIO python library
aioredis
A cache adapter for Memcached using the AsyncIO python library
aiomcache
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:
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.
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!') >>>
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 implementsCacheAdapter
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 valuevalue
, and automatically expire the key aftertimeout
seconds from now.
update_timeout
(key[, timeout])Update the timeout for a given
key
todatetime.utcnow() + timedelta(seconds=timeout)
Attributes
This module attribute acts as a singleton, containing an instance of
CacheWrapper
which is designed to allow painless usage of this caching module.For applications/packages which are primarily AsyncIO,
CacheWrapper
can cause problems such as the commonevent loop is already running
- and unfortunately,nest_asyncio
isn’t always able to fix it.Classes
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 attributecache_instance
.For applications/packages which are primarily AsyncIO,
CacheWrapper
can cause problems such as the commonevent loop is already running
- and unfortunately,nest_asyncio
isn’t always able to fix it.
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 plaindict
. Dependency free.
AsyncRedisCache
- Stores cache entries using a Redis server. Depends on the packageaioredis
AsyncMemcachedCache
- Stores cache entries using a Memcached server. Depends on the packageaiomcache
|
A Redis backed implementation of |
|
A very basic cache adapter which implements |
|
A Memcached backed implementation of |
|
An SQLite3 backed implementation of |
Core classes/functions used by AsyncIO Cache Adapters, including the base class |