seitime-frappe/frappe/utils/redis_wrapper.py
18alantom aa690553cf refactor: undo use of count in redis pop
count param in rpop and lpop was introduced in 6.20
at the time of this commit we are using an older
version that does not include it
2023-11-02 18:22:50 +05:30

289 lines
7.3 KiB
Python

# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import pickle
import re
import redis
from redis.commands.search import Search
import frappe
from frappe.utils import cstr
class RedisearchWrapper(Search):
def sugadd(self, key, *suggestions, **kwargs):
return super().sugadd(self.client.make_key(key), *suggestions, **kwargs)
def suglen(self, key):
return super().suglen(self.client.make_key(key))
def sugdel(self, key, string):
return super().sugdel(self.client.make_key(key), string)
def sugget(self, key, *args, **kwargs):
return super().sugget(self.client.make_key(key), *args, **kwargs)
class RedisWrapper(redis.Redis):
"""Redis client that will automatically prefix conf.db_name"""
def connected(self):
try:
self.ping()
return True
except redis.exceptions.ConnectionError:
return False
def __call__(self):
"""WARNING: Added for backward compatibility to support frappe.cache().method(...)"""
return self
def make_key(self, key, user=None, shared=False):
if shared:
return key
if user:
if user is True:
user = frappe.session.user
key = f"user:{user}:{key}"
return f"{frappe.conf.db_name}|{key}".encode()
def set_value(self, key, val, user=None, expires_in_sec=None, shared=False):
"""Sets cache value.
:param key: Cache key
:param val: Value to be cached
:param user: Prepends key with User
:param expires_in_sec: Expire value of this key in X seconds
"""
key = self.make_key(key, user, shared)
if not expires_in_sec:
frappe.local.cache[key] = val
try:
if expires_in_sec:
self.setex(name=key, time=expires_in_sec, value=pickle.dumps(val))
else:
self.set(key, pickle.dumps(val))
except redis.exceptions.ConnectionError:
return None
def get_value(self, key, generator=None, user=None, expires=False, shared=False):
"""Returns cache value. If not found and generator function is
given, it will call the generator.
:param key: Cache key.
:param generator: Function to be called to generate a value if `None` is returned.
:param expires: If the key is supposed to be with an expiry, don't store it in frappe.local
"""
original_key = key
key = self.make_key(key, user, shared)
if key in frappe.local.cache:
val = frappe.local.cache[key]
else:
val = None
try:
val = self.get(key)
except redis.exceptions.ConnectionError:
pass
if val is not None:
val = pickle.loads(val)
if not expires:
if val is None and generator:
val = generator()
self.set_value(original_key, val, user=user)
else:
frappe.local.cache[key] = val
return val
def get_all(self, key):
ret = {}
for k in self.get_keys(key):
ret[key] = self.get_value(k)
return ret
def get_keys(self, key):
"""Return keys starting with `key`."""
try:
key = self.make_key(key + "*")
return self.keys(key)
except redis.exceptions.ConnectionError:
regex = re.compile(cstr(key).replace("|", r"\|").replace("*", r"[\w]*"))
return [k for k in list(frappe.local.cache) if regex.match(cstr(k))]
def delete_keys(self, key):
"""Delete keys with wildcard `*`."""
self.delete_value(self.get_keys(key), make_keys=False)
def delete_key(self, *args, **kwargs):
self.delete_value(*args, **kwargs)
def delete_value(self, keys, user=None, make_keys=True, shared=False):
"""Delete value, list of values."""
if not keys:
return
if not isinstance(keys, (list, tuple)):
keys = (keys,)
if make_keys:
keys = [self.make_key(k, shared=shared, user=user) for k in keys]
for key in keys:
frappe.local.cache.pop(key, None)
try:
self.delete(*keys)
except redis.exceptions.ConnectionError:
pass
def lpush(self, key, value):
super().lpush(self.make_key(key), value)
def rpush(self, key, value):
super().rpush(self.make_key(key), value)
def lpop(self, key):
return super().lpop(self.make_key(key))
def rpop(self, key):
return super().rpop(self.make_key(key))
def llen(self, key):
return super().llen(self.make_key(key))
def lrange(self, key, start, stop):
return super().lrange(self.make_key(key), start, stop)
def ltrim(self, key, start, stop):
return super().ltrim(self.make_key(key), start, stop)
def hset(
self,
name: str,
key: str,
value,
shared: bool = False,
*args,
**kwargs,
):
if key is None:
return
_name = self.make_key(name, shared=shared)
# set in local
frappe.local.cache.setdefault(_name, {})[key] = value
# set in redis
try:
super().hset(_name, key, pickle.dumps(value), *args, **kwargs)
except redis.exceptions.ConnectionError:
pass
def hexists(self, name: str, key: str, shared: bool = False) -> bool:
if key is None:
return False
_name = self.make_key(name, shared=shared)
try:
return super().hexists(_name, key)
except redis.exceptions.ConnectionError:
return False
def exists(self, *names: str, user=None, shared=None) -> int:
names = [self.make_key(n, user=user, shared=shared) for n in names]
try:
return super().exists(*names)
except redis.exceptions.ConnectionError:
return False
def hgetall(self, name):
value = super().hgetall(self.make_key(name))
return {key: pickle.loads(value) for key, value in value.items()}
def hget(self, name, key, generator=None, shared=False):
_name = self.make_key(name, shared=shared)
if _name not in frappe.local.cache:
frappe.local.cache[_name] = {}
if not key:
return None
if key in frappe.local.cache[_name]:
return frappe.local.cache[_name][key]
value = None
try:
value = super().hget(_name, key)
except redis.exceptions.ConnectionError:
pass
if value is not None:
value = pickle.loads(value)
frappe.local.cache[_name][key] = value
elif generator:
value = generator()
self.hset(name, key, value, shared=shared)
return value
def hdel(self, name, key, shared=False):
_name = self.make_key(name, shared=shared)
if _name in frappe.local.cache:
if key in frappe.local.cache[_name]:
del frappe.local.cache[_name][key]
try:
super().hdel(_name, key)
except redis.exceptions.ConnectionError:
pass
def hdel_keys(self, name_starts_with, key):
"""Delete hash names with wildcard `*` and key"""
for name in self.get_keys(name_starts_with):
name = name.split("|", 1)[1]
self.hdel(name, key)
def hkeys(self, name):
try:
return super().hkeys(self.make_key(name))
except redis.exceptions.ConnectionError:
return []
def sadd(self, name, *values):
"""Add a member/members to a given set"""
super().sadd(self.make_key(name), *values)
def srem(self, name, *values):
"""Remove a specific member/list of members from the set"""
super().srem(self.make_key(name), *values)
def sismember(self, name, value):
"""Returns True or False based on if a given value is present in the set"""
return super().sismember(self.make_key(name), value)
def spop(self, name):
"""Removes and returns a random member from the set"""
return super().spop(self.make_key(name))
def srandmember(self, name, count=None):
"""Returns a random member from the set"""
return super().srandmember(self.make_key(name))
def smembers(self, name):
"""Return all members of the set"""
return super().smembers(self.make_key(name))
def ft(self, index_name="idx"):
return RedisearchWrapper(client=self, index_name=self.make_key(index_name))