+
diff --git a/runtimes/eoapi/stac/eoapi/stac/templates/item.html b/runtimes/eoapi/stac/eoapi/stac/templates/item.html new file mode 100644 index 0000000..f61b009 --- /dev/null +++ b/runtimes/eoapi/stac/eoapi/stac/templates/item.html @@ -0,0 +1,101 @@ +{% include "header.html" %} +{% if params %} + {% set urlq = url + '?' + params + '&' %} + {% else %} + {% set urlq = url + '?' %} +{% endif %} + + + +

Collection Item: {{ response.id }}

+ +
+
+

Properties

+
    +
  • ID: {{ response.id }}
  • + {% for key, value in response.properties.items() %} +
  • {{ key }}: {{ value }}
  • + {% endfor %} +
+
+
+
Loading...
+
+
+ + + +{% include "footer.html" %} diff --git a/runtimes/eoapi/stac/eoapi/stac/templates/items.html b/runtimes/eoapi/stac/eoapi/stac/templates/items.html new file mode 100644 index 0000000..d1d84f2 --- /dev/null +++ b/runtimes/eoapi/stac/eoapi/stac/templates/items.html @@ -0,0 +1,153 @@ +{% include "header.html" %} + +{% set show_prev_link = false %} +{% set show_next_link = false %} +{% if params %} + {% set urlq = url + '?' + params + '&' %} + {% else %} + {% set urlq = url + '?' %} +{% endif %} + + + + +

Collection Items: {{ response.title or response.id }}

+ +
Loading...
+ +

+ Number of matching items: {{ response.numberMatched }}
+ Number of returned items: {{ response.numberReturned }}
+ Page: of
+

+ +
+ {% for link in response.links %} + {% if link.rel == 'prev' %} + + {% endif %} + {% endfor %} +
+ +
+ {% for link in response.links %} + {% if link.rel == 'next' %} + + {% endif %} + {% endfor %} +
+
+{% if response.features is defined and response.features|length > 0 %} + + + +{% for key, value in response.features.0.properties.items() %} + +{% endfor %} + + +{% for feature in response.features %} + + + {% for key, value in feature.properties.items() %} + + {% endfor %} + +{% endfor %} + +
ID{{ key }}
{{ feature.id }}{{ value }}
+{% endif %} +
+ + + +{% include "footer.html" %} diff --git a/runtimes/eoapi/stac/eoapi/stac/templates/landing.html b/runtimes/eoapi/stac/eoapi/stac/templates/landing.html new file mode 100644 index 0000000..9aa2886 --- /dev/null +++ b/runtimes/eoapi/stac/eoapi/stac/templates/landing.html @@ -0,0 +1,33 @@ +{% include "header.html" %} +{% if params %} + {% set urlq = url + '?' + params + '&' %} + {% else %} + {% set urlq = url + '?' %} +{% endif %} + + + +

{{ response.title }}

+

+ {{ response.description }} +

+ +

Links

+ + +{% include "footer.html" %} diff --git a/runtimes/eoapi/stac/eoapi/stac/templates/queryables.html b/runtimes/eoapi/stac/eoapi/stac/templates/queryables.html new file mode 100644 index 0000000..66b8020 --- /dev/null +++ b/runtimes/eoapi/stac/eoapi/stac/templates/queryables.html @@ -0,0 +1,38 @@ +{% include "header.html" %} +{% if params %} + {% set urlq = url + '?' + params + '&' %} + {% else %} + {% set urlq = url + '?' %} +{% endif %} + + + +

Collection: {{ response.title or response.id }}

+ +
+
+

Queryables

+
    + {% for k,v in response.properties.items() %} +
  • {% if '$ref' in v %} + {{ k }} + {% else %} + {{ k }}: {{ v['type'] }} + {% endif %} + {% endfor %} +
+
+
+ +{% include "footer.html" %} diff --git a/runtimes/eoapi/stac/eoapi/stac/templates/search.html b/runtimes/eoapi/stac/eoapi/stac/templates/search.html new file mode 100644 index 0000000..55b736a --- /dev/null +++ b/runtimes/eoapi/stac/eoapi/stac/templates/search.html @@ -0,0 +1,153 @@ +{% include "header.html" %} + +{% set show_prev_link = false %} +{% set show_next_link = false %} +{% if params %} + {% set urlq = url + '?' + params + '&' %} + {% else %} + {% set urlq = url + '?' %} +{% endif %} + + + + +

