Skip to content

Commit c009e32

Browse files
committed
add query/sort/fields extension support for item_collection and refactor extensions mapping
1 parent df4c12a commit c009e32

File tree

3 files changed

+205
-19
lines changed

3 files changed

+205
-19
lines changed

stac_fastapi/pgstac/app.py

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,22 +23,29 @@
2323
from stac_fastapi.api.openapi import update_openapi
2424
from stac_fastapi.extensions.core import (
2525
FieldsExtension,
26-
FilterExtension,
2726
FreeTextExtension,
2827
OffsetPaginationExtension,
2928
SortExtension,
3029
TokenPaginationExtension,
3130
TransactionExtension,
3231
)
3332
from stac_fastapi.extensions.core.collection_search import CollectionSearchExtension
33+
from stac_fastapi.extensions.core.collection_search.request import (
34+
BaseCollectionSearchGetRequest,
35+
)
3436
from stac_fastapi.extensions.third_party import BulkTransactionExtension
3537
from starlette.middleware import Middleware
3638

3739
from stac_fastapi.pgstac.config import Settings
3840
from stac_fastapi.pgstac.core import CoreCrudClient
3941
from stac_fastapi.pgstac.db import close_db_connection, connect_to_db
4042
from stac_fastapi.pgstac.extensions import QueryExtension
41-
from stac_fastapi.pgstac.extensions.filter import FiltersClient
43+
from stac_fastapi.pgstac.extensions.filter import (
44+
CollectionSearchFilterExtension,
45+
FiltersClient,
46+
ItemCollectionFilterExtension,
47+
SearchFilterExtension,
48+
)
4249
from stac_fastapi.pgstac.transactions import BulkTransactionsClient, TransactionsClient
4350
from stac_fastapi.pgstac.types.search import PgstacSearch
4451

@@ -59,23 +66,48 @@
5966
"query": QueryExtension(),
6067
"sort": SortExtension(),
6168
"fields": FieldsExtension(),
62-
"filter": FilterExtension(client=FiltersClient()),
69+
"filter": SearchFilterExtension(client=FiltersClient()),
6370
"pagination": TokenPaginationExtension(),
6471
}
6572

6673
# collection_search extensions
6774
cs_extensions_map = {
68-
"query": QueryExtension(),
69-
"sort": SortExtension(),
70-
"fields": FieldsExtension(),
71-
"filter": FilterExtension(client=FiltersClient()),
72-
"free_text": FreeTextExtension(),
75+
"query": QueryExtension(
76+
conformance_classes=[
77+
"https://api.stacspec.org/v1.0.0-rc.1/collection-search#query"
78+
]
79+
),
80+
"sort": SortExtension(
81+
conformance_classes=[
82+
"https://api.stacspec.org/v1.0.0-rc.1/collection-search#sort"
83+
]
84+
),
85+
"fields": FieldsExtension(
86+
conformance_classes=[
87+
"https://api.stacspec.org/v1.0.0-rc.1/collection-search#fields"
88+
]
89+
),
90+
"filter": CollectionSearchFilterExtension(client=FiltersClient()),
91+
"free_text": FreeTextExtension(
92+
conformance_classes=[
93+
"https://api.stacspec.org/v1.0.0-rc.1/collection-search#free-text",
94+
],
95+
),
7396
"pagination": OffsetPaginationExtension(),
7497
}
7598

7699
# item_collection extensions
77100
itm_col_extensions_map = {
78-
"filter": FilterExtension(client=FiltersClient()),
101+
"query": QueryExtension(
102+
conformance_classes=["https://api.stacspec.org/v1.0.0/ogcapi-features#query"],
103+
),
104+
"sort": SortExtension(
105+
conformance_classes=["https://api.stacspec.org/v1.0.0/ogcapi-features#sort"],
106+
),
107+
"fields": FieldsExtension(
108+
conformance_classes=["https://api.stacspec.org/v1.0.0/ogcapi-features#fields"],
109+
),
110+
"filter": ItemCollectionFilterExtension(client=FiltersClient()),
79111
"pagination": TokenPaginationExtension(),
80112
}
81113

@@ -123,17 +155,31 @@
123155
extensions=itm_col_extensions,
124156
request_type="GET",
125157
)
158+
application_extensions.extend(itm_col_extensions)
126159

