Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add HTML output for stac-api #17

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 201 additions & 0 deletions runtimes/eoapi/stac/eoapi/stac/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
"""eoapi.stac.api: custom StacAPI class."""

from typing import Type

import attr
from stac_fastapi.api import app
from stac_fastapi.api.models import APIRequest, GeoJSONResponse
from stac_fastapi.api.routes import create_async_endpoint
from stac_pydantic import api
from stac_pydantic.api.collections import Collections
from stac_pydantic.shared import MimeTypes

from .extensions import HTMLorJSONGetRequest


@attr.s
class StacApi(app.StacApi):
"""Custom StacAPI."""

landing_get_model: Type[APIRequest] = attr.ib(default=HTMLorJSONGetRequest)
conformance_get_model: Type[APIRequest] = attr.ib(default=HTMLorJSONGetRequest)

def register_landing_page(self):
"""Register landing page (GET /).

Returns:
None
"""
self.router.add_api_route(
name="Landing Page",
path="/",
response_model=(
api.LandingPage if self.settings.enable_response_models else None
),
responses={
200: {
"content": {
MimeTypes.json.value: {},
MimeTypes.html.value: {},
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's a shame but we almost need a custom StacApi class just to add the response content 🤷‍♂️

},
"model": api.LandingPage,
},
},
response_class=self.response_class,
response_model_exclude_unset=False,
response_model_exclude_none=True,
methods=["GET"],
endpoint=create_async_endpoint(
self.client.landing_page, self.landing_get_model
),
)

def register_conformance_classes(self):
"""Register conformance classes (GET /conformance).

Returns:
None
"""
self.router.add_api_route(
name="Conformance Classes",
path="/conformance",
response_model=(
api.Conformance if self.settings.enable_response_models else None
),
responses={
200: {
"content": {
MimeTypes.json.value: {},
MimeTypes.html.value: {},
},
"model": api.Conformance,
},
},
response_class=self.response_class,
response_model_exclude_unset=True,
response_model_exclude_none=True,
methods=["GET"],
endpoint=create_async_endpoint(
self.client.conformance, self.conformance_get_model
),
)

def register_get_collections(self):
"""Register get collections endpoint (GET /collections).

Returns:
None
"""
self.router.add_api_route(
name="Get Collections",
path="/collections",
response_model=(
Collections if self.settings.enable_response_models else None
),
responses={
200: {
"content": {
MimeTypes.json.value: {},
MimeTypes.html.value: {},
},
"model": Collections,
},
},
response_class=self.response_class,
response_model_exclude_unset=True,
response_model_exclude_none=True,
methods=["GET"],
endpoint=create_async_endpoint(
self.client.all_collections, self.collections_get_request_model
),
)

def register_get_collection(self):
"""Register get collection endpoint (GET /collection/{collection_id}).

Returns:
None
"""
self.router.add_api_route(
name="Get Collection",
path="/collections/{collection_id}",
response_model=api.Collection
if self.settings.enable_response_models
else None,
responses={
200: {
"content": {
MimeTypes.json.value: {},
MimeTypes.html.value: {},
},
"model": api.Collection,
},
},
response_class=self.response_class,
response_model_exclude_unset=True,
response_model_exclude_none=True,
methods=["GET"],
endpoint=create_async_endpoint(
self.client.get_collection, self.collection_get_request_model
),
)

def register_get_item_collection(self):
"""Register get item collection endpoint (GET /collection/{collection_id}/items).

Returns:
None
"""
self.router.add_api_route(
name="Get ItemCollection",
path="/collections/{collection_id}/items",
response_model=(
api.ItemCollection if self.settings.enable_response_models else None
),
responses={
200: {
"content": {
MimeTypes.geojson.value: {},
MimeTypes.html.value: {},
},
"model": api.ItemCollection,
},
},
response_class=GeoJSONResponse,
response_model_exclude_unset=True,
response_model_exclude_none=True,
methods=["GET"],
endpoint=create_async_endpoint(
self.client.item_collection, self.items_get_request_model
),
)

