sfeos-helpers 6.0.0__py3-none-any.whl → 6.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,30 @@
1
+ """Base classes for index selection strategies."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Dict, List, Optional
5
+
6
+
7
+ class BaseIndexSelector(ABC):
8
+ """Base class for async index selectors."""
9
+
10
+ @abstractmethod
11
+ async def select_indexes(
12
+ self,
13
+ collection_ids: Optional[List[str]],
14
+ datetime_search: Dict[str, Optional[str]],
15
+ ) -> str:
16
+ """Select appropriate indexes asynchronously.
17
+
18
+ Args:
19
+ collection_ids (Optional[List[str]]): List of collection IDs to filter by.
20
+ datetime_search (Dict[str, Optional[str]]): Datetime search criteria.
21
+
22
+ Returns:
23
+ str: Comma-separated string of selected index names.
24
+ """
25
+ pass
26
+
27
+ @abstractmethod
28
+ async def refresh_cache(self):
29
+ """Refresh cache (no-op for unfiltered selector)."""
30
+ pass
@@ -0,0 +1,127 @@
1
+ """Cache management for index selection strategies."""
2
+
3
+ import threading
4
+ import time
5
+ from collections import defaultdict
6
+ from typing import Any, Dict, List, Optional
7
+
8
+ from stac_fastapi.sfeos_helpers.database import index_alias_by_collection_id
9
+ from stac_fastapi.sfeos_helpers.mappings import ITEMS_INDEX_PREFIX
10
+
11
+
12
+ class IndexCacheManager:
13
+ """Manages caching of index aliases with expiration."""
14
+
15
+ def __init__(self, cache_ttl_seconds: int = 3600):
16
+ """Initialize the cache manager.
17
+
18
+ Args:
19
+ cache_ttl_seconds (int): Time-to-live for cache entries in seconds.
20
+ """
21
+ self._cache: Optional[Dict[str, List[str]]] = None
22
+ self._timestamp: float = 0
23
+ self._ttl = cache_ttl_seconds
24
+ self._lock = threading.Lock()
25
+
26
+ @property
27
+ def is_expired(self) -> bool:
28
+ """Check if the cache has expired.
29
+
30
+ Returns:
31
+ bool: True if cache is expired, False otherwise.
32
+ """
33
+ return time.time() - self._timestamp > self._ttl
34
+
35
+ def get_cache(self) -> Optional[Dict[str, List[str]]]:
36
+ """Get the current cache if not expired.
37
+
38
+ Returns:
39
+ Optional[Dict[str, List[str]]]: Cache data if valid, None if expired.
40
+ """
41
+ with self._lock:
42
+ if self.is_expired:
43
+ return None
44
+ return {k: v.copy() for k, v in self._cache.items()}
45
+
46
+ def set_cache(self, data: Dict[str, List[str]]) -> None:
47
+ """Set cache data and update timestamp.
48
+
49
+ Args:
50
+ data (Dict[str, List[str]]): Cache data to store.
51
+ """
52
+ self._cache = data
53
+ self._timestamp = time.time()
54
+
55
+ def clear_cache(self) -> None:
56
+ """Clear the cache and reset timestamp."""
57
+ self._cache = None
58
+ self._timestamp = 0
59
+
60
+
61
+ class IndexAliasLoader:
62
+ """Asynchronous loader for index aliases."""
63
+
64
+ def __init__(self, client: Any, cache_manager: IndexCacheManager):
65
+ """Initialize the async alias loader.
66
+
67
+ Args:
68
+ client: Async search engine client instance.
69
+ cache_manager (IndexCacheManager): Cache manager instance.
70
+ """
71
+ self.client = client
72
+ self.cache_manager = cache_manager
73
+
74
+ async def load_aliases(self) -> Dict[str, List[str]]:
75
+ """Load index aliases from search engine.
76
+
77
+ Returns:
78
+ Dict[str, List[str]]: Mapping of base aliases to item aliases.
79
+ """
80
+ response = await self.client.indices.get_alias(index=f"{ITEMS_INDEX_PREFIX}*")
81
+ result = defaultdict(list)
82
+ for index_info in response.values():
83
+ aliases = index_info.get("aliases", {})
84
+ items_aliases = sorted(
85
+ [
86
+ alias
87
+ for alias in aliases.keys()
88
+ if alias.startswith(ITEMS_INDEX_PREFIX)
89
+ ]
90
+ )
91
+
92
+ if items_aliases:
93
+ result[items_aliases[0]].extend(items_aliases[1:])
94
+
95
+ self.cache_manager.set_cache(result)
96
+ return result
97
+
98
+ async def get_aliases(self) -> Dict[str, List[str]]:
99
+ """Get aliases from cache or load if expired.
100
+
101
+ Returns:
102
+ Dict[str, List[str]]: Alias mapping data.
103
+ """
104
+ cached = self.cache_manager.get_cache()
105
+ if cached is not None:
106
+ return cached
107
+ return await self.load_aliases()
108
+
109
+ async def refresh_aliases(self) -> Dict[str, List[str]]:
110
+ """Force refresh aliases from search engine.
111
+
112
+ Returns:
113
+ Dict[str, List[str]]: Fresh alias mapping data.
114
+ """
115
+ return await self.load_aliases()
116
+
117
+ async def get_collection_indexes(self, collection_id: str) -> List[str]:
118
+ """Get all index aliases for a specific collection.
119
+
120
+ Args:
121
+ collection_id (str): Collection identifier.
122
+
123
+ Returns:
124
+ List[str]: List of index aliases for the collection.
125
+ """
126
+ aliases = await self.get_aliases()
127
+ return aliases.get(index_alias_by_collection_id(collection_id), [])
@@ -0,0 +1,37 @@
1
+ """Factory for creating index selection strategies."""
2
+
3
+ from typing import Any
4
+
5
+ from stac_fastapi.core.utilities import get_bool_env
6
+
7
+ from .base import BaseIndexSelector
8
+ from .selectors import DatetimeBasedIndexSelector, UnfilteredIndexSelector
9
+
10
+
11
+ class IndexSelectorFactory:
12
+ """Factory class for creating index selector instances."""
13
+
14
+ @staticmethod
15
+ def create_selector(client: Any) -> BaseIndexSelector:
16
+ """Create an appropriate asynchronous index selector based on environment configuration.
17
+
18
+ Checks the ENABLE_DATETIME_INDEX_FILTERING environment variable to determine
19
+ whether to use datetime-based filtering or return all available indices.
20
+
21
+ Args:
22
+ client: Asynchronous Elasticsearch/OpenSearch client instance, used only if datetime
23
+ filtering is enabled.
24
+
25
+ Returns:
26
+ IndexSelectionStrategy: Either an AsyncDatetimeBasedIndexSelector if datetime
27
+ filtering is enabled, or an UnfilteredIndexSelector otherwise.
28
+ """
29
+ use_datetime_filtering = get_bool_env(
30
+ "ENABLE_DATETIME_INDEX_FILTERING", default="false"
31
+ )
32
+
33
+ return (
34
+ DatetimeBasedIndexSelector(client)
35
+ if use_datetime_filtering
36
+ else UnfilteredIndexSelector()
37
+ )
@@ -0,0 +1,129 @@
1
+ """Async index selectors with datetime-based filtering."""
2
+
3
+ from typing import Any, Dict, List, Optional
4
+
5
+ from stac_fastapi.sfeos_helpers.database import filter_indexes_by_datetime
6
+ from stac_fastapi.sfeos_helpers.mappings import ITEM_INDICES
7
+
8
+ from ...database import indices
9
+ from .base import BaseIndexSelector
10
+ from .cache_manager import IndexAliasLoader, IndexCacheManager
11
+
12
+
13
+ class DatetimeBasedIndexSelector(BaseIndexSelector):
14
+ """Asynchronous index selector that filters indices based on datetime criteria with caching."""
15
+
16
+ _instance = None
17
+
18
+ def __new__(cls, client):
19
+ """Create singleton instance.
20
+
21
+ Args:
22
+ client: Async search engine client instance.
23
+
24
+ Returns:
25
+ DatetimeBasedIndexSelector: Singleton instance.
26
+ """
27
+ if cls._instance is None:
28
+ cls._instance = super().__new__(cls)
29
+ return cls._instance
30
+
31
+ def __init__(self, client: Any):
32
+ """Initialize the datetime-based index selector.
33
+
34
+ Args:
35
+ client: Elasticsearch/OpenSearch client instance used for querying
36
+ index aliases and metadata.
37
+ """
38
+ if not hasattr(self, "_initialized"):
39
+ self.cache_manager = IndexCacheManager()
40
+ self.alias_loader = IndexAliasLoader(client, self.cache_manager)
41
+ self._initialized = True
42
+
43
+ async def refresh_cache(self) -> Dict[str, List[str]]:
44
+ """Force refresh of the aliases cache.
45
+
46
+ Returns:
47
+ Dict[str, List[str]]: Refreshed dictionary mapping base collection aliases
48
+ to lists of their corresponding item index aliases.
49
+ """
50
+ return await self.alias_loader.refresh_aliases()
51
+
52
+ async def get_collection_indexes(self, collection_id: str) -> List[str]:
53
+ """Get all index aliases for a specific collection.
54
+
55
+ Args:
56
+ collection_id (str): The ID of the collection to retrieve indexes for.
57
+
58
+ Returns:
59
+ List[str]: List of index aliases associated with the collection.
60
+ Returns empty list if collection is not found in cache.
61
+ """
62
+ return await self.alias_loader.get_collection_indexes(collection_id)
63
+
64
+ async def select_indexes(
65
+ self,
66
+ collection_ids: Optional[List[str]],
67
+ datetime_search: Dict[str, Optional[str]],
68
+ ) -> str:
69
+ """Select indexes filtered by collection IDs and datetime criteria.
70
+
71
+ For each specified collection, retrieves its associated indexes and filters
72
+ them based on datetime range. If no collection IDs are provided, returns
73
+ all item indices.
74
+
75
+ Args:
76
+ collection_ids (Optional[List[str]]): List of collection IDs to filter by.
77
+ If None or empty, returns all item indices.
78
+ datetime_search (Dict[str, Optional[str]]): Dictionary containing datetime
79
+ search criteria with 'gte' and 'lte' keys for range filtering.
80
+
81
+ Returns:
82
+ str: Comma-separated string of selected index names that match the
83
+ collection and datetime criteria. Returns empty string if no
84
+ indexes match the criteria.
85
+ """
86
+ if collection_ids:
87
+ selected_indexes = []
88
+ for collection_id in collection_ids:
89
+ collection_indexes = await self.get_collection_indexes(collection_id)
90
+ filtered_indexes = filter_indexes_by_datetime(
91
+ collection_indexes,
92
+ datetime_search.get("gte"),
93
+ datetime_search.get("lte"),
94
+ )
95
+ selected_indexes.extend(filtered_indexes)
96
+
97
+ return ",".join(selected_indexes) if selected_indexes else ""
98
+
99
+ return ITEM_INDICES
100
+
101
+
102
+ class UnfilteredIndexSelector(BaseIndexSelector):
103
+ """Index selector that returns all available indices without filtering."""
104
+
105
+ async def select_indexes(
106
+ self,
107
+ collection_ids: Optional[List[str]],
108
+ datetime_search: Dict[str, Optional[str]],
109
+ ) -> str:
110
+ """Select all indices for given collections without datetime filtering.
111
+
112
+ Args:
113
+ collection_ids (Optional[List[str]]): List of collection IDs to filter by.
114
+ If None, all collections are considered.
115
+ datetime_search (Dict[str, Optional[str]]): Datetime search criteria
116
+ (ignored by this implementation).
117
+
118
+ Returns:
119
+ str: Comma-separated string of all available index names for the collections.
120
+ """
121
+ return indices(collection_ids)
122
+
123
+ async def refresh_cache(self):
124
+ """Refresh cache (no-op for unfiltered selector).
125
+
126
+ Note:
127
+ Unfiltered selector doesn't use cache, so this is a no-op operation.
128
+ """
129
+ pass
@@ -1,2 +1,2 @@
1
1
  """library version."""
2
- __version__ = "6.0.0"
2
+ __version__ = "6.2.0"
@@ -1,21 +0,0 @@
1
- stac_fastapi/sfeos_helpers/mappings.py,sha256=z6GJFJUE7bRKF9ODc8_ddkb7JCOokMtj4p2LeaQqrQQ,8237
2
- stac_fastapi/sfeos_helpers/version.py,sha256=Fo5UFEQVxJZ3nywa3IY-enu5UQBE0X45nrQaRBe8c9o,45
3
- stac_fastapi/sfeos_helpers/aggregation/__init__.py,sha256=Mym17lFh90by1GnoQgMyIKAqRNJnvCgVSXDYzjBiPQk,1210
4
- stac_fastapi/sfeos_helpers/aggregation/client.py,sha256=JCUVBXsUXHdUXn59WaytGxB-R6OLhC_LApPm-pzQNTI,17818
5
- stac_fastapi/sfeos_helpers/aggregation/format.py,sha256=qUW1jjh2EEjy-V7riliFR77grpi-AgsTmP76z60K5Lo,2011
6
- stac_fastapi/sfeos_helpers/database/__init__.py,sha256=WAZc7Djw9VZHt-crkFkhtILVCZb8GzIegS6CpdGA4gc,2477
7
- stac_fastapi/sfeos_helpers/database/datetime.py,sha256=L_bY0EtRi6b8XIN9zcHbFe-O2jFdItkgkNvkQ4Z1jyY,2279
8
- stac_fastapi/sfeos_helpers/database/document.py,sha256=LtjX15gvaOuZC_k2t_oQhys_c-zRTLN5rwX0hNJkHnM,1725
9
- stac_fastapi/sfeos_helpers/database/index.py,sha256=7xFNzOQzFxBAtQUKUmhKcqz5j3_HRPuwIecdEEWb0i0,4476
10
- stac_fastapi/sfeos_helpers/database/mapping.py,sha256=4-MSd4xH5wg7yoC4aPjzYMDSEvP026bw4k2TfffMT5E,1387
11
- stac_fastapi/sfeos_helpers/database/query.py,sha256=3aFby56ggvTxtBg1vH1AXw2P2_CloPIV-cwL260P47E,2972
12
- stac_fastapi/sfeos_helpers/database/utils.py,sha256=nPTWqP-pdSZumeYRx92XTN4xmmufRSqCDHaiSLdg-3o,7311
13
- stac_fastapi/sfeos_helpers/filter/__init__.py,sha256=n3zL_MhEGOoxMz1KeijyK_UKiZ0MKPl90zHtYI5RAy8,1557
14
- stac_fastapi/sfeos_helpers/filter/client.py,sha256=QwjYWXkevoVS7HPtoXfeSzDy-_GJnFhPJtJM49D14oU,4229
15
- stac_fastapi/sfeos_helpers/filter/cql2.py,sha256=Cg9kRYD9CVkVSyRqOyB5oVXmlyteSn2bw88sqklGpUM,955
16
- stac_fastapi/sfeos_helpers/filter/transform.py,sha256=1GEWQSp-rbq7_1nDVv1ApDbWxt8DswJWxwaxzV85gj4,4644
17
- stac_fastapi/sfeos_helpers/models/patch.py,sha256=akzfF7vXI6AyGDrxgP8KqVwBvBDQayXYNNf4pm6Q8qM,4209
18
- sfeos_helpers-6.0.0.dist-info/METADATA,sha256=huD7geSPsXMjM-KvE7KsaM6gTuGHRO7QPDBlpyTBayc,31173
19
- sfeos_helpers-6.0.0.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
20
- sfeos_helpers-6.0.0.dist-info/top_level.txt,sha256=vqn-D9-HsRPTTxy0Vk_KkDmTiMES4owwBQ3ydSZYb2s,13
21
- sfeos_helpers-6.0.0.dist-info/RECORD,,