Skip to content

Features API item delete support #1156

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jun 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions planet/cli/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,19 @@ async def collection_get(ctx, collection_id, pretty):
echo_json(result, pretty)


@command(collections, name="delete")
@click.argument("collection_id", required=True)
async def collection_delete(ctx, collection_id, *args, **kwargs):
"""Delete a collection by ID

Example:

planet features collections delete my-collection-123
"""
async with features_client(ctx) as cl:
await cl.delete_collection(collection_id)


@features.group()
def items():
"""commands for interacting with Features API items (features
Expand Down Expand Up @@ -150,6 +163,36 @@ async def item_get(ctx, collection_id, feature_id, pretty):
echo_json(feature, pretty)


@command(items, name="delete")
@click.argument("collection_id")
@click.argument("feature_id", required=False)
async def item_delete(ctx, collection_id, feature_id, *args, **kwargs):
"""Delete a feature in a collection.

You may supply either a collection ID and a feature ID, or
a feature reference.

Example:

planet features items delete my-collection-123 item123
planet features items delete "pl:features/my/my-collection-123/item123"
"""

# ensure that either collection_id and feature_id were supplied, or that
# a feature ref was supplied as a single value.
if not ((collection_id and feature_id) or
("pl:features" in collection_id)):
raise ClickException(
"Must supply either collection_id and feature_id, or a valid feature reference."
)

if collection_id.startswith("pl:features"):
collection_id, feature_id = split_ref(collection_id)

async with features_client(ctx) as cl:
await cl.delete_item(collection_id, feature_id)


@command(items, name="add")
@click.argument("collection_id", required=True)
@click.argument("filename", required=True)
Expand Down
49 changes: 49 additions & 0 deletions planet/clients/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from typing import Any, AsyncIterator, Optional, Union, TypeVar

from planet.clients.base import _BaseClient
from planet.exceptions import ClientError
from planet.http import Session
from planet.models import Feature, GeoInterface, Paged
from planet.constants import PLANET_BASE_URL
Expand Down Expand Up @@ -167,6 +168,31 @@ async def get_item(self, collection_id: str, feature_id: str) -> Feature:
response = await self._session.request(method='GET', url=url)
return Feature(**response.json())

async def delete_item(self, collection_id: str, feature_id: str) -> None:
"""
Delete a feature from a collection.

Parameters:
collection_id: The ID of the collection containing the feature
feature_id: The ID of the feature to delete

Example:

```
await features_client.delete_item(
collection_id="my-collection",
feature_id="feature-123"
)
```
"""

# fail early instead of sending a delete request without a feature id.
if len(feature_id) < 1:
raise ClientError("Must provide a feature id")

url = f'{self._base_url}/collections/{collection_id}/items/{feature_id}'
await self._session.request(method='DELETE', url=url)

async def create_collection(self,
title: str,
description: Optional[str] = None) -> str:
Expand All @@ -192,6 +218,29 @@ async def create_collection(self,

return resp.json()["id"]

async def delete_collection(self, collection_id: str) -> None:
"""
Delete a collection.

Parameters:
collection_id: The ID of the collection to delete

Example:

```
await features_client.delete_collection(
collection_id="my-collection"
)
```
"""

# fail early instead of sending a delete request without a collection id.
if len(collection_id) < 1:
raise ClientError("Must provide a collection id")

url = f'{self._base_url}/collections/{collection_id}'
await self._session.request(method='DELETE', url=url)

async def add_items(self,
collection_id: str,
feature: Union[dict, GeoInterface],
Expand Down
35 changes: 35 additions & 0 deletions planet/sync/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,23 @@ def create_collection(self,
collection = self._client.create_collection(title, description)
return self._client._call_sync(collection)

def delete_collection(self, collection_id: str) -> None:
"""
Delete a collection.

Parameters:
collection_id: The ID of the collection to delete

Example:

```
pl = Planet()
pl.features.delete_collection(collection_id="my-collection")
```
"""
return self._client._call_sync(
self._client.delete_collection(collection_id))

def list_items(self,
collection_id: str,
limit: int = 0) -> Iterator[Feature]:
Expand Down Expand Up @@ -114,6 +131,24 @@ def get_item(self, collection_id: str, feature_id: str) -> Feature:
return self._client._call_sync(
self._client.get_item(collection_id, feature_id))

def delete_item(self, collection_id: str, feature_id: str) -> None:
"""
Delete a feature from a collection.

Parameters:
collection_id: The ID of the collection containing the feature
feature_id: The ID of the feature to delete

Example:

