Skip to content

Commit d823914

Browse files
committed
post collection to catalog
1 parent 6e9a653 commit d823914

File tree

5 files changed

+324
-17
lines changed

5 files changed

+324
-17
lines changed

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ This implementation follows the [STAC API Catalogs Extension](https://github.com
249249
- **POST `/catalogs`**: Create a new catalog (requires appropriate permissions)
250250
- **GET `/catalogs/{catalog_id}`**: Retrieve a specific catalog and its children
251251
- **GET `/catalogs/{catalog_id}/collections`**: Retrieve collections within a specific catalog
252+
- **POST `/catalogs/{catalog_id}/collections`**: Create a new collection within a specific catalog
252253
- **GET `/catalogs/{catalog_id}/collections/{collection_id}`**: Retrieve a specific collection within a catalog
253254
- **GET `/catalogs/{catalog_id}/collections/{collection_id}/items`**: Retrieve items within a collection in a catalog context
254255
- **GET `/catalogs/{catalog_id}/collections/{collection_id}/items/{item_id}`**: Retrieve a specific item within a catalog context
@@ -265,6 +266,22 @@ curl "http://localhost:8081/catalogs/earth-observation"
265266
# Get collections in a catalog
266267
curl "http://localhost:8081/catalogs/earth-observation/collections"
267268

269+
# Create a new collection within a catalog
270+
curl -X POST "http://localhost:8081/catalogs/earth-observation/collections" \
271+
-H "Content-Type: application/json" \
272+
-d '{
273+
"id": "landsat-9",
274+
"type": "Collection",
275+
"stac_version": "1.0.0",
276+
"description": "Landsat 9 satellite imagery collection",
277+
"title": "Landsat 9",
278+
"license": "MIT",
279+
"extent": {
280+
"spatial": {"bbox": [[-180, -90, 180, 90]]},
281+
"temporal": {"interval": [["2021-09-27T00:00:00Z", null]]}
282+
}
283+
}'
284+
268285
# Get specific collection within a catalog
269286
curl "http://localhost:8081/catalogs/earth-observation/collections/sentinel-2"
270287

stac_fastapi/core/stac_fastapi/core/extensions/catalogs.py

Lines changed: 207 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77
import attr
88
from fastapi import APIRouter, FastAPI, HTTPException, Query, Request
99
from fastapi.responses import JSONResponse
10+
from stac_pydantic import Collection
1011
from starlette.responses import Response
1112
from typing_extensions import TypedDict
1213

1314
from stac_fastapi.core.models import Catalog
15+
from stac_fastapi.sfeos_helpers.mappings import COLLECTIONS_INDEX
1416
from stac_fastapi.types import stac as stac_types
1517
from stac_fastapi.types.core import BaseCoreClient
1618
from stac_fastapi.types.extension import ApiExtension
@@ -101,6 +103,19 @@ def register(self, app: FastAPI, settings=None) -> None:
101103
tags=["Catalogs"],
102104
)
103105

