Skip to content

Commit d9570a0

Browse files
committed
Implement alternative backing for browsable features
1 parent edb5af9 commit d9570a0

File tree

9 files changed

+252
-5
lines changed

9 files changed

+252
-5
lines changed

docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ services:
5353
- GDAL_DISABLE_READDIR_ON_OPEN=EMPTY_DIR
5454
- DB_MIN_CONN_SIZE=1
5555
- DB_MAX_CONN_SIZE=1
56+
- BROWSABLE_HIERARCHY_DEFINITION=/app/stac_fastapi/testdata/joplin/hierarchy.json
5657
ports:
5758
- "8082:8082"
5859
volumes:

stac_fastapi/api/stac_fastapi/api/app.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from fastapi import APIRouter, FastAPI
77
from fastapi.openapi.utils import get_openapi
88
from pydantic import BaseModel
9-
from stac_pydantic import Children, Collection, Item, ItemCollection
9+
from stac_pydantic import Catalog, Collection, Item, ItemCollection
1010
from stac_pydantic.api import ConformanceClasses, LandingPage
1111
from stac_pydantic.api.collections import Collections
1212
from stac_pydantic.version import STAC_VERSION
@@ -15,6 +15,7 @@
1515
from stac_fastapi.api.errors import DEFAULT_STATUS_CODES, add_exception_handlers
1616
from stac_fastapi.api.models import (
1717
APIRequest,
18+
CatalogUri,
1819
CollectionUri,
1920
EmptyRequest,
2021
GeoJSONResponse,
@@ -31,6 +32,7 @@
3132
from stac_fastapi.types.core import AsyncBaseCoreClient, BaseCoreClient
3233
from stac_fastapi.types.extension import ApiExtension
3334
from stac_fastapi.types.search import BaseSearchGetRequest, BaseSearchPostRequest
35+
from stac_fastapi.types.stac import Children
3436

3537

3638
@attr.s
@@ -261,6 +263,25 @@ def register_get_collection(self):
261263
),
262264
)
263265

