Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/redis/commands/policies.py: 56%

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

81 statements  

1from abc import ABC, abstractmethod 

2from typing import Optional 

3 

4from redis._parsers.commands import CommandsParser 

5from redis.commands.metadata import ( 

6 AsyncDynamicMetadataResolver, 

7 AsyncMetadataResolver, 

8 AsyncStaticMetadataResolver, 

9 CommandPolicies, 

10 DynamicMetadataResolver, 

11 MetadataResolver, 

12 PolicyRecords, 

13 RequestPolicy, 

14 ResponsePolicy, 

15 StaticMetadataResolver, 

16 _build_commands_metadata_cache_from_policies, 

17 _load_commands_metadata_cache, 

18) 

19from redis.utils import warn_deprecated 

20 

21# ``STATIC_POLICIES`` is named here because it is served by the module ``__getattr__`` below 

22# rather than bound in the module namespace, and a wildcard import only reaches a lazy 

23# attribute through ``__all__``. The metadata resolvers this module imports are deliberately 

24# left out: their home is ``redis.commands.metadata``. 

25__all__ = [ 

26 "AsyncBasePolicyResolver", 

27 "AsyncDynamicPolicyResolver", 

28 "AsyncPolicyResolver", 

29 "AsyncStaticPolicyResolver", 

30 "BasePolicyResolver", 

31 "CommandPolicies", 

32 "CommandsParser", 

33 "DynamicPolicyResolver", 

34 "PolicyRecords", 

35 "PolicyResolver", 

36 "RequestPolicy", 

37 "ResponsePolicy", 

38 "STATIC_POLICIES", 

39 "StaticPolicyResolver", 

40] 

41 

42# ===================================================================================== 

43# DEPRECATED - DO NOT USE, DO NOT EDIT, DO NOT ADD TO. 

44# 

45# Nothing in this library reads this table. It is a frozen verbatim copy of the table that 

46# shipped in 7.1.0, kept only so that an external caller importing ``STATIC_POLICIES`` keeps 

47# working, and it will be removed in a future release. It is bound to a private name and 

48# served through the module ``__getattr__`` below, so that reading it warns. 

49# 

50# It is deliberately NOT derived from ``redis.commands.metadata._STATIC_COMMAND_METADATA`` and 

51# NOT kept in sync with it: the metadata table is the single source of truth, and 

52# ``StaticPolicyResolver`` resolves it through ``StaticMetadataResolver``, projecting one record 

53# at a time. Deriving this table instead would build ~100 throwaway objects on every import of 

54# ``redis`` for a table no code path consumes, and would silently change what an existing caller 

55# reads. So expect the two to disagree: this one answers "what did 7.1.0 route by", the metadata 

56# table answers "what does this client route by". 

57# 

58# To change routing, edit ``_STATIC_COMMAND_METADATA``. Nothing here. 

59# ===================================================================================== 

60# 

61# Declared without a value so that linters and type checkers see the name that ``__all__`` 

62# exports while the module ``__getattr__`` still serves it: an annotation alone binds nothing 

63# at runtime, whereas assigning here would bypass the deprecation warning. 

64STATIC_POLICIES: PolicyRecords 

65 

