From 87e78c11fe467d6088638e64d9313f44c06aa2a2 Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Tue, 8 Sep 2026 13:08:37 +0800 Subject: [PATCH 1/5] removed: deprecated extensions third party namespace --- CHANGES.md | 4 ++++ .../extensions/third_party/__init__.py | 17 ----------------- 2 files changed, 4 insertions(+), 17 deletions(-) delete mode 100644 stac_fastapi/extensions/stac_fastapi/extensions/third_party/__init__.py diff --git a/CHANGES.md b/CHANGES.md index d6331b230..716b41c1f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,10 @@ - Changed output from `string` to `dict` object (`BulkTransaction`) for bulk transaction `bulk_item_insert` methods. - Removed `app_host`, `app_port`, and `reload` attributes from `ApiSettings`. +### Removed + +- Removed deprecated `stac_fastapi.extensions.third_party` namespace. Import `BulkTransactionExtension` directly from `stac_fastapi.extensions` instead. + ## [6.6.0] - 2026-08-31 ### Changed diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/third_party/__init__.py b/stac_fastapi/extensions/stac_fastapi/extensions/third_party/__init__.py deleted file mode 100644 index 7380e423b..000000000 --- a/stac_fastapi/extensions/stac_fastapi/extensions/third_party/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -"""third_party backwards compatibility namespace.""" - -import warnings - -from ..bulk_transactions import BulkTransactionExtension - -# Issue a warning so users know to migrate, but the code won't break -warnings.warn( - """The 'stac_fastapi.extensions.third_party' namespace is deprecated - and will be removed in a future release. - Please import extensions directly from 'stac_fastapi.extensions' instead.""", - DeprecationWarning, - stacklevel=2, -) - - -__all__ = ("BulkTransactionExtension",) From b809e415bfff1d0a9041d4262a423e58fe934c10 Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Fri, 18 Sep 2026 13:59:08 +0800 Subject: [PATCH 2/5] fix: return location header for POST item/ collection --- CHANGES.md | 4 ++ .../extensions/transaction/transaction.py | 64 ++++++++++++++++++- .../extensions/tests/test_transaction.py | 9 ++- 3 files changed, 73 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 1122b02ed..5f76f3e9c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Return a `Location` header with the URI of the newly created resource for `POST /collections/{collection_id}/items` (single `Item`) and `POST /collections` in the Transaction extension, as required by the spec. `ItemCollection` payloads still return 201 without a `Location` header. + ## [7.0.0] - 2026-09-09 ### Changed diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/transaction/transaction.py b/stac_fastapi/extensions/stac_fastapi/extensions/transaction/transaction.py index ae5618c90..576a3efdb 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/transaction/transaction.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/transaction/transaction.py @@ -10,7 +10,9 @@ from pydantic import TypeAdapter from stac_pydantic import Collection, Item, ItemCollection from stac_pydantic.shared import MimeTypes +from starlette.requests import Request from starlette.responses import Response +from starlette.routing import NoMatchFound from stac_fastapi.api.models import CollectionUri, ItemUri, JSONResponse from stac_fastapi.api.routes import create_async_endpoint @@ -156,6 +158,15 @@ class PatchCollection(CollectionUri): ) +def _location_url(request: Request, route_name: str, **path_params: str) -> str: + """Build the canonical URL of a resource for the `Location` header.""" + try: + return str(request.url_for(route_name, **path_params)) + except NoMatchFound: + *_, resource_id = path_params.values() + return f"{request.base_url}{request.url.path.lstrip('/')}/{resource_id}" + + @attr.s class TransactionExtension(ApiExtension): """Transaction Extension. @@ -192,6 +203,34 @@ class TransactionExtension(ApiExtension): def register_create_item(self): """Register create item endpoint (POST /collections/{collection_id}/items).""" + endpoint = create_async_endpoint(self.client.create_item, PostItem) + + async def create_item( + request: Request, + response: Response, + request_data=Depends(PostItem), + ): + """Create item endpoint with Location header. + + The Transaction extension requires a `Location` header with the URI + of the newly created Item for single-Item payloads. + """ + item = await endpoint(request, request_data) + if request_data.item.type == "Feature" and not isinstance(item, Response): + item_id = ( + item.get("id") + if isinstance(item, dict) + else getattr(item, "id", None) + ) or request_data.item.id + if item_id: + response.headers["Location"] = _location_url( + request, + "Get Item", + collection_id=request_data.collection_id, + item_id=item_id, + ) + return item + self.router.add_api_route( name="Create Item", path="/collections/{collection_id}/items", @@ -209,7 +248,7 @@ def register_create_item(self): response_model_exclude_unset=True, response_model_exclude_none=True, methods=["POST"], - endpoint=create_async_endpoint(self.client.create_item, PostItem), + endpoint=create_item, dependencies=self.route_dependencies, ) @@ -303,6 +342,27 @@ def register_delete_item(self): def register_create_collection(self): """Register create collection endpoint (POST /collections).""" + endpoint = create_async_endpoint(self.client.create_collection, Collection) + + async def create_collection( + request: Request, + response: Response, + request_data: Collection, + ): + """Create collection endpoint with Location header.""" + collection = await endpoint(request, request_data) + if not isinstance(collection, Response): + collection_id = ( + collection.get("id") + if isinstance(collection, dict) + else getattr(collection, "id", None) + ) or request_data.id + if collection_id: + response.headers["Location"] = _location_url( + request, "Get Collection", collection_id=collection_id + ) + return collection + self.router.add_api_route( name="Create Collection", path="/collections", @@ -320,7 +380,7 @@ def register_create_collection(self): response_model_exclude_unset=True, response_model_exclude_none=True, methods=["POST"], - endpoint=create_async_endpoint(self.client.create_collection, Collection), + endpoint=create_collection, dependencies=self.route_dependencies, ) diff --git a/stac_fastapi/extensions/tests/test_transaction.py b/stac_fastapi/extensions/tests/test_transaction.py index 8f2c52b25..4ca3e40dd 100644 --- a/stac_fastapi/extensions/tests/test_transaction.py +++ b/stac_fastapi/extensions/tests/test_transaction.py @@ -99,8 +99,11 @@ def delete_collection(self, collection_id: str, **kwargs): def test_create_item(client: TestClient, item: Item) -> None: response = client.post("/collections/a-collection/items", json=item) - assert response.is_success, response.text + assert response.status_code == 201, response.text assert response.json()["type"] == "Feature" + assert response.headers["location"].endswith( + "/collections/a-collection/items/test_item" + ) def test_create_item_collection( @@ -109,6 +112,7 @@ def test_create_item_collection( response = client.post("/collections/a-collection/items", json=item_collection) assert response.is_success, response.text assert response.json()["type"] == "FeatureCollection" + assert "location" not in response.headers def test_update_item(client: TestClient, item: Item) -> None: @@ -155,8 +159,9 @@ def test_delete_item(client: TestClient) -> None: def test_create_collection(client: TestClient, collection: Collection) -> None: response = client.post("/collections", json=collection) - assert response.is_success, response.text + assert response.status_code == 201, response.text assert response.json()["type"] == "Collection" + assert response.headers["location"].endswith("/collections/test_collection") def test_update_collection(client: TestClient, collection: Collection) -> None: From 64b4d1ece2788bd1c6acd43353b8dca1ad785c8e Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Wed, 30 Sep 2026 18:19:21 +0800 Subject: [PATCH 3/5] deprecated: BulkTransactionExtension in favor of TransactionExtension --- CHANGES.md | 4 + .../bulk_transactions/bulk_transactions.py | 17 +++++ .../tests/test_bulk_transactions.py | 73 +++++++++++-------- 3 files changed, 63 insertions(+), 31 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 5f76f3e9c..385eae29e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Deprecated + +- Deprecated `BulkTransactionExtension` (`POST /collections/{collection_id}/bulk_items`). Use `TransactionExtension` instead, which supports `ItemCollection` payloads on `POST /collections/{collection_id}/items`. + ### Fixed - Return a `Location` header with the URI of the newly created resource for `POST /collections/{collection_id}/items` (single `Item`) and `POST /collections` in the Transaction extension, as required by the spec. `ItemCollection` payloads still return 201 without a `Location` header. diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/bulk_transactions/bulk_transactions.py b/stac_fastapi/extensions/stac_fastapi/extensions/bulk_transactions/bulk_transactions.py index 242026188..a12deea61 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/bulk_transactions/bulk_transactions.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/bulk_transactions/bulk_transactions.py @@ -1,6 +1,7 @@ """Bulk transactions extension.""" import abc +import warnings from collections.abc import Sequence from enum import StrEnum from typing import Any, TypedDict @@ -123,6 +124,10 @@ async def bulk_item_insert( class BulkTransactionExtension(ApiExtension): """Bulk Transaction Extension. + Deprecated: this extension is deprecated and will be removed in a future + release. Use `TransactionExtension` instead, which supports `ItemCollection` + payloads on `POST /collections/{collection_id}/items`. + Bulk Transaction extension adds the `POST /collections/{collection_id}/bulk_items` endpoint to the application for efficient bulk insertion of items. The input to this is an object with an @@ -151,6 +156,17 @@ class BulkTransactionExtension(ApiExtension): route_dependencies: Sequence[Depends] | None = attr.ib(default=None) enable_response_models: bool = attr.ib(default=True) + def __attrs_post_init__(self): + """Warn about the extension's deprecation.""" + warnings.warn( + "BulkTransactionExtension is deprecated and will be removed in a " + "future release. Use TransactionExtension instead, which supports " + "ItemCollection payloads on " + "POST /collections/{collection_id}/items.", + DeprecationWarning, + stacklevel=2, + ) + def register(self, app: FastAPI) -> None: """Register the extension with a FastAPI application. @@ -166,6 +182,7 @@ def register(self, app: FastAPI) -> None: router.add_api_route( name="Bulk Create Item", path="/collections/{collection_id}/bulk_items", + deprecated=True, response_model=BulkTransactionModel, responses={ 200: { diff --git a/stac_fastapi/extensions/tests/test_bulk_transactions.py b/stac_fastapi/extensions/tests/test_bulk_transactions.py index 1a7181a8e..b98ee0033 100644 --- a/stac_fastapi/extensions/tests/test_bulk_transactions.py +++ b/stac_fastapi/extensions/tests/test_bulk_transactions.py @@ -50,10 +50,17 @@ def test_base_client_chunks(): assert chunks_large == [[1, 2, 3, 4, 5, 6, 7]] +def test_bulk_transaction_extension_deprecated(): + """BulkTransactionExtension emits a DeprecationWarning.""" + with pytest.warns(DeprecationWarning, match="deprecated"): + BulkTransactionExtension(client=Mock(spec=AsyncBaseBulkTransactionsClient)) + + def test_bulk_transaction_extension_defaults(): """Test the default instantiation of the BulkTransactionExtension.""" mock_client = Mock(spec=AsyncBaseBulkTransactionsClient) - ext = BulkTransactionExtension(client=mock_client) + with pytest.warns(DeprecationWarning, match="deprecated"): + ext = BulkTransactionExtension(client=mock_client) assert ext.client == mock_client assert ext.schema_href is None @@ -67,12 +74,13 @@ def test_bulk_transaction_extension_customization(): custom_schema = "https://example.com/bulk-schema.json" custom_dependencies = [Depends(must_be_bob)] - ext = BulkTransactionExtension( - client=mock_client, - conformance_classes=custom_conformance, - schema_href=custom_schema, - route_dependencies=custom_dependencies, - ) + with pytest.warns(DeprecationWarning, match="deprecated"): + ext = BulkTransactionExtension( + client=mock_client, + conformance_classes=custom_conformance, + schema_href=custom_schema, + route_dependencies=custom_dependencies, + ) assert ext.conformance_classes == custom_conformance assert ext.schema_href == custom_schema @@ -154,13 +162,14 @@ def client( ) -> Iterator[TestClient]: """Fixture to set up the TestClient with the BulkTransactionExtension.""" settings = ApiSettings() - api = StacApi( - settings=settings, - client=core_client, - extensions=[ - BulkTransactionExtension(client=bulk_transactions_client), - ], - ) + with pytest.warns(DeprecationWarning, match="deprecated"): + api = StacApi( + settings=settings, + client=core_client, + extensions=[ + BulkTransactionExtension(client=bulk_transactions_client), + ], + ) with TestClient(api.app) as client: yield client @@ -169,13 +178,14 @@ def client( def client_with_error(core_client: DummyCoreClient) -> Iterator[TestClient]: """Fixture to set up the TestClient with the BulkTransactionExtension.""" settings = ApiSettings() - api = StacApi( - settings=settings, - client=core_client, - extensions=[ - BulkTransactionExtension(client=DummyBulkTransactionsClientWithError()), - ], - ) + with pytest.warns(DeprecationWarning, match="deprecated"): + api = StacApi( + settings=settings, + client=core_client, + extensions=[ + BulkTransactionExtension(client=DummyBulkTransactionsClientWithError()), + ], + ) with TestClient(api.app) as client: yield client @@ -242,16 +252,17 @@ def test_bulk_item_invalid_method(client: TestClient, item: dict) -> None: def test_bulk_item_insert_with_route_dependencies(item: dict) -> None: """Test route dependencies are applied to the bulk transactions route.""" settings = ApiSettings() - api = StacApi( - settings=settings, - client=DummyCoreClient(), - extensions=[ - BulkTransactionExtension( - client=DummyBulkTransactionsClient(), - route_dependencies=[Depends(must_be_bob)], - ), - ], - ) + with pytest.warns(DeprecationWarning, match="deprecated"): + api = StacApi( + settings=settings, + client=DummyCoreClient(), + extensions=[ + BulkTransactionExtension( + client=DummyBulkTransactionsClient(), + route_dependencies=[Depends(must_be_bob)], + ), + ], + ) payload = { "items": {item["id"]: item}, "method": "insert", From 79f484ce7b240f33501d6c3431089a7665a1ad5f Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Wed, 30 Sep 2026 18:33:45 +0800 Subject: [PATCH 4/5] changelog fix --- CHANGES.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 68d1d4496..5dc0d1830 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -29,10 +29,6 @@ - Removed deprecated `stac_fastapi.extensions.third_party` namespace. Import `BulkTransactionExtension` directly from `stac_fastapi.extensions` instead. ([#980](https://github.com/stac-utils/stac-fastapi/pull/980) - Removed deprecated `SortConformanceClasses` enum aliases (`COLLECTIONS`, `ITEMS`, `SEARCH`). Use `COLLECTION_SEARCH_SORT`, `FEATURES_SORT`, and `ITEM_SEARCH_SORT` instead. ([#979](https://github.com/stac-utils/stac-fastapi/pull/979)) -### Removed - -- Removed deprecated `stac_fastapi.extensions.third_party` namespace. Import `BulkTransactionExtension` directly from `stac_fastapi.extensions` instead. - ## [6.6.0] - 2026-08-31 ### Changed From af8fd5b9d07aa87410b342418b5e570effd1c101 Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Wed, 30 Sep 2026 18:36:39 +0800 Subject: [PATCH 5/5] add pr number --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 5dc0d1830..68bcc505e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,7 +4,7 @@ ### Deprecated -- Deprecated `BulkTransactionExtension` (`POST /collections/{collection_id}/bulk_items`). Use `TransactionExtension` instead, which supports `ItemCollection` payloads on `POST /collections/{collection_id}/items`. +- Deprecated `BulkTransactionExtension` (`POST /collections/{collection_id}/bulk_items`). Use `TransactionExtension` instead, which supports `ItemCollection` payloads on `POST /collections/{collection_id}/items`. ([#987](https://github.com/stac-utils/stac-fastapi/pull/987)) ### Fixed