266+
def register_get_catalog(self):
267+
"""Register get collection endpoint (GET /catalog/{catalog_path}).
268+
269+
Returns:
270+
None
271+
"""
272+
self.router.add_api_route(
273+
name="Get Catalog",
274+
path="/catalogs/{catalog_path:path}",
275+
response_model=Catalog if self.settings.enable_response_models else None,
276+
response_class=self.response_class,
277+
response_model_exclude_unset=True,
278+
response_model_exclude_none=True,
279+
methods=["GET"],
280+
endpoint=self._create_endpoint(
281+
self.client.get_catalog, CatalogUri, self.response_class
282+
),
283+
)
284+
264285
def register_get_collection_children(self):
265286
"""Register get collection children endpoint (GET /collection/{collection_id}/children).
266287
@@ -332,6 +353,7 @@ def register_core(self):
332353
self.register_get_collections()
333354
self.register_get_collection()
334355
self.register_get_collection_children()
356+
self.register_get_catalog()
335357
self.register_get_item_collection()
336358

337359
def customize_openapi(self) -> Optional[Dict[str, Any]]:

stac_fastapi/api/stac_fastapi/api/models.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,16 +97,23 @@ def create_post_request_model(
9797
)
9898

9999

100+
@attr.s # type:ignore
101+
class CatalogUri(APIRequest):
102+
"""Catalog Path."""
103+
104+
catalog_path: str = attr.ib(default=Path(..., description="Catalog Path"))
105+
106+
100107
@attr.s # type:ignore
101108
class CollectionUri(APIRequest):
102-
"""Delete collection."""
109+
"""Collection URI."""
103110

104111
collection_id: str = attr.ib(default=Path(..., description="Collection ID"))
105112

106113

107114
@attr.s
108115
class ItemUri(CollectionUri):
109-
"""Delete item."""
116+
"""Item URI."""
110117

111118
item_id: str = attr.ib(default=Path(..., description="Item ID"))
112119

stac_fastapi/pgstac/stac_fastapi/pgstac/app.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
"""FastAPI application using PGStac."""
2+
import json
3+
24
from fastapi.responses import ORJSONResponse
35

46
from stac_fastapi.api.app import StacApi
@@ -16,6 +18,7 @@
1618
from stac_fastapi.pgstac.extensions import QueryExtension
1719
from stac_fastapi.pgstac.transactions import TransactionsClient
1820
from stac_fastapi.pgstac.types.search import PgstacSearch
21+
from stac_fastapi.types.hierarchy import BrowsableNode, parse_hierarchy
1922

2023
settings = Settings()
2124
extensions = [
@@ -30,13 +33,18 @@
3033
TokenPaginationExtension(),
3134
ContextExtension(),
3235
]
36+
with open(settings.browsable_hierarchy_definition, "r") as definition_file:
37+
hierarchy_json = json.load(definition_file)
38+
hierarchy_definition: BrowsableNode = parse_hierarchy(hierarchy_json)
3339

3440
post_request_model = create_post_request_model(extensions, base_model=PgstacSearch)
3541

3642
api = StacApi(
3743
settings=settings,
3844
extensions=extensions,
39-
client=CoreCrudClient(post_request_model=post_request_model),
45+
client=CoreCrudClient(
46+
post_request_model=post_request_model, hierarchy_definition=hierarchy_definition
47+
),
4048
response_class=ORJSONResponse,
4149
search_get_request_model=create_get_request_model(extensions),
4250
search_post_request_model=post_request_model,

stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/app.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
"""FastAPI application."""
2+
import json
3+
24
from stac_fastapi.api.app import StacApi
35
from stac_fastapi.api.models import create_get_request_model, create_post_request_model
46
from stac_fastapi.extensions.core import (
@@ -17,6 +19,7 @@
1719
BulkTransactionsClient,
1820
TransactionsClient,
1921
)
22+
from stac_fastapi.types.hierarchy import BrowsableNode, parse_hierarchy
2023

2124
settings = SqlalchemySettings()
2225
session = Session.create_from_settings(settings)
@@ -29,14 +32,20 @@
2932
TokenPaginationExtension(),
3033
ContextExtension(),
3134
]
35+
with open(settings.browsable_hierarchy_definition, "r") as definition_file:
36+
hierarchy_json = json.load(definition_file)
37+
hierarchy_definition: BrowsableNode = parse_hierarchy(hierarchy_json)
3238

3339
post_request_model = create_post_request_model(extensions)
3440

3541
api = StacApi(
3642
settings=settings,
3743
extensions=extensions,
3844
client=CoreCrudClient(
39-
session=session, extensions=extensions, post_request_model=post_request_model
45+
session=session,
46+
extensions=extensions,
47+
post_request_model=post_request_model,
48+
hierarchy_definition=hierarchy_definition,
4049
),
4150
search_get_request_model=create_get_request_model(extensions),
4251
search_post_request_model=post_request_model,
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"children": [{
3+
"catalog_id": "joplin",
4+
"title": "Joplin Item Catalog",
5+
"description": "All joplin items",
6+
"children": [],
7+
"items": [
8+
["joplin", "fe916452-ba6f-4631-9154-c249924a122d"],
9+
["joplin", "f7f164c9-cfdf-436d-a3f0-69864c38ba2a"],
10+
["joplin", "f734401c-2df0-4694-a353-cdd3ea760cdc"],
11+
["joplin", "f490b7af-0019-45e2-854b-3854d07fd063"],
12+
["joplin", "f2cca2a3-288b-4518-8a3e-a4492bb60b08"],
13+
["joplin", "ea0fddf4-56f9-4a16-8a0b-f6b0b123b7cf"],
14+
["joplin", "e0a02e4e-aa0c-412e-8f63-6f5344f829df"],
15+
["joplin", "da6ef938-c58f-4bab-9d4e-89f6ae667da2"],
16+
["joplin", "d8461d8c-3d2b-4e4e-a931-7ae61ca06dbf"],
17+
["joplin", "d4eccfa2-7d77-4624-9e2a-3f59102285bb"]
18+
]
19+
}]
20+
}

stac_fastapi/types/stac_fastapi/types/config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ class ApiSettings(BaseSettings):
2929
openapi_url: str = "/api"
3030
docs_url: str = "/api.html"
3131

32+
# Path to JSON which defines the browsable hierarchy pending backend implementations
33+
browsable_hierarchy_definition: Optional[str] = None
34+
3235
class Config:
3336
"""model config (https://pydantic-docs.helpmanual.io/usage/model_config/)."""
3437

stac_fastapi/types/stac_fastapi/types/core.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@
1414
from stac_fastapi.types import stac as stac_types
1515
from stac_fastapi.types.conformance import BASE_CONFORMANCE_CLASSES
1616
from stac_fastapi.types.extension import ApiExtension
17+
from stac_fastapi.types.hierarchy import (
18+
BrowsableNode,
19+
CatalogNode,
20+
CollectionNode,
21+
browsable_catalog,
22+
browsable_child_link,
23+
browsable_item_link,
24+
)
1725
from stac_fastapi.types.search import BaseSearchPostRequest
1826
from stac_fastapi.types.stac import Conformance
1927

@@ -302,6 +310,7 @@ class BaseCoreClient(LandingPageMixin, abc.ABC):
302310
)
303311
extensions: List[ApiExtension] = attr.ib(default=attr.Factory(list))
304312
post_request_model = attr.ib(default=BaseSearchPostRequest)
313+
hierarchy_definition: Optional[BrowsableNode] = attr.ib(default=None)
305314