66_DEPRECATED_STATIC_POLICIES: PolicyRecords = { 

67 "ft": { 

68 "explaincli": CommandPolicies( 

69 request_policy=RequestPolicy.DEFAULT_KEYLESS, 

70 response_policy=ResponsePolicy.DEFAULT_KEYLESS, 

71 ), 

72 "suglen": CommandPolicies( 

73 request_policy=RequestPolicy.DEFAULT_KEYED, 

74 response_policy=ResponsePolicy.DEFAULT_KEYED, 

75 ), 

76 "profile": CommandPolicies( 

77 request_policy=RequestPolicy.DEFAULT_KEYLESS, 

78 response_policy=ResponsePolicy.DEFAULT_KEYLESS, 

79 ), 

80 "dropindex": CommandPolicies( 

81 request_policy=RequestPolicy.DEFAULT_KEYLESS, 

82 response_policy=ResponsePolicy.DEFAULT_KEYLESS, 

83 ), 

84 "aliasupdate": CommandPolicies( 

85 request_policy=RequestPolicy.DEFAULT_KEYLESS, 

86 response_policy=ResponsePolicy.DEFAULT_KEYLESS, 

87 ), 

88 "alter": CommandPolicies( 

89 request_policy=RequestPolicy.DEFAULT_KEYLESS, 

90 response_policy=ResponsePolicy.DEFAULT_KEYLESS, 

91 ), 

92 "aggregate": CommandPolicies( 

93 request_policy=RequestPolicy.DEFAULT_KEYLESS, 

94 response_policy=ResponsePolicy.DEFAULT_KEYLESS, 

95 ), 

96 "syndump": CommandPolicies( 

97 request_policy=RequestPolicy.DEFAULT_KEYLESS, 

98 response_policy=ResponsePolicy.DEFAULT_KEYLESS, 

99 ), 

100 "create": CommandPolicies( 

101 request_policy=RequestPolicy.DEFAULT_KEYLESS, 

102 response_policy=ResponsePolicy.DEFAULT_KEYLESS, 

103 ), 

104 "explain": CommandPolicies( 

105 request_policy=RequestPolicy.DEFAULT_KEYLESS, 

106 response_policy=ResponsePolicy.DEFAULT_KEYLESS, 

107 ), 

108 "sugget": CommandPolicies( 

109 request_policy=RequestPolicy.DEFAULT_KEYED, 

110 response_policy=ResponsePolicy.DEFAULT_KEYED, 

111 ), 

112 "dictdel": CommandPolicies( 

113 request_policy=RequestPolicy.DEFAULT_KEYLESS, 

114 response_policy=ResponsePolicy.DEFAULT_KEYLESS, 

115 ), 

116 "aliasadd": CommandPolicies( 

117 request_policy=RequestPolicy.DEFAULT_KEYLESS, 

118 response_policy=ResponsePolicy.DEFAULT_KEYLESS, 

119 ), 

120 "dictadd": CommandPolicies( 

121 request_policy=RequestPolicy.DEFAULT_KEYLESS, 

122 response_policy=ResponsePolicy.DEFAULT_KEYLESS, 

123 ), 

124 "synupdate": CommandPolicies( 

125 request_policy=RequestPolicy.DEFAULT_KEYLESS, 

126 response_policy=ResponsePolicy.DEFAULT_KEYLESS, 

127 ), 

128 "drop": CommandPolicies( 

129 request_policy=RequestPolicy.DEFAULT_KEYLESS, 

130 response_policy=ResponsePolicy.DEFAULT_KEYLESS, 

131 ), 

132 "info": CommandPolicies( 

133 request_policy=RequestPolicy.DEFAULT_KEYLESS, 

134 response_policy=ResponsePolicy.DEFAULT_KEYLESS, 

135 ), 

136 "sugadd": CommandPolicies( 

137 request_policy=RequestPolicy.DEFAULT_KEYED, 

138 response_policy=ResponsePolicy.DEFAULT_KEYED, 

139 ), 

140 "dictdump": CommandPolicies( 

141 request_policy=RequestPolicy.DEFAULT_KEYLESS, 

142 response_policy=ResponsePolicy.DEFAULT_KEYLESS, 

143 ), 

144 "cursor": CommandPolicies( 

145 request_policy=RequestPolicy.SPECIAL, 

146 response_policy=ResponsePolicy.DEFAULT_KEYLESS, 

147 ), 

148 "search": CommandPolicies( 

149 request_policy=RequestPolicy.DEFAULT_KEYLESS, 

150 response_policy=ResponsePolicy.DEFAULT_KEYLESS, 

151 ), 

152 "tagvals": CommandPolicies( 

153 request_policy=RequestPolicy.DEFAULT_KEYLESS, 

154 response_policy=ResponsePolicy.DEFAULT_KEYLESS, 

155 ), 

156 "aliasdel": CommandPolicies( 

157 request_policy=RequestPolicy.DEFAULT_KEYLESS, 

158 response_policy=ResponsePolicy.DEFAULT_KEYLESS, 

159 ), 

160 "aliaslist": CommandPolicies( 

161 request_policy=RequestPolicy.DEFAULT_KEYLESS, 

162 response_policy=ResponsePolicy.DEFAULT_KEYLESS, 

163 ), 

164 "sugdel": CommandPolicies( 

165 request_policy=RequestPolicy.DEFAULT_KEYED, 

166 response_policy=ResponsePolicy.DEFAULT_KEYED, 

167 ), 

168 "spellcheck": CommandPolicies( 

169 request_policy=RequestPolicy.DEFAULT_KEYLESS, 

170 response_policy=ResponsePolicy.DEFAULT_KEYLESS, 

171 ), 

172 }, 

173 "core": { 

174 "command": CommandPolicies( 

175 request_policy=RequestPolicy.DEFAULT_KEYLESS, 

176 response_policy=ResponsePolicy.DEFAULT_KEYLESS, 

177 ), 

178 }, 

179} 