Search

+ +
Loading...
+ +

+ Number of matching items: {{ response.numberMatched }}
+ Number of returned items: {{ response.numberReturned }}
+ Page: of
+

+ +
+ {% for link in response.links %} + {% if link.rel == 'prev' %} + + {% endif %} + {% endfor %} +
+ +
+ {% for link in response.links %} + {% if link.rel == 'next' %} + + {% endif %} + {% endfor %} +
+
+{% if response.features is defined and response.features|length > 0 %} + + + +{% for key, value in response.features.0.properties.items() %} + +{% endfor %} + + +{% for feature in response.features %} + + + {% for key, value in feature.properties.items() %} + + {% endfor %} + +{% endfor %} + +
ID{{ key }}
{{ feature.id }}{{ value }}
+{% endif %} +
+ + + +{% include "footer.html" %} From cc76952ff42dc75640e403c35764d3c3537f948b Mon Sep 17 00:00:00 2001 From: vincentsarago Date: Sat, 8 Feb 2025 22:05:45 +0100 Subject: [PATCH 2/2] update from main --- runtimes/eoapi/stac/eoapi/stac/api.py | 201 ++++++++++ runtimes/eoapi/stac/eoapi/stac/app.py | 44 ++- runtimes/eoapi/stac/eoapi/stac/client.py | 364 +++++++++++++++++- .../stac/{extension.py => extensions.py} | 130 ++++++- .../stac/eoapi/stac/templates/header.html | 1 + .../stac/eoapi/stac/templates/items.html | 23 +- .../stac/eoapi/stac/templates/landing.html | 2 +- .../stac/eoapi/stac/templates/queryables.html | 2 +- .../stac/eoapi/stac/templates/search.html | 23 +- 9 files changed, 731 insertions(+), 59 deletions(-) create mode 100644 runtimes/eoapi/stac/eoapi/stac/api.py rename runtimes/eoapi/stac/eoapi/stac/{extension.py => extensions.py} (51%) diff --git a/runtimes/eoapi/stac/eoapi/stac/api.py b/runtimes/eoapi/stac/eoapi/stac/api.py new file mode 100644 index 0000000..0fef3d1 --- /dev/null +++ b/runtimes/eoapi/stac/eoapi/stac/api.py @@ -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: {}, + }, + "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 + ), + ) diff --git a/runtimes/eoapi/stac/eoapi/stac/app.py b/runtimes/eoapi/stac/eoapi/stac/app.py index af8e3f1..1d2b555 100644 --- a/runtimes/eoapi/stac/eoapi/stac/app.py +++ b/runtimes/eoapi/stac/eoapi/stac/app.py @@ -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, @@ -19,9 +20,7 @@ CollectionSearchFilterExtension, FieldsExtension, FreeTextExtension, - ItemCollectionFilterExtension, OffsetPaginationExtension, - SearchFilterExtension, SortExtension, TokenPaginationExtension, ) @@ -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 @@ -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( @@ -77,8 +82,9 @@ QueryExtension(), SortExtension(), FieldsExtension(), - SearchFilterExtension(client=FiltersClient()), + SearchFilterExtension(client=FiltersClient()), # type: ignore TokenPaginationExtension(), + HTMLorGeoOutputExtension(), ] # collection_search extensions @@ -91,6 +97,7 @@ conformance_classes=[FreeTextConformanceClasses.COLLECTIONS], ), OffsetPaginationExtension(), + HTMLorJSONOutputExtension(), ] # item_collection extensions @@ -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 @@ -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", +) + @asynccontextmanager async def lifespan(app: FastAPI): @@ -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, ) diff --git a/runtimes/eoapi/stac/eoapi/stac/client.py b/runtimes/eoapi/stac/eoapi/stac/client.py index 35bd301..bfee9d3 100644 --- a/runtimes/eoapi/stac/eoapi/stac/client.py +++ b/runtimes/eoapi/stac/eoapi/stac/client.py @@ -1,16 +1,175 @@ """eoapi-devseed: Custom pgstac client.""" -from typing import Type +import re +from typing import Any, Dict, List, Literal, Optional, Type, get_args from urllib.parse import urljoin import attr +import jinja2 from fastapi import Request from stac_fastapi.pgstac.core import CoreCrudClient +from stac_fastapi.pgstac.extensions.filter import FiltersClient as PgSTACFiltersClient from stac_fastapi.pgstac.types.search import PgstacSearch from stac_fastapi.types.requests import get_base_url -from stac_fastapi.types.stac import LandingPage +from stac_fastapi.types.stac import ( + Collection, + Collections, + Conformance, + Item, + ItemCollection, + LandingPage, +) from stac_pydantic.links import Relations from stac_pydantic.shared import MimeTypes +from starlette.templating import Jinja2Templates, _TemplateResponse + +ResponseType = Literal["json", "html"] +GeoResponseType = Literal["geojson", "html"] +QueryablesResponseType = Literal["jsonschema", "html"] + + +jinja2_env = jinja2.Environment( + loader=jinja2.ChoiceLoader([jinja2.PackageLoader(__package__, "templates")]) +) +DEFAULT_TEMPLATES = Jinja2Templates(env=jinja2_env) + + +def accept_media_type(accept: str, mediatypes: List[MimeTypes]) -> Optional[MimeTypes]: + """Return MediaType based on accept header and available mediatype. + + Links: + - https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html + - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept + + """ + accept_values = {} + for m in accept.replace(" ", "").split(","): + values = m.split(";") + if len(values) == 1: + name = values[0] + quality = 1.0 + else: + name = values[0] + groups = dict([param.split("=") for param in values[1:]]) # type: ignore + try: + q = groups.get("q") + quality = float(q) if q else 1.0 + except ValueError: + quality = 0 + + # if quality is 0 we ignore encoding + if quality: + accept_values[name] = quality + + # Create Preference matrix + media_preference = { + v: [n for (n, q) in accept_values.items() if q == v] + for v in sorted(set(accept_values.values()), reverse=True) + } + + # Loop through available compression and encoding preference + for _, pref in media_preference.items(): + for media in mediatypes: + if media.value in pref: + return media + + # If no specified encoding is supported but "*" is accepted, + # take one of the available compressions. + if "*" in accept_values and mediatypes: + return mediatypes[0] + + return None + + +def create_html_response( + request: Request, + data: Any, + template_name: str, + title: Optional[str] = None, + router_prefix: Optional[str] = None, + **kwargs: Any, +) -> _TemplateResponse: + """Create Template response.""" + + router_prefix = request.app.state.router_prefix + + urlpath = request.url.path + if root_path := request.app.root_path: + urlpath = re.sub(r"^" + root_path, "", urlpath) + + if router_prefix: + urlpath = re.sub(r"^" + router_prefix, "", urlpath) + + crumbs = [] + baseurl = str(request.base_url).rstrip("/") + + if router_prefix: + baseurl += router_prefix + + crumbpath = str(baseurl) + if urlpath == "/": + urlpath = "" + + for crumb in urlpath.split("/"): + crumbpath = crumbpath.rstrip("/") + part = crumb + if part is None or part == "": + part = "Home" + crumbpath += f"/{crumb}" + crumbs.append({"url": crumbpath.rstrip("/"), "part": part.capitalize()}) + + return DEFAULT_TEMPLATES.TemplateResponse( + request, + name=f"{template_name}.html", + context={ + "response": data, + "template": { + "api_root": baseurl, + "params": request.query_params, + "title": title or template_name, + }, + "crumbs": crumbs, + "url": baseurl + urlpath, + "params": str(request.url.query), + **kwargs, + }, + ) + + +@attr.s +class FiltersClient(PgSTACFiltersClient): + async def get_queryables( + self, + request: Request, + collection_id: Optional[str] = None, + *args, + f: Optional[str] = None, + **kwargs: Any, + ) -> Dict[str, Any]: + """Get the queryables available for the given collection_id.""" + + queryables = await super().get_queryables( + request, collection_id, *args, **kwargs + ) + + output_type: Optional[MimeTypes] + if f: + output_type = MimeTypes[f] + else: + accepted_media = [MimeTypes[v] for v in get_args(QueryablesResponseType)] + output_type = accept_media_type( + request.headers.get("accept", ""), accepted_media + ) + + if output_type == MimeTypes.html: + return create_html_response( + request, + queryables, + template_name="queryables", + title=f"{collection_id} queryables", + ) + + return queryables @attr.s @@ -20,6 +179,7 @@ class PgSTACClient(CoreCrudClient): async def landing_page( self, request: Request, + f: Optional[str] = None, **kwargs, ) -> LandingPage: """Landing page. @@ -90,4 +250,202 @@ async def landing_page( } ) - return LandingPage(**landing_page) + landing = LandingPage(**landing_page) + + output_type: Optional[MimeTypes] + if f: + output_type = MimeTypes[f] + else: + accepted_media = [MimeTypes[v] for v in get_args(ResponseType)] + output_type = accept_media_type( + request.headers.get("accept", ""), accepted_media + ) + + if output_type == MimeTypes.html: + return create_html_response( + request, + landing, + template_name="landing", + title=landing["title"], + ) + + return landing + + async def conformance( + self, + request: Request, + f: Optional[str] = None, + **kwargs, + ) -> Conformance: + """Conformance classes. + + Called with `GET /conformance`. + + Returns: + Conformance classes which the server conforms to. + """ + conforms_to = Conformance(conformsTo=self.conformance_classes()) + + output_type: Optional[MimeTypes] + if f: + output_type = MimeTypes[f] + else: + accepted_media = [MimeTypes[v] for v in get_args(ResponseType)] + output_type = accept_media_type( + request.headers.get("accept", ""), accepted_media + ) + + if output_type == MimeTypes.html: + return create_html_response( + request, + conforms_to, + template_name="conformance", + ) + + return conforms_to + + async def all_collections( + self, + request: Request, + *args, + f: Optional[str] = None, + **kwargs, + ) -> Collections: + collections = await super().all_collections(request, *args, **kwargs) + + output_type: Optional[MimeTypes] + if f: + output_type = MimeTypes[f] + else: + accepted_media = [MimeTypes[v] for v in get_args(ResponseType)] + output_type = accept_media_type( + request.headers.get("accept", ""), accepted_media + ) + + if output_type == MimeTypes.html: + return create_html_response( + request, + collections, + template_name="collections", + title="Collections list", + ) + + return collections + + async def get_collection( + self, + collection_id: str, + request: Request, + *args, + f: Optional[str] = None, + **kwargs, + ) -> Collection: + collection = await super().get_collection( + collection_id, request, *args, **kwargs + ) + + output_type: Optional[MimeTypes] + if f: + output_type = MimeTypes[f] + else: + accepted_media = [MimeTypes[v] for v in get_args(ResponseType)] + output_type = accept_media_type( + request.headers.get("accept", ""), accepted_media + ) + + if output_type == MimeTypes.html: + return create_html_response( + request, + collection, + template_name="collection", + title=f"{collection_id} collection", + ) + + return collection + + async def item_collection( + self, + collection_id: str, + request: Request, + *args, + f: Optional[str] = None, + **kwargs, + ) -> ItemCollection: + items = await super().item_collection(collection_id, request, *args, **kwargs) + + output_type: Optional[MimeTypes] + if f: + output_type = MimeTypes[f] + else: + accepted_media = [MimeTypes[v] for v in get_args(GeoResponseType)] + output_type = accept_media_type( + request.headers.get("accept", ""), accepted_media + ) + + if output_type == MimeTypes.html: + items["id"] = collection_id + return create_html_response( + request, + items, + template_name="items", + title=f"{collection_id} items", + ) + + return items + + async def get_item( + self, + item_id: str, + collection_id: str, + request: Request, + *args, + f: Optional[str] = None, + **kwargs, + ) -> Item: + item = await super().get_item(item_id, collection_id, request, *args, **kwargs) + + output_type: Optional[MimeTypes] + if f: + output_type = MimeTypes[f] + else: + accepted_media = [MimeTypes[v] for v in get_args(GeoResponseType)] + output_type = accept_media_type( + request.headers.get("accept", ""), accepted_media + ) + + if output_type == MimeTypes.html: + return create_html_response( + request, + item, + template_name="item", + title=f"{collection_id}/{item_id} item", + ) + + return item + + async def get_search( + self, + request: Request, + *args, + f: Optional[str] = None, + **kwargs, + ) -> ItemCollection: + items = await super().get_search(request, *args, **kwargs) + + output_type: Optional[MimeTypes] + if f: + output_type = MimeTypes[f] + else: + accepted_media = [MimeTypes[v] for v in get_args(GeoResponseType)] + output_type = accept_media_type( + request.headers.get("accept", ""), accepted_media + ) + + if output_type == MimeTypes.html: + return create_html_response( + request, + items, + template_name="search", + ) + + return items diff --git a/runtimes/eoapi/stac/eoapi/stac/extension.py b/runtimes/eoapi/stac/eoapi/stac/extensions.py similarity index 51% rename from runtimes/eoapi/stac/eoapi/stac/extension.py rename to runtimes/eoapi/stac/eoapi/stac/extensions.py index 180fcd7..db4a276 100644 --- a/runtimes/eoapi/stac/eoapi/stac/extension.py +++ b/runtimes/eoapi/stac/eoapi/stac/extensions.py @@ -1,12 +1,17 @@ """TiTiler extension.""" -from typing import Optional +from typing import Annotated, Literal, Optional from urllib.parse import urlencode import attr from fastapi import APIRouter, FastAPI, HTTPException, Path, Query from fastapi.responses import RedirectResponse +from stac_fastapi.api.models import CollectionUri +from stac_fastapi.api.routes import create_async_endpoint +from stac_fastapi.extensions import core from stac_fastapi.types.extension import ApiExtension +from stac_fastapi.types.search import APIRequest +from stac_pydantic.shared import MimeTypes from starlette.requests import Request @@ -104,3 +109,126 @@ async def stac_viewer( return RedirectResponse(url) app.include_router(self.router, tags=["TiTiler Extension"]) + + +@attr.s +class HTMLorJSONGetRequest(APIRequest): + """HTML or JSON output.""" + + f: Annotated[ + Optional[Literal["json", "html"]], + Query(description="Response MediaType."), + ] = attr.ib(default=None) + + +@attr.s +class HTMLorGeoGetRequest(APIRequest): + """HTML or GeoJSON output.""" + + f: Annotated[ + Optional[Literal["geojson", "html"]], + Query(description="Response MediaType."), + ] = attr.ib(default=None) + + +@attr.s(kw_only=True) +class HTMLorJSONOutputExtension(ApiExtension): + """TiTiler extension.""" + + GET = HTMLorJSONGetRequest + POST = None + + def register(self, app: FastAPI) -> None: + pass + + +@attr.s(kw_only=True) +class HTMLorGeoOutputExtension(ApiExtension): + """TiTiler extension.""" + + GET = HTMLorGeoGetRequest + POST = None + + def register(self, app: FastAPI) -> None: + pass + + +@attr.s(kw_only=True) +class HTMLorSchemaGetRequest(APIRequest): + f: Annotated[ + Optional[Literal["jsonschema", "html"]], + Query(description="Response MediaType."), + ] = attr.ib(default=None) + + +@attr.s(kw_only=True) +class CollectionFilterGetRequestModel(CollectionUri, HTMLorSchemaGetRequest): + pass + + +@attr.s +class SearchFilterExtension(core.SearchFilterExtension): + """Item Search Filter Extension.""" + + def register(self, app: FastAPI) -> None: + """Register the extension with a FastAPI application. + + Args: + app: target FastAPI application. + + Returns: + None + """ + self.router.prefix = app.state.router_prefix + self.router.add_api_route( + name="Queryables", + path="/queryables", + methods=["GET"], + responses={ + 200: { + "content": { + MimeTypes.jsonschema.value: {}, + MimeTypes.html.value: {}, + }, + }, + }, + response_class=self.response_class, + endpoint=create_async_endpoint( + self.client.get_queryables, HTMLorSchemaGetRequest + ), + ) + app.include_router(self.router, tags=["Filter Extension"]) + + +@attr.s +class ItemCollectionFilterExtension(core.ItemCollectionFilterExtension): + """Item Collection Filter Extension.""" + + def register(self, app: FastAPI) -> None: + """Register the extension with a FastAPI application. + + Args: + app: target FastAPI application. + + Returns: + None + """ + self.router.prefix = app.state.router_prefix + self.router.add_api_route( + name="Collection Queryables", + path="/collections/{collection_id}/queryables", + methods=["GET"], + responses={ + 200: { + "content": { + MimeTypes.jsonschema.value: {}, + MimeTypes.html.value: {}, + }, + }, + }, + response_class=self.response_class, + endpoint=create_async_endpoint( + self.client.get_queryables, CollectionFilterGetRequestModel + ), + ) + app.include_router(self.router, tags=["Filter Extension"]) diff --git a/runtimes/eoapi/stac/eoapi/stac/templates/header.html b/runtimes/eoapi/stac/eoapi/stac/templates/header.html index 3723779..0d9706b 100644 --- a/runtimes/eoapi/stac/eoapi/stac/templates/header.html +++ b/runtimes/eoapi/stac/eoapi/stac/templates/header.html @@ -34,6 +34,7 @@ Home Conformance Collections + API Documentation
diff --git a/runtimes/eoapi/stac/eoapi/stac/templates/items.html b/runtimes/eoapi/stac/eoapi/stac/templates/items.html index d1d84f2..7b24f49 100644 --- a/runtimes/eoapi/stac/eoapi/stac/templates/items.html +++ b/runtimes/eoapi/stac/eoapi/stac/templates/items.html @@ -29,12 +29,11 @@

