Skip to content

Commit 519c678

Browse files
Yuri ZmytrakovYuri Zmytrakov
authored andcommitted
dummy
1 parent 7d6b741 commit 519c678

File tree

5 files changed

+175
-4
lines changed

5 files changed

+175
-4
lines changed

Makefile

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,22 +63,22 @@ docker-shell-os:
6363

6464
.PHONY: test-elasticsearch
6565
test-elasticsearch:
66-
-$(run_es) /bin/bash -c 'export && ./scripts/wait-for-it-es.sh elasticsearch:9200 && cd stac_fastapi/tests/ && pytest'
66+
-$(run_es) /bin/bash -c 'pip install redis==6.4.0 export && ./scripts/wait-for-it-es.sh elasticsearch:9200 && cd stac_fastapi/tests/ && pytest'
6767
docker compose down
6868

6969
.PHONY: test-opensearch
7070
test-opensearch:
71-
-$(run_os) /bin/bash -c 'export && ./scripts/wait-for-it-es.sh opensearch:9202 && cd stac_fastapi/tests/ && pytest'
71+
-$(run_os) /bin/bash -c 'pip install redis==6.4.0 export && ./scripts/wait-for-it-es.sh opensearch:9202 && cd stac_fastapi/tests/ && pytest'
7272
docker compose down
7373

7474
.PHONY: test-datetime-filtering-es
7575
test-datetime-filtering-es:
76-
-$(run_es) /bin/bash -c 'export ENABLE_DATETIME_INDEX_FILTERING=true && ./scripts/wait-for-it-es.sh elasticsearch:9200 && cd stac_fastapi/tests/ && pytest -s --cov=stac_fastapi --cov-report=term-missing -m datetime_filtering'
76+
-$(run_es) /bin/bash -c 'pip install redis==6.4.0 && export ENABLE_DATETIME_INDEX_FILTERING=true && ./scripts/wait-for-it-es.sh elasticsearch:9200 && cd stac_fastapi/tests/ && pytest -s --cov=stac_fastapi --cov-report=term-missing -m datetime_filtering'
7777
docker compose down
7878

7979
.PHONY: test-datetime-filtering-os
8080
test-datetime-filtering-os:
81-
-$(run_os) /bin/bash -c 'export ENABLE_DATETIME_INDEX_FILTERING=true && ./scripts/wait-for-it-es.sh opensearch:9202 && cd stac_fastapi/tests/ && pytest -s --cov=stac_fastapi --cov-report=term-missing -m datetime_filtering'
81+
-$(run_os) /bin/bash -c 'pip install redis==6.4.0 && export ENABLE_DATETIME_INDEX_FILTERING=true && ./scripts/wait-for-it-es.sh opensearch:9202 && cd stac_fastapi/tests/ && pytest -s --cov=stac_fastapi --cov-report=term-missing -m datetime_filtering'
8282
docker compose down
8383

8484
.PHONY: test

mypy.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[mypy]
2+
[mypy-redis.*]
3+
ignore_missing_imports = True

stac_fastapi/core/setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"pygeofilter~=0.3.1",
2020
"jsonschema~=4.0.0",
2121
"slowapi~=0.1.9",
22+
"redis==6.4.0",
2223
]
2324