180 

181 

182class PolicyResolver(ABC): 

183 @abstractmethod 

184 def resolve(self, command_name: str) -> Optional[CommandPolicies]: 

185 """ 

186 Resolves the command name and determines the associated command policies. 

187 

188 Args: 

189 command_name: The name of the command to resolve. 

190 

191 Returns: 

192 CommandPolicies: The policies associated with the specified command. 

193 """ 

194 pass 

195 

196 @abstractmethod 

197 def with_fallback(self, fallback: "PolicyResolver") -> "PolicyResolver": 

198 """ 

199 Factory method to instantiate a policy resolver with a fallback resolver. 

200 

201 Args: 

202 fallback: Fallback resolver 

203 

204 Returns: 

205 PolicyResolver: Returns a new policy resolver with the specified fallback resolver. 

206 """ 

207 pass 

208 

209 

210class AsyncPolicyResolver(ABC): 

211 @abstractmethod 

212 async def resolve(self, command_name: str) -> Optional[CommandPolicies]: 

213 """ 

214 Resolves the command name and determines the associated command policies. 

215 

216 Args: 

217 command_name: The name of the command to resolve. 

218 

219 Returns: 

220 CommandPolicies: The policies associated with the specified command. 

221 """ 

222 pass 

223 

224 @abstractmethod 

225 def with_fallback(self, fallback: "AsyncPolicyResolver") -> "AsyncPolicyResolver": 

226 """ 

227 Factory method to instantiate an async policy resolver with a fallback resolver. 

228 

229 Args: 

230 fallback: Fallback resolver 

231 

232 Returns: 

233 AsyncPolicyResolver: Returns a new policy resolver with the specified fallback resolver. 

234 """ 

235 pass 

236 

237 

238class BasePolicyResolver(PolicyResolver): 

239 """ 

240 Base class for policy resolvers. 

241 

242 A policy resolver is the routing view of a metadata resolver: the metadata resolver it 

243 holds owns the records and the lookup, and a caller here reads only the request/response 

244 policies of a resolved record. Fallback stays at this layer rather than being delegated 

245 to the metadata resolver, so a chain may include resolvers that are not metadata-backed. 

246 

247 A resolved record may withhold its routing policies, in which case this resolver reports 

248 the command exactly the way it reports one the records do not carry: unresolved, so the 

249 fallback gets its turn and a resolver without one leaves the cluster client to resolve the 

250 target itself. 

251 

252 Memoization of resolved policies belongs to the metadata resolver, which serves the 

253 routing projection through ``resolve_policies``, so a command is projected once no matter 

254 which layer it is resolved through. 

255 """ 

256 

257 def __init__( 

258 self, policies: PolicyRecords, fallback: Optional[PolicyResolver] = None 

259 ) -> None: 

260 """ 

261 Parameters: 

262 policies (PolicyRecords): Policy records to serve. Lifted into metadata records 

263 where every other field keeps its fail-closed default, so a resolver built 

264 this way reports no command as client-side-cacheable - the conservative answer 

265 for metadata that was never supplied. Keys are lowercased, because that is how 

266 a resolved command name is looked up. 

267 fallback (Optional[PolicyResolver]): An optional resolver to be used when the 

268 primary policies cannot handle a specific request. 

269 """ 

270 self._init_from_metadata_resolver( 

271 DynamicMetadataResolver( 

272 _build_commands_metadata_cache_from_policies(policies) 

273 ), 

274 fallback, 

275 ) 

276 

277 def _init_from_metadata_resolver( 

278 self, 

279 metadata_resolver: MetadataResolver, 

280 fallback: Optional[PolicyResolver] = None, 

281 ) -> None: 

282 """ 

283 Initialize from a metadata resolver instead of from policy records. 

284 

285 The purpose of this method is to allow the subclasses, whose own constructor carries 

286 no policy records to hand up, like ``StaticPolicyResolver`` and ``DynamicPolicyResolver``, 

287 to initialize using metadata_resolver. 

288 They resolve through a metadata resolver so that one object can serve routing 

289 and every other command-metadata consumer. 

290 """ 

291 self._metadata_resolver = metadata_resolver 

292 self._fallback = fallback 

293 

294 def resolve(self, command_name: str) -> Optional[CommandPolicies]: 