306315
def conformance_classes(self) -> List[str]:
307316
"""Generate conformance classes by adding extension conformance to base conformance classes."""
@@ -360,6 +369,20 @@ def landing_page(self, **kwargs) -> stac_types.LandingPage:
360369
}
361370
)
362371

372+
# Add links for browsable conformance
373+
if self.hierarchy_definition is not None:
374+
for child in self.hierarchy_definition["children"]:
375+
if isinstance(child, CollectionNode):
376+
landing_page["links"].append(
377+
browsable_child_link(child, urljoin(base_url, "collections"))
378+
)
379+
if isinstance(child, CatalogNode):
380+
landing_page["links"].append(
381+
browsable_child_link(child, urljoin(base_url, "catalogs"))
382+
)
383+
for item in self.hierarchy_definition["items"]:
384+
landing_page["links"].append(browsable_item_link(item, base_url))
385+
363386
# Add OpenAPI URL
364387
landing_page["links"].append(
365388
{
@@ -487,6 +510,23 @@ def get_collection_children(
487510
"""
488511
...
489512

513+
def get_catalog(self, catalog_path: str, **kwargs) -> stac_types.Catalog:
514+
"""Get collection by id.
515+
516+
Called with `GET /catalogs/{catalog_path}`.
517+
518+
Args:
519+
catalog_path: The full path of the catalog in the browsable hierarchy.
520+
521+
Returns:
522+
Catalog.
523+
"""
524+
split_path = catalog_path.split("/")
525+
remaining_hierarchy = self.hierarchy_definition
526+
for fork in split_path:
527+
remaining_hierarchy = remaining_hierarchy[fork]
528+
return browsable_catalog(catalog_path)
529+
490530
@abc.abstractmethod
491531
def item_collection(
492532
self, collection_id: str, limit: int = 10, token: str = None, **kwargs
@@ -519,6 +559,7 @@ class AsyncBaseCoreClient(LandingPageMixin, abc.ABC):
519559
)
520560
extensions: List[ApiExtension] = attr.ib(default=attr.Factory(list))
521561
post_request_model = attr.ib(default=BaseSearchPostRequest)
562+
hierarchy_definition: Optional[BrowsableNode] = attr.ib(default=None)
522563

523564
def conformance_classes(self) -> List[str]:
524565
"""Generate conformance classes by adding extension conformance to base conformance classes."""
@@ -552,6 +593,8 @@ async def landing_page(self, **kwargs) -> stac_types.LandingPage:
552593
conformance_classes=self.conformance_classes(),
553594
extension_schemas=extension_schemas,
554595
)
596+
597+
# Add Collections links
555598
collections = await self.all_collections(request=kwargs["request"])
556599
for collection in collections["collections"]:
557600
landing_page["links"].append(
@@ -563,6 +606,20 @@ async def landing_page(self, **kwargs) -> stac_types.LandingPage:
563606
}
564607
)
565608

609+
# Add links for browsable conformance
610+
if self.hierarchy_definition is not None:
611+
for child in self.hierarchy_definition["children"]:
612+
if "collection_id" in child:
613+
landing_page["links"].append(
614+
browsable_child_link(child, urljoin(base_url, "collections"))
615+
)
616+
if "catalog_id" in child:
617+
landing_page["links"].append(
618+
browsable_child_link(child, urljoin(base_url, "catalogs"))
619+
)
620+
for item in self.hierarchy_definition["items"]:
621+
landing_page["links"].append(browsable_item_link(item, base_url))
622+
566623
# Add OpenAPI URL
567624
landing_page["links"].append(
568625
{
@@ -694,6 +751,29 @@ async def get_collection_children(
694751
"""
695752
...
696753

754+
async def get_catalog(self, catalog_path: str, **kwargs) -> stac_types.Catalog:
755+
"""Get collection by id.
756+
757+
Called with `GET /catalogs/{catalog_path}`.
758+
759+
Args:
760+
catalog_path: The full path of the catalog in the browsable hierarchy.
761+
762+
Returns:
763+
Catalog.
764+
"""
765+
request: Request = kwargs["request"]
766+
base_url = str(request.base_url)
767+
split_path = catalog_path.split("/")
768+
remaining_hierarchy = self.hierarchy_definition
769+
for fork in split_path:
770+
remaining_hierarchy = next(
771+
node
772+
for node in remaining_hierarchy["children"]
773+
if node["catalog_id"] == fork
774+
)
775+
return browsable_catalog(remaining_hierarchy, base_url).dict(exclude_unset=True)
776+
697777
@abc.abstractmethod
698778
async def item_collection(
699779
self, collection_id: str, limit: int = 10, token: str = None, **kwargs

0 commit comments

Comments
 (0)