Skip to content

Commit e516bd5

Browse files
committed
route scratch
1 parent 7724f2f commit e516bd5

File tree

6 files changed

+575
-25
lines changed

6 files changed

+575
-25
lines changed

stac_fastapi/core/stac_fastapi/core/base_database_logic.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,39 @@ async def delete_collection(
138138
) -> None:
139139
"""Delete a collection from the database."""
140140
pass
141+
142+
@abc.abstractmethod
143+
async def get_all_catalogs(
144+
self,
145+
token: Optional[str],
146+
limit: int,
147+
request: Any = None,
148+
sort: Optional[List[Dict[str, Any]]] = None,
149+
) -> Tuple[List[Dict[str, Any]], Optional[str], Optional[int]]:
150+
"""Retrieve a list of catalogs from the database, supporting pagination.
151+
152+
Args:
153+
token (Optional[str]): The pagination token.
154+
limit (int): The number of results to return.
155+
request (Any, optional): The FastAPI request object. Defaults to None.
156+
sort (Optional[List[Dict[str, Any]]], optional): Optional sort parameter. Defaults to None.
157+
158+
Returns:
159+
A tuple of (catalogs, next pagination token if any, optional count).
160+
"""
161+
pass
162+
163+
@abc.abstractmethod
164+
async def create_catalog(self, catalog: Dict, refresh: bool = False) -> None:
165+
"""Create a catalog in the database."""
166+
pass
167+
168+
@abc.abstractmethod
169+
async def find_catalog(self, catalog_id: str) -> Dict:
170+
"""Find a catalog in the database."""
171+
pass
172+
173+
@abc.abstractmethod
174+
async def delete_catalog(self, catalog_id: str, refresh: bool = False) -> None:
175+
"""Delete a catalog from the database."""
176+
pass

stac_fastapi/core/stac_fastapi/core/core.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,14 @@
2323
from stac_fastapi.core.base_database_logic import BaseDatabaseLogic
2424
from stac_fastapi.core.base_settings import ApiBaseSettings
2525
from stac_fastapi.core.datetime_utils import format_datetime_range
26+
from stac_fastapi.core.models import Catalog
2627
from stac_fastapi.core.models.links import PagingLinks
2728
from stac_fastapi.core.redis_utils import redis_pagination_links
28-
from stac_fastapi.core.serializers import CollectionSerializer, ItemSerializer
29+
from stac_fastapi.core.serializers import (
30+
CatalogSerializer,
31+
CollectionSerializer,
32+
ItemSerializer,
33+
)
2934
from stac_fastapi.core.session import Session
3035
from stac_fastapi.core.utilities import filter_fields, get_bool_env
3136
from stac_fastapi.extensions.core.transaction import AsyncBaseTransactionsClient
@@ -82,6 +87,7 @@ class CoreClient(AsyncBaseCoreClient):
8287
collection_serializer: Type[CollectionSerializer] = attr.ib(
8388
default=CollectionSerializer
8489
)
90+
catalog_serializer: Type[CatalogSerializer] = attr.ib(default=CatalogSerializer)
8591
post_request_model = attr.ib(default=BaseSearchPostRequest)
8692
stac_version: str = attr.ib(default=STAC_VERSION)
8793
landing_page_id: str = attr.ib(default="stac-fastapi")
@@ -142,15 +148,24 @@ def _landing_page(
142148
)
143149
return landing_page
144150