127160
# /collections model
128161
collections_get_request_model = EmptyRequest
129162
if "collection_search" in enabled_extensions:
130-
cs_extensions = [
131-
extension
132-
for key, extension in cs_extensions_map.items()
133-
if key in enabled_extensions
163+
mixins = []
164+
mixing_conformances = [
165+
"https://api.stacspec.org/v1.0.0-rc.1/collection-search",
166+
"http://www.opengis.net/spec/ogcapi-common-2/1.0/conf/simple-query",
134167
]
135-
collection_search_extension = CollectionSearchExtension.from_extensions(cs_extensions)
136-
collections_get_request_model = collection_search_extension.GET
168+
for key, extension in cs_extensions_map.items():
169+
if key not in enabled_extensions:
170+
continue
171+
mixins.append(extension.GET)
172+
mixing_conformances.extend(extension.conformance_classes)
173+
174+
collections_get_request_model = create_request_model(
175+
model_name="CollectionsGetRequest",
176+
base_model=BaseCollectionSearchGetRequest,
177+
mixins=mixins,
178+
request_type="GET",
179+
)
180+
collection_search_extension = CollectionSearchExtension(
181+
GET=collections_get_request_model, conformance_classes=mixing_conformances
182+
)
137183
application_extensions.append(collection_search_extension)
138184

139185

stac_fastapi/pgstac/core.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -343,9 +343,12 @@ async def item_collection(
343343
datetime: Optional[str] = None,
344344
limit: Optional[int] = None,
345345
# Extensions
346-
token: Optional[str] = None,
346+
query: Optional[str] = None,
347+
fields: Optional[List[str]] = None,
348+
sortby: Optional[str] = None,
347349
filter_expr: Optional[str] = None,
348350
filter_lang: Optional[str] = None,
351+
token: Optional[str] = None,
349352
**kwargs,
350353
) -> ItemCollection:
351354
"""Get all items from a specific collection.
@@ -369,12 +372,15 @@ async def item_collection(
369372
"datetime": datetime,
370373
"limit": limit,
371374
"token": token,
375+
"query": orjson.loads(unquote_plus(query)) if query else query,
372376
}
373377

374378
clean = self._clean_search_args(
375379
base_args=base_args,
376380
filter_query=filter_expr,
377381
filter_lang=filter_lang,
382+
fields=fields,
383+
sortby=sortby,
378384
)
379385

380386
search_request = self.pgstac_search_model(**clean)
@@ -450,11 +456,11 @@ async def get_search(
450456
limit: Optional[int] = None,
451457
# Extensions
452458
query: Optional[str] = None,
453-
token: Optional[str] = None,
454459
fields: Optional[List[str]] = None,
455460
sortby: Optional[str] = None,
456461
filter_expr: Optional[str] = None,
457462
filter_lang: Optional[str] = None,
463+
token: Optional[str] = None,
458464
**kwargs,
459465
) -> ItemCollection:
460466
"""Cross catalog search (GET).

stac_fastapi/pgstac/extensions/filter.py

Lines changed: 136 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,24 @@
11
"""Get Queryables."""
22

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

5+
import attr
56
from buildpg import render
6-
from fastapi import Request
7+
from fastapi import APIRouter, FastAPI, Request
8+
from stac_fastapi.api.models import CollectionUri, EmptyRequest, JSONSchemaResponse
9+
from stac_fastapi.api.routes import create_async_endpoint
10+
from stac_fastapi.extensions.core import FilterExtension
11+
from stac_fastapi.extensions.core.collection_search.collection_search import (
12+
ConformanceClasses as CollectionSearchConformanceClasses,
13+
)
714
from stac_fastapi.extensions.core.filter.client import AsyncBaseFiltersClient
15+
from stac_fastapi.extensions.core.filter.filter import FilterConformanceClasses
16+
from stac_fastapi.extensions.core.filter.request import (
17+
FilterExtensionGetRequest,
18+
FilterExtensionPostRequest,
19+
)
820
from stac_fastapi.types.errors import NotFoundError
21+
from starlette.responses import Response
922

1023

1124
class FiltersClient(AsyncBaseFiltersClient):
@@ -40,3 +53,124 @@ async def get_queryables(
4053

4154
queryables["$id"] = str(request.url)
4255
return queryables
56+
57+
58+
@attr.s
59+
class SearchFilterExtension(FilterExtension):
60+
"""Item Search Filter Extension."""
61+
62+
GET = FilterExtensionGetRequest
63+
POST = FilterExtensionPostRequest
64+
65+
client: FiltersClient = attr.ib(factory=FiltersClient)
66+
conformance_classes: List[str] = attr.ib(
67+
default=[
68+
FilterConformanceClasses.FILTER,
69+
FilterConformanceClasses.ITEM_SEARCH_FILTER,
70+
FilterConformanceClasses.BASIC_CQL2,
71+
FilterConformanceClasses.CQL2_JSON,
72+
FilterConformanceClasses.CQL2_TEXT,
73+
]
74+
)
75+
router: APIRouter = attr.ib(factory=APIRouter)
76+
response_class: Type[Response] = attr.ib(default=JSONSchemaResponse)
77+
78+
def register(self, app: FastAPI) -> None:
79+
"""Register the extension with a FastAPI application.
80+
81+
Args:
82+
app: target FastAPI application.
83+
84+
Returns:
85+
None
86+
"""
87+
self.router.prefix = app.state.router_prefix
88+
self.router.add_api_route(
89+
name="Queryables",
90+
path="/queryables",
91+
methods=["GET"],
92+
responses={
93+
200: {
94+
"content": {
95+
"application/schema+json": {},
96+
},
97+
# TODO: add output model in stac-pydantic
98+
},
99+
},
100+
response_class=self.response_class,
101+
endpoint=create_async_endpoint(self.client.get_queryables, EmptyRequest),
102+
)
103+
app.include_router(self.router, tags=["Filter Extension"])
104+
105+
106+
@attr.s
107+
class ItemCollectionFilterExtension(FilterExtension):
108+
"""Item Collection Filter Extension."""
109+
110+
GET = FilterExtensionGetRequest
111+
POST = FilterExtensionPostRequest
112+
113+
client: FiltersClient = attr.ib(factory=FiltersClient)
114+
conformance_classes: List[str] = attr.ib(
115+
default=[
116+
FilterConformanceClasses.FILTER,
117+
FilterConformanceClasses.FEATURES_FILTER,
118+
FilterConformanceClasses.BASIC_CQL2,
119+
FilterConformanceClasses.CQL2_JSON,
120+
FilterConformanceClasses.CQL2_TEXT,
121+
]
122+
)
123+
router: APIRouter = attr.ib(factory=APIRouter)
124+
response_class: Type[Response] = attr.ib(default=JSONSchemaResponse)
125+
126+
def register(self, app: FastAPI) -> None:
127+
"""Register the extension with a FastAPI application.
128+
129+
Args:
130+
app: target FastAPI application.
131+
132+
Returns:
133+
None
134+
"""
135+
self.router.add_api_route(
136+
name="Collection Queryables",
137+
path="/collections/{collection_id}/queryables",
138+
methods=["GET"],
139+
responses={
140+
200: {
141+
"content": {
142+
"application/schema+json": {},
143+
},
144+
# TODO: add output model in stac-pydantic
145+
},
146+
},
147+
response_class=self.response_class,
148+
endpoint=create_async_endpoint(self.client.get_queryables, CollectionUri),
149+
)
150+
app.include_router(self.router, tags=["Filter Extension"])
151+
152+
153+
@attr.s
154+
class CollectionSearchFilterExtension(FilterExtension):
155+
"""Collection Search Filter Extension."""
156+
157+
GET = FilterExtensionGetRequest
158+
POST = FilterExtensionPostRequest
159+
160+
client: FiltersClient = attr.ib(factory=FiltersClient)
161+
conformance_classes: List[str] = attr.ib(
162+
default=[CollectionSearchConformanceClasses.FILTER]
163+
)
164+
router: APIRouter = attr.ib(factory=APIRouter)
165+
response_class: Type[Response] = attr.ib(default=JSONSchemaResponse)
166+
167+
def register(self, app: FastAPI) -> None:
168+
"""Register the extension with a FastAPI application.
169+
170+
Args:
171+
app: target FastAPI application.
172+
173+
Returns:
174+
None
175+
"""
176+
pass

0 commit comments

Comments
 (0)