stac-fastapi-core 6.7.6__py3-none-any.whl → 6.8.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- stac_fastapi/core/base_database_logic.py +40 -0
- stac_fastapi/core/core.py +43 -2
- stac_fastapi/core/extensions/__init__.py +2 -0
- stac_fastapi/core/extensions/catalogs.py +980 -0
- stac_fastapi/core/models/__init__.py +27 -1
- stac_fastapi/core/models/links.py +9 -2
- stac_fastapi/core/queryables.py +105 -0
- stac_fastapi/core/serializers.py +147 -1
- stac_fastapi/core/version.py +1 -1
- {stac_fastapi_core-6.7.6.dist-info → stac_fastapi_core-6.8.1.dist-info}/METADATA +2 -2
- {stac_fastapi_core-6.7.6.dist-info → stac_fastapi_core-6.8.1.dist-info}/RECORD +12 -10
- {stac_fastapi_core-6.7.6.dist-info → stac_fastapi_core-6.8.1.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,980 @@
|
|
|
1
|
+
"""Catalogs extension."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any, Dict, List, Optional, Type
|
|
5
|
+
from urllib.parse import parse_qs, urlencode, urlparse
|
|
6
|
+
|
|
7
|
+
import attr
|
|
8
|
+
from fastapi import APIRouter, FastAPI, HTTPException, Query, Request
|
|
9
|
+
from fastapi.responses import JSONResponse
|
|
10
|
+
from stac_pydantic import Collection
|
|
11
|
+
from starlette.responses import Response
|
|
12
|
+
from typing_extensions import TypedDict
|
|
13
|
+
|
|
14
|
+
from stac_fastapi.core.models import Catalog
|
|
15
|
+
from stac_fastapi.sfeos_helpers.mappings import COLLECTIONS_INDEX
|
|
16
|
+
from stac_fastapi.types import stac as stac_types
|
|
17
|
+
from stac_fastapi.types.core import BaseCoreClient
|
|
18
|
+
from stac_fastapi.types.extension import ApiExtension
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Catalogs(TypedDict, total=False):
|
|
24
|
+
"""Catalogs endpoint response.
|
|
25
|
+
|
|
26
|
+
Similar to Collections but for catalogs.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
catalogs: List[Catalog]
|
|
30
|
+
links: List[dict]
|
|
31
|
+
numberMatched: Optional[int]
|
|
32
|
+
numberReturned: Optional[int]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@attr.s
|
|
36
|
+
class CatalogsExtension(ApiExtension):
|
|
37
|
+
"""Catalogs Extension.
|
|
38
|
+
|
|
39
|
+
The Catalogs extension adds a /catalogs endpoint that returns a list of all catalogs
|
|
40
|
+
in the database, similar to how /collections returns a list of collections.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
client: BaseCoreClient = attr.ib(default=None)
|
|
44
|
+
settings: dict = attr.ib(default=attr.Factory(dict))
|
|
45
|
+
conformance_classes: List[str] = attr.ib(
|
|
46
|
+
default=attr.Factory(lambda: ["https://api.stacspec.org/v1.0.0-rc.2/children"])
|
|
47
|
+
)
|
|
48
|
+
router: APIRouter = attr.ib(default=attr.Factory(APIRouter))
|
|
49
|
+
response_class: Type[Response] = attr.ib(default=JSONResponse)
|
|
50
|
+
|
|
51
|
+
def register(self, app: FastAPI, settings=None) -> None:
|
|
52
|
+
"""Register the extension with a FastAPI application.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
app: target FastAPI application.
|
|
56
|
+
settings: extension settings (unused for now).
|
|
57
|
+
"""
|
|
58
|
+
self.settings = settings or {}
|
|
59
|
+
|
|
60
|
+
self.router.add_api_route(
|
|
61
|
+
path="/catalogs",
|
|
62
|
+
endpoint=self.catalogs,
|
|
63
|
+
methods=["GET"],
|
|
64
|
+
response_model=Catalogs,
|
|
65
|
+
response_class=self.response_class,
|
|
66
|
+
summary="Get All Catalogs",
|
|
67
|
+
description="Returns a list of all catalogs in the database.",
|
|
68
|
+
tags=["Catalogs"],
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Add endpoint for creating catalogs
|
|
72
|
+
self.router.add_api_route(
|
|
73
|
+
path="/catalogs",
|
|
74
|
+
endpoint=self.create_catalog,
|
|
75
|
+
methods=["POST"],
|
|
76
|
+
response_model=Catalog,
|
|
77
|
+
response_class=self.response_class,
|
|
78
|
+
status_code=201,
|
|
79
|
+
summary="Create Catalog",
|
|
80
|
+
description="Create a new STAC catalog.",
|
|
81
|
+
tags=["Catalogs"],
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Add endpoint for getting individual catalogs
|
|
85
|
+
self.router.add_api_route(
|
|
86
|
+
path="/catalogs/{catalog_id}",
|
|
87
|
+
endpoint=self.get_catalog,
|
|
88
|
+
methods=["GET"],
|
|
89
|
+
response_model=Catalog,
|
|
90
|
+
response_class=self.response_class,
|
|
91
|
+
summary="Get Catalog",
|
|
92
|
+
description="Get a specific STAC catalog by ID.",
|
|
93
|
+
tags=["Catalogs"],
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Add endpoint for getting collections in a catalog
|
|
97
|
+
self.router.add_api_route(
|
|
98
|
+
path="/catalogs/{catalog_id}/collections",
|
|
99
|
+
endpoint=self.get_catalog_collections,
|
|
100
|
+
methods=["GET"],
|
|
101
|
+
response_model=stac_types.Collections,
|
|
102
|
+
response_class=self.response_class,
|
|
103
|
+
summary="Get Catalog Collections",
|
|
104
|
+
description="Get collections linked from a specific catalog.",
|
|
105
|
+
tags=["Catalogs"],
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Add endpoint for creating collections in a catalog
|
|
109
|
+
self.router.add_api_route(
|
|
110
|
+
path="/catalogs/{catalog_id}/collections",
|
|
111
|
+
endpoint=self.create_catalog_collection,
|
|
112
|
+
methods=["POST"],
|
|
113
|
+
response_model=stac_types.Collection,
|
|
114
|
+
response_class=self.response_class,
|
|
115
|
+
status_code=201,
|
|
116
|
+
summary="Create Catalog Collection",
|
|
117
|
+
description="Create a new collection and link it to a specific catalog.",
|
|
118
|
+
tags=["Catalogs"],
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Add endpoint for deleting a catalog
|
|
122
|
+
self.router.add_api_route(
|
|
123
|
+
path="/catalogs/{catalog_id}",
|
|
124
|
+
endpoint=self.delete_catalog,
|
|
125
|
+
methods=["DELETE"],
|
|
126
|
+
response_class=self.response_class,
|
|
127
|
+
status_code=204,
|
|
128
|
+
summary="Delete Catalog",
|
|
129
|
+
description="Delete a catalog. All linked collections are unlinked and adopted by root if orphaned.",
|
|
130
|
+
tags=["Catalogs"],
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Add endpoint for getting a specific collection in a catalog
|
|
134
|
+
self.router.add_api_route(
|
|
135
|
+
path="/catalogs/{catalog_id}/collections/{collection_id}",
|
|
136
|
+
endpoint=self.get_catalog_collection,
|
|
137
|
+
methods=["GET"],
|
|
138
|
+
response_model=stac_types.Collection,
|
|
139
|
+
response_class=self.response_class,
|
|
140
|
+
summary="Get Catalog Collection",
|
|
141
|
+
description="Get a specific collection from a catalog.",
|
|
142
|
+
tags=["Catalogs"],
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Add endpoint for deleting a collection from a catalog
|
|
146
|
+
self.router.add_api_route(
|
|
147
|
+
path="/catalogs/{catalog_id}/collections/{collection_id}",
|
|
148
|
+
endpoint=self.delete_catalog_collection,
|
|
149
|
+
methods=["DELETE"],
|
|
150
|
+
response_class=self.response_class,
|
|
151
|
+
status_code=204,
|
|
152
|
+
summary="Delete Catalog Collection",
|
|
153
|
+
description="Delete a collection from a catalog. If the collection has multiple parent catalogs, only removes this catalog from parent_ids. If this is the only parent, deletes the collection entirely.",
|
|
154
|
+
tags=["Catalogs"],
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Add endpoint for getting items in a collection within a catalog
|
|
158
|
+
self.router.add_api_route(
|
|
159
|
+
path="/catalogs/{catalog_id}/collections/{collection_id}/items",
|
|
160
|
+
endpoint=self.get_catalog_collection_items,
|
|
161
|
+
methods=["GET"],
|
|
162
|
+
response_model=stac_types.ItemCollection,
|
|
163
|
+
response_class=self.response_class,
|
|
164
|
+
summary="Get Catalog Collection Items",
|
|
165
|
+
description="Get items from a collection in a catalog.",
|
|
166
|
+
tags=["Catalogs"],
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Add endpoint for getting a specific item in a collection within a catalog
|
|
170
|
+
self.router.add_api_route(
|
|
171
|
+
path="/catalogs/{catalog_id}/collections/{collection_id}/items/{item_id}",
|
|
172
|
+
endpoint=self.get_catalog_collection_item,
|
|
173
|
+
methods=["GET"],
|
|
174
|
+
response_model=stac_types.Item,
|
|
175
|
+
response_class=self.response_class,
|
|
176
|
+
summary="Get Catalog Collection Item",
|
|
177
|
+
description="Get a specific item from a collection in a catalog.",
|
|
178
|
+
tags=["Catalogs"],
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# Add endpoint for Children Extension
|
|
182
|
+
self.router.add_api_route(
|
|
183
|
+
path="/catalogs/{catalog_id}/children",
|
|
184
|
+
endpoint=self.get_catalog_children,
|
|
185
|
+
methods=["GET"],
|
|
186
|
+
response_class=self.response_class,
|
|
187
|
+
summary="Get Catalog Children",
|
|
188
|
+
description="Retrieve all children (Catalogs and Collections) of this catalog.",
|
|
189
|
+
tags=["Catalogs"],
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
app.include_router(self.router, tags=["Catalogs"])
|
|
193
|
+
|
|
194
|
+
async def catalogs(
|
|
195
|
+
self,
|
|
196
|
+
request: Request,
|
|
197
|
+
limit: Optional[int] = Query(
|
|
198
|
+
10,
|
|
199
|
+
ge=1,
|
|
200
|
+
description=(
|
|
201
|
+
"The maximum number of catalogs to return (page size). Defaults to 10."
|
|
202
|
+
),
|
|
203
|
+
),
|
|
204
|
+
token: Optional[str] = Query(
|
|
205
|
+
None,
|
|
206
|
+
description="Pagination token for the next page of results",
|
|
207
|
+
),
|
|
208
|
+
) -> Catalogs:
|
|
209
|
+
"""Get all catalogs with pagination support.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
request: Request object.
|
|
213
|
+
limit: The maximum number of catalogs to return (page size). Defaults to 10.
|
|
214
|
+
token: Pagination token for the next page of results.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
Catalogs object containing catalogs and pagination links.
|
|
218
|
+
"""
|
|
219
|
+
base_url = str(request.base_url)
|
|
220
|
+
|
|
221
|
+
# Get all catalogs from database with pagination
|
|
222
|
+
catalogs, next_token, _ = await self.client.database.get_all_catalogs(
|
|
223
|
+
token=token,
|
|
224
|
+
limit=limit,
|
|
225
|
+
request=request,
|
|
226
|
+
sort=[{"field": "id", "direction": "asc"}],
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Convert database catalogs to STAC format
|
|
230
|
+
catalog_stac_objects = []
|
|
231
|
+
for catalog in catalogs:
|
|
232
|
+
catalog_stac = self.client.catalog_serializer.db_to_stac(catalog, request)
|
|
233
|
+
catalog_stac_objects.append(catalog_stac)
|
|
234
|
+
|
|
235
|
+
# Create pagination links
|
|
236
|
+
links = [
|
|
237
|
+
{"rel": "root", "type": "application/json", "href": base_url},
|
|
238
|
+
{"rel": "parent", "type": "application/json", "href": base_url},
|
|
239
|
+
{"rel": "self", "type": "application/json", "href": str(request.url)},
|
|
240
|
+
]
|
|
241
|
+
|
|
242
|
+
# Add next link if there are more pages
|
|
243
|
+
if next_token:
|
|
244
|
+
query_params = {"limit": limit, "token": next_token}
|
|
245
|
+
next_link = {
|
|
246
|
+
"rel": "next",
|
|
247
|
+
"href": f"{base_url}catalogs?{urlencode(query_params)}",
|
|
248
|
+
"type": "application/json",
|
|
249
|
+
"title": "Next page of catalogs",
|
|
250
|
+
}
|
|
251
|
+
links.append(next_link)
|
|
252
|
+
|
|
253
|
+
# Return Catalogs object with catalogs
|
|
254
|
+
return Catalogs(
|
|
255
|
+
catalogs=catalog_stac_objects,
|
|
256
|
+
links=links,
|
|
257
|
+
numberReturned=len(catalog_stac_objects),
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
async def create_catalog(self, catalog: Catalog, request: Request) -> Catalog:
|
|
261
|
+
"""Create a new catalog.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
catalog: The catalog to create.
|
|
265
|
+
request: Request object.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
The created catalog.
|
|
269
|
+
"""
|
|
270
|
+
# Convert STAC catalog to database format
|
|
271
|
+
db_catalog = self.client.catalog_serializer.stac_to_db(catalog, request)
|
|
272
|
+
|
|
273
|
+
# Convert to dict and ensure type is set to Catalog
|
|
274
|
+
db_catalog_dict = db_catalog.model_dump()
|
|
275
|
+
db_catalog_dict["type"] = "Catalog"
|
|
276
|
+
|
|
277
|
+
# Create the catalog in the database with refresh to ensure immediate availability
|
|
278
|
+
await self.client.database.create_catalog(db_catalog_dict, refresh=True)
|
|
279
|
+
|
|
280
|
+
# Return the created catalog
|
|
281
|
+
return catalog
|
|
282
|
+
|
|
283
|
+
async def get_catalog(self, catalog_id: str, request: Request) -> Catalog:
|
|
284
|
+
"""Get a specific catalog by ID.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
catalog_id: The ID of the catalog to retrieve.
|
|
288
|
+
request: Request object.
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
The requested catalog.
|
|
292
|
+
"""
|
|
293
|
+
try:
|
|
294
|
+
# Get the catalog from the database
|
|
295
|
+
db_catalog = await self.client.database.find_catalog(catalog_id)
|
|
296
|
+
|
|
297
|
+
# Convert to STAC format
|
|
298
|
+
catalog = self.client.catalog_serializer.db_to_stac(db_catalog, request)
|
|
299
|
+
|
|
300
|
+
# DYNAMIC INJECTION: Ensure the 'children' link exists
|
|
301
|
+
# This link points to the /children endpoint which dynamically lists all children
|
|
302
|
+
base_url = str(request.base_url)
|
|
303
|
+
children_link = {
|
|
304
|
+
"rel": "children",
|
|
305
|
+
"type": "application/json",
|
|
306
|
+
"href": f"{base_url}catalogs/{catalog_id}/children",
|
|
307
|
+
"title": "Child catalogs and collections",
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
# Convert to dict if needed to manipulate links
|
|
311
|
+
if isinstance(catalog, dict):
|
|
312
|
+
catalog_dict = catalog
|
|
313
|
+
else:
|
|
314
|
+
catalog_dict = (
|
|
315
|
+
catalog.model_dump()
|
|
316
|
+
if hasattr(catalog, "model_dump")
|
|
317
|
+
else dict(catalog)
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
# Ensure catalog has a links array
|
|
321
|
+
if "links" not in catalog_dict:
|
|
322
|
+
catalog_dict["links"] = []
|
|
323
|
+
|
|
324
|
+
# Add children link if it doesn't already exist
|
|
325
|
+
if not any(
|
|
326
|
+
link.get("rel") == "children" for link in catalog_dict.get("links", [])
|
|
327
|
+
):
|
|
328
|
+
catalog_dict["links"].append(children_link)
|
|
329
|
+
|
|
330
|
+
# Return as Catalog object
|
|
331
|
+
return Catalog(**catalog_dict)
|
|
332
|
+
except HTTPException:
|
|
333
|
+
raise
|
|
334
|
+
except Exception as e:
|
|
335
|
+
logger.error(f"Error retrieving catalog {catalog_id}: {e}")
|
|
336
|
+
raise HTTPException(
|
|
337
|
+
status_code=404, detail=f"Catalog {catalog_id} not found"
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
async def delete_catalog(self, catalog_id: str, request: Request) -> None:
|
|
341
|
+
"""Delete a catalog (The Container).
|
|
342
|
+
|
|
343
|
+
Deletes the Catalog document itself. All linked Collections are unlinked
|
|
344
|
+
and adopted by Root if they become orphans. Collection data is NEVER deleted.
|
|
345
|
+
|
|
346
|
+
Logic:
|
|
347
|
+
1. Finds all Collections linked to this Catalog.
|
|
348
|
+
2. Unlinks them (removes catalog_id from their parent_ids).
|
|
349
|
+
3. If a Collection becomes an orphan, it is adopted by Root.
|
|
350
|
+
4. PERMANENTLY DELETES the Catalog document itself.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
catalog_id: The ID of the catalog to delete.
|
|
354
|
+
request: Request object.
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
None (204 No Content)
|
|
358
|
+
|
|
359
|
+
Raises:
|
|
360
|
+
HTTPException: If the catalog is not found.
|
|
361
|
+
"""
|
|
362
|
+
try:
|
|
363
|
+
# Verify the catalog exists
|
|
364
|
+
await self.client.database.find_catalog(catalog_id)
|
|
365
|
+
|
|
366
|
+
# Find all collections with this catalog in parent_ids
|
|
367
|
+
query_body = {"query": {"term": {"parent_ids": catalog_id}}}
|
|
368
|
+
search_result = await self.client.database.client.search(
|
|
369
|
+
index=COLLECTIONS_INDEX, body=query_body, size=10000
|
|
370
|
+
)
|
|
371
|
+
children = [hit["_source"] for hit in search_result["hits"]["hits"]]
|
|
372
|
+
|
|
373
|
+
# Safe Unlink: Remove catalog from all children's parent_ids
|
|
374
|
+
# If a child becomes an orphan, adopt it to root
|
|
375
|
+
root_id = self.settings.get("STAC_FASTAPI_LANDING_PAGE_ID", "stac-fastapi")
|
|
376
|
+
|
|
377
|
+
for child in children:
|
|
378
|
+
child_id = child.get("id")
|
|
379
|
+
try:
|
|
380
|
+
parent_ids = child.get("parent_ids", [])
|
|
381
|
+
if catalog_id in parent_ids:
|
|
382
|
+
parent_ids.remove(catalog_id)
|
|
383
|
+
|
|
384
|
+
# If orphan, move to root
|
|
385
|
+
if len(parent_ids) == 0:
|
|
386
|
+
parent_ids.append(root_id)
|
|
387
|
+
logger.info(
|
|
388
|
+
f"Collection {child_id} adopted by root after catalog deletion."
|
|
389
|
+
)
|
|
390
|
+
else:
|
|
391
|
+
logger.info(
|
|
392
|
+
f"Removed catalog {catalog_id} from collection {child_id}; still belongs to {len(parent_ids)} other catalog(s)"
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
child["parent_ids"] = parent_ids
|
|
396
|
+
await self.client.database.update_collection(
|
|
397
|
+
collection_id=child_id, collection=child, refresh=False
|
|
398
|
+
)
|
|
399
|
+
except Exception as e:
|
|
400
|
+
error_msg = str(e)
|
|
401
|
+
if "not found" in error_msg.lower():
|
|
402
|
+
logger.debug(
|
|
403
|
+
f"Collection {child_id} not found, skipping (may have been deleted elsewhere)"
|
|
404
|
+
)
|
|
405
|
+
else:
|
|
406
|
+
logger.warning(
|
|
407
|
+
f"Failed to process collection {child_id} during catalog deletion: {e}"
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
# Delete the catalog itself
|
|
411
|
+
await self.client.database.delete_catalog(catalog_id, refresh=True)
|
|
412
|
+
logger.info(f"Deleted catalog {catalog_id}")
|
|
413
|
+
|
|
414
|
+
except Exception as e:
|
|
415
|
+
error_msg = str(e)
|
|
416
|
+
if "not found" in error_msg.lower():
|
|
417
|
+
raise HTTPException(
|
|
418
|
+
status_code=404, detail=f"Catalog {catalog_id} not found"
|
|
419
|
+
)
|
|
420
|
+
logger.error(f"Error deleting catalog {catalog_id}: {e}")
|
|
421
|
+
raise HTTPException(
|
|
422
|
+
status_code=500,
|
|
423
|
+
detail=f"Failed to delete catalog: {str(e)}",
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
async def get_catalog_collections(
|
|
427
|
+
self, catalog_id: str, request: Request
|
|
428
|
+
) -> stac_types.Collections:
|
|
429
|
+
"""Get collections linked from a specific catalog.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
catalog_id: The ID of the catalog.
|
|
433
|
+
request: Request object.
|
|
434
|
+
|
|
435
|
+
Returns:
|
|
436
|
+
Collections object containing collections linked from the catalog.
|
|
437
|
+
"""
|
|
438
|
+
try:
|
|
439
|
+
# Verify the catalog exists
|
|
440
|
+
await self.client.database.find_catalog(catalog_id)
|
|
441
|
+
|
|
442
|
+
# Query collections by parent_ids field using Elasticsearch directly
|
|
443
|
+
# This uses the parent_ids field in the collection mapping to find all
|
|
444
|
+
# collections that have this catalog as a parent
|
|
445
|
+
query_body = {"query": {"term": {"parent_ids": catalog_id}}}
|
|
446
|
+
|
|
447
|
+
# Execute the search to get collection IDs
|
|
448
|
+
try:
|
|
449
|
+
search_result = await self.client.database.client.search(
|
|
450
|
+
index=COLLECTIONS_INDEX, body=query_body
|
|
451
|
+
)
|
|
452
|
+
except Exception as e:
|
|
453
|
+
logger.error(
|
|
454
|
+
f"Error searching for collections with parent {catalog_id}: {e}"
|
|
455
|
+
)
|
|
456
|
+
search_result = {"hits": {"hits": []}}
|
|
457
|
+
|
|
458
|
+
# Extract collection IDs from search results
|
|
459
|
+
collection_ids = []
|
|
460
|
+
hits = search_result.get("hits", {}).get("hits", [])
|
|
461
|
+
for hit in hits:
|
|
462
|
+
collection_ids.append(hit.get("_id"))
|
|
463
|
+
|
|
464
|
+
# Fetch the collections
|
|
465
|
+
collections = []
|
|
466
|
+
for coll_id in collection_ids:
|
|
467
|
+
try:
|
|
468
|
+
# Get the collection from database
|
|
469
|
+
collection_db = await self.client.database.find_collection(coll_id)
|
|
470
|
+
# Serialize with catalog context (sets parent to catalog, injects catalog link)
|
|
471
|
+
collection = (
|
|
472
|
+
self.client.collection_serializer.db_to_stac_in_catalog(
|
|
473
|
+
collection_db,
|
|
474
|
+
request,
|
|
475
|
+
catalog_id=catalog_id,
|
|
476
|
+
extensions=[
|
|
477
|
+
type(ext).__name__
|
|
478
|
+
for ext in self.client.database.extensions
|
|
479
|
+
],
|
|
480
|
+
)
|
|
481
|
+
)
|
|
482
|
+
collections.append(collection)
|
|
483
|
+
except HTTPException as e:
|
|
484
|
+
# Only skip collections that are not found (404)
|
|
485
|
+
if e.status_code == 404:
|
|
486
|
+
logger.debug(f"Collection {coll_id} not found, skipping")
|
|
487
|
+
continue
|
|
488
|
+
else:
|
|
489
|
+
# Re-raise other HTTP exceptions (5xx server errors, etc.)
|
|
490
|
+
logger.error(f"HTTP error retrieving collection {coll_id}: {e}")
|
|
491
|
+
raise
|
|
492
|
+
except Exception as e:
|
|
493
|
+
# Log unexpected errors and re-raise them
|
|
494
|
+
logger.error(
|
|
495
|
+
f"Unexpected error retrieving collection {coll_id}: {e}"
|
|
496
|
+
)
|
|
497
|
+
raise
|
|
498
|
+
|
|
499
|
+
# Return in Collections format
|
|
500
|
+
base_url = str(request.base_url)
|
|
501
|
+
return stac_types.Collections(
|
|
502
|
+
collections=collections,
|
|
503
|
+
links=[
|
|
504
|
+
{"rel": "root", "type": "application/json", "href": base_url},
|
|
505
|
+
{"rel": "parent", "type": "application/json", "href": base_url},
|
|
506
|
+
{
|
|
507
|
+
"rel": "self",
|
|
508
|
+
"type": "application/json",
|
|
509
|
+
"href": f"{base_url}catalogs/{catalog_id}/collections",
|
|
510
|
+
},
|
|
511
|
+
],
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
except HTTPException:
|
|
515
|
+
# Re-raise HTTP exceptions as-is
|
|
516
|
+
raise
|
|
517
|
+
except Exception as e:
|
|
518
|
+
logger.error(
|
|
519
|
+
f"Error retrieving collections for catalog {catalog_id}: {e}",
|
|
520
|
+
exc_info=True,
|
|
521
|
+
)
|
|
522
|
+
raise HTTPException(
|
|
523
|
+
status_code=404, detail=f"Catalog {catalog_id} not found"
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
async def create_catalog_collection(
|
|
527
|
+
self, catalog_id: str, collection: Collection, request: Request
|
|
528
|
+
) -> stac_types.Collection:
|
|
529
|
+
"""Create a new collection and link it to a specific catalog.
|
|
530
|
+
|
|
531
|
+
Args:
|
|
532
|
+
catalog_id: The ID of the catalog to link the collection to.
|
|
533
|
+
collection: The collection to create.
|
|
534
|
+
request: Request object.
|
|
535
|
+
|
|
536
|
+
Returns:
|
|
537
|
+
The created collection.
|
|
538
|
+
|
|
539
|
+
Raises:
|
|
540
|
+
HTTPException: If the catalog is not found or collection creation fails.
|
|
541
|
+
"""
|
|
542
|
+
try:
|
|
543
|
+
# Verify the catalog exists
|
|
544
|
+
await self.client.database.find_catalog(catalog_id)
|
|
545
|
+
|
|
546
|
+
# Check if the collection already exists in the database
|
|
547
|
+
try:
|
|
548
|
+
existing_collection_db = await self.client.database.find_collection(
|
|
549
|
+
collection.id
|
|
550
|
+
)
|
|
551
|
+
# Collection exists, just add the parent ID if not already present
|
|
552
|
+
existing_collection_dict = existing_collection_db
|
|
553
|
+
|
|
554
|
+
# Ensure parent_ids field exists
|
|
555
|
+
if "parent_ids" not in existing_collection_dict:
|
|
556
|
+
existing_collection_dict["parent_ids"] = []
|
|
557
|
+
|
|
558
|
+
# Add catalog_id to parent_ids if not already present
|
|
559
|
+
if catalog_id not in existing_collection_dict["parent_ids"]:
|
|
560
|
+
existing_collection_dict["parent_ids"].append(catalog_id)
|
|
561
|
+
|
|
562
|
+
# Update the collection in the database
|
|
563
|
+
await self.client.database.update_collection(
|
|
564
|
+
collection_id=collection.id,
|
|
565
|
+
collection=existing_collection_dict,
|
|
566
|
+
refresh=True,
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
# Convert back to STAC format for the response
|
|
570
|
+
updated_collection = (
|
|
571
|
+
self.client.database.collection_serializer.db_to_stac(
|
|
572
|
+
existing_collection_dict,
|
|
573
|
+
request,
|
|
574
|
+
extensions=[
|
|
575
|
+
type(ext).__name__
|
|
576
|
+
for ext in self.client.database.extensions
|
|
577
|
+
],
|
|
578
|
+
)
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
return updated_collection
|
|
582
|
+
|
|
583
|
+
except Exception as e:
|
|
584
|
+
# Only proceed to create if collection truly doesn't exist
|
|
585
|
+
error_msg = str(e)
|
|
586
|
+
if "not found" not in error_msg.lower():
|
|
587
|
+
# Re-raise if it's a different error
|
|
588
|
+
raise
|
|
589
|
+
# Collection doesn't exist, create it
|
|
590
|
+
# Create the collection using the same pattern as TransactionsClient.create_collection
|
|
591
|
+
# This handles the Collection model from stac_pydantic correctly
|
|
592
|
+
collection_dict = collection.model_dump(mode="json")
|
|
593
|
+
|
|
594
|
+
# Add the catalog ID to the parent_ids field
|
|
595
|
+
if "parent_ids" not in collection_dict:
|
|
596
|
+
collection_dict["parent_ids"] = []
|
|
597
|
+
|
|
598
|
+
if catalog_id not in collection_dict["parent_ids"]:
|
|
599
|
+
collection_dict["parent_ids"].append(catalog_id)
|
|
600
|
+
|
|
601
|
+
# Note: We do NOT store catalog links in the database.
|
|
602
|
+
# Catalog links are injected dynamically by the serializer based on context.
|
|
603
|
+
# This allows the same collection to have different catalog links
|
|
604
|
+
# depending on which catalog it's accessed from.
|
|
605
|
+
|
|
606
|
+
# Now convert to database format (this will process the links)
|
|
607
|
+
collection_db = self.client.database.collection_serializer.stac_to_db(
|
|
608
|
+
collection_dict, request
|
|
609
|
+
)
|
|
610
|
+
await self.client.database.create_collection(
|
|
611
|
+
collection=collection_db, refresh=True
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
# Convert back to STAC format for the response
|
|
615
|
+
created_collection = (
|
|
616
|
+
self.client.database.collection_serializer.db_to_stac(
|
|
617
|
+
collection_db,
|
|
618
|
+
request,
|
|
619
|
+
extensions=[
|
|
620
|
+
type(ext).__name__
|
|
621
|
+
for ext in self.client.database.extensions
|
|
622
|
+
],
|
|
623
|
+
)
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
return created_collection
|
|
627
|
+
|
|
628
|
+
except HTTPException as e:
|
|
629
|
+
# Re-raise HTTP exceptions (e.g., catalog not found, collection validation errors)
|
|
630
|
+
raise e
|
|
631
|
+
except Exception as e:
|
|
632
|
+
# Check if this is a "not found" error from find_catalog
|
|
633
|
+
error_msg = str(e)
|
|
634
|
+
if "not found" in error_msg.lower():
|
|
635
|
+
raise HTTPException(status_code=404, detail=error_msg)
|
|
636
|
+
|
|
637
|
+
# Handle unexpected errors
|
|
638
|
+
logger.error(f"Error creating collection in catalog {catalog_id}: {e}")
|
|
639
|
+
raise HTTPException(
|
|
640
|
+
status_code=500,
|
|
641
|
+
detail=f"Failed to create collection in catalog: {str(e)}",
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
async def get_catalog_collection(
|
|
645
|
+
self, catalog_id: str, collection_id: str, request: Request
|
|
646
|
+
) -> stac_types.Collection:
|
|
647
|
+
"""Get a specific collection from a catalog.
|
|
648
|
+
|
|
649
|
+
Args:
|
|
650
|
+
catalog_id: The ID of the catalog.
|
|
651
|
+
collection_id: The ID of the collection.
|
|
652
|
+
request: Request object.
|
|
653
|
+
|
|
654
|
+
Returns:
|
|
655
|
+
The requested collection.
|
|
656
|
+
"""
|
|
657
|
+
# Verify the catalog exists
|
|
658
|
+
try:
|
|
659
|
+
await self.client.database.find_catalog(catalog_id)
|
|
660
|
+
except Exception:
|
|
661
|
+
raise HTTPException(
|
|
662
|
+
status_code=404, detail=f"Catalog {catalog_id} not found"
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
# Verify the collection exists and has the catalog as a parent
|
|
666
|
+
try:
|
|
667
|
+
collection_db = await self.client.database.find_collection(collection_id)
|
|
668
|
+
|
|
669
|
+
# Check if the catalog_id is in the collection's parent_ids
|
|
670
|
+
parent_ids = collection_db.get("parent_ids", [])
|
|
671
|
+
if catalog_id not in parent_ids:
|
|
672
|
+
raise HTTPException(
|
|
673
|
+
status_code=404,
|
|
674
|
+
detail=f"Collection {collection_id} does not belong to catalog {catalog_id}",
|
|
675
|
+
)
|
|
676
|
+
except HTTPException:
|
|
677
|
+
raise
|
|
678
|
+
except Exception:
|
|
679
|
+
raise HTTPException(
|
|
680
|
+
status_code=404, detail=f"Collection {collection_id} not found"
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
# Return the collection with catalog context
|
|
684
|
+
collection_db = await self.client.database.find_collection(collection_id)
|
|
685
|
+
return self.client.collection_serializer.db_to_stac_in_catalog(
|
|
686
|
+
collection_db,
|
|
687
|
+
request,
|
|
688
|
+
catalog_id=catalog_id,
|
|
689
|
+
extensions=[type(ext).__name__ for ext in self.client.database.extensions],
|
|
690
|
+
)
|
|
691
|
+
|
|
692
|
+
async def get_catalog_collection_items(
|
|
693
|
+
self,
|
|
694
|
+
catalog_id: str,
|
|
695
|
+
collection_id: str,
|
|
696
|
+
request: Request,
|
|
697
|
+
bbox: Optional[List[float]] = None,
|
|
698
|
+
datetime: Optional[str] = None,
|
|
699
|
+
limit: Optional[int] = None,
|
|
700
|
+
sortby: Optional[str] = None,
|
|
701
|
+
filter_expr: Optional[str] = None,
|
|
702
|
+
filter_lang: Optional[str] = None,
|
|
703
|
+
token: Optional[str] = None,
|
|
704
|
+
query: Optional[str] = None,
|
|
705
|
+
fields: Optional[List[str]] = None,
|
|
706
|
+
) -> stac_types.ItemCollection:
|
|
707
|
+
"""Get items from a collection in a catalog.
|
|
708
|
+
|
|
709
|
+
Args:
|
|
710
|
+
catalog_id: The ID of the catalog.
|
|
711
|
+
collection_id: The ID of the collection.
|
|
712
|
+
request: Request object.
|
|
713
|
+
bbox: Optional bounding box filter.
|
|
714
|
+
datetime: Optional datetime or interval filter.
|
|
715
|
+
limit: Optional page size.
|
|
716
|
+
sortby: Optional sort specification.
|
|
717
|
+
filter_expr: Optional filter expression.
|
|
718
|
+
filter_lang: Optional filter language.
|
|
719
|
+
token: Optional pagination token.
|
|
720
|
+
query: Optional query string.
|
|
721
|
+
fields: Optional fields to include or exclude.
|
|
722
|
+
|
|
723
|
+
Returns:
|
|
724
|
+
ItemCollection containing items from the collection.
|
|
725
|
+
"""
|
|
726
|
+
# Verify the catalog exists
|
|
727
|
+
try:
|
|
728
|
+
await self.client.database.find_catalog(catalog_id)
|
|
729
|
+
except Exception:
|
|
730
|
+
raise HTTPException(
|
|
731
|
+
status_code=404, detail=f"Catalog {catalog_id} not found"
|
|
732
|
+
)
|
|
733
|
+
|
|
734
|
+
# Delegate to the core client's item_collection method with all parameters
|
|
735
|
+
return await self.client.item_collection(
|
|
736
|
+
collection_id=collection_id,
|
|
737
|
+
request=request,
|
|
738
|
+
bbox=bbox,
|
|
739
|
+
datetime=datetime,
|
|
740
|
+
limit=limit,
|
|
741
|
+
sortby=sortby,
|
|
742
|
+
filter_expr=filter_expr,
|
|
743
|
+
filter_lang=filter_lang,
|
|
744
|
+
token=token,
|
|
745
|
+
query=query,
|
|
746
|
+
fields=fields,
|
|
747
|
+
)
|
|
748
|
+
|
|
749
|
+
async def get_catalog_collection_item(
|
|
750
|
+
self, catalog_id: str, collection_id: str, item_id: str, request: Request
|
|
751
|
+
) -> stac_types.Item:
|
|
752
|
+
"""Get a specific item from a collection in a catalog.
|
|
753
|
+
|
|
754
|
+
Args:
|
|
755
|
+
catalog_id: The ID of the catalog.
|
|
756
|
+
collection_id: The ID of the collection.
|
|
757
|
+
item_id: The ID of the item.
|
|
758
|
+
request: Request object.
|
|
759
|
+
|
|
760
|
+
Returns:
|
|
761
|
+
The requested item.
|
|
762
|
+
"""
|
|
763
|
+
# Verify the catalog exists
|
|
764
|
+
try:
|
|
765
|
+
await self.client.database.find_catalog(catalog_id)
|
|
766
|
+
except Exception:
|
|
767
|
+
raise HTTPException(
|
|
768
|
+
status_code=404, detail=f"Catalog {catalog_id} not found"
|
|
769
|
+
)
|
|
770
|
+
|
|
771
|
+
# Delegate to the core client's get_item method
|
|
772
|
+
return await self.client.get_item(
|
|
773
|
+
item_id=item_id, collection_id=collection_id, request=request
|
|
774
|
+
)
|
|
775
|
+
|
|
776
|
+
async def get_catalog_children(
|
|
777
|
+
self,
|
|
778
|
+
catalog_id: str,
|
|
779
|
+
request: Request,
|
|
780
|
+
limit: int = 10,
|
|
781
|
+
token: str = None,
|
|
782
|
+
type: Optional[str] = Query(
|
|
783
|
+
None, description="Filter by resource type (Catalog or Collection)"
|
|
784
|
+
),
|
|
785
|
+
) -> Dict[str, Any]:
|
|
786
|
+
"""
|
|
787
|
+
Get all children (Catalogs and Collections) of a specific catalog.
|
|
788
|
+
|
|
789
|
+
This is a 'Union' endpoint that returns mixed content types.
|
|
790
|
+
"""
|
|
791
|
+
# 1. Verify the parent catalog exists
|
|
792
|
+
await self.client.database.find_catalog(catalog_id)
|
|
793
|
+
|
|
794
|
+
# 2. Build the Search Query
|
|
795
|
+
# We search the COLLECTIONS_INDEX because it holds both Catalogs and Collections
|
|
796
|
+
|
|
797
|
+
# Base filter: Parent match
|
|
798
|
+
# This finds anything where 'parent_ids' contains this catalog_id
|
|
799
|
+
filter_queries = [{"term": {"parent_ids": catalog_id}}]
|
|
800
|
+
|
|
801
|
+
# Optional filter: Type
|
|
802
|
+
if type:
|
|
803
|
+
# If user asks for ?type=Catalog, we only return Catalogs
|
|
804
|
+
filter_queries.append({"term": {"type": type}})
|
|
805
|
+
|
|
806
|
+
# 3. Calculate Pagination (Search After)
|
|
807
|
+
body = {
|
|
808
|
+
"query": {"bool": {"filter": filter_queries}},
|
|
809
|
+
"sort": [{"id": {"order": "asc"}}], # Stable sort for pagination
|
|
810
|
+
"size": limit,
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
# Handle search_after token - split by '|' to get all sort values
|
|
814
|
+
search_after: Optional[List[str]] = None
|
|
815
|
+
if token:
|
|
816
|
+
try:
|
|
817
|
+
# The token should be a pipe-separated string of sort values
|
|
818
|
+
# e.g., "collection-1"
|
|
819
|
+
from typing import cast
|
|
820
|
+
|
|
821
|
+
search_after_parts = cast(List[str], token.split("|"))
|
|
822
|
+
# If the number of sort fields doesn't match token parts, ignore the token
|
|
823
|
+
if len(search_after_parts) != len(body["sort"]): # type: ignore
|
|
824
|
+
search_after = None
|
|
825
|
+
else:
|
|
826
|
+
search_after = search_after_parts
|
|
827
|
+
except Exception:
|
|
828
|
+
search_after = None
|
|
829
|
+
|
|
830
|
+
if search_after is not None:
|
|
831
|
+
body["search_after"] = search_after
|
|
832
|
+
|
|
833
|
+
# 4. Execute Search
|
|
834
|
+
search_result = await self.client.database.client.search(
|
|
835
|
+
index=COLLECTIONS_INDEX, body=body
|
|
836
|
+
)
|
|
837
|
+
|
|
838
|
+
# 5. Process Results
|
|
839
|
+
hits = search_result.get("hits", {}).get("hits", [])
|
|
840
|
+
total = search_result.get("hits", {}).get("total", {}).get("value", 0)
|
|
841
|
+
|
|
842
|
+
children = []
|
|
843
|
+
for hit in hits:
|
|
844
|
+
doc = hit["_source"]
|
|
845
|
+
resource_type = doc.get(
|
|
846
|
+
"type", "Collection"
|
|
847
|
+
) # Default to Collection if missing
|
|
848
|
+
|
|
849
|
+
# Serialize based on type
|
|
850
|
+
# This ensures we hide internal fields like 'parent_ids' correctly
|
|
851
|
+
if resource_type == "Catalog":
|
|
852
|
+
child = self.client.catalog_serializer.db_to_stac(doc, request)
|
|
853
|
+
else:
|
|
854
|
+
child = self.client.collection_serializer.db_to_stac(doc, request)
|
|
855
|
+
|
|
856
|
+
children.append(child)
|
|
857
|
+
|
|
858
|
+
# 6. Format Response
|
|
859
|
+
# The Children extension uses a specific response format
|
|
860
|
+
response = {
|
|
861
|
+
"children": children,
|
|
862
|
+
"links": [
|
|
863
|
+
{"rel": "self", "type": "application/json", "href": str(request.url)},
|
|
864
|
+
{
|
|
865
|
+
"rel": "root",
|
|
866
|
+
"type": "application/json",
|
|
867
|
+
"href": str(request.base_url),
|
|
868
|
+
},
|
|
869
|
+
{
|
|
870
|
+
"rel": "parent",
|
|
871
|
+
"type": "application/json",
|
|
872
|
+
"href": f"{str(request.base_url)}catalogs/{catalog_id}",
|
|
873
|
+
},
|
|
874
|
+
],
|
|
875
|
+
"numberReturned": len(children),
|
|
876
|
+
"numberMatched": total,
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
# 7. Generate Next Link
|
|
880
|
+
next_token = None
|
|
881
|
+
if len(hits) == limit:
|
|
882
|
+
next_token_values = hits[-1].get("sort")
|
|
883
|
+
if next_token_values:
|
|
884
|
+
# Join all sort values with '|' to create the token
|
|
885
|
+
next_token = "|".join(str(val) for val in next_token_values)
|
|
886
|
+
|
|
887
|
+
if next_token:
|
|
888
|
+
# Get existing query params
|
|
889
|
+
parsed_url = urlparse(str(request.url))
|
|
890
|
+
params = parse_qs(parsed_url.query)
|
|
891
|
+
|
|
892
|
+
# Update params
|
|
893
|
+
params["token"] = [next_token]
|
|
894
|
+
params["limit"] = [str(limit)]
|
|
895
|
+
if type:
|
|
896
|
+
params["type"] = [type]
|
|
897
|
+
|
|
898
|
+
# Flatten params for urlencode (parse_qs returns lists)
|
|
899
|
+
flat_params = {
|
|
900
|
+
k: v[0] if isinstance(v, list) else v for k, v in params.items()
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
next_link = {
|
|
904
|
+
"rel": "next",
|
|
905
|
+
"type": "application/json",
|
|
906
|
+
"href": f"{request.base_url}catalogs/{catalog_id}/children?{urlencode(flat_params)}",
|
|
907
|
+
}
|
|
908
|
+
response["links"].append(next_link)
|
|
909
|
+
|
|
910
|
+
return response
|
|
911
|
+
|
|
912
|
+
async def delete_catalog_collection(
|
|
913
|
+
self, catalog_id: str, collection_id: str, request: Request
|
|
914
|
+
) -> None:
|
|
915
|
+
"""Delete a collection from a catalog (Unlink only).
|
|
916
|
+
|
|
917
|
+
Removes the catalog from the collection's parent_ids.
|
|
918
|
+
If the collection becomes an orphan (no parents), it is adopted by the Root.
|
|
919
|
+
It NEVER deletes the collection data.
|
|
920
|
+
|
|
921
|
+
Args:
|
|
922
|
+
catalog_id: The ID of the catalog.
|
|
923
|
+
collection_id: The ID of the collection.
|
|
924
|
+
request: Request object.
|
|
925
|
+
|
|
926
|
+
Raises:
|
|
927
|
+
HTTPException: If the catalog or collection is not found, or if the
|
|
928
|
+
collection does not belong to the catalog.
|
|
929
|
+
"""
|
|
930
|
+
try:
|
|
931
|
+
# Verify the catalog exists
|
|
932
|
+
await self.client.database.find_catalog(catalog_id)
|
|
933
|
+
|
|
934
|
+
# Get the collection
|
|
935
|
+
collection_db = await self.client.database.find_collection(collection_id)
|
|
936
|
+
|
|
937
|
+
# Check if the catalog_id is in the collection's parent_ids
|
|
938
|
+
parent_ids = collection_db.get("parent_ids", [])
|
|
939
|
+
if catalog_id not in parent_ids:
|
|
940
|
+
raise HTTPException(
|
|
941
|
+
status_code=404,
|
|
942
|
+
detail=f"Collection {collection_id} does not belong to catalog {catalog_id}",
|
|
943
|
+
)
|
|
944
|
+
|
|
945
|
+
# SAFE UNLINK LOGIC
|
|
946
|
+
parent_ids.remove(catalog_id)
|
|
947
|
+
|
|
948
|
+
# Check if it is now an orphan (empty list)
|
|
949
|
+
if len(parent_ids) == 0:
|
|
950
|
+
# Fallback to Root / Landing Page
|
|
951
|
+
# You can hardcode 'root' or fetch the ID from settings
|
|
952
|
+
root_id = self.settings.get(
|
|
953
|
+
"STAC_FASTAPI_LANDING_PAGE_ID", "stac-fastapi"
|
|
954
|
+
)
|
|
955
|
+
parent_ids.append(root_id)
|
|
956
|
+
logger.info(
|
|
957
|
+
f"Collection {collection_id} unlinked from {catalog_id}. Orphaned, so adopted by root ({root_id})."
|
|
958
|
+
)
|
|
959
|
+
else:
|
|
960
|
+
logger.info(
|
|
961
|
+
f"Removed catalog {catalog_id} from collection {collection_id}; still belongs to {len(parent_ids)} other catalog(s)"
|
|
962
|
+
)
|
|
963
|
+
|
|
964
|
+
# Update the collection in the database
|
|
965
|
+
collection_db["parent_ids"] = parent_ids
|
|
966
|
+
await self.client.database.update_collection(
|
|
967
|
+
collection_id=collection_id, collection=collection_db, refresh=True
|
|
968
|
+
)
|
|
969
|
+
|
|
970
|
+
except HTTPException:
|
|
971
|
+
raise
|
|
972
|
+
except Exception as e:
|
|
973
|
+
logger.error(
|
|
974
|
+
f"Error removing collection {collection_id} from catalog {catalog_id}: {e}",
|
|
975
|
+
exc_info=True,
|
|
976
|
+
)
|
|
977
|
+
raise HTTPException(
|
|
978
|
+
status_code=500,
|
|
979
|
+
detail=f"Failed to remove collection from catalog: {str(e)}",
|
|
980
|
+
)
|