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.
- sfeos_helpers-6.7.0.dist-info/METADATA +55 -0
- {sfeos_helpers-6.5.1.dist-info → sfeos_helpers-6.7.0.dist-info}/RECORD +12 -10
- {sfeos_helpers-6.5.1.dist-info → sfeos_helpers-6.7.0.dist-info}/WHEEL +1 -2
- stac_fastapi/sfeos_helpers/aggregation/README.md +57 -0
- stac_fastapi/sfeos_helpers/database/README.md +61 -0
- stac_fastapi/sfeos_helpers/database/__init__.py +6 -1
- stac_fastapi/sfeos_helpers/database/query.py +136 -1
- stac_fastapi/sfeos_helpers/database/utils.py +80 -2
- stac_fastapi/sfeos_helpers/filter/README.md +30 -0
- stac_fastapi/sfeos_helpers/filter/client.py +46 -16
- stac_fastapi/sfeos_helpers/mappings.py +1 -1
- stac_fastapi/sfeos_helpers/version.py +1 -1
- sfeos_helpers-6.5.1.dist-info/METADATA +0 -714
- sfeos_helpers-6.5.1.dist-info/top_level.txt +0 -1
|
@@ -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
|
+
[](https://pepy.tech/project/stac-fastapi-core)
|
|
29
|
+
[](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/graphs/contributors)
|
|
30
|
+
[](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/stargazers)
|
|
31
|
+
[](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/network/members)
|
|
32
|
+
[](https://pypi.org/project/stac-fastapi-elasticsearch/)
|
|
33
|
+
[](https://github.com/radiantearth/stac-spec/tree/v1.1.0)
|
|
34
|
+
[](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=
|
|
2
|
-
stac_fastapi/sfeos_helpers/version.py,sha256=
|
|
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/
|
|
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=
|
|
12
|
-
stac_fastapi/sfeos_helpers/database/utils.py,sha256=
|
|
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=
|
|
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.
|
|
30
|
-
sfeos_helpers-6.
|
|
31
|
-
sfeos_helpers-6.
|
|
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,,
|
|
@@ -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
|
-
|
|
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,
|
|
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,
|
|
23
|
-
|
|
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
|
|
43
|
-
|
|
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":
|
|
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
|
|
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[
|
|
66
|
-
enum_fields:
|
|
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
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
)
|
|
110
|
-
|
|
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
|