Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/redis/cache.py: 53%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from abc import ABC, abstractmethod
2from collections import OrderedDict
3from dataclasses import dataclass
4from enum import Enum
5from typing import Any, List, Optional, Union
8class CacheEntryStatus(Enum):
9 VALID = "VALID"
10 IN_PROGRESS = "IN_PROGRESS"
13class EvictionPolicyType(Enum):
14 time_based = "time_based"
15 frequency_based = "frequency_based"
18@dataclass(frozen=True)
19class CacheKey:
20 command: str
21 redis_keys: tuple
24class CacheEntry:
25 def __init__(
26 self,
27 cache_key: CacheKey,
28 cache_value: bytes,
29 status: CacheEntryStatus,
30 connection_ref,
31 ):
32 self.cache_key = cache_key
33 self.cache_value = cache_value
34 self.status = status
35 self.connection_ref = connection_ref
37 def __hash__(self):
38 return hash(
39 (self.cache_key, self.cache_value, self.status, self.connection_ref)
40 )
42 def __eq__(self, other):
43 return hash(self) == hash(other)
46class EvictionPolicyInterface(ABC):
47 @property
48 @abstractmethod
49 def cache(self):
50 pass
52 @cache.setter
53 @abstractmethod
54 def cache(self, value):
55 pass
57 @property
58 @abstractmethod
59 def type(self) -> EvictionPolicyType:
60 pass
62 @abstractmethod
63 def evict_next(self) -> CacheKey:
64 pass
66 @abstractmethod
67 def evict_many(self, count: int) -> List[CacheKey]:
68 pass
70 @abstractmethod
71 def touch(self, cache_key: CacheKey) -> None:
72 pass
75class CacheConfigurationInterface(ABC):
76 @abstractmethod
77 def get_cache_class(self):
78 pass
80 @abstractmethod
81 def get_max_size(self) -> int:
82 pass
84 @abstractmethod
85 def get_eviction_policy(self):
86 pass
88 @abstractmethod
89 def is_exceeds_max_size(self, count: int) -> bool:
90 pass
92 @abstractmethod
93 def is_allowed_to_cache(self, command: str) -> bool:
94 pass
97class CacheInterface(ABC):
98 @property
99 @abstractmethod
100 def collection(self) -> OrderedDict:
101 pass
103 @property
104 @abstractmethod
105 def config(self) -> CacheConfigurationInterface:
106 pass
108 @property
109 @abstractmethod
110 def eviction_policy(self) -> EvictionPolicyInterface:
111 pass
113 @property
114 @abstractmethod
115 def size(self) -> int:
116 pass
118 @abstractmethod
119 def get(self, key: CacheKey) -> Union[CacheEntry, None]:
120 pass
122 @abstractmethod
123 def set(self, entry: CacheEntry) -> bool:
124 pass
126 @abstractmethod
127 def delete_by_cache_keys(self, cache_keys: List[CacheKey]) -> List[bool]:
128 pass
130 @abstractmethod
131 def delete_by_redis_keys(self, redis_keys: List[bytes]) -> List[bool]:
132 pass
134 @abstractmethod
135 def flush(self) -> int:
136 pass
138 @abstractmethod
139 def is_cachable(self, key: CacheKey) -> bool:
140 pass
143class DefaultCache(CacheInterface):
144 def __init__(
145 self,
146 cache_config: CacheConfigurationInterface,
147 ) -> None:
148 self._cache = OrderedDict()
149 self._cache_config = cache_config
150 self._eviction_policy = self._cache_config.get_eviction_policy().value()
151 self._eviction_policy.cache = self
153 @property
154 def collection(self) -> OrderedDict:
155 return self._cache
157 @property
158 def config(self) -> CacheConfigurationInterface:
159 return self._cache_config
161 @property
162 def eviction_policy(self) -> EvictionPolicyInterface:
163 return self._eviction_policy
165 @property
166 def size(self) -> int:
167 return len(self._cache)
169 def set(self, entry: CacheEntry) -> bool:
170 if not self.is_cachable(entry.cache_key):
171 return False
173 self._cache[entry.cache_key] = entry
174 self._eviction_policy.touch(entry.cache_key)
176 if self._cache_config.is_exceeds_max_size(len(self._cache)):
177 self._eviction_policy.evict_next()
179 return True
181 def get(self, key: CacheKey) -> Union[CacheEntry, None]:
182 entry = self._cache.get(key, None)
184 if entry is None:
185 return None
187 self._eviction_policy.touch(key)
188 return entry
190 def delete_by_cache_keys(self, cache_keys: List[CacheKey]) -> List[bool]:
191 response = []
193 for key in cache_keys:
194 if self.get(key) is not None:
195 self._cache.pop(key)
196 response.append(True)
197 else:
198 response.append(False)
200 return response
202 def delete_by_redis_keys(self, redis_keys: List[bytes]) -> List[bool]:
203 response = []
204 keys_to_delete = []
206 for redis_key in redis_keys:
207 if isinstance(redis_key, bytes):
208 redis_key = redis_key.decode()
209 for cache_key in self._cache:
210 if redis_key in cache_key.redis_keys:
211 keys_to_delete.append(cache_key)
212 response.append(True)
214 for key in keys_to_delete:
215 self._cache.pop(key)
217 return response
219 def flush(self) -> int:
220 elem_count = len(self._cache)
221 self._cache.clear()
222 return elem_count
224 def is_cachable(self, key: CacheKey) -> bool:
225 return self._cache_config.is_allowed_to_cache(key.command)
228class LRUPolicy(EvictionPolicyInterface):
229 def __init__(self):
230 self.cache = None
232 @property
233 def cache(self):
234 return self._cache
236 @cache.setter
237 def cache(self, cache: CacheInterface):
238 self._cache = cache
240 @property
241 def type(self) -> EvictionPolicyType:
242 return EvictionPolicyType.time_based
244 def evict_next(self) -> CacheKey:
245 self._assert_cache()
246 popped_entry = self._cache.collection.popitem(last=False)
247 return popped_entry[0]
249 def evict_many(self, count: int) -> List[CacheKey]:
250 self._assert_cache()
251 if count > len(self._cache.collection):
252 raise ValueError("Evictions count is above cache size")
254 popped_keys = []
256 for _ in range(count):
257 popped_entry = self._cache.collection.popitem(last=False)
258 popped_keys.append(popped_entry[0])
260 return popped_keys
262 def touch(self, cache_key: CacheKey) -> None:
263 self._assert_cache()
265 if self._cache.collection.get(cache_key) is None:
266 raise ValueError("Given entry does not belong to the cache")
268 self._cache.collection.move_to_end(cache_key)
270 def _assert_cache(self):
271 if self.cache is None or not isinstance(self.cache, CacheInterface):
272 raise ValueError("Eviction policy should be associated with valid cache.")
275class EvictionPolicy(Enum):
276 LRU = LRUPolicy
279class CacheConfig(CacheConfigurationInterface):
280 DEFAULT_CACHE_CLASS = DefaultCache
281 DEFAULT_EVICTION_POLICY = EvictionPolicy.LRU
282 DEFAULT_MAX_SIZE = 10000
284 DEFAULT_ALLOW_LIST = [
285 "BITCOUNT",
286 "BITFIELD_RO",
287 "BITPOS",
288 "EXISTS",
289 "GEODIST",
290 "GEOHASH",
291 "GEOPOS",
292 "GEORADIUSBYMEMBER_RO",
293 "GEORADIUS_RO",
294 "GEOSEARCH",
295 "GET",
296 "GETBIT",
297 "GETRANGE",
298 "HEXISTS",
299 "HGET",
300 "HGETALL",
301 "HKEYS",
302 "HLEN",
303 "HMGET",
304 "HSTRLEN",
305 "HVALS",
306 "JSON.ARRINDEX",
307 "JSON.ARRLEN",
308 "JSON.GET",
309 "JSON.MGET",
310 "JSON.OBJKEYS",
311 "JSON.OBJLEN",
312 "JSON.RESP",
313 "JSON.STRLEN",
314 "JSON.TYPE",
315 "LCS",
316 "LINDEX",
317 "LLEN",
318 "LPOS",
319 "LRANGE",
320 "MGET",
321 "SCARD",
322 "SDIFF",
323 "SINTER",
324 "SINTERCARD",
325 "SISMEMBER",
326 "SMEMBERS",
327 "SMISMEMBER",
328 "SORT_RO",
329 "STRLEN",
330 "SUBSTR",
331 "SUNION",
332 "TS.GET",
333 "TS.INFO",
334 "TS.RANGE",
335 "TS.REVRANGE",
336 "TYPE",
337 "XLEN",
338 "XPENDING",
339 "XRANGE",
340 "XREAD",
341 "XREVRANGE",
342 "ZCARD",
343 "ZCOUNT",
344 "ZDIFF",
345 "ZINTER",
346 "ZINTERCARD",
347 "ZLEXCOUNT",
348 "ZMSCORE",
349 "ZRANGE",
350 "ZRANGEBYLEX",
351 "ZRANGEBYSCORE",
352 "ZRANK",
353 "ZREVRANGE",
354 "ZREVRANGEBYLEX",
355 "ZREVRANGEBYSCORE",
356 "ZREVRANK",
357 "ZSCORE",
358 "ZUNION",
359 ]
361 def __init__(
362 self,
363 max_size: int = DEFAULT_MAX_SIZE,
364 cache_class: Any = DEFAULT_CACHE_CLASS,
365 eviction_policy: EvictionPolicy = DEFAULT_EVICTION_POLICY,
366 ):
367 self._cache_class = cache_class
368 self._max_size = max_size
369 self._eviction_policy = eviction_policy
371 def get_cache_class(self):
372 return self._cache_class
374 def get_max_size(self) -> int:
375 return self._max_size
377 def get_eviction_policy(self) -> EvictionPolicy:
378 return self._eviction_policy
380 def is_exceeds_max_size(self, count: int) -> bool:
381 return count > self._max_size
383 def is_allowed_to_cache(self, command: str) -> bool:
384 return command in self.DEFAULT_ALLOW_LIST
387class CacheFactoryInterface(ABC):
388 @abstractmethod
389 def get_cache(self) -> CacheInterface:
390 pass
393class CacheFactory(CacheFactoryInterface):
394 def __init__(self, cache_config: Optional[CacheConfig] = None):
395 self._config = cache_config
397 if self._config is None:
398 self._config = CacheConfig()
400 def get_cache(self) -> CacheInterface:
401 cache_class = self._config.get_cache_class()
402 return cache_class(cache_config=self._config)