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

223 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 @abstractmethod 

54 def cache(self, value): 

55 pass 

56 

57 @property 

58 @abstractmethod 

59 def type(self) -> EvictionPolicyType: 

60 pass 

61 

62 @abstractmethod 

63 def evict_next(self) -> CacheKey: 

64 pass 

65 

66 @abstractmethod 

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

68 pass 

69 

70 @abstractmethod 

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

72 pass 

73 

74 

75class CacheConfigurationInterface(ABC): 

76 @abstractmethod 

77 def get_cache_class(self): 

78 pass 

79 

80 @abstractmethod 

81 def get_max_size(self) -> int: 

82 pass 

83 

84 @abstractmethod 

85 def get_eviction_policy(self): 

86 pass 

87 

88 @abstractmethod 

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

90 pass 

91 

92 @abstractmethod 

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

94 pass 

95 

96 

97class CacheInterface(ABC): 

98 @property 

99 @abstractmethod 

100 def collection(self) -> OrderedDict: 

101 pass 

102 

103 @property 

104 @abstractmethod 

105 def config(self) -> CacheConfigurationInterface: 

106 pass 

107 

108 @property 

109 @abstractmethod 

110 def eviction_policy(self) -> EvictionPolicyInterface: 

111 pass 

112 

113 @property 

114 @abstractmethod 

115 def size(self) -> int: 

116 pass 

117 

118 @abstractmethod 

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

120 pass 

121 

122 @abstractmethod 

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

124 pass 

125 

126 @abstractmethod 

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

128 pass 

129 

130 @abstractmethod 

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

132 pass 

133 

134 @abstractmethod 

135 def flush(self) -> int: 

136 pass 

137 

138 @abstractmethod 

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

140 pass 

141 

142 

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 

152 

153 @property 

154 def collection(self) -> OrderedDict: 

155 return self._cache 

156 

157 @property 

158 def config(self) -> CacheConfigurationInterface: 

159 return self._cache_config 

160 

161 @property 

162 def eviction_policy(self) -> EvictionPolicyInterface: 

163 return self._eviction_policy 

164 

165 @property 

166 def size(self) -> int: 

167 return len(self._cache) 

168 

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

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

171 return False 

172 

173 self._cache[entry.cache_key] = entry 

174 self._eviction_policy.touch(entry.cache_key) 

175 

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

177 self._eviction_policy.evict_next() 

178 

179 return True 

180 

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

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

183 

184 if entry is None: 

185 return None 

186 

187 self._eviction_policy.touch(key) 

188 return entry 

189 

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

191 response = [] 

192 

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) 

199 

200 return response 

201 

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

203 response = [] 

204 keys_to_delete = [] 

205 

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) 

213 

214 for key in keys_to_delete: 

215 self._cache.pop(key) 

216 

217 return response 

218 

219 def flush(self) -> int: 

220 elem_count = len(self._cache) 

221 self._cache.clear() 

222 return elem_count 

223 

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

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

226 

227 

228class LRUPolicy(EvictionPolicyInterface): 

229 def __init__(self): 

230 self.cache = None 

231 

232 @property 

233 def cache(self): 

234 return self._cache 

235 

236 @cache.setter 

237 def cache(self, cache: CacheInterface): 

238 self._cache = cache 

239 

240 @property 

241 def type(self) -> EvictionPolicyType: 

242 return EvictionPolicyType.time_based 

243 

244 def evict_next(self) -> CacheKey: 

245 self._assert_cache() 

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

247 return popped_entry[0] 

248 

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

253 

254 popped_keys = [] 

255 

256 for _ in range(count): 

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

258 popped_keys.append(popped_entry[0]) 

259 

260 return popped_keys 

261 

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

263 self._assert_cache() 

264 

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

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

267 

268 self._cache.collection.move_to_end(cache_key) 

269 

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

273 

274 

275class EvictionPolicy(Enum): 

276 LRU = LRUPolicy 

277 

278 

279class CacheConfig(CacheConfigurationInterface): 

280 DEFAULT_CACHE_CLASS = DefaultCache 

281 DEFAULT_EVICTION_POLICY = EvictionPolicy.LRU 

282 DEFAULT_MAX_SIZE = 10000 

283 

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 ] 

360 

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 

370 

371 def get_cache_class(self): 

372 return self._cache_class 

373 

374 def get_max_size(self) -> int: 

375 return self._max_size 

376 

377 def get_eviction_policy(self) -> EvictionPolicy: 

378 return self._eviction_policy 

379 

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

381 return count > self._max_size 

382 

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

384 return command in self.DEFAULT_ALLOW_LIST 

385 

386 

387class CacheFactoryInterface(ABC): 

388 @abstractmethod 

389 def get_cache(self) -> CacheInterface: 

390 pass 

391 

392 

393class CacheFactory(CacheFactoryInterface): 

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

395 self._config = cache_config 

396 

397 if self._config is None: 

398 self._config = CacheConfig() 

399 

400 def get_cache(self) -> CacheInterface: 

401 cache_class = self._config.get_cache_class() 

402 return cache_class(cache_config=self._config)