Skip to content

Commit 5edb5d3

Browse files
Yuri ZmytrakovYuri Zmytrakov
authored andcommitted
temporary
1 parent 5f552e0 commit 5edb5d3

File tree

2 files changed

+88
-184
lines changed

2 files changed

+88
-184
lines changed

stac_fastapi/core/stac_fastapi/core/redis_utils.py

Lines changed: 68 additions & 160 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import logging
55
from typing import List, Optional, Tuple
66

7-
from pydantic import field_validator
87
from pydantic_settings import BaseSettings
98
from redis import asyncio as aioredis
109
from redis.asyncio.sentinel import Sentinel
@@ -13,244 +12,155 @@
1312

1413

1514
class 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 [
28+
def get_sentinel_nodes(self) -> List[Tuple[str, int]]:
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 = [
7135
h.strip() for h in self.REDIS_SENTINEL_HOSTS.split(",") if h.strip()
7236
]
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()
37+
ports = [
38+
int(p.strip())
39+
for p in self.REDIS_SENTINEL_PORTS.split(",")
40+
if p.strip()
8441
]
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 []
9442

9543
if len(ports) == 1 and len(hosts) > 1:
9644
ports = ports * len(hosts)
9745

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)]
46+
return list(zip(hosts, ports))
10447

10548

10649
class RedisSettings(BaseSettings):
107-
"""Configuration for connecting Redis."""
50+
"""Configuration settings for connecting to a standalone Redis instance."""
10851

10952
REDIS_HOST: str = ""
11053
REDIS_PORT: int = 6379
11154
REDIS_DB: int = 0
11255

11356
REDIS_MAX_CONNECTIONS: int = 10
114-
REDIS_RETRY_TIMEOUT: bool = True
11557
REDIS_DECODE_RESPONSES: bool = True
11658
REDIS_CLIENT_NAME: str = "stac-fastapi-app"
11759
REDIS_HEALTH_CHECK_INTERVAL: int = 30
11860
REDIS_SELF_LINK_TTL: int = 1800
11961

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
62+
16263
sentinel_settings = RedisSentinelSettings()
16364
standalone_settings = RedisSettings()
16465

66+
redis: Optional[aioredis.Redis] = None
67+
16568

16669
async def connect_redis() -> Optional[aioredis.Redis]:
167-
"""Return a Redis connection Redis or Redis Sentinel."""
70+
"""Initialize global Redis connection (Sentinel or Standalone)."""
71+
global redis
72+
if redis:
73+
return redis
74+
16875
try:
169-
if sentinel_settings.REDIS_SENTINEL_HOSTS:
76+
if sentinel_settings.REDIS_SENTINEL_HOSTS.strip():
17077
sentinel_nodes = sentinel_settings.get_sentinel_nodes()
17178
sentinel = Sentinel(
17279
sentinel_nodes,
173-
decode_responses=sentinel_settings.REDIS_DECODE_RESPONSES,
80+
decode_responses=True,
17481
)
17582

17683
redis = sentinel.master_for(
17784
service_name=sentinel_settings.REDIS_SENTINEL_MASTER_NAME,
17885
db=sentinel_settings.REDIS_DB,
179-
decode_responses=sentinel_settings.REDIS_DECODE_RESPONSES,
180-
retry_on_timeout=sentinel_settings.REDIS_RETRY_TIMEOUT,
86+
decode_responses=True,
18187
client_name=sentinel_settings.REDIS_CLIENT_NAME,
18288
max_connections=sentinel_settings.REDIS_MAX_CONNECTIONS,
18389
health_check_interval=sentinel_settings.REDIS_HEALTH_CHECK_INTERVAL,
18490
)
18591
logger.info("Connected to Redis Sentinel")
92+
return redis
18693

