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.
@@ -3,7 +3,7 @@
3
3
  import re
4
4
  from typing import Any, Dict, Optional, Union
5
5
 
6
- from pydantic import BaseModel, computed_field, model_validator
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
- path: str
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
- index_: Optional[int] = None
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["path"] = data["path"].lstrip("/").replace("/", ".")
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
- return f"ctx._source.{self.location}.size() - {-self.index_}"
102
+ data["key"] = data["parts"].pop(-1)
103
+ data["nest"] = "/".join(data["parts"])
104
+ data["path"] = data["nest"] + "/" + data["key"]
118
105
 
119
- return self.index_
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
- @computed_field # type: ignore[misc]
122
- @property
123
- def location(self) -> str:
124
- """Compute location of path.
125
-
126
- Returns:
127
- str: path location
128
- """
129
- return self.nest + self.partition + self.key
130
-
131
- @computed_field # type: ignore[misc]
132
- @property
133
- def es_location(self) -> str:
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
- Returns:
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]