Skip to content

Commit 0ca9fbc

Browse files
Merge pull request #41 from developmentseed/feature/stac-update-req5.0
STAC: update stac-fastapi-pgstac version to 5.0
2 parents a083a0f + 6353ae4 commit 0ca9fbc

File tree

5 files changed

+96
-135
lines changed

5 files changed

+96
-135
lines changed

infrastructure/handlers/stac_handler.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import os
66

77
from eoapi.stac.app import app
8+
from eoapi.stac.config import PostgresSettings
89
from mangum import Mangum
910
from stac_fastapi.pgstac.db import connect_to_db
1011

@@ -15,7 +16,7 @@
1516
@app.on_event("startup")
1617
async def startup_event() -> None:
1718
"""Connect to database on startup."""
18-
await connect_to_db(app)
19+
await connect_to_db(app, postgres_settings=PostgresSettings())
1920

2021

2122
handler = Mangum(app, lifespan="off")

runtimes/eoapi/stac/eoapi/stac/app.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
from . import __version__ as eoapi_devseed_version
4545
from .api import StacApi
4646
from .client import FiltersClient, PgSTACClient
47-
from .config import Settings
47+
from .config import PostgresSettings, Settings
4848
from .extensions import (
4949
HTMLorGeoMultiOutputExtension,
5050
HTMLorGeoOutputExtension,
@@ -65,6 +65,7 @@
6565
templates = Jinja2Templates(env=jinja2_env)
6666

6767
settings = Settings()
68+
pg_settings = PostgresSettings()
6869
auth_settings = OpenIdConnectSettings()
6970

7071

@@ -172,7 +173,7 @@
172173
@asynccontextmanager
173174
async def lifespan(app: FastAPI):
174175
"""FastAPI Lifespan."""
175-
await connect_to_db(app)
176+
await connect_to_db(app, postgres_settings=pg_settings)
176177
yield
177178
await close_db_connection(app)
178179

runtimes/eoapi/stac/eoapi/stac/client.py

Lines changed: 83 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,19 @@
1414
Type,
1515
get_args,
1616
)
17-
from urllib.parse import unquote_plus, urlencode, urljoin
17+
from urllib.parse import unquote_plus, urlencode
1818

