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
66from fastapi .responses import JSONResponse
77from pydantic import BaseModel
88from 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+
27133class 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