stac-fastapi-core 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.
- stac_fastapi/core/base_database_logic.py +14 -2
- stac_fastapi/core/core.py +102 -48
- stac_fastapi/core/extensions/collections_search.py +200 -10
- stac_fastapi/core/redis_utils.py +301 -0
- stac_fastapi/core/serializers.py +6 -0
- stac_fastapi/core/utilities.py +1 -9
- stac_fastapi/core/version.py +1 -1
- stac_fastapi_core-6.7.0.dist-info/METADATA +66 -0
- {stac_fastapi_core-6.5.1.dist-info → stac_fastapi_core-6.7.0.dist-info}/RECORD +10 -10
- {stac_fastapi_core-6.5.1.dist-info → stac_fastapi_core-6.7.0.dist-info}/WHEEL +1 -2
- stac_fastapi_core-6.5.1.dist-info/METADATA +0 -726
- stac_fastapi_core-6.5.1.dist-info/top_level.txt +0 -1
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import abc
|
|
4
4
|
from typing import Any, Dict, Iterable, List, Optional, Tuple
|
|
5
5
|
|
|
6
|
+
from stac_pydantic.shared import BBox
|
|
7
|
+
|
|
6
8
|
|
|
7
9
|
class BaseDatabaseLogic(abc.ABC):
|
|
8
10
|
"""
|
|
@@ -19,7 +21,12 @@ class BaseDatabaseLogic(abc.ABC):
|
|
|
19
21
|
limit: int,
|
|
20
22
|
request: Any = None,
|
|
21
23
|
sort: Optional[List[Dict[str, Any]]] = None,
|
|
22
|
-
|
|
24
|
+
bbox: Optional[BBox] = None,
|
|
25
|
+
q: Optional[List[str]] = None,
|
|
26
|
+
filter: Optional[Dict[str, Any]] = None,
|
|
27
|
+
query: Optional[Dict[str, Dict[str, Any]]] = None,
|
|
28
|
+
datetime: Optional[str] = None,
|
|
29
|
+
) -> Tuple[List[Dict[str, Any]], Optional[str], Optional[int]]:
|
|
23
30
|
"""Retrieve a list of collections from the database, supporting pagination.
|
|
24
31
|
|
|
25
32
|
Args:
|
|
@@ -27,9 +34,14 @@ class BaseDatabaseLogic(abc.ABC):
|
|
|
27
34
|
limit (int): The number of results to return.
|
|
28
35
|
request (Any, optional): The FastAPI request object. Defaults to None.
|
|
29
36
|
sort (Optional[List[Dict[str, Any]]], optional): Optional sort parameter. Defaults to None.
|
|
37
|
+
bbox (Optional[BBox], optional): Bounding box to filter collections by spatial extent. Defaults to None.
|
|
38
|
+
q (Optional[List[str]], optional): Free text search terms. Defaults to None.
|
|
39
|
+
filter (Optional[Dict[str, Any]], optional): Structured query in CQL2 format. Defaults to None.
|
|
40
|
+
query (Optional[Dict[str, Dict[str, Any]]], optional): Query extension parameters. Defaults to None.
|
|
41
|
+
datetime (Optional[str], optional): Temporal filter. Defaults to None.
|
|
30
42
|
|
|
31
43
|
Returns:
|
|
32
|
-
A tuple of (collections, next pagination token if any).
|
|
44
|
+
A tuple of (collections, next pagination token if any, optional count).
|
|
33
45
|
"""
|
|
34
46
|
pass
|
|
35
47
|
|
stac_fastapi/core/core.py
CHANGED
|
@@ -24,9 +24,10 @@ from stac_fastapi.core.base_database_logic import BaseDatabaseLogic
|
|
|
24
24
|
from stac_fastapi.core.base_settings import ApiBaseSettings
|
|
25
25
|
from stac_fastapi.core.datetime_utils import format_datetime_range
|
|
26
26
|
from stac_fastapi.core.models.links import PagingLinks
|
|
27
|
+
from stac_fastapi.core.redis_utils import redis_pagination_links
|
|
27
28
|
from stac_fastapi.core.serializers import CollectionSerializer, ItemSerializer
|
|
28
29
|
from stac_fastapi.core.session import Session
|
|
29
|
-
from stac_fastapi.core.utilities import filter_fields
|
|
30
|
+
from stac_fastapi.core.utilities import filter_fields, get_bool_env
|
|
30
31
|
from stac_fastapi.extensions.core.transaction import AsyncBaseTransactionsClient
|
|
31
32
|
from stac_fastapi.extensions.core.transaction.request import (
|
|
32
33
|
PartialCollection,
|
|
@@ -201,17 +202,6 @@ class CoreClient(AsyncBaseCoreClient):
|
|
|
201
202
|
]
|
|
202
203
|
)
|
|
203
204
|
|
|
204
|
-
collections = await self.all_collections(request=kwargs["request"])
|
|
205
|
-
for collection in collections["collections"]:
|
|
206
|
-
landing_page["links"].append(
|
|
207
|
-
{
|
|
208
|
-
"rel": Relations.child.value,
|
|
209
|
-
"type": MimeTypes.json.value,
|
|
210
|
-
"title": collection.get("title") or collection.get("id"),
|
|
211
|
-
"href": urljoin(base_url, f"collections/{collection['id']}"),
|
|
212
|
-
}
|
|
213
|
-
)
|
|
214
|
-
|
|
215
205
|
# Add OpenAPI URL
|
|
216
206
|
landing_page["links"].append(
|
|
217
207
|
{
|
|
@@ -241,6 +231,7 @@ class CoreClient(AsyncBaseCoreClient):
|
|
|
241
231
|
async def all_collections(
|
|
242
232
|
self,
|
|
243
233
|
limit: Optional[int] = None,
|
|
234
|
+
bbox: Optional[BBox] = None,
|
|
244
235
|
datetime: Optional[str] = None,
|
|
245
236
|
fields: Optional[List[str]] = None,
|
|
246
237
|
sortby: Optional[Union[str, List[str]]] = None,
|
|
@@ -255,49 +246,50 @@ class CoreClient(AsyncBaseCoreClient):
|
|
|
255
246
|
"""Read all collections from the database.
|
|
256
247
|
|
|
257
248
|
Args:
|
|
258
|
-
datetime (Optional[str]): Filter collections by datetime range.
|
|
259
249
|
limit (Optional[int]): Maximum number of collections to return.
|
|
250
|
+
bbox (Optional[BBox]): Bounding box to filter collections by spatial extent.
|
|
251
|
+
datetime (Optional[str]): Filter collections by datetime range.
|
|
260
252
|
fields (Optional[List[str]]): Fields to include or exclude from the results.
|
|
261
|
-
sortby (Optional[str]): Sorting options for the results.
|
|
253
|
+
sortby (Optional[Union[str, List[str]]]): Sorting options for the results.
|
|
262
254
|
filter_expr (Optional[str]): Structured filter expression in CQL2 JSON or CQL2-text format.
|
|
263
|
-
query (Optional[str]): Legacy query parameter (deprecated).
|
|
264
255
|
filter_lang (Optional[str]): Must be 'cql2-json' or 'cql2-text' if specified, other values will result in an error.
|
|
265
256
|
q (Optional[Union[str, List[str]]]): Free text search terms.
|
|
257
|
+
query (Optional[str]): Legacy query parameter (deprecated).
|
|
258
|
+
request (Request): FastAPI Request object.
|
|
259
|
+
token (Optional[str]): Pagination token for retrieving the next page of results.
|
|
266
260
|
**kwargs: Keyword arguments from the request.
|
|
267
261
|
|
|
268
262
|
Returns:
|
|
269
263
|
A Collections object containing all the collections in the database and links to various resources.
|
|
270
264
|
"""
|
|
271
265
|
base_url = str(request.base_url)
|
|
266
|
+
redis_enable = get_bool_env("REDIS_ENABLE", default=False)
|
|
272
267
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
268
|
+
global_max_limit = (
|
|
269
|
+
int(os.getenv("STAC_GLOBAL_COLLECTION_MAX_LIMIT"))
|
|
270
|
+
if os.getenv("STAC_GLOBAL_COLLECTION_MAX_LIMIT")
|
|
271
|
+
else None
|
|
272
|
+
)
|
|
273
|
+
query_limit = request.query_params.get("limit")
|
|
274
|
+
default_limit = int(os.getenv("STAC_DEFAULT_COLLECTION_LIMIT", 300))
|
|
275
|
+
|
|
276
|
+
body_limit = None
|
|
277
|
+
try:
|
|
278
|
+
if request.method == "POST" and request.body():
|
|
279
|
+
body_data = await request.json()
|
|
280
|
+
body_limit = body_data.get("limit")
|
|
281
|
+
except Exception:
|
|
282
|
+
pass
|
|
283
|
+
|
|
284
|
+
if body_limit is not None:
|
|
285
|
+
limit = int(body_limit)
|
|
286
|
+
elif query_limit:
|
|
287
|
+
limit = int(query_limit)
|
|
290
288
|
else:
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
try:
|
|
296
|
-
limit = int(query_limit)
|
|
297
|
-
except ValueError:
|
|
298
|
-
limit = 10
|
|
299
|
-
else:
|
|
300
|
-
limit = 10
|
|
289
|
+
limit = default_limit
|
|
290
|
+
|
|
291
|
+
if global_max_limit is not None:
|
|
292
|
+
limit = min(limit, global_max_limit)
|
|
301
293
|
|
|
302
294
|
# Get token from query params only if not already provided (for GET requests)
|
|
303
295
|
if token is None:
|
|
@@ -401,6 +393,7 @@ class CoreClient(AsyncBaseCoreClient):
|
|
|
401
393
|
limit=limit,
|
|
402
394
|
request=request,
|
|
403
395
|
sort=sort,
|
|
396
|
+
bbox=bbox,
|
|
404
397
|
q=q_list,
|
|
405
398
|
filter=parsed_filter,
|
|
406
399
|
query=parsed_query,
|
|
@@ -426,6 +419,14 @@ class CoreClient(AsyncBaseCoreClient):
|
|
|
426
419
|
},
|
|
427
420
|
]
|
|
428
421
|
|
|
422
|
+
if redis_enable:
|
|
423
|
+
await redis_pagination_links(
|
|
424
|
+
current_url=str(request.url),
|
|
425
|
+
token=token,
|
|
426
|
+
next_token=next_token,
|
|
427
|
+
links=links,
|
|
428
|
+
)
|
|
429
|
+
|
|
429
430
|
if next_token:
|
|
430
431
|
next_link = PagingLinks(next=next_token, request=request).link_next()
|
|
431
432
|
links.append(next_link)
|
|
@@ -502,6 +503,7 @@ class CoreClient(AsyncBaseCoreClient):
|
|
|
502
503
|
# Pass all parameters from search_request to all_collections
|
|
503
504
|
return await self.all_collections(
|
|
504
505
|
limit=search_request.limit if hasattr(search_request, "limit") else None,
|
|
506
|
+
bbox=search_request.bbox if hasattr(search_request, "bbox") else None,
|
|
505
507
|
datetime=search_request.datetime
|
|
506
508
|
if hasattr(search_request, "datetime")
|
|
507
509
|
else None,
|
|
@@ -569,7 +571,7 @@ class CoreClient(AsyncBaseCoreClient):
|
|
|
569
571
|
request (Request): FastAPI Request object.
|
|
570
572
|
bbox (Optional[BBox]): Optional bounding box filter.
|
|
571
573
|
datetime (Optional[str]): Optional datetime or interval filter.
|
|
572
|
-
limit (Optional[int]): Optional page size. Defaults to env
|
|
574
|
+
limit (Optional[int]): Optional page size. Defaults to env `STAC_DEFAULT_ITEM_LIMIT` when unset.
|
|
573
575
|
sortby (Optional[str]): Optional sort specification. Accepts repeated values
|
|
574
576
|
like ``sortby=-properties.datetime`` or ``sortby=+id``. Bare fields (e.g. ``sortby=id``)
|
|
575
577
|
imply ascending order.
|
|
@@ -660,15 +662,12 @@ class CoreClient(AsyncBaseCoreClient):
|
|
|
660
662
|
q (Optional[List[str]]): Free text query to filter the results.
|
|
661
663
|
intersects (Optional[str]): GeoJSON geometry to search in.
|
|
662
664
|
kwargs: Additional parameters to be passed to the API.
|
|
663
|
-
|
|
664
665
|
Returns:
|
|
665
666
|
ItemCollection: Collection of `Item` objects representing the search results.
|
|
666
667
|
|
|
667
668
|
Raises:
|
|
668
669
|
HTTPException: If any error occurs while searching the catalog.
|
|
669
670
|
"""
|
|
670
|
-
limit = int(request.query_params.get("limit", os.getenv("STAC_ITEM_LIMIT", 10)))
|
|
671
|
-
|
|
672
671
|
base_args = {
|
|
673
672
|
"collections": collections,
|
|
674
673
|
"ids": ids,
|
|
@@ -743,9 +742,37 @@ class CoreClient(AsyncBaseCoreClient):
|
|
|
743
742
|
Raises:
|
|
744
743
|
HTTPException: If there is an error with the cql2_json filter.
|
|
745
744
|
"""
|
|
746
|
-
|
|
745
|
+
global_max_limit = (
|
|
746
|
+
int(os.getenv("STAC_GLOBAL_ITEM_MAX_LIMIT"))
|
|
747
|
+
if os.getenv("STAC_GLOBAL_ITEM_MAX_LIMIT")
|
|
748
|
+
else None
|
|
749
|
+
)
|
|
750
|
+
query_limit = request.query_params.get("limit")
|
|
751
|
+
default_limit = int(os.getenv("STAC_DEFAULT_ITEM_LIMIT", 10))
|
|
747
752
|
|
|
753
|
+
body_limit = None
|
|
754
|
+
try:
|
|
755
|
+
if request.method == "POST" and request.body():
|
|
756
|
+
body_data = await request.json()
|
|
757
|
+
body_limit = body_data.get("limit")
|
|
758
|
+
except Exception:
|
|
759
|
+
pass
|
|
760
|
+
|
|
761
|
+
if body_limit is not None:
|
|
762
|
+
limit = int(body_limit)
|
|
763
|
+
elif query_limit:
|
|
764
|
+
limit = int(query_limit)
|
|
765
|
+
else:
|
|
766
|
+
limit = default_limit
|
|
767
|
+
|
|
768
|
+
if global_max_limit:
|
|
769
|
+
limit = min(limit, global_max_limit)
|
|
770
|
+
|
|
771
|
+
search_request.limit = limit
|
|
772
|
+
|
|
773
|
+
base_url = str(request.base_url)
|
|
748
774
|
search = self.database.make_search()
|
|
775
|
+
redis_enable = get_bool_env("REDIS_ENABLE", default=False)
|
|
749
776
|
|
|
750
777
|
if search_request.ids:
|
|
751
778
|
search = self.database.apply_ids_filter(
|
|
@@ -819,7 +846,6 @@ class CoreClient(AsyncBaseCoreClient):
|
|
|
819
846
|
if hasattr(search_request, "sortby") and getattr(search_request, "sortby"):
|
|
820
847
|
sort = self.database.populate_sort(getattr(search_request, "sortby"))
|
|
821
848
|
|
|
822
|
-
limit = 10
|
|
823
849
|
if search_request.limit:
|
|
824
850
|
limit = search_request.limit
|
|
825
851
|
|
|
@@ -850,6 +876,34 @@ class CoreClient(AsyncBaseCoreClient):
|
|
|
850
876
|
]
|
|
851
877
|
links = await PagingLinks(request=request, next=next_token).get_links()
|
|
852
878
|
|
|
879
|
+
collection_links = []
|
|
880
|
+
# Add "collection" and "parent" rels only for /collections/{collection_id}/items
|
|
881
|
+
if search_request.collections and "/items" in str(request.url):
|
|
882
|
+
for collection_id in search_request.collections:
|
|
883
|
+
collection_links.extend(
|
|
884
|
+
[
|
|
885
|
+
{
|
|
886
|
+
"rel": "collection",
|
|
887
|
+
"type": "application/json",
|
|
888
|
+
"href": urljoin(base_url, f"collections/{collection_id}"),
|
|
889
|
+
},
|
|
890
|
+
{
|
|
891
|
+
"rel": "parent",
|
|
892
|
+
"type": "application/json",
|
|
893
|
+
"href": urljoin(base_url, f"collections/{collection_id}"),
|
|
894
|
+
},
|
|
895
|
+
]
|
|
896
|
+
)
|
|
897
|
+
links.extend(collection_links)
|
|
898
|
+
|
|
899
|
+
if redis_enable:
|
|
900
|
+
await redis_pagination_links(
|
|
901
|
+
current_url=str(request.url),
|
|
902
|
+
token=token_param,
|
|
903
|
+
next_token=next_token,
|
|
904
|
+
links=links,
|
|
905
|
+
)
|
|
906
|
+
|
|
853
907
|
return stac_types.ItemCollection(
|
|
854
908
|
type="FeatureCollection",
|
|
855
909
|
features=items,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""Collections search extension."""
|
|
2
2
|
|
|
3
|
-
from typing import List, Optional, Type, Union
|
|
3
|
+
from typing import Any, Dict, List, Optional, Type, Union
|
|
4
4
|
|
|
5
|
-
from fastapi import APIRouter, FastAPI, Request
|
|
5
|
+
from fastapi import APIRouter, Body, FastAPI, Query, Request
|
|
6
6
|
from fastapi.responses import JSONResponse
|
|
7
7
|
from pydantic import BaseModel
|
|
8
8
|
from stac_pydantic.api.search import ExtendedSearch
|
|
@@ -22,6 +22,168 @@ class CollectionsSearchRequest(ExtendedSearch):
|
|
|
22
22
|
query: Optional[
|
|
23
23
|
str
|
|
24
24
|
] = None # Legacy query extension (deprecated but still supported)
|
|
25
|
+
filter_expr: Optional[str] = None
|
|
26
|
+
filter_lang: Optional[str] = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def build_get_collections_search_doc(original_endpoint):
|
|
30
|
+
"""Return a documented GET endpoint wrapper for /collections-search."""
|
|
31
|
+
|
|
32
|
+
async def documented_endpoint(
|
|
33
|
+
request: Request,
|
|
34
|
+
q: Optional[Union[str, List[str]]] = Query(
|
|
35
|
+
None,
|
|
36
|
+
description="Free text search query",
|
|
37
|
+
),
|
|
38
|
+
query: Optional[str] = Query(
|
|
39
|
+
None,
|
|
40
|
+
description="Additional filtering expressed as a string (legacy support)",
|
|
41
|
+
example="platform=landsat AND collection_category=level2",
|
|
42
|
+
),
|
|
43
|
+
limit: int = Query(
|
|
44
|
+
10,
|
|
45
|
+
ge=1,
|
|
46
|
+
description=(
|
|
47
|
+
"The maximum number of collections to return (page size). Defaults to 10."
|
|
48
|
+
),
|
|
49
|
+
),
|
|
50
|
+
token: Optional[str] = Query(
|
|
51
|
+
None,
|
|
52
|
+
description="Pagination token for the next page of results",
|
|
53
|
+
),
|
|
54
|
+
bbox: Optional[str] = Query(
|
|
55
|
+
None,
|
|
56
|
+
description=(
|
|
57
|
+
"Bounding box for spatial filtering in format 'minx,miny,maxx,maxy' "
|
|
58
|
+
"or 'minx,miny,minz,maxx,maxy,maxz'"
|
|
59
|
+
),
|
|
60
|
+
),
|
|
61
|
+
datetime: Optional[str] = Query(
|
|
62
|
+
None,
|
|
63
|
+
description=(
|
|
64
|
+
"Temporal filter in ISO 8601 format (e.g., "
|
|
65
|
+
"'2020-01-01T00:00:00Z/2021-01-01T00:00:00Z')"
|
|
66
|
+
),
|
|
67
|
+
),
|
|
68
|
+
sortby: Optional[str] = Query(
|
|
69
|
+
None,
|
|
70
|
+
description=(
|
|
71
|
+
"Sorting criteria in the format 'field' or '-field' for descending order"
|
|
72
|
+
),
|
|
73
|
+
),
|
|
74
|
+
fields: Optional[List[str]] = Query(
|
|
75
|
+
None,
|
|
76
|
+
description=(
|
|
77
|
+
"Comma-separated list of fields to include or exclude (use -field to exclude)"
|
|
78
|
+
),
|
|
79
|
+
alias="fields[]",
|
|
80
|
+
),
|
|
81
|
+
filter: Optional[str] = Query(
|
|
82
|
+
None,
|
|
83
|
+
description=(
|
|
84
|
+
"Structured filter expression in CQL2 JSON or CQL2-text format"
|
|
85
|
+
),
|
|
86
|
+
example='{"op": "=", "args": [{"property": "properties.category"}, "level2"]}',
|
|
87
|
+
),
|
|
88
|
+
filter_lang: Optional[str] = Query(
|
|
89
|
+
None,
|
|
90
|
+
description=(
|
|
91
|
+
"Filter language. Must be 'cql2-json' or 'cql2-text' if specified"
|
|
92
|
+
),
|
|
93
|
+
example="cql2-json",
|
|
94
|
+
),
|
|
95
|
+
):
|
|
96
|
+
# Delegate to original endpoint with parameters
|
|
97
|
+
# Since FastAPI extracts parameters from the URL when they're defined as function parameters,
|
|
98
|
+
# we need to create a request wrapper that provides our modified query_params
|
|
99
|
+
|
|
100
|
+
# Create a mutable copy of query_params
|
|
101
|
+
if hasattr(request, "_query_params"):
|
|
102
|
+
query_params = dict(request._query_params)
|
|
103
|
+
else:
|
|
104
|
+
query_params = dict(request.query_params)
|
|
105
|
+
|
|
106
|
+
# Add q parameter back to query_params if it was provided
|
|
107
|
+
# Convert to list format to match /collections behavior
|
|
108
|
+
if q is not None:
|
|
109
|
+
if isinstance(q, str):
|
|
110
|
+
# Single string should become a list with one element
|
|
111
|
+
query_params["q"] = [q]
|
|
112
|
+
elif isinstance(q, list):
|
|
113
|
+
# Already a list, use as-is
|
|
114
|
+
query_params["q"] = q
|
|
115
|
+
|
|
116
|
+
# Add filter parameters back to query_params if they were provided
|
|
117
|
+
if filter is not None:
|
|
118
|
+
query_params["filter"] = filter
|
|
119
|
+
if filter_lang is not None:
|
|
120
|
+
query_params["filter-lang"] = filter_lang
|
|
121
|
+
|
|
122
|
+
# Create a request wrapper that provides our modified query_params
|
|
123
|
+
class RequestWrapper:
|
|
124
|
+
def __init__(self, original_request, modified_query_params):
|
|
125
|
+
self._original = original_request
|
|
126
|
+
self._query_params = modified_query_params
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def query_params(self):
|
|
130
|
+
return self._query_params
|
|
131
|
+
|
|
132
|
+
def __getattr__(self, name):
|
|
133
|
+
# Delegate all other attributes to the original request
|
|
134
|
+
return getattr(self._original, name)
|
|
135
|
+
|
|
136
|
+
wrapped_request = RequestWrapper(request, query_params)
|
|
137
|
+
return await original_endpoint(wrapped_request)
|
|
138
|
+
|
|
139
|
+
documented_endpoint.__name__ = original_endpoint.__name__
|
|
140
|
+
return documented_endpoint
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def build_post_collections_search_doc(original_post_endpoint):
|
|
144
|
+
"""Return a documented POST endpoint wrapper for /collections-search."""
|
|
145
|
+
|
|
146
|
+
async def documented_post_endpoint(
|
|
147
|
+
request: Request,
|
|
148
|
+
body: Dict[str, Any] = Body(
|
|
149
|
+
...,
|
|
150
|
+
description=(
|
|
151
|
+
"Search parameters for collections.\n\n"
|
|
152
|
+
"- `q`: Free text search query (string or list of strings)\n"
|
|
153
|
+
"- `query`: Additional filtering expressed as a string (legacy support)\n"
|
|
154
|
+
"- `filter`: Structured filter expression in CQL2 JSON or CQL2-text format\n"
|
|
155
|
+
"- `filter_lang`: Filter language. Must be 'cql2-json' or 'cql2-text' if specified\n"
|
|
156
|
+
"- `limit`: Maximum number of results to return (default: 10)\n"
|
|
157
|
+
"- `token`: Pagination token for the next page of results\n"
|
|
158
|
+
"- `bbox`: Bounding box [minx, miny, maxx, maxy] or [minx, miny, minz, maxx, maxy, maxz]\n"
|
|
159
|
+
"- `datetime`: Temporal filter in ISO 8601 (e.g., '2020-01-01T00:00:00Z/2021-01-01T12:31:12Z')\n"
|
|
160
|
+
"- `sortby`: List of sort criteria objects with 'field' and 'direction' (asc/desc)\n"
|
|
161
|
+
"- `fields`: Object with 'include' and 'exclude' arrays for field selection"
|
|
162
|
+
),
|
|
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
|
+
},
|
|
181
|
+
),
|
|
182
|
+
) -> Union[Collections, Response]:
|
|
183
|
+
return await original_post_endpoint(request, body)
|
|
184
|
+
|
|
185
|
+
documented_post_endpoint.__name__ = original_post_endpoint.__name__
|
|
186
|
+
return documented_post_endpoint
|
|
25
187
|
|
|
26
188
|
|
|
27
189
|
class CollectionsSearchEndpointExtension(ApiExtension):
|
|
@@ -54,7 +216,6 @@ class CollectionsSearchEndpointExtension(ApiExtension):
|
|
|
54
216
|
self.POST = POST
|
|
55
217
|
self.conformance_classes = conformance_classes or []
|
|
56
218
|
self.router = APIRouter()
|
|
57
|
-
self.create_endpoints()
|
|
58
219
|
|
|
59
220
|
def register(self, app: FastAPI) -> None:
|
|
60
221
|
"""Register the extension with a FastAPI application.
|
|
@@ -65,32 +226,53 @@ class CollectionsSearchEndpointExtension(ApiExtension):
|
|
|
65
226
|
Returns:
|
|
66
227
|
None
|
|
67
228
|
"""
|
|
68
|
-
|
|
229
|
+
# Remove any existing routes to avoid duplicates
|
|
230
|
+
self.router.routes = []
|
|
69
231
|
|
|
70
|
-
|
|
71
|
-
"""Create endpoints for the extension."""
|
|
232
|
+
# Recreate endpoints with proper OpenAPI documentation
|
|
72
233
|
if self.GET:
|
|
234
|
+
original_endpoint = self.collections_search_get_endpoint
|
|
235
|
+
documented_endpoint = build_get_collections_search_doc(original_endpoint)
|
|
236
|
+
|
|
73
237
|
self.router.add_api_route(
|
|
74
|
-
name="Get Collections Search",
|
|
75
238
|
path="/collections-search",
|
|
239
|
+
endpoint=documented_endpoint,
|
|
76
240
|
response_model=None,
|
|
77
241
|
response_class=JSONResponse,
|
|
78
242
|
methods=["GET"],
|
|
79
|
-
|
|
243
|
+
summary="Search collections",
|
|
244
|
+
description=(
|
|
245
|
+
"Search for collections using query parameters. "
|
|
246
|
+
"Supports filtering, sorting, and field selection."
|
|
247
|
+
),
|
|
248
|
+
response_description="A list of collections matching the search criteria",
|
|
249
|
+
tags=["Collections Search Extension"],
|
|
80
250
|
**(self.settings if isinstance(self.settings, dict) else {}),
|
|
81
251
|
)
|
|
82
252
|
|
|
83
253
|
if self.POST:
|
|
254
|
+
original_post_endpoint = self.collections_search_post_endpoint
|
|
255
|
+
documented_post_endpoint = build_post_collections_search_doc(
|
|
256
|
+
original_post_endpoint
|
|
257
|
+
)
|
|
258
|
+
|
|
84
259
|
self.router.add_api_route(
|
|
85
|
-
name="Post Collections Search",
|
|
86
260
|
path="/collections-search",
|
|
261
|
+
endpoint=documented_post_endpoint,
|
|
87
262
|
response_model=None,
|
|
88
263
|
response_class=JSONResponse,
|
|
89
264
|
methods=["POST"],
|
|
90
|
-
|
|
265
|
+
summary="Search collections",
|
|
266
|
+
description=(
|
|
267
|
+
"Search for collections using a JSON request body. "
|
|
268
|
+
"Supports filtering, sorting, field selection, and pagination."
|
|
269
|
+
),
|
|
270
|
+
tags=["Collections Search Extension"],
|
|
91
271
|
**(self.settings if isinstance(self.settings, dict) else {}),
|
|
92
272
|
)
|
|
93
273
|
|
|
274
|
+
app.include_router(self.router)
|
|
275
|
+
|
|
94
276
|
async def collections_search_get_endpoint(
|
|
95
277
|
self, request: Request
|
|
96
278
|
) -> Union[Collections, Response]:
|
|
@@ -124,6 +306,14 @@ class CollectionsSearchEndpointExtension(ApiExtension):
|
|
|
124
306
|
sortby = sortby_str.split(",")
|
|
125
307
|
params["sortby"] = sortby
|
|
126
308
|
|
|
309
|
+
# Handle filter parameter mapping (fixed for collections-search)
|
|
310
|
+
if "filter" in params:
|
|
311
|
+
params["filter_expr"] = params.pop("filter")
|
|
312
|
+
|
|
313
|
+
# Handle filter-lang parameter mapping (fixed for collections-search)
|
|
314
|
+
if "filter-lang" in params:
|
|
315
|
+
params["filter_lang"] = params.pop("filter-lang")
|
|
316
|
+
|
|
127
317
|
collections = await self.client.all_collections(request=request, **params)
|
|
128
318
|
return collections
|
|
129
319
|
|