stac-fastapi-core 6.9.0__tar.gz → 6.10.1__tar.gz
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-6.9.0 → stac_fastapi_core-6.10.1}/.gitignore +11 -0
- {stac_fastapi_core-6.9.0 → stac_fastapi_core-6.10.1}/PKG-INFO +1 -1
- {stac_fastapi_core-6.9.0 → stac_fastapi_core-6.10.1}/stac_fastapi/core/core.py +52 -16
- {stac_fastapi_core-6.9.0 → stac_fastapi_core-6.10.1}/stac_fastapi/core/extensions/catalogs.py +296 -80
- {stac_fastapi_core-6.9.0 → stac_fastapi_core-6.10.1}/stac_fastapi/core/serializers.py +3 -0
- {stac_fastapi_core-6.9.0 → stac_fastapi_core-6.10.1}/stac_fastapi/core/version.py +1 -1
- {stac_fastapi_core-6.9.0 → stac_fastapi_core-6.10.1}/README.md +0 -0
- {stac_fastapi_core-6.9.0 → stac_fastapi_core-6.10.1}/pyproject.toml +0 -0
- {stac_fastapi_core-6.9.0 → stac_fastapi_core-6.10.1}/pytest.ini +0 -0
- {stac_fastapi_core-6.9.0 → stac_fastapi_core-6.10.1}/stac_fastapi/core/__init__.py +0 -0
- {stac_fastapi_core-6.9.0 → stac_fastapi_core-6.10.1}/stac_fastapi/core/base_database_logic.py +0 -0
- {stac_fastapi_core-6.9.0 → stac_fastapi_core-6.10.1}/stac_fastapi/core/base_settings.py +0 -0
- {stac_fastapi_core-6.9.0 → stac_fastapi_core-6.10.1}/stac_fastapi/core/basic_auth.py +0 -0
- {stac_fastapi_core-6.9.0 → stac_fastapi_core-6.10.1}/stac_fastapi/core/datetime_utils.py +0 -0
- {stac_fastapi_core-6.9.0 → stac_fastapi_core-6.10.1}/stac_fastapi/core/extensions/__init__.py +0 -0
- {stac_fastapi_core-6.9.0 → stac_fastapi_core-6.10.1}/stac_fastapi/core/extensions/aggregation.py +0 -0
- {stac_fastapi_core-6.9.0 → stac_fastapi_core-6.10.1}/stac_fastapi/core/extensions/collections_search.py +0 -0
- {stac_fastapi_core-6.9.0 → stac_fastapi_core-6.10.1}/stac_fastapi/core/extensions/fields.py +0 -0
- {stac_fastapi_core-6.9.0 → stac_fastapi_core-6.10.1}/stac_fastapi/core/extensions/filter.py +0 -0
- {stac_fastapi_core-6.9.0 → stac_fastapi_core-6.10.1}/stac_fastapi/core/extensions/query.py +0 -0
- {stac_fastapi_core-6.9.0 → stac_fastapi_core-6.10.1}/stac_fastapi/core/models/__init__.py +0 -0
- {stac_fastapi_core-6.9.0 → stac_fastapi_core-6.10.1}/stac_fastapi/core/models/links.py +0 -0
- {stac_fastapi_core-6.9.0 → stac_fastapi_core-6.10.1}/stac_fastapi/core/models/search.py +0 -0
- {stac_fastapi_core-6.9.0 → stac_fastapi_core-6.10.1}/stac_fastapi/core/queryables.py +0 -0
- {stac_fastapi_core-6.9.0 → stac_fastapi_core-6.10.1}/stac_fastapi/core/rate_limit.py +0 -0
- {stac_fastapi_core-6.9.0 → stac_fastapi_core-6.10.1}/stac_fastapi/core/redis_utils.py +0 -0
- {stac_fastapi_core-6.9.0 → stac_fastapi_core-6.10.1}/stac_fastapi/core/route_dependencies.py +0 -0
- {stac_fastapi_core-6.9.0 → stac_fastapi_core-6.10.1}/stac_fastapi/core/session.py +0 -0
- {stac_fastapi_core-6.9.0 → stac_fastapi_core-6.10.1}/stac_fastapi/core/utilities.py +0 -0
|
@@ -141,3 +141,14 @@ venv
|
|
|
141
141
|
/docs/src/api/*
|
|
142
142
|
|
|
143
143
|
.DS_Store
|
|
144
|
+
|
|
145
|
+
# Helm
|
|
146
|
+
*.tgz
|
|
147
|
+
charts/*/charts/
|
|
148
|
+
charts/*/requirements.lock
|
|
149
|
+
charts/*/Chart.lock
|
|
150
|
+
helm-chart/stac-fastapi/charts/
|
|
151
|
+
helm-chart/stac-fastapi/Chart.lock
|
|
152
|
+
helm-chart/stac-fastapi/*.tgz
|
|
153
|
+
helm-chart/test-results/
|
|
154
|
+
helm-chart/tmp/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stac_fastapi_core
|
|
3
|
-
Version: 6.
|
|
3
|
+
Version: 6.10.1
|
|
4
4
|
Summary: Core library for the Elasticsearch and Opensearch stac-fastapi backends.
|
|
5
5
|
Project-URL: Homepage, https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch
|
|
6
6
|
License: MIT
|
|
@@ -546,18 +546,20 @@ class CoreClient(AsyncBaseCoreClient):
|
|
|
546
546
|
return await self.all_collections(
|
|
547
547
|
limit=search_request.limit if hasattr(search_request, "limit") else None,
|
|
548
548
|
bbox=search_request.bbox if hasattr(search_request, "bbox") else None,
|
|
549
|
-
datetime=
|
|
550
|
-
|
|
551
|
-
|
|
549
|
+
datetime=(
|
|
550
|
+
search_request.datetime if hasattr(search_request, "datetime") else None
|
|
551
|
+
),
|
|
552
552
|
token=search_request.token if hasattr(search_request, "token") else None,
|
|
553
553
|
fields=fields,
|
|
554
554
|
sortby=sortby,
|
|
555
|
-
filter_expr=
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
filter_lang=
|
|
559
|
-
|
|
560
|
-
|
|
555
|
+
filter_expr=(
|
|
556
|
+
search_request.filter if hasattr(search_request, "filter") else None
|
|
557
|
+
),
|
|
558
|
+
filter_lang=(
|
|
559
|
+
search_request.filter_lang
|
|
560
|
+
if hasattr(search_request, "filter_lang")
|
|
561
|
+
else None
|
|
562
|
+
),
|
|
561
563
|
query=search_request.query if hasattr(search_request, "query") else None,
|
|
562
564
|
q=search_request.q if hasattr(search_request, "q") else None,
|
|
563
565
|
request=request,
|
|
@@ -1004,14 +1006,31 @@ class TransactionsClient(AsyncBaseTransactionsClient):
|
|
|
1004
1006
|
database=self.database, settings=self.settings
|
|
1005
1007
|
)
|
|
1006
1008
|
features = item_dict["features"]
|
|
1007
|
-
|
|
1009
|
+
all_prepped = [
|
|
1008
1010
|
bulk_client.preprocess_item(
|
|
1009
1011
|
feature, base_url, BulkTransactionMethod.INSERT
|
|
1010
1012
|
)
|
|
1011
1013
|
for feature in features
|
|
1012
1014
|
]
|
|
1015
|
+
# Filter out None values (skipped duplicates from DB check)
|
|
1016
|
+
processed_items = [item for item in all_prepped if item is not None]
|
|
1017
|
+
skipped_db_duplicates = len(all_prepped) - len(processed_items)
|
|
1018
|
+
|
|
1019
|
+
# Deduplicate items within the batch by ID (keep last occurrence)
|
|
1020
|
+
# This matches ES behavior where later items overwrite earlier ones
|
|
1021
|
+
seen_ids: dict = {}
|
|
1022
|
+
for item in processed_items:
|
|
1023
|
+
seen_ids[item["id"]] = item
|
|
1024
|
+
unique_items = list(seen_ids.values())
|
|
1025
|
+
skipped_batch_duplicates = len(processed_items) - len(unique_items)
|
|
1026
|
+
processed_items = unique_items
|
|
1027
|
+
|
|
1028
|
+
skipped = skipped_db_duplicates + skipped_batch_duplicates
|
|
1013
1029
|
attempted = len(processed_items)
|
|
1014
1030
|
|
|
1031
|
+
if not processed_items:
|
|
1032
|
+
return f"No items to insert. {skipped} items were skipped (duplicates)."
|
|
1033
|
+
|
|
1015
1034
|
success, errors = await self.database.bulk_async(
|
|
1016
1035
|
collection_id=collection_id,
|
|
1017
1036
|
processed_items=processed_items,
|
|
@@ -1025,7 +1044,7 @@ class TransactionsClient(AsyncBaseTransactionsClient):
|
|
|
1025
1044
|
logger.info(
|
|
1026
1045
|
f"Bulk async operation succeeded with {success} actions for collection {collection_id}."
|
|
1027
1046
|
)
|
|
1028
|
-
return f"Successfully added {success} Items. {attempted - success} errors occurred."
|
|
1047
|
+
return f"Successfully added {success} Items. {skipped} skipped (duplicates). {attempted - success} errors occurred."
|
|
1029
1048
|
|
|
1030
1049
|
# Handle single item
|
|
1031
1050
|
await self.database.create_item(
|
|
@@ -1338,18 +1357,35 @@ class BulkTransactionsClient(BaseBulkTransactionsClient):
|
|
|
1338
1357
|
base_url = ""
|
|
1339
1358
|
|
|
1340
1359
|
processed_items = []
|
|
1360
|
+
skipped_db_duplicates = 0
|
|
1341
1361
|
for item in items.items.values():
|
|
1342
1362
|
try:
|
|
1343
1363
|
validated = Item(**item) if not isinstance(item, Item) else item
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
validated.model_dump(mode="json"), base_url, items.method
|
|
1347
|
-
)
|
|
1364
|
+
prepped = self.preprocess_item(
|
|
1365
|
+
validated.model_dump(mode="json"), base_url, items.method
|
|
1348
1366
|
)
|
|
1367
|
+
if prepped is not None:
|
|
1368
|
+
processed_items.append(prepped)
|
|
1369
|
+
else:
|
|
1370
|
+
skipped_db_duplicates += 1
|
|
1349
1371
|
except ValidationError:
|
|
1350
1372
|
# Immediately raise on the first invalid item (strict mode)
|
|
1351
1373
|
raise
|
|
1352
1374
|
|
|
1375
|
+
# Deduplicate items within the batch by ID (keep last occurrence)
|
|
1376
|
+
# This matches ES behavior where later items overwrite earlier ones
|
|
1377
|
+
seen_ids: dict = {}
|
|
1378
|
+
for item in processed_items:
|
|
1379
|
+
seen_ids[item["id"]] = item
|
|
1380
|
+
unique_items = list(seen_ids.values())
|
|
1381
|
+
skipped_batch_duplicates = len(processed_items) - len(unique_items)
|
|
1382
|
+
processed_items = unique_items
|
|
1383
|
+
|
|
1384
|
+
skipped = skipped_db_duplicates + skipped_batch_duplicates
|
|
1385
|
+
|
|
1386
|
+
if not processed_items:
|
|
1387
|
+
return f"No items to insert. {skipped} items were skipped (duplicates)."
|
|
1388
|
+
|
|
1353
1389
|
collection_id = processed_items[0]["collection"]
|
|
1354
1390
|
attempted = len(processed_items)
|
|
1355
1391
|
success, errors = self.database.bulk_sync(
|
|
@@ -1362,4 +1398,4 @@ class BulkTransactionsClient(BaseBulkTransactionsClient):
|
|
|
1362
1398
|
else:
|
|
1363
1399
|
logger.info(f"Bulk sync operation succeeded with {success} actions.")
|
|
1364
1400
|
|
|
1365
|
-
return f"Successfully added/updated {success} Items. {attempted - success} errors occurred."
|
|
1401
|
+
return f"Successfully added/updated {success} Items. {skipped} skipped (duplicates). {attempted - success} errors occurred."
|
{stac_fastapi_core-6.9.0 → stac_fastapi_core-6.10.1}/stac_fastapi/core/extensions/catalogs.py
RENAMED
|
@@ -12,9 +12,15 @@ from starlette.responses import Response
|
|
|
12
12
|
from typing_extensions import TypedDict
|
|
13
13
|
|
|
14
14
|
from stac_fastapi.core.models import Catalog
|
|
15
|
-
from stac_fastapi.sfeos_helpers.
|
|
15
|
+
from stac_fastapi.sfeos_helpers.database import (
|
|
16
|
+
search_children_with_pagination_shared,
|
|
17
|
+
search_collections_by_parent_id_shared,
|
|
18
|
+
search_sub_catalogs_with_pagination_shared,
|
|
19
|
+
update_catalog_in_index_shared,
|
|
20
|
+
)
|
|
16
21
|
from stac_fastapi.types import stac as stac_types
|
|
17
22
|
from stac_fastapi.types.core import BaseCoreClient
|
|
23
|
+
from stac_fastapi.types.errors import NotFoundError
|
|
18
24
|
from stac_fastapi.types.extension import ApiExtension
|
|
19
25
|
|
|
20
26
|
logger = logging.getLogger(__name__)
|
|
@@ -106,6 +112,31 @@ class CatalogsExtension(ApiExtension):
|
|
|
106
112
|
tags=["Catalogs"],
|
|
107
113
|
)
|
|
108
114
|
|
|
115
|
+
# Add endpoint for getting sub-catalogs of a catalog
|
|
116
|
+
self.router.add_api_route(
|
|
117
|
+
path="/catalogs/{catalog_id}/catalogs",
|
|
118
|
+
endpoint=self.get_catalog_catalogs,
|
|
119
|
+
methods=["GET"],
|
|
120
|
+
response_model=Catalogs,
|
|
121
|
+
response_class=self.response_class,
|
|
122
|
+
summary="Get Catalog Sub-Catalogs",
|
|
123
|
+
description="Get sub-catalogs linked from a specific catalog.",
|
|
124
|
+
tags=["Catalogs"],
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Add endpoint for creating sub-catalogs in a catalog
|
|
128
|
+
self.router.add_api_route(
|
|
129
|
+
path="/catalogs/{catalog_id}/catalogs",
|
|
130
|
+
endpoint=self.create_catalog_catalog,
|
|
131
|
+
methods=["POST"],
|
|
132
|
+
response_model=Catalog,
|
|
133
|
+
response_class=self.response_class,
|
|
134
|
+
status_code=201,
|
|
135
|
+
summary="Create Catalog Sub-Catalog",
|
|
136
|
+
description="Create a new catalog and link it as a sub-catalog of a specific catalog.",
|
|
137
|
+
tags=["Catalogs"],
|
|
138
|
+
)
|
|
139
|
+
|
|
109
140
|
# Add endpoint for creating collections in a catalog
|
|
110
141
|
self.router.add_api_route(
|
|
111
142
|
path="/catalogs/{catalog_id}/collections",
|
|
@@ -119,6 +150,18 @@ class CatalogsExtension(ApiExtension):
|
|
|
119
150
|
tags=["Catalogs"],
|
|
120
151
|
)
|
|
121
152
|
|
|
153
|
+
# Add endpoint for updating a catalog
|
|
154
|
+
self.router.add_api_route(
|
|
155
|
+
path="/catalogs/{catalog_id}",
|
|
156
|
+
endpoint=self.update_catalog,
|
|
157
|
+
methods=["PUT"],
|
|
158
|
+
response_model=Catalog,
|
|
159
|
+
response_class=self.response_class,
|
|
160
|
+
summary="Update Catalog",
|
|
161
|
+
description="Update an existing STAC catalog.",
|
|
162
|
+
tags=["Catalogs"],
|
|
163
|
+
)
|
|
164
|
+
|
|
122
165
|
# Add endpoint for deleting a catalog
|
|
123
166
|
self.router.add_api_route(
|
|
124
167
|
path="/catalogs/{catalog_id}",
|
|
@@ -281,6 +324,55 @@ class CatalogsExtension(ApiExtension):
|
|
|
281
324
|
# Return the created catalog
|
|
282
325
|
return catalog
|
|
283
326
|
|
|
327
|
+
async def update_catalog(
|
|
328
|
+
self, catalog_id: str, catalog: Catalog, request: Request
|
|
329
|
+
) -> Catalog:
|
|
330
|
+
"""Update an existing catalog.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
catalog_id: The ID of the catalog to update.
|
|
334
|
+
catalog: The updated catalog data.
|
|
335
|
+
request: Request object.
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
The updated catalog.
|
|
339
|
+
|
|
340
|
+
Raises:
|
|
341
|
+
HTTPException: If the catalog is not found.
|
|
342
|
+
"""
|
|
343
|
+
try:
|
|
344
|
+
# Verify the catalog exists
|
|
345
|
+
existing_catalog_db = await self.client.database.find_catalog(catalog_id)
|
|
346
|
+
|
|
347
|
+
# Convert STAC catalog to database format
|
|
348
|
+
db_catalog = self.client.catalog_serializer.stac_to_db(catalog, request)
|
|
349
|
+
db_catalog_dict = db_catalog.model_dump()
|
|
350
|
+
db_catalog_dict["type"] = "Catalog"
|
|
351
|
+
|
|
352
|
+
# Preserve parent_ids and other internal fields from the existing catalog
|
|
353
|
+
if "parent_ids" in existing_catalog_db:
|
|
354
|
+
db_catalog_dict["parent_ids"] = existing_catalog_db["parent_ids"]
|
|
355
|
+
|
|
356
|
+
# Update the catalog in the database (upsert via create_catalog)
|
|
357
|
+
await self.client.database.create_catalog(db_catalog_dict, refresh=True)
|
|
358
|
+
|
|
359
|
+
# Return the updated catalog
|
|
360
|
+
return catalog
|
|
361
|
+
|
|
362
|
+
except HTTPException:
|
|
363
|
+
raise
|
|
364
|
+
except Exception as e:
|
|
365
|
+
error_msg = str(e)
|
|
366
|
+
if "not found" in error_msg.lower():
|
|
367
|
+
raise HTTPException(
|
|
368
|
+
status_code=404, detail=f"Catalog {catalog_id} not found"
|
|
369
|
+
)
|
|
370
|
+
logger.error(f"Error updating catalog {catalog_id}: {e}")
|
|
371
|
+
raise HTTPException(
|
|
372
|
+
status_code=500,
|
|
373
|
+
detail=f"Failed to update catalog: {str(e)}",
|
|
374
|
+
)
|
|
375
|
+
|
|
284
376
|
async def get_catalog(self, catalog_id: str, request: Request) -> Catalog:
|
|
285
377
|
"""Get a specific catalog by ID.
|
|
286
378
|
|
|
@@ -365,11 +457,9 @@ class CatalogsExtension(ApiExtension):
|
|
|
365
457
|
await self.client.database.find_catalog(catalog_id)
|
|
366
458
|
|
|
367
459
|
# Find all collections with this catalog in parent_ids
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
index=COLLECTIONS_INDEX, body=query_body
|
|
460
|
+
children = await search_collections_by_parent_id_shared(
|
|
461
|
+
self.client.database.client, catalog_id
|
|
371
462
|
)
|
|
372
|
-
children = [hit["_source"] for hit in search_result["hits"]["hits"]]
|
|
373
463
|
|
|
374
464
|
# Safe Unlink: Remove catalog from all children's parent_ids
|
|
375
465
|
# If a child becomes an orphan, adopt it to root
|
|
@@ -440,27 +530,15 @@ class CatalogsExtension(ApiExtension):
|
|
|
440
530
|
# Verify the catalog exists
|
|
441
531
|
await self.client.database.find_catalog(catalog_id)
|
|
442
532
|
|
|
443
|
-
# Query collections by parent_ids field
|
|
533
|
+
# Query collections by parent_ids field
|
|
444
534
|
# This uses the parent_ids field in the collection mapping to find all
|
|
445
535
|
# collections that have this catalog as a parent
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
try:
|
|
450
|
-
search_result = await self.client.database.client.search(
|
|
451
|
-
index=COLLECTIONS_INDEX, body=query_body
|
|
452
|
-
)
|
|
453
|
-
except Exception as e:
|
|
454
|
-
logger.error(
|
|
455
|
-
f"Error searching for collections with parent {catalog_id}: {e}"
|
|
456
|
-
)
|
|
457
|
-
search_result = {"hits": {"hits": []}}
|
|
536
|
+
collections_data = await search_collections_by_parent_id_shared(
|
|
537
|
+
self.client.database.client, catalog_id
|
|
538
|
+
)
|
|
458
539
|
|
|
459
|
-
# Extract collection IDs from
|
|
460
|
-
collection_ids = []
|
|
461
|
-
hits = search_result.get("hits", {}).get("hits", [])
|
|
462
|
-
for hit in hits:
|
|
463
|
-
collection_ids.append(hit.get("_id"))
|
|
540
|
+
# Extract collection IDs from results
|
|
541
|
+
collection_ids = [coll.get("id") for coll in collections_data]
|
|
464
542
|
|
|
465
543
|
# Fetch the collections
|
|
466
544
|
collections = []
|
|
@@ -524,6 +602,194 @@ class CatalogsExtension(ApiExtension):
|
|
|
524
602
|
status_code=404, detail=f"Catalog {catalog_id} not found"
|
|
525
603
|
)
|
|
526
604
|
|
|
605
|
+
async def get_catalog_catalogs(
|
|
606
|
+
self,
|
|
607
|
+
catalog_id: str,
|
|
608
|
+
request: Request,
|
|
609
|
+
limit: int = Query(10, ge=1, le=100),
|
|
610
|
+
token: Optional[str] = Query(None),
|
|
611
|
+
) -> Catalogs:
|
|
612
|
+
"""Get all sub-catalogs of a specific catalog with pagination.
|
|
613
|
+
|
|
614
|
+
Args:
|
|
615
|
+
catalog_id: The ID of the parent catalog.
|
|
616
|
+
request: Request object.
|
|
617
|
+
limit: Maximum number of results to return (default: 10, max: 100).
|
|
618
|
+
token: Pagination token for cursor-based pagination.
|
|
619
|
+
|
|
620
|
+
Returns:
|
|
621
|
+
A Catalogs response containing sub-catalogs with pagination links.
|
|
622
|
+
|
|
623
|
+
Raises:
|
|
624
|
+
HTTPException: If the catalog is not found.
|
|
625
|
+
"""
|
|
626
|
+
try:
|
|
627
|
+
# Verify the catalog exists
|
|
628
|
+
await self.client.database.find_catalog(catalog_id)
|
|
629
|
+
|
|
630
|
+
# Search for sub-catalogs with pagination
|
|
631
|
+
(
|
|
632
|
+
catalogs_data,
|
|
633
|
+
total_hits,
|
|
634
|
+
next_token,
|
|
635
|
+
) = await search_sub_catalogs_with_pagination_shared(
|
|
636
|
+
self.client.database.client, catalog_id, limit, token
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
# Serialize to STAC format
|
|
640
|
+
catalogs = []
|
|
641
|
+
for catalog_data in catalogs_data:
|
|
642
|
+
try:
|
|
643
|
+
catalog = self.client.catalog_serializer.db_to_stac(
|
|
644
|
+
catalog_data,
|
|
645
|
+
request,
|
|
646
|
+
extensions=[
|
|
647
|
+
type(ext).__name__
|
|
648
|
+
for ext in self.client.database.extensions
|
|
649
|
+
],
|
|
650
|
+
)
|
|
651
|
+
catalogs.append(catalog)
|
|
652
|
+
except Exception as e:
|
|
653
|
+
logger.error(
|
|
654
|
+
f"Error serializing catalog {catalog_data.get('id')}: {e}"
|
|
655
|
+
)
|
|
656
|
+
continue
|
|
657
|
+
|
|
658
|
+
# Generate pagination links
|
|
659
|
+
base_url = str(request.base_url)
|
|
660
|
+
links = [
|
|
661
|
+
{"rel": "root", "type": "application/json", "href": base_url},
|
|
662
|
+
{
|
|
663
|
+
"rel": "parent",
|
|
664
|
+
"type": "application/json",
|
|
665
|
+
"href": f"{base_url}catalogs/{catalog_id}",
|
|
666
|
+
},
|
|
667
|
+
{
|
|
668
|
+
"rel": "self",
|
|
669
|
+
"type": "application/json",
|
|
670
|
+
"href": str(request.url),
|
|
671
|
+
},
|
|
672
|
+
]
|
|
673
|
+
|
|
674
|
+
# Add next link if more results exist
|
|
675
|
+
if next_token:
|
|
676
|
+
query_params = {"limit": limit, "token": next_token}
|
|
677
|
+
links.append(
|
|
678
|
+
{
|
|
679
|
+
"rel": "next",
|
|
680
|
+
"href": f"{base_url}catalogs/{catalog_id}/catalogs?{urlencode(query_params)}",
|
|
681
|
+
"type": "application/json",
|
|
682
|
+
"title": "Next page",
|
|
683
|
+
}
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
return {
|
|
687
|
+
"catalogs": catalogs,
|
|
688
|
+
"links": links,
|
|
689
|
+
"numberReturned": len(catalogs),
|
|
690
|
+
"numberMatched": total_hits,
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
except HTTPException:
|
|
694
|
+
# Re-raise HTTP exceptions as-is
|
|
695
|
+
raise
|
|
696
|
+
except Exception as e:
|
|
697
|
+
logger.error(
|
|
698
|
+
f"Error retrieving catalogs for catalog {catalog_id}: {e}",
|
|
699
|
+
exc_info=True,
|
|
700
|
+
)
|
|
701
|
+
raise HTTPException(
|
|
702
|
+
status_code=404, detail=f"Catalog {catalog_id} not found"
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
async def create_catalog_catalog(
|
|
706
|
+
self, catalog_id: str, catalog: Catalog, request: Request
|
|
707
|
+
) -> Catalog:
|
|
708
|
+
"""Create a new catalog or link an existing catalog as a sub-catalog.
|
|
709
|
+
|
|
710
|
+
Logic:
|
|
711
|
+
1. Verifies the parent catalog exists.
|
|
712
|
+
2. If the sub-catalog already exists: Appends the parent ID to its parent_ids
|
|
713
|
+
(enabling poly-hierarchy - a catalog can have multiple parents).
|
|
714
|
+
3. If the sub-catalog is new: Creates it with parent_ids initialized to [catalog_id].
|
|
715
|
+
|
|
716
|
+
Args:
|
|
717
|
+
catalog_id: The ID of the parent catalog.
|
|
718
|
+
catalog: The catalog to create or link.
|
|
719
|
+
request: Request object.
|
|
720
|
+
|
|
721
|
+
Returns:
|
|
722
|
+
The created or linked catalog.
|
|
723
|
+
|
|
724
|
+
Raises:
|
|
725
|
+
HTTPException: If the parent catalog is not found or operation fails.
|
|
726
|
+
"""
|
|
727
|
+
try:
|
|
728
|
+
# 1. Verify the parent catalog exists
|
|
729
|
+
await self.client.database.find_catalog(catalog_id)
|
|
730
|
+
|
|
731
|
+
# 2. Check if the sub-catalog already exists
|
|
732
|
+
try:
|
|
733
|
+
existing_catalog = await self.client.database.find_catalog(catalog.id)
|
|
734
|
+
|
|
735
|
+
# --- UPDATE PATH (Existing Catalog) ---
|
|
736
|
+
# We are linking an existing catalog to a new parent (poly-hierarchy)
|
|
737
|
+
|
|
738
|
+
# Ensure parent_ids list exists
|
|
739
|
+
if "parent_ids" not in existing_catalog:
|
|
740
|
+
existing_catalog["parent_ids"] = []
|
|
741
|
+
|
|
742
|
+
# Append if not already present
|
|
743
|
+
if catalog_id not in existing_catalog["parent_ids"]:
|
|
744
|
+
existing_catalog["parent_ids"].append(catalog_id)
|
|
745
|
+
|
|
746
|
+
# Persist the update
|
|
747
|
+
await update_catalog_in_index_shared(
|
|
748
|
+
self.client.database.client, catalog.id, existing_catalog
|
|
749
|
+
)
|
|
750
|
+
logger.info(
|
|
751
|
+
f"Linked existing catalog {catalog.id} to parent {catalog_id}"
|
|
752
|
+
)
|
|
753
|
+
|
|
754
|
+
# Return the STAC object
|
|
755
|
+
return self.client.catalog_serializer.db_to_stac(
|
|
756
|
+
existing_catalog, request
|
|
757
|
+
)
|
|
758
|
+
|
|
759
|
+
except NotFoundError:
|
|
760
|
+
# --- CREATE PATH (New Catalog) ---
|
|
761
|
+
# Catalog does not exist, so we create it
|
|
762
|
+
|
|
763
|
+
# Convert STAC catalog to database format
|
|
764
|
+
db_catalog = self.client.catalog_serializer.stac_to_db(catalog, request)
|
|
765
|
+
|
|
766
|
+
# Convert to dict
|
|
767
|
+
db_catalog_dict = db_catalog.model_dump()
|
|
768
|
+
db_catalog_dict["type"] = "Catalog"
|
|
769
|
+
|
|
770
|
+
# Initialize parent_ids
|
|
771
|
+
db_catalog_dict["parent_ids"] = [catalog_id]
|
|
772
|
+
|
|
773
|
+
# Create in DB
|
|
774
|
+
await self.client.database.create_catalog(db_catalog_dict, refresh=True)
|
|
775
|
+
logger.info(
|
|
776
|
+
f"Created new catalog {catalog.id} with parent {catalog_id}"
|
|
777
|
+
)
|
|
778
|
+
|
|
779
|
+
return catalog
|
|
780
|
+
|
|
781
|
+
except HTTPException:
|
|
782
|
+
raise
|
|
783
|
+
except Exception as e:
|
|
784
|
+
logger.error(
|
|
785
|
+
f"Error processing sub-catalog {catalog.id} in parent {catalog_id}: {e}",
|
|
786
|
+
exc_info=True,
|
|
787
|
+
)
|
|
788
|
+
raise HTTPException(
|
|
789
|
+
status_code=500,
|
|
790
|
+
detail=f"Failed to process sub-catalog: {str(e)}",
|
|
791
|
+
)
|
|
792
|
+
|
|
527
793
|
async def create_catalog_collection(
|
|
528
794
|
self, catalog_id: str, collection: Collection, request: Request
|
|
529
795
|
) -> stac_types.Collection:
|
|
@@ -792,57 +1058,14 @@ class CatalogsExtension(ApiExtension):
|
|
|
792
1058
|
# 1. Verify the parent catalog exists
|
|
793
1059
|
await self.client.database.find_catalog(catalog_id)
|
|
794
1060
|
|
|
795
|
-
# 2.
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
# Base filter: Parent match
|
|
799
|
-
# This finds anything where 'parent_ids' contains this catalog_id
|
|
800
|
-
filter_queries = [{"term": {"parent_ids": catalog_id}}]
|
|
801
|
-
|
|
802
|
-
# Optional filter: Type
|
|
803
|
-
if type:
|
|
804
|
-
# If user asks for ?type=Catalog, we only return Catalogs
|
|
805
|
-
filter_queries.append({"term": {"type": type}})
|
|
806
|
-
|
|
807
|
-
# 3. Calculate Pagination (Search After)
|
|
808
|
-
body = {
|
|
809
|
-
"query": {"bool": {"filter": filter_queries}},
|
|
810
|
-
"sort": [{"id": {"order": "asc"}}], # Stable sort for pagination
|
|
811
|
-
"size": limit,
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
# Handle search_after token - split by '|' to get all sort values
|
|
815
|
-
search_after: Optional[List[str]] = None
|
|
816
|
-
if token:
|
|
817
|
-
try:
|
|
818
|
-
# The token should be a pipe-separated string of sort values
|
|
819
|
-
# e.g., "collection-1"
|
|
820
|
-
from typing import cast
|
|
821
|
-
|
|
822
|
-
search_after_parts = cast(List[str], token.split("|"))
|
|
823
|
-
# If the number of sort fields doesn't match token parts, ignore the token
|
|
824
|
-
if len(search_after_parts) != len(body["sort"]): # type: ignore
|
|
825
|
-
search_after = None
|
|
826
|
-
else:
|
|
827
|
-
search_after = search_after_parts
|
|
828
|
-
except Exception:
|
|
829
|
-
search_after = None
|
|
830
|
-
|
|
831
|
-
if search_after is not None:
|
|
832
|
-
body["search_after"] = search_after
|
|
833
|
-
|
|
834
|
-
# 4. Execute Search
|
|
835
|
-
search_result = await self.client.database.client.search(
|
|
836
|
-
index=COLLECTIONS_INDEX, body=body
|
|
1061
|
+
# 2. Search for children with pagination
|
|
1062
|
+
children_data, total, next_token = await search_children_with_pagination_shared(
|
|
1063
|
+
self.client.database.client, catalog_id, limit, token, type
|
|
837
1064
|
)
|
|
838
1065
|
|
|
839
|
-
#
|
|
840
|
-
hits = search_result.get("hits", {}).get("hits", [])
|
|
841
|
-
total = search_result.get("hits", {}).get("total", {}).get("value", 0)
|
|
842
|
-
|
|
1066
|
+
# 3. Serialize children based on type
|
|
843
1067
|
children = []
|
|
844
|
-
for
|
|
845
|
-
doc = hit["_source"]
|
|
1068
|
+
for doc in children_data:
|
|
846
1069
|
resource_type = doc.get(
|
|
847
1070
|
"type", "Collection"
|
|
848
1071
|
) # Default to Collection if missing
|
|
@@ -856,7 +1079,7 @@ class CatalogsExtension(ApiExtension):
|
|
|
856
1079
|
|
|
857
1080
|
children.append(child)
|
|
858
1081
|
|
|
859
|
-
#
|
|
1082
|
+
# 4. Format Response
|
|
860
1083
|
# The Children extension uses a specific response format
|
|
861
1084
|
response = {
|
|
862
1085
|
"children": children,
|
|
@@ -877,14 +1100,7 @@ class CatalogsExtension(ApiExtension):
|
|
|
877
1100
|
"numberMatched": total,
|
|
878
1101
|
}
|
|
879
1102
|
|
|
880
|
-
#
|
|
881
|
-
next_token = None
|
|
882
|
-
if len(hits) == limit:
|
|
883
|
-
next_token_values = hits[-1].get("sort")
|
|
884
|
-
if next_token_values:
|
|
885
|
-
# Join all sort values with '|' to create the token
|
|
886
|
-
next_token = "|".join(str(val) for val in next_token_values)
|
|
887
|
-
|
|
1103
|
+
# 5. Generate Next Link
|
|
888
1104
|
if next_token:
|
|
889
1105
|
# Get existing query params
|
|
890
1106
|
parsed_url = urlparse(str(request.url))
|
|
@@ -355,6 +355,9 @@ class CatalogSerializer(Serializer):
|
|
|
355
355
|
# Avoid modifying the input dict in-place
|
|
356
356
|
catalog = deepcopy(catalog)
|
|
357
357
|
|
|
358
|
+
# Remove internal fields (not part of STAC spec)
|
|
359
|
+
catalog.pop("parent_ids", None)
|
|
360
|
+
|
|
358
361
|
# Set defaults
|
|
359
362
|
catalog.setdefault("type", "Catalog")
|
|
360
363
|
catalog.setdefault("stac_extensions", [])
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""library version."""
|
|
2
|
-
__version__ = "6.
|
|
2
|
+
__version__ = "6.10.1"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stac_fastapi_core-6.9.0 → stac_fastapi_core-6.10.1}/stac_fastapi/core/base_database_logic.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stac_fastapi_core-6.9.0 → stac_fastapi_core-6.10.1}/stac_fastapi/core/extensions/__init__.py
RENAMED
|
File without changes
|
{stac_fastapi_core-6.9.0 → stac_fastapi_core-6.10.1}/stac_fastapi/core/extensions/aggregation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stac_fastapi_core-6.9.0 → stac_fastapi_core-6.10.1}/stac_fastapi/core/route_dependencies.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|