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