11"""Utilities for connecting to and managing Redis connections.""" 
22
3+ import  logging 
34from  typing  import  Optional 
45
56from  pydantic_settings  import  BaseSettings 
67from  redis  import  asyncio  as  aioredis 
78from  redis .asyncio .sentinel  import  Sentinel 
89
10+ from  stac_fastapi .core .utilities  import  get_bool_env 
11+ 
912redis_pool : Optional [aioredis .Redis ] =  None 
1013
14+ logger  =  logging .getLogger (__name__ )
15+ 
1116
1217class  RedisSentinelSettings (BaseSettings ):
1318    """Configuration for connecting to Redis Sentinel.""" 
@@ -38,75 +43,76 @@ class RedisSettings(BaseSettings):
3843    REDIS_HEALTH_CHECK_INTERVAL : int  =  30 
3944
4045
41- # Select the Redis or Redis Sentinel configuration  
42- redis_settings :  BaseSettings   =   RedisSettings ()
43- 
46+ # Create settings instances  
47+ sentinel_settings   =   RedisSentinelSettings ()
48+ standalone_settings   =   RedisSettings () 
4449
45- async  def  connect_redis (settings : Optional [RedisSettings ] =  None ) ->  aioredis .Redis :
46-     """Return a Redis connection.""" 
47-     global  redis_pool 
48-     settings  =  settings  or  redis_settings 
49- 
50-     if  not  settings .REDIS_HOST  or  not  settings .REDIS_PORT :
51-         return  None 
5250
53-     if  redis_pool  is  None :
54-         pool  =  aioredis .ConnectionPool (
55-             host = settings .REDIS_HOST ,
56-             port = settings .REDIS_PORT ,
57-             db = settings .REDIS_DB ,
58-             max_connections = settings .REDIS_MAX_CONNECTIONS ,
59-             decode_responses = settings .REDIS_DECODE_RESPONSES ,
60-             retry_on_timeout = settings .REDIS_RETRY_TIMEOUT ,
61-             health_check_interval = settings .REDIS_HEALTH_CHECK_INTERVAL ,
62-         )
63-         redis_pool  =  aioredis .Redis (
64-             connection_pool = pool , client_name = settings .REDIS_CLIENT_NAME 
65-         )
66-     return  redis_pool 
67- 
68- 
69- async  def  connect_redis_sentinel (
70-     settings : Optional [RedisSentinelSettings ] =  None ,
71- ) ->  Optional [aioredis .Redis ]:
72-     """Return a Redis Sentinel connection.""" 
51+ async  def  connect_redis () ->  Optional [aioredis .Redis ]:
52+     """Return a Redis connection, automatically detecting Sentinel or standalone.""" 
7353    global  redis_pool 
7454
75-     settings  =  settings  or  redis_settings 
55+     if  redis_pool  is  not None :
56+         return  redis_pool 
57+ 
58+     try :
59+         # Check if Sentinel is configured 
60+         if  sentinel_settings .REDIS_SENTINEL_HOSTS :
61+             # Use Sentinel connection 
62+             hosts  =  [
63+                 h .strip ()
64+                 for  h  in  sentinel_settings .REDIS_SENTINEL_HOSTS .split ("," )
65+                 if  h .strip ()
66+             ]
67+             ports  =  [
68+                 int (p .strip ())
69+                 for  p  in  sentinel_settings .REDIS_SENTINEL_PORTS .split ("," )
70+                 if  p .strip ()
71+             ]
7672
77-     if  (
78-         not  settings .REDIS_SENTINEL_HOSTS 
79-         or  not  settings .REDIS_SENTINEL_PORTS 
80-         or  not  settings .REDIS_SENTINEL_MASTER_NAME 
81-     ):
82-         return  None 
83- 
84-     hosts  =  [h .strip () for  h  in  settings .REDIS_SENTINEL_HOSTS .split ("," ) if  h .strip ()]
85-     ports  =  [
86-         int (p .strip ()) for  p  in  settings .REDIS_SENTINEL_PORTS .split ("," ) if  p .strip ()
87-     ]
88- 
89-     if  redis_pool  is  None :
90-         try :
9173            sentinel  =  Sentinel (
9274                [(h , p ) for  h , p  in  zip (hosts , ports )],
93-                 decode_responses = settings .REDIS_DECODE_RESPONSES ,
75+                 decode_responses = sentinel_settings .REDIS_DECODE_RESPONSES ,
9476            )
95-             master  =  sentinel .master_for (
96-                 service_name = settings .REDIS_SENTINEL_MASTER_NAME ,
97-                 db = settings .REDIS_DB ,
98-                 decode_responses = settings .REDIS_DECODE_RESPONSES ,
99-                 retry_on_timeout = settings .REDIS_RETRY_TIMEOUT ,
100-                 client_name = settings .REDIS_CLIENT_NAME ,
101-                 max_connections = settings .REDIS_MAX_CONNECTIONS ,
102-                 health_check_interval = settings .REDIS_HEALTH_CHECK_INTERVAL ,
77+ 
78+             redis_pool  =  sentinel .master_for (
79+                 service_name = sentinel_settings .REDIS_SENTINEL_MASTER_NAME ,
80+                 db = sentinel_settings .REDIS_DB ,
81+                 decode_responses = sentinel_settings .REDIS_DECODE_RESPONSES ,
82+                 retry_on_timeout = sentinel_settings .REDIS_RETRY_TIMEOUT ,
83+                 client_name = sentinel_settings .REDIS_CLIENT_NAME ,
84+                 max_connections = sentinel_settings .REDIS_MAX_CONNECTIONS ,
85+                 health_check_interval = sentinel_settings .REDIS_HEALTH_CHECK_INTERVAL ,
10386            )
104-             redis_pool  =  master 
10587
106-         except  Exception :
88+             logger .info ("Connected to Redis Sentinel" )
89+ 
90+         elif  standalone_settings .REDIS_HOST :
91+             # Use standalone Redis connection 
92+             pool  =  aioredis .ConnectionPool (
93+                 host = standalone_settings .REDIS_HOST ,
94+                 port = standalone_settings .REDIS_PORT ,
95+                 db = standalone_settings .REDIS_DB ,
96+                 max_connections = standalone_settings .REDIS_MAX_CONNECTIONS ,
97+                 decode_responses = standalone_settings .REDIS_DECODE_RESPONSES ,
98+                 retry_on_timeout = standalone_settings .REDIS_RETRY_TIMEOUT ,
99+                 health_check_interval = standalone_settings .REDIS_HEALTH_CHECK_INTERVAL ,
100+             )
101+             redis_pool  =  aioredis .Redis (
102+                 connection_pool = pool , client_name = standalone_settings .REDIS_CLIENT_NAME 
103+             )
104+ 
105+             logger .info ("Connected to standalone Redis" )
106+         else :
107+             logger .warning ("No Redis configuration found" )
107108            return  None 
108109
109-     return  redis_pool 
110+         return  redis_pool 
111+ 
112+     except  Exception  as  e :
113+         logger .error (f"Failed to connect to Redis: { e }  )
114+         redis_pool  =  None 
115+         return  None 
110116
111117
112118async  def  save_self_link (
@@ -122,3 +128,34 @@ async def get_prev_link(redis: aioredis.Redis, token: Optional[str]) -> Optional
122128    if  not  token :
123129        return  None 
124130    return  await  redis .get (f"nav:self:{ token }  )
131+ 
132+ 
133+ async  def  handle_pagination_links (
134+     current_url : str , token : str , next_token : str , links : list 
135+ ) ->  None :
136+     """Handle Redis pagination.""" 
137+     redis_enable  =  get_bool_env ("REDIS_ENABLE" , default = False )
138+     redis  =  None 
139+     if  redis_enable :
140+         try :
141+             redis  =  await  connect_redis ()
142+             logger .info ("Redis connection established successfully" )
143+         except  Exception  as  e :
144+             redis  =  None 
145+             logger .warning (f"Redis connection failed, continuing without Redis: { e }  )
146+ 
147+     if  redis_enable  and  redis :
148+         if  next_token :
149+             await  save_self_link (redis , next_token , current_url )
150+ 
151+         prev_link  =  await  get_prev_link (redis , token )
152+         if  prev_link :
153+             links .insert (
154+                 0 ,
155+                 {
156+                     "rel" : "prev" ,
157+                     "type" : "application/json" ,
158+                     "method" : "GET" ,
159+                     "href" : prev_link ,
160+                 },
161+             )
0 commit comments