```
pl = Planet()
pl.features.delete_item(collection_id="my-collection", feature_id="feature-123")
```
"""
return self._client._call_sync(
self._client.delete_item(collection_id, feature_id))

def add_items(self,
collection_id: str,
feature: Union[dict, GeoInterface],
Expand Down
57 changes: 55 additions & 2 deletions tests/integration/test_features_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,11 @@ def __geo_interface__(self) -> dict:
return TEST_GEOM


def mock_response(url: str, json: Any, method: str = "get"):
mock_resp = httpx.Response(HTTPStatus.OK, json=json)
def mock_response(url: str,
json: Any,
method: str = "get",
status_code: int = HTTPStatus.OK):
mock_resp = httpx.Response(status_code, json=json)
respx.request(method, url).return_value = mock_resp


Expand Down Expand Up @@ -260,3 +263,53 @@ def assertf(resp):
req_body = json.loads(respx.calls[0].request.content)
assert req_body["type"] == "Feature"
assert req_body["geometry"] == expected_body


@respx.mock
async def test_get_item():
collection_id = "test"
item_id = "test123"
items_url = f"{TEST_URL}/collections/{collection_id}/items/{item_id}"

mock_response(items_url, to_feature_model(item_id))

def assertf(resp):
assert resp["id"] == item_id

assertf(await cl_async.get_item(collection_id, item_id))
assertf(cl_sync.get_item(collection_id, item_id))


@respx.mock
async def test_delete_item():
collection_id = "test"
item_id = "test123"
items_url = f"{TEST_URL}/collections/{collection_id}/items/{item_id}"

mock_response(items_url,
json=None,
method="delete",
status_code=HTTPStatus.NO_CONTENT)

def assertf(resp):
assert resp is None

assertf(await cl_async.delete_item(collection_id, item_id))
assertf(cl_sync.delete_item(collection_id, item_id))


@respx.mock
async def test_delete_collection():
collection_id = "test"
collections_url = f"{TEST_URL}/collections/{collection_id}"

mock_response(collections_url,
json=None,
method="delete",
status_code=HTTPStatus.NO_CONTENT)

def assertf(resp):
assert resp is None

assertf(await cl_async.delete_collection(collection_id))
assertf(cl_sync.delete_collection(collection_id))
63 changes: 61 additions & 2 deletions tests/integration/test_features_cli.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from http import HTTPStatus
import tempfile
import json
import pytest
Expand All @@ -6,7 +7,7 @@
from click.testing import CliRunner

from planet.cli import cli
from tests.integration.test_features_api import TEST_COLLECTION_1, TEST_COLLECTION_LIST, TEST_FEAT, TEST_GEOM, TEST_URL, list_collections_response, list_features_response, mock_response, to_collection_model
from tests.integration.test_features_api import TEST_COLLECTION_1, TEST_COLLECTION_LIST, TEST_FEAT, TEST_GEOM, TEST_URL, list_collections_response, list_features_response, mock_response, to_collection_model, to_feature_model


def invoke(*args):
Expand All @@ -17,7 +18,11 @@ def invoke(*args):

result = runner.invoke(cli.main, args=args)
assert result.exit_code == 0, result.output
return json.loads(result.output)
if len(result.output) > 0:
return json.loads(result.output)

# some commands (delete) return no value.
return None


@respx.mock
Expand Down Expand Up @@ -122,3 +127,57 @@ def assertf(resp):
req_body = json.loads(respx.calls[0].request.content)
assert req_body["type"] == "Feature"
assert req_body["geometry"] == expected_body


@respx.mock
def test_get_item():
collection_id = "test"
item_id = "test123"
get_item_url = f'{TEST_URL}/collections/{collection_id}/items/{item_id}'

mock_response(get_item_url,
json=to_feature_model("test123"),
method="get",
status_code=HTTPStatus.OK)

def assertf(resp):
assert resp["id"] == "test123"

assertf(invoke("items", "get", collection_id, item_id))
assertf(invoke("items", "get",
f"pl:features/my/{collection_id}/{item_id}"))


@respx.mock
def test_delete_item():
collection_id = "test"
item_id = "test123"
delete_item_url = f'{TEST_URL}/collections/{collection_id}/items/{item_id}'

mock_response(delete_item_url,
json=None,
method="delete",
status_code=HTTPStatus.NO_CONTENT)

def assertf(resp):
assert resp is None

assertf(invoke("items", "delete", collection_id, item_id))
assertf(
invoke("items", "delete", f"pl:features/my/{collection_id}/{item_id}"))


@respx.mock
def test_delete_collection():
collection_id = "test"
collection_url = f'{TEST_URL}/collections/{collection_id}'

mock_response(collection_url,
json=None,
method="delete",
status_code=HTTPStatus.NO_CONTENT)

def assertf(resp):
assert resp is None

assertf(invoke("collections", "delete", collection_id))