Repository: planetlabs/planet-client-python
Pull request #1109 (2025-03-27 14:38:07)

feat: add item coverage and asset management in DataClient
The changes focus on improving item assessment(`item-get`,`item-coverage`), asset management(`asset-get`, `asset-list`) and making the API more consistent.

1. implementations:
- `item-get`: new in Async client, Sync Client, CLI
- `item-coverage`: new in Async client, Sync Client, CLI
- `asset-get`: new(un-commented) in CLI
- `asset-list`: new in CLI
2. tests
- added async, sync, and cli test for the new implementations
4. doc updates: 
- `docs/cli/cli-data.md`
- `design-docs/CLI-DATA.md`
  - align parameter order with `ITEM_TYPE, ID` convention (few examples are out-of-date and no longer work)

ischneider: looks good so far!

one thing we've been trying to do w/ tests is combine the sync/async calls into a single case since the setup and assertions are the same, the only difference is in invocation... see `tests/integration/test_features_api.py` for an example of this (and no reason to clean up existing tests, more to stop proliferation of new cases)
wiki0831: > looks good so far!
> 
> one thing we've been trying to do w/ tests is combine the sync/async calls into a single case since the setup and assertions are the same, the only difference is in invocation... see `tests/integration/test_features_api.py` for an example of this (and no reason to clean up existing tests, more to stop proliferation of new cases)

thanks @ischneider 
yes I agree, writing double the test is kinda tedious. 

I was just trying to follow the existing pattern.

This should go into a separate PR. will create an issue for this.


Patch:
diff --git a/design-docs/CLI-Data.md b/design-docs/CLI-Data.md
index d855703..009769c 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.
 
@@ -609,23 +609,165 @@ 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, snow)
+
+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 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
+```
+{
+  "clear_percent": 95,
+  "status": "complete"
+}
+```
+
+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 snow
+```
+response
+```
+{
+  "snow_percent": 0.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
+```
+{
+  "_links": {
+    "_self": "SELFURL",
+    "activate": "ACTIVATEURL",
+    "type": "https://api.planet.com/data/v1/asset-types/basic_udm2"
+  },
+  "_permissions": ["download"],
+  "md5_digest": null,
+  "status": "inactive",
+  "type": "basic_udm2"
+}
+```
+
+## asset-list
+
+### Interface
+
+planet data asset-list [OPTIONS] ITEM_TYPE ITEM_ID
+
+List all assets available for an item.
+
+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
+```
+{
+  "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 +779,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 basic_analytic_4b
 ```
 
 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 && \
-ASSET_TYPE=analytic && \
-planet data asset-activate $ID $ITEM_TYPE $ASSET_TYPE && \
-planet data asset-wait $ID $ITEM_TYPE $ASSET_TYPE && \
+$ ITEM_TYPE=PSScene && \
+ITEM_ID=20210819_162141_68_2276 && \
+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 \
-$ID $ITEM_TYPE $ASSET_TYPE
+$ITEM_TYPE $ITEM_ID $ASSET_TYPE
 
 data/<psscene_naming 20210819_162141_68_2276>.tif
 ```
@@ -660,15 +802,15 @@ data/<psscene_naming 20210819_162141_68_2276>.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 +822,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 +850,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 basic_analytic_4b
 data/<psscene_naming 20210819_162141_68_2276>.tif
 ```
 
diff --git a/docs/cli/cli-data.md b/docs/cli/cli-data.md
index 6ee031d..8e745aa 100644
--- a/docs/cli/cli-data.md
+++ b/docs/cli/cli-data.md
@@ -424,6 +424,66 @@ 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 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:
+
+* `--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
+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
+
+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
+```
+
+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
diff --git a/planet/cli/data.py b/planet/cli/data.py
index 1c444ee..b183033 100644
--- a/planet/cli/data.py
+++ b/planet/cli/data.py
@@ -638,22 +638,78 @@ 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,
+              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:
+        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 3081746..fbbe491 100644
--- a/planet/clients/data.py
+++ b/planet/clients/data.py
@@ -434,6 +434,63 @@ class DataClient:
                                                json=request)
         return response.json()
 
+    async def get_item(self, item_type_id: str, item_id: str) -> dict:
+        """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.
+        """
+        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.
 
diff --git a/planet/sync/data.py b/planet/sync/data.py
index a611e41..4a4c962 100644
--- a/planet/sync/data.py
+++ b/planet/sync/data.py
@@ -413,3 +413,49 @@ class DataAPI:
                 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))


Test patch:
diff --git a/tests/integration/test_data_api.py b/tests/integration/test_data_api.py
index e4ae440..e717de7 100644
--- a/tests/integration/test_data_api.py
+++ b/tests/integration/test_data_api.py
@@ -1576,3 +1576,157 @@ 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
+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):
+    """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
+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):
+    """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)
+
+
+@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)
diff --git a/tests/integration/test_data_cli.py b/tests/integration/test_data_cli.py
index a7e02c1..5021a1b 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, "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
+    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, "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
+    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