sfeos-helpers 6.7.5__py3-none-any.whl → 6.8.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sfeos_helpers
3
- Version: 6.7.5
3
+ Version: 6.8.0
4
4
  Summary: Helper library for the Elasticsearch and Opensearch stac-fastapi backends.
5
5
  Project-URL: Homepage, https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch
6
6
  License: MIT
@@ -14,7 +14,7 @@ Classifier: Programming Language :: Python :: 3.12
14
14
  Classifier: Programming Language :: Python :: 3.13
15
15
  Classifier: Programming Language :: Python :: 3.14
16
16
  Requires-Python: >=3.11
17
- Requires-Dist: stac-fastapi-core==6.7.5
17
+ Requires-Dist: stac-fastapi-core==6.8.0
18
18
  Description-Content-Type: text/markdown
19
19
 
20
20
  # sfeos-helpers
@@ -29,7 +29,7 @@ Description-Content-Type: text/markdown
29
29
  [![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)
30
30
  [![PyPI version](https://img.shields.io/pypi/v/stac-fastapi-elasticsearch.svg?color=blue)](https://pypi.org/project/stac-fastapi-elasticsearch/)
31
31
  [![STAC](https://img.shields.io/badge/STAC-1.1.0-blue.svg)](https://github.com/radiantearth/stac-spec/tree/v1.1.0)
32
- [![stac-fastapi](https://img.shields.io/badge/stac--fastapi-6.0.0-blue.svg)](https://github.com/stac-utils/stac-fastapi)
32
+ [![stac-fastapi](https://img.shields.io/badge/stac--fastapi-6.1.1-blue.svg)](https://github.com/stac-utils/stac-fastapi)
33
33
 
34
34
  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).
35
35
 
@@ -1,22 +1,22 @@
1
- stac_fastapi/sfeos_helpers/mappings.py,sha256=AikPY20F7cqbiHHkFEmWdMtxruhfNJbYK7hugKq7qTQ,8602
2
- stac_fastapi/sfeos_helpers/version.py,sha256=WRUWk2u8pozjztw-4p6a3fAkitIN0nGhe4tLZ6P6xUk,45
1
+ stac_fastapi/sfeos_helpers/mappings.py,sha256=uzgwm-JTTLU_yti6YsjDBk790AuReF5YavLTrp4Otx0,8645
2
+ stac_fastapi/sfeos_helpers/version.py,sha256=ib-H9zXMsXhMlZkHut5gkqf27FPYcv0_wBkKUIXnJbA,45
3
3
  stac_fastapi/sfeos_helpers/aggregation/README.md,sha256=SDlvCOpKyaJrlJvx84T2RzCnGALe_PK51zNeo3RP9ac,2122
4
4
  stac_fastapi/sfeos_helpers/aggregation/__init__.py,sha256=Mym17lFh90by1GnoQgMyIKAqRNJnvCgVSXDYzjBiPQk,1210
5
5
  stac_fastapi/sfeos_helpers/aggregation/client.py,sha256=PPUk0kAZnms46FlLGrR5w8wa52vG-dT6BG37896R5CY,17939
6
6
  stac_fastapi/sfeos_helpers/aggregation/format.py,sha256=qUW1jjh2EEjy-V7riliFR77grpi-AgsTmP76z60K5Lo,2011
7
7
  stac_fastapi/sfeos_helpers/database/README.md,sha256=TVYFDD4PqDD57ZsWBv4i4LawaL_DAEIOjM6OQuqwLAU,4049
8
8
  stac_fastapi/sfeos_helpers/database/__init__.py,sha256=Kvnz8hpXq_sSz8K5OW3PoPsvh9864Vv1zWhI5hxgd4o,2891
9
- stac_fastapi/sfeos_helpers/database/datetime.py,sha256=XHS5mg9NXUi2P0FtBpjTjy-g7nRzCtFgp3juq6Fr1_M,6831
9
+ stac_fastapi/sfeos_helpers/database/datetime.py,sha256=LMh8dFjifpjfB_IKvOqQ7bQWMy4SXAYvWaLHMsad4tg,6806
10
10
  stac_fastapi/sfeos_helpers/database/document.py,sha256=LtjX15gvaOuZC_k2t_oQhys_c-zRTLN5rwX0hNJkHnM,1725
11
11
  stac_fastapi/sfeos_helpers/database/index.py,sha256=g7_sKfd5XUwq4IhdKRNiasejk045dKlullsdeDSZTq8,6585
12
- stac_fastapi/sfeos_helpers/database/mapping.py,sha256=4-MSd4xH5wg7yoC4aPjzYMDSEvP026bw4k2TfffMT5E,1387
12
+ stac_fastapi/sfeos_helpers/database/mapping.py,sha256=4vGUuBLGoBOkQ984pYOciiz9UUWb4sbZyt-iViIsmdM,3809
13
13
  stac_fastapi/sfeos_helpers/database/query.py,sha256=bbSYe0cLC7oFbhkHR5WTKCF7Ca9iZI3fdanD90KYN98,9476
14
14
  stac_fastapi/sfeos_helpers/database/utils.py,sha256=CLtZgoUT37oklc9MsExXsxDviv4bzK-ZP7oxAOXS32Y,11780
15
15
  stac_fastapi/sfeos_helpers/filter/README.md,sha256=Rb5qHmDkI-7-o3I82Lb_zfmrviqUj958wef021xI6pQ,1955
16
16
  stac_fastapi/sfeos_helpers/filter/__init__.py,sha256=n3zL_MhEGOoxMz1KeijyK_UKiZ0MKPl90zHtYI5RAy8,1557
17
- stac_fastapi/sfeos_helpers/filter/client.py,sha256=QZP0Dm_T7SoMdR65IOjFmKBW7Rphr4z2xPgozZ93TPs,5339
17
+ stac_fastapi/sfeos_helpers/filter/client.py,sha256=_LX3mlW9MYhoMGxvyi7Eg-LJICElQXYGbzzhSPXYRtw,6517
18
18
  stac_fastapi/sfeos_helpers/filter/cql2.py,sha256=Cg9kRYD9CVkVSyRqOyB5oVXmlyteSn2bw88sqklGpUM,955
19
- stac_fastapi/sfeos_helpers/filter/transform.py,sha256=-hslJ1MUNssEnaMyTknVUMCbaTnR--yvkd4D2CmUEr4,5171
19
+ stac_fastapi/sfeos_helpers/filter/transform.py,sha256=wu6t7jbhgK9JIROQ5W82sAyCn6lHnBwuoQzb6o63luI,5725
20
20
  stac_fastapi/sfeos_helpers/models/patch.py,sha256=s5n85ktnH6M2SMqpqyItR8uLxliXmnSTg1WO0QLVsmI,3127
21
21
  stac_fastapi/sfeos_helpers/search_engine/__init__.py,sha256=Bi0cAtul3FuLjFceTPtEcaWNBfmUX5vKaqDvbSUAm0o,754
22
22
  stac_fastapi/sfeos_helpers/search_engine/base.py,sha256=9KOLW3NjW9PzWQzqLuhIjQU7FOHdDnB3ZNwDq469JZU,1400
@@ -26,9 +26,9 @@ stac_fastapi/sfeos_helpers/search_engine/inserters.py,sha256=o-I_4OowMJetMwRFPdq
26
26
  stac_fastapi/sfeos_helpers/search_engine/managers.py,sha256=nldomKmw8iQfOxeGZbBRGG_rWk-vB5Hy_cOjJ2e0ArE,6454
27
27
  stac_fastapi/sfeos_helpers/search_engine/selection/__init__.py,sha256=qKd4KzZkERwF_yhIeFcjAUnq5vQarr3CuXxE3SWmt6c,441
28
28
  stac_fastapi/sfeos_helpers/search_engine/selection/base.py,sha256=106c4FK50cgMmTpPJkWdgbExPkU2yIH4Wq684Ww-fYE,859
29
- stac_fastapi/sfeos_helpers/search_engine/selection/cache_manager.py,sha256=5yrgf9JA4mgRNMPDKih6xySF8mD724lEWnXhWud7m2c,4039
29
+ stac_fastapi/sfeos_helpers/search_engine/selection/cache_manager.py,sha256=jG5XYWocCfhMgopA0bknGdw6R6zZ1cjanlX2554TFTA,4039
30
30
  stac_fastapi/sfeos_helpers/search_engine/selection/factory.py,sha256=vbgNVCUW2lviePqzpgsPLxp6IEqcX3GHiahqN2oVObA,1305
31
31
  stac_fastapi/sfeos_helpers/search_engine/selection/selectors.py,sha256=q83nfCfNfLUqtkHpORwNHNRU9Pa-heeaDIPO0RlHb-8,4779
32
- sfeos_helpers-6.7.5.dist-info/METADATA,sha256=Uc87_remruIvFaSeGvZLSNhU-4RENX1f2E1DKK00ys8,3114
33
- sfeos_helpers-6.7.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
34
- sfeos_helpers-6.7.5.dist-info/RECORD,,
32
+ sfeos_helpers-6.8.0.dist-info/METADATA,sha256=muFxhUPRtjlWPHqaQgIIcCUpaSKfLGz-WYiz8RwXIpA,3114
33
+ sfeos_helpers-6.8.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
34
+ sfeos_helpers-6.8.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: hatchling 1.28.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -53,12 +53,12 @@ def return_date(
53
53
  if "/" in interval:
54
54
  parts = interval.split("/")
55
55
  result["gte"] = (
56
- parts[0] if parts[0] != ".." else MIN_DATE_NANOS.isoformat() + "Z"
56
+ parts[0] if parts[0] != ".." else MIN_DATE_NANOS.isoformat()
57
57
  )
58
58
  result["lte"] = (
59
59
  parts[1]
60
60
  if len(parts) > 1 and parts[1] != ".."
61
- else MAX_DATE_NANOS.isoformat() + "Z"
61
+ else MAX_DATE_NANOS.isoformat()
62
62
  )
63
63
  else:
64
64
  converted_time = interval if interval != ".." else None
@@ -75,7 +75,7 @@ def return_date(
75
75
  dt_utc = MIN_DATE_NANOS
76
76
  elif dt_utc > MAX_DATE_NANOS:
77
77
  dt_utc = MAX_DATE_NANOS
78
- datetime_iso = dt_utc.isoformat()
78
+ datetime_iso = dt_utc.isoformat().replace("+00:00", "Z")
79
79
  result["gte"] = result["lte"] = datetime_iso
80
80
  elif isinstance(interval, tuple):
81
81
  start, end = interval
@@ -90,7 +90,7 @@ def return_date(
90
90
  start_utc = MIN_DATE_NANOS
91
91
  elif start_utc > MAX_DATE_NANOS:
92
92
  start_utc = MAX_DATE_NANOS
93
- result["gte"] = start_utc.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
93
+ result["gte"] = start_utc.isoformat().replace("+00:00", "Z")
94
94
  if end:
95
95
  end_utc = (
96
96
  end.astimezone(timezone.utc)
@@ -101,7 +101,7 @@ def return_date(
101
101
  end_utc = MIN_DATE_NANOS
102
102
  elif end_utc > MAX_DATE_NANOS:
103
103
  end_utc = MAX_DATE_NANOS
104
- result["lte"] = end_utc.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
104
+ result["lte"] = end_utc.isoformat().replace("+00:00", "Z")
105
105
 
106
106
  return result
107
107
 
@@ -131,9 +131,9 @@ def return_date(
131
131
  start, end = interval
132
132
  # Ensure datetimes are converted to UTC and formatted with 'Z'
133
133
  if start:
134
- result["gte"] = start.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
134
+ result["gte"] = start.isoformat().replace("+00:00", "Z")
135
135
  if end:
136
- result["lte"] = end.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
136
+ result["lte"] = end.isoformat().replace("+00:00", "Z")
137
137
 
138
138
  return result
139
139
 
@@ -3,14 +3,62 @@
3
3
  This module provides functions for working with Elasticsearch/OpenSearch mappings.
4
4
  """
5
5
 
6
- from typing import Any, Dict
6
+ import os
7
+ from collections import deque
8
+ from typing import Any, Dict, Set
9
+
10
+
11
+ def _get_excluded_from_queryables() -> Set[str]:
12
+ """Get fields to exclude from queryables endpoint and filtering.
13
+
14
+ Reads from EXCLUDED_FROM_QUERYABLES environment variable.
15
+ Supports comma-separated list of field names.
16
+
17
+ For each exclusion pattern, both the original and the version with/without
18
+ 'properties.' prefix are included. This ensures fields are excluded regardless
19
+ of whether they appear at the top level or under 'properties' in the mapping.
20
+
21
+ Example:
22
+ EXCLUDED_FROM_QUERYABLES="properties.auth:schemes,storage:schemes"
23
+
24
+ This will exclude:
25
+ - properties.auth:schemes (and children like properties.auth:schemes.s3.type)
26
+ - auth:schemes (and children like auth:schemes.s3.type)
27
+ - storage:schemes (and children)
28
+ - properties.storage:schemes (and children)
29
+
30
+ Returns:
31
+ Set[str]: Set of field names to exclude from queryables
32
+ """
33
+ excluded = os.getenv("EXCLUDED_FROM_QUERYABLES", "")
34
+ if not excluded:
35
+ return set()
36
+
37
+ result = set()
38
+ for field in excluded.split(","):
39
+ field = field.strip()
40
+ if not field:
41
+ continue
42
+
43
+ result.add(field)
44
+
45
+ if field.startswith("properties."):
46
+ result.add(field.removeprefix("properties."))
47
+ else:
48
+ result.add(f"properties.{field}")
49
+
50
+ return result
7
51
 
8
52
 
9
53
  async def get_queryables_mapping_shared(
10
- mappings: Dict[str, Dict[str, Any]], collection_id: str = "*"
54
+ mappings: Dict[str, Dict[str, Any]],
55
+ collection_id: str = "*",
11
56
  ) -> Dict[str, str]:
12
57
  """Retrieve mapping of Queryables for search.
13
58
 
59
+ Fields listed in the EXCLUDED_FROM_QUERYABLES environment variable will be
60
+ excluded from the result, along with their children.
61
+
14
62
  Args:
15
63
  mappings (Dict[str, Dict[str, Any]]): The mapping information returned from
16
64
  Elasticsearch/OpenSearch client's indices.get_mapping() method.
@@ -20,19 +68,44 @@ async def get_queryables_mapping_shared(
20
68
 
21
69
  Returns:
22
70
  Dict[str, str]: A dictionary containing the Queryables mappings, where keys are
23
- field names and values are the corresponding paths in the Elasticsearch/OpenSearch
24
- document structure.
71
+ field names (with 'properties.' prefix removed) and values are the
72
+ corresponding paths in the Elasticsearch/OpenSearch document structure.
25
73
  """
26
74
  queryables_mapping = {}
75
+ excluded = _get_excluded_from_queryables()
76
+
77
+ def is_excluded(path: str) -> bool:
78
+ """Check if the path starts with any excluded prefix."""
79
+ return any(
80
+ path == prefix or path.startswith(prefix + ".") for prefix in excluded
81
+ )
27
82
 
28
83
  for mapping in mappings.values():
29
- fields = mapping["mappings"].get("properties", {})
30
- properties = fields.pop("properties", {}).get("properties", {}).keys()
84
+ mapping_properties = mapping["mappings"].get("properties", {})
85
+
86
+ stack: deque[tuple[str, Dict[str, Any]]] = deque(mapping_properties.items())
87
+
88
+ while stack:
89
+ field_fqn, field_def = stack.popleft()
90
+
91
+ nested_properties = field_def.get("properties")
92
+ if nested_properties:
93
+ stack.extend(
94
+ (f"{field_fqn}.{k}", v)
95
+ for k, v in nested_properties.items()
96
+ if v.get("enabled", True) and not is_excluded(f"{field_fqn}.{k}")
97
+ )
98
+
99
+ field_type = field_def.get("type")
100
+ if (
101
+ not field_type
102
+ or not field_def.get("enabled", True)
103
+ or is_excluded(field_fqn)
104
+ ):
105
+ continue
31
106
 
32
- for field_key in fields:
33
- queryables_mapping[field_key] = field_key
107
+ field_name = field_fqn.removeprefix("properties.")
34
108
 
35
- for property_key in properties:
36
- queryables_mapping[property_key] = f"properties.{property_key}"
109
+ queryables_mapping[field_name] = field_fqn
37
110
 
38
111
  return queryables_mapping
@@ -26,8 +26,12 @@ class EsAsyncBaseFiltersClient(AsyncBaseFiltersClient):
26
26
  Reads from EXCLUDED_FROM_QUERYABLES environment variable.
27
27
  Supports comma-separated list of field names.
28
28
 
29
+ For each exclusion pattern, both the original and the version with/without
30
+ 'properties.' prefix are included. This ensures fields are excluded regardless
31
+ of whether they appear at the top level or under 'properties' in the mapping.
32
+
29
33
  Example:
30
- EXCLUDED_FROM_QUERYABLES="auth:schemes,storage:schemes"
34
+ EXCLUDED_FROM_QUERYABLES="properties.auth:schemes,storage:schemes"
31
35
 
32
36
  Returns:
33
37
  Set[str]: Set of field names to exclude from queryables
@@ -35,7 +39,41 @@ class EsAsyncBaseFiltersClient(AsyncBaseFiltersClient):
35
39
  excluded = os.getenv("EXCLUDED_FROM_QUERYABLES", "")
36
40
  if not excluded:
37
41
  return set()
38
- return {field.strip() for field in excluded.split(",") if field.strip()}
42
+
43
+ result = set()
44
+ for field in excluded.split(","):
45
+ field = field.strip()
46
+ if not field:
47
+ continue
48
+
49
+ result.add(field)
50
+
51
+ if field.startswith("properties."):
52
+ result.add(field.removeprefix("properties."))
53
+ else:
54
+ result.add(f"properties.{field}")
55
+
56
+ return result
57
+
58
+ @staticmethod
59
+ def _is_excluded(field_fqn: str, excluded: set[str]) -> bool:
60
+ """Check if a field should be excluded based on prefix matching.
61
+
62
+ A field is excluded if:
63
+ - It exactly matches an exclusion pattern
64
+ - It starts with an exclusion pattern followed by a dot (nested child)
65
+
66
+ Args:
67
+ field_fqn: Fully qualified field name (e.g., "properties.auth:schemes.s3.type")
68
+ excluded: Set of exclusion patterns
69
+
70
+ Returns:
71
+ True if field should be excluded, False otherwise
72
+ """
73
+ for prefix in excluded:
74
+ if field_fqn == prefix or field_fqn.startswith(prefix + "."):
75
+ return True
76
+ return False
39
77
 
40
78
  async def get_queryables(
41
79
  self,
@@ -92,23 +130,20 @@ class EsAsyncBaseFiltersClient(AsyncBaseFiltersClient):
92
130
  while stack:
93
131
  field_fqn, field_def = stack.popleft()
94
132
 
95
- # Iterate over nested fields
133
+ if self._is_excluded(field_fqn, excluded_fields):
134
+ continue
135
+
96
136
  field_properties = field_def.get("properties")
97
137
  if field_properties:
98
138
  stack.extend(
99
139
  (f"{field_fqn}.{k}", v)
100
140
  for k, v in field_properties.items()
101
141
  if v.get("enabled", True)
102
- and f"{field_fqn}.{k}" not in excluded_fields
103
142
  )
104
143
 
105
144
  # Skip non-indexed or disabled fields
106
145
  field_type = field_def.get("type")
107
- if (
108
- not field_type
109
- or not field_def.get("enabled", True)
110
- or field_fqn in excluded_fields
111
- ):
146
+ if not field_type or not field_def.get("enabled", True):
112
147
  continue
113
148
 
114
149
  # Fields in Item Properties should be exposed with their un-prefixed names,
@@ -22,7 +22,20 @@ def to_es_field(queryables_mapping: Dict[str, Any], field: str) -> str:
22
22
  Returns:
23
23
  str: The mapped field name suitable for Elasticsearch queries.
24
24
  """
25
- return queryables_mapping.get(field, field)
25
+ # First, try to find the field as-is in the mapping
26
+ if field in queryables_mapping:
27
+ return queryables_mapping[field]
28
+
29
+ # If field has 'properties.' prefix, try without it
30
+ # This handles cases where users specify 'properties.eo:cloud_cover'
31
+ # but queryables_mapping uses 'eo:cloud_cover' as the key
32
+ if field.startswith("properties."):
33
+ normalized_field = field[11:] # len("properties.") == 11
34
+ if normalized_field in queryables_mapping:
35
+ return queryables_mapping[normalized_field]
36
+
37
+ # If not found, return the original field
38
+ return field
26
39
 
27
40
 
28
41
  def to_es(queryables_mapping: Dict[str, Any], query: Dict[str, Any]) -> Dict[str, Any]:
@@ -160,6 +160,7 @@ ES_COLLECTIONS_MAPPINGS = {
160
160
  "dynamic_templates": ES_MAPPINGS_DYNAMIC_TEMPLATES,
161
161
  "properties": {
162
162
  "id": {"type": "keyword"},
163
+ "parent_ids": {"type": "keyword"},
163
164
  "bbox_shape": {"type": "geo_shape"},
164
165
  "extent.temporal.interval": {
165
166
  "type": "date",
@@ -12,7 +12,7 @@ from stac_fastapi.sfeos_helpers.mappings import ITEMS_INDEX_PREFIX
12
12
  class IndexCacheManager:
13
13
  """Manages caching of index aliases with expiration."""
14
14
 
15
- def __init__(self, cache_ttl_seconds: int = 3600):
15
+ def __init__(self, cache_ttl_seconds: int = 1800):
16
16
  """Initialize the cache manager.
17
17
 
18
18
  Args:
@@ -1,2 +1,2 @@
1
1
  """library version."""
2
- __version__ = "6.7.5"
2
+ __version__ = "6.8.0"