Skip to content

Commit e6e4fef

Browse files
authored
Update api docs for collections-search routes (stac-utils#508)
**Related Issue(s):** - None **Description:** - Update api docs for collections-search routes **PR Checklist:** - [x] Code is formatted and linted (run `pre-commit run --all-files`) - [x] Tests pass (run `make test`) - [x] Documentation has been updated to reflect changes, if applicable - [x] Changes are added to the changelog
1 parent e15f45b commit e6e4fef

File tree

2 files changed

+130
-12
lines changed

2 files changed

+130
-12
lines changed

CHANGELOG.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
99

1010
### Added
1111

12-
- CloudFerro logo to sponsors and supporters list [#485](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/485)
13-
- Latest news section to README [#485](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/485)
1412
- Environment variable `EXCLUDED_FROM_QUERYABLES` to exclude specific fields from queryables endpoint and filtering. Supports comma-separated list of fully qualified field names (e.g., `properties.auth:schemes,properties.storage:schemes`) [#489](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/489)
1513

1614
### Changed
@@ -21,6 +19,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
2119

2220
### Updated
2321

22+
- Improved OpenAPI docs for `/collections-search` GET and POST endpoints. [#508](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/508)
2423

2524
## [v6.6.0] - 2025-10-21
2625

stac_fastapi/core/stac_fastapi/core/extensions/collections_search.py

Lines changed: 129 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
"""Collections search extension."""
22

3-
from typing import List, Optional, Type, Union
3+
from typing import Any, Dict, List, Optional, Type, Union
44

5-
from fastapi import APIRouter, FastAPI, Request
5+
from fastapi import APIRouter, Body, FastAPI, Query, Request
66
from fastapi.responses import JSONResponse
77
from pydantic import BaseModel
88
from stac_pydantic.api.search import ExtendedSearch
@@ -24,6 +24,105 @@ class CollectionsSearchRequest(ExtendedSearch):
2424
] = None # Legacy query extension (deprecated but still supported)
2525

2626

27+
def build_get_collections_search_doc(original_endpoint):
28+
"""Return a documented GET endpoint wrapper for /collections-search."""
29+
30+
async def documented_endpoint(
31+
request: Request,
32+
q: Optional[str] = Query(
33+
None,
34+
description="Free text search query",
35+
),
36+
query: Optional[str] = Query(
37+
None,
38+
description="Additional filtering expressed as a string (legacy support)",
39+
example="platform=landsat AND collection_category=level2",
40+
),
41+
limit: int = Query(
42+
10,
43+
ge=1,
44+
description=(
45+
"The maximum number of collections to return (page size). Defaults to 10."
46+
),
47+
),
48+
token: Optional[str] = Query(
49+
None,
50+
description="Pagination token for the next page of results",
51+
),
52+
bbox: Optional[str] = Query(
53+
None,
54+
description=(
55+
"Bounding box for spatial filtering in format 'minx,miny,maxx,maxy' "
56+
"or 'minx,miny,minz,maxx,maxy,maxz'"
57+
),
58+
),
59+
datetime: Optional[str] = Query(
60+
None,
61+
description=(
62+
"Temporal filter in ISO 8601 format (e.g., "
63+
"'2020-01-01T00:00:00Z/2021-01-01T00:00:00Z')"
64+
),
65+
),
66+
sortby: Optional[str] = Query(
67+
None,
68+
description=(
69+
"Sorting criteria in the format 'field' or '-field' for descending order"
70+
),
71+
),
72+
fields: Optional[List[str]] = Query(
73+
None,
74+
description=(
75+
"Comma-separated list of fields to include or exclude (use -field to exclude)"
76+
),
77+
alias="fields[]",
78+
),
79+
):
80+
# Delegate to original endpoint which reads from request.query_params
81+
return await original_endpoint(request)
82+
83+
documented_endpoint.__name__ = original_endpoint.__name__
84+
return documented_endpoint
85+
86+
87+
def build_post_collections_search_doc(original_post_endpoint):
88+
"""Return a documented POST endpoint wrapper for /collections-search."""
89+
90+
async def documented_post_endpoint(
91+
request: Request,
92+
body: Dict[str, Any] = Body(
93+
...,
94+
description=(
95+
"Search parameters for collections.\n\n"
96+
"- `q`: Free text search query (string or list of strings)\n"
97+
"- `query`: Additional filtering expressed as a string (legacy support)\n"
98+
"- `limit`: Maximum number of results to return (default: 10)\n"
99+
"- `token`: Pagination token for the next page of results\n"
100+
"- `bbox`: Bounding box [minx, miny, maxx, maxy] or [minx, miny, minz, maxx, maxy, maxz]\n"
101+
"- `datetime`: Temporal filter in ISO 8601 (e.g., '2020-01-01T00:00:00Z/2021-01-01T12:31:12Z')\n"
102+
"- `sortby`: List of sort criteria objects with 'field' and 'direction' (asc/desc)\n"
103+
"- `fields`: Object with 'include' and 'exclude' arrays for field selection"
104+
),
105+
example={
106+
"q": "landsat",
107+
"query": "platform=landsat AND collection_category=level2",
108+
"limit": 10,
109+
"token": "next-page-token",
110+
"bbox": [-180, -90, 180, 90],
111+
"datetime": "2020-01-01T00:00:00Z/2021-01-01T12:31:12Z",
112+
"sortby": [{"field": "id", "direction": "asc"}],
113+
"fields": {
114+
"include": ["id", "title", "description"],
115+
"exclude": ["properties"],
116+
},
117+
},
118+
),
119+
) -> Union[Collections, Response]:
120+
return await original_post_endpoint(request, body)
121+
122+
documented_post_endpoint.__name__ = original_post_endpoint.__name__
123+
return documented_post_endpoint
124+
125+
27126
class CollectionsSearchEndpointExtension(ApiExtension):
28127
"""Collections search endpoint extension.
29128
@@ -54,7 +153,6 @@ def __init__(
54153
self.POST = POST
55154
self.conformance_classes = conformance_classes or []
56155
self.router = APIRouter()
57-
self.create_endpoints()
58156

59157
def register(self, app: FastAPI) -> None:
60158
"""Register the extension with a FastAPI application.
@@ -65,32 +163,53 @@ def register(self, app: FastAPI) -> None:
65163
Returns:
66164
None
67165
"""
68-
app.include_router(self.router)
166+
# Remove any existing routes to avoid duplicates
167+
self.router.routes = []
69168

70-
def create_endpoints(self) -> None:
71-
"""Create endpoints for the extension."""
169+
# Recreate endpoints with proper OpenAPI documentation
72170
if self.GET:
171+
original_endpoint = self.collections_search_get_endpoint
172+
documented_endpoint = build_get_collections_search_doc(original_endpoint)
173+
73174
self.router.add_api_route(
74-
name="Get Collections Search",
75175
path="/collections-search",
176+
endpoint=documented_endpoint,
76177
response_model=None,
77178
response_class=JSONResponse,
78179
methods=["GET"],
79-
endpoint=self.collections_search_get_endpoint,
180+
summary="Search collections",
181+
description=(
182+
"Search for collections using query parameters. "
183+
"Supports filtering, sorting, and field selection."
184+
),
185+
response_description="A list of collections matching the search criteria",
186+
tags=["Collections Search Extension"],
80187
**(self.settings if isinstance(self.settings, dict) else {}),
81188
)
82189

83190
if self.POST:
191+
original_post_endpoint = self.collections_search_post_endpoint
192+
documented_post_endpoint = build_post_collections_search_doc(
193+
original_post_endpoint
194+
)
195+
84196
self.router.add_api_route(
85-
name="Post Collections Search",
86197
path="/collections-search",
198+
endpoint=documented_post_endpoint,
87199
response_model=None,
88200
response_class=JSONResponse,
89201
methods=["POST"],
90-
endpoint=self.collections_search_post_endpoint,
202+
summary="Search collections",
203+
description=(
204+
"Search for collections using a JSON request body. "
205+
"Supports filtering, sorting, field selection, and pagination."
206+
),
207+
tags=["Collections Search Extension"],
91208
**(self.settings if isinstance(self.settings, dict) else {}),
92209
)
93210

211+
app.include_router(self.router)
212+
94213
async def collections_search_get_endpoint(
95214
self, request: Request
96215
) -> Union[Collections, Response]:

0 commit comments

Comments
 (0)