44import logging
55from typing import List , Optional , Tuple
66
7- from pydantic import field_validator
87from pydantic_settings import BaseSettings
98from redis import asyncio as aioredis
109from redis .asyncio .sentinel import Sentinel
1312
1413
1514class RedisSentinelSettings (BaseSettings ):
16- """Configuration for connecting to Redis Sentinel."""
15+ """Configuration settings for connecting to Redis Sentinel."""
1716
1817 REDIS_SENTINEL_HOSTS : str = ""
1918 REDIS_SENTINEL_PORTS : str = "26379"
2019 REDIS_SENTINEL_MASTER_NAME : str = "master"
21- REDIS_DB : int = 15
20+ REDIS_DB : int = 0
2221
2322 REDIS_MAX_CONNECTIONS : int = 10
24- REDIS_RETRY_TIMEOUT : bool = True
2523 REDIS_DECODE_RESPONSES : bool = True
2624 REDIS_CLIENT_NAME : str = "stac-fastapi-app"
2725 REDIS_HEALTH_CHECK_INTERVAL : int = 30
2826 REDIS_SELF_LINK_TTL : int = 1800
2927
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 [
71- h .strip () for h in self .REDIS_SENTINEL_HOSTS .split ("," ) if h .strip ()
72- ]
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 ()
84- ]
85- return [int (port ) for port in ports_str_list ]
86-
8728 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 []
29+ """Return list of (host, port) tuples."""
30+ try :
31+ hosts = json .loads (self .REDIS_SENTINEL_HOSTS )
32+ ports = json .loads (self .REDIS_SENTINEL_PORTS )
33+ except json .JSONDecodeError :
34+ hosts = [h .strip () for h in self .REDIS_SENTINEL_HOSTS .split ("," ) if h .strip ()]
35+ ports = [int (p .strip ()) for p in self .REDIS_SENTINEL_PORTS .split ("," ) if p .strip ()]
9436
9537 if len (ports ) == 1 and len (hosts ) > 1 :
9638 ports = ports * len (hosts )
9739
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 )]
40+ return list (zip (hosts , ports ))
10441
10542
10643class RedisSettings (BaseSettings ):
107- """Configuration for connecting Redis."""
44+ """Configuration settings for connecting to a standalone Redis instance ."""
10845
10946 REDIS_HOST : str = ""
11047 REDIS_PORT : int = 6379
11148 REDIS_DB : int = 0
11249
11350 REDIS_MAX_CONNECTIONS : int = 10
114- REDIS_RETRY_TIMEOUT : bool = True
11551 REDIS_DECODE_RESPONSES : bool = True
11652 REDIS_CLIENT_NAME : str = "stac-fastapi-app"
11753 REDIS_HEALTH_CHECK_INTERVAL : int = 30
11854 REDIS_SELF_LINK_TTL : int = 1800
11955
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
56+
16257sentinel_settings = RedisSentinelSettings ()
16358standalone_settings = RedisSettings ()
16459
60+ redis : Optional [aioredis .Redis ] = None
61+
16562
16663async def connect_redis () -> Optional [aioredis .Redis ]:
167- """Return a Redis connection Redis or Redis Sentinel."""
64+ """Initialize global Redis connection (Sentinel or Standalone)."""
65+ global redis
66+ if redis :
67+ return redis
68+
16869 try :
169- if sentinel_settings .REDIS_SENTINEL_HOSTS :
70+ if sentinel_settings .REDIS_SENTINEL_HOSTS . strip () :
17071 sentinel_nodes = sentinel_settings .get_sentinel_nodes ()
17172 sentinel = Sentinel (
17273 sentinel_nodes ,
173- decode_responses = sentinel_settings . REDIS_DECODE_RESPONSES ,
74+ decode_responses = True ,
17475 )
17576
17677 redis = sentinel .master_for (
17778 service_name = sentinel_settings .REDIS_SENTINEL_MASTER_NAME ,
17879 db = sentinel_settings .REDIS_DB ,
179- decode_responses = sentinel_settings .REDIS_DECODE_RESPONSES ,
180- retry_on_timeout = sentinel_settings .REDIS_RETRY_TIMEOUT ,
80+ decode_responses = True ,
18181 client_name = sentinel_settings .REDIS_CLIENT_NAME ,
18282 max_connections = sentinel_settings .REDIS_MAX_CONNECTIONS ,
18383 health_check_interval = sentinel_settings .REDIS_HEALTH_CHECK_INTERVAL ,
18484 )
18585 logger .info ("Connected to Redis Sentinel" )
86+ return redis
18687
187- elif standalone_settings .REDIS_HOST :
188- pool = aioredis .ConnectionPool (
88+ if standalone_settings .REDIS_HOST . strip () :
89+ redis = aioredis .Redis (
18990 host = standalone_settings .REDIS_HOST ,
19091 port = standalone_settings .REDIS_PORT ,
19192 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 ,
93+ decode_responses = True ,
94+ client_name = standalone_settings .REDIS_CLIENT_NAME ,
19595 health_check_interval = standalone_settings .REDIS_HEALTH_CHECK_INTERVAL ,
19696 )
197- redis = aioredis .Redis (
198- connection_pool = pool , client_name = standalone_settings .REDIS_CLIENT_NAME
199- )
200- logger .info ("Connected to Redis" )
201- else :
202- logger .warning ("No Redis configuration found" )
203- return None
97+ logger .info ("Connected to standalone Redis" )
98+ return redis
20499
205- return redis
206-
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 } " )
100+ logger .warning ("No Redis configuration found — skipping connection." )
215101 return None
102+
216103 except Exception as e :
217104 logger .error (f"Failed to connect to Redis: { e } " )
105+ redis = None
218106 return None
219107
220108
221- async def save_self_link (
222- redis : aioredis .Redis , token : Optional [str ], self_href : str
223- ) -> 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 )
109+ async def close_redis ():
110+ """Close global Redis connection."""
111+ global redis
112+ if redis :
113+ await redis .close ()
114+ redis = None
115+ logger .info ("Redis connection closed." )
116+
117+
118+ async def save_self_link (redis : aioredis .Redis , token : Optional [str ], self_href : str ) -> None :
119+ """Save current self link for token."""
120+ if not token :
121+ return
122+
123+ ttl = (
124+ sentinel_settings .REDIS_SELF_LINK_TTL
125+ if sentinel_settings .REDIS_SENTINEL_HOSTS .strip ()
126+ else standalone_settings .REDIS_SELF_LINK_TTL
127+ )
128+ await redis .setex (f"nav:self:{ token } " , ttl , self_href )
231129
232130
233131async def get_prev_link (redis : aioredis .Redis , token : Optional [str ]) -> Optional [str ]:
234- """Get the previous page link for the current token (if exists) ."""
132+ """Return previous page link for token."""
235133 if not token :
236134 return None
237135 return await redis .get (f"nav:self:{ token } " )
238136
239137
240138async def redis_pagination_links (
241- current_url : str , token : str , next_token : str , links : list
139+ current_url : str , token : str , next_token : str , links : list , redis_conn : Optional [ aioredis . Redis ] = None
242140) -> None :
243- """Handle Redis pagination."""
244- redis = await connect_redis ()
245- if not redis :
246- logger .warning ("Redis connection failed ." )
141+ """Manage pagination links stored in Redis ."""
142+ redis_conn = redis_conn or await connect_redis ()
143+ if not redis_conn :
144+ logger .warning ("Redis not available for pagination ." )
247145 return
248146
249147 try :
250148 if next_token :
251- await save_self_link (redis , next_token , current_url )
149+ await save_self_link (redis_conn , next_token , current_url )
252150
253- prev_link = await get_prev_link (redis , token )
151+ prev_link = await get_prev_link (redis_conn , token )
254152 if prev_link :
255153 links .insert (
256154 0 ,
@@ -262,6 +160,4 @@ async def redis_pagination_links(
262160 },
263161 )
264162 except Exception as e :
265- logger .warning (f"Redis pagination operation failed: { e } " )
266- finally :
267- await redis .close ()
163+ logger .warning (f"Redis pagination failed: { e } " )
0 commit comments