sfeos-helpers 6.5.1__py3-none-any.whl → 6.7.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,55 @@
1
+ Metadata-Version: 2.4
2
+ Name: sfeos_helpers
3
+ Version: 6.7.0
4
+ Summary: Helper library for the Elasticsearch and Opensearch stac-fastapi backends.
5
+ Project-URL: Homepage, https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch
6
+ License: MIT
7
+ Keywords: Elasticsearch,FastAPI,Opensearch,STAC,STAC-API,stac-fastapi
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Intended Audience :: Information Technology
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Requires-Python: >=3.9
19
+ Requires-Dist: stac-fastapi-core==6.7.0
20
+ Description-Content-Type: text/markdown
21
+
22
+ # sfeos-helpers
23
+
24
+ <p align="left">
25
+ <img src="https://raw.githubusercontent.com/stac-utils/stac-fastapi-elasticsearch-opensearch/refs/heads/main/assets/sfeos.png" width=1000>
26
+ </p>
27
+
28
+ [![Downloads](https://static.pepy.tech/badge/stac-fastapi-core?color=blue)](https://pepy.tech/project/stac-fastapi-core)
29
+ [![GitHub contributors](https://img.shields.io/github/contributors/stac-utils/stac-fastapi-elasticsearch-opensearch?color=blue)](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/graphs/contributors)
30
+ [![GitHub stars](https://img.shields.io/github/stars/stac-utils/stac-fastapi-elasticsearch-opensearch.svg?color=blue)](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/stargazers)
31
+ [![GitHub forks](https://img.shields.io/github/forks/stac-utils/stac-fastapi-elasticsearch-opensearch.svg?color=blue)](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/network/members)
32
+ [![PyPI version](https://img.shields.io/pypi/v/stac-fastapi-elasticsearch.svg?color=blue)](https://pypi.org/project/stac-fastapi-elasticsearch/)
33
+ [![STAC](https://img.shields.io/badge/STAC-1.1.0-blue.svg)](https://github.com/radiantearth/stac-spec/tree/v1.1.0)
34
+ [![stac-fastapi](https://img.shields.io/badge/stac--fastapi-6.0.0-blue.svg)](https://github.com/stac-utils/stac-fastapi)
35
+
36
+ Helper utilities for the stac-fastapi project. For full documentation, please see the [main README](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/blob/main/README.md).
37
+
38
+ ## Package Information
39
+
40
+ - **Package name**: sfeos-helpers
41
+ - **Description**: Helper utilities for the stac-fastapi project.
42
+ - **Documentation**: [https://stac-utils.github.io/stac-fastapi-elasticsearch-opensearch/](https://stac-utils.github.io/stac-fastapi-elasticsearch-opensearch/)
43
+ - **Source**: [GitHub Repository](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/)
44
+
45
+ ## Installation
46
+
47
+ This package is a dependency of stac-fastapi-elasticsearch and stac-fastapi-opensearch and is typically installed automatically.
48
+
49
+ ```bash
50
+ pip install stac-fastapi-elasticsearch # or stac-fastapi-opensearch
51
+ ```
52
+
53
+ ## Quick Start
54
+
55
+ For detailed usage and examples, please refer to the [main documentation](https://stac-utils.github.io/stac-fastapi-elasticsearch-opensearch/).
@@ -1,17 +1,20 @@
1
- stac_fastapi/sfeos_helpers/mappings.py,sha256=u7ZHyUu3NPS_sXaflfQPSP2Yleld3jgqCG0yKcQ64w4,8600
2
- stac_fastapi/sfeos_helpers/version.py,sha256=FuGC3fKnAmD4Wk95swJ6qCVBs5mZiShrlRKuSH-voyE,45
1
+ stac_fastapi/sfeos_helpers/mappings.py,sha256=IqYd0tQUG-FnWVg6Fp1MU9audntF_-pMGm-BUTGbZMM,8596
2
+ stac_fastapi/sfeos_helpers/version.py,sha256=GMc7YzxyWeUVpr_RMVlweeoH2lCLxOWemF-FOkqKXx8,45
3
+ stac_fastapi/sfeos_helpers/aggregation/README.md,sha256=SDlvCOpKyaJrlJvx84T2RzCnGALe_PK51zNeo3RP9ac,2122
3
4
  stac_fastapi/sfeos_helpers/aggregation/__init__.py,sha256=Mym17lFh90by1GnoQgMyIKAqRNJnvCgVSXDYzjBiPQk,1210
4
5
  stac_fastapi/sfeos_helpers/aggregation/client.py,sha256=PPUk0kAZnms46FlLGrR5w8wa52vG-dT6BG37896R5CY,17939
5
6
  stac_fastapi/sfeos_helpers/aggregation/format.py,sha256=qUW1jjh2EEjy-V7riliFR77grpi-AgsTmP76z60K5Lo,2011
6
- stac_fastapi/sfeos_helpers/database/__init__.py,sha256=T0YwePfhG3ukL1oUFCh3FYHA9jZZe36FJRYCQplfb18,2645
7
+ stac_fastapi/sfeos_helpers/database/README.md,sha256=TVYFDD4PqDD57ZsWBv4i4LawaL_DAEIOjM6OQuqwLAU,4049
8
+ stac_fastapi/sfeos_helpers/database/__init__.py,sha256=Kvnz8hpXq_sSz8K5OW3PoPsvh9864Vv1zWhI5hxgd4o,2891
7
9
  stac_fastapi/sfeos_helpers/database/datetime.py,sha256=XMyi9Q09cuP_hj97qbGbHFtelq7WQVPdehUfzqNZFV4,4040
8
10
  stac_fastapi/sfeos_helpers/database/document.py,sha256=LtjX15gvaOuZC_k2t_oQhys_c-zRTLN5rwX0hNJkHnM,1725
9
11
  stac_fastapi/sfeos_helpers/database/index.py,sha256=g7_sKfd5XUwq4IhdKRNiasejk045dKlullsdeDSZTq8,6585
10
12
  stac_fastapi/sfeos_helpers/database/mapping.py,sha256=4-MSd4xH5wg7yoC4aPjzYMDSEvP026bw4k2TfffMT5E,1387
11
- stac_fastapi/sfeos_helpers/database/query.py,sha256=g2iGdfgqpx6o8GoQJBMl3AMmqcbSf792qvKWfWipR5w,4193
12
- stac_fastapi/sfeos_helpers/database/utils.py,sha256=9zU9hEglZb6f-uxOhd95saSw2id9w5PR36dWtyfXTb0,8757
13
+ stac_fastapi/sfeos_helpers/database/query.py,sha256=bbSYe0cLC7oFbhkHR5WTKCF7Ca9iZI3fdanD90KYN98,9476
14
+ stac_fastapi/sfeos_helpers/database/utils.py,sha256=CLtZgoUT37oklc9MsExXsxDviv4bzK-ZP7oxAOXS32Y,11780
15
+ stac_fastapi/sfeos_helpers/filter/README.md,sha256=Rb5qHmDkI-7-o3I82Lb_zfmrviqUj958wef021xI6pQ,1955
13
16
  stac_fastapi/sfeos_helpers/filter/__init__.py,sha256=n3zL_MhEGOoxMz1KeijyK_UKiZ0MKPl90zHtYI5RAy8,1557
14
- stac_fastapi/sfeos_helpers/filter/client.py,sha256=QQ6gnUEMqzS_qG8G5QHcX9x3LNk5JvCp1rRJltMTmM4,4414
17
+ stac_fastapi/sfeos_helpers/filter/client.py,sha256=QZP0Dm_T7SoMdR65IOjFmKBW7Rphr4z2xPgozZ93TPs,5339
15
18
  stac_fastapi/sfeos_helpers/filter/cql2.py,sha256=Cg9kRYD9CVkVSyRqOyB5oVXmlyteSn2bw88sqklGpUM,955
16
19
  stac_fastapi/sfeos_helpers/filter/transform.py,sha256=1GEWQSp-rbq7_1nDVv1ApDbWxt8DswJWxwaxzV85gj4,4644
17
20
  stac_fastapi/sfeos_helpers/models/patch.py,sha256=s5n85ktnH6M2SMqpqyItR8uLxliXmnSTg1WO0QLVsmI,3127
@@ -26,7 +29,6 @@ stac_fastapi/sfeos_helpers/search_engine/selection/base.py,sha256=106c4FK50cgMmT
26
29
  stac_fastapi/sfeos_helpers/search_engine/selection/cache_manager.py,sha256=5yrgf9JA4mgRNMPDKih6xySF8mD724lEWnXhWud7m2c,4039
27
30
  stac_fastapi/sfeos_helpers/search_engine/selection/factory.py,sha256=vbgNVCUW2lviePqzpgsPLxp6IEqcX3GHiahqN2oVObA,1305
28
31
  stac_fastapi/sfeos_helpers/search_engine/selection/selectors.py,sha256=q83nfCfNfLUqtkHpORwNHNRU9Pa-heeaDIPO0RlHb-8,4779
29
- sfeos_helpers-6.5.1.dist-info/METADATA,sha256=yCy4qbL5pERMI0QAwOwK2PKff7u0Cus4BLjRr8xuGHg,41326
30
- sfeos_helpers-6.5.1.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
31
- sfeos_helpers-6.5.1.dist-info/top_level.txt,sha256=vqn-D9-HsRPTTxy0Vk_KkDmTiMES4owwBQ3ydSZYb2s,13
32
- sfeos_helpers-6.5.1.dist-info/RECORD,,
32
+ sfeos_helpers-6.7.0.dist-info/METADATA,sha256=GHCTNfc1-AdmRtby5GAqUQMmB56qyZdhRpGudb63mp4,3214
33
+ sfeos_helpers-6.7.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
34
+ sfeos_helpers-6.7.0.dist-info/RECORD,,
@@ -1,5 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.45.1)
2
+ Generator: hatchling 1.27.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
-
@@ -0,0 +1,57 @@
1
+ # STAC FastAPI Aggregation Package
2
+
3
+ This package contains shared aggregation functionality used by both the Elasticsearch and OpenSearch implementations of STAC FastAPI. It helps reduce code duplication and ensures consistent behavior between the two implementations.
4
+
5
+ ## Package Structure
6
+
7
+ The aggregation package is organized into three main modules:
8
+
9
+ - **client.py**: Contains the base aggregation client implementation
10
+ - `EsAsyncBaseAggregationClient`: The main class that implements the STAC aggregation extension for Elasticsearch/OpenSearch
11
+ - Methods for handling aggregation requests, validating parameters, and formatting responses
12
+
13
+ - **format.py**: Contains functions for formatting aggregation responses
14
+ - `frequency_agg`: Formats frequency distribution aggregation responses
15
+ - `metric_agg`: Formats metric aggregation responses
16
+
17
+ - **__init__.py**: Package initialization and exports
18
+ - Exports the main classes and functions for use by other modules
19
+
20
+ ## Features
21
+
22
+ The aggregation package provides the following features:
23
+
24
+ - Support for various aggregation types:
25
+ - Datetime frequency
26
+ - Collection frequency
27
+ - Property frequency
28
+ - Geospatial grid aggregations (geohash, geohex, geotile)
29
+ - Metric aggregations (min, max, etc.)
30
+
31
+ - Parameter validation:
32
+ - Precision validation for geospatial aggregations
33
+ - Interval validation for datetime aggregations
34
+
35
+ - Response formatting:
36
+ - Consistent response structure
37
+ - Proper typing and documentation
38
+
39
+ ## Usage
40
+
41
+ The aggregation package is used by the Elasticsearch and OpenSearch implementations to provide aggregation functionality for STAC API. The main entry point is the `EsAsyncBaseAggregationClient` class, which is instantiated in the respective app.py files.
42
+
43
+ Example:
44
+ ```python
45
+ from stac_fastapi.sfeos_helpers.aggregation import EsAsyncBaseAggregationClient
46
+
47
+ # Create an instance of the aggregation client
48
+ aggregation_client = EsAsyncBaseAggregationClient(database)
49
+
50
+ # Register the aggregation extension with the API
51
+ api = StacApi(
52
+ ...,
53
+ extensions=[
54
+ ...,
55
+ AggregationExtension(client=aggregation_client),
56
+ ],
57
+ )
@@ -0,0 +1,61 @@
1
+ # STAC FastAPI Database Package
2
+
3
+ This package contains shared database operations used by both the Elasticsearch and OpenSearch
4
+ implementations of STAC FastAPI. It helps reduce code duplication and ensures consistent behavior
5
+ between the two implementations.
6
+
7
+ ## Package Structure
8
+
9
+ The database package is organized into five main modules:
10
+
11
+ - **index.py**: Contains functions for managing indices
12
+ - [create_index_templates_shared](cci:1://file:///home/computer/Code/stac-fastapi-elasticsearch-opensearch/stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/database_logic_helpers.py:15:0-48:33): Creates index templates for Collections and Items
13
+ - [delete_item_index_shared](cci:1://file:///home/computer/Code/stac-fastapi-elasticsearch-opensearch/stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/database_logic_helpers.py:128:0-153:30): Deletes an item index for a collection
14
+ - [index_by_collection_id](cci:1://file:///home/computer/Code/stac-fastapi-elasticsearch-opensearch/stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/utilities.py:86:0-100:5): Translates a collection ID into an index name
15
+ - [index_alias_by_collection_id](cci:1://file:///home/computer/Code/stac-fastapi-elasticsearch-opensearch/stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/utilities.py:103:0-115:5): Translates a collection ID into an index alias
16
+ - [indices](cci:1://file:///home/computer/Code/stac-fastapi-elasticsearch-opensearch/stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/utilities.py:118:0-132:5): Gets a comma-separated string of index names
17
+
18
+ - **query.py**: Contains functions for building and manipulating queries
19
+ - [apply_free_text_filter_shared](cci:1://file:///home/computer/Code/stac-fastapi-elasticsearch-opensearch/stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/database_logic_helpers.py:51:0-74:16): Applies a free text filter to a search
20
+ - [apply_intersects_filter_shared](cci:1://file:///home/computer/Code/stac-fastapi-elasticsearch-opensearch/stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/database_logic_helpers.py:77:0-104:5): Creates a geo_shape filter for intersecting geometry
21
+ - [populate_sort_shared](cci:1://file:///home/computer/Code/stac-fastapi-elasticsearch-opensearch/stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/database_logic_helpers.py:107:0-125:16): Creates a sort configuration for queries
22
+
23
+ - **mapping.py**: Contains functions for working with mappings
24
+ - [get_queryables_mapping_shared](cci:1://file:///home/computer/Code/stac-fastapi-elasticsearch-opensearch/stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/database_logic_helpers.py:156:0-185:27): Retrieves mapping of Queryables for search
25
+
26
+ - **document.py**: Contains functions for working with documents
27
+ - [mk_item_id](cci:1://file:///home/computer/Code/stac-fastapi-elasticsearch-opensearch/stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/utilities.py:140:0-150:5): Creates a document ID for an Item
28
+ - [mk_actions](cci:1://file:///home/computer/Code/stac-fastapi-elasticsearch-opensearch/stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/utilities.py:153:0-175:5): Creates bulk actions for indexing items
29
+
30
+ - **utils.py**: Contains utility functions for database operations
31
+ - [validate_refresh](cci:1://file:///home/computer/Code/stac-fastapi-elasticsearch-opensearch/stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/utilities.py:41:0-78:5): Validates the refresh parameter value
32
+
33
+ ## Usage
34
+
35
+ Import the necessary components from the database package:
36
+
37
+ ```python
38
+ from stac_fastapi.sfeos_helpers.database import (
39
+ # Index operations
40
+ create_index_templates_shared,
41
+ delete_item_index_shared,
42
+ index_alias_by_collection_id,
43
+ index_by_collection_id,
44
+ indices,
45
+
46
+ # Query operations
47
+ apply_free_text_filter_shared,
48
+ apply_intersects_filter_shared,
49
+ populate_sort_shared,
50
+
51
+ # Mapping operations
52
+ get_queryables_mapping_shared,
53
+
54
+ # Document operations
55
+ mk_item_id,
56
+ mk_actions,
57
+
58
+ # Utility functions
59
+ validate_refresh,
60
+ )
61
+ ```
@@ -42,11 +42,13 @@ from .index import (
42
42
  )
43
43
  from .mapping import get_queryables_mapping_shared
44
44
  from .query import (
45
+ apply_collections_bbox_filter_shared,
46
+ apply_collections_datetime_filter_shared,
45
47
  apply_free_text_filter_shared,
46
48
  apply_intersects_filter_shared,
47
49
  populate_sort_shared,
48
50
  )
49
- from .utils import get_bool_env, validate_refresh
51
+ from .utils import add_bbox_shape_to_collection, get_bool_env, validate_refresh
50
52
 
51
53
  __all__ = [
52
54
  # Index operations
@@ -59,6 +61,8 @@ __all__ = [
59
61
  # Query operations
60
62
  "apply_free_text_filter_shared",
61
63
  "apply_intersects_filter_shared",
64
+ "apply_collections_bbox_filter_shared",
65
+ "apply_collections_datetime_filter_shared",
62
66
  "populate_sort_shared",
63
67
  # Mapping operations
64
68
  "get_queryables_mapping_shared",
@@ -68,6 +72,7 @@ __all__ = [
68
72
  # Utility functions
69
73
  "validate_refresh",
70
74
  "get_bool_env",
75
+ "add_bbox_shape_to_collection",
71
76
  # Datetime utilities
72
77
  "return_date",
73
78
  "extract_date",
@@ -3,8 +3,10 @@
3
3
  This module provides functions for building and manipulating Elasticsearch/OpenSearch queries.
4
4
  """
5
5
 
6
- from typing import Any, Dict, List, Optional
6
+ import logging
7
+ from typing import Any, Dict, List, Optional, Union
7
8
 
9
+ from stac_fastapi.core.utilities import bbox2polygon
8
10
  from stac_fastapi.sfeos_helpers.mappings import Geometry
9
11
 
10
12
  ES_MAX_URL_LENGTH = 4096
@@ -66,6 +68,139 @@ def apply_intersects_filter_shared(
66
68
  }
67
69
 
68
70
 
71
+ def apply_collections_datetime_filter_shared(
72
+ datetime_str: Optional[str],
73
+ ) -> Optional[Dict[str, Any]]:
74
+ """Create a temporal filter for collections based on their extent.
75
+
76
+ Args:
77
+ datetime_str: The datetime parameter. Can be:
78
+ - A single datetime string (e.g., "2020-01-01T00:00:00Z")
79
+ - A datetime range with "/" separator (e.g., "2020-01-01T00:00:00Z/2021-01-01T00:00:00Z")
80
+ - Open-ended ranges using ".." (e.g., "../2021-01-01T00:00:00Z" or "2020-01-01T00:00:00Z/..")
81
+ - None if no datetime filter is provided
82
+
83
+ Returns:
84
+ Optional[Dict[str, Any]]: A dictionary containing the temporal filter configuration
85
+ that can be used with Elasticsearch/OpenSearch queries, or None if datetime_str is None.
86
+ Example return value:
87
+ {
88
+ "bool": {
89
+ "must": [
90
+ {"range": {"extent.temporal.interval": {"lte": "2021-01-01T00:00:00Z"}}},
91
+ {"range": {"extent.temporal.interval": {"gte": "2020-01-01T00:00:00Z"}}}
92
+ ]
93
+ }
94
+ }
95
+
96
+ Notes:
97
+ - This function is specifically for filtering collections by their temporal extent
98
+ - It queries the extent.temporal.interval field
99
+ - Open-ended ranges (..) are replaced with concrete dates (1800-01-01 for start, 2999-12-31 for end)
100
+ """
101
+ if not datetime_str:
102
+ return None
103
+
104
+ # Parse the datetime string into start and end
105
+ if "/" in datetime_str:
106
+ start, end = datetime_str.split("/")
107
+ # Replace open-ended ranges with concrete dates
108
+ if start == "..":
109
+ # For open-ended start, use a very early date
110
+ start = "1800-01-01T00:00:00Z"
111
+ if end == "..":
112
+ # For open-ended end, use a far future date
113
+ end = "2999-12-31T23:59:59Z"
114
+ else:
115
+ # If it's just a single date, use it for both start and end
116
+ start = end = datetime_str
117
+
118
+ return {
119
+ "bool": {
120
+ "must": [
121
+ # Check if any date in the array is less than or equal to the query end date
122
+ # This will match if the collection's start date is before or equal to the query end date
123
+ {"range": {"extent.temporal.interval": {"lte": end}}},
124
+ # Check if any date in the array is greater than or equal to the query start date
125
+ # This will match if the collection's end date is after or equal to the query start date
126
+ {"range": {"extent.temporal.interval": {"gte": start}}},
127
+ ]
128
+ }
129
+ }
130
+
131
+
132
+ def apply_collections_bbox_filter_shared(
133
+ bbox: Union[str, List[float], None]
134
+ ) -> Optional[Dict[str, Dict]]:
135
+ """Create a geo_shape filter for collections bbox search.
136
+
137
+ This function handles bbox parsing from both GET requests (string format) and POST requests
138
+ (list format), and constructs a geo_shape query for filtering collections by their bbox_shape field.
139
+
140
+ Args:
141
+ bbox: The bounding box parameter. Can be:
142
+ - A string of comma-separated coordinates (from GET requests)
143
+ - A list of floats [minx, miny, maxx, maxy] for 2D bbox
144
+ - None if no bbox filter is provided
145
+
146
+ Returns:
147
+ Optional[Dict[str, Dict]]: A dictionary containing the geo_shape filter configuration
148
+ that can be used with Elasticsearch/OpenSearch queries, or None if bbox is invalid.
149
+ Example return value:
150
+ {
151
+ "geo_shape": {
152
+ "bbox_shape": {
153
+ "shape": {
154
+ "type": "Polygon",
155
+ "coordinates": [[[minx, miny], [maxx, miny], [maxx, maxy], [minx, maxy], [minx, miny]]]
156
+ },
157
+ "relation": "intersects"
158
+ }
159
+ }
160
+ }
161
+
162
+ Notes:
163
+ - This function is specifically for filtering collections by their spatial extent
164
+ - It queries the bbox_shape field (not the geometry field used for items)
165
+ - The bbox is expected to be 2D (4 values) after any 3D to 2D conversion in the API layer
166
+ """
167
+ logger = logging.getLogger(__name__)
168
+
169
+ if not bbox:
170
+ return None
171
+
172
+ # Parse bbox if it's a string (from GET requests)
173
+ if isinstance(bbox, str):
174
+ try:
175
+ bbox = [float(x.strip()) for x in bbox.split(",")]
176
+ except (ValueError, AttributeError) as e:
177
+ logger.error(f"Invalid bbox format: {bbox}, error: {e}")
178
+ return None
179
+
180
+ if not bbox or len(bbox) != 4:
181
+ if bbox:
182
+ logger.warning(
183
+ f"bbox has incorrect number of coordinates (length={len(bbox)}), expected 4 (2D bbox)"
184
+ )
185
+ return None
186
+
187
+ # Convert bbox to a polygon for geo_shape query
188
+ bbox_polygon = {
189
+ "type": "Polygon",
190
+ "coordinates": bbox2polygon(bbox[0], bbox[1], bbox[2], bbox[3]),
191
+ }
192
+
193
+ # Return geo_shape query for bbox_shape field
194
+ return {
195
+ "geo_shape": {
196
+ "bbox_shape": {
197
+ "shape": bbox_polygon,
198
+ "relation": "intersects",
199
+ }
200
+ }
201
+ }
202
+
203
+
69
204
  def populate_sort_shared(sortby: List) -> Optional[Dict[str, Dict[str, str]]]:
70
205
  """Create a sort configuration for Elasticsearch/OpenSearch queries.
71
206
 
@@ -5,9 +5,9 @@ in Elasticsearch/OpenSearch, such as parameter validation.
5
5
  """
6
6
 
7
7
  import logging
8
- from typing import Dict, List, Union
8
+ from typing import Any, Dict, List, Union
9
9
 
10
- from stac_fastapi.core.utilities import get_bool_env
10
+ from stac_fastapi.core.utilities import bbox2polygon, get_bool_env
11
11
  from stac_fastapi.extensions.core.transaction.request import (
12
12
  PatchAddReplaceTest,
13
13
  PatchOperation,
@@ -15,6 +15,84 @@ from stac_fastapi.extensions.core.transaction.request import (
15
15
  )
16
16
  from stac_fastapi.sfeos_helpers.models.patch import ElasticPath, ESCommandSet
17
17
 
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ def add_bbox_shape_to_collection(collection: Dict[str, Any]) -> bool:
22
+ """Add bbox_shape field to a collection document for spatial queries.
23
+
24
+ This function extracts the bounding box from a collection's spatial extent
25
+ and converts it to a GeoJSON polygon shape that can be used for geospatial
26
+ queries in Elasticsearch/OpenSearch.
27
+
28
+ Args:
29
+ collection: Collection document dictionary to modify in-place.
30
+
31
+ Returns:
32
+ bool: True if bbox_shape was added, False if it was skipped (already exists,
33
+ no spatial extent, or invalid bbox).
34
+
35
+ Notes:
36
+ - Modifies the collection dictionary in-place by adding a 'bbox_shape' field
37
+ - Handles both 2D [minx, miny, maxx, maxy] and 3D [minx, miny, minz, maxx, maxy, maxz] bboxes
38
+ - Uses the first bbox if multiple are present in the collection
39
+ - Logs warnings for collections with invalid or missing bbox data
40
+ """
41
+ collection_id = collection.get("id", "unknown")
42
+
43
+ # Check if bbox_shape already exists
44
+ if "bbox_shape" in collection:
45
+ logger.debug(
46
+ f"Collection '{collection_id}' already has bbox_shape field, skipping"
47
+ )
48
+ return False
49
+
50
+ # Check if collection has spatial extent
51
+ if "extent" not in collection or "spatial" not in collection["extent"]:
52
+ logger.warning(f"Collection '{collection_id}' has no spatial extent, skipping")
53
+ return False
54
+
55
+ spatial_extent = collection["extent"]["spatial"]
56
+ if "bbox" not in spatial_extent or not spatial_extent["bbox"]:
57
+ logger.warning(
58
+ f"Collection '{collection_id}' has no bbox in spatial extent, skipping"
59
+ )
60
+ return False
61
+
62
+ # Get the first bbox (collections can have multiple bboxes, but we use the first one)
63
+ bbox = (
64
+ spatial_extent["bbox"][0]
65
+ if isinstance(spatial_extent["bbox"][0], list)
66
+ else spatial_extent["bbox"]
67
+ )
68
+
69
+ if len(bbox) < 4:
70
+ logger.warning(
71
+ f"Collection '{collection_id}': bbox has insufficient coordinates (length={len(bbox)}), expected at least 4"
72
+ )
73
+ return False
74
+
75
+ # Extract 2D coordinates (bbox can be 2D [minx, miny, maxx, maxy] or 3D [minx, miny, minz, maxx, maxy, maxz])
76
+ # For 2D polygon, we only need the x,y coordinates and discard altitude (z) values
77
+ minx, miny = bbox[0], bbox[1]
78
+ if len(bbox) == 4:
79
+ # 2D bbox: [minx, miny, maxx, maxy]
80
+ maxx, maxy = bbox[2], bbox[3]
81
+ else:
82
+ # 3D bbox: [minx, miny, minz, maxx, maxy, maxz]
83
+ # Extract indices 3,4 for maxx,maxy - discarding altitude at indices 2 (minz) and 5 (maxz)
84
+ maxx, maxy = bbox[3], bbox[4]
85
+
86
+ # Convert bbox to GeoJSON polygon
87
+ bbox_polygon_coords = bbox2polygon(minx, miny, maxx, maxy)
88
+ collection["bbox_shape"] = {
89
+ "type": "Polygon",
90
+ "coordinates": bbox_polygon_coords,
91
+ }
92
+
93
+ logger.debug(f"Collection '{collection_id}': Added bbox_shape field")
94
+ return True
95
+
18
96
 
19
97
  def validate_refresh(value: Union[str, bool]) -> str:
20
98
  """
@@ -0,0 +1,30 @@
1
+ # STAC FastAPI Filter Package
2
+
3
+ This package contains shared filter extension functionality used by both the Elasticsearch and OpenSearch
4
+ implementations of STAC FastAPI. It helps reduce code duplication and ensures consistent behavior
5
+ between the two implementations.
6
+
7
+ ## Package Structure
8
+
9
+ The filter package is organized into three main modules:
10
+
11
+ - **cql2.py**: Contains functions for converting CQL2 patterns to Elasticsearch/OpenSearch compatible formats
12
+
13
+ - [cql2_like_to_es](cci:1://file:///home/computer/Code/stac-fastapi-elasticsearch-opensearch/stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/filter.py:59:0-75:5): Converts CQL2 "LIKE" characters to Elasticsearch "wildcard" characters
14
+ - [\_replace_like_patterns](cci:1://file:///home/computer/Code/stac-fastapi-elasticsearch-opensearch/stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/filter.py:51:0-56:71): Helper function for pattern replacement
15
+
16
+ - **transform.py**: Contains functions for transforming CQL2 queries to Elasticsearch/OpenSearch query DSL
17
+
18
+ - [to_es_field](cci:1://file:///home/computer/Code/stac-fastapi-elasticsearch-opensearch/stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/filter.py:83:0-93:47): Maps field names using queryables mapping
19
+ - [to_es](cci:1://file:///home/computer/Code/stac-fastapi-elasticsearch-opensearch/stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/filter.py:96:0-201:13): Transforms CQL2 query structures to Elasticsearch/OpenSearch query DSL
20
+
21
+ - **client.py**: Contains the base filter client implementation
22
+ - [EsAsyncBaseFiltersClient](cci:2://file:///home/computer/Code/stac-fastapi-elasticsearch-opensearch/stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/filter.py:209:0-293:25): Base class for implementing the STAC filter extension
23
+
24
+ ## Usage
25
+
26
+ Import the necessary components from the filter package:
27
+
28
+ ```python
29
+ from stac_fastapi.sfeos_helpers.filter import cql2_like_to_es, to_es, EsAsyncBaseFiltersClient
30
+ ```
@@ -1,7 +1,8 @@
1
1
  """Filter client implementation for Elasticsearch/OpenSearch."""
2
2
 
3
+ import os
3
4
  from collections import deque
4
- from typing import Any, Dict, Optional, Tuple
5
+ from typing import Any, Optional
5
6
 
6
7
  import attr
7
8
  from fastapi import Request
@@ -18,9 +19,29 @@ class EsAsyncBaseFiltersClient(AsyncBaseFiltersClient):
18
19
 
19
20
  database: BaseDatabaseLogic = attr.ib()
20
21
 
22
+ @staticmethod
23
+ def _get_excluded_from_queryables() -> set[str]:
24
+ """Get fields to exclude from queryables endpoint and filtering.
25
+
26
+ Reads from EXCLUDED_FROM_QUERYABLES environment variable.
27
+ Supports comma-separated list of field names.
28
+
29
+ Example:
30
+ EXCLUDED_FROM_QUERYABLES="auth:schemes,storage:schemes"
31
+
32
+ Returns:
33
+ Set[str]: Set of field names to exclude from queryables
34
+ """
35
+ excluded = os.getenv("EXCLUDED_FROM_QUERYABLES", "")
36
+ if not excluded:
37
+ return set()
38
+ return {field.strip() for field in excluded.split(",") if field.strip()}
39
+
21
40
  async def get_queryables(
22
- self, collection_id: Optional[str] = None, **kwargs
23
- ) -> Dict[str, Any]:
41
+ self,
42
+ collection_id: Optional[str] = None, # noqa: UP045
43
+ **kwargs: Any,
44
+ ) -> dict[str, Any]:
24
45
  """Get the queryables available for the given collection_id.
25
46
 
26
47
  If collection_id is None, returns the intersection of all
@@ -38,21 +59,23 @@ class EsAsyncBaseFiltersClient(AsyncBaseFiltersClient):
38
59
  Returns:
39
60
  Dict[str, Any]: A dictionary containing the queryables for the given collection.
40
61
  """
41
- request: Optional[Request] = kwargs.get("request")
42
- url_str: str = str(request.url) if request else ""
43
- queryables: Dict[str, Any] = {
62
+ request: Optional[Request] = kwargs.get("request") # noqa: UP045
63
+ url_str = str(request.url) if request else ""
64
+
65
+ queryables: dict[str, Any] = {
44
66
  "$schema": "https://json-schema.org/draft-07/schema",
45
- "$id": f"{url_str}",
67
+ "$id": url_str,
46
68
  "type": "object",
47
69
  "title": "Queryables for STAC API",
48
70
  "description": "Queryable names for the STAC API Item Search filter.",
49
71
  "properties": DEFAULT_QUERYABLES,
50
72
  "additionalProperties": True,
51
73
  }
74
+
52
75
  if not collection_id:
53
76
  return queryables
54
77
 
55
- properties: Dict[str, Any] = queryables["properties"].copy()
78
+ properties = queryables["properties"].copy()
56
79
  queryables.update(
57
80
  {
58
81
  "properties": properties,
@@ -62,8 +85,9 @@ class EsAsyncBaseFiltersClient(AsyncBaseFiltersClient):
62
85
 
63
86
  mapping_data = await self.database.get_items_mapping(collection_id)
64
87
  mapping_properties = next(iter(mapping_data.values()))["mappings"]["properties"]
65
- stack: deque[Tuple[str, Dict[str, Any]]] = deque(mapping_properties.items())
66
- enum_fields: Dict[str, Dict[str, Any]] = {}
88
+ stack: deque[tuple[str, dict[str, Any]]] = deque(mapping_properties.items())
89
+ enum_fields: dict[str, dict[str, Any]] = {}
90
+ excluded_fields = self._get_excluded_from_queryables()
67
91
 
68
92
  while stack:
69
93
  field_fqn, field_def = stack.popleft()
@@ -75,11 +99,16 @@ class EsAsyncBaseFiltersClient(AsyncBaseFiltersClient):
75
99
  (f"{field_fqn}.{k}", v)
76
100
  for k, v in field_properties.items()
77
101
  if v.get("enabled", True)
102
+ and f"{field_fqn}.{k}" not in excluded_fields
78
103
  )
79
104
 
80
105
  # Skip non-indexed or disabled fields
81
106
  field_type = field_def.get("type")
82
- if not field_type or not field_def.get("enabled", True):
107
+ if (
108
+ not field_type
109
+ or not field_def.get("enabled", True)
110
+ or field_fqn in excluded_fields
111
+ ):
83
112
  continue
84
113
 
85
114
  # Fields in Item Properties should be exposed with their un-prefixed names,
@@ -88,7 +117,7 @@ class EsAsyncBaseFiltersClient(AsyncBaseFiltersClient):
88
117
  field_name = field_fqn.removeprefix("properties.")
89
118
 
90
119
  # Generate field properties
91
- field_result = ALL_QUERYABLES.get(field_name, {})
120
+ field_result = ALL_QUERYABLES.get(field_name, {}).copy()
92
121
  properties[field_name] = field_result
93
122
 
94
123
  field_name_human = field_name.replace("_", " ").title()
@@ -104,9 +133,10 @@ class EsAsyncBaseFiltersClient(AsyncBaseFiltersClient):
104
133
  enum_fields[field_fqn] = field_result
105
134
 
106
135
  if enum_fields:
107
- for field_fqn, unique_values in (
108
- await self.database.get_items_unique_values(collection_id, enum_fields)
109
- ).items():
110
- enum_fields[field_fqn]["enum"] = unique_values
136
+ unique_values = await self.database.get_items_unique_values(
137
+ collection_id, enum_fields
138
+ )
139
+ for field_fqn, values in unique_values.items():
140
+ enum_fields[field_fqn]["enum"] = values
111
141
 
112
142
  return queryables