def register_get_search(self):
"""Register search endpoint (GET /search).

Returns:
None
"""
self.router.add_api_route(
name="Search",
path="/search",
response_model=api.ItemCollection
if self.settings.enable_response_models
else None,
responses={
200: {
"content": {
MimeTypes.geojson.value: {},
MimeTypes.html.value: {},
},
"model": api.ItemCollection,
},
},
response_class=GeoJSONResponse,
response_model_exclude_unset=True,
response_model_exclude_none=True,
methods=["GET"],
endpoint=create_async_endpoint(
self.client.get_search, self.search_get_request_model
),
)
44 changes: 35 additions & 9 deletions runtimes/eoapi/stac/eoapi/stac/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
from eoapi.auth_utils import OpenIdConnectAuth, OpenIdConnectSettings
from fastapi import FastAPI
from fastapi.responses import ORJSONResponse
from stac_fastapi.api.app import StacApi
from stac_fastapi.api.models import (
CollectionUri,
ItemCollectionUri,
ItemUri,
create_get_request_model,
create_post_request_model,
create_request_model,
Expand All @@ -19,9 +20,7 @@
CollectionSearchFilterExtension,
FieldsExtension,
FreeTextExtension,
ItemCollectionFilterExtension,
OffsetPaginationExtension,
SearchFilterExtension,
SortExtension,
TokenPaginationExtension,
)
Expand All @@ -31,7 +30,6 @@
from stac_fastapi.extensions.core.sort import SortConformanceClasses
from stac_fastapi.pgstac.db import close_db_connection, connect_to_db
from stac_fastapi.pgstac.extensions import QueryExtension
from stac_fastapi.pgstac.extensions.filter import FiltersClient
from stac_fastapi.pgstac.types.search import PgstacSearch
from starlette.middleware import Middleware
from starlette.middleware.cors import CORSMiddleware
Expand All @@ -41,9 +39,16 @@
from starlette_cramjam.middleware import CompressionMiddleware

from . import __version__ as eoapi_devseed_version
from .client import PgSTACClient
from .api import StacApi
from .client import FiltersClient, PgSTACClient
from .config import Settings
from .extension import TiTilerExtension
from .extensions import (
HTMLorGeoOutputExtension,
HTMLorJSONOutputExtension,
ItemCollectionFilterExtension,
SearchFilterExtension,
TiTilerExtension,
)
from .logs import init_logging

jinja2_env = jinja2.Environment(
Expand Down Expand Up @@ -77,8 +82,9 @@
QueryExtension(),
SortExtension(),
FieldsExtension(),
SearchFilterExtension(client=FiltersClient()),
SearchFilterExtension(client=FiltersClient()), # type: ignore
TokenPaginationExtension(),
HTMLorGeoOutputExtension(),
]

# collection_search extensions
Expand All @@ -91,6 +97,7 @@
conformance_classes=[FreeTextConformanceClasses.COLLECTIONS],
),
OffsetPaginationExtension(),
HTMLorJSONOutputExtension(),
]

# item_collection extensions
Expand All @@ -102,8 +109,9 @@
conformance_classes=[SortConformanceClasses.ITEMS],
),
FieldsExtension(conformance_classes=[FieldsConformanceClasses.ITEMS]),
ItemCollectionFilterExtension(client=FiltersClient()),
ItemCollectionFilterExtension(client=FiltersClient()), # type: ignore
TokenPaginationExtension(),
HTMLorGeoOutputExtension(),
]

# Request Models
Expand All @@ -128,6 +136,22 @@
collections_get_model = collection_search_extension.GET
application_extensions.append(collection_search_extension)

# /collections/{collectionId} model
collection_get_model = create_request_model(
model_name="CollectionUri",
base_model=CollectionUri,
extensions=[HTMLorJSONOutputExtension()],
request_type="GET",
)

# /collections/{collectionId}/items/itemId model
item_get_model = create_request_model(
model_name="ItemUri",
base_model=ItemUri,
extensions=[HTMLorGeoOutputExtension()],
request_type="GET",
Copy link
Member Author

@vincentsarago vincentsarago Feb 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: in subsequent PR we will add more output type support (e.g GeoParquet)

)


@asynccontextmanager
async def lifespan(app: FastAPI):
Expand Down Expand Up @@ -173,10 +197,12 @@ async def lifespan(app: FastAPI):
description=settings.stac_fastapi_description,
pgstac_search_model=search_post_model,
),
item_get_request_model=item_get_model,
items_get_request_model=items_get_model,
collection_get_request_model=collection_get_model,
collections_get_request_model=collections_get_model,
search_get_request_model=search_get_model,
search_post_request_model=search_post_model,
collections_get_request_model=collections_get_model,
response_class=ORJSONResponse,
middlewares=middlewares,
)
Expand Down
Loading