295 policies = self._metadata_resolver.resolve_policies(command_name) 

296 

297 if policies is None and self._fallback is not None: 

298 return self._fallback.resolve(command_name) 

299 

300 return policies 

301 

302 @abstractmethod 

303 def with_fallback(self, fallback: "PolicyResolver") -> "PolicyResolver": 

304 pass 

305 

306 

307class AsyncBasePolicyResolver(AsyncPolicyResolver): 

308 """ 

309 Async base class for policy resolvers. 

310 

311 A policy resolver is the routing view of a metadata resolver: the metadata resolver it 

312 holds owns the records and the lookup, and a caller here reads only the request/response 

313 policies of a resolved record. Fallback stays at this layer rather than being delegated 

314 to the metadata resolver, so a chain may include resolvers that are not metadata-backed. 

315 

316 A resolved record may withhold its routing policies, in which case this resolver reports 

317 the command exactly the way it reports one the records do not carry: unresolved, so the 

318 fallback gets its turn and a resolver without one leaves the cluster client to resolve the 

319 target itself. 

320 

321 Memoization of resolved policies belongs to the metadata resolver, which serves the 

322 routing projection through ``resolve_policies``, so a command is projected once no matter 

323 which layer it is resolved through. 

324 """ 

325 

326 def __init__( 

327 self, policies: PolicyRecords, fallback: Optional[AsyncPolicyResolver] = None 

328 ) -> None: 

329 """ 

330 Parameters: 

331 policies (PolicyRecords): Policy records to serve. Mirrors 

332 sync ``BasePolicyResolver`` - see its note. 

333 fallback (Optional[AsyncPolicyResolver]): An optional resolver to be used when 

334 the primary policies cannot handle a specific request. 

335 """ 

336 self._init_from_metadata_resolver( 

337 AsyncDynamicMetadataResolver( 

338 _build_commands_metadata_cache_from_policies(policies) 

339 ), 

340 fallback, 

341 ) 

342 

343 def _init_from_metadata_resolver( 

344 self, 

345 metadata_resolver: AsyncMetadataResolver, 

346 fallback: Optional[AsyncPolicyResolver] = None, 

347 ) -> None: 

348 """Async mirror of ``BasePolicyResolver._init_from_metadata_resolver`` - see its note.""" 

349 self._metadata_resolver = metadata_resolver 

350 self._fallback = fallback 

351 

352 async def resolve(self, command_name: str) -> Optional[CommandPolicies]: 

353 policies = await self._metadata_resolver.resolve_policies(command_name) 

354 

355 if policies is None and self._fallback is not None: 

356 return await self._fallback.resolve(command_name) 

357 

358 return policies 

359 

360 @abstractmethod 

361 def with_fallback(self, fallback: "AsyncPolicyResolver") -> "AsyncPolicyResolver": 

362 pass 

363 

364 

365class DynamicPolicyResolver(BasePolicyResolver): 

366 """ 

367 Resolves policy dynamically based on the COMMAND output. 

368 """ 

369 

370 def __init__( 

371 self, commands_parser: CommandsParser, fallback: Optional[PolicyResolver] = None 

372 ) -> None: 

373 """ 

374 Parameters: 

375 commands_parser (CommandsParser): COMMAND output parser. 

376 fallback (Optional[PolicyResolver]): An optional resolver to be used when the 

377 primary policies cannot handle a specific request. 

378 """ 

379 self._commands_parser = commands_parser 

380 self._init_from_metadata_resolver( 

381 DynamicMetadataResolver(_load_commands_metadata_cache(commands_parser)), 

382 fallback, 

383 ) 

384 

385 def with_fallback(self, fallback: "PolicyResolver") -> "PolicyResolver": 

386 return DynamicPolicyResolver(self._commands_parser, fallback) 

387 

388 

389class StaticPolicyResolver(BasePolicyResolver): 

390 """ 

391 Resolves policy from a static list of command metadata records. 

392 """ 

393 

394 def __init__( 

395 self, 

396 fallback: Optional[PolicyResolver] = None, 

397 metadata_resolver: Optional[MetadataResolver] = None, 

398 ) -> None: 

399 """ 

400 Parameters: 

401 fallback (Optional[PolicyResolver]): An optional fallback policy resolver 

402 used for resolving policies if static policies are inadequate. 

403 metadata_resolver (Optional[MetadataResolver]): The metadata resolver to project 

404 the routing view of. Defaults to a ``StaticMetadataResolver``. Pass one to 

405 route by the same records another consumer - the client-side cache, say - 

406 resolves through, so a client configured with a single metadata resolver has 

407 a single source of truth. A chain that starts with a static resolver keeps 

408 serving the static records first, which is what this class promises. 

409 """ 

