Name | redis-func-cache JSON |
Version |
0.2.1
JSON |
| download |
home_page | None |
Summary | A python library for caching function results in redis, like that in stdlib's functools |
upload_time | 2024-12-19 08:50:48 |
maintainer | None |
docs_url | None |
author | None |
requires_python | >=3.8 |
license | AGPLv3+ |
keywords |
redis
cache
decorate
decorator
|
VCS |
|
bugtrack_url |
|
requirements |
No requirements were recorded.
|
Travis-CI |
No Travis.
|
coveralls test coverage |
|
# redis_func_cache
[![python-package](https://github.com/tanbro/redis_func_cache/actions/workflows/python-package.yml/badge.svg)](https://github.com/tanbro/redis_func_cache/actions/workflows/python-package.yml)
[![codecov](https://codecov.io/gh/tanbro/redis_func_cache/graph/badge.svg?token=BgeXJZdPbJ)](https://codecov.io/gh/tanbro/redis_func_cache)
[![readthedocs](https://readthedocs.org/projects/redis-func-cache/badge/)](https://redis-func-cache.readthedocs.io/)
[![pypi-version](https://img.shields.io/pypi/v/redis_func_cache.svg)](https://pypi.org/project/redis_func_cache/)
`redis_func_cache` is a _Python_ library for caching function return values in [Redis][], similar to the caching functionality provided by the standard library's [`functools`](https://docs.python.org/library/functools.html) module, which comes with some cache decorators quite handy when we want to code something with memorization.
When we need to cache function return values distributed over multiple processes or machines, we can use [Redis][] as a backend.
The purpose of the project is to provide a simple and clean way to use [Redis][] as a backend for cache decorators.
It implements caches with _LRU_, _RR_, _FIFO_, _RR_ and _LFU_ eviction/replacement policies(<https://wikipedia.org/wiki/Cache_replacement_policies>).
## Features
- Supports multiple cache eviction policies: _LRU_, _FIFO_, _LFU_, _RR_ ...
- Asynchronous and synchronous support.
- Simple decorator syntax.
- Based on [redis-py][], the official Python client for [Redis][].
## Install
- install from PyPI:
```bash
pip install -U redis_func_cache
```
- install from source:
```bash
git clone https://github.com/tanbro/redis_func_cache.git
cd redis_func_cache
python setup.py install
```
## Data structure
The library combines a pair of [Redis][] data structures to manage cache data:
- The first one is a sorted set, which stores the hash values of the decorated function calls along with a score for each item.
When the cache reaches its maximum size, the score is used to determine which item to evict.
- The second one is a hash map, which stores the hash values of the function calls and their corresponding return values.
This can be visualized as follows:
![data_structure](images/data_structure.svg)
The main idea of eviction policy is that the cache keys are stored in a sorted set, and the cache values are stored in a hash map. Eviction is performed by removing the lowest-scoring item from the set, and then deleting the corresponding value from the hash map.
Here is an example showing how the _LRU_ cache's eviction policy works(maximum size is 3):
![eviction_example](images/eviction_example.svg)
The [`RedisFuncCache`][] executes a decorated function with specified arguments and cache its result. Here's a breakdown of the steps:
1. **Initialize Scripts**: Retrieve two Lua script objects for cache hitting and update from `policy.lua_scripts`.
1. **Calculate Keys and Hash**: Compute the cache keys using `policy.calc_keys`, compute the hash value using `policy.calc_hash`, and compute any additional arguments using `policy.calc_ext_args`.
1. **Attempt Cache Retrieval**: Attempt retrieving a cached result. If a cache hit occurs, deserialize and return the cached result.
1. **Execute User Function**: If no cache hit occurs, execute the decorated function with the provided arguments and keyword arguments.
1. **Serialize Result and Cache**: Serialize the result of the user function and store it in redis.
1. **Return Result**: Return the result of the decorated function.
```mermaid
flowchart TD
A[Start] --> B[Initialize Scripts]
B --> C{Scripts Valid?}
C -->|Invalid| D[Raise RuntimeError]
C -->|Valid| E[Calculate Keys and Hash]
E --> F[Attempt Cache Retrieval]
F --> G{Cache Hit?}
G -->|Yes| H[Deserialize and Return Cached Result]
G -->|No| I[Execute User Function]
I --> J[Serialize Result]
J --> K[Store in Cache]
K --> L[Return User Function Result]
```
## Basic Usage
### First example
Using _LRU_ cache to decorate a recursive Fibonacci function:
```python
from redis import Redis
from redis_func_cache import RedisFuncCache, TLruPolicy
redis_client = Redis(...)
lru_cache = RedisFuncCache("my-first-lru-cache", TLruPolicy, redis_client)
@lru_cache
def fib(n):
if n <= 1:
return n
if n == 2:
return 1
return fib(n - 1) + fib(n - 2)
```
In this example, we first create a [Redis][] client, then create a [`RedisFuncCache`][] instance with the [Redis][] client and [`TLruPolicy`][] as its arguments.
Next, we use the `@lru_cache` decorator to decorate the `fib` function.
This way, each computed result is cached, and subsequent calls with the same parameters retrieve the result directly from the cache, thereby improving performance.
It works almost the same as the standard library's `functools.lru_cache`, except that it uses [Redis][] as the backend instead of the local machine's memory.
If we browse the [Redis][] database, we can find the pair of keys' names look like:
- `func-cache:my-first-lru-cache:lru:__main__:fib:0`
The key (with `0` suffix) is a sorted set that stores the hash of function invoking and their corresponding scores.
- `func-cache:my-first-lru-cache:lru:__main__:fib:1`
The key (with `1` suffix) is a hash map. Each key field in it is the hash value of a function invoking, and the value filed is the return value of the function.
### Async functions
To decorate async functions, we shall pass a `Async Redis client` to [`RedisFuncCache`][]'s `client` argument:
```python
from redis.asyncio import Redis as AsyncRedis
from redis_func_cache import RedisFuncCache, TLruPolicy
my_async_cache = RedisFuncCache(__name__, TLruPolicy, AsyncRedis)
@my_async_cache
async def my_async_func(...):
...
```
> ⁉️ **Attention**\
> When a [`RedisFuncCache`][] is created with an async [Redis][] client, the cache can only be used to decorate async functions.
> These async functions will be decorated with an asynchronous wrapper, and the IO operations with [Redis][] will be performed asynchronously.
> Additionally, the normal synchronous [`RedisFuncCache`][] can only decorate normal synchronous functions, which will be decorated with a synchronous wrapper, and the IO operations with [Redis][] will be performed synchronously.
### Eviction policies
If want to use other eviction policies, you can specify another policy class as the second argument of [`RedisFuncCache`][].
For example, we use [`FifoPolicy`][] to implement a _FIFO_ cache:
```python
from redis_func_cache import FifoPolicy
fifo_cache = RedisFuncCache("my-cache-2", FifoPolicy, redis_client)
@fifo_cache
def func1(x):
...
```
Use [`RrPolicy`][] to implement a random-remove cache:
```python
from redis_func_cache import RrPolicy
rr_cache = RedisFuncCache("my-cache-3", RrPolicy, redis_client)
@rr_cache
def func2(x):
...
```
So far, the following cache eviction policies are available:
- **[`TLruPolicy`][]**
> 💡**Tip**:\
> It is a pseudo _LRU_ policy, not very serious/legitimate.
> The policy removes the lowest member according to the timestamp of invocation, and does not completely ensure eviction of the least recently used item, since the timestamp may be inaccurate.
> However, the policy is still **MOST RECOMMENDED** for common use. It is faster than the LRU policy and accurate enough for most cases.
- [`FifoPolicy`][]: first in first out
- [`LfuPolicy`][]: least frequently used
- [`LruPolicy`][]: least recently used
- [`MruPolicy`][]: most recently used
- [`RrPolicy`][]: random remove
> ℹ️ **Info**:\
> Explore source codes in directory `src/redis_func_cache/policies` for more details.
### Multiple [Redis][] key pairs
As described above, the cache keys are in a pair form. All decorated functions share the same two keys.
But some times, we may want a standalone key pair for each decorated function.
One solution is to use different [`RedisFuncCache`][] instances to decorate different functions.
Another way is to use a policy that stores cache data in different [Redis][] key pairs for each function. There are several policies to do that out of the box.
For example, we can use [`TLruMultiplePolicy`][] for a _LRU_ cache that has multiple different [Redis][] key pairs to store return values of different functions, and each function has a standalone keys pair:
```python
from redis_func_cache import TLruMultiplePolicy
cache = RedisFuncCache("my-cache-4", TLruMultiplePolicy, redis_client)
@cache
def func1(x):
...
@cache
def func2(x):
...
```
In the example, [`TLruMultiplePolicy`][] inherits [`BaseMultiplePolicy`][] which implements how to store cache keys and values for each function.
When called, we can see such keys in the [Redis][] database:
- key pair for `func1`:
- `func-cache:my-cache-4:tlru-m:__main__:func1#<hash1>:0`
- `func-cache:my-cache-4:tlru-m:__main__:func1#<hash1>:1`
- key pair for `func2`:
- `func-cache:my-cache-4:tlru-m:__main__:func2#<hash2>:0`
- `func-cache:my-cache-4:tlru-m:__main__:func2#<hash2>:1`
where `<hash1>` and `<hash2>` are the hash values of the definitions of `func1` and `func2` respectively.
Policies that store cache in multiple [Redis][] key pairs are:
- [`FifoMultiplePolicy`][]
- [`LfuMultiplePolicy`][]
- [`LruMultiplePolicy`][]
- [`MruMultiplePolicy`][]
- [`RrMultiplePolicy`][]
- [`TLruMultiplePolicy`][]
### [Redis][] Cluster support
We already known that the library implements cache algorithms based on a pair of [Redis][] data structures, the two **MUST** be in a same [Redis][] node, or it will not work correctly.
While a [Redis][] cluster will distribute keys to different nodes based on the hash value, we need to guarantee that two keys are placed on the same node. Several cluster policies are provided to achieve this. These policies use the `{...}` pattern in key names.
For example, here we use a [`TLruClusterPolicy`][] to implement a cluster-aware _LRU_ cache:
```python
from redis_func_cache import TLruClusterPolicy
cache = RedisFuncCache("my-cluster-cache", TLruClusterPolicy, redis_client)
@cache
def my_func(x):
...
```
Thus, the names of the key pair may be like:
- `func-cache:{my-cluster-cache:tlru-c}:0`
- `func-cache:{my-cluster-cache:tlru-c}:1`
Notice what is in `{...}`: the [Redis][] cluster will determine which node to use by the `{...}` pattern rather than the entire key string.
Policies that support cluster are:
- [`FifoClusterPolicy`][]
- [`LfuClusterPolicy`][]
- [`LruClusterPolicy`][]
- [`MruClusterPolicy`][]
- [`RrClusterPolicy`][]
- [`TLruClusterPolicy`][]
### [Redis][] Cluster support with multiple key pairs
Policies that support both cluster and store cache in multiple [Redis][] key pairs are:
- [`FifoClusterMultiplePolicy`][]
- [`LfuClusterMultiplePolicy`][]
- [`LruClusterMultiplePolicy`][]
- [`MruClusterMultiplePolicy`][]
- [`RrClusterMultiplePolicy`][]
- [`TLruClusterMultiplePolicy`][]
### Max size and expiration time
The [`RedisFuncCache`][] instance has two arguments to control the maximum size and expiration time of the cache:
- `maxsize`: the maximum number of items that the cache can hold.
When the cache reaches its `maxsize`, adding a new item will cause an existing cached item to be removed according to the eviction policy.
> ℹ️ **Note**:\
> For "multiple" policies, each decorated function has its own standalone data structure, so the value represents the maximum size of each individual data structure.
- `ttl`: The expiration time (in seconds) for the cache data structure.
The cache's [redis][] data structure will expire and be released after the specified time.
Each time the cache is accessed, the expiration time will be reset.
> ℹ️ **Note**:\
> For "multiple" policies, each decorated function has its own standalone data structure, so the `ttl` value represents the expiration time of each individual data structure. The expiration time will be reset each time the cache is accessed individually.
### Complex return types
The return value (de)serializer [JSON][] (`json` module of std-lib) by default, which does not work with complex objects.
But, still, we can use [`pickle`][] to serialize the return value, by specifying `serializers` argument of [`RedisFuncCache`][]:
```python
import pickle
from redis_func_cache import RedisFuncCache, TLruPolicy
def redis_factory():
...
my_pickle_cache = RedisFuncCache(
__name__,
TLruPolicy,
redis_factory,
serializer=(pickle.dumps, pickle.loads)
)
```
> ⚠️ **Warning**:\
> [`pickle`][] is considered a security risk, and should not be used with runtime/version sensitive data. Use it cautiously and only when necessary.
> It's a good practice to only cache functions that return simple, [JSON][] serializable data types.
Other serialization functions also should be workable, such as [simplejson](https://pypi.org/project/simplejson/), [cJSON](https://github.com/DaveGamble/cJSON), [msgpack](https://msgpack.org/), [cloudpickle](https://github.com/cloudpipe/cloudpickle), etc.
## Advanced Usage
### Custom key format
An instance of [`RedisFuncCache`][] calculate key pair names string by calling method `calc_keys` of it's policy.
There are four basic policies that implement respective kinds of key formats:
- [`BaseSinglePolicy`][]: All functions share the same key pair, [Redis][] cluster is NOT supported.
The format is: `<prefix><name>:<__key__>:<0|1>`
- [`BaseMultiplePolicy`][]: Each function has its own key pair, [Redis][] cluster is NOT supported.
The format is: `<prefix><name>:<__key__>:<function_name>#<function_hash>:<0|1>`
- [`BaseClusterSinglePolicy`][]: All functions share the same key pair, [Redis][] cluster is supported.
The format is: `<prefix>{<name>:<__key__>}:<0|1>`
- [`BaseClusterMultiplePolicy`][]: Each function has its own key pair, and [Redis][] cluster is supported.
The format is: `<prefix><name>:<__key__>:<function_name>#{<function_hash>}:<0|1>`
Variables in the format string are defined as follows:
| | |
| --------------- | ----------------------------------------------------------------- |
| `prefix` | `prefix` argument of [`RedisFuncCache`][] |
| `name` | `name` argument of [`RedisFuncCache`][] |
| `__key__` | `__key__` attribute the policy class used in [`RedisFuncCache`][] |
| `function_name` | full name of the decorated function |
| `function_hash` | hash value of the decorated function |
`0` and `1` at the end of the keys are used to distinguish between the two data structures:
- `0`: a sorted or unsorted set, used to store the hash value and sorting score of function invoking
- `1`: a hash table, used to store the return value of the function invoking
If want to use a different format, you can subclass [`AbstractPolicy`][] or any of above policy classes, and implement `calc_keys` method, then pass the custom policy class to [`RedisFuncCache`][].
The following example demonstrates how to custom key format for a _LRU_ policy:
```python
from typing import TYPE_CHECKING, Any, Callable, Mapping, Sequence, Tuple, override
from redis_func_cache import AbstractPolicy, RedisFuncCache
from redis_func_cache.mixins.hash import PickleMd5HashMixin
from redis_func_cache.mixins.policy import LruScriptsMixin
if TYPE_CHECKING:
from redis.typing import KeyT
def redis_factory():
...
MY_PREFIX = "my_prefix"
class MyPolicy(LruScriptsMixin, PickleMd5HashMixin, AbstractPolicy):
__key__ = "my_key"
@override
def calc_keys(
self, f: Callable|None = None, args: Sequence|None = None, kwds: Mapping[str, Any] | None= None
) -> Tuple[KeyT, KeyT]:
k = f"{self.cache.prefix}-{self.cache.name}-{f.__name__}-{self.__key__}"
return f"{k}-set", f"{k}-map"
my_cache = RedisFuncCache(name="my_cache", policy=MyPolicy, redis=redis_factory, prefix=MY_PREFIX)
@my_cache
def my_func(...):
...
```
In the example, we'll get a cache generates [redis][] keys separated by `-`, instead of `:`, prefixed by `"my-prefix"`, and suffixed by `"set"` and `"map"`, rather than `"0"` and `"1"`. The key pair names could be like `my_prefix-my_cache_func-my_key-set` and `my_prefix-my_cache_func-my_key-map`.
> ❗ **Important**:\
> The calculated key name **SHOULD** be unique.
`LruScriptsMixin` tells the policy which lua script to use, and `PickleMd5HashMixin` tells the policy to use [`pickle`][] to serialize and `md5` to calculate the hash value of the function.
### Custom Hash Algorithm
When the library performs a get or put action with [redis][], the hash value of the function invocation will be used.
For the sorted set data structures, the hash value will be used as the member. For the hash map data structure, the hash value will be used as the hash field.
The algorithm used to calculate the hash value is defined in `AbstractHashMixin`, it can be described as below:
```python
class AbstractHashMixin:
...
def calc_hash(
self, f: Optional[Callable] = None, args: Optional[Sequence] = None, kwds: Optional[Mapping[str, Any]] = None
) -> KeyT:
if not callable(f):
raise TypeError(f"Can not calculate hash for {f=}")
conf = self.__hash_config__
h = hashlib.new(conf.algorithm)
h.update(get_fullname(f).encode())
source = get_source(f)
if source is not None:
h.update(source.encode())
if args is not None:
h.update(conf.serializer(args))
if kwds is not None:
h.update(conf.serializer(kwds))
if conf.decoder is None:
return h.digest()
return conf.decoder(h)
```
As the code snippet above, the hash value is calculated by the full name of the function, the source code of the function, the arguments and keyword arguments --- they are serialized and hashed, then decoded.
The serializer and decoder are defined in `__hash_config__` attribute of the policy class, and they are used to serialize and decode the arguments and keyword arguments. The default serializer and decoder are [`pickle`][] and `md5` respectively. If no `decoder` is provided, the hash value will be returned as bytes.
If want to use a different algorithm, we can select a mixin hash class defined in `src/redis_func_cache/mixins/hash.py`.
For example:
```python
from redis_func_cache import AbstractHashMixin, RedisFuncCache
from redis_func_cache.mixins.hash import JsonSha1HexHashMixin
from redis_func_cache.mixins.policy import LruScriptsMixin
class MyLruPolicy(LruScriptsMixin, JsonSha1HexHashMixin, AbstractPolicy):
__key__ = "my-lru"
my_json_sha1_hex_cache = RedisFuncCache(name="json_sha1_hex", policy=MyLruPolicy, redis=redis_factory)
```
If want to use write a new algorithm, you can subclass [`AbstractHashMixin`][] and implement `calc_hash` method.
For example:
```python
from redis_func_cache import AbstractHashMixin, RedisFuncCache
from redis_func_cache.mixins.policy import LruScriptsMixin
def my_func_hash(...):
...
class MyHashMixin(AbstractHashMixin):
def calc_hash(self, f=None, args=None, kwds=None):
return my_func_hash(f, args, kwds)
class MyLruPolicy2(LruScriptsMixin, MyHashMixin, AbstractPolicy):
__key__ = "my-custom-hash-lru"
my_custom_hash_cache = RedisFuncCache(name=__name__, policy=MyLruPolicy2, redis=redis_factory)
@my_custom_hash_cache
def some_func(...):
...
```
## Known Issues
- Cannot decorate a function that has an argument not serializable by [`pickle`][] or other serialization libraries.
- For a common method defined inside a class, the class must be serializable; otherwise, the first `self` argument cannot be serialized.
- For a class method (decorated by `@classmethod`), the class type itself, i.e., the first `cls` argument, must be serializable.
- Compatibility with other decorators is not guaranteed.
- The cache eviction policies are mainly based on [Redis][] sorted set's score ordering. For most policies, the score is a positive integer. Its maximum value is `2^32-1` in [Redis][], which limits the number of times of eviction replacement. [Redis][] will return an `overflow` error when the score overflows.
- High concurrency or long-running decorated functions may result in unexpected cache misses and increased I/O operations. This can occur because the result value might not be saved quickly enough before the next call can hit the cache again.
- Generator functions are not supported.
- If there are multiple [`RedisFuncCache`][] instances with the same name, they may share the same cache data.
This may lead to serious errors, so we should avoid using the same name for different instances.
[redis]: https://redis.io/ "Redis is an in-memory data store used by millions of developers as a cache"
[redis-py]: https://redis.io/docs/develop/clients/redis-py/ "Connect your Python application to a Redis database"
[json]: https://www.json.org/ "JSON (JavaScript Object Notation) is a lightweight data-interchange format."
[`pickle`]: https://docs.python.org/library/pickle.html "The pickle module implements binary protocols for serializing and de-serializing a Python object structure."
[`RedisFuncCache`]: redis_func_cache.cache.RedisFuncCache
[`AbstractPolicy`]: redis_func_cache.policies.abstract.AbstractPolicy
[`BaseSinglePolicy`]: redis_func_cache.policies.base.BaseSinglePolicy
[`BaseMultiplePolicy`]: redis_func_cache.policies.base.BaseMultiplePolicy
[`BaseClusterSinglePolicy`]: redis_func_cache.policies.base.BaseClusterSinglePolicy
[`BaseClusterMultiplePolicy`]: redis_func_cache.policies.base.BaseClusterMultiplePolicy
[`FifoPolicy`]: redis_func_cache.policies.fifo.FifoPolicy "First In First Out policy"
[`LfuPolicy`]: redis_func_cache.policies.lfu.LfuPolicy "Least Frequently Used policy"
[`LruPolicy`]: redis_func_cache.policies.lru.LruPolicy "Least Recently Used policy"
[`MruPolicy`]: redis_func_cache.policies.mru.MruPolicy "Most Recently Used policy"
[`RrPolicy`]: redis_func_cache.policies.rr.RrPolicy "Random Remove policy"
[`TLruPolicy`]: redis_func_cache.policies.tlru.TLruPolicy "Time based Least Recently Used policy."
[`FifoMultiplePolicy`]: redis_func_cache.policies.fifo.FifoMultiplePolicy
[`LfuMultiplePolicy`]: redis_func_cache.policies.lfu.LfuMultiplePolicy
[`LruMultiplePolicy`]: redis_func_cache.policies.lru.LruMultiplePolicy
[`MruMultiplePolicy`]: redis_func_cache.policies.mru.MruMultiplePolicy
[`RrMultiplePolicy`]: redis_func_cache.policies.rr.RrMultiplePolicy
[`TLruMultiplePolicy`]: redis_func_cache.policies.tlru.TLruMultiplePolicy
[`FifoClusterPolicy`]: redis_func_cache.policies.fifo.FifoClusterPolicy
[`LfuClusterPolicy`]: redis_func_cache.policies.lfu.LfuClusterPolicy
[`LruClusterPolicy`]: redis_func_cache.policies.lru.LruClusterPolicy
[`MruClusterPolicy`]: redis_func_cache.policies.mru.MruClusterPolicy
[`RrClusterPolicy`]: redis_func_cache.policies.rr.RrClusterPolicy
[`TLruClusterPolicy`]: redis_func_cache.policies.tlru.TLruClusterPolicy
[`FifoClusterMultiplePolicy`]: redis_func_cache.policies.fifo.FifoClusterMultiplePolicy
[`LfuClusterMultiplePolicy`]: redis_func_cache.policies.lfu.LfuClusterMultiplePolicy
[`LruClusterMultiplePolicy`]: redis_func_cache.policies.lru.LruClusterMultiplePolicy
[`MruClusterMultiplePolicy`]: redis_func_cache.policies.mru.MruClusterMultiplePolicy
[`RrClusterMultiplePolicy`]: redis_func_cache.policies.rr.RrClusterMultiplePolicy
[`TLruClusterMultiplePolicy`]: redis_func_cache.policies.tlru.TLruClusterMultiplePolicy
Raw data
{
"_id": null,
"home_page": null,
"name": "redis-func-cache",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.8",
"maintainer_email": null,
"keywords": "redis, cache, decorate, decorator",
"author": null,
"author_email": "liu xue yan <liu_xue_yan@foxmail.com>",
"download_url": "https://files.pythonhosted.org/packages/e7/49/d70a08a7b160a245c1275b9f1f7aa418baa91578b8c1454fd0410c8873fd/redis_func_cache-0.2.1.tar.gz",
"platform": null,
"description": "# redis_func_cache\n\n[![python-package](https://github.com/tanbro/redis_func_cache/actions/workflows/python-package.yml/badge.svg)](https://github.com/tanbro/redis_func_cache/actions/workflows/python-package.yml)\n[![codecov](https://codecov.io/gh/tanbro/redis_func_cache/graph/badge.svg?token=BgeXJZdPbJ)](https://codecov.io/gh/tanbro/redis_func_cache)\n[![readthedocs](https://readthedocs.org/projects/redis-func-cache/badge/)](https://redis-func-cache.readthedocs.io/)\n[![pypi-version](https://img.shields.io/pypi/v/redis_func_cache.svg)](https://pypi.org/project/redis_func_cache/)\n\n`redis_func_cache` is a _Python_ library for caching function return values in [Redis][], similar to the caching functionality provided by the standard library's [`functools`](https://docs.python.org/library/functools.html) module, which comes with some cache decorators quite handy when we want to code something with memorization.\n\nWhen we need to cache function return values distributed over multiple processes or machines, we can use [Redis][] as a backend.\nThe purpose of the project is to provide a simple and clean way to use [Redis][] as a backend for cache decorators.\nIt implements caches with _LRU_, _RR_, _FIFO_, _RR_ and _LFU_ eviction/replacement policies(<https://wikipedia.org/wiki/Cache_replacement_policies>).\n\n## Features\n\n- Supports multiple cache eviction policies: _LRU_, _FIFO_, _LFU_, _RR_ ...\n- Asynchronous and synchronous support.\n- Simple decorator syntax.\n- Based on [redis-py][], the official Python client for [Redis][].\n\n## Install\n\n- install from PyPI:\n\n ```bash\n pip install -U redis_func_cache\n ```\n\n- install from source:\n\n ```bash\n git clone https://github.com/tanbro/redis_func_cache.git\n cd redis_func_cache\n python setup.py install\n ```\n\n## Data structure\n\nThe library combines a pair of [Redis][] data structures to manage cache data:\n\n- The first one is a sorted set, which stores the hash values of the decorated function calls along with a score for each item.\n\n When the cache reaches its maximum size, the score is used to determine which item to evict.\n\n- The second one is a hash map, which stores the hash values of the function calls and their corresponding return values.\n\nThis can be visualized as follows:\n\n![data_structure](images/data_structure.svg)\n\nThe main idea of eviction policy is that the cache keys are stored in a sorted set, and the cache values are stored in a hash map. Eviction is performed by removing the lowest-scoring item from the set, and then deleting the corresponding value from the hash map.\n\nHere is an example showing how the _LRU_ cache's eviction policy works(maximum size is 3):\n\n![eviction_example](images/eviction_example.svg)\n\nThe [`RedisFuncCache`][] executes a decorated function with specified arguments and cache its result. Here's a breakdown of the steps:\n\n1. **Initialize Scripts**: Retrieve two Lua script objects for cache hitting and update from `policy.lua_scripts`.\n1. **Calculate Keys and Hash**: Compute the cache keys using `policy.calc_keys`, compute the hash value using `policy.calc_hash`, and compute any additional arguments using `policy.calc_ext_args`.\n1. **Attempt Cache Retrieval**: Attempt retrieving a cached result. If a cache hit occurs, deserialize and return the cached result.\n1. **Execute User Function**: If no cache hit occurs, execute the decorated function with the provided arguments and keyword arguments.\n1. **Serialize Result and Cache**: Serialize the result of the user function and store it in redis.\n1. **Return Result**: Return the result of the decorated function.\n\n```mermaid\nflowchart TD\n A[Start] --> B[Initialize Scripts]\n B --> C{Scripts Valid?}\n C -->|Invalid| D[Raise RuntimeError]\n C -->|Valid| E[Calculate Keys and Hash]\n E --> F[Attempt Cache Retrieval]\n F --> G{Cache Hit?}\n G -->|Yes| H[Deserialize and Return Cached Result]\n G -->|No| I[Execute User Function]\n I --> J[Serialize Result]\n J --> K[Store in Cache]\n K --> L[Return User Function Result]\n```\n\n## Basic Usage\n\n### First example\n\nUsing _LRU_ cache to decorate a recursive Fibonacci function:\n\n```python\nfrom redis import Redis\nfrom redis_func_cache import RedisFuncCache, TLruPolicy\n\nredis_client = Redis(...)\n\nlru_cache = RedisFuncCache(\"my-first-lru-cache\", TLruPolicy, redis_client)\n\n@lru_cache\ndef fib(n):\n if n <= 1:\n return n\n if n == 2:\n return 1\n return fib(n - 1) + fib(n - 2)\n```\n\nIn this example, we first create a [Redis][] client, then create a [`RedisFuncCache`][] instance with the [Redis][] client and [`TLruPolicy`][] as its arguments.\nNext, we use the `@lru_cache` decorator to decorate the `fib` function.\nThis way, each computed result is cached, and subsequent calls with the same parameters retrieve the result directly from the cache, thereby improving performance.\n\nIt works almost the same as the standard library's `functools.lru_cache`, except that it uses [Redis][] as the backend instead of the local machine's memory.\n\nIf we browse the [Redis][] database, we can find the pair of keys' names look like:\n\n- `func-cache:my-first-lru-cache:lru:__main__:fib:0`\n\n The key (with `0` suffix) is a sorted set that stores the hash of function invoking and their corresponding scores.\n\n- `func-cache:my-first-lru-cache:lru:__main__:fib:1`\n\n The key (with `1` suffix) is a hash map. Each key field in it is the hash value of a function invoking, and the value filed is the return value of the function.\n\n### Async functions\n\nTo decorate async functions, we shall pass a `Async Redis client` to [`RedisFuncCache`][]'s `client` argument:\n\n```python\nfrom redis.asyncio import Redis as AsyncRedis\nfrom redis_func_cache import RedisFuncCache, TLruPolicy\n\nmy_async_cache = RedisFuncCache(__name__, TLruPolicy, AsyncRedis)\n\n@my_async_cache\nasync def my_async_func(...):\n ...\n```\n\n> \u2049\ufe0f **Attention**\\\n> When a [`RedisFuncCache`][] is created with an async [Redis][] client, the cache can only be used to decorate async functions.\n> These async functions will be decorated with an asynchronous wrapper, and the IO operations with [Redis][] will be performed asynchronously.\n> Additionally, the normal synchronous [`RedisFuncCache`][] can only decorate normal synchronous functions, which will be decorated with a synchronous wrapper, and the IO operations with [Redis][] will be performed synchronously.\n\n### Eviction policies\n\nIf want to use other eviction policies, you can specify another policy class as the second argument of [`RedisFuncCache`][].\n\nFor example, we use [`FifoPolicy`][] to implement a _FIFO_ cache:\n\n```python\nfrom redis_func_cache import FifoPolicy\n\nfifo_cache = RedisFuncCache(\"my-cache-2\", FifoPolicy, redis_client)\n\n@fifo_cache\ndef func1(x):\n ...\n```\n\nUse [`RrPolicy`][] to implement a random-remove cache:\n\n```python\nfrom redis_func_cache import RrPolicy\n\nrr_cache = RedisFuncCache(\"my-cache-3\", RrPolicy, redis_client)\n\n@rr_cache\ndef func2(x):\n ...\n```\n\nSo far, the following cache eviction policies are available:\n\n- **[`TLruPolicy`][]**\n\n > \ud83d\udca1**Tip**:\\\n > It is a pseudo _LRU_ policy, not very serious/legitimate.\n > The policy removes the lowest member according to the timestamp of invocation, and does not completely ensure eviction of the least recently used item, since the timestamp may be inaccurate.\n > However, the policy is still **MOST RECOMMENDED** for common use. It is faster than the LRU policy and accurate enough for most cases.\n\n- [`FifoPolicy`][]: first in first out\n- [`LfuPolicy`][]: least frequently used\n- [`LruPolicy`][]: least recently used\n- [`MruPolicy`][]: most recently used\n- [`RrPolicy`][]: random remove\n\n> \u2139\ufe0f **Info**:\\\n> Explore source codes in directory `src/redis_func_cache/policies` for more details.\n\n### Multiple [Redis][] key pairs\n\nAs described above, the cache keys are in a pair form. All decorated functions share the same two keys.\nBut some times, we may want a standalone key pair for each decorated function.\n\nOne solution is to use different [`RedisFuncCache`][] instances to decorate different functions.\n\nAnother way is to use a policy that stores cache data in different [Redis][] key pairs for each function. There are several policies to do that out of the box.\nFor example, we can use [`TLruMultiplePolicy`][] for a _LRU_ cache that has multiple different [Redis][] key pairs to store return values of different functions, and each function has a standalone keys pair:\n\n```python\nfrom redis_func_cache import TLruMultiplePolicy\n\ncache = RedisFuncCache(\"my-cache-4\", TLruMultiplePolicy, redis_client)\n\n@cache\ndef func1(x):\n ...\n\n@cache\ndef func2(x):\n ...\n```\n\nIn the example, [`TLruMultiplePolicy`][] inherits [`BaseMultiplePolicy`][] which implements how to store cache keys and values for each function.\n\nWhen called, we can see such keys in the [Redis][] database:\n\n- key pair for `func1`:\n\n - `func-cache:my-cache-4:tlru-m:__main__:func1#<hash1>:0`\n - `func-cache:my-cache-4:tlru-m:__main__:func1#<hash1>:1`\n\n- key pair for `func2`:\n\n - `func-cache:my-cache-4:tlru-m:__main__:func2#<hash2>:0`\n - `func-cache:my-cache-4:tlru-m:__main__:func2#<hash2>:1`\n\nwhere `<hash1>` and `<hash2>` are the hash values of the definitions of `func1` and `func2` respectively.\n\nPolicies that store cache in multiple [Redis][] key pairs are:\n\n- [`FifoMultiplePolicy`][]\n- [`LfuMultiplePolicy`][]\n- [`LruMultiplePolicy`][]\n- [`MruMultiplePolicy`][]\n- [`RrMultiplePolicy`][]\n- [`TLruMultiplePolicy`][]\n\n### [Redis][] Cluster support\n\nWe already known that the library implements cache algorithms based on a pair of [Redis][] data structures, the two **MUST** be in a same [Redis][] node, or it will not work correctly.\n\nWhile a [Redis][] cluster will distribute keys to different nodes based on the hash value, we need to guarantee that two keys are placed on the same node. Several cluster policies are provided to achieve this. These policies use the `{...}` pattern in key names.\n\nFor example, here we use a [`TLruClusterPolicy`][] to implement a cluster-aware _LRU_ cache:\n\n```python\nfrom redis_func_cache import TLruClusterPolicy\n\ncache = RedisFuncCache(\"my-cluster-cache\", TLruClusterPolicy, redis_client)\n\n@cache\ndef my_func(x):\n ...\n```\n\nThus, the names of the key pair may be like:\n\n- `func-cache:{my-cluster-cache:tlru-c}:0`\n- `func-cache:{my-cluster-cache:tlru-c}:1`\n\nNotice what is in `{...}`: the [Redis][] cluster will determine which node to use by the `{...}` pattern rather than the entire key string.\n\nPolicies that support cluster are:\n\n- [`FifoClusterPolicy`][]\n- [`LfuClusterPolicy`][]\n- [`LruClusterPolicy`][]\n- [`MruClusterPolicy`][]\n- [`RrClusterPolicy`][]\n- [`TLruClusterPolicy`][]\n\n### [Redis][] Cluster support with multiple key pairs\n\nPolicies that support both cluster and store cache in multiple [Redis][] key pairs are:\n\n- [`FifoClusterMultiplePolicy`][]\n- [`LfuClusterMultiplePolicy`][]\n- [`LruClusterMultiplePolicy`][]\n- [`MruClusterMultiplePolicy`][]\n- [`RrClusterMultiplePolicy`][]\n- [`TLruClusterMultiplePolicy`][]\n\n### Max size and expiration time\n\nThe [`RedisFuncCache`][] instance has two arguments to control the maximum size and expiration time of the cache:\n\n- `maxsize`: the maximum number of items that the cache can hold.\n\n When the cache reaches its `maxsize`, adding a new item will cause an existing cached item to be removed according to the eviction policy.\n\n > \u2139\ufe0f **Note**:\\\n > For \"multiple\" policies, each decorated function has its own standalone data structure, so the value represents the maximum size of each individual data structure.\n\n- `ttl`: The expiration time (in seconds) for the cache data structure.\n\n The cache's [redis][] data structure will expire and be released after the specified time.\n Each time the cache is accessed, the expiration time will be reset.\n\n > \u2139\ufe0f **Note**:\\\n > For \"multiple\" policies, each decorated function has its own standalone data structure, so the `ttl` value represents the expiration time of each individual data structure. The expiration time will be reset each time the cache is accessed individually.\n\n### Complex return types\n\nThe return value (de)serializer [JSON][] (`json` module of std-lib) by default, which does not work with complex objects.\n\nBut, still, we can use [`pickle`][] to serialize the return value, by specifying `serializers` argument of [`RedisFuncCache`][]:\n\n```python\nimport pickle\n\nfrom redis_func_cache import RedisFuncCache, TLruPolicy\n\n\ndef redis_factory():\n ...\n\n\nmy_pickle_cache = RedisFuncCache(\n __name__,\n TLruPolicy,\n redis_factory,\n serializer=(pickle.dumps, pickle.loads)\n)\n```\n\n> \u26a0\ufe0f **Warning**:\\\n> [`pickle`][] is considered a security risk, and should not be used with runtime/version sensitive data. Use it cautiously and only when necessary.\n> It's a good practice to only cache functions that return simple, [JSON][] serializable data types.\n\nOther serialization functions also should be workable, such as [simplejson](https://pypi.org/project/simplejson/), [cJSON](https://github.com/DaveGamble/cJSON), [msgpack](https://msgpack.org/), [cloudpickle](https://github.com/cloudpipe/cloudpickle), etc.\n\n## Advanced Usage\n\n### Custom key format\n\nAn instance of [`RedisFuncCache`][] calculate key pair names string by calling method `calc_keys` of it's policy.\nThere are four basic policies that implement respective kinds of key formats:\n\n- [`BaseSinglePolicy`][]: All functions share the same key pair, [Redis][] cluster is NOT supported.\n\n The format is: `<prefix><name>:<__key__>:<0|1>`\n\n- [`BaseMultiplePolicy`][]: Each function has its own key pair, [Redis][] cluster is NOT supported.\n\n The format is: `<prefix><name>:<__key__>:<function_name>#<function_hash>:<0|1>`\n\n- [`BaseClusterSinglePolicy`][]: All functions share the same key pair, [Redis][] cluster is supported.\n\n The format is: `<prefix>{<name>:<__key__>}:<0|1>`\n\n- [`BaseClusterMultiplePolicy`][]: Each function has its own key pair, and [Redis][] cluster is supported.\n\n The format is: `<prefix><name>:<__key__>:<function_name>#{<function_hash>}:<0|1>`\n\nVariables in the format string are defined as follows:\n\n| | |\n| --------------- | ----------------------------------------------------------------- |\n| `prefix` | `prefix` argument of [`RedisFuncCache`][] |\n| `name` | `name` argument of [`RedisFuncCache`][] |\n| `__key__` | `__key__` attribute the policy class used in [`RedisFuncCache`][] |\n| `function_name` | full name of the decorated function |\n| `function_hash` | hash value of the decorated function |\n\n`0` and `1` at the end of the keys are used to distinguish between the two data structures:\n\n- `0`: a sorted or unsorted set, used to store the hash value and sorting score of function invoking\n- `1`: a hash table, used to store the return value of the function invoking\n\nIf want to use a different format, you can subclass [`AbstractPolicy`][] or any of above policy classes, and implement `calc_keys` method, then pass the custom policy class to [`RedisFuncCache`][].\n\nThe following example demonstrates how to custom key format for a _LRU_ policy:\n\n```python\nfrom typing import TYPE_CHECKING, Any, Callable, Mapping, Sequence, Tuple, override\n\nfrom redis_func_cache import AbstractPolicy, RedisFuncCache\nfrom redis_func_cache.mixins.hash import PickleMd5HashMixin\nfrom redis_func_cache.mixins.policy import LruScriptsMixin\n\nif TYPE_CHECKING:\n from redis.typing import KeyT\n\n\ndef redis_factory():\n ...\n\n\nMY_PREFIX = \"my_prefix\"\n\n\nclass MyPolicy(LruScriptsMixin, PickleMd5HashMixin, AbstractPolicy):\n __key__ = \"my_key\"\n\n @override\n def calc_keys(\n self, f: Callable|None = None, args: Sequence|None = None, kwds: Mapping[str, Any] | None= None\n ) -> Tuple[KeyT, KeyT]:\n k = f\"{self.cache.prefix}-{self.cache.name}-{f.__name__}-{self.__key__}\"\n return f\"{k}-set\", f\"{k}-map\"\n\nmy_cache = RedisFuncCache(name=\"my_cache\", policy=MyPolicy, redis=redis_factory, prefix=MY_PREFIX)\n\n@my_cache\ndef my_func(...):\n ...\n```\n\nIn the example, we'll get a cache generates [redis][] keys separated by `-`, instead of `:`, prefixed by `\"my-prefix\"`, and suffixed by `\"set\"` and `\"map\"`, rather than `\"0\"` and `\"1\"`. The key pair names could be like `my_prefix-my_cache_func-my_key-set` and `my_prefix-my_cache_func-my_key-map`.\n\n> \u2757 **Important**:\\\n> The calculated key name **SHOULD** be unique.\n\n`LruScriptsMixin` tells the policy which lua script to use, and `PickleMd5HashMixin` tells the policy to use [`pickle`][] to serialize and `md5` to calculate the hash value of the function.\n\n### Custom Hash Algorithm\n\nWhen the library performs a get or put action with [redis][], the hash value of the function invocation will be used.\n\nFor the sorted set data structures, the hash value will be used as the member. For the hash map data structure, the hash value will be used as the hash field.\n\nThe algorithm used to calculate the hash value is defined in `AbstractHashMixin`, it can be described as below:\n\n```python\nclass AbstractHashMixin:\n\n ...\n\n def calc_hash(\n self, f: Optional[Callable] = None, args: Optional[Sequence] = None, kwds: Optional[Mapping[str, Any]] = None\n ) -> KeyT:\n if not callable(f):\n raise TypeError(f\"Can not calculate hash for {f=}\")\n conf = self.__hash_config__\n h = hashlib.new(conf.algorithm)\n h.update(get_fullname(f).encode())\n source = get_source(f)\n if source is not None:\n h.update(source.encode())\n if args is not None:\n h.update(conf.serializer(args))\n if kwds is not None:\n h.update(conf.serializer(kwds))\n if conf.decoder is None:\n return h.digest()\n return conf.decoder(h)\n```\n\nAs the code snippet above, the hash value is calculated by the full name of the function, the source code of the function, the arguments and keyword arguments --- they are serialized and hashed, then decoded.\n\nThe serializer and decoder are defined in `__hash_config__` attribute of the policy class, and they are used to serialize and decode the arguments and keyword arguments. The default serializer and decoder are [`pickle`][] and `md5` respectively. If no `decoder` is provided, the hash value will be returned as bytes.\n\nIf want to use a different algorithm, we can select a mixin hash class defined in `src/redis_func_cache/mixins/hash.py`.\nFor example:\n\n```python\nfrom redis_func_cache import AbstractHashMixin, RedisFuncCache\nfrom redis_func_cache.mixins.hash import JsonSha1HexHashMixin\nfrom redis_func_cache.mixins.policy import LruScriptsMixin\n\n\nclass MyLruPolicy(LruScriptsMixin, JsonSha1HexHashMixin, AbstractPolicy):\n __key__ = \"my-lru\"\n\nmy_json_sha1_hex_cache = RedisFuncCache(name=\"json_sha1_hex\", policy=MyLruPolicy, redis=redis_factory)\n```\n\nIf want to use write a new algorithm, you can subclass [`AbstractHashMixin`][] and implement `calc_hash` method.\nFor example:\n\n```python\nfrom redis_func_cache import AbstractHashMixin, RedisFuncCache\nfrom redis_func_cache.mixins.policy import LruScriptsMixin\n\ndef my_func_hash(...):\n ...\n\n\nclass MyHashMixin(AbstractHashMixin):\n def calc_hash(self, f=None, args=None, kwds=None):\n return my_func_hash(f, args, kwds)\n\n\nclass MyLruPolicy2(LruScriptsMixin, MyHashMixin, AbstractPolicy):\n __key__ = \"my-custom-hash-lru\"\n\n\nmy_custom_hash_cache = RedisFuncCache(name=__name__, policy=MyLruPolicy2, redis=redis_factory)\n\n\n@my_custom_hash_cache\ndef some_func(...):\n ...\n```\n\n## Known Issues\n\n- Cannot decorate a function that has an argument not serializable by [`pickle`][] or other serialization libraries.\n\n - For a common method defined inside a class, the class must be serializable; otherwise, the first `self` argument cannot be serialized.\n - For a class method (decorated by `@classmethod`), the class type itself, i.e., the first `cls` argument, must be serializable.\n\n- Compatibility with other decorators is not guaranteed.\n\n- The cache eviction policies are mainly based on [Redis][] sorted set's score ordering. For most policies, the score is a positive integer. Its maximum value is `2^32-1` in [Redis][], which limits the number of times of eviction replacement. [Redis][] will return an `overflow` error when the score overflows.\n\n- High concurrency or long-running decorated functions may result in unexpected cache misses and increased I/O operations. This can occur because the result value might not be saved quickly enough before the next call can hit the cache again.\n\n- Generator functions are not supported.\n\n- If there are multiple [`RedisFuncCache`][] instances with the same name, they may share the same cache data.\n This may lead to serious errors, so we should avoid using the same name for different instances.\n\n[redis]: https://redis.io/ \"Redis is an in-memory data store used by millions of developers as a cache\"\n[redis-py]: https://redis.io/docs/develop/clients/redis-py/ \"Connect your Python application to a Redis database\"\n\n[json]: https://www.json.org/ \"JSON (JavaScript Object Notation) is a lightweight data-interchange format.\"\n[`pickle`]: https://docs.python.org/library/pickle.html \"The pickle module implements binary protocols for serializing and de-serializing a Python object structure.\"\n\n[`RedisFuncCache`]: redis_func_cache.cache.RedisFuncCache\n[`AbstractPolicy`]: redis_func_cache.policies.abstract.AbstractPolicy\n\n[`BaseSinglePolicy`]: redis_func_cache.policies.base.BaseSinglePolicy\n[`BaseMultiplePolicy`]: redis_func_cache.policies.base.BaseMultiplePolicy\n[`BaseClusterSinglePolicy`]: redis_func_cache.policies.base.BaseClusterSinglePolicy\n[`BaseClusterMultiplePolicy`]: redis_func_cache.policies.base.BaseClusterMultiplePolicy\n\n[`FifoPolicy`]: redis_func_cache.policies.fifo.FifoPolicy \"First In First Out policy\"\n[`LfuPolicy`]: redis_func_cache.policies.lfu.LfuPolicy \"Least Frequently Used policy\"\n[`LruPolicy`]: redis_func_cache.policies.lru.LruPolicy \"Least Recently Used policy\"\n[`MruPolicy`]: redis_func_cache.policies.mru.MruPolicy \"Most Recently Used policy\"\n[`RrPolicy`]: redis_func_cache.policies.rr.RrPolicy \"Random Remove policy\"\n[`TLruPolicy`]: redis_func_cache.policies.tlru.TLruPolicy \"Time based Least Recently Used policy.\"\n\n[`FifoMultiplePolicy`]: redis_func_cache.policies.fifo.FifoMultiplePolicy\n[`LfuMultiplePolicy`]: redis_func_cache.policies.lfu.LfuMultiplePolicy\n[`LruMultiplePolicy`]: redis_func_cache.policies.lru.LruMultiplePolicy\n[`MruMultiplePolicy`]: redis_func_cache.policies.mru.MruMultiplePolicy\n[`RrMultiplePolicy`]: redis_func_cache.policies.rr.RrMultiplePolicy\n[`TLruMultiplePolicy`]: redis_func_cache.policies.tlru.TLruMultiplePolicy\n\n[`FifoClusterPolicy`]: redis_func_cache.policies.fifo.FifoClusterPolicy\n[`LfuClusterPolicy`]: redis_func_cache.policies.lfu.LfuClusterPolicy\n[`LruClusterPolicy`]: redis_func_cache.policies.lru.LruClusterPolicy\n[`MruClusterPolicy`]: redis_func_cache.policies.mru.MruClusterPolicy\n[`RrClusterPolicy`]: redis_func_cache.policies.rr.RrClusterPolicy\n[`TLruClusterPolicy`]: redis_func_cache.policies.tlru.TLruClusterPolicy\n\n[`FifoClusterMultiplePolicy`]: redis_func_cache.policies.fifo.FifoClusterMultiplePolicy\n[`LfuClusterMultiplePolicy`]: redis_func_cache.policies.lfu.LfuClusterMultiplePolicy\n[`LruClusterMultiplePolicy`]: redis_func_cache.policies.lru.LruClusterMultiplePolicy\n[`MruClusterMultiplePolicy`]: redis_func_cache.policies.mru.MruClusterMultiplePolicy\n[`RrClusterMultiplePolicy`]: redis_func_cache.policies.rr.RrClusterMultiplePolicy\n[`TLruClusterMultiplePolicy`]: redis_func_cache.policies.tlru.TLruClusterMultiplePolicy\n",
"bugtrack_url": null,
"license": "AGPLv3+",
"summary": "A python library for caching function results in redis, like that in stdlib's functools",
"version": "0.2.1",
"project_urls": {
"Bug Tracker": "https://github.com/tanbro/redis_func_cache/issues",
"Changelog": "https://github.com/tanbro/spam/redis_func_cache/main/CHANGELOG.md",
"Documentation": "https://redis-func-cache.readthedocs.io/",
"Homepage": "https://github.com/tanbro/redis_func_cache",
"Repository": "https://github.com/tanbro/redis_func_cache"
},
"split_keywords": [
"redis",
" cache",
" decorate",
" decorator"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "a1165b562cd3acf114f953ba7f5c7745eaf1209dc548666ef870048b68ae17a5",
"md5": "7ab376a11492583c7497ec12fa48db9b",
"sha256": "878e1a885658ff86262b3013f4dcd326510fa0de31f1408ad84e33472f183d6d"
},
"downloads": -1,
"filename": "redis_func_cache-0.2.1-py3-none-any.whl",
"has_sig": false,
"md5_digest": "7ab376a11492583c7497ec12fa48db9b",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.8",
"size": 30065,
"upload_time": "2024-12-19T08:50:47",
"upload_time_iso_8601": "2024-12-19T08:50:47.394418Z",
"url": "https://files.pythonhosted.org/packages/a1/16/5b562cd3acf114f953ba7f5c7745eaf1209dc548666ef870048b68ae17a5/redis_func_cache-0.2.1-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "e749d70a08a7b160a245c1275b9f1f7aa418baa91578b8c1454fd0410c8873fd",
"md5": "097032c5390e6755b2ca1c2c545c28c3",
"sha256": "8eb2c20643ec9a4ae0092c8be9d73473cbc0d753d4d819692f1a2dd114bd2891"
},
"downloads": -1,
"filename": "redis_func_cache-0.2.1.tar.gz",
"has_sig": false,
"md5_digest": "097032c5390e6755b2ca1c2c545c28c3",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.8",
"size": 32489,
"upload_time": "2024-12-19T08:50:48",
"upload_time_iso_8601": "2024-12-19T08:50:48.576345Z",
"url": "https://files.pythonhosted.org/packages/e7/49/d70a08a7b160a245c1275b9f1f7aa418baa91578b8c1454fd0410c8873fd/redis_func_cache-0.2.1.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-12-19 08:50:48",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "tanbro",
"github_project": "redis_func_cache",
"travis_ci": false,
"coveralls": true,
"github_actions": true,
"requirements": [],
"lcname": "redis-func-cache"
}