Collection Items: {{ response.title or response.id }}

Number of matching items: {{ response.numberMatched }}
Number of returned items: {{ response.numberReturned }}
- Page: of

{% for link in response.links %} - {% if link.rel == 'prev' %} + {% if link.rel == 'previous' %} {% endif %} {% endfor %} @@ -121,26 +120,6 @@

Collection Items: {{ response.title or response.id }}

document.getElementById("map").style.display = "none"; } - // - // paging - // - var offset = 0; // defaults - var limit = 10; - - {% if "offset" in template.params %} - offset = {{ template.params.offset }}; - {% endif %} - {% if "limit" in template.params %} - limit = {{ template.params.limit }}; - {% endif %} - - var matched = (geojson.numberMatched) ? geojson.numberMatched : 10; - - var current_page = (offset + limit)/limit; - $("#current_page").html(current_page); - var total_pages = Math.ceil(matched/ limit); - $("#total_pages").html(total_pages); - // // event handling // diff --git a/runtimes/eoapi/stac/eoapi/stac/templates/landing.html b/runtimes/eoapi/stac/eoapi/stac/templates/landing.html index 9aa2886..6458ea0 100644 --- a/runtimes/eoapi/stac/eoapi/stac/templates/landing.html +++ b/runtimes/eoapi/stac/eoapi/stac/templates/landing.html @@ -26,7 +26,7 @@

