stac-fastapi-core 6.8.1__py3-none-any.whl → 6.9.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.
stac_fastapi/core/core.py CHANGED
@@ -315,7 +315,7 @@ class CoreClient(AsyncBaseCoreClient):
315
315
 
316
316
  body_limit = None
317
317
  try:
318
- if request.method == "POST" and request.body():
318
+ if request.method == "POST" and await request.body():
319
319
  body_data = await request.json()
320
320
  body_limit = body_data.get("limit")
321
321
  except Exception:
@@ -56,6 +56,7 @@ class CatalogsExtension(ApiExtension):
56
56
  settings: extension settings (unused for now).
57
57
  """
58
58
  self.settings = settings or {}
59
+ self.router = APIRouter()
59
60
 
60
61
  self.router.add_api_route(
61
62
  path="/catalogs",
@@ -364,9 +365,9 @@ class CatalogsExtension(ApiExtension):
364
365
  await self.client.database.find_catalog(catalog_id)
365
366
 
366
367
  # Find all collections with this catalog in parent_ids
367
- query_body = {"query": {"term": {"parent_ids": catalog_id}}}
368
+ query_body = {"query": {"term": {"parent_ids": catalog_id}}, "size": 10000}
368
369
  search_result = await self.client.database.client.search(
369
- index=COLLECTIONS_INDEX, body=query_body, size=10000
370
+ index=COLLECTIONS_INDEX, body=query_body
370
371
  )
371
372
  children = [hit["_source"] for hit in search_result["hits"]["hits"]]
372
373
 
@@ -38,7 +38,7 @@ def build_get_collections_search_doc(original_endpoint):
38
38
  query: Optional[str] = Query(
39
39
  None,
40
40
  description="Additional filtering expressed as a string (legacy support)",
41
- example="platform=landsat AND collection_category=level2",
41
+ examples=["platform=landsat AND collection_category=level2"],
42
42
  ),
43
43
  limit: int = Query(
44
44
  10,
@@ -83,14 +83,16 @@ def build_get_collections_search_doc(original_endpoint):
83
83
  description=(
84
84
  "Structured filter expression in CQL2 JSON or CQL2-text format"
85
85
  ),
86
- example='{"op": "=", "args": [{"property": "properties.category"}, "level2"]}',
86
+ examples=[
87
+ '{"op": "=", "args": [{"property": "properties.category"}, "level2"]}'
88
+ ],
87
89
  ),
88
90
  filter_lang: Optional[str] = Query(
89
91
  None,
90
92
  description=(
91
93
  "Filter language. Must be 'cql2-json' or 'cql2-text' if specified"
92
94
  ),
93
- example="cql2-json",
95
+ examples=["cql2-json"],
94
96
  ),
95
97
  ):
96
98
  # Delegate to original endpoint with parameters
@@ -160,24 +162,26 @@ def build_post_collections_search_doc(original_post_endpoint):
160
162
  "- `sortby`: List of sort criteria objects with 'field' and 'direction' (asc/desc)\n"
161
163
  "- `fields`: Object with 'include' and 'exclude' arrays for field selection"
162
164
  ),
163
- example={
164
- "q": "landsat",
165
- "query": "platform=landsat AND collection_category=level2",
166
- "filter": {
167
- "op": "=",
168
- "args": [{"property": "properties.category"}, "level2"],
169
- },
170
- "filter_lang": "cql2-json",
171
- "limit": 10,
172
- "token": "next-page-token",
173
- "bbox": [-180, -90, 180, 90],
174
- "datetime": "2020-01-01T00:00:00Z/2021-01-01T12:31:12Z",
175
- "sortby": [{"field": "id", "direction": "asc"}],
176
- "fields": {
177
- "include": ["id", "title", "description"],
178
- "exclude": ["properties"],
179
- },
180
- },
165
+ examples=[
166
+ {
167
+ "q": "landsat",
168
+ "query": "platform=landsat AND collection_category=level2",
169
+ "filter": {
170
+ "op": "=",
171
+ "args": [{"property": "properties.category"}, "level2"],
172
+ },
173
+ "filter_lang": "cql2-json",
174
+ "limit": 10,
175
+ "token": "next-page-token",
176
+ "bbox": [-180, -90, 180, 90],
177
+ "datetime": "2020-01-01T00:00:00Z/2021-01-01T12:31:12Z",
178
+ "sortby": [{"field": "id", "direction": "asc"}],
179
+ "fields": {
180
+ "include": ["id", "title", "description"],
181
+ "exclude": ["properties"],
182
+ },
183
+ }
184
+ ],
181
185
  ),
182
186
  ) -> Union[Collections, Response]:
183
187
  return await original_post_endpoint(request, body)
@@ -292,4 +292,4 @@ async def redis_pagination_links(
292
292
  except Exception as e:
293
293
  logger.warning(f"Redis pagination operation failed: {e}")
294
294
  finally:
295
- await redis.close()
295
+ await redis.aclose() # type: ignore
@@ -6,6 +6,7 @@ such as converting bounding boxes to polygon representations.
6
6
 
7
7
  import logging
8
8
  import os
9
+ import re
9
10
  from typing import Any, Dict, List, Optional, Set, Union
10
11
 
11
12
  from stac_fastapi.types.stac import Item
@@ -70,8 +71,6 @@ def bbox2polygon(b0: float, b1: float, b2: float, b3: float) -> List[List[List[f
70
71
  return [[[b0, b1], [b2, b1], [b2, b3], [b0, b3], [b0, b1]]]
71
72
 
72
73
 
73
- # copied from stac-fastapi-pgstac
74
- # https://github.com/stac-utils/stac-fastapi-pgstac/blob/26f6d918eb933a90833f30e69e21ba3b4e8a7151/stac_fastapi/pgstac/utils.py#L10-L116
75
74
  def filter_fields( # noqa: C901
76
75
  item: Union[Item, Dict[str, Any]],
77
76
  include: Optional[Set[str]] = None,
@@ -87,15 +86,60 @@ def filter_fields( # noqa: C901
87
86
  if not include and not exclude:
88
87
  return item
89
88
 
90
- # Build a shallow copy of included fields on an item, or a sub-tree of an item
89
+ def match_pattern(pattern: str, key: str) -> bool:
90
+ """Check if a key matches a wildcard pattern."""
91
+ regex_pattern = "^" + re.escape(pattern).replace(r"\*", ".*") + "$"
92
+ return bool(re.match(regex_pattern, key))
93
+
94
+ def get_matching_keys(source: Dict[str, Any], pattern: str) -> List[str]:
95
+ """Get all keys that match the pattern."""
96
+ if not isinstance(source, dict):
97
+ return []
98
+ return [key for key in source.keys() if match_pattern(pattern, key)]
99
+
91
100
  def include_fields(
92
101
  source: Dict[str, Any], fields: Optional[Set[str]]
93
102
  ) -> Dict[str, Any]:
103
+ """Include only the specified fields from the source dictionary."""
94
104
  if not fields:
95
105
  return source
96
106
 
107
+ def recursive_include(
108
+ source: Dict[str, Any], path_parts: List[str]
109
+ ) -> Dict[str, Any]:
110
+ """Recursively include fields matching the pattern path."""
111
+ if not path_parts:
112
+ return source
113
+
114
+ if not isinstance(source, dict):
115
+ return {}
116
+
117
+ current_pattern = path_parts[0]
118
+ remaining_parts = path_parts[1:]
119
+
120
+ matching_keys = get_matching_keys(source, current_pattern)
121
+
122
+ if not matching_keys:
123
+ return {}
124
+
125
+ result: Dict[str, Any] = {}
126
+ for key in matching_keys:
127
+ if remaining_parts:
128
+ if isinstance(source[key], dict):
129
+ value = recursive_include(source[key], remaining_parts)
130
+ if value:
131
+ result[key] = value
132
+ else:
133
+ result[key] = source[key]
134
+
135
+ return result
136
+
97
137
  clean_item: Dict[str, Any] = {}
98
138
  for key_path in fields or []:
139
+ if "*" in key_path:
140
+ value = recursive_include(source, key_path.split("."))
141
+ dict_deep_update(clean_item, value)
142
+ continue
99
143
  key_path_parts = key_path.split(".")
100
144
  key_root = key_path_parts[0]
101
145
  if key_root in source:
@@ -125,12 +169,46 @@ def filter_fields( # noqa: C901
125
169
  # The key, or root key of a multi-part key, is not present in the item,
126
170
  # so it is ignored
127
171
  pass
172
+
128
173
  return clean_item
129
174
 
130
- # For an item built up for included fields, remove excluded fields. This
131
- # modifies `source` in place.
132
- def exclude_fields(source: Dict[str, Any], fields: Optional[Set[str]]) -> None:
175
+ def exclude_fields(
176
+ source: Dict[str, Any],
177
+ fields: Optional[Set[str]],
178
+ ) -> None:
179
+ """Exclude fields from source."""
180
+
181
+ def recursive_exclude(
182
+ source: Dict[str, Any], path_parts: List[str], current_path: str = ""
183
+ ) -> None:
184
+ """Recursively exclude fields matching the pattern path."""
185
+ if not path_parts or not isinstance(source, dict):
186
+ return
187
+
188
+ current_pattern = path_parts[0]
189
+ remaining_parts = path_parts[1:]
190
+
191
+ matching_keys = get_matching_keys(source, current_pattern)
192
+
193
+ for key in list(matching_keys):
194
+ if key not in source:
195
+ continue
196
+
197
+ # Build the full path for this key
198
+ full_path = f"{current_path}.{key}" if current_path else key
199
+
200
+ if remaining_parts:
201
+ if isinstance(source[key], dict):
202
+ recursive_exclude(source[key], remaining_parts, full_path)
203
+ if not source[key]:
204
+ del source[key]
205
+ else:
206
+ source.pop(key, None)
207
+
133
208
  for key_path in fields or []:
209
+ if "*" in key_path:
210
+ recursive_exclude(source, key_path.split("."))
211
+ continue
134
212
  key_path_part = key_path.split(".")
135
213
  key_root = key_path_part[0]
136
214
  if key_root in source:
@@ -1,2 +1,2 @@
1
1
  """library version."""
2
- __version__ = "6.8.1"
2
+ __version__ = "6.9.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stac_fastapi_core
3
- Version: 6.8.1
3
+ Version: 6.9.0
4
4
  Summary: Core 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
@@ -2,26 +2,26 @@ stac_fastapi/core/__init__.py,sha256=8izV3IWRGdXmDOK1hIPQAanbWs9EI04PJCGgqG1ZGIs
2
2
  stac_fastapi/core/base_database_logic.py,sha256=JL7DRcDdqeaLbSPPGcIUMs7q6I3Gm_E5XCOwFG458Io,6053
3
3
  stac_fastapi/core/base_settings.py,sha256=R3_Sx7n5XpGMs3zAwFJD7y008WvGU_uI2xkaabm82Kg,239
4
4
  stac_fastapi/core/basic_auth.py,sha256=RhFv3RVSHF6OaqnaaU2DO4ncJ_S5nB1q8UNpnVJJsrk,2155
5
- stac_fastapi/core/core.py,sha256=QvwZbXi5PssZXBXxyggLsYHfvzj57NidloI4uVXenzM,52238
5
+ stac_fastapi/core/core.py,sha256=LTUF1253Bq-E9G9oaK3cPgou7Mk4iemHkS7hDVgwSow,52244
6
6
  stac_fastapi/core/datetime_utils.py,sha256=QygF2mJFfI_zqCwmSIec3HYqrsVsn3nUcaRQx3CD7Zw,4683
7
7
  stac_fastapi/core/queryables.py,sha256=0gKdxlmCVaIj3ODpmyIfzLChEB1nNKXPZhA3K9ApfL0,3755
8
8
  stac_fastapi/core/rate_limit.py,sha256=Gu8dAaJReGsj1L91U6m2tflU6RahpXDRs2-AYSKoybA,1318
9
- stac_fastapi/core/redis_utils.py,sha256=6_lrXfZBi6vCCCibLDdwwHC3lLaXYTEmqQpxOMaCUH4,9689
9
+ stac_fastapi/core/redis_utils.py,sha256=xqZfXwrZ0Wei6EDereOygW_Aq6DBDykhQYD86Ws9P28,9706
10
10
  stac_fastapi/core/route_dependencies.py,sha256=hdtuMkv-zY1vg0YxiCz1aKP0SbBcORqDGEKDGgEazW8,5482
11
11
  stac_fastapi/core/serializers.py,sha256=l5EWZvlGjlfsZ3S4wHjWKD8sJBf83zd2dkursu18fV4,13345
12
12
  stac_fastapi/core/session.py,sha256=aXqu4LXfVbAAsChMVXd9gAhczA2bZPne6HqPeklAwMY,474
13
- stac_fastapi/core/utilities.py,sha256=XR_9afK_j8wCydgoXj-CMtRyI8KqgIL3d4HZOE779dU,7807
14
- stac_fastapi/core/version.py,sha256=mmv17WIhpgYJwiq4ovHUKIHbPTRN4ViHSjE63rArov0,45
13
+ stac_fastapi/core/utilities.py,sha256=B-tLc_H_v92q8ZNpzk-9nKQMKe-bVHUk64HpybGqYX0,10398
14
+ stac_fastapi/core/version.py,sha256=Zec8murh7xqMR7l6fB74XJcq2QdyNqRgOdhLMwBYPAI,45
15
15
  stac_fastapi/core/extensions/__init__.py,sha256=oaK-UJDQSEISdQ8VtM0ESxpsv7Hx1HbAdmMnh6MTFD4,356
16
16
  stac_fastapi/core/extensions/aggregation.py,sha256=v1hUHqlYuMqfQ554g3cTp16pUyRYucQxPERbHPAFtf8,1878
17
- stac_fastapi/core/extensions/catalogs.py,sha256=WwUI4Q20zbD5uN2le12uGnGPmQsoQnpD7bcJ__bi02Y,37963
18
- stac_fastapi/core/extensions/collections_search.py,sha256=xpv51nffMq5a8grNSaLbv2IzeI5JH_pqcoWRbWhzn6Y,14406
17
+ stac_fastapi/core/extensions/catalogs.py,sha256=iOMmPdY0vhGx9QkIMEaLbEaCLvGY2iyg6U07Gdpjejc,38000
18
+ stac_fastapi/core/extensions/collections_search.py,sha256=ZxCRQ5jyhrg6zDs037qLDzCz0T3tq3a9_lXfYQ9GweQ,14546
19
19
  stac_fastapi/core/extensions/fields.py,sha256=NCT5XHvfaf297eDPNaIFsIzvJnbbUTpScqF0otdx0NA,1066
20
20
  stac_fastapi/core/extensions/filter.py,sha256=-NQGME7rR_ereuDx-LAa1M5JhEXFaKiTtkH2asraYHE,2998
21
21
  stac_fastapi/core/extensions/query.py,sha256=Xmo8pfZEZKPudZEjjozv3R0wLOP0ayjC9E67sBOXqWY,1803
22
22
  stac_fastapi/core/models/__init__.py,sha256=sUsEB7umGZVYXjT4EHqLwm8p2wevtRBdig2Ioj2ZdVQ,631
23
23
  stac_fastapi/core/models/links.py,sha256=5KEZKisFN34U4UuOzSQnDy0QdsUOT2VRuuY36vs-FGw,7074
24
24
  stac_fastapi/core/models/search.py,sha256=7SgAUyzHGXBXSqB4G6cwq9FMwoAS00momb7jvBkjyow,27
25
- stac_fastapi_core-6.8.1.dist-info/METADATA,sha256=GB6GOulxa_ja31Bf1iRvzhCsGDPOVz44zBun5xuL_RE,3480
26
- stac_fastapi_core-6.8.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
27
- stac_fastapi_core-6.8.1.dist-info/RECORD,,
25
+ stac_fastapi_core-6.9.0.dist-info/METADATA,sha256=4ot6ylGlLz67tpQZ5VPxAbw2kOC1O_VCAzGXZ2dQ39U,3480
26
+ stac_fastapi_core-6.9.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
27
+ stac_fastapi_core-6.9.0.dist-info/RECORD,,