106+
# Add endpoint for creating collections in a catalog
107+
self.router.add_api_route(
108+
path="/catalogs/{catalog_id}/collections",
109+
endpoint=self.create_catalog_collection,
110+
methods=["POST"],
111+
response_model=stac_types.Collection,
112+
response_class=self.response_class,
113+
status_code=201,
114+
summary="Create Catalog Collection",
115+
description="Create a new collection and link it to a specific catalog.",
116+
tags=["Catalogs"],
117+
)
118+
104119
# Add endpoint for getting a specific collection in a catalog
105120
self.router.add_api_route(
106121
path="/catalogs/{catalog_id}/collections/{collection_id}",
@@ -289,9 +304,18 @@ async def get_catalog_collections(
289304
base_path = urlparse(base_url).path.rstrip("/")
290305

291306
for link in catalog.links:
292-
if link.get("rel") in ["child", "item"]:
307+
rel = (
308+
link.get("rel")
309+
if hasattr(link, "get")
310+
else getattr(link, "rel", None)
311+
)
312+
if rel in ["child", "item"]:
293313
# Extract collection ID from href using proper URL parsing
294-
href = link.get("href", "")
314+
href = (
315+
link.get("href", "")
316+
if hasattr(link, "get")
317+
else getattr(link, "href", "")
318+
)
295319
if href:
296320
try:
297321
parsed_url = urlparse(href)
@@ -380,11 +404,191 @@ async def get_catalog_collections(
380404
],
381405
)
382406

383-
except Exception:
407+
except HTTPException:
408+
# Re-raise HTTP exceptions as-is
409+
raise
410+
except Exception as e:
411+
logger.error(
412+
f"Error retrieving collections for catalog {catalog_id}: {e}",
413+
exc_info=True,
414+
)
384415
raise HTTPException(
385416
status_code=404, detail=f"Catalog {catalog_id} not found"
386417
)
387418

419+
async def create_catalog_collection(
420+
self, catalog_id: str, collection: Collection, request: Request
421+
) -> stac_types.Collection:
422+
"""Create a new collection and link it to a specific catalog.
423+
424+
Args:
425+
catalog_id: The ID of the catalog to link the collection to.
426+
collection: The collection to create.
427+
request: Request object.
428+
429+
Returns:
430+
The created collection.
431+
432+
Raises:
433+
HTTPException: If the catalog is not found or collection creation fails.
434+
"""
435+
try:
436+
# Verify the catalog exists
437+
await self.client.database.find_catalog(catalog_id)
438+
439+
# Create the collection using the same pattern as TransactionsClient.create_collection
440+
# This handles the Collection model from stac_pydantic correctly
441+
collection_dict = collection.model_dump(mode="json")
442+
443+
# Add a link from the collection back to its parent catalog BEFORE saving to database
444+
base_url = str(request.base_url)
445+
catalog_link = {
446+
"rel": "catalog",
447+
"type": "application/json",
448+
"href": f"{base_url}catalogs/{catalog_id}",
449+
"title": catalog_id,
450+
}
451+
452+
# Add the catalog link to the collection dict
453+
if "links" not in collection_dict:
454+
collection_dict["links"] = []
455+
456+
# Check if the catalog link already exists
457+
catalog_href = catalog_link["href"]
458+
link_exists = any(
459+
link.get("href") == catalog_href and link.get("rel") == "catalog"
460+
for link in collection_dict.get("links", [])
461+
)
462+
463+
if not link_exists:
464+
collection_dict["links"].append(catalog_link)
465+
466+
# Now convert to database format (this will process the links)
467+
collection_db = self.client.database.collection_serializer.stac_to_db(
468+
collection_dict, request
469+
)
470+
await self.client.database.create_collection(
471+
collection=collection_db, refresh=True
472+
)
473+
474+
# Convert back to STAC format for the response
475+
created_collection = self.client.database.collection_serializer.db_to_stac(
476+
collection_db,
477+
request,
478+
extensions=[
479+
type(ext).__name__ for ext in self.client.database.extensions
480+
],
481+
)
482+
483+
# Update the catalog to include a link to the new collection
484+
await self._add_collection_to_catalog_links(
485+
catalog_id, collection.id, request
486+
)
487+
488+
return created_collection
489+
490+
except HTTPException as e:
491+
# Re-raise HTTP exceptions (e.g., catalog not found, collection validation errors)
492+
raise e
493+
except Exception as e:
494+
# Check if this is a "not found" error from find_catalog
495+
error_msg = str(e)
496+
if "not found" in error_msg.lower():
497+
raise HTTPException(status_code=404, detail=error_msg)
498+
499+
# Handle unexpected errors
500+
logger.error(f"Error creating collection in catalog {catalog_id}: {e}")
501+
raise HTTPException(
502+
status_code=500,
503+
detail=f"Failed to create collection in catalog: {str(e)}",
504+
)
505+
506+
async def _add_collection_to_catalog_links(
507+
self, catalog_id: str, collection_id: str, request: Request
508+
) -> None:
509+
"""Add a collection link to a catalog.
510+
511+
This helper method updates a catalog's links to include a reference
512+
to a collection by reindexing the updated catalog document.
513+
514+
Args:
515+
catalog_id: The ID of the catalog to update.
516+
collection_id: The ID of the collection to link.
517+
request: Request object for base URL construction.
518+
"""
519+
try:
520+
# Get the current catalog
521+
db_catalog = await self.client.database.find_catalog(catalog_id)
522+
catalog = self.client.catalog_serializer.db_to_stac(db_catalog, request)
523+
524+
# Create the collection link
525+
base_url = str(request.base_url)
526+
collection_link = {
527+
"rel": "child",
528+
"href": f"{base_url}collections/{collection_id}",
529+
"type": "application/json",
530+
"title": collection_id,
531+
}
532+
533+
# Add the link to the catalog if it doesn't already exist
534+
catalog_links = (
535+
catalog.get("links")
536+
if isinstance(catalog, dict)
537+
else getattr(catalog, "links", None)
538+
)
539+
if not catalog_links:
540+
catalog_links = []
541+
if isinstance(catalog, dict):
542+
catalog["links"] = catalog_links
543+
else:
544+
catalog.links = catalog_links
545+
546+
# Check if the collection link already exists
547+
collection_href = collection_link["href"]
548+
link_exists = any(
549+
(
550+
link.get("href")
551+
if hasattr(link, "get")
552+
else getattr(link, "href", None)
553+
)
554+
== collection_href
555+
for link in catalog_links
556+
)
557+
558+
if not link_exists:
559+
catalog_links.append(collection_link)
560+
561+
# Update the catalog in the database by reindexing it
562+
# Convert back to database format
563+
updated_db_catalog = self.client.catalog_serializer.stac_to_db(
564+
catalog, request
565+
)
566+
updated_db_catalog_dict = (
567+
updated_db_catalog.model_dump()
568+
if hasattr(updated_db_catalog, "model_dump")
569+
else updated_db_catalog
570+
)
571+
updated_db_catalog_dict["type"] = "Catalog"
572+
573+
# Use the same approach as create_catalog to update the document
574+
await self.client.database.client.index(
575+
index=COLLECTIONS_INDEX,
576+
id=catalog_id,
577+
body=updated_db_catalog_dict,
578+
refresh=True,
579+
)
580+
581+
logger.info(
582+
f"Updated catalog {catalog_id} to include link to collection {collection_id}"
583+
)
584+
585+
except Exception as e:
586+
logger.error(
587+
f"Failed to update catalog {catalog_id} links: {e}", exc_info=True
588+
)
589+
# Don't fail the entire operation if link update fails
590+
# The collection was created successfully, just the catalog link is missing
591+
388592
async def get_catalog_collection(
389593
self, catalog_id: str, collection_id: str, request: Request
390594
) -> stac_types.Collection:

stac_fastapi/tests/api/test_api.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
"POST /catalogs",
5555
"GET /catalogs/{catalog_id}",
5656
"GET /catalogs/{catalog_id}/collections",
57+
"POST /catalogs/{catalog_id}/collections",
5758
"GET /catalogs/{catalog_id}/collections/{collection_id}",
5859
"GET /catalogs/{catalog_id}/collections/{collection_id}/items",
5960
"GET /catalogs/{catalog_id}/collections/{collection_id}/items/{item_id}",

stac_fastapi/tests/data/test_catalog.json

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,9 @@
1717
},
1818
{
1919
"rel": "child",
20-
"href": "http://test-server/collections/test-collection-1",
20+
"href": "http://test-server/collections/placeholder-collection",
2121
"type": "application/json",
22-
"title": "Test Collection 1"
23-
},
24-
{
25-
"rel": "child",
26-
"href": "http://test-server/collections/test-collection-2",
27-
"type": "application/json",
28-
"title": "Test Collection 2"
29-
},
30-
{
31-
"rel": "child",
32-
"href": "http://test-server/collections/test-collection-3",
33-
"type": "application/json",
34-
"title": "Test Collection 3"
22+
"title": "Placeholder Collection"
3523
}
3624
]
3725
}

0 commit comments

Comments
 (0)