1919
import attr
2020
import jinja2
2121
import orjson
22-
from fastapi import Request
22+
from fastapi import HTTPException, Request
2323
from geojson_pydantic.geometries import parse_geometry_obj
24+
from pydantic import ValidationError
2425
from stac_fastapi.api.models import JSONResponse
2526
from stac_fastapi.pgstac.core import CoreCrudClient
2627
from stac_fastapi.pgstac.extensions.filter import FiltersClient as PgSTACFiltersClient
2728
from stac_fastapi.pgstac.models.links import ItemCollectionLinks
2829
from stac_fastapi.pgstac.types.search import PgstacSearch
29-
from stac_fastapi.types.errors import NotFoundError
30-
from stac_fastapi.types.requests import get_base_url
3130
from stac_fastapi.types.stac import (
3231
Collection,
3332
Collections,
@@ -275,7 +274,6 @@ class PgSTACClient(CoreCrudClient):
275274

276275
async def landing_page(
277276
self,
278-
request: Request,
279277
f: Optional[str] = None,
280278
**kwargs,
281279
) -> LandingPage:
@@ -287,67 +285,9 @@ async def landing_page(
287285
API landing page, serving as an entry point to the API.
288286
289287
"""
290-
base_url = get_base_url(request)
291-
292-
landing_page = self._landing_page(
293-
base_url=base_url,
294-
conformance_classes=self.conformance_classes(),
295-
extension_schemas=[],
296-
)
297-
298-
# Add Queryables link
299-
if self.extension_is_enabled("FilterExtension") or self.extension_is_enabled(
300-
"SearchFilterExtension"
301-
):
302-
landing_page["links"].append(
303-
{
304-
"rel": Relations.queryables.value,
305-
"type": MimeTypes.jsonschema.value,
306-
"title": "Queryables",
307-
"href": urljoin(base_url, "queryables"),
308-
}
309-
)
310-
311-
# Add Aggregation links
312-
if self.extension_is_enabled("AggregationExtension"):
313-
landing_page["links"].extend(
314-
[
315-
{
316-
"rel": "aggregate",
317-
"type": "application/json",
318-
"title": "Aggregate",
319-
"href": urljoin(base_url, "aggregate"),
320-
},
321-
{
322-
"rel": "aggregations",
323-
"type": "application/json",
324-
"title": "Aggregations",
325-
"href": urljoin(base_url, "aggregations"),
326-
},
327-
]
328-
)
329-
330-
# Add OpenAPI URL
331-
landing_page["links"].append(
332-
{
333-
"rel": Relations.service_desc.value,
334-
"type": MimeTypes.openapi.value,
335-
"title": "OpenAPI service description",
336-
"href": str(request.url_for("openapi")),
337-
}
338-
)
288+
request: Request = kwargs["request"]
339289

340-
# Add human readable service-doc
341-
landing_page["links"].append(
342-
{
343-
"rel": Relations.service_doc.value,
344-
"type": MimeTypes.html.value,
345-
"title": "OpenAPI service documentation",
346-
"href": str(request.url_for("swagger_ui_html")),
347-
}
348-
)
349-
350-
landing = LandingPage(**landing_page)
290+
landing = await super().landing_page(**kwargs)
351291

352292
output_type: Optional[MimeTypes]
353293
if f:
@@ -476,6 +416,37 @@ async def get_collection(
476416

477417
return collection
478418

419+
async def get_item(
420+
self,
421+
item_id: str,
422+
collection_id: str,
423+
request: Request,
424+
f: Optional[str] = None,
425+
**kwargs,
426+
) -> Item:
427+
item = await super().get_item(item_id, collection_id, request, **kwargs)
428+
429+
output_type: Optional[MimeTypes]
430+
if f:
431+
output_type = MimeTypes[f]
432+
else:
433+
accepted_media = [MimeTypes[v] for v in get_args(GeoResponseType)]
434+
output_type = accept_media_type(
435+
request.headers.get("accept", ""), accepted_media
436+
)
437+
438+
if output_type == MimeTypes.html:
439+
return create_html_response(
440+
request,
441+
item,
442+
template_name="item",
443+
title=f"{collection_id}/{item_id} item",
444+
)
445+
446+
return item
447+
448+
# NOTE: We can't use `super.item_collection(...)` because of the `fields` extension
449+
# which, when used, might return a JSONResponse directly instead of a ItemCollection (TypeDict)
479450
async def item_collection(
480451
self,
481452
collection_id: str,
@@ -493,16 +464,6 @@ async def item_collection(
493464
f: Optional[str] = None,
494465
**kwargs,
495466
) -> ItemCollection:
496-
output_type: Optional[MimeTypes]
497-
if f:
498-
output_type = MimeTypes[f]
499-
else:
500-
accepted_media = [MimeTypes[v] for v in get_args(GeoMultiResponseType)]
501-
output_type = accept_media_type(
502-
request.headers.get("accept", ""), accepted_media
503-
)
504-
505-
# Check if collection exist
506467
await self.get_collection(collection_id, request=request)
507468

508469
base_args = {
@@ -521,12 +482,30 @@ async def item_collection(
521482
sortby=sortby,
522483
)
523484

524-
search_request = self.pgstac_search_model(**clean)
485+
try:
486+
search_request = self.pgstac_search_model(**clean)
487+
except ValidationError as e:
488+
raise HTTPException(
489+
status_code=400, detail=f"Invalid parameters provided {e}"
490+
) from e
491+
525492
item_collection = await self._search_base(search_request, request=request)
526493
item_collection["links"] = await ItemCollectionLinks(
527494
collection_id=collection_id, request=request
528495
).get_links(extra_links=item_collection["links"])
529496

497+
#######################################################################
498+
# Custom Responses
499+
#######################################################################
500+
output_type: Optional[MimeTypes]
501+
if f:
502+
output_type = MimeTypes[f]
503+
else:
504+
accepted_media = [MimeTypes[v] for v in get_args(GeoMultiResponseType)]
505+
output_type = accept_media_type(
506+
request.headers.get("accept", ""), accepted_media
507+
)
508+
530509
# Additional Headers for StreamingResponse
531510
additional_headers = {}
532511
links = item_collection.get("links", [])
@@ -581,45 +560,8 @@ async def item_collection(
581560

582561
return ItemCollection(**item_collection)
583562

584-
async def get_item(
585-
self,
586-
item_id: str,
587-
collection_id: str,
588-
request: Request,
589-
f: Optional[str] = None,
590-
**kwargs,
591-
) -> Item:
592-
output_type: Optional[MimeTypes]
593-
if f:
594-
output_type = MimeTypes[f]
595-
else:
596-
accepted_media = [MimeTypes[v] for v in get_args(GeoResponseType)]
597-
output_type = accept_media_type(
598-
request.headers.get("accept", ""), accepted_media
599-
)
600-
601-
# Check if collection exist
602-
await self.get_collection(collection_id, request=request)
603-
604-
search_request = self.pgstac_search_model(
605-
ids=[item_id], collections=[collection_id], limit=1
606-
)
607-
item_collection = await self._search_base(search_request, request=request)
608-
if not item_collection["features"]:
609-
raise NotFoundError(
610-
f"Item {item_id} in Collection {collection_id} does not exist."
611-
)
612-
613-
if output_type == MimeTypes.html:
614-
return create_html_response(
615-
request,
616-
item_collection["features"][0],
617-
template_name="item",
618-
title=f"{collection_id}/{item_id} item",
619-
)
620-
621-
return Item(**item_collection["features"][0])
622-
563+
# NOTE: We can't use `super.get_search(...)` because of the `fields` extension
564+
# which, when used, might return a JSONResponse directly instead of a ItemCollection (TypeDict)
623565
async def get_search(
624566
self,
625567
request: Request,
@@ -639,16 +581,6 @@ async def get_search(
639581
f: Optional[str] = None,
640582
**kwargs,
641583
) -> ItemCollection:
642-
output_type: Optional[MimeTypes]
643-
if f:
644-
output_type = MimeTypes[f]
645-
else:
646-
accepted_media = [MimeTypes[v] for v in get_args(GeoMultiResponseType)]
647-
output_type = accept_media_type(
648-
request.headers.get("accept", ""), accepted_media
649-
)
650-
651-
# Parse request parameters
652584
base_args = {
653585
"collections": collections,
654586
"ids": ids,
@@ -668,9 +600,27 @@ async def get_search(
668600
filter_lang=filter_lang,
669601
)
670602

671-
search_request = self.pgstac_search_model(**clean)
603+
try:
604+
search_request = self.pgstac_search_model(**clean)
605+
except ValidationError as e:
606+
raise HTTPException(
607+
status_code=400, detail=f"Invalid parameters provided {e}"
608+
) from e
609+
672610
item_collection = await self._search_base(search_request, request=request)
673611

612+
#######################################################################
613+
# Custom Responses
614+
#######################################################################
615+
output_type: Optional[MimeTypes]
616+
if f:
617+
output_type = MimeTypes[f]
618+
else:
619+
accepted_media = [MimeTypes[v] for v in get_args(GeoMultiResponseType)]
620+
output_type = accept_media_type(
621+
request.headers.get("accept", ""), accepted_media
622+
)
623+
674624
# Additional Headers for StreamingResponse
675625
additional_headers = {}
676626
links = item_collection.get("links", [])
@@ -720,19 +670,24 @@ async def get_search(
720670

721671
return ItemCollection(**item_collection)
722672

673+
# NOTE: We can't use `super.post_search(...)` because of the `fields` extension
674+
# which, when used, might return a JSONResponse directly instead of a ItemCollection (TypeDict)
723675
async def post_search(
724676
self,
725677
search_request: PgstacSearch,
726678
request: Request,
727679
**kwargs,
728680
) -> ItemCollection:
681+
item_collection = await self._search_base(search_request, request=request)
682+
683+
#######################################################################
684+
# Custom Responses
685+
#######################################################################
729686
accepted_media = [MimeTypes[v] for v in get_args(PostMultiResponseType)]
730687
output_type = accept_media_type(
731688
request.headers.get("accept", ""), accepted_media
732689
)
733690

734-
item_collection = await self._search_base(search_request, request=request)
735-
736691
# Additional Headers for StreamingResponse
737692
additional_headers = {}
738693
links = item_collection.get("links", [])

runtimes/eoapi/stac/eoapi/stac/config.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,21 +33,25 @@ def get_secret_dict(secret_name: str):
3333

3434

3535
class Settings(config.Settings):
36-
"""Extent stac-fastapi-pgstac settings"""
36+
"""Extent stac-fastapi-pgstac API settings"""
3737

3838
stac_fastapi_title: str = "eoAPI-stac"
3939
stac_fastapi_description: str = "Custom stac-fastapi application for eoAPI-Devseed"
4040
stac_fastapi_landing_id: str = "eoapi-devseed-stac"
4141

4242
cachecontrol: str = "public, max-age=3600"
4343

44-
pgstac_secret_arn: Optional[str] = None
45-
4644
titiler_endpoint: Optional[str] = None
4745
enable_transaction: bool = False
4846

4947
debug: bool = False
5048

49+
50+
class PostgresSettings(config.PostgresSettings):
51+
"""Extent stac-fastapi-pgstac PostgresSettings settings"""
52+
53+
pgstac_secret_arn: Optional[str] = None
54+
5155
@model_validator(mode="before")
5256
def get_postgres_setting(cls, data: Any) -> Any:
5357
if arn := data.get("pgstac_secret_arn"):

runtimes/eoapi/stac/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ classifiers = [
2020
]
2121
dynamic = ["version"]
2222
dependencies = [
23-
"stac-fastapi.pgstac>=4.0.2,<4.1",
23+
"stac-fastapi.pgstac>=5.0,<5.1",
2424
"jinja2>=2.11.2,<4.0.0",
2525
"starlette-cramjam>=0.4,<0.5",
2626
"psycopg_pool",

0 commit comments

Comments
 (0)