From 507602e74338f7ee8cb3de7dc5080e0376ac1c1b Mon Sep 17 00:00:00 2001 From: wiki0831 Date: Wed, 26 Mar 2025 15:35:57 -0400 Subject: [PATCH 01/10] support data api clear coverage - async client - cli --- planet/cli/data.py | 77 +++++++++++++++++++++++++++++++++--------- planet/clients/data.py | 59 ++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 16 deletions(-) diff --git a/planet/cli/data.py b/planet/cli/data.py index 1c444ee2..e03b843f 100644 --- a/planet/cli/data.py +++ b/planet/cli/data.py @@ -638,22 +638,67 @@ async def asset_wait(ctx, item_type, item_id, asset_type, delay, max_attempts): click.echo(status) -# @data.command() -# @click.pass_context -# @translate_exceptions -# @coro -# @click.argument("item_type") -# @click.argument("item_id") -# @click.argument("asset_type_id") -# @pretty -# async def asset_get(ctx, item_type, item_id, asset_type_id, pretty): -# """Get an item asset.""" -# async with data_client(ctx) as cl: -# asset = await cl.get_asset(item_type, item_id, asset_type_id) -# echo_json(asset, pretty) - -# TODO: search_run()". -# TODO: item_get()". +@data.command() # type: ignore +@click.pass_context +@translate_exceptions +@coro +@click.argument("item_type") +@click.argument("item_id") +@click.argument("asset_type_id") +async def asset_get(ctx, item_type, item_id, asset_type_id): + """Get an item asset.""" + async with data_client(ctx) as cl: + asset = await cl.get_asset(item_type, item_id, asset_type_id) + echo_json(asset, pretty) + + +@data.command() # type: ignore +@click.pass_context +@translate_exceptions +@coro +@click.argument("item_type", type=str, callback=check_item_type) +@click.argument("item_id") +async def asset_list(ctx, item_type, item_id): + """List item assets.""" + async with data_client(ctx) as cl: + item_assets = await cl.list_item_assets(item_type, item_id) + echo_json(item_assets, pretty) + + +@data.command() # type: ignore +@click.pass_context +@translate_exceptions +@coro +@click.argument("item_type", type=str, callback=check_item_type) +@click.argument("item_id") +async def item_get(ctx, item_type, item_id): + """Get an item.""" + async with data_client(ctx) as cl: + item = await cl.get_item(item_type, item_id) + echo_json(item, pretty) + + +@data.command() # type: ignore +@click.pass_context +@translate_exceptions +@coro +@click.argument("item_type", type=str, callback=check_item_type) +@click.argument("item_id") +@click.option("--geom", + type=types.Geometry(), + callback=check_geom, + required=True) +@click.option('--mode', type=str, required=False) +@click.option('--band', type=str, required=False) +async def item_coverage(ctx, item_type, item_id, geom, mode, band): + """Get item clear coverage.""" + async with data_client(ctx) as cl: + item_assets = await cl.get_item_coverage(item_type, + item_id, + geom, + mode, + band) + echo_json(item_assets, pretty) @data.command() # type: ignore diff --git a/planet/clients/data.py b/planet/clients/data.py index 30817467..8ebee939 100644 --- a/planet/clients/data.py +++ b/planet/clients/data.py @@ -434,6 +434,65 @@ async def get_stats(self, json=request) return response.json() + async def get_item(self, item_type_id: str, item_id: str) -> dict: + """Get an item. + + Retrives item details using the provided item_type_id and item_id + + Parameters: + item_type_id: Item type identifier. + item_id: Item identifier. + + Returns: + Description of an item. + + Raises: + planet.exceptions.APIError: On API error. + """ + url = self._item_url(item_type_id, item_id) + + response = await self._session.request(method="GET", url=url) + return response.json() + + async def get_item_coverage( + self, + item_type_id: str, + item_id: str, + geometry: GeojsonLike, + mode: Optional[str] = None, + band: Optional[str] = None, + ) -> dict: + """Estimate the clear coverage over an item within a custom AOI + + Parameters: + item_type_id: Item type identifier. + item_id: Item identifier. + geometry: A feature reference or a GeoJSON + mode: Method used for coverage calculation + band: Specific band to extract from UDM2 + + Returns: + Description of the clear coverage for the provided AOI within the scene. + + Raises: + planet.exceptions.APIError: On API error. + """ + url = f"{self._item_url(item_type_id, item_id)}/coverage" + + params = {} + if mode is not None: + params["mode"] = mode + if band is not None: + params["band"] = band + + request_json = {'geometry': as_geom_or_ref(geometry)} + + response = await self._session.request(method="POST", + url=url, + json=request_json, + params=params) + return response.json() + async def list_item_assets(self, item_type_id: str, item_id: str) -> dict: """List all assets available for an item. From 833d6e7e0c10fca48b3816837fdae2df5c4a0dc7 Mon Sep 17 00:00:00 2001 From: wiki0831 Date: Wed, 26 Mar 2025 16:10:55 -0400 Subject: [PATCH 02/10] add to sync client --- planet/clients/data.py | 4 +--- planet/sync/data.py | 46 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/planet/clients/data.py b/planet/clients/data.py index 8ebee939..fbbe4916 100644 --- a/planet/clients/data.py +++ b/planet/clients/data.py @@ -435,9 +435,7 @@ async def get_stats(self, return response.json() async def get_item(self, item_type_id: str, item_id: str) -> dict: - """Get an item. - - Retrives item details using the provided item_type_id and item_id + """Get an item by item_type_id and item_id. Parameters: item_type_id: Item type identifier. diff --git a/planet/sync/data.py b/planet/sync/data.py index a611e41e..4a4c9620 100644 --- a/planet/sync/data.py +++ b/planet/sync/data.py @@ -413,3 +413,49 @@ def validate_checksum(asset: Dict[str, Any], filename: Path): checksums do not match. """ return DataClient.validate_checksum(asset, filename) + + def get_item(self, item_type_id: str, item_id: str) -> Dict[str, Any]: + """Get an item by item_type_id and item_id. + + Parameters: + item_type_id: Item type identifier. + item_id: Item identifier. + + Returns: + Description of an item. + + Raises: + planet.exceptions.APIError: On API error. + """ + return self._client._call_sync( + self._client.get_item(item_type_id, item_id)) + + def get_item_coverage( + self, + item_type_id: str, + item_id: str, + geometry: GeojsonLike, + mode: Optional[str] = None, + band: Optional[str] = None, + ) -> Dict[str, Any]: + """Get clear coverage for an item within a custom area of interest. + + Parameters: + item_type_id: Item type identifier. + item_id: Item identifier. + geometry: A feature reference or a GeoJSON + mode: Method used for coverage calculation + band: Specific band to extract from UDM2 + + Returns: + Description of the clear coverage for the provided AOI within the scene. + + Raises: + planet.exceptions.APIError: On API error. + """ + return self._client._call_sync( + self._client.get_item_coverage(item_type_id=item_type_id, + item_id=item_id, + geometry=geometry, + mode=mode, + band=band)) From b8d64416267ff8234ec0141689cd6922ae494795 Mon Sep 17 00:00:00 2001 From: wiki0831 Date: Wed, 26 Mar 2025 18:59:56 -0400 Subject: [PATCH 03/10] sync test --- tests/integration/test_data_api.py | 81 ++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/tests/integration/test_data_api.py b/tests/integration/test_data_api.py index e4ae4401..1a8434c8 100644 --- a/tests/integration/test_data_api.py +++ b/tests/integration/test_data_api.py @@ -1576,3 +1576,84 @@ def test_validate_checksum_sync(hashes_match, md5_entry, expectation, tmpdir): with expectation: DataAPI.validate_checksum(basic_udm2_asset, testfile) + + +@respx.mock +@pytest.mark.anyio +async def test_get_item_success(item_descriptions, session): + """Test getting an item successfully.""" + item = item_descriptions[0] + item_id = item['id'] + item_type = item['properties']['item_type'] + item_url = f'{TEST_URL}/item-types/{item_type}/items/{item_id}' + + respx.get(item_url).return_value = httpx.Response(HTTPStatus.OK, json=item) + + cl = DataClient(session, base_url=TEST_URL) + result = await cl.get_item(item_type, item_id) + + assert result == item + assert respx.calls.last.request.url == item_url + assert respx.calls.last.response.status_code == HTTPStatus.OK + + +@respx.mock +@pytest.mark.anyio +async def test_get_item_not_found(item_descriptions, session): + """Test getting a non-existent item.""" + item_type = item_descriptions[0]['properties']['item_type'] + item_id = 'non-existent-id' + item_url = f'{TEST_URL}/item-types/{item_type}/items/{item_id}' + + respx.get(item_url).return_value = httpx.Response(404, json={}) + + cl = DataClient(session, base_url=TEST_URL) + with pytest.raises(exceptions.MissingResource): + await cl.get_item(item_type, item_id) + + +@respx.mock +@pytest.mark.anyio +async def test_get_item_coverage_success(item_descriptions, session): + """Test get item coverage successfully.""" + + item = item_descriptions[0] + item_id = item['id'] + item_type = item['properties']['item_type'] + + mock_response = {'clear_percent': 28, 'status': 'complete'} + + coverage_url = f'{TEST_URL}/item-types/{item_type}/items/{item_id}/coverage' + respx.post(coverage_url).return_value = httpx.Response(HTTPStatus.OK, + json=mock_response) + + cl = DataClient(session, base_url=TEST_URL) + result = await cl.get_item_coverage(item_type_id=item_type, + item_id=item_id, + geometry=item['geometry'], + mode='UDM2', + band='cloud') + + assert str(respx.calls.last.request.url).split('?')[0] == coverage_url + + assert respx.calls.last.request.url.params['mode'] == 'UDM2' + assert respx.calls.last.request.url.params['band'] == 'cloud' + + assert result == mock_response + + +@respx.mock +@pytest.mark.anyio +async def test_get_item_coverage_invalid_geometry(item_descriptions, session): + item = item_descriptions[0] + item_id = item['id'] + item_type = item['properties']['item_type'] + + invalid_geom = copy.deepcopy(item['geometry']) + invalid_geom['type'] = 'invalid_type' + + cl = DataClient(session, base_url=TEST_URL) + with pytest.raises(exceptions.GeoJSONError): + await cl.get_item_coverage(item_type_id=item_type, + item_id=item_id, + geometry=invalid_geom) From d3b5dee0a56ff53552de031d68599862bc45a615 Mon Sep 17 00:00:00 2001 From: wiki0831 Date: Wed, 26 Mar 2025 19:04:12 -0400 Subject: [PATCH 04/10] async test --- tests/integration/test_data_api.py | 73 ++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/tests/integration/test_data_api.py b/tests/integration/test_data_api.py index 1a8434c8..e717de74 100644 --- a/tests/integration/test_data_api.py +++ b/tests/integration/test_data_api.py @@ -1597,6 +1597,23 @@ async def test_get_item_success(item_descriptions, session): assert respx.calls.last.response.status_code == HTTPStatus.OK +@respx.mock +def test_get_item_success_sync(item_descriptions, data_api): + """Test getting an item successfully.""" + item = item_descriptions[0] + item_id = item['id'] + item_type = item['properties']['item_type'] + item_url = f'{TEST_URL}/item-types/{item_type}/items/{item_id}' + + respx.get(item_url).return_value = httpx.Response(HTTPStatus.OK, json=item) + + result = data_api.get_item(item_type, item_id) + + assert result == item + assert respx.calls.last.request.url == item_url + assert respx.calls.last.response.status_code == HTTPStatus.OK + + @respx.mock @pytest.mark.anyio async def test_get_item_not_found(item_descriptions, session): @@ -1612,6 +1629,19 @@ async def test_get_item_not_found(item_descriptions, session): await cl.get_item(item_type, item_id) +@respx.mock +def test_get_item_not_found_sync(item_descriptions, data_api): + """Test getting a non-existent item.""" + item_type = item_descriptions[0]['properties']['item_type'] + item_id = 'non-existent-id' + item_url = f'{TEST_URL}/item-types/{item_type}/items/{item_id}' + + respx.get(item_url).return_value = httpx.Response(404, json={}) + + with pytest.raises(exceptions.MissingResource): + data_api.get_item(item_type, item_id) + + @respx.mock @pytest.mark.anyio async def test_get_item_coverage_success(item_descriptions, session): @@ -1657,3 +1687,46 @@ async def test_get_item_coverage_invalid_geometry(item_descriptions, session): await cl.get_item_coverage(item_type_id=item_type, item_id=item_id, geometry=invalid_geom) + + +@respx.mock +def test_get_item_coverage_success_sync(item_descriptions, data_api): + """Test get item coverage successfully.""" + item = item_descriptions[0] + item_id = item['id'] + item_type = item['properties']['item_type'] + + mock_response = {'clear_percent': 28, 'status': 'complete'} + + coverage_url = f'{TEST_URL}/item-types/{item_type}/items/{item_id}/coverage' + respx.post(coverage_url).return_value = httpx.Response(HTTPStatus.OK, + json=mock_response) + + result = data_api.get_item_coverage(item_type_id=item_type, + item_id=item_id, + geometry=item['geometry'], + mode='UDM2', + band='cloud') + + assert str(respx.calls.last.request.url).split('?')[0] == coverage_url + + assert respx.calls.last.request.url.params['mode'] == 'UDM2' + assert respx.calls.last.request.url.params['band'] == 'cloud' + + assert result == mock_response + + +@respx.mock +def test_get_item_coverage_invalid_geometry_sync(item_descriptions, data_api): + """Test get item coverage with invalid geometry.""" + item = item_descriptions[0] + item_id = item['id'] + item_type = item['properties']['item_type'] + + invalid_geom = copy.deepcopy(item['geometry']) + invalid_geom['type'] = 'invalid_type' + + with pytest.raises(exceptions.GeoJSONError): + data_api.get_item_coverage(item_type_id=item_type, + item_id=item_id, + geometry=invalid_geom) From 1e3e33180ed7b38705e9dcffe5c6498dc265e16d Mon Sep 17 00:00:00 2001 From: wiki0831 Date: Wed, 26 Mar 2025 19:58:05 -0400 Subject: [PATCH 05/10] cli test --- tests/integration/test_data_cli.py | 201 +++++++++++++++++++++-------- 1 file changed, 148 insertions(+), 53 deletions(-) diff --git a/tests/integration/test_data_cli.py b/tests/integration/test_data_cli.py index a7e02c1e..92f73544 100644 --- a/tests/integration/test_data_cli.py +++ b/tests/integration/test_data_cli.py @@ -1072,59 +1072,154 @@ def test_asset_wait(invoke, assert "status: active" in result.output -# @respx.mock -# def test_asset_get(invoke): -# item_type = 'PSScene' -# item_id = '20221003_002705_38_2461xx' -# asset_type_id = 'basic_udm2' -# dl_url = f'{TEST_URL}/1?token=IAmAToken' - -# basic_udm2_asset = { -# "_links": { -# "_self": "SELFURL", -# "activate": "ACTIVATEURL", -# "type": "https://api.planet.com/data/v1/asset-types/basic_udm2" -# }, -# "_permissions": ["download"], -# "md5_digest": None, -# "status": 'active', -# "location": dl_url, -# "type": "basic_udm2" -# } - -# page_response = { -# "basic_analytic_4b": { -# "_links": { -# "_self": -# "SELFURL", -# "activate": -# "ACTIVATEURL", -# "type": -# "https://api.planet.com/data/v1/asset-types/basic_analytic_4b" -# }, -# "_permissions": ["download"], -# "md5_digest": None, -# "status": "inactive", -# "type": "basic_analytic_4b" -# }, -# "basic_udm2": basic_udm2_asset -# } - -# mock_resp = httpx.Response(HTTPStatus.OK, json=page_response) -# assets_url = f'{TEST_URL}/item-types/{item_type}/items/{item_id}/assets' -# respx.get(assets_url).return_value = mock_resp - -# runner = CliRunner() -# result = invoke(['asset-get', item_type, item_id, asset_type_id], -# runner=runner) - -# assert not result.exception -# assert json.dumps(basic_udm2_asset) in result.output - -# TODO: basic test for "planet data search-list". -# TODO: basic test for "planet data search-run". -# TODO: basic test for "planet data item-get". -# TODO: basic test for "planet data stats". +@respx.mock +def test_asset_get_success(invoke, + mock_asset_get_response, + item_type, + item_id, + asset_type): + """Test successful asset get command.""" + mock_asset_get_response() + result = invoke(["asset-get", item_type, item_id, asset_type]) + assert result.exit_code == 0 + response = json.loads(result.output) + assert response["status"] == "active" + assert response["type"] == asset_type + + +@respx.mock +def test_asset_get_not_found(invoke, item_type, item_id): + """Test asset get command with non-existent asset.""" + asset_url = f'{TEST_URL}/item-types/{item_type}/items/{item_id}/assets' + respx.get(asset_url).return_value = httpx.Response(HTTPStatus.NOT_FOUND, + json={}) + + result = invoke(["asset-get", item_type, item_id, "non_existent_asset"]) + assert result.exit_code == 1 + + +@respx.mock +def test_asset_list_success(invoke, + mock_asset_get_response, + item_type, + item_id): + """Test successful asset list command.""" + mock_asset_get_response() + result = invoke(["asset-list", item_type, item_id]) + assert result.exit_code == 0 + assets = json.loads(result.output) + assert "basic_udm2" in assets + assert "basic_analytic_4b" in assets + assert assets["basic_udm2"]["status"] == "active" + assert assets["basic_analytic_4b"]["status"] == "inactive" + + +@respx.mock +def test_asset_list_not_found(invoke, item_type, item_id): + """Test asset list command with non-existent item.""" + asset_url = f'{TEST_URL}/item-types/{item_type}/items/{item_id}/assets' + respx.get(asset_url).return_value = httpx.Response(HTTPStatus.NOT_FOUND, + json={}) + + result = invoke(["asset-list", item_type, item_id]) + assert result.exit_code == 1 + + +@respx.mock +def test_item_get_success(invoke, item_type, item_id, search_result): + """Test successful item get command.""" + item_url = f'{TEST_URL}/item-types/{item_type}/items/{item_id}' + mock_resp = httpx.Response(HTTPStatus.OK, json=search_result) + respx.get(item_url).return_value = mock_resp + + result = invoke(["item-get", item_type, item_id]) + assert result.exit_code == 0 + assert json.loads(result.output) == search_result + + +@respx.mock +def test_item_get_not_found(invoke, item_type, item_id): + """Test item get command with non-existent item.""" + item_url = f'{TEST_URL}/item-types/{item_type}/items/{item_id}' + mock_resp = httpx.Response(HTTPStatus.NOT_FOUND, json={}) + respx.get(item_url).return_value = mock_resp + + result = invoke(["item-get", item_type, item_id]) + assert result.exit_code == 1 + + +@respx.mock +def test_item_coverage_success(invoke, item_type, item_id, geom_geojson): + """Test successful item coverage command.""" + coverage_url = f'{TEST_URL}/item-types/{item_type}/items/{item_id}/coverage' + mock_coverage = {"clear_percent": 90.0, "status": "complete"} + mock_resp = httpx.Response(HTTPStatus.OK, json=mock_coverage) + respx.post(coverage_url).return_value = mock_resp + + result = invoke([ + "item-coverage", + item_type, + item_id, + "--geom", + json.dumps(geom_geojson) + ]) + assert result.exit_code == 0 + coverage = json.loads(result.output) + assert coverage["clear_percent"] == 90.0 + assert coverage["status"] == "complete" + + +@respx.mock +def test_item_coverage_with_mode_and_band(invoke, + item_type, + item_id, + geom_geojson): + """Test item coverage command with mode and band options.""" + coverage_url = f'{TEST_URL}/item-types/{item_type}/items/{item_id}/coverage' + mock_coverage = {"cloud_percent": 90.0, "status": "complete"} + mock_resp = httpx.Response(HTTPStatus.OK, json=mock_coverage) + respx.post(coverage_url).return_value = mock_resp + + result = invoke([ + "item-coverage", + item_type, + item_id, + "--geom", + json.dumps(geom_geojson), + "--mode", + "UDM2", + "--band", + "cloud" + ]) + assert result.exit_code == 0 + coverage = json.loads(result.output) + assert coverage["cloud_percent"] == 90.0 + assert coverage["status"] == "complete" + + +@respx.mock +def test_item_coverage_invalid_geometry(invoke, item_type, item_id): + """Test item coverage command with invalid geometry.""" + result = invoke( + ["item-coverage", item_type, item_id, "--geom", "invalid geom"]) + assert result.exit_code == 1 + + +@respx.mock +def test_item_coverage_not_found(invoke, item_type, item_id, geom_geojson): + """Test item coverage command with non-existent item.""" + coverage_url = f'{TEST_URL}/item-types/{item_type}/items/{item_id}/coverage' + mock_resp = httpx.Response(HTTPStatus.NOT_FOUND, json={}) + respx.post(coverage_url).return_value = mock_resp + + result = invoke([ + "item-coverage", + item_type, + item_id, + "--geom", + json.dumps(geom_geojson) + ]) + assert result.exit_code == 1 @respx.mock From aacfd4ca26188d21bf7ce6b7356226064b2ea44d Mon Sep 17 00:00:00 2001 From: wiki0831 Date: Wed, 26 Mar 2025 21:02:53 -0400 Subject: [PATCH 06/10] update CLI doc --- design-docs/CLI-Data.md | 185 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 168 insertions(+), 17 deletions(-) diff --git a/design-docs/CLI-Data.md b/design-docs/CLI-Data.md index d855703d..2cafd8dc 100644 --- a/design-docs/CLI-Data.md +++ b/design-docs/CLI-Data.md @@ -588,7 +588,7 @@ A series of GeoJSON descriptions for each of the returned items. ### Interface ``` -planet data item-get [OPTIONS] ID ITEM_TYPE +planet data item-get [OPTIONS] ITEM_TYPE ITEM_ID Get an item. @@ -601,7 +601,7 @@ Options: --pretty - flag. Pretty-print output Output: -A full GeoJSON description of the returned item. +A full GeoJSON description of the item, including its properties, assets, and metadata. ``` ### Usage Examples @@ -609,23 +609,174 @@ A full GeoJSON description of the returned item. User Story: As a CLI user I would like to get the details of an item ``` -$ planet data item-get 20210819_162141_68_2276 PSScene +$ planet data item-get PSScene 20210819_162141_68_2276 {"_links": {...}, ..., "type": "Feature"} ``` +## item-coverage + +### Interface + +``` +planet data item-coverage [OPTIONS] ITEM_TYPE ITEM_ID + +Get item clear coverage within a custom area of interest. + +Arguments: +ITEM_TYPE - The type of item (e.g., PSScene) +ITEM_ID - The ID of the item + +Options: +--geom TEXT - A GeoJSON geometry or feature reference. [required] +--mode TEXT - Method used for coverage calculation (e.g., UDM2, estimate) +--band TEXT - Specific band to extract from UDM2 (e.g., cloud, haze) +--pretty - Pretty print the output. + +Output: +A JSON description of the clear coverage for the provided AOI within the scene. +``` + +### Usage Examples + +User Story: As a CLI user I want to get clear coverage information for a specific area within an item. + +```console +$ planet data item-coverage PSScene 20221003_002705_38_2461 \ + --geom '{"type": "Polygon", "coordinates": [[[37.791595458984375, 14.84923123791421], + [37.90214538574219, 14.84923123791421], + [37.90214538574219, 14.945448293647944], + [37.791595458984375, 14.945448293647944], + [37.791595458984375, 14.84923123791421]]]}' +``` +response (pretty-printed) +``` +{ + "clear_percent": 90.0, + "status": "complete" +} +``` + +User Story: As a CLI user I want to get haze coverage over my Feature Ref. + +```console +$ planet data item-coverage PSScene 20221003_002705_38_2461 \ + --geom 'pl:features/my/[collection-id]/[feature-id]' \ + --band haze +``` +response (pretty-printed) +``` +{ + "haze_percent": 90.0, + "status": "complete" +} +``` + +## asset-get + +### Interface + +planet data asset-get [OPTIONS] ITEM_TYPE ITEM_ID ASSET_TYPE_ID + +Get an item asset. + +Arguments: +ITEM_TYPE - The type of item (e.g., PSScene, SkySatScene) +ITEM_ID - The ID of the item +ASSET_TYPE_ID - The type of asset to get (e.g., basic_udm2) + +Output: +A JSON description of the asset, including its status, permissions, and download location if available. + +### Usage Examples + +User Story: As a CLI user I want to get information about a specific asset for an item. + +```console +$ planet data asset-get PSScene 20221003_002705_38_2461 basic_udm2 +``` +response (pretty-printed) +``` +{ + "_links": { + "_self": "SELFURL", + "activate": "ACTIVATEURL", + "type": "https://api.planet.com/data/v1/asset-types/basic_udm2" + }, + "_permissions": ["download"], + "md5_digest": null, + "status": "active", + "location": "https://api.planet.com/data/v1/1?token=IAmAToken", + "type": "basic_udm2" +} +``` + +## asset-list + +### Interface + +planet data asset-list [OPTIONS] ITEM_TYPE ITEM_ID + +List all assets available for an item. + +Options: +- --pretty - Pretty print the output. + +Arguments: +- ITEM_TYPE - The type of item (e.g., PSScene, SkySatScene) +- ITEM_ID - The ID of the item + +Output: +A JSON dictionary with asset_type_id as keys and asset descriptions as values. + +### Usage Examples + +User Story: As a CLI user I want to see all available assets for an item. + +```console +$ planet data asset-list PSScene 20221003_002705_38_2461 +``` +response (pretty-printed) +``` +{ + "basic_analytic_4b": { + "_links": { + "_self": "SELFURL", + "activate": "ACTIVATEURL", + "type": "https://api.planet.com/data/v1/asset-types/basic_analytic_4b" + }, + "_permissions": ["download"], + "md5_digest": null, + "status": "inactive", + "type": "basic_analytic_4b" + }, + "basic_udm2": { + "_links": { + "_self": "SELFURL", + "activate": "ACTIVATEURL", + "type": "https://api.planet.com/data/v1/asset-types/basic_udm2" + }, + "_permissions": ["download"], + "md5_digest": null, + "status": "active", + "location": "https://api.planet.com/data/v1/1?token=IAmAToken", + "type": "basic_udm2" + } +} +``` + ## asset-activate ### Interface ``` -planet data asset-activate ID ITEM_TYPE ASSET_TYPE +planet data asset-activate ITEM_TYPE ITEM_ID ASSET_TYPE Activate an asset. Arguments: -ID - string. Item identifier. ITEM_TYPE - string. Item type identifier. +ITEM_ID - string. Item identifier. ASSET_TYPE - string. Asset type identifier. Output: @@ -637,20 +788,20 @@ None. User Story: As a CLI user I would like to activate an asset for download. ``` -$ planet data asset-activate 20210819_162141_68_2276 PSScene analytic +$ planet data asset-activate PSScene 20210819_162141_68_2276 analytic ``` User Story: As a CLI user I would like to activate, wait, and then download an asset. ``` -$ ID=20210819_162141_68_2276 && \ -ITEM_TYPE=PSScene && \ +$ ITEM_TYPE=PSScene && \ +ITEM_ID=20210819_162141_68_2276 && \ ASSET_TYPE=analytic && \ -planet data asset-activate $ID $ITEM_TYPE $ASSET_TYPE && \ -planet data asset-wait $ID $ITEM_TYPE $ASSET_TYPE && \ +planet data asset-activate $ITEM_TYPE $ITEM_ID $ASSET_TYPE && \ +planet data asset-wait $ITEM_TYPE $ITEM_ID $ASSET_TYPE && \ planet data asset-download --directory data \ -$ID $ITEM_TYPE $ASSET_TYPE +$ITEM_TYPE $ITEM_ID $ASSET_TYPE data/.tif ``` @@ -660,15 +811,15 @@ data/.tif ### Interface ``` -planet data asset-wait ID ITEM_TYPE ASSET_TYPE +planet data asset-wait ITEM_TYPE ITEM_ID ASSET_TYPE Wait for an asset to be activated. -Returns when the asset state has reached ‘activated’ and the asset is available. +Returns when the asset state has reached 'activated' and the asset is available. Arguments: -ID - string. Item identifier. ITEM_TYPE - string. Item type identifier. +ITEM_ID - string. Item identifier. ASSET_TYPE - string. Asset type identifier. Output: @@ -680,15 +831,15 @@ None. ### Interface ``` -planet data asset-download [OPTIONS] ID ITEM_TYPE ASSET_TYPE +planet data asset-download [OPTIONS] ITEM_TYPE ITEM_ID ASSET_TYPE Download an activated asset. Will fail if the asset state is not activated. Consider calling `asset-wait` before this command to ensure the asset is activated. Arguments: -ID - string. Item identifier. ITEM_TYPE - string. Item type identifier. +ITEM_ID - string. Item identifier. ASSET_TYPE - string. Asset type identifier. Options: @@ -708,7 +859,7 @@ directory, overwriting if the file already exists, and silencing reporting. $ planet --quiet data asset-download \ --directory data \ --overwrite \ -20210819_162141_68_2276 PSScene analytic +PSScene 20210819_162141_68_2276 analytic data/.tif ``` From acd92235c984db6339b2e026177cecde1087b53b Mon Sep 17 00:00:00 2001 From: wiki0831 Date: Wed, 26 Mar 2025 22:21:14 -0400 Subject: [PATCH 07/10] add to cli docs --- docs/cli/cli-data.md | 53 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/docs/cli/cli-data.md b/docs/cli/cli-data.md index 6ee031df..65143539 100644 --- a/docs/cli/cli-data.md +++ b/docs/cli/cli-data.md @@ -424,6 +424,59 @@ curl -s https://raw.githubusercontent.com/ropensci/geojsonio/main/inst/examples/ Just pipe the results to `jq '.buckets | map(.count) | add'` and it’ll give you the total of all the values. +## Get Item Details and Assess Item Clear Coverage + +Once you've found items of interest through search, you may want to examine a specific item in detail. The CLI provides a command to retrieve and display detailed information about a single item. + +### Get Item + +The `item-get` command allows you to retrieve detailed information about a specific item by its Type and ID: + +```sh +planet data item-get PSScene 20230310_083933_71_2431 +``` + +### Get Item Coverage + +The `item-coverage` command estimates the clear coverage of an item within a specified area of interest (AOI). This is useful for determining how much of your area of interest is covered by clouds or other obstructions: + +```sh +planet data item-coverage PSScene 20230310_083933_71_2431 --geom geometry.geojson +``` + +You can also specify additional parameters: +- `--mode`: The mode for coverage calculation +- `--band`: The band to use for coverage calculation + +For example: +```sh +planet data item-coverage PSScene 20230310_083933_71_2431 --geom geometry.geojson --band "haze" +``` + +## Item Asset Management + +The CLI provides several commands for managing and working with item assets. + +### List Available Assets + +To see all available assets for a specific item: + +```sh +planet data asset-list PSScene 20230310_083933_71_2431 +``` + +This will show you all asset available for the item, including their status and activation requirements. + +### Get Asset Details + +To get detailed information about a specific asset: + +```sh +planet data asset-get PSScene 20230310_083933_71_2431 ortho_analytic_8b_sr +``` + +This command provides information about the asset's status download availability. + ## Asset Activation and Download While we recommend using the Orders or Subscriptions API’s to deliver Planet data, the Data API has the capability From e405d6ef13fa94f434ee3b9a6f6097e93469e100 Mon Sep 17 00:00:00 2001 From: wiki0831 Date: Thu, 27 Mar 2025 01:36:14 -0400 Subject: [PATCH 08/10] minor code tweak and doc refresh --- design-docs/CLI-Data.md | 23 +++++++++-------------- docs/cli/cli-data.md | 7 +++++-- planet/cli/data.py | 17 ++++++++++++++--- tests/integration/test_data_cli.py | 8 ++++---- 4 files changed, 32 insertions(+), 23 deletions(-) diff --git a/design-docs/CLI-Data.md b/design-docs/CLI-Data.md index 2cafd8dc..ae171a8b 100644 --- a/design-docs/CLI-Data.md +++ b/design-docs/CLI-Data.md @@ -642,17 +642,13 @@ A JSON description of the clear coverage for the provided AOI within the scene. User Story: As a CLI user I want to get clear coverage information for a specific area within an item. ```console -$ planet data item-coverage PSScene 20221003_002705_38_2461 \ - --geom '{"type": "Polygon", "coordinates": [[[37.791595458984375, 14.84923123791421], - [37.90214538574219, 14.84923123791421], - [37.90214538574219, 14.945448293647944], - [37.791595458984375, 14.945448293647944], - [37.791595458984375, 14.84923123791421]]]}' +$ planet data item-coverage PSScene 20250304_162555_90_24f2 \ + --geom '{"type":"Polygon","coordinates":[[[-81.45,30.31],[-81.45,30.23],[-81.38,30.23],[-81.45,30.31]]]}' ``` response (pretty-printed) ``` { - "clear_percent": 90.0, + "clear_percent": 95, "status": "complete" } ``` @@ -660,14 +656,14 @@ response (pretty-printed) User Story: As a CLI user I want to get haze coverage over my Feature Ref. ```console -$ planet data item-coverage PSScene 20221003_002705_38_2461 \ +$ planet data item-coverage PSScene 20250304_162555_90_24f2 \ --geom 'pl:features/my/[collection-id]/[feature-id]' \ --band haze ``` response (pretty-printed) ``` { - "haze_percent": 90.0, + "haze_percent": 0.0, "status": "complete" } ``` @@ -705,8 +701,7 @@ response (pretty-printed) }, "_permissions": ["download"], "md5_digest": null, - "status": "active", - "location": "https://api.planet.com/data/v1/1?token=IAmAToken", + "status": "inactive", "type": "basic_udm2" } ``` @@ -788,7 +783,7 @@ None. User Story: As a CLI user I would like to activate an asset for download. ``` -$ planet data asset-activate PSScene 20210819_162141_68_2276 analytic +$ planet data asset-activate PSScene 20210819_162141_68_2276 basic_analytic_4b ``` User Story: As a CLI user I would like to activate, wait, and then download an @@ -797,7 +792,7 @@ asset. ``` $ ITEM_TYPE=PSScene && \ ITEM_ID=20210819_162141_68_2276 && \ -ASSET_TYPE=analytic && \ +ASSET_TYPE=basic_analytic_4b && \ planet data asset-activate $ITEM_TYPE $ITEM_ID $ASSET_TYPE && \ planet data asset-wait $ITEM_TYPE $ITEM_ID $ASSET_TYPE && \ planet data asset-download --directory data \ @@ -859,7 +854,7 @@ directory, overwriting if the file already exists, and silencing reporting. $ planet --quiet data asset-download \ --directory data \ --overwrite \ -PSScene 20210819_162141_68_2276 analytic +PSScene 20210819_162141_68_2276 basic_analytic_4b data/.tif ``` diff --git a/docs/cli/cli-data.md b/docs/cli/cli-data.md index 65143539..dd2f7aae 100644 --- a/docs/cli/cli-data.md +++ b/docs/cli/cli-data.md @@ -445,10 +445,13 @@ planet data item-coverage PSScene 20230310_083933_71_2431 --geom geometry.geojso ``` You can also specify additional parameters: -- `--mode`: The mode for coverage calculation -- `--band`: The band to use for coverage calculation + +* `--mode`: The mode for coverage calculation + +* `--band`: The band to use for coverage calculation For example: + ```sh planet data item-coverage PSScene 20230310_083933_71_2431 --geom geometry.geojson --band "haze" ``` diff --git a/planet/cli/data.py b/planet/cli/data.py index e03b843f..b1830333 100644 --- a/planet/cli/data.py +++ b/planet/cli/data.py @@ -687,9 +687,20 @@ async def item_get(ctx, item_type, item_id): @click.option("--geom", type=types.Geometry(), callback=check_geom, - required=True) -@click.option('--mode', type=str, required=False) -@click.option('--band', type=str, required=False) + required=True, + help="""Either a GeoJSON or a Features API reference""") +@click.option('--mode', + type=str, + required=False, + help="""Method used for coverage calculation. + 'UDM2': activates UDM2 asset for accurate coverage, may take time. + 'estimate': provides a quick rough estimate without activation""") +@click.option('--band', + type=str, + required=False, + help="""Specific band to extract from UDM2 + (e.g., 'clear', 'cloud', 'snow_ice'). + For full details, refer to the UDM2 product specifications.""") async def item_coverage(ctx, item_type, item_id, geom, mode, band): """Get item clear coverage.""" async with data_client(ctx) as cl: diff --git a/tests/integration/test_data_cli.py b/tests/integration/test_data_cli.py index 92f73544..5021a1be 100644 --- a/tests/integration/test_data_cli.py +++ b/tests/integration/test_data_cli.py @@ -1152,7 +1152,7 @@ def test_item_get_not_found(invoke, item_type, item_id): def test_item_coverage_success(invoke, item_type, item_id, geom_geojson): """Test successful item coverage command.""" coverage_url = f'{TEST_URL}/item-types/{item_type}/items/{item_id}/coverage' - mock_coverage = {"clear_percent": 90.0, "status": "complete"} + mock_coverage = {"clear_percent": 90, "status": "complete"} mock_resp = httpx.Response(HTTPStatus.OK, json=mock_coverage) respx.post(coverage_url).return_value = mock_resp @@ -1165,7 +1165,7 @@ def test_item_coverage_success(invoke, item_type, item_id, geom_geojson): ]) assert result.exit_code == 0 coverage = json.loads(result.output) - assert coverage["clear_percent"] == 90.0 + assert coverage["clear_percent"] == 90 assert coverage["status"] == "complete" @@ -1176,7 +1176,7 @@ def test_item_coverage_with_mode_and_band(invoke, geom_geojson): """Test item coverage command with mode and band options.""" coverage_url = f'{TEST_URL}/item-types/{item_type}/items/{item_id}/coverage' - mock_coverage = {"cloud_percent": 90.0, "status": "complete"} + mock_coverage = {"cloud_percent": 90, "status": "complete"} mock_resp = httpx.Response(HTTPStatus.OK, json=mock_coverage) respx.post(coverage_url).return_value = mock_resp @@ -1193,7 +1193,7 @@ def test_item_coverage_with_mode_and_band(invoke, ]) assert result.exit_code == 0 coverage = json.loads(result.output) - assert coverage["cloud_percent"] == 90.0 + assert coverage["cloud_percent"] == 90 assert coverage["status"] == "complete" From 2309d4763449e8474fb66f74a9c9d808f2acc8ab Mon Sep 17 00:00:00 2001 From: wiki0831 Date: Thu, 27 Mar 2025 10:40:17 -0400 Subject: [PATCH 09/10] doc example update --- design-docs/CLI-Data.md | 22 +++++++++------------- docs/cli/cli-data.md | 6 +++--- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/design-docs/CLI-Data.md b/design-docs/CLI-Data.md index ae171a8b..009769cc 100644 --- a/design-docs/CLI-Data.md +++ b/design-docs/CLI-Data.md @@ -601,7 +601,7 @@ Options: --pretty - flag. Pretty-print output Output: -A full GeoJSON description of the item, including its properties, assets, and metadata. +A full GeoJSON description of the returned item. ``` ### Usage Examples @@ -630,8 +630,7 @@ ITEM_ID - The ID of the item Options: --geom TEXT - A GeoJSON geometry or feature reference. [required] --mode TEXT - Method used for coverage calculation (e.g., UDM2, estimate) ---band TEXT - Specific band to extract from UDM2 (e.g., cloud, haze) ---pretty - Pretty print the output. +--band TEXT - Specific band to extract from UDM2 (e.g., cloud, snow) Output: A JSON description of the clear coverage for the provided AOI within the scene. @@ -645,7 +644,7 @@ User Story: As a CLI user I want to get clear coverage information for a specifi $ planet data item-coverage PSScene 20250304_162555_90_24f2 \ --geom '{"type":"Polygon","coordinates":[[[-81.45,30.31],[-81.45,30.23],[-81.38,30.23],[-81.45,30.31]]]}' ``` -response (pretty-printed) +response ``` { "clear_percent": 95, @@ -653,17 +652,17 @@ response (pretty-printed) } ``` -User Story: As a CLI user I want to get haze coverage over my Feature Ref. +User Story: As a CLI user I want to get snow coverage over my Feature Ref. ```console $ planet data item-coverage PSScene 20250304_162555_90_24f2 \ --geom 'pl:features/my/[collection-id]/[feature-id]' \ - --band haze + --band snow ``` -response (pretty-printed) +response ``` { - "haze_percent": 0.0, + "snow_percent": 0.0, "status": "complete" } ``` @@ -691,7 +690,7 @@ User Story: As a CLI user I want to get information about a specific asset for a ```console $ planet data asset-get PSScene 20221003_002705_38_2461 basic_udm2 ``` -response (pretty-printed) +response ``` { "_links": { @@ -714,9 +713,6 @@ planet data asset-list [OPTIONS] ITEM_TYPE ITEM_ID List all assets available for an item. -Options: -- --pretty - Pretty print the output. - Arguments: - ITEM_TYPE - The type of item (e.g., PSScene, SkySatScene) - ITEM_ID - The ID of the item @@ -731,7 +727,7 @@ User Story: As a CLI user I want to see all available assets for an item. ```console $ planet data asset-list PSScene 20221003_002705_38_2461 ``` -response (pretty-printed) +response ``` { "basic_analytic_4b": { diff --git a/docs/cli/cli-data.md b/docs/cli/cli-data.md index dd2f7aae..54dc02ec 100644 --- a/docs/cli/cli-data.md +++ b/docs/cli/cli-data.md @@ -441,7 +441,7 @@ planet data item-get PSScene 20230310_083933_71_2431 The `item-coverage` command estimates the clear coverage of an item within a specified area of interest (AOI). This is useful for determining how much of your area of interest is covered by clouds or other obstructions: ```sh -planet data item-coverage PSScene 20230310_083933_71_2431 --geom geometry.geojson +planet data item-coverage PSScene 20250304_162555_90_24f2 --geom='{"type":"Polygon","coordinates":[[[-81.45,30.31],[-81.45,30.23],[-81.38,30.23],[-81.45,30.31]]]}' ``` You can also specify additional parameters: @@ -453,7 +453,7 @@ You can also specify additional parameters: For example: ```sh -planet data item-coverage PSScene 20230310_083933_71_2431 --geom geometry.geojson --band "haze" +planet data item-coverage PSScene 20250304_162555_90_24f2 --geom='{"type":"Polygon","coordinates":[[[-81.45,30.31],[-81.45,30.23],[-81.38,30.23],[-81.45,30.31]]]}' --band='snow' ``` ## Item Asset Management @@ -475,7 +475,7 @@ This will show you all asset available for the item, including their status and To get detailed information about a specific asset: ```sh -planet data asset-get PSScene 20230310_083933_71_2431 ortho_analytic_8b_sr +planet data asset-get PSScene 20230310_083933_71_2431 ortho_analytic_8b ``` This command provides information about the asset's status download availability. From 7c20f2185631771d3b6f3483fe116da3411ad427 Mon Sep 17 00:00:00 2001 From: wiki0831 Date: Mon, 7 Apr 2025 00:11:23 -0400 Subject: [PATCH 10/10] improve CLI documentation Co-authored-by: Steve Hillier --- docs/cli/cli-data.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/cli/cli-data.md b/docs/cli/cli-data.md index 54dc02ec..8e745aa1 100644 --- a/docs/cli/cli-data.md +++ b/docs/cli/cli-data.md @@ -447,9 +447,13 @@ planet data item-coverage PSScene 20250304_162555_90_24f2 --geom='{"type":"Polyg You can also specify additional parameters: * `--mode`: The mode for coverage calculation + * `UDM2`: calculate clear coverage using a UDM2 asset [default] + * `estimate`: estimate clear coverage based on preview imagery * `--band`: The band to use for coverage calculation +See [Data API documentation](https://docs.planet.com/develop/apis/data/items/#estimate-clear-coverage-over-an-individual-item-with-a-custom-aoi) for an overview of coverage options. + For example: ```sh