Skip to content

Commit f376725

Browse files
committed
update api docs
1 parent 0d49758 commit f376725

File tree

2 files changed

+138
-10
lines changed

2 files changed

+138
-10
lines changed

CHANGELOG.md

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

1212
### Changed
1313

14+
- Improved OpenAPI docs for `/collections-search` GET and POST endpoints.
15+
1416
### Fixed
1517

1618
### Removed

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

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

59164
def register(self, app: FastAPI) -> None:
60165
"""Register the extension with a FastAPI application.
@@ -65,32 +170,53 @@ def register(self, app: FastAPI) -> None:
65170
Returns:
66171
None
67172
"""
68-
app.include_router(self.router)
173+
# Remove any existing routes to avoid duplicates
174+
self.router.routes = []
69175

70-
def create_endpoints(self) -> None:
71-
"""Create endpoints for the extension."""
176+
# Recreate endpoints with proper OpenAPI documentation
72177
if self.GET:
178+
original_endpoint = self.collections_search_get_endpoint
179+
documented_endpoint = build_get_collections_search_doc(original_endpoint)
180+
73181
self.router.add_api_route(
74-
name="Get Collections Search",
75182
path="/collections-search",
183+
endpoint=documented_endpoint,
76184
response_model=None,
77185
response_class=JSONResponse,
78186
methods=["GET"],
79-
endpoint=self.collections_search_get_endpoint,
187+
summary="Search collections",
188+
description=(
189+
"Search for collections using query parameters. "
190+
"Supports filtering, sorting, and field selection."
191+
),
192+
response_description="A list of collections matching the search criteria",
193+
tags=["Collections Search Extension"],
80194
**(self.settings if isinstance(self.settings, dict) else {}),
81195
)
82196

83197
if self.POST:
198+
original_post_endpoint = self.collections_search_post_endpoint
199+
documented_post_endpoint = build_post_collections_search_doc(
200+
original_post_endpoint
201+
)
202+
84203
self.router.add_api_route(
85-
name="Post Collections Search",
86204
path="/collections-search",
205+
endpoint=documented_post_endpoint,
87206
response_model=None,
88207
response_class=JSONResponse,
89208
methods=["POST"],
90-
endpoint=self.collections_search_post_endpoint,
209+
summary="Search collections",
210+
description=(
211+
"Search for collections using a JSON request body. "
212+
"Supports filtering, sorting, field selection, and pagination."
213+
),
214+
tags=["Collections Search Extension"],
91215
**(self.settings if isinstance(self.settings, dict) else {}),
92216
)
93217

218+
app.include_router(self.router)
219+
94220
async def collections_search_get_endpoint(
95221
self, request: Request
96222
) -> Union[Collections, Response]:

0 commit comments

Comments
 (0)