410 if metadata_resolver is None: 

411 metadata_resolver = StaticMetadataResolver() 

412 

413 self._init_from_metadata_resolver(metadata_resolver, fallback) 

414 

415 def with_fallback(self, fallback: "PolicyResolver") -> "PolicyResolver": 

416 return StaticPolicyResolver(fallback, self._metadata_resolver) 

417 

418 

419class AsyncDynamicPolicyResolver(AsyncBasePolicyResolver): 

420 """ 

421 Async version of DynamicPolicyResolver. 

422 

423 Takes records rather than the parser that produced them, because 

424 ``AsyncCommandsParser.get_commands_metadata_cache`` is a coroutine and cannot be awaited in a 

425 constructor. That is why this class and its sync counterpart differ in what they accept, 

426 and the difference is forced rather than an oversight: the sync parser can be read in 

427 ``__init__`` and the async one cannot. To serve full command metadata asynchronously, 

428 build an ``AsyncDynamicMetadataResolver`` from the records and pass it to 

429 ``AsyncBasePolicyResolver`` through ``AsyncStaticPolicyResolver(metadata_resolver=...)``. 

430 """ 

431 

432 def __init__( 

433 self, 

434 policy_records: PolicyRecords, 

435 fallback: Optional[AsyncPolicyResolver] = None, 

436 ) -> None: 

437 """ 

438 Parameters: 

439 policy_records (PolicyRecords): Policy records, lifted into metadata 

440 records where every other field keeps its fail-closed default - so a resolver 

441 built this way reports no command as client-side-cacheable, which is the 

442 conservative answer for metadata it was never given. Keys are lowercased, 

443 because that is how a resolved command name is looked up. 

444 fallback (Optional[AsyncPolicyResolver]): An optional resolver to be used when the 

445 primary policies cannot handle a specific request. 

446 """ 

447 self._policy_records = policy_records 

448 super().__init__(policy_records, fallback) 

449 

450 def with_fallback(self, fallback: "AsyncPolicyResolver") -> "AsyncPolicyResolver": 

451 return AsyncDynamicPolicyResolver(self._policy_records, fallback) 

452 

453 

454class AsyncStaticPolicyResolver(AsyncBasePolicyResolver): 

455 """ 

456 Async version of StaticPolicyResolver. 

457 """ 

458 

459 def __init__( 

460 self, 

461 fallback: Optional[AsyncPolicyResolver] = None, 

462 metadata_resolver: Optional[AsyncMetadataResolver] = None, 

463 ) -> None: 

464 """ 

465 Parameters: 

466 fallback (Optional[AsyncPolicyResolver]): An optional fallback policy resolver 

467 used for resolving policies if static policies are inadequate. 

468 metadata_resolver (Optional[AsyncMetadataResolver]): The metadata resolver to 

469 project the routing view of. Defaults to an ``AsyncStaticMetadataResolver``. 

470 Mirrors ``StaticPolicyResolver`` - see its note. 

471 """ 

472 if metadata_resolver is None: 

473 metadata_resolver = AsyncStaticMetadataResolver() 

474 

475 self._init_from_metadata_resolver(metadata_resolver, fallback) 

476 

477 def with_fallback(self, fallback: "AsyncPolicyResolver") -> "AsyncPolicyResolver": 

478 return AsyncStaticPolicyResolver(fallback, self._metadata_resolver) 

479 

480 

481def __getattr__(name: str): 

482 """ 

483 Serve the deprecated ``STATIC_POLICIES`` table, warning on the way. 

484 

485 A module attribute cannot be deprecated by decoration, so the table is bound to a private 

486 name above and resolved here instead - which is only reached because no module attribute 

487 of that name exists. 

488 """ 

489 if name == "STATIC_POLICIES": 

490 warn_deprecated( 

491 "STATIC_POLICIES", 

492 reason=( 

493 "Nothing in this library reads this table any more. It is a frozen copy of " 

494 "the 7.1.0 routing table, kept for backwards compatibility only, and it does " 

495 "not describe what this client routes by" 

496 ), 

497 version="8.1.0", 

498 stacklevel=3, 

499 ) 

500 return _DEPRECATED_STATIC_POLICIES 

501 

502 raise AttributeError(f"module {__name__!r} has no attribute {name!r}")