Skip to content

Commit 312ba8a

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

File tree

2 files changed

+82
-178
lines changed

2 files changed

+82
-178
lines changed

stac_fastapi/core/stac_fastapi/core/redis_utils.py

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

7-
from pydantic import field_validator
7+
# from pydantic import field_validator
88
from pydantic_settings import BaseSettings
99
from redis import asyncio as aioredis
1010
from redis.asyncio.sentinel import Sentinel
@@ -13,225 +13,135 @@
1313

1414

1515
class 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

10650
class 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+
16264
sentinel_settings = RedisSentinelSettings()
16365
standalone_settings = RedisSettings()
16466

16567

68+
redis: Optional[aioredis.Redis] = None
69+
70+
16671
async 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+
221128
async 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

233143
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)."""
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
240150
async 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}")

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)