Skip to content

Commit d2749b2

Browse files
add query/sort/fields extension support for item_collection (#192)
* add query/sort/fields extension support for item_collection and refactor extensions mapping * update for stac-fastapi 5.0 * more tests * do not migrate items * update changelog
1 parent df4c12a commit d2749b2

File tree

9 files changed

+96
-47
lines changed

9 files changed

+96
-47
lines changed

CHANGES.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,28 @@
44

55
### Changed
66

7-
- handle `next` and `dev` tokens now returned as links from pgstac>=0.9.0 (author @zstatmanweil, <https://github.com/stac-utils/stac-fastapi-pgstac/pull/140>)
7+
- remove `python 3.8` support
8+
- update `stac-fastapi-*` requirement to `~=5.0`
89
- keep `/search` and `/collections` extensions separate ([#158](https://github.com/stac-utils/stac-fastapi-pgstac/pull/158))
910
- update `pypgstac` requirement to `>=0.8,<0.10`
1011
- set `pypgstac==0.9.*` for test requirements
11-
- update `stac-fastapi-*` requirement to `~=4.0`
12-
- remove `python 3.8` support
1312
- renamed `post_request_model` attribute to `pgstac_search_model` in `CoreCrudClient` class
1413
- changed `datetime` input type to `string` in GET endpoint methods
1514
- renamed `filter` to `filter_expr` input attributes in GET endpoint methods
1615
- delete `utils.format_datetime_range` function
1716

17+
### Fixed
18+
19+
- handle `next` and `dev` tokens now returned as links from pgstac>=0.9.0 (author @zstatmanweil, <https://github.com/stac-utils/stac-fastapi-pgstac/pull/140>)
20+
1821
### Added
1922

2023
- add [collection search extension](https://github.com/stac-api-extensions/collection-search) support ([#139](https://github.com/stac-utils/stac-fastapi-pgstac/pull/139))
2124
- add [free-text extension](https://github.com/stac-api-extensions/freetext-search) to collection search extensions ([#162](https://github.com/stac-utils/stac-fastapi-pgstac/pull/162))
2225
- add [filter extension](https://github.com/stac-api-extensions/filter) support to Item Collection endpoint
26+
- add [sort extension](https://github.com/stac-api-extensions/sort) support to Item Collection endpoint ([#192](https://github.com/stac-utils/stac-fastapi-pgstac/pull/192))
27+
- add [query extension](https://github.com/stac-api-extensions/query) support to Item Collection endpoint ([#162](https://github.com/stac-utils/stac-fastapi-pgstac/pull/192))
28+
- add [fields extension](https://github.com/stac-api-extensions/fields) support to Item Collection endpoint ([#162](https://github.com/stac-utils/stac-fastapi-pgstac/pull/192))
2329

2430
### Fixed
2531

docker-compose.nginx.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
version: '3'
21
services:
32
nginx:
43
image: nginx

nginx.conf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,4 @@ http {
1717
proxy_redirect off;
1818
}
1919
}
20-
}
20+
}

setup.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
"orjson",
1111
"pydantic",
1212
"stac_pydantic==3.1.*",
13-
"stac-fastapi.api~=4.0",
14-
"stac-fastapi.extensions~=4.0",
15-
"stac-fastapi.types~=4.0",
13+
"stac-fastapi.api~=5.0",
14+
"stac-fastapi.extensions~=5.0",
15+
"stac-fastapi.types~=5.0",
1616
"asyncpg",
1717
"buildpg",
1818
"brotli_asgi",

stac_fastapi/pgstac/app.py

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,22 @@
2020
create_post_request_model,
2121
create_request_model,
2222
)
23-
from stac_fastapi.api.openapi import update_openapi
2423
from stac_fastapi.extensions.core import (
24+
CollectionSearchExtension,
25+
CollectionSearchFilterExtension,
2526
FieldsExtension,
26-
FilterExtension,
2727
FreeTextExtension,
28+
ItemCollectionFilterExtension,
2829
OffsetPaginationExtension,
30+
SearchFilterExtension,
2931
SortExtension,
3032
TokenPaginationExtension,
3133
TransactionExtension,
3234
)
33-
from stac_fastapi.extensions.core.collection_search import CollectionSearchExtension
35+
from stac_fastapi.extensions.core.fields import FieldsConformanceClasses
36+
from stac_fastapi.extensions.core.free_text import FreeTextConformanceClasses
37+
from stac_fastapi.extensions.core.query import QueryConformanceClasses
38+
from stac_fastapi.extensions.core.sort import SortConformanceClasses
3439
from stac_fastapi.extensions.third_party import BulkTransactionExtension
3540
from starlette.middleware import Middleware
3641

@@ -59,23 +64,32 @@
5964
"query": QueryExtension(),
6065
"sort": SortExtension(),
6166
"fields": FieldsExtension(),
62-
"filter": FilterExtension(client=FiltersClient()),
67+
"filter": SearchFilterExtension(client=FiltersClient()),
6368
"pagination": TokenPaginationExtension(),
6469
}
6570

6671
# collection_search extensions
6772
cs_extensions_map = {
68-
"query": QueryExtension(),
69-
"sort": SortExtension(),
70-
"fields": FieldsExtension(),
71-
"filter": FilterExtension(client=FiltersClient()),
72-
"free_text": FreeTextExtension(),
73+
"query": QueryExtension(conformance_classes=[QueryConformanceClasses.COLLECTIONS]),
74+
"sort": SortExtension(conformance_classes=[SortConformanceClasses.COLLECTIONS]),
75+
"fields": FieldsExtension(conformance_classes=[FieldsConformanceClasses.COLLECTIONS]),
76+
"filter": CollectionSearchFilterExtension(client=FiltersClient()),
77+
"free_text": FreeTextExtension(
78+
conformance_classes=[FreeTextConformanceClasses.COLLECTIONS],
79+
),
7380
"pagination": OffsetPaginationExtension(),
7481
}
7582

7683
# item_collection extensions
7784
itm_col_extensions_map = {
78-
"filter": FilterExtension(client=FiltersClient()),
85+
"query": QueryExtension(
86+
conformance_classes=[QueryConformanceClasses.ITEMS],
87+
),
88+
"sort": SortExtension(
89+
conformance_classes=[SortConformanceClasses.ITEMS],
90+
),
91+
"fields": FieldsExtension(conformance_classes=[FieldsConformanceClasses.ITEMS]),
92+
"filter": ItemCollectionFilterExtension(client=FiltersClient()),
7993
"pagination": TokenPaginationExtension(),
8094
}
8195

@@ -123,6 +137,7 @@
123137
extensions=itm_col_extensions,
124138
request_type="GET",
125139
)
140+
application_extensions.extend(itm_col_extensions)
126141

127142
# /collections model
128143
collections_get_request_model = EmptyRequest
@@ -145,17 +160,17 @@ async def lifespan(app: FastAPI):
145160
await close_db_connection(app)
146161

147162

148-
fastapp = FastAPI(
149-
openapi_url=settings.openapi_url,
150-
docs_url=settings.docs_url,
151-
redoc_url=None,
152-
root_path=settings.root_path,
153-
lifespan=lifespan,
154-
)
155-
156-
157163
api = StacApi(
158-
app=update_openapi(fastapp),
164+
app=FastAPI(
165+
openapi_url=settings.openapi_url,
166+
docs_url=settings.docs_url,
167+
redoc_url=None,
168+
root_path=settings.root_path,
169+
title=settings.stac_fastapi_title,
170+
version=settings.stac_fastapi_version,
171+
description=settings.stac_fastapi_description,
172+
lifespan=lifespan,
173+
),
159174
settings=settings,
160175
extensions=application_extensions,
161176
client=CoreCrudClient(pgstac_search_model=post_request_model),

stac_fastapi/pgstac/core.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,9 @@ async def all_collections( # noqa: C901
123123
collection_id=coll["id"], request=request
124124
).get_links(extra_links=coll.get("links"))
125125

126-
if self.extension_is_enabled("FilterExtension"):
126+
if self.extension_is_enabled(
127+
"FilterExtension"
128+
) or self.extension_is_enabled("ItemCollectionFilterExtension"):
127129
coll["links"].append(
128130
{
129131
"rel": Relations.queryables.value,
@@ -178,7 +180,9 @@ async def get_collection(
178180
collection_id=collection_id, request=request
179181
).get_links(extra_links=collection.get("links"))
180182

181-
if self.extension_is_enabled("FilterExtension"):
183+
if self.extension_is_enabled("FilterExtension") or self.extension_is_enabled(
184+
"ItemCollectionFilterExtension"
185+
):
182186
base_url = get_base_url(request)
183187
collection["links"].append(
184188
{
@@ -343,9 +347,12 @@ async def item_collection(
343347
datetime: Optional[str] = None,
344348
limit: Optional[int] = None,
345349
# Extensions
346-
token: Optional[str] = None,
350+
query: Optional[str] = None,
351+
fields: Optional[List[str]] = None,
352+
sortby: Optional[str] = None,
347353
filter_expr: Optional[str] = None,
348354
filter_lang: Optional[str] = None,
355+
token: Optional[str] = None,
349356
**kwargs,
350357
) -> ItemCollection:
351358
"""Get all items from a specific collection.
@@ -369,12 +376,15 @@ async def item_collection(
369376
"datetime": datetime,
370377
"limit": limit,
371378
"token": token,
379+
"query": orjson.loads(unquote_plus(query)) if query else query,
372380
}
373381

374382
clean = self._clean_search_args(
375383
base_args=base_args,
376384
filter_query=filter_expr,
377385
filter_lang=filter_lang,
386+
fields=fields,
387+
sortby=sortby,
378388
)
379389

380390
search_request = self.pgstac_search_model(**clean)
@@ -450,11 +460,11 @@ async def get_search(
450460
limit: Optional[int] = None,
451461
# Extensions
452462
query: Optional[str] = None,
453-
token: Optional[str] = None,
454463
fields: Optional[List[str]] = None,
455464
sortby: Optional[str] = None,
456465
filter_expr: Optional[str] = None,
457466
filter_lang: Optional[str] = None,
467+
token: Optional[str] = None,
458468
**kwargs,
459469
) -> ItemCollection:
460470
"""Cross catalog search (GET).

tests/api/test_api.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
FieldsExtension,
1818
TransactionExtension,
1919
)
20+
from stac_fastapi.extensions.core.fields import FieldsConformanceClasses
2021
from stac_fastapi.types import stac as stac_types
2122

2223
from stac_fastapi.pgstac.core import CoreCrudClient, Settings
@@ -86,10 +87,12 @@ async def test_landing_links(app_client):
8687

8788
async def test_get_queryables_content_type(app_client, load_test_collection):
8889
resp = await app_client.get("queryables")
90+
assert resp.status_code == 200
8991
assert resp.headers["content-type"] == "application/schema+json"
9092

9193
coll = load_test_collection
9294
resp = await app_client.get(f"collections/{coll['id']}/queryables")
95+
assert resp.status_code == 200
9396
assert resp.headers["content-type"] == "application/schema+json"
9497

9598

@@ -487,6 +490,7 @@ async def test_search_duplicate_forward_headers(
487490
@pytest.mark.asyncio
488491
async def test_base_queryables(load_test_data, app_client, load_test_collection):
489492
resp = await app_client.get("/queryables")
493+
assert resp.status_code == 200
490494
assert resp.headers["Content-Type"] == "application/schema+json"
491495
q = resp.json()
492496
assert q["$id"].endswith("/queryables")
@@ -498,6 +502,7 @@ async def test_base_queryables(load_test_data, app_client, load_test_collection)
498502
@pytest.mark.asyncio
499503
async def test_collection_queryables(load_test_data, app_client, load_test_collection):
500504
resp = await app_client.get("/collections/test-collection/queryables")
505+
assert resp.status_code == 200
501506
assert resp.headers["Content-Type"] == "application/schema+json"
502507
q = resp.json()
503508
assert q["$id"].endswith("/collections/test-collection/queryables")
@@ -733,7 +738,7 @@ async def get_collection(
733738

734739
collection_search_extension = CollectionSearchExtension.from_extensions(
735740
extensions=[
736-
FieldsExtension(),
741+
FieldsExtension(conformance_classes=[FieldsConformanceClasses.COLLECTIONS]),
737742
]
738743
)
739744

tests/conftest.py

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,20 @@
2525
)
2626
from stac_fastapi.extensions.core import (
2727
CollectionSearchExtension,
28+
CollectionSearchFilterExtension,
2829
FieldsExtension,
29-
FilterExtension,
3030
FreeTextExtension,
31+
ItemCollectionFilterExtension,
3132
OffsetPaginationExtension,
33+
SearchFilterExtension,
3234
SortExtension,
3335
TokenPaginationExtension,
3436
TransactionExtension,
3537
)
38+
from stac_fastapi.extensions.core.fields import FieldsConformanceClasses
39+
from stac_fastapi.extensions.core.free_text import FreeTextConformanceClasses
40+
from stac_fastapi.extensions.core.query import QueryConformanceClasses
41+
from stac_fastapi.extensions.core.sort import SortConformanceClasses
3642
from stac_fastapi.extensions.third_party import BulkTransactionExtension
3743
from stac_pydantic import Collection, Item
3844

@@ -143,17 +149,19 @@ def api_client(request, database):
143149
QueryExtension(),
144150
SortExtension(),
145151
FieldsExtension(),
146-
FilterExtension(client=FiltersClient()),
152+
SearchFilterExtension(client=FiltersClient()),
147153
TokenPaginationExtension(),
148154
]
149155
application_extensions.extend(search_extensions)
150156

151157
collection_extensions = [
152-
QueryExtension(),
153-
SortExtension(),
154-
FieldsExtension(),
155-
FilterExtension(client=FiltersClient()),
156-
FreeTextExtension(),
158+
QueryExtension(conformance_classes=[QueryConformanceClasses.COLLECTIONS]),
159+
SortExtension(conformance_classes=[SortConformanceClasses.COLLECTIONS]),
160+
FieldsExtension(conformance_classes=[FieldsConformanceClasses.COLLECTIONS]),
161+
CollectionSearchFilterExtension(client=FiltersClient()),
162+
FreeTextExtension(
163+
conformance_classes=[FreeTextConformanceClasses.COLLECTIONS],
164+
),
157165
OffsetPaginationExtension(),
158166
]
159167
collection_search_extension = CollectionSearchExtension.from_extensions(
@@ -162,11 +170,17 @@ def api_client(request, database):
162170
application_extensions.append(collection_search_extension)
163171

164172
item_collection_extensions = [
165-
FilterExtension(client=FiltersClient()),
173+
QueryExtension(
174+
conformance_classes=[QueryConformanceClasses.ITEMS],
175+
),
176+
SortExtension(
177+
conformance_classes=[SortConformanceClasses.ITEMS],
178+
),
179+
FieldsExtension(conformance_classes=[FieldsConformanceClasses.ITEMS]),
180+
ItemCollectionFilterExtension(client=FiltersClient()),
166181
TokenPaginationExtension(),
167182
]
168-
# NOTE: we don't need to add the extensions to application_extensions
169-
# because they are already in it
183+
application_extensions.extend(item_collection_extensions)
170184

171185
items_get_request_model = create_request_model(
172186
model_name="ItemCollectionUri",
@@ -179,16 +193,14 @@ def api_client(request, database):
179193
search_extensions, base_model=PgstacSearch
180194
)
181195

182-
collections_get_request_model = collection_search_extension.GET
183-
184196
api = StacApi(
185197
settings=api_settings,
186198
extensions=application_extensions,
187199
client=CoreCrudClient(pgstac_search_model=search_post_request_model),
188200
items_get_request_model=items_get_request_model,
189201
search_get_request_model=search_get_request_model,
190202
search_post_request_model=search_post_request_model,
191-
collections_get_request_model=collections_get_request_model,
203+
collections_get_request_model=collection_search_extension.GET,
192204
response_class=ORJSONResponse,
193205
router=APIRouter(prefix=prefix),
194206
)

tests/resources/test_item.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,9 @@ async def test_fetches_valid_item(
148148
mock_root = pystac.Catalog(
149149
id="test", description="test desc", href="https://example.com"
150150
)
151-
item = pystac.Item.from_dict(item_dict, preserve_dict=False, root=mock_root)
151+
item = pystac.Item.from_dict(
152+
item_dict, preserve_dict=False, root=mock_root, migrate=False
153+
)
152154
item.validate()
153155

154156

0 commit comments

Comments
 (0)