rediscache 0.3.2__py3-none-any.whl → 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rediscache/__init__.py +23 -77
- {rediscache-0.3.2.dist-info → rediscache-1.0.0.dist-info}/METADATA +8 -23
- rediscache-1.0.0.dist-info/RECORD +6 -0
- rediscache-0.3.2.dist-info/RECORD +0 -6
- {rediscache-0.3.2.dist-info → rediscache-1.0.0.dist-info}/LICENSE +0 -0
- {rediscache-0.3.2.dist-info → rediscache-1.0.0.dist-info}/WHEEL +0 -0
rediscache/__init__.py
CHANGED
|
@@ -10,10 +10,10 @@ rediscache is a function decorator that will cache its result in a Redis databas
|
|
|
10
10
|
|
|
11
11
|
Parameters:
|
|
12
12
|
- refresh: the number of seconds after which the cached data will be refreshed using the decorated function.
|
|
13
|
-
- expire: the number of seconds after which the cached data will altogether
|
|
13
|
+
- expire: the number of seconds after which the cached data will altogether disappear from the Redis database.
|
|
14
14
|
- default: the value that will be returned if the data is not found in the cache. It cannot be None.
|
|
15
15
|
It can be bytes, string, int or float.
|
|
16
|
-
- enabled: This is True by default but enables to programmatically disable the
|
|
16
|
+
- enabled: This is True by default but enables to programmatically disable the cache if required.
|
|
17
17
|
|
|
18
18
|
It also reads from the environment:
|
|
19
19
|
- REDIS_SERVICE_HOST: the redis server host name or IP. Default is 'localhost'.
|
|
@@ -23,7 +23,7 @@ It also reads from the environment:
|
|
|
23
23
|
|
|
24
24
|
Note:
|
|
25
25
|
A key associated to the decorated function is created using the function name and its parameters. This is based
|
|
26
|
-
on the value returned by the repr() function (ie: the __repr__() member) of each
|
|
26
|
+
on the value returned by the repr() function (ie: the __repr__() member) of each parameter. User defined objects
|
|
27
27
|
will have this function return by default a string like this:
|
|
28
28
|
"<amadeusbook.services.EmployeesService object at 0x7f41dedd7128>"
|
|
29
29
|
This will not do as each instance of the object will have a different representation no matter what. The direct
|
|
@@ -40,10 +40,10 @@ import logging
|
|
|
40
40
|
import os
|
|
41
41
|
import threading
|
|
42
42
|
from time import sleep
|
|
43
|
-
from typing import Any, Callable, Dict, List, Optional, ParamSpec, TypeVar
|
|
43
|
+
from typing import Any, Callable, cast, Dict, List, Optional, ParamSpec, TypeVar
|
|
44
44
|
|
|
45
|
-
import redis
|
|
46
45
|
from executiontime import printexecutiontime, YELLOW, RED
|
|
46
|
+
import redis
|
|
47
47
|
|
|
48
48
|
PREFIX = "."
|
|
49
49
|
REFRESH = "Refresh" # Number of times the cached function was actually called.
|
|
@@ -56,7 +56,7 @@ DEFAULT = "Default" # Number of times the default value was used because nothin
|
|
|
56
56
|
STATS = [REFRESH, WAIT, SLEEP, FAILED, MISSED, SUCCESS, DEFAULT]
|
|
57
57
|
|
|
58
58
|
P = ParamSpec("P")
|
|
59
|
-
T = TypeVar("T")
|
|
59
|
+
T = TypeVar("T", str, bytes)
|
|
60
60
|
|
|
61
61
|
|
|
62
62
|
class RedisCache:
|
|
@@ -64,8 +64,6 @@ class RedisCache:
|
|
|
64
64
|
Having the decorator provided by a class allows to have some context to improve performances.
|
|
65
65
|
"""
|
|
66
66
|
|
|
67
|
-
# pylint: disable=too-many-arguments
|
|
68
|
-
|
|
69
67
|
def __init__(
|
|
70
68
|
self,
|
|
71
69
|
host: Optional[str] = None,
|
|
@@ -77,7 +75,7 @@ class RedisCache:
|
|
|
77
75
|
):
|
|
78
76
|
self.enabled = enabled
|
|
79
77
|
if self.enabled:
|
|
80
|
-
# If environment variables are set for redis server, they
|
|
78
|
+
# If environment variables are set for redis server, they supersede the default values.
|
|
81
79
|
# But if provided at the construction, it has priority.
|
|
82
80
|
if not host:
|
|
83
81
|
host = os.environ.get("REDIS_SERVICE_HOST", "localhost")
|
|
@@ -120,25 +118,18 @@ class RedisCache:
|
|
|
120
118
|
|
|
121
119
|
return f"{name}({','.join(values)})"
|
|
122
120
|
|
|
123
|
-
# pylint: disable=line-too-long
|
|
124
121
|
def cache(
|
|
125
122
|
self,
|
|
126
123
|
refresh: int,
|
|
127
124
|
expire: int,
|
|
125
|
+
default: T,
|
|
128
126
|
retry: Optional[int] = None,
|
|
129
|
-
default: Any = "",
|
|
130
127
|
wait: bool = False,
|
|
131
|
-
serializer: Optional[Callable[..., Any]] = None,
|
|
132
|
-
deserializer: Optional[Callable[..., Any]] = None,
|
|
133
128
|
use_args: Optional[List[int]] = None,
|
|
134
129
|
use_kwargs: Optional[List[str]] = None,
|
|
135
130
|
) -> Callable[[Callable[P, T]], Callable[P, T]]:
|
|
136
131
|
"""
|
|
137
|
-
Full decorator will all possible parameters.
|
|
138
|
-
|
|
139
|
-
Specific examples when to use this decorator:
|
|
140
|
-
- Raw storage of byte string that you do not want to be decoded: use the decode=False.
|
|
141
|
-
- JSON dumps data that doesn't need to be loaded before it is sent by a REST API: use serializer=dumps but no deserializer.
|
|
132
|
+
Full decorator will all possible parameters.
|
|
142
133
|
"""
|
|
143
134
|
|
|
144
135
|
logger = logging.getLogger(__name__)
|
|
@@ -164,7 +155,7 @@ class RedisCache:
|
|
|
164
155
|
color=RED,
|
|
165
156
|
output=logger.info,
|
|
166
157
|
)
|
|
167
|
-
def
|
|
158
|
+
def refresh_value(key: str) -> T:
|
|
168
159
|
"""
|
|
169
160
|
This gets the value provided by the function and stores it in local Redis database
|
|
170
161
|
"""
|
|
@@ -188,31 +179,22 @@ class RedisCache:
|
|
|
188
179
|
self.server.incr(DEFAULT)
|
|
189
180
|
new_value = default
|
|
190
181
|
|
|
191
|
-
# Serialize the value if requested
|
|
192
|
-
if serializer:
|
|
193
|
-
new_value = serializer(new_value)
|
|
194
182
|
# Store value in cache with expiration time
|
|
195
|
-
self.server.set(key, new_value, ex=expire)
|
|
183
|
+
self.server.set(key, new_value, ex=expire)
|
|
196
184
|
# Set refresh key with refresh time
|
|
197
185
|
self.server.set(PREFIX + key, 1, ex=refresh)
|
|
198
186
|
return new_value
|
|
199
187
|
|
|
200
|
-
def
|
|
188
|
+
def refresh_value_in_thread(key: str) -> None:
|
|
201
189
|
"""
|
|
202
190
|
Run the refresh value in a separate thread
|
|
203
191
|
"""
|
|
204
|
-
thread = threading.Thread(target=
|
|
192
|
+
thread = threading.Thread(target=refresh_value, args=(key,))
|
|
205
193
|
thread.start()
|
|
206
194
|
|
|
207
195
|
# If the cache is disabled, directly call the function
|
|
208
196
|
if not self.enabled:
|
|
209
|
-
|
|
210
|
-
# If we have decided to serialize, we always do it to be consistent
|
|
211
|
-
if serializer:
|
|
212
|
-
direct_value = serializer(direct_value)
|
|
213
|
-
if deserializer:
|
|
214
|
-
direct_value = deserializer(direct_value)
|
|
215
|
-
return direct_value
|
|
197
|
+
return function(*args, **kwargs)
|
|
216
198
|
|
|
217
199
|
# Lets create a key from the function's name and its parameters values
|
|
218
200
|
key = self._create_key(name=function.__name__, args=args, use_args=use_args, kwargs=kwargs, use_kwargs=use_kwargs)
|
|
@@ -225,7 +207,7 @@ class RedisCache:
|
|
|
225
207
|
|
|
226
208
|
# Get the value from the cache.
|
|
227
209
|
# If it is not there we will get None.
|
|
228
|
-
cached_value = self.server.get(key)
|
|
210
|
+
cached_value = cast(T, self.server.get(key))
|
|
229
211
|
|
|
230
212
|
# Time to update stats counters
|
|
231
213
|
if cached_value is None:
|
|
@@ -239,12 +221,12 @@ class RedisCache:
|
|
|
239
221
|
# If we found a value in the cash, we will not wait for the refresh
|
|
240
222
|
if cached_value or not wait:
|
|
241
223
|
# We just update the cache in another thread.
|
|
242
|
-
|
|
224
|
+
refresh_value_in_thread(key)
|
|
243
225
|
else:
|
|
244
226
|
# Here we will wait, let's count it
|
|
245
227
|
self.server.incr(WAIT)
|
|
246
228
|
# We update the cash and return the value.
|
|
247
|
-
cached_value =
|
|
229
|
+
cached_value = refresh_value(key)
|
|
248
230
|
|
|
249
231
|
# We may still have decided to wait but another process is already getting the cache updated.
|
|
250
232
|
if cached_value is None and wait:
|
|
@@ -254,60 +236,24 @@ class RedisCache:
|
|
|
254
236
|
# Let's count how many times we wait 1s
|
|
255
237
|
self.server.incr(SLEEP)
|
|
256
238
|
sleep(1)
|
|
257
|
-
cached_value = self.server.get(key)
|
|
239
|
+
cached_value = cast(T, self.server.get(key))
|
|
258
240
|
|
|
259
241
|
# If the cache was empty, we have None in the cached_value.
|
|
260
242
|
if cached_value is None:
|
|
261
243
|
# We are going to return the default value
|
|
262
244
|
self.server.incr(DEFAULT)
|
|
263
|
-
cached_value =
|
|
245
|
+
cached_value = default
|
|
264
246
|
|
|
265
247
|
# Return whatever value we have at this point.
|
|
266
|
-
return
|
|
248
|
+
return cached_value
|
|
249
|
+
|
|
250
|
+
# If we want to bypass the cache at runtime, we need a reference to the decorated function
|
|
251
|
+
wrapper.function = function # type: ignore
|
|
267
252
|
|
|
268
253
|
return wrapper
|
|
269
254
|
|
|
270
255
|
return decorator
|
|
271
256
|
|
|
272
|
-
def cache_raw(self, refresh: int, expire: int, retry: Optional[int] = None, default: Any = "") -> Callable[[Callable[P, T]], Callable[P, T]]:
|
|
273
|
-
"""
|
|
274
|
-
Normal caching of values directly storable in redis: byte string, string, int, float.
|
|
275
|
-
"""
|
|
276
|
-
return self.cache(refresh=refresh, expire=expire, retry=retry, default=default)
|
|
277
|
-
|
|
278
|
-
def cache_raw_wait(self, refresh: int, expire: int, retry: Optional[int] = None, default: Any = "") -> Callable[[Callable[P, T]], Callable[P, T]]:
|
|
279
|
-
"""
|
|
280
|
-
Same as cache_raw() but will wait for the completion of the cached function if no value is found in redis.
|
|
281
|
-
"""
|
|
282
|
-
return self.cache(refresh=refresh, expire=expire, retry=retry, default=default, wait=True)
|
|
283
|
-
|
|
284
|
-
def cache_json(self, refresh: int, expire: int, retry: Optional[int] = None, default: Any = "") -> Callable[[Callable[P, T]], Callable[P, T]]:
|
|
285
|
-
"""
|
|
286
|
-
JSON dumps the values to be stored in redis and loads them again when returning them to the caller.
|
|
287
|
-
"""
|
|
288
|
-
return self.cache(
|
|
289
|
-
refresh=refresh,
|
|
290
|
-
expire=expire,
|
|
291
|
-
retry=retry,
|
|
292
|
-
default=default,
|
|
293
|
-
serializer=dumps,
|
|
294
|
-
deserializer=loads,
|
|
295
|
-
)
|
|
296
|
-
|
|
297
|
-
def cache_json_wait(self, refresh: int, expire: int, retry: Optional[int] = None, default: Any = "") -> Callable[[Callable[P, T]], Callable[P, T]]:
|
|
298
|
-
"""
|
|
299
|
-
Same as cache_json() but will wait for the completion of the cached function if no value is found in redis.
|
|
300
|
-
"""
|
|
301
|
-
return self.cache(
|
|
302
|
-
refresh=refresh,
|
|
303
|
-
expire=expire,
|
|
304
|
-
retry=retry,
|
|
305
|
-
default=default,
|
|
306
|
-
wait=True,
|
|
307
|
-
serializer=dumps,
|
|
308
|
-
deserializer=loads,
|
|
309
|
-
)
|
|
310
|
-
|
|
311
257
|
def get_stats(self, delete: bool = False) -> Dict[str, Any]:
|
|
312
258
|
"""
|
|
313
259
|
Get the stats stored by RedisCache. See the list and definition at the top of this file.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: rediscache
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
Summary: Redis caching of functions evolving over time
|
|
5
5
|
Home-page: https://github.com/AmadeusITGroup/RedisCache
|
|
6
6
|
License: MIT
|
|
@@ -40,9 +40,9 @@ There are already quite a few Python decorators to cache functions in a Redis da
|
|
|
40
40
|
- [redis-simple-cache-py3](https://pypi.org/project/redis-simple-cache-py3/)
|
|
41
41
|
- and more ...
|
|
42
42
|
|
|
43
|
-
But none I could find allows to set two expiration times as we do it here. The first given time is how long before we should update the value stored in the cache. The second given time, longer of course, is how long the data stored in the cache is still good enough to be sent back to the caller. The refreshing of the cache is only done when the function is called. And by default it is done asynchronously, so the caller doesn't have to wait. When the data in the cache becomes too old, it
|
|
43
|
+
But none I could find allows to set two expiration times as we do it here. The first given time is how long before we should update the value stored in the cache. The second given time, longer of course, is how long the data stored in the cache is still good enough to be sent back to the caller. The refreshing of the cache is only done when the function is called. And by default it is done asynchronously, so the caller doesn't have to wait. When the data in the cache becomes too old, it disappear automatically.
|
|
44
44
|
|
|
45
|
-
This is a great caching mechanism for functions that will give a consistent output according to their parameters and at a given time. A
|
|
45
|
+
This is a great caching mechanism for functions that will give a consistent output according to their parameters and at a given time. A purely random function should not be cached. And a function that is independent of the time should be cached with a different mechanism like the LRU cache in the [functools](https://docs.python.org/3/library/functools.html) standard module.
|
|
46
46
|
|
|
47
47
|
## Installation
|
|
48
48
|
|
|
@@ -73,7 +73,7 @@ All the parameters for the `RedisCache` constructor are optional. Their default
|
|
|
73
73
|
- db: Database number in the Redis server. [`0`]
|
|
74
74
|
- password: Password required to read and write on the Redis server. [`None`]
|
|
75
75
|
- decode: Decode the data stored in the cache as byte string. For example, it should not be done if you actually want to cache byte strings. [`True`]
|
|
76
|
-
- enabled: When False it allows to
|
|
76
|
+
- enabled: When False it allows to programmatically disable the cache. It can be useful for unit tests. [`True`]
|
|
77
77
|
|
|
78
78
|
### Environment variables
|
|
79
79
|
|
|
@@ -98,7 +98,7 @@ This is the main decorator. All the parameters are available. The mandatory ones
|
|
|
98
98
|
- serializer: The only type of data that can be stored directly in the Redis database are `byte`, `str`, `int` and `float`. Any other will have to be serialized with the function provided here. [`None`]
|
|
99
99
|
- deserializer: If the value was serialized to be stored in the cache, it needs to deserialized when it is retrieved. [`None`]
|
|
100
100
|
- use_args: This is the list of positional parameters (a list of integers) to be taken into account to generate the key that will be used in Redis. If `None`, they will all be used. [`None`]
|
|
101
|
-
- use_kwargs: This is the list of named
|
|
101
|
+
- use_kwargs: This is the list of named parameters (a list of names) to be taken into account to generate the key that will be used in Redis. If `None`, they will all be used. [`None`]
|
|
102
102
|
|
|
103
103
|
Example:
|
|
104
104
|
|
|
@@ -115,22 +115,6 @@ See `test_rediscache.py` for more examples.
|
|
|
115
115
|
|
|
116
116
|
Note: when you choose to wait for the value, you do not have an absolute guarantee that you will not get the default value. For example if it takes more than the retry time to get an answer from the function, the decorator will give up.
|
|
117
117
|
|
|
118
|
-
### `cache_raw` decorator helper
|
|
119
|
-
|
|
120
|
-
No serializer or deserializer. This will only work if the cached function only returns `byte`, `str`, `int` or `float` types. Even `None` will fail.
|
|
121
|
-
|
|
122
|
-
### `cache_raw_wait` decorator helper
|
|
123
|
-
|
|
124
|
-
Same as above but waits for the value if not in the cache.
|
|
125
|
-
|
|
126
|
-
### `cache_json` decorator helper
|
|
127
|
-
|
|
128
|
-
Serialize the value with `json.dumps()` and desiralize the value with `json.loads()`.
|
|
129
|
-
|
|
130
|
-
### `cache_json_wait` decorator helper
|
|
131
|
-
|
|
132
|
-
Same as above but waits for the value if not in the cache.
|
|
133
|
-
|
|
134
118
|
### `get_stats(delete=False)`
|
|
135
119
|
|
|
136
120
|
This will get the stats stored when using the cache. The `delete` option is to reset the counters after read.
|
|
@@ -138,6 +122,7 @@ The output is a dictionary with the following keys and values:
|
|
|
138
122
|
|
|
139
123
|
- **Refresh**: Number of times the cached function was actually called.
|
|
140
124
|
- **Wait**: Number of times we waited for the result when executing the function.
|
|
125
|
+
- **Sleep**: Number of 1 seconds we waited for the results to be found in the cache.
|
|
141
126
|
- **Failed**: Number of times the cached function raised an exception when called.
|
|
142
127
|
- **Missed**: Number of times the functions result was not found in the cache.
|
|
143
128
|
- **Success**: Number of times the function's result was found in the cache.
|
|
@@ -201,7 +186,7 @@ My development environment is handled by Poetry. I use `Python 3.11.7`.
|
|
|
201
186
|
|
|
202
187
|
### Testing
|
|
203
188
|
|
|
204
|
-
To make sure we use Redis properly, we do not mock it in the unit tess. So you will need a localhost default instance of Redis server without a password. This means that the unit tests are more like
|
|
189
|
+
To make sure we use Redis properly, we do not mock it in the unit tess. So you will need a localhost default instance of Redis server without a password. This means that the unit tests are more like integration tests.
|
|
205
190
|
|
|
206
191
|
The execution of the tests including coverage result can be done with `test.sh`. You can also run just `pytest`:
|
|
207
192
|
|
|
@@ -218,7 +203,7 @@ We use the GitHub workflow to check each new commit. See `.github/workflows/pyth
|
|
|
218
203
|
We get help from re-usable actions. Here is the [Marketplace](https://github.com/marketplace?type=actions).
|
|
219
204
|
|
|
220
205
|
- [Checkout](https://github.com/marketplace/actions/checkout)
|
|
221
|
-
- [Install
|
|
206
|
+
- [Install Poetry Action](https://github.com/marketplace/actions/install-poetry-action)
|
|
222
207
|
- [Setup Python](https://github.com/marketplace/actions/setup-python)
|
|
223
208
|
|
|
224
209
|
### Publish to PyPI
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
rediscache/__init__.py,sha256=MAmNU52cyqKmAJBEO4SN7spLcbvMTaYWUiw_Kx7ZvLI,11747
|
|
2
|
+
rediscache/tools.py,sha256=0I4l2f-SKZR6ec5y-GJOMhLdWimPjs6gGzHtPdPdYpg,995
|
|
3
|
+
rediscache-1.0.0.dist-info/LICENSE,sha256=glwtaJmUmkPhzLEfrVcc2JmLoCyTnHst84tadlSIK_8,1123
|
|
4
|
+
rediscache-1.0.0.dist-info/METADATA,sha256=aMYLH8XmO9l-ZTdnp7NU8dJG30vLWerpODwkyMb0PAg,9558
|
|
5
|
+
rediscache-1.0.0.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
|
|
6
|
+
rediscache-1.0.0.dist-info/RECORD,,
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
rediscache/__init__.py,sha256=48eo_z_ohwxRYIeZxZTp373XxsCx6nEtC1EqE7FBfyk,14447
|
|
2
|
-
rediscache/tools.py,sha256=0I4l2f-SKZR6ec5y-GJOMhLdWimPjs6gGzHtPdPdYpg,995
|
|
3
|
-
rediscache-0.3.2.dist-info/LICENSE,sha256=glwtaJmUmkPhzLEfrVcc2JmLoCyTnHst84tadlSIK_8,1123
|
|
4
|
-
rediscache-0.3.2.dist-info/METADATA,sha256=1wOtgyEM_4R1xti4LSA5sczhI6N1uoySIIl4-zkBi3k,9975
|
|
5
|
-
rediscache-0.3.2.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
|
|
6
|
-
rediscache-0.3.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|