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

222 statements  

1from abc import ABC, abstractmethod 

2from collections import OrderedDict 

3from dataclasses import dataclass 

4from enum import Enum 

5from typing import Any, List, Optional, Union 

6 

7 

8class CacheEntryStatus(Enum): 

9 VALID = "VALID" 

10 IN_PROGRESS = "IN_PROGRESS" 

11 

12 

13class EvictionPolicyType(Enum): 

14 time_based = "time_based" 

15 frequency_based = "frequency_based" 

16 

17 

18@dataclass(frozen=True) 

19class CacheKey: 

20 command: str 

21 redis_keys: tuple 

22 

23 

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 

36 

37 def __hash__(self): 

38 return hash( 

39 (self.cache_key, self.cache_value, self.status, self.connection_ref) 

40 ) 

41 

42 def __eq__(self, other): 

43 return hash(self) == hash(other) 

44 

45 

46class EvictionPolicyInterface(ABC): 

47 @property 

48 @abstractmethod 

49 def cache(self): 

50 pass 

51 

52 @cache.setter 

53 def cache(self, value): 

54 pass 

55 

56 @property 

57 @abstractmethod 

58 def type(self) -> EvictionPolicyType: 

59 pass 

60 

61 @abstractmethod 

62 def evict_next(self) -> CacheKey: 

63 pass 

64 

65 @abstractmethod 

66 def evict_many(self, count: int) -> List[CacheKey]: 

67 pass 

68 

69 @abstractmethod 

70 def touch(self, cache_key: CacheKey) -> None: 

71 pass 

72 

73 

74class CacheConfigurationInterface(ABC): 

75 @abstractmethod 

76 def get_cache_class(self): 

77 pass 

78 

79 @abstractmethod 

80 def get_max_size(self) -> int: 

81 pass 

82 

83 @abstractmethod 

84 def get_eviction_policy(self): 

85 pass 

86 

87 @abstractmethod 

88 def is_exceeds_max_size(self, count: int) -> bool: 

89 pass 

90 

91 @abstractmethod 

92 def is_allowed_to_cache(self, command: str) -> bool: 

93 pass 

94 

95 

96class CacheInterface(ABC): 

97 @property 

98 @abstractmethod 

99 def collection(self) -> OrderedDict: 

100 pass 

101 

102 @property 

103 @abstractmethod 

104 def config(self) -> CacheConfigurationInterface: 

105 pass 

106 

107 @property 

108 @abstractmethod 

109 def eviction_policy(self) -> EvictionPolicyInterface: 

110 pass 

111 

112 @property 

113 @abstractmethod 

114 def size(self) -> int: 

115 pass 

116 

117 @abstractmethod 

118 def get(self, key: CacheKey) -> Union[CacheEntry, None]: 

119 pass 

120 

121 @abstractmethod 

122 def set(self, entry: CacheEntry) -> bool: 

123 pass 

124 

125 @abstractmethod 

126 def delete_by_cache_keys(self, cache_keys: List[CacheKey]) -> List[bool]: 

127 pass 

128 

129 @abstractmethod 

130 def delete_by_redis_keys(self, redis_keys: List[bytes]) -> List[bool]: 

131 pass 

132 

133 @abstractmethod 

134 def flush(self) -> int: 

135 pass 

136 

137 @abstractmethod 

138 def is_cachable(self, key: CacheKey) -> bool: 

139 pass 

140 

141 

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 

151 

152 @property 

153 def collection(self) -> OrderedDict: 

154 return self._cache 

155 

156 @property 

157 def config(self) -> CacheConfigurationInterface: 

158 return self._cache_config 

159 

160 @property 

161 def eviction_policy(self) -> EvictionPolicyInterface: 

162 return self._eviction_policy 

163 

164 @property 

165 def size(self) -> int: 

166 return len(self._cache) 

167 

168 def set(self, entry: CacheEntry) -> bool: 

169 if not self.is_cachable(entry.cache_key): 

