Skip to content

Commit aeebb45

Browse files
Features API item delete support (#1156)
* add delete feature functionality * fix refs * format * guardrail for deleting without a feature id * adjust error message * add delete collection capability * replace unused `pretty` with argskwargs * lint
1 parent 7d17324 commit aeebb45

File tree

5 files changed

+243
-4
lines changed

5 files changed

+243
-4
lines changed

planet/cli/features.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,19 @@ async def collection_get(ctx, collection_id, pretty):
9898
echo_json(result, pretty)
9999

100100

101+
@command(collections, name="delete")
102+
@click.argument("collection_id", required=True)
103+
async def collection_delete(ctx, collection_id, *args, **kwargs):
104+
"""Delete a collection by ID
105+
106+
Example:
107+
108+
planet features collections delete my-collection-123
109+
"""
110+
async with features_client(ctx) as cl:
111+
await cl.delete_collection(collection_id)
112+
113+
101114
@features.group()
102115
def items():
103116
"""commands for interacting with Features API items (features
@@ -150,6 +163,36 @@ async def item_get(ctx, collection_id, feature_id, pretty):
150163
echo_json(feature, pretty)
151164

152165

166+
@command(items, name="delete")
167+
@click.argument("collection_id")
168+
@click.argument("feature_id", required=False)
169+
async def item_delete(ctx, collection_id, feature_id, *args, **kwargs):
170+
"""Delete a feature in a collection.
171+
172+
You may supply either a collection ID and a feature ID, or
173+
a feature reference.
174+
175+
Example:
176+
177+
planet features items delete my-collection-123 item123
178+
planet features items delete "pl:features/my/my-collection-123/item123"
179+
"""
180+
181+
# ensure that either collection_id and feature_id were supplied, or that
182+
# a feature ref was supplied as a single value.
183+
if not ((collection_id and feature_id) or
184+
("pl:features" in collection_id)):
185+
raise ClickException(
186+
"Must supply either collection_id and feature_id, or a valid feature reference."
187+
)
188+
189+
if collection_id.startswith("pl:features"):
190+
collection_id, feature_id = split_ref(collection_id)
191+
192+
async with features_client(ctx) as cl:
193+
await cl.delete_item(collection_id, feature_id)
194+
195+
153196
@command(items, name="add")
154197
@click.argument("collection_id", required=True)
155198
@click.argument("filename", required=True)

planet/clients/features.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from typing import Any, AsyncIterator, Optional, Union, TypeVar
1717

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

171+
async def delete_item(self, collection_id: str, feature_id: str) -> None:
172+
"""
173+
Delete a feature from a collection.
174+
175+
Parameters:
176+
collection_id: The ID of the collection containing the feature
177+
feature_id: The ID of the feature to delete
178+
179+
Example:
180+
181+
```
182+
await features_client.delete_item(
183+
collection_id="my-collection",
184+
feature_id="feature-123"
185+
)
186+
```
187+
"""
188+
189+
# fail early instead of sending a delete request without a feature id.
190+
if len(feature_id) < 1:
191+
raise ClientError("Must provide a feature id")
192+
193+
url = f'{self._base_url}/collections/{collection_id}/items/{feature_id}'
194+
await self._session.request(method='DELETE', url=url)
195+
170196
async def create_collection(self,
171197
title: str,
172198
description: Optional[str] = None) -> str:
@@ -192,6 +218,29 @@ async def create_collection(self,
192218

193219
return resp.json()["id"]
194220

221+
async def delete_collection(self, collection_id: str) -> None:
222+
"""
223+
Delete a collection.
224+
225+
Parameters:
226+
collection_id: The ID of the collection to delete
227+
228+
Example:
229+
230+
```
231+
await features_client.delete_collection(
232+
collection_id="my-collection"
233+
)
234+
```
235+
"""
236+
237+
# fail early instead of sending a delete request without a collection id.
238+
if len(collection_id) < 1:
239+
raise ClientError("Must provide a collection id")
240+
241+
url = f'{self._base_url}/collections/{collection_id}'
242+
await self._session.request(method='DELETE', url=url)
243+
195244
async def add_items(self,
196245
collection_id: str,
197246
feature: Union[dict, GeoInterface],

planet/sync/features.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,23 @@ def create_collection(self,
7878
collection = self._client.create_collection(title, description)
7979
return self._client._call_sync(collection)
8080

81+
def delete_collection(self, collection_id: str) -> None:
82+
"""
83+
Delete a collection.
84+
85+
Parameters:
86+
collection_id: The ID of the collection to delete
87+
88+
Example:
89+
90+
```
91+
pl = Planet()
92+
pl.features.delete_collection(collection_id="my-collection")
93+
```
94+
"""
95+
return self._client._call_sync(
96+
self._client.delete_collection(collection_id))
97+
8198
def list_items(self,
8299
collection_id: str,
83100
limit: int = 0) -> Iterator[Feature]:
@@ -114,6 +131,24 @@ def get_item(self, collection_id: str, feature_id: str) -> Feature:
114131
return self._client._call_sync(
115132
self._client.get_item(collection_id, feature_id))
116133

134+
def delete_item(self, collection_id: str, feature_id: str) -> None:
135+
"""
136+
Delete a feature from a collection.
137+
138+
Parameters:
139+
collection_id: The ID of the collection containing the feature
140+
feature_id: The ID of the feature to delete
141+
142+
Example:
143+
144+
```
145+
pl = Planet()
146+
pl.features.delete_item(collection_id="my-collection", feature_id="feature-123")
147+
```
148+
"""
149+
return self._client._call_sync(
150+
self._client.delete_item(collection_id, feature_id))
151+
117152
def add_items(self,
118153
collection_id: str,
119154
feature: Union[dict, GeoInterface],

tests/integration/test_features_api.py

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,11 @@ def __geo_interface__(self) -> dict:
5959
return TEST_GEOM
6060

6161

62-
def mock_response(url: str, json: Any, method: str = "get"):
63-
mock_resp = httpx.Response(HTTPStatus.OK, json=json)
62+
def mock_response(url: str,
63+
json: Any,
64+
method: str = "get",
65+
status_code: int = HTTPStatus.OK):
66+
mock_resp = httpx.Response(status_code, json=json)
6467
respx.request(method, url).return_value = mock_resp
6568

6669

@@ -260,3 +263,53 @@ def assertf(resp):
260263
req_body = json.loads(respx.calls[0].request.content)
261264
assert req_body["type"] == "Feature"
262265
assert req_body["geometry"] == expected_body
266+
267+
268+
@respx.mock
269+
async def test_get_item():
270+
collection_id = "test"
271+
item_id = "test123"
272+
items_url = f"{TEST_URL}/collections/{collection_id}/items/{item_id}"
273+
274+
mock_response(items_url, to_feature_model(item_id))
275+
276+
def assertf(resp):
277+
assert resp["id"] == item_id
278+
279+
assertf(await cl_async.get_item(collection_id, item_id))
280+
assertf(cl_sync.get_item(collection_id, item_id))
281+
282+
283+
@respx.mock
284+
async def test_delete_item():
285+
collection_id = "test"
286+
item_id = "test123"
287+
items_url = f"{TEST_URL}/collections/{collection_id}/items/{item_id}"
288+
289+
mock_response(items_url,
290+
json=None,
291+
method="delete",
292+
status_code=HTTPStatus.NO_CONTENT)
293+
294+
def assertf(resp):
295+
assert resp is None
296+
297+
assertf(await cl_async.delete_item(collection_id, item_id))
298+
assertf(cl_sync.delete_item(collection_id, item_id))
299+
300+
301+
@respx.mock
302+
async def test_delete_collection():
303+
collection_id = "test"
304+
collections_url = f"{TEST_URL}/collections/{collection_id}"
305+
306+
mock_response(collections_url,
307+
json=None,
308+
method="delete",
309+
status_code=HTTPStatus.NO_CONTENT)
310+
311+
def assertf(resp):
312+
assert resp is None
313+
314+
assertf(await cl_async.delete_collection(collection_id))
315+
assertf(cl_sync.delete_collection(collection_id))

tests/integration/test_features_cli.py

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from http import HTTPStatus
12
import tempfile
23
import json
34
import pytest
@@ -6,7 +7,7 @@
67
from click.testing import CliRunner
78

89
from planet.cli import cli
9-
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
10+
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
1011

1112

1213
def invoke(*args):
@@ -17,7 +18,11 @@ def invoke(*args):
1718

1819
result = runner.invoke(cli.main, args=args)
1920
assert result.exit_code == 0, result.output
20-
return json.loads(result.output)
21+
if len(result.output) > 0:
22+
return json.loads(result.output)
23+
24+
# some commands (delete) return no value.
25+
return None
2126

2227

2328
@respx.mock
@@ -122,3 +127,57 @@ def assertf(resp):
122127
req_body = json.loads(respx.calls[0].request.content)
123128
assert req_body["type"] == "Feature"
124129
assert req_body["geometry"] == expected_body
130+
131+
132+
@respx.mock
133+
def test_get_item():
134+
collection_id = "test"
135+
item_id = "test123"
136+
get_item_url = f'{TEST_URL}/collections/{collection_id}/items/{item_id}'
137+
138+
mock_response(get_item_url,
139+
json=to_feature_model("test123"),
140+
method="get",
141+
status_code=HTTPStatus.OK)
142+
143+
def assertf(resp):
144+
assert resp["id"] == "test123"
145+
146+
assertf(invoke("items", "get", collection_id, item_id))
147+
assertf(invoke("items", "get",
148+
f"pl:features/my/{collection_id}/{item_id}"))
149+
150+
151+
@respx.mock
152+
def test_delete_item():
153+
collection_id = "test"
154+
item_id = "test123"
155+
delete_item_url = f'{TEST_URL}/collections/{collection_id}/items/{item_id}'
156+
157+
mock_response(delete_item_url,
158+
json=None,
159+
method="delete",
160+
status_code=HTTPStatus.NO_CONTENT)
161+
162+
def assertf(resp):
163+
assert resp is None
164+
165+
assertf(invoke("items", "delete", collection_id, item_id))
166+
assertf(
167+
invoke("items", "delete", f"pl:features/my/{collection_id}/{item_id}"))
168+
169+
170+
@respx.mock
171+
def test_delete_collection():
172+
collection_id = "test"
173+
collection_url = f'{TEST_URL}/collections/{collection_id}'
174+
175+
mock_response(collection_url,
176+
json=None,
177+
method="delete",
178+
status_code=HTTPStatus.NO_CONTENT)
179+
180+
def assertf(resp):
181+
assert resp is None
182+
183+
assertf(invoke("collections", "delete", collection_id))

0 commit comments

Comments
 (0)