187-
elif standalone_settings.REDIS_HOST:
188-
pool = aioredis.ConnectionPool(
94+
if standalone_settings.REDIS_HOST.strip():
95+
redis = aioredis.Redis(
18996
host=standalone_settings.REDIS_HOST,
19097
port=standalone_settings.REDIS_PORT,
19198
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,
99+
decode_responses=True,
100+
client_name=standalone_settings.REDIS_CLIENT_NAME,
195101
health_check_interval=standalone_settings.REDIS_HEALTH_CHECK_INTERVAL,
196102
)
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
204-
205-
return redis
103+
logger.info("Connected to standalone Redis")
104+
return redis
206105

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}")
106+
logger.warning("No Redis configuration found — skipping connection.")
215107
return None
108+
216109
except Exception as e:
217110
logger.error(f"Failed to connect to Redis: {e}")
111+
redis = None
218112
return None
219113

220114

115+
async def close_redis():
116+
"""Close global Redis connection."""
117+
global redis
118+
if redis:
119+
await redis.close()
120+
redis = None
121+
logger.info("Redis connection closed.")
122+
123+
221124
async def save_self_link(
222125
redis: aioredis.Redis, token: Optional[str], self_href: str
223126
) -> 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)
127+
"""Save current self link for token."""
128+
if not token:
129+
return
130+
131+
ttl = (
132+
sentinel_settings.REDIS_SELF_LINK_TTL
133+
if sentinel_settings.REDIS_SENTINEL_HOSTS.strip()
134+
else standalone_settings.REDIS_SELF_LINK_TTL
135+
)
136+
await redis.setex(f"nav:self:{token}", ttl, self_href)
231137

232138

233139
async def get_prev_link(redis: aioredis.Redis, token: Optional[str]) -> Optional[str]:
234-
"""Get the previous page link for the current token (if exists)."""
140+
"""Return previous page link for token."""
235141
if not token:
236142
return None
237143
return await redis.get(f"nav:self:{token}")
238144

239145

240146
async def redis_pagination_links(
241-
current_url: str, token: str, next_token: str, links: list
147+
current_url: str,
148+
token: str,
149+
next_token: str,
150+
links: list,
151+
redis_conn: Optional[aioredis.Redis] = None,
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 = redis_conn or 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}")

stac_fastapi/tests/redis/test_redis_utils.py

Lines changed: 20 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,38 +6,34 @@
66
@pytest.mark.asyncio
77
async def test_redis_connection():
88
"""Test Redis connection."""
9-
async with connect_redis() as redis:
10-
if redis is None:
11-
pytest.skip("Redis not configured")
9+
redis = await connect_redis()
1210

13-
await redis.set("string_key", "string_value")
14-
string_value = await redis.get("string_key")
15-
assert string_value == "string_value"
11+
await redis.set("string_key", "string_value")
12+
string_value = await redis.get("string_key")
13+
assert string_value == "string_value"
1614

17-
exists = await redis.exists("string_key")
18-
assert exists == 1
15+
exists = await redis.exists("string_key")
16+
assert exists == 1
1917

20-
await redis.delete("string_key")
21-
deleted_value = await redis.get("string_key")
22-
assert deleted_value is None
18+
await redis.delete("string_key")
19+
deleted_value = await redis.get("string_key")
20+
assert deleted_value is None
2321

2422

2523
@pytest.mark.asyncio
2624
async def test_redis_utils_functions():
27-
async with connect_redis() as redis:
28-
if redis is None:
29-
pytest.skip("Redis not configured")
25+
redis = await connect_redis()
3026

31-
token = "test_token_123"
32-
self_link = "http://mywebsite.com/search?token=test_token_123"
27+
token = "test_token_123"
28+
self_link = "http://mywebsite.com/search?token=test_token_123"
3329

34-
await save_self_link(redis, token, self_link)
35-
retrieved_link = await get_prev_link(redis, token)
36-
assert retrieved_link == self_link
30+
await save_self_link(redis, token, self_link)
31+
retrieved_link = await get_prev_link(redis, token)
32+
assert retrieved_link == self_link
3733

38-
await save_self_link(redis, None, "should_not_save")
39-
null_result = await get_prev_link(redis, None)
40-
assert null_result is None
34+
await save_self_link(redis, None, "should_not_save")
35+
null_result = await get_prev_link(redis, None)
36+
assert null_result is None
4137

42-
non_existent = await get_prev_link(redis, "non_existent_token")
43-
assert non_existent is None
38+
non_existent = await get_prev_link(redis, "non_existent_token")
39+
assert non_existent is None

0 commit comments

Comments
 (0)