From d71eb89af0768d0d2afc6bb8971e9a630c0f6c5c Mon Sep 17 00:00:00 2001 From: Kevin Lacaille Date: Tue, 28 Feb 2023 10:08:20 -0800 Subject: [PATCH 01/34] Added set for the new registered command. --- tests/integration/test_data_cli.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/integration/test_data_cli.py b/tests/integration/test_data_cli.py index 223f3b378..6cbc2f95b 100644 --- a/tests/integration/test_data_cli.py +++ b/tests/integration/test_data_cli.py @@ -55,6 +55,9 @@ def test_data_command_registered(invoke): assert "search-get" in result.output assert "search-delete" in result.output assert "search-update" in result.output + assert "asset_download" in result.output + # assert "asset_activate" in result.output + # assert "asset_wait" in result.output # Add other sub-commands here. From 0347cbc7f6c90447a97296c01aa3aaab1ea93a16 Mon Sep 17 00:00:00 2001 From: Kevin Lacaille Date: Tue, 28 Feb 2023 10:08:43 -0800 Subject: [PATCH 02/34] Added a todo for the new callback function. --- tests/unit/test_data_callbacks.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unit/test_data_callbacks.py b/tests/unit/test_data_callbacks.py index 41d7b78d5..7aa4e0b33 100644 --- a/tests/unit/test_data_callbacks.py +++ b/tests/unit/test_data_callbacks.py @@ -53,3 +53,5 @@ def test_item_type_fail(): ctx = MockContext() with pytest.raises(click.BadParameter): check_item_types(ctx, 'item_type', "bad_item_type") + +# def test_asset_type_success(item_types, bundles): From 3700ed65842c06043f8b42eb54ab87bb25af598b Mon Sep 17 00:00:00 2001 From: Kevin Lacaille Date: Tue, 28 Feb 2023 10:09:40 -0800 Subject: [PATCH 03/34] Added a skelleton to a needed callback function, along with the outline for the asset_download CLI command. --- planet/cli/data.py | 70 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/planet/cli/data.py b/planet/cli/data.py index e4fa562f5..fed01107f 100644 --- a/planet/cli/data.py +++ b/planet/cli/data.py @@ -14,6 +14,7 @@ """The Planet Data CLI.""" from typing import List, Optional from contextlib import asynccontextmanager +from pathlib import Path import click @@ -21,6 +22,7 @@ from planet.clients.data import (SEARCH_SORT, SEARCH_SORT_DEFAULT, STATS_INTERVAL) + from planet.specs import (get_item_types, validate_item_type, SpecificationException) @@ -74,6 +76,18 @@ def check_item_types(ctx, param, item_types) -> Optional[List[dict]]: raise click.BadParameter(str(e)) +async def check_asset_types(ctx, param, item_type, item_id, + asset_type_id) -> Optional[List[dict]]: + '''Validates the asset type by comparying the inputted asset type to all + supported asset types.''' + try: + async with data_client(ctx) as cl: + asset = cl.get_asset(item_type, item_id, asset_type_id) + return asset + except SpecificationException as e: + raise click.BadParameter(str(e)) + + def date_range_to_filter(ctx, param, values) -> Optional[List[dict]]: def _func(obj): @@ -399,8 +413,62 @@ async def search_update(ctx, echo_json(items, pretty) +@data.command() +@click.pass_context +@translate_exceptions +@coro +@click.argument('item_id') +@click.argument("item_types", + type=types.CommaSeparatedString(), + callback=check_item_types) +@click.argument("asset_types", + type=types.CommaSeparatedString(), + callback=check_asset_types) +@click.option('--directory', + default='.', + help=('Base directory for file download.'), + type=click.Path(exists=True, + resolve_path=True, + writable=True, + file_okay=False)) +@click.option('--overwrite', + is_flag=True, + default=False, + help=('Overwrite files if they already exist.')) +@click.option('--checksum', + default=None, + type=click.Choice(['MD5', 'SHA256'], case_sensitive=False), + help=('Verify that checksums match.')) +@pretty +async def asset_download(ctx, + item_id, + item_types, + asset_types, + directory, + overwrite, + pretty, + checksum): + """Download an activated asset. + + This function outputs the path to the downloaded asset, optionally + pretty-printed. + + This function will fail if the asset state is not activated. Consider + calling `asset-wait` before this command to ensure the asset is activated. + """ + quiet = ctx.obj['QUIET'] + async with data_client(ctx) as cl: + path = await cl.download_asset(asset_types, + filename, + Path(directory), + overwrite, + progress_bar=not quiet) + if checksum: + cl.validate_checksum(path, checksum) + echo_json(path, pretty) + + # TODO: search_run()". # TODO: item_get()". # TODO: asset_activate()". # TODO: asset_wait()". -# TODO: asset_download()". From 03998a8cc016290591f4b2c1d711ade04475b5ec Mon Sep 17 00:00:00 2001 From: Kevin Lacaille Date: Wed, 1 Mar 2023 11:59:09 -0800 Subject: [PATCH 04/34] Added commands: asset-download, get, activate, wait --- planet/cli/data.py | 109 ++++++++++++++++++++++++++++++--------------- 1 file changed, 74 insertions(+), 35 deletions(-) diff --git a/planet/cli/data.py b/planet/cli/data.py index fed01107f..aee635367 100644 --- a/planet/cli/data.py +++ b/planet/cli/data.py @@ -18,6 +18,7 @@ import click +from planet.reporting import StateBar from planet import data_filter, DataClient from planet.clients.data import (SEARCH_SORT, SEARCH_SORT_DEFAULT, @@ -76,16 +77,16 @@ def check_item_types(ctx, param, item_types) -> Optional[List[dict]]: raise click.BadParameter(str(e)) -async def check_asset_types(ctx, param, item_type, item_id, - asset_type_id) -> Optional[List[dict]]: - '''Validates the asset type by comparying the inputted asset type to all - supported asset types.''' - try: - async with data_client(ctx) as cl: - asset = cl.get_asset(item_type, item_id, asset_type_id) - return asset - except SpecificationException as e: - raise click.BadParameter(str(e)) +# async def check_asset(ctx, param, item_type, item_id, +# asset_type_id) -> Optional[List[dict]]: +# '''Validates the asset type by comparying the inputted asset type to all +# supported asset types.''' +# try: +# async with data_client(ctx) as cl: +# asset = cl.get_asset(item_type, item_id, asset_type_id) +# return asset +# except SpecificationException as e: +# raise click.BadParameter(str(e)) def date_range_to_filter(ctx, param, values) -> Optional[List[dict]]: @@ -417,13 +418,7 @@ async def search_update(ctx, @click.pass_context @translate_exceptions @coro -@click.argument('item_id') -@click.argument("item_types", - type=types.CommaSeparatedString(), - callback=check_item_types) -@click.argument("asset_types", - type=types.CommaSeparatedString(), - callback=check_asset_types) +@click.argument("asset", type=types.JSON()) @click.option('--directory', default='.', help=('Base directory for file download.'), @@ -431,41 +426,85 @@ async def search_update(ctx, resolve_path=True, writable=True, file_okay=False)) +@click.option('--filename', + default=None, + help=('Custom name to assign to downloaded file.'), + type=str) @click.option('--overwrite', is_flag=True, default=False, help=('Overwrite files if they already exist.')) @click.option('--checksum', + is_flag=True, default=None, - type=click.Choice(['MD5', 'SHA256'], case_sensitive=False), help=('Verify that checksums match.')) -@pretty -async def asset_download(ctx, - item_id, - item_types, - asset_types, - directory, - overwrite, - pretty, - checksum): +async def asset_download(ctx, asset, directory, filename, overwrite, checksum): """Download an activated asset. - This function outputs the path to the downloaded asset, optionally + This function outputs the path to the downloaded asset, optionally pretty-printed. - This function will fail if the asset state is not activated. Consider + This function will fail if the asset state is not activated. Consider calling `asset-wait` before this command to ensure the asset is activated. """ quiet = ctx.obj['QUIET'] async with data_client(ctx) as cl: - path = await cl.download_asset(asset_types, - filename, - Path(directory), - overwrite, + path = await cl.download_asset(asset=asset, + filename=filename, + directory=Path(directory), + overwrite=overwrite, progress_bar=not quiet) if checksum: - cl.validate_checksum(path, checksum) - echo_json(path, pretty) + cl.validate_checksum(asset, path) + + +@data.command() +@click.pass_context +@translate_exceptions +@coro +@click.argument("item_types") +@click.argument("item_id") +@click.argument("asset_type_id") +@pretty +async def asset_get(ctx, item_types, item_id, asset_type_id, pretty): + async with data_client(ctx) as cl: + asset = await cl.get_asset(item_types, item_id, asset_type_id) + echo_json(asset, pretty) + + +@data.command() +@click.pass_context +@translate_exceptions +@coro +@click.argument("asset", type=types.JSON()) +async def asset_activate(ctx, asset): + async with data_client(ctx) as cl: + await cl.activate_asset(asset) + + +@data.command() +@click.pass_context +@translate_exceptions +@coro +@click.argument("asset", type=types.JSON()) +@click.option('--delay', + type=int, + default=5, + help='Time (in seconds) between polls.') +@click.option('--max-attempts', + type=int, + default=200, + show_default=True, + help='Maximum number of polls. Set to zero for no limit.') +async def asset_wait(ctx, asset, delay, max_attempts): + quiet = ctx.obj['QUIET'] + async with data_client(ctx) as cl: + with StateBar(order_id="my asset", disable=quiet) as bar: + state = await cl.wait_asset(asset, + delay, + max_attempts, + callback=bar.update_state) + click.echo(state) # TODO: search_run()". From fd1077d7106889b14b98cb2f1bd1d631e008206c Mon Sep 17 00:00:00 2001 From: Kevin Lacaille Date: Wed, 1 Mar 2023 13:16:16 -0800 Subject: [PATCH 05/34] A working test for download. --- tests/integration/test_data_cli.py | 152 ++++++++++++++++++++++++++++- 1 file changed, 151 insertions(+), 1 deletion(-) diff --git a/tests/integration/test_data_cli.py b/tests/integration/test_data_cli.py index 6cbc2f95b..5d2c14beb 100644 --- a/tests/integration/test_data_cli.py +++ b/tests/integration/test_data_cli.py @@ -15,6 +15,8 @@ from http import HTTPStatus import json import logging +from pathlib import Path +import math import httpx import respx @@ -782,6 +784,155 @@ def test_search_update_fail(invoke, search_id, search_filter): assert result.exception +@respx.mock +@pytest.mark.asyncio +@pytest.mark.parametrize("exists, overwrite", + [(False, False), (True, False), (True, True), + (False, True)]) +def test_asset_download_default(invoke, open_test_img, exists, overwrite): + # mock_download_response() + dl_url = f'{TEST_URL}/1?token=IAmAToken' + + img_headers = { + 'Content-Type': 'image/tiff', + 'Content-Length': '527', + 'Content-Disposition': 'attachment; filename="img.tif"' + } + + async def _stream_img(): + data = open_test_img.read() + v = memoryview(data) + + chunksize = 100 + for i in range(math.ceil(len(v) / (chunksize))): + yield v[i * chunksize:min((i + 1) * chunksize, len(v))] + + # populate request parameter to avoid respx cloning, which throws + # an error caused by respx and not this code + # https://github.com/lundberg/respx/issues/130 + mock_resp = httpx.Response(HTTPStatus.OK, + stream=_stream_img(), + headers=img_headers, + request='donotcloneme') + respx.get(dl_url).return_value = mock_resp + + 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" + } + + runner = CliRunner() + with runner.isolated_filesystem() as folder: + if exists: + Path(folder, 'img.tif').write_text('i exist') + asset_download_call = [ + 'asset-download', + json.dumps(basic_udm2_asset), + '--directory', + Path(folder), + '--filename', + 'img.tif' + ] + if overwrite: + asset_download_call.append('--overwrite') + result = invoke(asset_download_call, runner=runner) + assert result.exit_code == 0 + + path = Path(folder, 'img.tif') + + assert path.name == 'img.tif' + assert path.is_file() + + if exists and not overwrite: + assert path.read_text() == 'i exist' + assert len(result.output) == 0 + else: + assert len(path.read_bytes()) == 527 + assert path.name in result.output + + +# @respx.mock +# def test_cli_orders_download_checksum(invoke, mock_download_response, oid): +# mock_download_response() + +# runner = CliRunner() +# with runner.isolated_filesystem() as folder: +# result = invoke(['download', oid], runner=runner) +# assert result.exit_code == 0 + +# # basic check of progress reporting +# assert 'm1.json' in result.output + +# # Check that the files were downloaded and have the correct contents +# f1_path = Path(folder) / 'oid/itemtype/m1.json' +# assert json.load(open(f1_path)) == {'key': 'value'} +# f2_path = Path(folder) / 'oid/itemtype/m2.json' +# assert json.load(open(f2_path)) == {'key2': 'value2'} + +# @respx.mock +# def test_cli_orders_download_dest(invoke, mock_download_response, oid): +# mock_download_response() + +# runner = CliRunner() +# with runner.isolated_filesystem() as folder: +# dest_dir = Path(folder) / 'foobar' +# dest_dir.mkdir() +# result = invoke(['download', '--directory', 'foobar', oid], +# runner=runner) +# assert result.exit_code == 0 + +# # Check that the files were downloaded to the custom directory +# f1_path = dest_dir / 'oid/itemtype/m1.json' +# assert json.load(open(f1_path)) == {'key': 'value'} +# f2_path = dest_dir / 'oid/itemtype/m2.json' +# assert json.load(open(f2_path)) == {'key2': 'value2'} + +# @respx.mock +# def test_cli_orders_download_overwrite(invoke, +# mock_download_response, +# oid, +# write_to_tmp_json_file): +# mock_download_response() + +# runner = CliRunner() +# with runner.isolated_filesystem() as folder: +# filepath = Path(folder) / 'oid/itemtype/m1.json' +# filepath.parent.mkdir(parents=True) +# filepath.write_text(json.dumps({'foo': 'bar'})) + +# # check the file doesn't get overwritten by default +# result = invoke(['download', oid], runner=runner) +# assert result.exit_code == 0 +# assert json.load(open(filepath)) == {'foo': 'bar'} + +# # check the file gets overwritten +# result = invoke(['download', '--overwrite', oid], runner=runner) +# assert result.exit_code == 0 +# assert json.load(open(filepath)) == {'key': 'value'} + +# @respx.mock +# def test_cli_orders_download_state(invoke, order_description, oid): +# get_url = f'{TEST_ORDERS_URL}/{oid}' + +# order_description['state'] = 'running' +# mock_resp = httpx.Response(HTTPStatus.OK, json=order_description) +# respx.get(get_url).return_value = mock_resp + +# runner = CliRunner() +# result = invoke(['download', oid], runner=runner) + +# assert result.exit_code == 1 +# assert 'order state (running) is not a final state.' in result.output + # TODO: basic test for "planet data search-create". # TODO: basic test for "planet data search-get". # TODO: basic test for "planet data search-list". @@ -789,5 +940,4 @@ def test_search_update_fail(invoke, search_id, search_filter): # TODO: basic test for "planet data item-get". # TODO: basic test for "planet data asset-activate". # TODO: basic test for "planet data asset-wait". -# TODO: basic test for "planet data asset-download". # TODO: basic test for "planet data stats". From 9200fcf3f7f6b02946acde8be3e4cfb320ae8527 Mon Sep 17 00:00:00 2001 From: Kevin Lacaille Date: Wed, 1 Mar 2023 13:56:18 -0800 Subject: [PATCH 06/34] Removed commented out examples. --- tests/integration/test_data_cli.py | 84 +++--------------------------- 1 file changed, 7 insertions(+), 77 deletions(-) diff --git a/tests/integration/test_data_cli.py b/tests/integration/test_data_cli.py index 5d2c14beb..b57cc68ab 100644 --- a/tests/integration/test_data_cli.py +++ b/tests/integration/test_data_cli.py @@ -790,7 +790,8 @@ def test_search_update_fail(invoke, search_id, search_filter): [(False, False), (True, False), (True, True), (False, True)]) def test_asset_download_default(invoke, open_test_img, exists, overwrite): - # mock_download_response() + # NOTE: this is a slightly edited version of test_download_asset from + # tests/integration/test_data_api dl_url = f'{TEST_URL}/1?token=IAmAToken' img_headers = { @@ -834,7 +835,8 @@ async def _stream_img(): with runner.isolated_filesystem() as folder: if exists: Path(folder, 'img.tif').write_text('i exist') - asset_download_call = [ + + asset_download_command = [ 'asset-download', json.dumps(basic_udm2_asset), '--directory', @@ -843,8 +845,9 @@ async def _stream_img(): 'img.tif' ] if overwrite: - asset_download_call.append('--overwrite') - result = invoke(asset_download_call, runner=runner) + asset_download_command.append('--overwrite') + + result = invoke(asset_download_command, runner=runner) assert result.exit_code == 0 path = Path(folder, 'img.tif') @@ -860,79 +863,6 @@ async def _stream_img(): assert path.name in result.output -# @respx.mock -# def test_cli_orders_download_checksum(invoke, mock_download_response, oid): -# mock_download_response() - -# runner = CliRunner() -# with runner.isolated_filesystem() as folder: -# result = invoke(['download', oid], runner=runner) -# assert result.exit_code == 0 - -# # basic check of progress reporting -# assert 'm1.json' in result.output - -# # Check that the files were downloaded and have the correct contents -# f1_path = Path(folder) / 'oid/itemtype/m1.json' -# assert json.load(open(f1_path)) == {'key': 'value'} -# f2_path = Path(folder) / 'oid/itemtype/m2.json' -# assert json.load(open(f2_path)) == {'key2': 'value2'} - -# @respx.mock -# def test_cli_orders_download_dest(invoke, mock_download_response, oid): -# mock_download_response() - -# runner = CliRunner() -# with runner.isolated_filesystem() as folder: -# dest_dir = Path(folder) / 'foobar' -# dest_dir.mkdir() -# result = invoke(['download', '--directory', 'foobar', oid], -# runner=runner) -# assert result.exit_code == 0 - -# # Check that the files were downloaded to the custom directory -# f1_path = dest_dir / 'oid/itemtype/m1.json' -# assert json.load(open(f1_path)) == {'key': 'value'} -# f2_path = dest_dir / 'oid/itemtype/m2.json' -# assert json.load(open(f2_path)) == {'key2': 'value2'} - -# @respx.mock -# def test_cli_orders_download_overwrite(invoke, -# mock_download_response, -# oid, -# write_to_tmp_json_file): -# mock_download_response() - -# runner = CliRunner() -# with runner.isolated_filesystem() as folder: -# filepath = Path(folder) / 'oid/itemtype/m1.json' -# filepath.parent.mkdir(parents=True) -# filepath.write_text(json.dumps({'foo': 'bar'})) - -# # check the file doesn't get overwritten by default -# result = invoke(['download', oid], runner=runner) -# assert result.exit_code == 0 -# assert json.load(open(filepath)) == {'foo': 'bar'} - -# # check the file gets overwritten -# result = invoke(['download', '--overwrite', oid], runner=runner) -# assert result.exit_code == 0 -# assert json.load(open(filepath)) == {'key': 'value'} - -# @respx.mock -# def test_cli_orders_download_state(invoke, order_description, oid): -# get_url = f'{TEST_ORDERS_URL}/{oid}' - -# order_description['state'] = 'running' -# mock_resp = httpx.Response(HTTPStatus.OK, json=order_description) -# respx.get(get_url).return_value = mock_resp - -# runner = CliRunner() -# result = invoke(['download', oid], runner=runner) - -# assert result.exit_code == 1 -# assert 'order state (running) is not a final state.' in result.output - # TODO: basic test for "planet data search-create". # TODO: basic test for "planet data search-get". # TODO: basic test for "planet data search-list". From cfcadbd7acfc41dc4a4c952a5392cc97b4f05efd Mon Sep 17 00:00:00 2001 From: Kevin Lacaille Date: Wed, 1 Mar 2023 14:21:32 -0800 Subject: [PATCH 07/34] tests for activate and wait --- tests/integration/test_data_cli.py | 55 +++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/tests/integration/test_data_cli.py b/tests/integration/test_data_cli.py index b57cc68ab..baaa6b1be 100644 --- a/tests/integration/test_data_cli.py +++ b/tests/integration/test_data_cli.py @@ -863,11 +863,64 @@ async def _stream_img(): assert path.name in result.output +@respx.mock +def test_asset_activate(invoke): + mock_resp = httpx.Response(HTTPStatus.OK) + dl_url = f'{TEST_URL}/1?token=IAmAToken' + respx.get(dl_url).return_value = mock_resp + + 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" + } + + runner = CliRunner() + result = invoke(['asset-activate', json.dumps(basic_udm2_asset)], + runner=runner) + + assert not result.exception + + +@respx.mock +def test_asset_wait(invoke): + mock_resp = httpx.Response(HTTPStatus.OK) + dl_url = f'{TEST_URL}/1?token=IAmAToken' + respx.get(dl_url).return_value = mock_resp + + 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" + } + + runner = CliRunner() + result = invoke( + ['asset-wait', json.dumps(basic_udm2_asset), '--delay', '0'], + runner=runner) + + assert not result.exception + assert "state: active" in result.output + + # TODO: basic test for "planet data search-create". # TODO: basic test for "planet data search-get". # 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 asset-activate". # TODO: basic test for "planet data asset-wait". # TODO: basic test for "planet data stats". From 19a45a07e88cd47c336e7e35caa22869b4351f58 Mon Sep 17 00:00:00 2001 From: Kevin Lacaille Date: Wed, 1 Mar 2023 14:22:53 -0800 Subject: [PATCH 08/34] Docstrings. --- planet/cli/data.py | 47 +++++++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/planet/cli/data.py b/planet/cli/data.py index aee635367..ee78813f9 100644 --- a/planet/cli/data.py +++ b/planet/cli/data.py @@ -441,11 +441,15 @@ async def search_update(ctx, async def asset_download(ctx, asset, directory, filename, overwrite, checksum): """Download an activated asset. - This function outputs the path to the downloaded asset, optionally - pretty-printed. - This function will fail if the asset state is not activated. Consider calling `asset-wait` before this command to ensure the asset is activated. + + If --checksum is provided, the associated checksums given in the manifest + are compared against the downloaded files to verify that they match. + + If --checksum is provided, files are already downloaded, and --overwrite is + not specified, this will simply validate the checksums of the files against + the manifest. """ quiet = ctx.obj['QUIET'] async with data_client(ctx) as cl: @@ -458,26 +462,13 @@ async def asset_download(ctx, asset, directory, filename, overwrite, checksum): cl.validate_checksum(asset, path) -@data.command() -@click.pass_context -@translate_exceptions -@coro -@click.argument("item_types") -@click.argument("item_id") -@click.argument("asset_type_id") -@pretty -async def asset_get(ctx, item_types, item_id, asset_type_id, pretty): - async with data_client(ctx) as cl: - asset = await cl.get_asset(item_types, item_id, asset_type_id) - echo_json(asset, pretty) - - @data.command() @click.pass_context @translate_exceptions @coro @click.argument("asset", type=types.JSON()) async def asset_activate(ctx, asset): + '''Activate an asset.''' async with data_client(ctx) as cl: await cl.activate_asset(asset) @@ -497,6 +488,11 @@ async def asset_activate(ctx, asset): show_default=True, help='Maximum number of polls. Set to zero for no limit.') async def asset_wait(ctx, asset, delay, max_attempts): + '''Wait for an asset to be activated. + + Returns when the asset state has reached "activated" and the asset is + available. + ''' quiet = ctx.obj['QUIET'] async with data_client(ctx) as cl: with StateBar(order_id="my asset", disable=quiet) as bar: @@ -507,7 +503,20 @@ async def asset_wait(ctx, asset, delay, max_attempts): click.echo(state) +@data.command() +@click.pass_context +@translate_exceptions +@coro +@click.argument("item_types") +@click.argument("item_id") +@click.argument("asset_type_id") +@pretty +async def asset_get(ctx, item_types, item_id, asset_type_id, pretty): + '''Get an item asset.''' + async with data_client(ctx) as cl: + asset = await cl.get_asset(item_types, item_id, asset_type_id) + echo_json(asset, pretty) + + # TODO: search_run()". # TODO: item_get()". -# TODO: asset_activate()". -# TODO: asset_wait()". From 1bacfeaecd44509291b3954f924bae9912ec5471 Mon Sep 17 00:00:00 2001 From: Kevin Lacaille Date: Wed, 1 Mar 2023 14:26:45 -0800 Subject: [PATCH 09/34] linting --- planet/cli/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/planet/cli/data.py b/planet/cli/data.py index ee78813f9..7faa662ff 100644 --- a/planet/cli/data.py +++ b/planet/cli/data.py @@ -490,7 +490,7 @@ async def asset_activate(ctx, asset): async def asset_wait(ctx, asset, delay, max_attempts): '''Wait for an asset to be activated. - Returns when the asset state has reached "activated" and the asset is + Returns when the asset state has reached "activated" and the asset is available. ''' quiet = ctx.obj['QUIET'] From 0dddfb2f3d2360d8fd01f551da43422d6b6410c2 Mon Sep 17 00:00:00 2001 From: Kevin Lacaille Date: Wed, 1 Mar 2023 14:41:36 -0800 Subject: [PATCH 10/34] test for asset-get --- tests/integration/test_data_cli.py | 56 ++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/tests/integration/test_data_cli.py b/tests/integration/test_data_cli.py index baaa6b1be..65254c8fa 100644 --- a/tests/integration/test_data_cli.py +++ b/tests/integration/test_data_cli.py @@ -58,8 +58,9 @@ def test_data_command_registered(invoke): assert "search-delete" in result.output assert "search-update" in result.output assert "asset_download" in result.output - # assert "asset_activate" in result.output - # assert "asset_wait" in result.output + assert "asset_activate" in result.output + assert "asset_wait" in result.output + assert "asset_get" in result.output # Add other sub-commands here. @@ -917,10 +918,59 @@ def test_asset_wait(invoke): assert "state: 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-create". # TODO: basic test for "planet data search-get". # 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 asset-wait". # TODO: basic test for "planet data stats". From 23264d12db4da6d4e9c7db0c635a8ba375341173 Mon Sep 17 00:00:00 2001 From: Kevin Lacaille Date: Wed, 1 Mar 2023 14:45:54 -0800 Subject: [PATCH 11/34] Removed commented out function. --- planet/cli/data.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/planet/cli/data.py b/planet/cli/data.py index 7faa662ff..4b298e7d0 100644 --- a/planet/cli/data.py +++ b/planet/cli/data.py @@ -77,18 +77,6 @@ def check_item_types(ctx, param, item_types) -> Optional[List[dict]]: raise click.BadParameter(str(e)) -# async def check_asset(ctx, param, item_type, item_id, -# asset_type_id) -> Optional[List[dict]]: -# '''Validates the asset type by comparying the inputted asset type to all -# supported asset types.''' -# try: -# async with data_client(ctx) as cl: -# asset = cl.get_asset(item_type, item_id, asset_type_id) -# return asset -# except SpecificationException as e: -# raise click.BadParameter(str(e)) - - def date_range_to_filter(ctx, param, values) -> Optional[List[dict]]: def _func(obj): From a246c06d95d51c04005370ee088a465e2e8f3e1f Mon Sep 17 00:00:00 2001 From: Kevin Lacaille Date: Wed, 1 Mar 2023 14:46:49 -0800 Subject: [PATCH 12/34] Removed unused tests. --- tests/unit/test_data_callbacks.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/unit/test_data_callbacks.py b/tests/unit/test_data_callbacks.py index 7aa4e0b33..41d7b78d5 100644 --- a/tests/unit/test_data_callbacks.py +++ b/tests/unit/test_data_callbacks.py @@ -53,5 +53,3 @@ def test_item_type_fail(): ctx = MockContext() with pytest.raises(click.BadParameter): check_item_types(ctx, 'item_type', "bad_item_type") - -# def test_asset_type_success(item_types, bundles): From 7a087ce0eb83f8067447c63095704fe7874f7ea2 Mon Sep 17 00:00:00 2001 From: Kevin Lacaille Date: Wed, 1 Mar 2023 19:40:24 -0800 Subject: [PATCH 13/34] removed plural item types. --- planet/cli/data.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/planet/cli/data.py b/planet/cli/data.py index 4b298e7d0..b3c8db196 100644 --- a/planet/cli/data.py +++ b/planet/cli/data.py @@ -495,14 +495,14 @@ async def asset_wait(ctx, asset, delay, max_attempts): @click.pass_context @translate_exceptions @coro -@click.argument("item_types") +@click.argument("item_type") @click.argument("item_id") @click.argument("asset_type_id") @pretty -async def asset_get(ctx, item_types, item_id, 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_types, item_id, asset_type_id) + asset = await cl.get_asset(item_type, item_id, asset_type_id) echo_json(asset, pretty) From 48ab53c1658fa4487143e43d3d78426be4df17e1 Mon Sep 17 00:00:00 2001 From: Kevin Lacaille Date: Wed, 1 Mar 2023 19:40:57 -0800 Subject: [PATCH 14/34] took echo out of context of data_client Co-authored-by: Jennifer Reiber Kyle --- planet/cli/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/planet/cli/data.py b/planet/cli/data.py index b3c8db196..42519cf01 100644 --- a/planet/cli/data.py +++ b/planet/cli/data.py @@ -503,7 +503,7 @@ 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) + echo_json(asset, pretty) # TODO: search_run()". From 002b6d5132d73b2fdfb87df5af43ad04bbafdf4f Mon Sep 17 00:00:00 2001 From: Kevin Lacaille Date: Wed, 1 Mar 2023 19:41:37 -0800 Subject: [PATCH 15/34] upgraded readability of CLI test Co-authored-by: Jennifer Reiber Kyle --- tests/integration/test_data_cli.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/integration/test_data_cli.py b/tests/integration/test_data_cli.py index 65254c8fa..be6bd4fd5 100644 --- a/tests/integration/test_data_cli.py +++ b/tests/integration/test_data_cli.py @@ -840,8 +840,7 @@ async def _stream_img(): asset_download_command = [ 'asset-download', json.dumps(basic_udm2_asset), - '--directory', - Path(folder), + f'--directory={Path(folder)}', '--filename', 'img.tif' ] From f7362af4daf632ca628787c8aae0df842b6f9e7a Mon Sep 17 00:00:00 2001 From: Kevin Lacaille Date: Thu, 2 Mar 2023 09:52:29 -0800 Subject: [PATCH 16/34] updated asset-download to follow the useage docs, commented out asset-get --- planet/cli/data.py | 40 +++++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/planet/cli/data.py b/planet/cli/data.py index 42519cf01..c50c730f2 100644 --- a/planet/cli/data.py +++ b/planet/cli/data.py @@ -406,7 +406,9 @@ async def search_update(ctx, @click.pass_context @translate_exceptions @coro -@click.argument("asset", type=types.JSON()) +@click.argument("item_type") +@click.argument("item_id") +@click.argument("asset_type_id") @click.option('--directory', default='.', help=('Base directory for file download.'), @@ -426,7 +428,14 @@ async def search_update(ctx, is_flag=True, default=None, help=('Verify that checksums match.')) -async def asset_download(ctx, asset, directory, filename, overwrite, checksum): +async def asset_download(ctx, + item_type, + item_id, + asset_type_id, + directory, + filename, + overwrite, + checksum): """Download an activated asset. This function will fail if the asset state is not activated. Consider @@ -441,6 +450,7 @@ async def asset_download(ctx, asset, directory, filename, overwrite, checksum): """ quiet = ctx.obj['QUIET'] async with data_client(ctx) as cl: + asset = await cl.get_asset(item_type, item_id, asset_type_id) path = await cl.download_asset(asset=asset, filename=filename, directory=Path(directory), @@ -491,19 +501,19 @@ async def asset_wait(ctx, asset, delay, max_attempts): click.echo(state) -@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) +# @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()". From da9c14a0fa4c1c5b3302fdecfdb42bfd5709277c Mon Sep 17 00:00:00 2001 From: Kevin Lacaille Date: Thu, 2 Mar 2023 09:53:05 -0800 Subject: [PATCH 17/34] updated asset-download test, commented out asset-get test --- tests/integration/test_data_cli.py | 173 +++++++++++++++++------------ 1 file changed, 99 insertions(+), 74 deletions(-) diff --git a/tests/integration/test_data_cli.py b/tests/integration/test_data_cli.py index be6bd4fd5..383de3a9f 100644 --- a/tests/integration/test_data_cli.py +++ b/tests/integration/test_data_cli.py @@ -793,8 +793,50 @@ def test_search_update_fail(invoke, search_id, search_filter): def test_asset_download_default(invoke, open_test_img, exists, overwrite): # NOTE: this is a slightly edited version of test_download_asset from # tests/integration/test_data_api + + # Description of the data we're going to get and download + 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 the response for get_asset + mock_resp_get = 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_get + img_headers = { 'Content-Type': 'image/tiff', 'Content-Length': '527', @@ -809,28 +851,12 @@ async def _stream_img(): for i in range(math.ceil(len(v) / (chunksize))): yield v[i * chunksize:min((i + 1) * chunksize, len(v))] - # populate request parameter to avoid respx cloning, which throws - # an error caused by respx and not this code - # https://github.com/lundberg/respx/issues/130 - mock_resp = httpx.Response(HTTPStatus.OK, - stream=_stream_img(), - headers=img_headers, - request='donotcloneme') - respx.get(dl_url).return_value = mock_resp - - 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" - } + # Mock the response for download_asset + mock_resp_download = httpx.Response(HTTPStatus.OK, + stream=_stream_img(), + headers=img_headers, + request='donotcloneme') + respx.get(dl_url).return_value = mock_resp_download runner = CliRunner() with runner.isolated_filesystem() as folder: @@ -839,7 +865,9 @@ async def _stream_img(): asset_download_command = [ 'asset-download', - json.dumps(basic_udm2_asset), + item_type, + item_id, + asset_type_id, f'--directory={Path(folder)}', '--filename', 'img.tif' @@ -917,58 +945,55 @@ def test_asset_wait(invoke): assert "state: 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 - +# @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-create". -# TODO: basic test for "planet data search-get". # TODO: basic test for "planet data search-list". # TODO: basic test for "planet data search-run". # TODO: basic test for "planet data item-get". From 3dd0ef73d96c759b863dbfe3ba07aaa2704477d1 Mon Sep 17 00:00:00 2001 From: Kevin Lacaille Date: Thu, 2 Mar 2023 10:02:18 -0800 Subject: [PATCH 18/34] Added callback function to item type in download call --- planet/cli/data.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/planet/cli/data.py b/planet/cli/data.py index c50c730f2..289860ff3 100644 --- a/planet/cli/data.py +++ b/planet/cli/data.py @@ -406,7 +406,9 @@ async def search_update(ctx, @click.pass_context @translate_exceptions @coro -@click.argument("item_type") +@click.argument("item_type", + type=types.CommaSeparatedString(), + callback=check_item_types) @click.argument("item_id") @click.argument("asset_type_id") @click.option('--directory', @@ -449,6 +451,8 @@ async def asset_download(ctx, the manifest. """ quiet = ctx.obj['QUIET'] + # The callback function returns a list, but we want the item type as a str + item_type = item_type.pop() async with data_client(ctx) as cl: asset = await cl.get_asset(item_type, item_id, asset_type_id) path = await cl.download_asset(asset=asset, @@ -515,6 +519,5 @@ async def asset_wait(ctx, asset, delay, max_attempts): # asset = await cl.get_asset(item_type, item_id, asset_type_id) # echo_json(asset, pretty) - # TODO: search_run()". # TODO: item_get()". From 3970e1e47356434b91e777b1a7141adbc4573add Mon Sep 17 00:00:00 2001 From: Kevin Lacaille Date: Thu, 2 Mar 2023 10:13:22 -0800 Subject: [PATCH 19/34] write bytes to tif, not text --- tests/integration/test_data_cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/test_data_cli.py b/tests/integration/test_data_cli.py index 383de3a9f..592977ed4 100644 --- a/tests/integration/test_data_cli.py +++ b/tests/integration/test_data_cli.py @@ -861,7 +861,7 @@ async def _stream_img(): runner = CliRunner() with runner.isolated_filesystem() as folder: if exists: - Path(folder, 'img.tif').write_text('i exist') + Path(folder, 'img.tif').write_bytes(b'01010') asset_download_command = [ 'asset-download', @@ -884,7 +884,7 @@ async def _stream_img(): assert path.is_file() if exists and not overwrite: - assert path.read_text() == 'i exist' + assert len(path.read_bytes()) == 5 assert len(result.output) == 0 else: assert len(path.read_bytes()) == 527 From f106489a7be0ed70feb57adbfd14d4960e295d14 Mon Sep 17 00:00:00 2001 From: Kevin Lacaille Date: Thu, 2 Mar 2023 10:35:22 -0800 Subject: [PATCH 20/34] ensured asset-wait and -activate followed the docs. --- planet/cli/data.py | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/planet/cli/data.py b/planet/cli/data.py index 289860ff3..1e73533e7 100644 --- a/planet/cli/data.py +++ b/planet/cli/data.py @@ -449,6 +449,10 @@ async def asset_download(ctx, If --checksum is provided, files are already downloaded, and --overwrite is not specified, this will simply validate the checksums of the files against the manifest. + + Output: + The full path of the downloaded file. If the quiet flag is not set, this + also provides ANSI download status reporting. """ quiet = ctx.obj['QUIET'] # The callback function returns a list, but we want the item type as a str @@ -468,10 +472,16 @@ async def asset_download(ctx, @click.pass_context @translate_exceptions @coro -@click.argument("asset", type=types.JSON()) -async def asset_activate(ctx, asset): +@click.argument("item_type", + type=types.CommaSeparatedString(), + callback=check_item_types) +@click.argument("item_id") +@click.argument("asset_type_id") +async def asset_activate(ctx, item_type, item_id, asset_type_id): '''Activate an asset.''' + item_type = item_type.pop() async with data_client(ctx) as cl: + asset = await cl.get_asset(item_type, item_id, asset_type_id) await cl.activate_asset(asset) @@ -479,7 +489,11 @@ async def asset_activate(ctx, asset): @click.pass_context @translate_exceptions @coro -@click.argument("asset", type=types.JSON()) +@click.argument("item_type", + type=types.CommaSeparatedString(), + callback=check_item_types) +@click.argument("item_id") +@click.argument("asset_type_id") @click.option('--delay', type=int, default=5, @@ -489,14 +503,21 @@ async def asset_activate(ctx, asset): default=200, show_default=True, help='Maximum number of polls. Set to zero for no limit.') -async def asset_wait(ctx, asset, delay, max_attempts): +async def asset_wait(ctx, + item_type, + item_id, + asset_type_id, + delay, + max_attempts): '''Wait for an asset to be activated. Returns when the asset state has reached "activated" and the asset is available. ''' quiet = ctx.obj['QUIET'] + item_type = item_type.pop() async with data_client(ctx) as cl: + asset = await cl.get_asset(item_type, item_id, asset_type_id) with StateBar(order_id="my asset", disable=quiet) as bar: state = await cl.wait_asset(asset, delay, From 0b3a24d23419ee48164cff9d8b9c73473d6ddf6f Mon Sep 17 00:00:00 2001 From: Kevin Lacaille Date: Thu, 2 Mar 2023 10:48:44 -0800 Subject: [PATCH 21/34] More elegant version of removing item type from list. --- planet/cli/data.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/planet/cli/data.py b/planet/cli/data.py index 1e73533e7..c3e6eed24 100644 --- a/planet/cli/data.py +++ b/planet/cli/data.py @@ -455,10 +455,8 @@ async def asset_download(ctx, also provides ANSI download status reporting. """ quiet = ctx.obj['QUIET'] - # The callback function returns a list, but we want the item type as a str - item_type = item_type.pop() async with data_client(ctx) as cl: - asset = await cl.get_asset(item_type, item_id, asset_type_id) + asset = await cl.get_asset(item_type.pop(), item_id, asset_type_id) path = await cl.download_asset(asset=asset, filename=filename, directory=Path(directory), @@ -479,9 +477,8 @@ async def asset_download(ctx, @click.argument("asset_type_id") async def asset_activate(ctx, item_type, item_id, asset_type_id): '''Activate an asset.''' - item_type = item_type.pop() async with data_client(ctx) as cl: - asset = await cl.get_asset(item_type, item_id, asset_type_id) + asset = await cl.get_asset(item_type.pop(), item_id, asset_type_id) await cl.activate_asset(asset) @@ -515,9 +512,8 @@ async def asset_wait(ctx, available. ''' quiet = ctx.obj['QUIET'] - item_type = item_type.pop() async with data_client(ctx) as cl: - asset = await cl.get_asset(item_type, item_id, asset_type_id) + asset = await cl.get_asset(item_type.pop(), item_id, asset_type_id) with StateBar(order_id="my asset", disable=quiet) as bar: state = await cl.wait_asset(asset, delay, From 689517fdb75ed0144b8865655285f129057c50b5 Mon Sep 17 00:00:00 2001 From: Kevin Lacaille Date: Thu, 2 Mar 2023 10:49:35 -0800 Subject: [PATCH 22/34] Added asset-get response to wait and activate --- tests/integration/test_data_cli.py | 87 +++++++++++++++++++++++++++--- 1 file changed, 81 insertions(+), 6 deletions(-) diff --git a/tests/integration/test_data_cli.py b/tests/integration/test_data_cli.py index 592977ed4..49cb9a765 100644 --- a/tests/integration/test_data_cli.py +++ b/tests/integration/test_data_cli.py @@ -893,9 +893,12 @@ async def _stream_img(): @respx.mock def test_asset_activate(invoke): - mock_resp = httpx.Response(HTTPStatus.OK) + # Description of the data we're going to get and download + item_type = 'PSScene' + item_id = '20221003_002705_38_2461xx' + asset_type_id = 'basic_udm2' + dl_url = f'{TEST_URL}/1?token=IAmAToken' - respx.get(dl_url).return_value = mock_resp basic_udm2_asset = { "_links": { @@ -910,8 +913,36 @@ def test_asset_activate(invoke): "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 the response for get_asset + mock_resp_get = 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_get + + # Mock the response for activate_asset + mock_resp_activate = httpx.Response(HTTPStatus.OK) + dl_url = f'{TEST_URL}/1?token=IAmAToken' + respx.get(dl_url).return_value = mock_resp_activate + runner = CliRunner() - result = invoke(['asset-activate', json.dumps(basic_udm2_asset)], + result = invoke(['asset-activate', item_type, item_id, asset_type_id], runner=runner) assert not result.exception @@ -919,9 +950,53 @@ def test_asset_activate(invoke): @respx.mock def test_asset_wait(invoke): - mock_resp = httpx.Response(HTTPStatus.OK) + # Description of the data we're going to get and download + 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 the response for get_asset + mock_resp_get = 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_get + + # Mock the response for wait_asset + mock_resp_wait = httpx.Response(HTTPStatus.OK) dl_url = f'{TEST_URL}/1?token=IAmAToken' - respx.get(dl_url).return_value = mock_resp + respx.get(dl_url).return_value = mock_resp_wait basic_udm2_asset = { "_links": { @@ -938,7 +1013,7 @@ def test_asset_wait(invoke): runner = CliRunner() result = invoke( - ['asset-wait', json.dumps(basic_udm2_asset), '--delay', '0'], + ['asset-wait', item_type, item_id, asset_type_id, '--delay', '0'], runner=runner) assert not result.exception From 8aa9be54267cd3de0f784b0747abf78aa6db9292 Mon Sep 17 00:00:00 2001 From: Kevin Lacaille Date: Thu, 2 Mar 2023 11:08:19 -0800 Subject: [PATCH 23/34] moved asset-get response into a fixture --- tests/integration/test_data_cli.py | 240 +++++++++++------------------ 1 file changed, 91 insertions(+), 149 deletions(-) diff --git a/tests/integration/test_data_cli.py b/tests/integration/test_data_cli.py index 49cb9a765..9965a20a5 100644 --- a/tests/integration/test_data_cli.py +++ b/tests/integration/test_data_cli.py @@ -46,6 +46,69 @@ def _invoke(extra_args, runner=None): return _invoke +@pytest.fixture +def item_type(): + return 'PSScene' + + +@pytest.fixture +def item_id(): + return '20221003_002705_38_2461xx' + + +@pytest.fixture +def asset_type_id(): + return 'basic_udm2' + + +@pytest.fixture +def dl_url(): + return f'{TEST_URL}/1?token=IAmAToken' + + +@pytest.fixture +def mock_asset_get_response(item_type, item_id, asset_type_id, dl_url): + + def _func(): + 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": asset_type_id + } + + 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 the response for get_asset + mock_resp_get = httpx.Response(HTTPStatus.OK, json=page_response) + asset_url = f'{TEST_URL}/item-types/{item_type}/items/{item_id}/assets' + respx.get(asset_url).return_value = mock_resp_get + + return _func + + def test_data_command_registered(invoke): """planet-data command prints help and usage message.""" runner = CliRunner() @@ -57,10 +120,9 @@ def test_data_command_registered(invoke): assert "search-get" in result.output assert "search-delete" in result.output assert "search-update" in result.output - assert "asset_download" in result.output - assert "asset_activate" in result.output - assert "asset_wait" in result.output - assert "asset_get" in result.output + assert "asset-download" in result.output + assert "asset-activate" in result.output + assert "asset-wait" in result.output # Add other sub-commands here. @@ -790,52 +852,17 @@ def test_search_update_fail(invoke, search_id, search_filter): @pytest.mark.parametrize("exists, overwrite", [(False, False), (True, False), (True, True), (False, True)]) -def test_asset_download_default(invoke, open_test_img, exists, overwrite): - # NOTE: this is a slightly edited version of test_download_asset from - # tests/integration/test_data_api - - # Description of the data we're going to get and download - 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 the response for get_asset - mock_resp_get = 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_get +def test_asset_download_default(invoke, + open_test_img, + exists, + overwrite, + mock_asset_get_response, + item_type, + item_id, + asset_type_id, + dl_url): + + mock_asset_get_response() img_headers = { 'Content-Type': 'image/tiff', @@ -892,53 +919,17 @@ async def _stream_img(): @respx.mock -def test_asset_activate(invoke): - # Description of the data we're going to get and download - 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 - } +def test_asset_activate(invoke, + mock_asset_get_response, + item_type, + item_id, + asset_type_id, + dl_url): - # Mock the response for get_asset - mock_resp_get = 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_get + mock_asset_get_response() # Mock the response for activate_asset mock_resp_activate = httpx.Response(HTTPStatus.OK) - dl_url = f'{TEST_URL}/1?token=IAmAToken' respx.get(dl_url).return_value = mock_resp_activate runner = CliRunner() @@ -949,68 +940,19 @@ def test_asset_activate(invoke): @respx.mock -def test_asset_wait(invoke): - # Description of the data we're going to get and download - 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 - } +def test_asset_wait(invoke, + mock_asset_get_response, + item_type, + item_id, + asset_type_id, + dl_url): - # Mock the response for get_asset - mock_resp_get = 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_get + mock_asset_get_response() # Mock the response for wait_asset mock_resp_wait = httpx.Response(HTTPStatus.OK) - dl_url = f'{TEST_URL}/1?token=IAmAToken' respx.get(dl_url).return_value = mock_resp_wait - 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" - } - runner = CliRunner() result = invoke( ['asset-wait', item_type, item_id, asset_type_id, '--delay', '0'], From 60057a01ea26628f5551ea07c293acf6cf3a1983 Mon Sep 17 00:00:00 2001 From: Kevin Lacaille Date: Thu, 2 Mar 2023 11:10:16 -0800 Subject: [PATCH 24/34] linting --- tests/integration/test_data_cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/test_data_cli.py b/tests/integration/test_data_cli.py index 9965a20a5..332c428ae 100644 --- a/tests/integration/test_data_cli.py +++ b/tests/integration/test_data_cli.py @@ -90,8 +90,8 @@ def _func(): "SELFURL", "activate": "ACTIVATEURL", - "type": - "https://api.planet.com/data/v1/asset-types/basic_analytic_4b" + "type": ('https://api.planet.com/data/v1/asset-types/' + 'basic_analytic_4b') }, "_permissions": ["download"], "md5_digest": None, From 9d1739c528a627fed44a24dd2312f8be9d5090b1 Mon Sep 17 00:00:00 2001 From: Kevin Lacaille Date: Thu, 2 Mar 2023 11:19:59 -0800 Subject: [PATCH 25/34] fixed merge conflict linting issues --- planet/cli/data.py | 7 ++----- tests/integration/test_data_cli.py | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/planet/cli/data.py b/planet/cli/data.py index e1c06543c..1bd17fa95 100644 --- a/planet/cli/data.py +++ b/planet/cli/data.py @@ -19,15 +19,12 @@ import click from planet.reporting import StateBar -from planet import data_filter, DataClient -from planet.clients.data import (SEARCH_SORT, - from planet import data_filter, DataClient, exceptions -from planet.clients.data import (LIST_SEARCH_TYPE, +from planet.clients.data import (SEARCH_SORT, + LIST_SEARCH_TYPE, LIST_SEARCH_TYPE_DEFAULT, LIST_SORT_ORDER, LIST_SORT_DEFAULT, - SEARCH_SORT, SEARCH_SORT_DEFAULT, STATS_INTERVAL) diff --git a/tests/integration/test_data_cli.py b/tests/integration/test_data_cli.py index e77b2bc3b..7d88e0b6c 100644 --- a/tests/integration/test_data_cli.py +++ b/tests/integration/test_data_cli.py @@ -113,6 +113,7 @@ def _func(): return _func + def item_descriptions(get_test_file_json): item_ids = [ '20220125_075509_67_1061', @@ -123,7 +124,6 @@ def item_descriptions(get_test_file_json): return items - def test_data_command_registered(invoke): """planet-data command prints help and usage message.""" runner = CliRunner() From c995a8c07facff05b2bb4fe40de2c129ffb08d01 Mon Sep 17 00:00:00 2001 From: Kevin Lacaille Date: Thu, 2 Mar 2023 11:24:40 -0800 Subject: [PATCH 26/34] added back decorator to a fixture, whoopsie --- tests/integration/test_data_cli.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration/test_data_cli.py b/tests/integration/test_data_cli.py index 7d88e0b6c..9c69ee964 100644 --- a/tests/integration/test_data_cli.py +++ b/tests/integration/test_data_cli.py @@ -114,6 +114,7 @@ def _func(): return _func +@pytest.fixture def item_descriptions(get_test_file_json): item_ids = [ '20220125_075509_67_1061', From 6da207d0ce7d9ae8aeeb13d7eac3c9c4bfb1f8a5 Mon Sep 17 00:00:00 2001 From: Kevin Lacaille Date: Thu, 2 Mar 2023 11:49:13 -0800 Subject: [PATCH 27/34] Added data asset commands to docs. --- docs/cli/cli-data.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/cli/cli-data.md b/docs/cli/cli-data.md index a00034221..5b60a062d 100644 --- a/docs/cli/cli-data.md +++ b/docs/cli/cli-data.md @@ -326,6 +326,25 @@ that are of standard (aka not test) quality. Therefore, these filters can be eas planet data filter --permission --std-quality --asset ortho_analytic_8b_sr | planet data search PSScene --filter - ``` +## `data asset` command basics + +To activate an asset for download three commands must be queried, in sequence: +1. `asset-activate` - activate an asset +2. `asset-wait` - wait for an asset to be activated +3. `asset-download` - download an activated asset + +For example, if we want to download a `basic_udm2` asset from item ID +`20221003_002705_38_2461`, a `PSScene` item type: + +``` +planet data asset-activate PSScene 20221003_002705_38_2461 basic_udm2 && \ +planet data asset-wait PSScene 20221003_002705_38_2461 basic_udm2 && \ +planet data asset-download PSScene 20221003_002705_38_2461 basic_udm2 --directory /path/to/data/ +00:00 - order my asset - state: active +{'_links': {'_self': 'https://api.planet.com/data/v1/assets/eyJpIjogIjIwMjIxMDAzXzAwMjcwNV8zOF8yNDYxIiwgImMiOiAiUFNTY2VuZSIsICJ0IjogImJhc2ljX3VkbTIiLCAiY3QiOiAiaXRlbS10eXBlIn0', 'activate': 'https://api.planet.com/data/v1/assets/eyJpIjogIjIwMjIxMDAzXzAwMjcwNV8zOF8yNDYxIiwgImMiOiAiUFNTY2VuZSIsICJ0IjogImJhc2ljX3VkbTIiLCAiY3QiOiAiaXRlbS10eXBlIn0/activate', 'type': 'https://api.planet.com/data/v1/asset-types/basic_udm2'}, '_permissions': ['download'], 'expires_at': '2023-03-02T19:30:48.942718', 'location': 'https://api.planet.com/data/v1/download?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJQYWtGNHZuUEs3WXRmSFNGUklHY2I3YTNXT3piaTlaam4zWUpZMmxnd0x5cVlFMVBRSHU5QXNCcjR5Q3FxSjBNbl9yN3VwVEFQYUI1ZzhYNUJmcDhmUT09IiwiZXhwIjoxNjc3Nzg1NDQ4LCJ0b2tlbl90eXBlIjoidHlwZWQtaXRlbSIsIml0ZW1fdHlwZV9pZCI6IlBTU2NlbmUiLCJpdGVtX2lkIjoiMjAyMjEwMDNfMDAyNzA1XzM4XzI0NjEiLCJhc3NldF90eXBlIjoiYmFzaWNfdWRtMiJ9.Dd0opDjW3bBS6qLLZoNiJkfBsO2n5Xz9pM5apEUz_K6viDPFexhJiy6bMbaySbby8W0YvuATdb1uYXS2FkweDg', 'md5_digest': '3a9f7dd1ce500f699d0a96afdd0e3aa2', 'status': 'active', 'type': 'basic_udm2'} +/path/to/data/20221003_002705_38_2461_1A_udm2.tif: 100%|██████████████████████████████████| 3.16k/3.16k [00:00<00:00, 32.0MB/s] +``` + ## Stats TODO From f01a519a1addcbd2ddd8e2ee64ab1fa33a168647 Mon Sep 17 00:00:00 2001 From: Kevin Lacaille Date: Fri, 3 Mar 2023 09:58:38 -0800 Subject: [PATCH 28/34] Added new test for check_item_type, renamed tests for check_item_types --- tests/unit/test_data_callbacks.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_data_callbacks.py b/tests/unit/test_data_callbacks.py index 41d7b78d5..8591ecd2f 100644 --- a/tests/unit/test_data_callbacks.py +++ b/tests/unit/test_data_callbacks.py @@ -14,7 +14,7 @@ import logging import pytest import click -from planet.cli.data import check_item_types +from planet.cli.data import check_item_types, check_item_type LOGGER = logging.getLogger(__name__) @@ -43,13 +43,26 @@ def __init__(self): 'PSScene4Band', 'REScene' ]) -def test_item_type_success(item_types): +def test_item_types_success(item_types): ctx = MockContext() result = check_item_types(ctx, 'item_types', [item_types]) assert result == [item_types] +def test_item_types_fail(): + ctx = MockContext() + with pytest.raises(click.BadParameter): + check_item_types(ctx, 'item_types', "bad_item_type") + + +def test_item_type_success(): + ctx = MockContext() + item_type = "PSScene" + result = check_item_type(ctx, 'item_type', item_type) + assert result == item_type + + def test_item_type_fail(): ctx = MockContext() with pytest.raises(click.BadParameter): - check_item_types(ctx, 'item_type', "bad_item_type") + check_item_type(ctx, 'item_type', "bad_item_type") From 5d95584ca1d3e195773786607c4f51616978266b Mon Sep 17 00:00:00 2001 From: Kevin Lacaille Date: Fri, 3 Mar 2023 10:03:25 -0800 Subject: [PATCH 29/34] Added new callback function to validate a single time type and edited associated functions. --- planet/cli/data.py | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/planet/cli/data.py b/planet/cli/data.py index 1bd17fa95..4135dcdc4 100644 --- a/planet/cli/data.py +++ b/planet/cli/data.py @@ -71,8 +71,8 @@ def assets_to_filter(ctx, param, assets: List[str]) -> Optional[dict]: def check_item_types(ctx, param, item_types) -> Optional[List[dict]]: - '''Validates the item type by comparing the inputted item type to all - supported item types.''' + '''Validates each item types provided by comparing them to all supported + item types.''' try: for item_type in item_types: validate_item_type(item_type) @@ -81,6 +81,16 @@ def check_item_types(ctx, param, item_types) -> Optional[List[dict]]: raise click.BadParameter(str(e)) +def check_item_type(ctx, param, item_type) -> Optional[List[dict]]: + '''Validates the item type provided by comparing it to all supported + item types.''' + try: + validate_item_type(item_type) + return item_type + except SpecificationException as e: + raise click.BadParameter(str(e)) + + def check_search_id(ctx, param, search_id) -> str: '''Ensure search id is a valix hex string''' try: @@ -487,9 +497,7 @@ async def search_update(ctx, @click.pass_context @translate_exceptions @coro -@click.argument("item_type", - type=types.CommaSeparatedString(), - callback=check_item_types) +@click.argument("item_type", type=str, callback=check_item_type) @click.argument("item_id") @click.argument("asset_type_id") @click.option('--directory', @@ -537,7 +545,7 @@ async def asset_download(ctx, """ quiet = ctx.obj['QUIET'] async with data_client(ctx) as cl: - asset = await cl.get_asset(item_type.pop(), item_id, asset_type_id) + asset = await cl.get_asset(item_type, item_id, asset_type_id) path = await cl.download_asset(asset=asset, filename=filename, directory=Path(directory), @@ -551,15 +559,13 @@ async def asset_download(ctx, @click.pass_context @translate_exceptions @coro -@click.argument("item_type", - type=types.CommaSeparatedString(), - callback=check_item_types) +@click.argument("item_type", type=str, callback=check_item_type) @click.argument("item_id") @click.argument("asset_type_id") async def asset_activate(ctx, item_type, item_id, asset_type_id): '''Activate an asset.''' async with data_client(ctx) as cl: - asset = await cl.get_asset(item_type.pop(), item_id, asset_type_id) + asset = await cl.get_asset(item_type, item_id, asset_type_id) await cl.activate_asset(asset) @@ -567,9 +573,7 @@ async def asset_activate(ctx, item_type, item_id, asset_type_id): @click.pass_context @translate_exceptions @coro -@click.argument("item_type", - type=types.CommaSeparatedString(), - callback=check_item_types) +@click.argument("item_type", type=str, callback=check_item_type) @click.argument("item_id") @click.argument("asset_type_id") @click.option('--delay', @@ -594,7 +598,7 @@ async def asset_wait(ctx, ''' quiet = ctx.obj['QUIET'] async with data_client(ctx) as cl: - asset = await cl.get_asset(item_type.pop(), item_id, asset_type_id) + asset = await cl.get_asset(item_type, item_id, asset_type_id) with StateBar(order_id="my asset", disable=quiet) as bar: state = await cl.wait_asset(asset, delay, From cf13ee9deb76d85299e6337439426bd7cbf4033d Mon Sep 17 00:00:00 2001 From: Kevin Lacaille Date: Fri, 3 Mar 2023 10:14:34 -0800 Subject: [PATCH 30/34] Added BadParameter if more than 1 item type was provided. --- planet/cli/data.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/planet/cli/data.py b/planet/cli/data.py index 4135dcdc4..d3f5e466c 100644 --- a/planet/cli/data.py +++ b/planet/cli/data.py @@ -84,6 +84,8 @@ def check_item_types(ctx, param, item_types) -> Optional[List[dict]]: def check_item_type(ctx, param, item_type) -> Optional[List[dict]]: '''Validates the item type provided by comparing it to all supported item types.''' + if len(item_type.split(",")) > 1: + raise click.BadParameter("Only provide 1 item type.") try: validate_item_type(item_type) return item_type From 55740963d8f33f55992b255a24ccd1ec74640281 Mon Sep 17 00:00:00 2001 From: Kevin Lacaille Date: Fri, 3 Mar 2023 10:14:49 -0800 Subject: [PATCH 31/34] Added test if more than 1 item type was provided. --- tests/unit/test_data_callbacks.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/unit/test_data_callbacks.py b/tests/unit/test_data_callbacks.py index 8591ecd2f..6a6099ebf 100644 --- a/tests/unit/test_data_callbacks.py +++ b/tests/unit/test_data_callbacks.py @@ -66,3 +66,9 @@ def test_item_type_fail(): ctx = MockContext() with pytest.raises(click.BadParameter): check_item_type(ctx, 'item_type', "bad_item_type") + + +def test_item_type_too_many_item_types(): + ctx = MockContext() + with pytest.raises(click.BadParameter): + check_item_types(ctx, 'item_type', "PSScene,SkySatScene") From faf07f5e136672f3ae4e1ecc3c66f4ead78bce31 Mon Sep 17 00:00:00 2001 From: Kevin Lacaille Date: Fri, 3 Mar 2023 19:48:24 -0800 Subject: [PATCH 32/34] move return outside of try/except block Co-authored-by: Jennifer Reiber Kyle --- planet/cli/data.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/planet/cli/data.py b/planet/cli/data.py index d3f5e466c..fc371e62d 100644 --- a/planet/cli/data.py +++ b/planet/cli/data.py @@ -88,11 +88,10 @@ def check_item_type(ctx, param, item_type) -> Optional[List[dict]]: raise click.BadParameter("Only provide 1 item type.") try: validate_item_type(item_type) - return item_type except SpecificationException as e: raise click.BadParameter(str(e)) - + return item_type def check_search_id(ctx, param, search_id) -> str: '''Ensure search id is a valix hex string''' try: From c753e9e70fd7830ab660302f7a712dfdd1fdabbc Mon Sep 17 00:00:00 2001 From: Kevin Lacaille Date: Fri, 3 Mar 2023 19:51:46 -0800 Subject: [PATCH 33/34] remove badparameter for more than 1 item type Co-authored-by: Jennifer Reiber Kyle --- planet/cli/data.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/planet/cli/data.py b/planet/cli/data.py index fc371e62d..52f4c4e3f 100644 --- a/planet/cli/data.py +++ b/planet/cli/data.py @@ -84,8 +84,6 @@ def check_item_types(ctx, param, item_types) -> Optional[List[dict]]: def check_item_type(ctx, param, item_type) -> Optional[List[dict]]: '''Validates the item type provided by comparing it to all supported item types.''' - if len(item_type.split(",")) > 1: - raise click.BadParameter("Only provide 1 item type.") try: validate_item_type(item_type) except SpecificationException as e: From ee268a79364893f5a257a8ca6d397253bc877c2c Mon Sep 17 00:00:00 2001 From: Kevin Lacaille Date: Fri, 3 Mar 2023 19:56:08 -0800 Subject: [PATCH 34/34] asset_type_id -> asset_type --- planet/cli/data.py | 25 +++++++++++-------------- tests/integration/test_data_cli.py | 18 +++++++++--------- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/planet/cli/data.py b/planet/cli/data.py index 52f4c4e3f..12d73e940 100644 --- a/planet/cli/data.py +++ b/planet/cli/data.py @@ -90,6 +90,8 @@ def check_item_type(ctx, param, item_type) -> Optional[List[dict]]: raise click.BadParameter(str(e)) return item_type + + def check_search_id(ctx, param, search_id) -> str: '''Ensure search id is a valix hex string''' try: @@ -498,7 +500,7 @@ async def search_update(ctx, @coro @click.argument("item_type", type=str, callback=check_item_type) @click.argument("item_id") -@click.argument("asset_type_id") +@click.argument("asset_type") @click.option('--directory', default='.', help=('Base directory for file download.'), @@ -521,7 +523,7 @@ async def search_update(ctx, async def asset_download(ctx, item_type, item_id, - asset_type_id, + asset_type, directory, filename, overwrite, @@ -544,7 +546,7 @@ async def asset_download(ctx, """ quiet = ctx.obj['QUIET'] async with data_client(ctx) as cl: - asset = await cl.get_asset(item_type, item_id, asset_type_id) + asset = await cl.get_asset(item_type, item_id, asset_type) path = await cl.download_asset(asset=asset, filename=filename, directory=Path(directory), @@ -560,11 +562,11 @@ async def asset_download(ctx, @coro @click.argument("item_type", type=str, callback=check_item_type) @click.argument("item_id") -@click.argument("asset_type_id") -async def asset_activate(ctx, item_type, item_id, asset_type_id): +@click.argument("asset_type") +async def asset_activate(ctx, item_type, item_id, asset_type): '''Activate an asset.''' async with data_client(ctx) as cl: - asset = await cl.get_asset(item_type, item_id, asset_type_id) + asset = await cl.get_asset(item_type, item_id, asset_type) await cl.activate_asset(asset) @@ -574,7 +576,7 @@ async def asset_activate(ctx, item_type, item_id, asset_type_id): @coro @click.argument("item_type", type=str, callback=check_item_type) @click.argument("item_id") -@click.argument("asset_type_id") +@click.argument("asset_type") @click.option('--delay', type=int, default=5, @@ -584,12 +586,7 @@ async def asset_activate(ctx, item_type, item_id, asset_type_id): default=200, show_default=True, help='Maximum number of polls. Set to zero for no limit.') -async def asset_wait(ctx, - item_type, - item_id, - asset_type_id, - delay, - max_attempts): +async def asset_wait(ctx, item_type, item_id, asset_type, delay, max_attempts): '''Wait for an asset to be activated. Returns when the asset state has reached "activated" and the asset is @@ -597,7 +594,7 @@ async def asset_wait(ctx, ''' quiet = ctx.obj['QUIET'] async with data_client(ctx) as cl: - asset = await cl.get_asset(item_type, item_id, asset_type_id) + asset = await cl.get_asset(item_type, item_id, asset_type) with StateBar(order_id="my asset", disable=quiet) as bar: state = await cl.wait_asset(asset, delay, diff --git a/tests/integration/test_data_cli.py b/tests/integration/test_data_cli.py index 9c69ee964..5172d3ced 100644 --- a/tests/integration/test_data_cli.py +++ b/tests/integration/test_data_cli.py @@ -62,7 +62,7 @@ def item_id(): @pytest.fixture -def asset_type_id(): +def asset_type(): return 'basic_udm2' @@ -72,7 +72,7 @@ def dl_url(): @pytest.fixture -def mock_asset_get_response(item_type, item_id, asset_type_id, dl_url): +def mock_asset_get_response(item_type, item_id, asset_type, dl_url): def _func(): basic_udm2_asset = { @@ -85,7 +85,7 @@ def _func(): "md5_digest": None, "status": 'active', "location": dl_url, - "type": asset_type_id + "type": asset_type } page_response = { @@ -951,7 +951,7 @@ def test_asset_download_default(invoke, mock_asset_get_response, item_type, item_id, - asset_type_id, + asset_type, dl_url): mock_asset_get_response() @@ -986,7 +986,7 @@ async def _stream_img(): 'asset-download', item_type, item_id, - asset_type_id, + asset_type, f'--directory={Path(folder)}', '--filename', 'img.tif' @@ -1015,7 +1015,7 @@ def test_asset_activate(invoke, mock_asset_get_response, item_type, item_id, - asset_type_id, + asset_type, dl_url): mock_asset_get_response() @@ -1025,7 +1025,7 @@ def test_asset_activate(invoke, respx.get(dl_url).return_value = mock_resp_activate runner = CliRunner() - result = invoke(['asset-activate', item_type, item_id, asset_type_id], + result = invoke(['asset-activate', item_type, item_id, asset_type], runner=runner) assert not result.exception @@ -1036,7 +1036,7 @@ def test_asset_wait(invoke, mock_asset_get_response, item_type, item_id, - asset_type_id, + asset_type, dl_url): mock_asset_get_response() @@ -1047,7 +1047,7 @@ def test_asset_wait(invoke, runner = CliRunner() result = invoke( - ['asset-wait', item_type, item_id, asset_type_id, '--delay', '0'], + ['asset-wait', item_type, item_id, asset_type, '--delay', '0'], runner=runner) assert not result.exception