44import logging
55from typing import List , Optional , Tuple
66
7- from pydantic import field_validator
7+ # from pydantic import field_validator
88from pydantic_settings import BaseSettings
99from redis import asyncio as aioredis
1010from redis .asyncio .sentinel import Sentinel
1313
1414
1515class RedisSentinelSettings (BaseSettings ):
16- """Configuration for connecting to Redis Sentinel."""
16+ """Configuration settings for connecting to Redis Sentinel."""
1717
1818 REDIS_SENTINEL_HOSTS : str = ""
1919 REDIS_SENTINEL_PORTS : str = "26379"
2020 REDIS_SENTINEL_MASTER_NAME : str = "master"
21- REDIS_DB : int = 15
21+ REDIS_DB : int = 0
2222
2323 REDIS_MAX_CONNECTIONS : int = 10
24- REDIS_RETRY_TIMEOUT : bool = True
2524 REDIS_DECODE_RESPONSES : bool = True
2625 REDIS_CLIENT_NAME : str = "stac-fastapi-app"
2726 REDIS_HEALTH_CHECK_INTERVAL : int = 30
2827 REDIS_SELF_LINK_TTL : int = 1800
2928
30- @field_validator ("REDIS_DB" )
31- @classmethod
32- def validate_db_sentinel (cls , v : int ) -> int :
33- """Validate REDIS_DB is not negative int."""
34- if v < 0 :
35- raise ValueError ("REDIS_DB must be a positive integer" )
36- return v
37-
38- @field_validator ("REDIS_MAX_CONNECTIONS" )
39- @classmethod
40- def validate_max_connections_sentinel (cls , v : int ) -> int :
41- """Validate REDIS_MAX_CONNECTIONS is at least 1."""
42- if v < 1 :
43- raise ValueError ("REDIS_MAX_CONNECTIONS must be at least 1" )
44- return v
45-
46- @field_validator ("REDIS_HEALTH_CHECK_INTERVAL" )
47- @classmethod
48- def validate_health_check_interval_sentinel (cls , v : int ) -> int :
49- """Validate REDIS_HEALTH_CHECK_INTERVAL is not negative integer."""
50- if v < 0 :
51- raise ValueError ("REDIS_HEALTH_CHECK_INTERVAL must be a positive integer" )
52- return v
53-
54- @field_validator ("REDIS_SELF_LINK_TTL" )
55- @classmethod
56- def validate_self_link_ttl_sentinel (cls , v : int ) -> int :
57- """Validate REDIS_SELF_LINK_TTL is not a negative integer."""
58- if v < 0 :
59- raise ValueError ("REDIS_SELF_LINK_TTL must be a positive integer" )
60- return v
61-
62- def get_sentinel_hosts (self ) -> List [str ]:
63- """Parse Redis Sentinel hosts from string to list."""
64- if not self .REDIS_SENTINEL_HOSTS :
65- return []
66-
67- if self .REDIS_SENTINEL_HOSTS .strip ().startswith ("[" ):
68- return json .loads (self .REDIS_SENTINEL_HOSTS )
69- else :
70- return [
29+ def get_sentinel_nodes (self ) -> List [Tuple [str , int ]]:
30+ """Return list of (host, port) tuples."""
31+ try :
32+ hosts = json .loads (self .REDIS_SENTINEL_HOSTS )
33+ ports = json .loads (self .REDIS_SENTINEL_PORTS )
34+ except json .JSONDecodeError :
35+ hosts = [
7136 h .strip () for h in self .REDIS_SENTINEL_HOSTS .split ("," ) if h .strip ()
7237 ]
73-
74- def get_sentinel_ports (self ) -> List [int ]:
75- """Parse Redis Sentinel ports from string to list of integers."""
76- if not self .REDIS_SENTINEL_PORTS :
77- return [26379 ]
78-
79- if self .REDIS_SENTINEL_PORTS .strip ().startswith ("[" ):
80- return json .loads (self .REDIS_SENTINEL_PORTS )
81- else :
82- ports_str_list = [
83- p .strip () for p in self .REDIS_SENTINEL_PORTS .split ("," ) if p .strip ()
38+ ports = [
39+ int (p .strip ())
40+ for p in self .REDIS_SENTINEL_PORTS .split ("," )
41+ if p .strip ()
8442 ]
85- return [int (port ) for port in ports_str_list ]
86-
87- def get_sentinel_nodes (self ) -> List [Tuple [str , int ]]:
88- """Get list of (host, port) tuples for Sentinel connection."""
89- hosts = self .get_sentinel_hosts ()
90- ports = self .get_sentinel_ports ()
91-
92- if not hosts :
93- return []
9443
9544 if len (ports ) == 1 and len (hosts ) > 1 :
9645 ports = ports * len (hosts )
9746
98- if len (hosts ) != len (ports ):
99- raise ValueError (
100- f"Mismatch between hosts ({ len (hosts )} ) and ports ({ len (ports )} )"
101- )
102-
103- return [(str (host ), int (port )) for host , port in zip (hosts , ports )]
47+ return list (zip (hosts , ports ))
10448
10549
10650class RedisSettings (BaseSettings ):
107- """Configuration for connecting Redis."""
51+ """Configuration settings for connecting to a standalone Redis instance ."""
10852
10953 REDIS_HOST : str = ""
11054 REDIS_PORT : int = 6379
11155 REDIS_DB : int = 0
11256
11357 REDIS_MAX_CONNECTIONS : int = 10
114- REDIS_RETRY_TIMEOUT : bool = True
11558 REDIS_DECODE_RESPONSES : bool = True
11659 REDIS_CLIENT_NAME : str = "stac-fastapi-app"
11760 REDIS_HEALTH_CHECK_INTERVAL : int = 30
11861 REDIS_SELF_LINK_TTL : int = 1800
11962
120- @field_validator ("REDIS_PORT" )
121- @classmethod
122- def validate_port_standalone (cls , v : int ) -> int :
123- """Validate REDIS_PORT is not a negative integer."""
124- if v < 0 :
125- raise ValueError ("REDIS_PORT must be a positive integer" )
126- return v
127-
128- @field_validator ("REDIS_DB" )
129- @classmethod
130- def validate_db_standalone (cls , v : int ) -> int :
131- """Validate REDIS_DB is not a negative integer."""
132- if v < 0 :
133- raise ValueError ("REDIS_DB must be a positive integer" )
134- return v
135-
136- @field_validator ("REDIS_MAX_CONNECTIONS" )
137- @classmethod
138- def validate_max_connections_standalone (cls , v : int ) -> int :
139- """Validate REDIS_MAX_CONNECTIONS is at least 1."""
140- if v < 1 :
141- raise ValueError ("REDIS_MAX_CONNECTIONS must be at least 1" )
142- return v
143-
144- @field_validator ("REDIS_HEALTH_CHECK_INTERVAL" )
145- @classmethod
146- def validate_health_check_interval_standalone (cls , v : int ) -> int :
147- """Validate REDIS_HEALTH_CHECK_INTERVAL is not a negative."""
148- if v < 0 :
149- raise ValueError ("REDIS_HEALTH_CHECK_INTERVAL must be a positive integer" )
150- return v
151-
152- @field_validator ("REDIS_SELF_LINK_TTL" )
153- @classmethod
154- def validate_self_link_ttl_standalone (cls , v : int ) -> int :
155- """Validate REDIS_SELF_LINK_TTL is negative."""
156- if v < 0 :
157- raise ValueError ("REDIS_SELF_LINK_TTL must be a positive integer" )
158- return v
159-
160-
161- # Configure only one Redis configuration
63+
16264sentinel_settings = RedisSentinelSettings ()
16365standalone_settings = RedisSettings ()
16466
16567
68+ redis : Optional [aioredis .Redis ] = None
69+
70+
16671async def connect_redis () -> Optional [aioredis .Redis ]:
167- """Return a Redis connection Redis or Redis Sentinel."""
72+ """Initialize global Redis connection (Sentinel or Standalone)."""
73+ global redis
74+ if redis :
75+ return redis
76+
16877 try :
16978 if sentinel_settings .REDIS_SENTINEL_HOSTS :
17079 sentinel_nodes = sentinel_settings .get_sentinel_nodes ()
17180 sentinel = Sentinel (
17281 sentinel_nodes ,
173- decode_responses = sentinel_settings . REDIS_DECODE_RESPONSES ,
82+ decode_responses = True ,
17483 )
17584
17685 redis = sentinel .master_for (
17786 service_name = sentinel_settings .REDIS_SENTINEL_MASTER_NAME ,
17887 db = sentinel_settings .REDIS_DB ,
179- decode_responses = sentinel_settings .REDIS_DECODE_RESPONSES ,
180- retry_on_timeout = sentinel_settings .REDIS_RETRY_TIMEOUT ,
88+ decode_responses = True ,
18189 client_name = sentinel_settings .REDIS_CLIENT_NAME ,
18290 max_connections = sentinel_settings .REDIS_MAX_CONNECTIONS ,
18391 health_check_interval = sentinel_settings .REDIS_HEALTH_CHECK_INTERVAL ,
18492 )
18593 logger .info ("Connected to Redis Sentinel" )
18694
18795 elif standalone_settings .REDIS_HOST :
188- pool = aioredis .ConnectionPool (
96+ redis = aioredis .Redis (
18997 host = standalone_settings .REDIS_HOST ,
19098 port = standalone_settings .REDIS_PORT ,
19199 db = standalone_settings .REDIS_DB ,
192- max_connections = standalone_settings .REDIS_MAX_CONNECTIONS ,
193- decode_responses = standalone_settings .REDIS_DECODE_RESPONSES ,
194- retry_on_timeout = standalone_settings .REDIS_RETRY_TIMEOUT ,
100+ decode_responses = True ,
101+ client_name = standalone_settings .REDIS_CLIENT_NAME ,
195102 health_check_interval = standalone_settings .REDIS_HEALTH_CHECK_INTERVAL ,
196103 )
197- redis = aioredis .Redis (
198- connection_pool = pool , client_name = standalone_settings .REDIS_CLIENT_NAME
199- )
200- logger .info ("Connected to Redis" )
104+ logger .info ("Connected to standalone Redis" )
105+
201106 else :
202- logger .warning ("No Redis configuration found" )
107+ logger .warning ("No Redis configuration found. " )
203108 return None
204109
110+ await redis .ping ()
205111 return redis
206112
207- except aioredis .ConnectionError as e :
208- logger .error (f"Redis connection error: { e } " )
209- return None
210- except aioredis .AuthenticationError as e :
211- logger .error (f"Redis authentication error: { e } " )
212- return None
213- except aioredis .TimeoutError as e :
214- logger .error (f"Redis timeout error: { e } " )
215- return None
216113 except Exception as e :
217114 logger .error (f"Failed to connect to Redis: { e } " )
115+ redis = None
218116 return None
219117
220118
119+ async def close_redis ():
120+ """Close global Redis connection."""
121+ global redis
122+ if redis :
123+ await redis .close ()
124+ redis = None
125+ logger .info ("Redis connection closed." )
126+
127+
221128async def save_self_link (
222129 redis : aioredis .Redis , token : Optional [str ], self_href : str
223130) -> None :
224- """Save the self link for the current token."""
225- if token :
226- if sentinel_settings .REDIS_SENTINEL_HOSTS :
227- ttl_seconds = sentinel_settings .REDIS_SELF_LINK_TTL
228- elif standalone_settings .REDIS_HOST :
229- ttl_seconds = standalone_settings .REDIS_SELF_LINK_TTL
230- await redis .setex (f"nav:self:{ token } " , ttl_seconds , self_href )
131+ """Save current self link for token."""
132+ if not token :
133+ return
134+
135+ ttl = (
136+ sentinel_settings .REDIS_SELF_LINK_TTL
137+ if sentinel_settings .REDIS_SENTINEL_HOSTS
138+ else standalone_settings .REDIS_SELF_LINK_TTL
139+ )
140+ await redis .setex (f"nav:self:{ token } " , ttl , self_href )
231141
232142
233143async def get_prev_link (redis : aioredis .Redis , token : Optional [str ]) -> Optional [str ]:
234- """Get the previous page link for the current token (if exists) ."""
144+ """Return previous page link for token."""
235145 if not token :
236146 return None
237147 return await redis .get (f"nav:self:{ token } " )
@@ -240,17 +150,17 @@ async def get_prev_link(redis: aioredis.Redis, token: Optional[str]) -> Optional
240150async def redis_pagination_links (
241151 current_url : str , token : str , next_token : str , links : list
242152) -> None :
243- """Handle Redis pagination."""
244- redis = await connect_redis ()
245- if not redis :
246- logger .warning ("Redis connection failed ." )
153+ """Manage pagination links stored in Redis ."""
154+ redis_conn = await connect_redis ()
155+ if not redis_conn :
156+ logger .warning ("Redis not available for pagination ." )
247157 return
248158
249159 try :
250160 if next_token :
251- await save_self_link (redis , next_token , current_url )
161+ await save_self_link (redis_conn , next_token , current_url )
252162
253- prev_link = await get_prev_link (redis , token )
163+ prev_link = await get_prev_link (redis_conn , token )
254164 if prev_link :
255165 links .insert (
256166 0 ,
@@ -262,6 +172,4 @@ async def redis_pagination_links(
262172 },
263173 )
264174 except Exception as e :
265- logger .warning (f"Redis pagination operation failed: { e } " )
266- finally :
267- await redis .close ()
175+ logger .warning (f"Redis pagination failed: { e } " )
0 commit comments