1- from typing import Optional
1+ from typing import Callable , ContextManager , Iterable , List , Literal , Optional , Protocol , overload
22from urllib .parse import quote , urlparse , urlunparse
33import logging
44import time
55
6+ from redis .typing import EncodableT , ExpiryT , FieldT , KeysT , KeyT , ZScoreBoundT
7+ import redis .client
8+
69import redis
710
811from .base_config import BaseConfig
@@ -15,7 +18,155 @@ class RedisConfig(BaseConfig):
1518 redis_password : Optional [str ] = None
1619
1720
18- def get_redis_client (config : RedisConfig ) -> "redis.Redis[bytes]" :
21+ class TypedRedis (Protocol ):
22+ """
23+ A typed subset of redis.Redis methods used by flatpak-indexer.
24+
25+ The redis-py type annotations are a mess: async and sync return types
26+ are mixed together, and there is no representation of the fact that
27+ the client *might* decode the responses from bytes to str, or not,
28+ depending on how it was constructed.
29+
30+ This Protocol represents the methods we actually use, with more precise
31+ types - we simply cast the redis.Redis instances we get to this type.
32+ """
33+
34+ def delete (self , * names : KeyT ) -> int : ...
35+
36+ def execute (self ) -> List [object ]: ...
37+
38+ def exists (self , name : KeyT ) -> bool : ...
39+
40+ def hmget (self , name : KeyT , keys : List [KeyT ], * args ) -> list [bytes | None ]: ...
41+
42+ def hget (self , name : KeyT , key : KeyT ) -> bytes | None : ...
43+
44+ def get (self , name : KeyT ) -> Optional [bytes ]: ...
45+
46+ @overload
47+ def hset (self , name : KeyT , key : KeyT , value : EncodableT ) -> int : ...
48+
49+ @overload
50+ def hset (self , name : KeyT , mapping : dict [KeyT , EncodableT ]) -> int : ...
51+
52+ def mget (self , keys : KeysT , * args ) -> list [bytes | None ]: ...
53+
54+ def multi (self ): ...
55+
56+ def pipeline (self , * args , ** kwargs ) -> "TypedPipeline" : ...
57+
58+ def publish (self , channel : KeyT , message : EncodableT ) -> int : ...
59+
60+ def sadd (self , name : KeyT , * values : FieldT ) -> int : ...
61+
62+ def scan_iter (self , match : Optional [EncodableT ] = None ) -> Iterable [str ]: ...
63+
64+ def scard (self , name : KeyT ) -> int : ...
65+
66+ def set (self , name : KeyT , value : EncodableT ) -> bool : ...
67+
68+ def setex (self , name : KeyT , time : ExpiryT , value : EncodableT ) -> bool : ...
69+
70+ @overload
71+ def srandmember (self , name : KeyT , number : None = None ) -> Optional [bytes ]: ...
72+
73+ @overload
74+ def srandmember (self , name : KeyT , number : int ) -> List [bytes ]: ...
75+
76+ def srem (self , name : KeyT , * values : FieldT ) -> int : ...
77+
78+ def transaction (self , func : Callable [["TypedPipeline" ], None ]): ...
79+
80+ def watch (self , keys : KeysT ): ...
81+
82+ def zadd (self , name : KeyT , mapping : dict [EncodableT , float ], xx : bool = False ) -> int : ...
83+
84+ def zcard (self , name : KeyT ) -> int : ...
85+
86+ @overload
87+ def zrange (
88+ self ,
89+ name : KeyT ,
90+ start : int ,
91+ end : int ,
92+ * ,
93+ desc : bool = False ,
94+ withscores : Literal [False ] = False ,
95+ score_cast_func : type | Callable = float ,
96+ byscore : bool = False ,
97+ bylex : bool = False ,
98+ offset : Optional [int ] = None ,
99+ num : Optional [int ] = None ,
100+ ) -> list [bytes ]: ...
101+
102+ @overload
103+ def zrange (
104+ self ,
105+ name : KeyT ,
106+ start : int ,
107+ end : int ,
108+ * ,
109+ desc : bool = False ,
110+ withscores : Literal [True ],
111+ score_cast_func : type | Callable = float ,
112+ byscore : bool = False ,
113+ bylex : bool = False ,
114+ offset : Optional [int ] = None ,
115+ num : Optional [int ] = None ,
116+ ) -> list [tuple [bytes , float ]]: ...
117+
118+ def zrangebylex (
119+ self ,
120+ name : KeyT ,
121+ min : EncodableT ,
122+ max : EncodableT ,
123+ start : Optional [int ] = None ,
124+ num : Optional [int ] = None ,
125+ ) -> list [bytes ]: ...
126+
127+ @overload
128+ def zrangebyscore (
129+ self ,
130+ name : KeyT ,
131+ min : ZScoreBoundT ,
132+ max : ZScoreBoundT ,
133+ * ,
134+ start : Optional [int ] = None ,
135+ num : Optional [int ] = None ,
136+ withscores : Literal [False ] = False ,
137+ score_cast_func : type | Callable = float ,
138+ ) -> list [bytes ]: ...
139+
140+ @overload
141+ def zrangebyscore (
142+ self ,
143+ name : KeyT ,
144+ min : ZScoreBoundT ,
145+ max : ZScoreBoundT ,
146+ * ,
147+ start : Optional [int ] = None ,
148+ num : Optional [int ] = None ,
149+ withscores : Literal [True ],
150+ score_cast_func : type | Callable = float ,
151+ ) -> list [tuple [bytes , float ]]: ...
152+
153+ def zrem (self , name : KeyT , * values : EncodableT ) -> int : ...
154+
155+ def zremrangebyscore (
156+ self ,
157+ name : KeyT ,
158+ min : ZScoreBoundT ,
159+ max : ZScoreBoundT ,
160+ ) -> int : ...
161+
162+ def zscore (self , name : KeyT , value : EncodableT ) -> float | None : ...
163+
164+
165+ class TypedPipeline (TypedRedis , ContextManager ["TypedPipeline" ]):
166+ pass
167+
168+
169+ def get_redis_client (config : RedisConfig ) -> TypedRedis :
19170 url = config .redis_url
20171
21172 # redis.Redis.from_url() doesn't support passing the password separately
0 commit comments