2425
setup(

stac_fastapi/core/stac_fastapi/core/core.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@
2424
from stac_fastapi.core.base_settings import ApiBaseSettings
2525
from stac_fastapi.core.datetime_utils import format_datetime_range
2626
from stac_fastapi.core.models.links import PagingLinks
27+
from stac_fastapi.core.redis_utils import (
28+
connect_redis_sentinel,
29+
get_prev_link,
30+
save_self_link,
31+
)
2732
from stac_fastapi.core.serializers import CollectionSerializer, ItemSerializer
2833
from stac_fastapi.core.session import Session
2934
from stac_fastapi.core.utilities import filter_fields
@@ -255,6 +260,13 @@ async def all_collections(
255260
if parsed_sort:
256261
sort = parsed_sort
257262

263+
current_url = str(request.url)
264+
redis = None
265+
try:
266+
redis = await connect_redis_sentinel()
267+
except Exception:
268+
redis = None
269+
258270
collections, next_token = await self.database.get_all_collections(
259271
token=token, limit=limit, request=request, sort=sort
260272
)
@@ -269,6 +281,22 @@ async def all_collections(
269281
},
270282
]
271283

284+
if redis:
285+
if next_token:
286+
await save_self_link(redis, next_token, current_url)
287+
288+
prev_link = await get_prev_link(redis, token)
289+
if prev_link:
290+
links.insert(
291+
0,
292+
{
293+
"rel": "prev",
294+
"type": "application/json",
295+
"method": "GET",
296+
"href": prev_link,
297+
},
298+
)
299+
272300
if next_token:
273301
next_link = PagingLinks(next=next_token, request=request).link_next()
274302
links.append(next_link)
@@ -499,6 +527,10 @@ async def post_search(
499527
HTTPException: If there is an error with the cql2_json filter.
500528
"""
501529
base_url = str(request.base_url)
530+
try:
531+
redis = await connect_redis_sentinel()
532+
except Exception:
533+
redis = None
502534

503535
search = self.database.make_search()
504536

@@ -609,6 +641,22 @@ async def post_search(
609641
]
610642
links = await PagingLinks(request=request, next=next_token).get_links()
611643

644+
if redis:
645+
self_link = str(request.url)
646+
await save_self_link(redis, next_token, self_link)
647+
648+
prev_link = await get_prev_link(redis, token_param)
649+
if prev_link:
650+
links.insert(
651+
0,
652+
{
653+
"rel": "prev",
654+
"type": "application/json",
655+
"method": "GET",
656+
"href": prev_link,
657+
},
658+
)
659+
612660
return stac_types.ItemCollection(
613661
type="FeatureCollection",
614662
features=items,
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
"""Utilities for connecting to and managing Redis connections."""
2+
3+
import logging
4+
from typing import Optional
5+
6+
from pydantic_settings import BaseSettings
7+
from redis import asyncio as aioredis
8+
from redis.asyncio.sentinel import Sentinel
9+
10+
logger = logging.getLogger(__name__)
11+
redis_pool: Optional[aioredis.Redis] = None
12+
13+
14+
class RedisSentinelSettings(BaseSettings):
15+
"""Configuration for connecting to Redis Sentinel."""
16+
17+
REDIS_SENTINEL_HOSTS: str = ""
18+
REDIS_SENTINEL_PORTS: str = "26379"
19+
REDIS_SENTINEL_MASTER_NAME: str = "master"
20+
REDIS_DB: int = 15
21+
22+
REDIS_MAX_CONNECTIONS: int = 10
23+
REDIS_RETRY_TIMEOUT: bool = True
24+
REDIS_DECODE_RESPONSES: bool = True
25+
REDIS_CLIENT_NAME: str = "stac-fastapi-app"
26+
REDIS_HEALTH_CHECK_INTERVAL: int = 30
27+
28+
29+
class RedisSettings(BaseSettings):
30+
"""Configuration for connecting Redis Sentinel."""
31+
32+
REDIS_HOST: str = ""
33+
REDIS_PORT: int = 6379
34+
REDIS_DB: int = 0
35+
36+
REDIS_MAX_CONNECTIONS: int = 10
37+
REDIS_RETRY_TIMEOUT: bool = True
38+
REDIS_DECODE_RESPONSES: bool = True
39+
REDIS_CLIENT_NAME: str = "stac-fastapi-app"
40+
REDIS_HEALTH_CHECK_INTERVAL: int = 30
41+
42+
43+
# Select the Redis or Redis Sentinel configuration
44+
redis_settings: BaseSettings = RedisSentinelSettings()
45+
46+
47+
async def connect_redis(settings: Optional[RedisSettings] = None) -> aioredis.Redis:
48+
"""Return a Redis connection."""
49+
global redis_pool
50+
settings = settings or redis_settings
51+
52+
if redis_pool is None:
53+
pool = aioredis.ConnectionPool(
54+
host=settings.REDIS_HOST,
55+
port=settings.REDIS_PORT,
56+
db=settings.REDIS_DB,
57+
max_connections=settings.REDIS_MAX_CONNECTIONS,
58+
decode_responses=settings.REDIS_DECODE_RESPONSES,
59+
retry_on_timeout=settings.REDIS_RETRY_TIMEOUT,
60+
health_check_interval=settings.REDIS_HEALTH_CHECK_INTERVAL,
61+
)
62+
redis_pool = aioredis.Redis(
63+
connection_pool=pool, client_name=settings.REDIS_CLIENT_NAME
64+
)
65+
return redis_pool
66+
67+
68+
async def connect_redis_sentinel(
69+
settings: Optional[RedisSentinelSettings] = None,
70+
) -> Optional[aioredis.Redis]:
71+
"""Return a Redis Sentinel connection."""
72+
global redis_pool
73+
74+
settings = settings or redis_settings
75+
76+
if not settings.REDIS_SENTINEL_HOSTS or not settings.REDIS_SENTINEL_PORTS or not settings.REDIS_SENTINEL_MASTER_NAME:
77+
return None
78+
79+
hosts = [h.strip() for h in settings.REDIS_SENTINEL_HOSTS.split(",") if h.strip()]
80+
ports = [
81+
int(p.strip()) for p in settings.REDIS_SENTINEL_PORTS.split(",") if p.strip()
82+
]
83+
84+
if redis_pool is None:
85+
try:
86+
sentinel = Sentinel(
87+
[(h, p) for h, p in zip(hosts, ports)],
88+
decode_responses=settings.REDIS_DECODE_RESPONSES,
89+
)
90+
master = sentinel.master_for(
91+
service_name=settings.REDIS_SENTINEL_MASTER_NAME,
92+
db=settings.REDIS_DB,
93+
decode_responses=settings.REDIS_DECODE_RESPONSES,
94+
retry_on_timeout=settings.REDIS_RETRY_TIMEOUT,
95+
client_name=settings.REDIS_CLIENT_NAME,
96+
max_connections=settings.REDIS_MAX_CONNECTIONS,
97+
health_check_interval=settings.REDIS_HEALTH_CHECK_INTERVAL,
98+
)
99+
redis_pool = master
100+
101+
except Exception:
102+
return None
103+
104+
return redis_pool
105+
106+
107+
async def save_self_link(
108+
redis: aioredis.Redis, token: Optional[str], self_href: str
109+
) -> None:
110+
"""Save the self link for the current token with 30 min TTL."""
111+
if token:
112+
await redis.setex(f"nav:self:{token}", 1800, self_href)
113+
114+
115+
async def get_prev_link(redis: aioredis.Redis, token: Optional[str]) -> Optional[str]:
116+
"""Get the previous page link for the current token (if exists)."""
117+
if not token:
118+
return None
119+
return await redis.get(f"nav:self:{token}")

0 commit comments

Comments
 (0)