Repository: stac-utils/stac-fastapi
Pull request #666 (2024-04-19 15:00:27)

Fix PUT collections
**Related Issue(s):**

- #665

**Description:**

* Change the registration of the PUT `/collections/{collection_id}` endpoint, to include the missing URL fragment when calling the `update_collection` function
* Add missing tests, and change function signatures in dummy clients to match `BaseTransactionsClient`
* Minor documentation fixes

**PR Checklist:**

- [x] `pre-commit` hooks pass locally
- [x] Tests pass (run `make test`)
- [x] Documentation has been updated to reflect changes, if applicable, and docs build successfully (run `make docs`)
- [x] Changes are added to the [CHANGELOG](https://github.com/stac-utils/stac-fastapi/blob/main/CHANGES.md).



Patch:
diff --git a/CHANGES.md b/CHANGES.md
index 0f057de..ca786b0 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -4,11 +4,12 @@
 
 ### Fixed
 
+* Fix missing payload for the PUT `collection/{collection_id}` endpoint ([#665](https://github.com/stac-utils/stac-fastapi/issues/665))
 * Return 400 for datetime errors ([#670](https://github.com/stac-utils/stac-fastapi/pull/670))
 
 ## [2.5.3] - 2024-04-23
 
-### Fixed 
+### Fixed
 
 * Remove the str2list converter from intersection queries via BaseSearchGetRequest ([#668](https://github.com/stac-utils/stac-fastapi/pull/668))
 * Apply datetime converter in ItemCollection endpoint model ([#667](https://github.com/stac-utils/stac-fastapi/pull/667))
diff --git a/stac_fastapi/api/stac_fastapi/api/models.py b/stac_fastapi/api/stac_fastapi/api/models.py
index c1d70b3..0721413 100644
--- a/stac_fastapi/api/stac_fastapi/api/models.py
+++ b/stac_fastapi/api/stac_fastapi/api/models.py
@@ -103,14 +103,14 @@ def create_post_request_model(
 
 @attr.s  # type:ignore
 class CollectionUri(APIRequest):
-    """Delete collection."""
+    """Get or delete collection."""
 
     collection_id: str = attr.ib(default=Path(..., description="Collection ID"))
 
 
 @attr.s
 class ItemUri(CollectionUri):
-    """Delete item."""
+    """Get or delete item."""
 
     item_id: str = attr.ib(default=Path(..., description="Item ID"))
 
diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py
index 0ebcc61..86e1bfc 100644
--- a/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py
+++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py
@@ -31,6 +31,13 @@ class PutItem(ItemUri):
     item: stac_types.Item = attr.ib(default=Body(None))
 
 
+@attr.s
+class PutCollection(CollectionUri):
+    """Update Collection."""
+
+    collection: stac_types.Collection = attr.ib(default=Body(None))
+
+
 @attr.s
 class TransactionExtension(ApiExtension):
     """Transaction Extension.
@@ -128,7 +135,7 @@ class TransactionExtension(ApiExtension):
             response_model_exclude_none=True,
             methods=["PUT"],
             endpoint=create_async_endpoint(
-                self.client.update_collection, stac_types.Collection
+                self.client.update_collection, PutCollection
             ),
         )
 
diff --git a/stac_fastapi/types/stac_fastapi/types/core.py b/stac_fastapi/types/stac_fastapi/types/core.py
index 77078ac..2c18649 100644
--- a/stac_fastapi/types/stac_fastapi/types/core.py
+++ b/stac_fastapi/types/stac_fastapi/types/core.py
@@ -109,10 +109,10 @@ class BaseTransactionsClient(abc.ABC):
     ) -> Optional[Union[stac_types.Collection, Response]]:
         """Perform a complete update on an existing collection.
 
-        Called with `PUT /collections/{collection_id}`. It is expected that this item
-        already exists.  The update should do a diff against the saved collection and
-        perform any necessary updates.  Partial updates are not supported by the
-        transactions extension.
+        Called with `PUT /collections/{collection_id}`. It is expected that this
+        collection already exists.  The update should do a diff against the saved
+        collection and perform any necessary updates.  Partial updates are not
+        supported by the transactions extension.
 
         Args:
             collection_id: id of the existing collection to be updated


Test patch:
diff --git a/stac_fastapi/extensions/tests/test_transaction.py b/stac_fastapi/extensions/tests/test_transaction.py
index fc5acc2..e6416ea 100644
--- a/stac_fastapi/extensions/tests/test_transaction.py
+++ b/stac_fastapi/extensions/tests/test_transaction.py
@@ -8,7 +8,7 @@ from stac_fastapi.api.app import StacApi
 from stac_fastapi.extensions.core import TransactionExtension
 from stac_fastapi.types.config import ApiSettings
 from stac_fastapi.types.core import BaseCoreClient, BaseTransactionsClient
-from stac_fastapi.types.stac import Item, ItemCollection
+from stac_fastapi.types.stac import Collection, Item, ItemCollection
 
 
 class DummyCoreClient(BaseCoreClient):
@@ -32,25 +32,32 @@ class DummyCoreClient(BaseCoreClient):
 
 
 class DummyTransactionsClient(BaseTransactionsClient):
-    """Defines a pattern for implementing the STAC transaction extension."""
+    """Dummy client returning parts of the request, rather than proper STAC items."""
 
-    def create_item(self, item: Union[Item, ItemCollection], *args, **kwargs):
-        return {"created": True, "type": item["type"]}
+    def create_item(self, item: Union[Item, ItemCollection], **kwargs):
+        return {"type": item["type"]}
 
-    def update_item(self, *args, **kwargs):
-        raise NotImplementedError
+    def update_item(self, collection_id: str, item_id: str, item: Item, **kwargs):
+        return {
+            "path_collection_id": collection_id,
+            "path_item_id": item_id,
+            "type": item["type"],
+        }
 
-    def delete_item(self, *args, **kwargs):
-        raise NotImplementedError
+    def delete_item(self, item_id: str, collection_id: str, **kwargs):
+        return {
+            "path_collection_id": collection_id,
+            "path_item_id": item_id,
+        }
 
-    def create_collection(self, *args, **kwargs):
-        raise NotImplementedError
+    def create_collection(self, collection: Collection, **kwargs):
+        return {"type": collection["type"]}
 
-    def update_collection(self, *args, **kwargs):
-        raise NotImplementedError
+    def update_collection(self, collection_id: str, collection: Collection, **kwargs):
+        return {"path_collection_id": collection_id, "type": collection["type"]}
 
-    def delete_collection(self, *args, **kwargs):
-        raise NotImplementedError
+    def delete_collection(self, collection_id: str, **kwargs):
+        return {"path_collection_id": collection_id}
 
 
 def test_create_item(client: TestClient, item: Item) -> None:
@@ -69,6 +76,42 @@ def test_create_item_collection(
     assert response.json()["type"] == "FeatureCollection"
 
 
+def test_update_item(client: TestClient, item: Item) -> None:
+    response = client.put(
+        "/collections/a-collection/items/an-item", content=json.dumps(item)
+    )
+    assert response.is_success, response.text
+    assert response.json()["path_collection_id"] == "a-collection"
+    assert response.json()["path_item_id"] == "an-item"
+    assert response.json()["type"] == "Feature"
+
+
+def test_delete_item(client: TestClient) -> None:
+    response = client.delete("/collections/a-collection/items/an-item")
+    assert response.is_success, response.text
+    assert response.json()["path_collection_id"] == "a-collection"
+    assert response.json()["path_item_id"] == "an-item"
+
+
+def test_create_collection(client: TestClient, collection: Collection) -> None:
+    response = client.post("/collections", content=json.dumps(collection))
+    assert response.is_success, response.text
+    assert response.json()["type"] == "Collection"
+
+
+def test_update_collection(client: TestClient, collection: Collection) -> None:
+    response = client.put("/collections/a-collection", content=json.dumps(collection))
+    assert response.is_success, response.text
+    assert response.json()["path_collection_id"] == "a-collection"
+    assert response.json()["type"] == "Collection"
+
+
+def test_delete_collection(client: TestClient, collection: Collection) -> None:
+    response = client.delete("/collections/a-collection")
+    assert response.is_success, response.text
+    assert response.json()["path_collection_id"] == "a-collection"
+
+
 @pytest.fixture
 def client(
     core_client: DummyCoreClient, transactions_client: DummyTransactionsClient
@@ -119,3 +162,19 @@ def item() -> Item:
         "assets": {},
         "collection": "test_collection",
     }
+
+
+@pytest.fixture
+def collection() -> Collection:
+    return {
+        "type": "Collection",
+        "stac_version": "1.0.0",
+        "stac_extensions": [],
+        "id": "test_collection",
+        "extent": {
+            "spatial": {"bbox": [[-180, -90, 180, 90]]},
+            "temporal": {
+                "interval": [["2000-01-01T00:00:00Z", "2024-01-01T00:00:00Z"]]
+            },
+        },
+    }