sfeos-helpers 6.1.0__py3-none-any.whl → 6.2.1__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.
- {sfeos_helpers-6.1.0.dist-info → sfeos_helpers-6.2.1.dist-info}/METADATA +89 -5
- sfeos_helpers-6.2.1.dist-info/RECORD +32 -0
- {sfeos_helpers-6.1.0.dist-info → sfeos_helpers-6.2.1.dist-info}/WHEEL +1 -1
- stac_fastapi/sfeos_helpers/aggregation/client.py +5 -2
- stac_fastapi/sfeos_helpers/database/__init__.py +5 -1
- stac_fastapi/sfeos_helpers/database/datetime.py +64 -3
- stac_fastapi/sfeos_helpers/database/index.py +59 -2
- stac_fastapi/sfeos_helpers/database/query.py +5 -2
- stac_fastapi/sfeos_helpers/database/utils.py +75 -38
- stac_fastapi/sfeos_helpers/models/patch.py +34 -76
- stac_fastapi/sfeos_helpers/search_engine/__init__.py +27 -0
- stac_fastapi/sfeos_helpers/search_engine/base.py +51 -0
- stac_fastapi/sfeos_helpers/search_engine/factory.py +36 -0
- stac_fastapi/sfeos_helpers/search_engine/index_operations.py +167 -0
- stac_fastapi/sfeos_helpers/search_engine/inserters.py +309 -0
- stac_fastapi/sfeos_helpers/search_engine/managers.py +198 -0
- stac_fastapi/sfeos_helpers/search_engine/selection/__init__.py +15 -0
- stac_fastapi/sfeos_helpers/search_engine/selection/base.py +30 -0
- stac_fastapi/sfeos_helpers/search_engine/selection/cache_manager.py +127 -0
- stac_fastapi/sfeos_helpers/search_engine/selection/factory.py +37 -0
- stac_fastapi/sfeos_helpers/search_engine/selection/selectors.py +129 -0
- stac_fastapi/sfeos_helpers/version.py +1 -1
- sfeos_helpers-6.1.0.dist-info/RECORD +0 -21
- {sfeos_helpers-6.1.0.dist-info → sfeos_helpers-6.2.1.dist-info}/top_level.txt +0 -0
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import re
|
|
4
4
|
from typing import Any, Dict, Optional, Union
|
|
5
5
|
|
|
6
|
-
from pydantic import BaseModel,
|
|
6
|
+
from pydantic import BaseModel, model_validator
|
|
7
7
|
|
|
8
8
|
regex = re.compile(r"([^.' ]*:[^.'[ ]*)\.?")
|
|
9
9
|
replacements = str.maketrans({"/": "", ".": "", ":": "", "[": "", "]": ""})
|
|
@@ -71,16 +71,23 @@ class ElasticPath(BaseModel):
|
|
|
71
71
|
|
|
72
72
|
"""
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
parts: list[str] = []
|
|
75
|
+
|
|
76
|
+
path: Optional[str] = None
|
|
77
|
+
key: Optional[Union[str, int]] = None
|
|
75
78
|
nest: Optional[str] = None
|
|
76
|
-
partition: Optional[str] = None
|
|
77
|
-
key: Optional[str] = None
|
|
78
79
|
|
|
79
80
|
es_path: Optional[str] = None
|
|
80
|
-
es_nest: Optional[str] = None
|
|
81
81
|
es_key: Optional[str] = None
|
|
82
|
+
es_nest: Optional[str] = None
|
|
83
|
+
|
|
84
|
+
variable_name: Optional[str] = None
|
|
85
|
+
param_key: Optional[str] = None
|
|
82
86
|
|
|
83
|
-
|
|
87
|
+
class Config:
|
|
88
|
+
"""Class config."""
|
|
89
|
+
|
|
90
|
+
frozen = True
|
|
84
91
|
|
|
85
92
|
@model_validator(mode="before")
|
|
86
93
|
@classmethod
|
|
@@ -90,77 +97,28 @@ class ElasticPath(BaseModel):
|
|
|
90
97
|
Args:
|
|
91
98
|
data (Any): input data
|
|
92
99
|
"""
|
|
93
|
-
data["
|
|
94
|
-
data["nest"], data["partition"], data["key"] = data["path"].rpartition(".")
|
|
95
|
-
|
|
96
|
-
if data["key"].lstrip("-").isdigit() or data["key"] == "-":
|
|
97
|
-
data["index_"] = -1 if data["key"] == "-" else int(data["key"])
|
|
98
|
-
data["path"] = f"{data['nest']}[{data['index_']}]"
|
|
99
|
-
data["nest"], data["partition"], data["key"] = data["nest"].rpartition(".")
|
|
100
|
-
|
|
101
|
-
data["es_path"] = to_es(data["path"])
|
|
102
|
-
data["es_nest"] = f".{to_es(data['nest'])}" if data["nest"] else ""
|
|
103
|
-
data["es_key"] = to_es(data["key"])
|
|
104
|
-
|
|
105
|
-
return data
|
|
106
|
-
|
|
107
|
-
@computed_field # type: ignore[misc]
|
|
108
|
-
@property
|
|
109
|
-
def index(self) -> Union[int, str, None]:
|
|
110
|
-
"""Compute location of path.
|
|
111
|
-
|
|
112
|
-
Returns:
|
|
113
|
-
str: path index
|
|
114
|
-
"""
|
|
115
|
-
if self.index_ and self.index_ < 0:
|
|
100
|
+
data["parts"] = data["path"].lstrip("/").split("/")
|
|
116
101
|
|
|
117
|
-
|
|
102
|
+
data["key"] = data["parts"].pop(-1)
|
|
103
|
+
data["nest"] = "/".join(data["parts"])
|
|
104
|
+
data["path"] = data["nest"] + "/" + data["key"]
|
|
118
105
|
|
|
119
|
-
|
|
106
|
+
data["es_key"] = data["key"]
|
|
107
|
+
data["es_nest"] = "".join([f"['{part}']" for part in data["parts"]])
|
|
108
|
+
data["es_path"] = data["es_nest"] + f"['{data['es_key']}']"
|
|
120
109
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
"""Compute location of path.
|
|
135
|
-
|
|
136
|
-
Returns:
|
|
137
|
-
str: path location
|
|
138
|
-
"""
|
|
139
|
-
if self.es_key and ":" in self.es_key:
|
|
140
|
-
return self.es_nest + self.es_key
|
|
141
|
-
return self.es_nest + self.partition + self.es_key
|
|
142
|
-
|
|
143
|
-
@computed_field # type: ignore[misc]
|
|
144
|
-
@property
|
|
145
|
-
def variable_name(self) -> str:
|
|
146
|
-
"""Variable name for scripting.
|
|
147
|
-
|
|
148
|
-
Returns:
|
|
149
|
-
str: variable name
|
|
150
|
-
"""
|
|
151
|
-
if self.index is not None:
|
|
152
|
-
return f"{self.location.replace('.','_').replace(':','_')}_{self.index}"
|
|
153
|
-
|
|
154
|
-
return (
|
|
155
|
-
f"{self.nest.replace('.','_').replace(':','_')}_{self.key.replace(':','_')}"
|
|
156
|
-
)
|
|
157
|
-
|
|
158
|
-
@computed_field # type: ignore[misc]
|
|
159
|
-
@property
|
|
160
|
-
def param_key(self) -> str:
|
|
161
|
-
"""Param key for scripting.
|
|
110
|
+
if data["key"].lstrip("-").isdigit() or data["key"] == "-":
|
|
111
|
+
data["key"] = -1 if data["key"] == "-" else int(data["key"])
|
|
112
|
+
data["es_key"] = (
|
|
113
|
+
f"ctx._source{data['es_nest']}.size() - {-data['key']}"
|
|
114
|
+
if data["key"] < 0
|
|
115
|
+
else str(data["key"])
|
|
116
|
+
)
|
|
117
|
+
data["es_path"] = data["es_nest"] + f"[{data['es_key']}]"
|
|
118
|
+
|
|
119
|
+
data[
|
|
120
|
+
"variable_name"
|
|
121
|
+
] = f"{data['nest'].replace('/','_').replace(':','_')}_{str(data['key']).replace(':','_')}"
|
|
122
|
+
data["param_key"] = data["path"].translate(replacements)
|
|
162
123
|
|
|
163
|
-
|
|
164
|
-
str: param key
|
|
165
|
-
"""
|
|
166
|
-
return self.path.translate(replacements)
|
|
124
|
+
return data
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Search engine index management package."""
|
|
2
|
+
|
|
3
|
+
from .base import BaseIndexInserter
|
|
4
|
+
from .factory import IndexInsertionFactory
|
|
5
|
+
from .index_operations import IndexOperations
|
|
6
|
+
from .inserters import DatetimeIndexInserter, SimpleIndexInserter
|
|
7
|
+
from .managers import DatetimeIndexManager, IndexSizeManager
|
|
8
|
+
from .selection import (
|
|
9
|
+
BaseIndexSelector,
|
|
10
|
+
DatetimeBasedIndexSelector,
|
|
11
|
+
IndexSelectorFactory,
|
|
12
|
+
UnfilteredIndexSelector,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"BaseIndexInserter",
|
|
17
|
+
"BaseIndexSelector",
|
|
18
|
+
"IndexOperations",
|
|
19
|
+
"IndexSizeManager",
|
|
20
|
+
"DatetimeIndexManager",
|
|
21
|
+
"DatetimeIndexInserter",
|
|
22
|
+
"SimpleIndexInserter",
|
|
23
|
+
"IndexInsertionFactory",
|
|
24
|
+
"DatetimeBasedIndexSelector",
|
|
25
|
+
"UnfilteredIndexSelector",
|
|
26
|
+
"IndexSelectorFactory",
|
|
27
|
+
]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Base classes for index inserters."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import Any, Dict, List
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BaseIndexInserter(ABC):
|
|
8
|
+
"""Base async index inserter with common async methods."""
|
|
9
|
+
|
|
10
|
+
@abstractmethod
|
|
11
|
+
async def get_target_index(
|
|
12
|
+
self, collection_id: str, product: Dict[str, Any]
|
|
13
|
+
) -> str:
|
|
14
|
+
"""Get target index for a product asynchronously.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
collection_id (str): Collection identifier.
|
|
18
|
+
product (Dict[str, Any]): Product data.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
str: Target index name.
|
|
22
|
+
"""
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
async def prepare_bulk_actions(
|
|
27
|
+
self, collection_id: str, items: List[Dict[str, Any]]
|
|
28
|
+
) -> List[Dict[str, Any]]:
|
|
29
|
+
"""Prepare bulk actions for multiple items asynchronously.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
collection_id (str): Collection identifier.
|
|
33
|
+
items (List[Dict[str, Any]]): List of items to process.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
List[Dict[str, Any]]: List of bulk actions.
|
|
37
|
+
"""
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
async def create_simple_index(self, client: Any, collection_id: str) -> str:
|
|
42
|
+
"""Create a simple index asynchronously.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
client: Search engine client instance.
|
|
46
|
+
collection_id (str): Collection identifier.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
str: Created index name.
|
|
50
|
+
"""
|
|
51
|
+
pass
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Factory for creating index insertion strategies."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from stac_fastapi.core.utilities import get_bool_env
|
|
6
|
+
|
|
7
|
+
from .base import BaseIndexInserter
|
|
8
|
+
from .index_operations import IndexOperations
|
|
9
|
+
from .inserters import DatetimeIndexInserter, SimpleIndexInserter
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class IndexInsertionFactory:
|
|
13
|
+
"""Factory for creating index insertion strategies."""
|
|
14
|
+
|
|
15
|
+
@staticmethod
|
|
16
|
+
def create_insertion_strategy(
|
|
17
|
+
client: Any,
|
|
18
|
+
) -> BaseIndexInserter:
|
|
19
|
+
"""Create async insertion strategy based on configuration.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
client: Async search engine client instance.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
BaseIndexInserter: Configured async insertion strategy.
|
|
26
|
+
"""
|
|
27
|
+
index_operations = IndexOperations()
|
|
28
|
+
|
|
29
|
+
use_datetime_partitioning = get_bool_env(
|
|
30
|
+
"ENABLE_DATETIME_INDEX_FILTERING", default="false"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
if use_datetime_partitioning:
|
|
34
|
+
return DatetimeIndexInserter(client, index_operations)
|
|
35
|
+
else:
|
|
36
|
+
return SimpleIndexInserter(index_operations, client)
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Search engine adapters for different implementations."""
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from typing import Any, Dict
|
|
5
|
+
|
|
6
|
+
from stac_fastapi.sfeos_helpers.database import (
|
|
7
|
+
index_alias_by_collection_id,
|
|
8
|
+
index_by_collection_id,
|
|
9
|
+
)
|
|
10
|
+
from stac_fastapi.sfeos_helpers.mappings import (
|
|
11
|
+
_ES_INDEX_NAME_UNSUPPORTED_CHARS_TABLE,
|
|
12
|
+
ES_ITEMS_MAPPINGS,
|
|
13
|
+
ES_ITEMS_SETTINGS,
|
|
14
|
+
ITEMS_INDEX_PREFIX,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class IndexOperations:
|
|
19
|
+
"""Base class for search engine adapters with common implementations."""
|
|
20
|
+
|
|
21
|
+
async def create_simple_index(self, client: Any, collection_id: str) -> str:
|
|
22
|
+
"""Create a simple index for the given collection.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
client: Search engine client instance.
|
|
26
|
+
collection_id (str): Collection identifier.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
str: Created index name.
|
|
30
|
+
"""
|
|
31
|
+
index_name = f"{index_by_collection_id(collection_id)}-000001"
|
|
32
|
+
alias_name = index_alias_by_collection_id(collection_id)
|
|
33
|
+
|
|
34
|
+
await client.indices.create(
|
|
35
|
+
index=index_name,
|
|
36
|
+
body=self._create_index_body({alias_name: {}}),
|
|
37
|
+
params={"ignore": [400]},
|
|
38
|
+
)
|
|
39
|
+
return index_name
|
|
40
|
+
|
|
41
|
+
async def create_datetime_index(
|
|
42
|
+
self, client: Any, collection_id: str, start_date: str
|
|
43
|
+
) -> str:
|
|
44
|
+
"""Create a datetime-based index for the given collection.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
client: Search engine client instance.
|
|
48
|
+
collection_id (str): Collection identifier.
|
|
49
|
+
start_date (str): Start date for the alias.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
str: Created index alias name.
|
|
53
|
+
"""
|
|
54
|
+
index_name = self.create_index_name(collection_id)
|
|
55
|
+
alias_name = self.create_alias_name(collection_id, start_date)
|
|
56
|
+
collection_alias = index_alias_by_collection_id(collection_id)
|
|
57
|
+
await client.indices.create(
|
|
58
|
+
index=index_name,
|
|
59
|
+
body=self._create_index_body({collection_alias: {}, alias_name: {}}),
|
|
60
|
+
)
|
|
61
|
+
return alias_name
|
|
62
|
+
|
|
63
|
+
@staticmethod
|
|
64
|
+
async def update_index_alias(client: Any, end_date: str, old_alias: str) -> str:
|
|
65
|
+
"""Update index alias with new end date.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
client: Search engine client instance.
|
|
69
|
+
end_date (str): End date for the alias.
|
|
70
|
+
old_alias (str): Current alias name.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
str: New alias name.
|
|
74
|
+
"""
|
|
75
|
+
new_alias = f"{old_alias}-{end_date}"
|
|
76
|
+
aliases_info = await client.indices.get_alias(name=old_alias)
|
|
77
|
+
actions = []
|
|
78
|
+
|
|
79
|
+
for index_name in aliases_info.keys():
|
|
80
|
+
actions.append({"remove": {"index": index_name, "alias": old_alias}})
|
|
81
|
+
actions.append({"add": {"index": index_name, "alias": new_alias}})
|
|
82
|
+
|
|
83
|
+
await client.indices.update_aliases(body={"actions": actions})
|
|
84
|
+
return new_alias
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
async def change_alias_name(client: Any, old_alias: str, new_alias: str) -> None:
|
|
88
|
+
"""Change alias name from old to new.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
client: Search engine client instance.
|
|
92
|
+
old_alias (str): Current alias name.
|
|
93
|
+
new_alias (str): New alias name.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
None
|
|
97
|
+
"""
|
|
98
|
+
aliases_info = await client.indices.get_alias(name=old_alias)
|
|
99
|
+
actions = []
|
|
100
|
+
|
|
101
|
+
for index_name in aliases_info.keys():
|
|
102
|
+
actions.append({"remove": {"index": index_name, "alias": old_alias}})
|
|
103
|
+
actions.append({"add": {"index": index_name, "alias": new_alias}})
|
|
104
|
+
await client.indices.update_aliases(body={"actions": actions})
|
|
105
|
+
|
|
106
|
+
@staticmethod
|
|
107
|
+
def create_index_name(collection_id: str) -> str:
|
|
108
|
+
"""Create index name from collection ID and uuid4.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
collection_id (str): Collection identifier.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
str: Formatted index name.
|
|
115
|
+
"""
|
|
116
|
+
cleaned = collection_id.translate(_ES_INDEX_NAME_UNSUPPORTED_CHARS_TABLE)
|
|
117
|
+
return f"{ITEMS_INDEX_PREFIX}{cleaned.lower()}_{uuid.uuid4()}"
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
def create_alias_name(collection_id: str, start_date: str) -> str:
|
|
121
|
+
"""Create index name from collection ID and uuid4.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
collection_id (str): Collection identifier.
|
|
125
|
+
start_date (str): Start date for the alias.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
str: Alias name with initial date.
|
|
129
|
+
"""
|
|
130
|
+
cleaned = collection_id.translate(_ES_INDEX_NAME_UNSUPPORTED_CHARS_TABLE)
|
|
131
|
+
return f"{ITEMS_INDEX_PREFIX}{cleaned.lower()}_{start_date}"
|
|
132
|
+
|
|
133
|
+
@staticmethod
|
|
134
|
+
def _create_index_body(aliases: Dict[str, Dict]) -> Dict[str, Any]:
|
|
135
|
+
"""Create index body with common settings.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
aliases (Dict[str, Dict]): Aliases configuration.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Dict[str, Any]: Index body configuration.
|
|
142
|
+
"""
|
|
143
|
+
return {
|
|
144
|
+
"aliases": aliases,
|
|
145
|
+
"mappings": ES_ITEMS_MAPPINGS,
|
|
146
|
+
"settings": ES_ITEMS_SETTINGS,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
@staticmethod
|
|
150
|
+
async def find_latest_item_in_index(client: Any, index_name: str) -> dict[str, Any]:
|
|
151
|
+
"""Find the latest item date in the specified index.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
client: Search engine client instance.
|
|
155
|
+
index_name (str): Name of the index to query.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
datetime: Date of the latest item in the index.
|
|
159
|
+
"""
|
|
160
|
+
query = {
|
|
161
|
+
"size": 1,
|
|
162
|
+
"sort": [{"properties.datetime": {"order": "desc"}}],
|
|
163
|
+
"_source": ["properties.datetime"],
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
response = await client.search(index=index_name, body=query)
|
|
167
|
+
return response["hits"]["hits"][0]
|