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 def cache(self, value):
54 pass
56 @property
57 @abstractmethod
58 def type(self) -> EvictionPolicyType:
59 pass
61 @abstractmethod
62 def evict_next(self) -> CacheKey:
63 pass
65 @abstractmethod
66 def evict_many(self, count: int) -> List[CacheKey]:
67 pass
69 @abstractmethod
70 def touch(self, cache_key: CacheKey) -> None:
71 pass
74class CacheConfigurationInterface(ABC):
75 @abstractmethod
76 def get_cache_class(self):
77 pass
79 @abstractmethod
80 def get_max_size(self) -> int:
81 pass
83 @abstractmethod
84 def get_eviction_policy(self):
85 pass
87 @abstractmethod
88 def is_exceeds_max_size(self, count: int) -> bool:
89 pass
91 @abstractmethod
92 def is_allowed_to_cache(self, command: str) -> bool:
93 pass
96class CacheInterface(ABC):
97 @property
98 @abstractmethod
99 def collection(self) -> OrderedDict:
100 pass
102 @property
103 @abstractmethod
104 def config(self) -> CacheConfigurationInterface:
105 pass
107 @property
108 @abstractmethod
109 def eviction_policy(self) -> EvictionPolicyInterface:
110 pass
112 @property
113 @abstractmethod
114 def size(self) -> int:
115 pass
117 @abstractmethod
118 def get(self, key: CacheKey) -> Union[CacheEntry, None]:
119 pass
121 @abstractmethod
122 def set(self, entry: CacheEntry) -> bool:
123 pass
125 @abstractmethod
126 def delete_by_cache_keys(self, cache_keys: List[CacheKey]) -> List[bool]:
127 pass
129 @abstractmethod
130 def delete_by_redis_keys(self, redis_keys: List[bytes]) -> List[bool]:
131 pass
133 @abstractmethod
134 def flush(self) -> int:
135 pass
137 @abstractmethod
138 def is_cachable(self, key: CacheKey) -> bool:
139 pass
142class DefaultCache(CacheInterface):
143 def __init__(
144 self,
145 cache_config: CacheConfigurationInterface,
146 ) -> None:
147 self._cache = OrderedDict()
148 self._cache_config = cache_config
149 self._eviction_policy = self._cache_config.get_eviction_policy().value()
150 self._eviction_policy.cache = self
152 @property
153 def collection(self) -> OrderedDict:
154 return self._cache
156 @property
157 def config(self) -> CacheConfigurationInterface:
158 return self._cache_config
160 @property
161 def eviction_policy(self) -> EvictionPolicyInterface:
162 return self._eviction_policy
164 @property
165 def size(self) -> int:
166 return len(self._cache)
168 def set(self, entry: CacheEntry) -> bool:
169 if not self.is_cachable(entry.cache_key):
170 return False
172 self._cache[entry.cache_key] = entry
173 self._eviction_policy.touch(entry.cache_key)
175 if self._cache_config.is_exceeds_max_size(len(self._cache)):
176 self._eviction_policy.evict_next()
178 return True
180 def get(self, key: CacheKey) -> Union[CacheEntry, None]:
181 entry = self._cache.get(key, None)
183 if entry is None:
184 return None
186 self._eviction_policy.touch(key)
187 return entry
189 def delete_by_cache_keys(self, cache_keys: List[CacheKey]) -> List[bool]:
190 response = []
192 for key in cache_keys:
193 if self.get(key) is not None:
194 self._cache.pop(key)
195 response.append(True)
196 else:
197 response.append(False)
199 return response
201 def delete_by_redis_keys(self, redis_keys: List[bytes]) -> List[bool]:
202 response = []
203 keys_to_delete = []
205 for redis_key in redis_keys:
206 if isinstance(redis_key, bytes):
207 redis_key = redis_key.decode()
208 for cache_key in self._cache:
209 if redis_key in cache_key.redis_keys:
210 keys_to_delete.append(cache_key)
211 response.append(True)
213 for key in keys_to_delete:
214 self._cache.pop(key)
216 return response
218 def flush(self) -> int:
219 elem_count = len(self._cache)
220 self._cache.clear()
221 return elem_count
223 def is_cachable(self, key: CacheKey) -> bool:
224 return self._cache_config.is_allowed_to_cache(key.command)
227class LRUPolicy(EvictionPolicyInterface):
228 def __init__(self):
229 self.cache = None
231 @property
232 def cache(self):
233 return self._cache
235 @cache.setter
236 def cache(self, cache: CacheInterface):
237 self._cache = cache
239 @property
240 def type(self) -> EvictionPolicyType:
241 return EvictionPolicyType.time_based
243 def evict_next(self) -> CacheKey:
244 self._assert_cache()
245 popped_entry = self._cache.collection.popitem(last=False)
246 return popped_entry[0]
248 def evict_many(self, count: int) -> List[CacheKey]:
249 self._assert_cache()
250 if count > len(self._cache.collection):
251 raise ValueError("Evictions count is above cache size")
253 popped_keys = []
255 for _ in range(count):
256 popped_entry = self._cache.collection.popitem(last=False)
257 popped_keys.append(popped_entry[0])
259 return popped_keys
261 def touch(self, cache_key: CacheKey) -> None:
262 self._assert_cache()
264 if self._cache.collection.get(cache_key) is None:
265 raise ValueError("Given entry does not belong to the cache")
267 self._cache.collection.move_to_end(cache_key)
269 def _assert_cache(self):
270 if self.cache is None or not isinstance(self.cache, CacheInterface):
271 raise ValueError("Eviction policy should be associated with valid cache.")
274class EvictionPolicy(Enum):
275 LRU = LRUPolicy
278class CacheConfig(CacheConfigurationInterface):
279 DEFAULT_CACHE_CLASS = DefaultCache
280 DEFAULT_EVICTION_POLICY = EvictionPolicy.LRU
281 DEFAULT_MAX_SIZE = 10000
283 DEFAULT_ALLOW_LIST = [
284 "BITCOUNT",
285 "BITFIELD_RO",
286 "BITPOS",
287 "EXISTS",
288 "GEODIST",
289 "GEOHASH",
290 "GEOPOS",
291 "GEORADIUSBYMEMBER_RO",
292 "GEORADIUS_RO",
293 "GEOSEARCH",
294 "GET",
295 "GETBIT",
296 "GETRANGE",
297 "HEXISTS",
298 "HGET",
299 "HGETALL",
300 "HKEYS",
301 "HLEN",
302 "HMGET",
303 "HSTRLEN",
304 "HVALS",
305 "JSON.ARRINDEX",
306 "JSON.ARRLEN",
307 "JSON.GET",
308 "JSON.MGET",
309 "JSON.OBJKEYS",
310 "JSON.OBJLEN",
311 "JSON.RESP",
312 "JSON.STRLEN",
313 "JSON.TYPE",
314 "LCS",
315 "LINDEX",
316 "LLEN",
317 "LPOS",
318 "LRANGE",
319 "MGET",
320 "SCARD",
321 "SDIFF",
322 "SINTER",
323 "SINTERCARD",
324 "SISMEMBER",
325 "SMEMBERS",
326 "SMISMEMBER",
327 "SORT_RO",
328 "STRLEN",
329 "SUBSTR",
330 "SUNION",
331 "TS.GET",
332 "TS.INFO",
333 "TS.RANGE",
334 "TS.REVRANGE",
335 "TYPE",
336 "XLEN",
337 "XPENDING",
338 "XRANGE",
339 "XREAD",
340 "XREVRANGE",
341 "ZCARD",
342 "ZCOUNT",
343 "ZDIFF",
344 "ZINTER",
345 "ZINTERCARD",
346 "ZLEXCOUNT",
347 "ZMSCORE",
348 "ZRANGE",
349 "ZRANGEBYLEX",
350 "ZRANGEBYSCORE",
351 "ZRANK",
352 "ZREVRANGE",
353 "ZREVRANGEBYLEX",
354 "ZREVRANGEBYSCORE",
355 "ZREVRANK",
356 "ZSCORE",
357 "ZUNION",
358 ]
360 def __init__(
361 self,
362 max_size: int = DEFAULT_MAX_SIZE,
363 cache_class: Any = DEFAULT_CACHE_CLASS,
364 eviction_policy: EvictionPolicy = DEFAULT_EVICTION_POLICY,
365 ):
366 self._cache_class = cache_class
367 self._max_size = max_size
368 self._eviction_policy = eviction_policy
370 def get_cache_class(self):
371 return self._cache_class
373 def get_max_size(self) -> int:
374 return self._max_size
376 def get_eviction_policy(self) -> EvictionPolicy:
377 return self._eviction_policy
379 def is_exceeds_max_size(self, count: int) -> bool:
380 return count > self._max_size
382 def is_allowed_to_cache(self, command: str) -> bool:
383 return command in self.DEFAULT_ALLOW_LIST
386class CacheFactoryInterface(ABC):
387 @abstractmethod
388 def get_cache(self) -> CacheInterface:
389 pass
392class CacheFactory(CacheFactoryInterface):
393 def __init__(self, cache_config: Optional[CacheConfig] = None):
394 self._config = cache_config
396 if self._config is None:
397 self._config = CacheConfig()
399 def get_cache(self) -> CacheInterface:
400 cache_class = self._config.get_cache_class()
401 return cache_class(cache_config=self._config)