{{ response.title }}

Links

diff --git a/runtimes/eoapi/stac/eoapi/stac/templates/queryables.html b/runtimes/eoapi/stac/eoapi/stac/templates/queryables.html index 66b8020..d3b9210 100644 --- a/runtimes/eoapi/stac/eoapi/stac/templates/queryables.html +++ b/runtimes/eoapi/stac/eoapi/stac/templates/queryables.html @@ -14,7 +14,7 @@ {% endif %} {% endfor %} - + diff --git a/runtimes/eoapi/stac/eoapi/stac/templates/search.html b/runtimes/eoapi/stac/eoapi/stac/templates/search.html index 55b736a..20bb086 100644 --- a/runtimes/eoapi/stac/eoapi/stac/templates/search.html +++ b/runtimes/eoapi/stac/eoapi/stac/templates/search.html @@ -29,12 +29,11 @@

Search

Number of matching items: {{ response.numberMatched }}
Number of returned items: {{ response.numberReturned }}
- Page: of

{% for link in response.links %} - {% if link.rel == 'prev' %} + {% if link.rel == 'previous' %} {% endif %} {% endfor %} @@ -121,26 +120,6 @@

Search

document.getElementById("map").style.display = "none"; } - // - // paging - // - var offset = 0; // defaults - var limit = 10; - - {% if "offset" in template.params %} - offset = {{ template.params.offset }}; - {% endif %} - {% if "limit" in template.params %} - limit = {{ template.params.limit }}; - {% endif %} - - var matched = (geojson.numberMatched) ? geojson.numberMatched : 10; - - var current_page = (offset + limit)/limit; - $("#current_page").html(current_page); - var total_pages = Math.ceil(matched/ limit); - $("#total_pages").html(total_pages); - // // event handling //