|
7 | 7 | import attr |
8 | 8 | from fastapi import APIRouter, FastAPI, HTTPException, Query, Request |
9 | 9 | from fastapi.responses import JSONResponse |
| 10 | +from stac_pydantic import Collection |
10 | 11 | from starlette.responses import Response |
11 | 12 | from typing_extensions import TypedDict |
12 | 13 |
|
13 | 14 | from stac_fastapi.core.models import Catalog |
| 15 | +from stac_fastapi.sfeos_helpers.mappings import COLLECTIONS_INDEX |
14 | 16 | from stac_fastapi.types import stac as stac_types |
15 | 17 | from stac_fastapi.types.core import BaseCoreClient |
16 | 18 | from stac_fastapi.types.extension import ApiExtension |
@@ -101,6 +103,19 @@ def register(self, app: FastAPI, settings=None) -> None: |
101 | 103 | tags=["Catalogs"], |
102 | 104 | ) |
103 | 105 |
|
| 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 | + |
104 | 119 | # Add endpoint for getting a specific collection in a catalog |
105 | 120 | self.router.add_api_route( |
106 | 121 | path="/catalogs/{catalog_id}/collections/{collection_id}", |
@@ -289,9 +304,18 @@ async def get_catalog_collections( |
289 | 304 | base_path = urlparse(base_url).path.rstrip("/") |
290 | 305 |
|
291 | 306 | 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"]: |
293 | 313 | # 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 | + ) |
295 | 319 | if href: |
296 | 320 | try: |
297 | 321 | parsed_url = urlparse(href) |
@@ -380,11 +404,191 @@ async def get_catalog_collections( |
380 | 404 | ], |
381 | 405 | ) |
382 | 406 |
|
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 | + ) |
384 | 415 | raise HTTPException( |
385 | 416 | status_code=404, detail=f"Catalog {catalog_id} not found" |
386 | 417 | ) |
387 | 418 |
|
| 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 | + |
388 | 592 | async def get_catalog_collection( |
389 | 593 | self, catalog_id: str, collection_id: str, request: Request |
390 | 594 | ) -> stac_types.Collection: |
|
0 commit comments