145-
async def landing_page(self, **kwargs) -> stac_types.LandingPage:
151+
async def landing_page(self, **kwargs) -> Union[stac_types.LandingPage, Catalog]:
146152
"""Landing page.
147153
148154
Called with `GET /`.
149155
150156
Returns:
151-
API landing page, serving as an entry point to the API.
157+
API landing page, serving as an entry point to the API, or root catalog if CatalogsExtension is enabled.
152158
"""
153159
request: Request = kwargs["request"]
160+
161+
# If CatalogsExtension is enabled, return root catalog instead of landing page
162+
if self.extension_is_enabled("CatalogsExtension"):
163+
# Find the CatalogsExtension and call its catalogs method
164+
for extension in self.extensions:
165+
if extension.__class__.__name__ == "CatalogsExtension":
166+
return await extension.catalogs(request)
167+
168+
# Normal landing page logic
154169
base_url = get_base_url(request)
155170
landing_page = self._landing_page(
156171
base_url=base_url,
Lines changed: 211 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
11
"""Catalogs extension."""
22

3-
from typing import List, Type, Union
3+
from typing import List, Type
44

55
import attr
6-
from fastapi import APIRouter, FastAPI, Request
6+
from fastapi import APIRouter, FastAPI, HTTPException, Request
77
from fastapi.responses import JSONResponse
88
from starlette.responses import Response
99

10+
from stac_fastapi.core.models import Catalog
11+
from stac_fastapi.types import stac as stac_types
1012
from stac_fastapi.types.core import BaseCoreClient
1113
from stac_fastapi.types.extension import ApiExtension
12-
from stac_fastapi.types.stac import LandingPage
1314

1415

1516
@attr.s
1617
class CatalogsExtension(ApiExtension):
1718
"""Catalogs Extension.
1819
19-
The Catalogs extension adds a /catalogs endpoint that returns the root catalog.
20+
The Catalogs extension adds a /catalogs endpoint that returns the root catalog
21+
containing child links to all catalogs in the database.
2022
"""
2123

2224
client: BaseCoreClient = attr.ib(default=None)
@@ -25,40 +27,229 @@ class CatalogsExtension(ApiExtension):
2527
router: APIRouter = attr.ib(default=attr.Factory(APIRouter))
2628
response_class: Type[Response] = attr.ib(default=JSONResponse)
2729

28-
def register(self, app: FastAPI) -> None:
30+
def register(self, app: FastAPI, settings=None) -> None:
2931
"""Register the extension with a FastAPI application.
3032
3133
Args:
3234
app: target FastAPI application.
33-
34-
Returns:
35-
None
35+
settings: extension settings (unused for now).
3636
"""
37-
response_model = (
38-
self.settings.get("response_model")
39-
if isinstance(self.settings, dict)
40-
else getattr(self.settings, "response_model", None)
41-
)
37+
self.settings = settings or {}
4238

4339
self.router.add_api_route(
4440
path="/catalogs",
4541
endpoint=self.catalogs,
4642
methods=["GET"],
47-
response_model=LandingPage if response_model else None,
43+
response_model=Catalog,
44+
response_class=self.response_class,
45+
summary="Get Root Catalog",
46+
description="Returns the root catalog containing links to all catalogs.",
47+
tags=["Catalogs"],
48+
)
49+
50+
# Add endpoint for creating catalogs
51+
self.router.add_api_route(
52+
path="/catalogs",
53+
endpoint=self.create_catalog,
54+
methods=["POST"],
55+
response_model=Catalog,
56+
response_class=self.response_class,
57+
status_code=201,
58+
summary="Create Catalog",
59+
description="Create a new STAC catalog.",
60+
tags=["Catalogs"],
61+
)
62+
63+
# Add endpoint for getting individual catalogs
64+
self.router.add_api_route(
65+
path="/catalogs/{catalog_id}",
66+
endpoint=self.get_catalog,
67+
methods=["GET"],
68+
response_model=Catalog,
4869
response_class=self.response_class,
49-
summary="Get Catalogs",
50-
description="Returns the root catalog.",
70+
summary="Get Catalog",
71+
description="Get a specific STAC catalog by ID.",
5172
tags=["Catalogs"],
5273
)
74+
75+
# Add endpoint for getting collections in a catalog
76+
self.router.add_api_route(
77+
path="/catalogs/{catalog_id}/collections",
78+
endpoint=self.get_catalog_collections,
79+
methods=["GET"],
80+
response_model=stac_types.Collections,
81+
response_class=self.response_class,
82+
summary="Get Catalog Collections",
83+
description="Get collections linked from a specific catalog.",
84+
tags=["Catalogs"],
85+
)
86+
5387
app.include_router(self.router, tags=["Catalogs"])
5488

55-
async def catalogs(self, request: Request) -> Union[LandingPage, Response]:
56-
"""Get catalogs.
89+
async def catalogs(self, request: Request) -> Catalog:
90+
"""Get root catalog with links to all catalogs.
5791
5892
Args:
5993
request: Request object.
6094
6195
Returns:
62-
The root catalog (landing page).
96+
Root catalog containing child links to all catalogs in the database.
6397
"""
64-
return await self.client.landing_page(request=request)
98+
base_url = str(request.base_url)
99+
100+
# Get all catalogs from database
101+
catalogs, _, _ = await self.client.database.get_all_catalogs(
102+
token=None,
103+
limit=1000, # Large limit to get all catalogs
104+
request=request,
105+
sort=[{"field": "id", "direction": "asc"}],
106+
)
107+
108+
# Create child links to each catalog
109+
child_links = []
110+
for catalog in catalogs:
111+
child_links.append(
112+
{
113+
"rel": "child",
114+
"href": f"{base_url}catalogs/{catalog.id}",
115+
"type": "application/json",
116+
"title": catalog.title or catalog.id,
117+
}
118+
)
119+
120+
# Create root catalog
121+
root_catalog = {
122+
"type": "Catalog",
123+
"stac_version": "1.0.0",
124+
"id": "root",
125+
"title": "Root Catalog",
126+
"description": "Root catalog containing all available catalogs",
127+
"links": [
128+
{
129+
"rel": "self",
130+
"href": f"{base_url}catalogs",
131+
"type": "application/json",
132+
},
133+
{
134+
"rel": "root",
135+
"href": f"{base_url}catalogs",
136+
"type": "application/json",
137+
},
138+
{
139+
"rel": "parent",
140+
"href": base_url.rstrip("/"),
141+
"type": "application/json",
142+
},
143+
]
144+
+ child_links,
145+
}
146+
147+
return Catalog(**root_catalog)
148+
149+
async def create_catalog(self, catalog: Catalog, request: Request) -> Catalog:
150+
"""Create a new catalog.
151+
152+
Args:
153+
catalog: The catalog to create.
154+
request: Request object.
155+
156+
Returns:
157+
The created catalog.
158+
"""
159+
# Convert STAC catalog to database format
160+
db_catalog = self.client.catalog_serializer.stac_to_db(catalog, request)
161+
162+
# Create the catalog in the database
163+
await self.client.database.create_catalog(db_catalog.model_dump())
164+
165+
# Return the created catalog
166+
return catalog
167+
168+
async def get_catalog(self, catalog_id: str, request: Request) -> Catalog:
169+
"""Get a specific catalog by ID.
170+
171+
Args:
172+
catalog_id: The ID of the catalog to retrieve.
173+
request: Request object.
174+
175+
Returns:
176+
The requested catalog.
177+
"""
178+
try:
179+
# Get the catalog from the database
180+
db_catalog = await self.client.database.find_catalog(catalog_id)
181+
182+
# Convert to STAC format
183+
catalog = self.client.catalog_serializer.db_to_stac(db_catalog, request)
184+
185+
return catalog
186+
except Exception:
187+
raise HTTPException(
188+
status_code=404, detail=f"Catalog {catalog_id} not found"
189+
)
190+
191+
async def get_catalog_collections(
192+
self, catalog_id: str, request: Request
193+
) -> stac_types.Collections:
194+
"""Get collections linked from a specific catalog.
195+
196+
Args:
197+
catalog_id: The ID of the catalog.
198+
request: Request object.
199+
200+
Returns:
201+
Collections object containing collections linked from the catalog.
202+
"""
203+
try:
204+
# Get the catalog from the database
205+
db_catalog = await self.client.database.find_catalog(catalog_id)
206+
207+
# Convert to STAC format to access links
208+
catalog = self.client.catalog_serializer.db_to_stac(db_catalog, request)
209+
210+
# Extract collection IDs from catalog links
211+
collection_ids = []
212+
if hasattr(catalog, "links") and catalog.links:
213+
for link in catalog.links:
214+
if link.get("rel") in ["child", "item"]:
215+
# Extract collection ID from href
216+
href = link.get("href", "")
217+
# Look for patterns like /collections/{id} or collections/{id}
218+
if "/collections/" in href:
219+
collection_id = href.split("/collections/")[-1].split("/")[
220+
0
221+
]
222+
if collection_id and collection_id not in collection_ids:
223+
collection_ids.append(collection_id)
224+
225+
# Fetch the collections
226+
collections = []
227+
for coll_id in collection_ids:
228+
try:
229+
collection = await self.client.get_collection(
230+
coll_id, request=request
231+
)
232+
collections.append(collection)
233+
except Exception:
234+
# Skip collections that can't be found
235+
continue
236+
237+
# Return in Collections format
238+
base_url = str(request.base_url)
239+
return stac_types.Collections(
240+
collections=collections,
241+
links=[
242+
{"rel": "root", "type": "application/json", "href": base_url},
243+
{"rel": "parent", "type": "application/json", "href": base_url},
244+
{
245+
"rel": "self",
246+
"type": "application/json",
247+
"href": f"{base_url}catalogs/{catalog_id}/collections",
248+
},
249+
],
250+
)
251+
252+
except Exception:
253+
raise HTTPException(
254+
status_code=404, detail=f"Catalog {catalog_id} not found"
255+
)
Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,27 @@
1-
"""stac_fastapi.elasticsearch.models module."""
1+
"""STAC models."""
2+
3+
from typing import Any, Dict, List, Optional
4+
5+
from pydantic import BaseModel
6+
7+
8+
class Catalog(BaseModel):
9+
"""STAC Catalog model."""
10+
11+
type: str = "Catalog"
12+
stac_version: str
13+
id: str
14+
title: Optional[str] = None
15+
description: Optional[str] = None
16+
links: List[Dict[str, Any]]
17+
stac_extensions: Optional[List[str]] = None
18+
19+
20+
class PartialCatalog(BaseModel):
21+
"""Partial STAC Catalog model for updates."""
22+
23+
id: str
24+
title: Optional[str] = None
25+
description: Optional[str] = None
26+
links: Optional[List[Dict[str, Any]]] = None
27+
stac_extensions: Optional[List[str]] = None

0 commit comments

Comments
 (0)