170 return False 

171 

172 self._cache[entry.cache_key] = entry 

173 self._eviction_policy.touch(entry.cache_key) 

174 

175 if self._cache_config.is_exceeds_max_size(len(self._cache)): 

176 self._eviction_policy.evict_next() 

177 

178 return True 

179 

180 def get(self, key: CacheKey) -> Union[CacheEntry, None]: 

181 entry = self._cache.get(key, None) 

182 

183 if entry is None: 

184 return None 

185 

186 self._eviction_policy.touch(key) 

187 return entry 

188 

189 def delete_by_cache_keys(self, cache_keys: List[CacheKey]) -> List[bool]: 

190 response = [] 

191 

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) 

198 

199 return response 

200 

201 def delete_by_redis_keys(self, redis_keys: List[bytes]) -> List[bool]: 

202 response = [] 

203 keys_to_delete = [] 

204 

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) 

212 

213 for key in keys_to_delete: 

214 self._cache.pop(key) 

215 

216 return response 

217 

218 def flush(self) -> int: 

219 elem_count = len(self._cache) 

220 self._cache.clear() 

221 return elem_count 

222 

223 def is_cachable(self, key: CacheKey) -> bool: 

224 return self._cache_config.is_allowed_to_cache(key.command) 

225 

226 

227class LRUPolicy(EvictionPolicyInterface): 

228 def __init__(self): 

229 self.cache = None 

230 

231 @property 

232 def cache(self): 

233 return self._cache 

234 

235 @cache.setter 

236 def cache(self, cache: CacheInterface): 

237 self._cache = cache 

238 

239 @property 

240 def type(self) -> EvictionPolicyType: 

241 return EvictionPolicyType.time_based 

242 

243 def evict_next(self) -> CacheKey: 

244 self._assert_cache() 

245 popped_entry = self._cache.collection.popitem(last=False) 

246 return popped_entry[0] 

247 

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") 

252 

253 popped_keys = [] 

254 

255 for _ in range(count): 

256 popped_entry = self._cache.collection.popitem(last=False) 

257 popped_keys.append(popped_entry[0]) 

258 

259 return popped_keys 

260 

261 def touch(self, cache_key: CacheKey) -> None: 

262 self._assert_cache() 

263 

264 if self._cache.collection.get(cache_key) is None: 

265 raise ValueError("Given entry does not belong to the cache") 

266 

267 self._cache.collection.move_to_end(cache_key) 

268 

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.") 

272 

273 

274class EvictionPolicy(Enum): 

275 LRU = LRUPolicy 

276 

277 

278class CacheConfig(CacheConfigurationInterface): 

279 DEFAULT_CACHE_CLASS = DefaultCache 

280 DEFAULT_EVICTION_POLICY = EvictionPolicy.LRU 

281 DEFAULT_MAX_SIZE = 10000 

282 

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 ] 

359 

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 

369 

370 def get_cache_class(self): 

371 return self._cache_class 

372 

373 def get_max_size(self) -> int: 

374 return self._max_size 

375 

376 def get_eviction_policy(self) -> EvictionPolicy: 

377 return self._eviction_policy 

378 

379 def is_exceeds_max_size(self, count: int) -> bool: 

380 return count > self._max_size 

381 

382 def is_allowed_to_cache(self, command: str) -> bool: 

383 return command in self.DEFAULT_ALLOW_LIST 

384 

385 

386class CacheFactoryInterface(ABC): 

387 @abstractmethod 

388 def get_cache(self) -> CacheInterface: 

389 pass 

390 

391 

392class CacheFactory(CacheFactoryInterface): 

393 def __init__(self, cache_config: Optional[CacheConfig] = None): 

394 self._config = cache_config 

395 

396 if self._config is None: 

397 self._config = CacheConfig() 

398 

399 def get_cache(self) -> CacheInterface: 

400 cache_class = self._config.get_cache_class() 

401 return cache_class(cache_config=self._config)