diff --git a/planet/cli/orders.py b/planet/cli/orders.py index fc031636c..60d52ae55 100644 --- a/planet/cli/orders.py +++ b/planet/cli/orders.py @@ -225,26 +225,22 @@ async def create(ctx, request: str, pretty): @click.pass_context @translate_exceptions @coro +@click.argument('item_type', + metavar='ITEM_TYPE', + type=click.Choice(planet.specs.get_item_types(), + case_sensitive=False)) +@click.argument('bundle', + metavar='BUNDLE', + type=click.Choice(planet.specs.get_product_bundles(), + case_sensitive=False)) @click.option('--name', required=True, help='Order name. Does not need to be unique.', type=click.STRING) -@click.option( - '--bundle', - multiple=False, - required=True, - help='Product bundle.', - type=click.Choice(planet.specs.get_product_bundles(), - case_sensitive=False), -) @click.option('--id', help='One or more comma-separated item IDs.', type=types.CommaSeparatedString(), required=True) -@click.option('--item-type', - required=True, - help='Specify an item type', - type=click.STRING) @click.option('--clip', type=types.JSON(), help="""Clip feature GeoJSON. Can be a json string, filename, @@ -271,12 +267,12 @@ async def create(ctx, request: str, pretty): format. Not specifying either defaults to including it (--stac).""") @pretty async def request(ctx, - name, + item_type, bundle, + name, id, clip, tools, - item_type, email, cloudconfig, stac, diff --git a/planet/order_request.py b/planet/order_request.py index 911927bc6..b24a1ecab 100644 --- a/planet/order_request.py +++ b/planet/order_request.py @@ -113,12 +113,13 @@ def product(item_ids: List[str], are not valid bundles or if item_type is not valid for the given bundle or fallback bundle. ''' - validated_product_bundle = specs.validate_bundle(product_bundle) - item_type = specs.validate_item_type(item_type, validated_product_bundle) + item_type = specs.validate_item_type(item_type) + validated_product_bundle = specs.validate_bundle(item_type, product_bundle) if fallback_bundle is not None: - validated_fallback_bundle = specs.validate_bundle(fallback_bundle) - specs.validate_item_type(item_type, validated_fallback_bundle) + item_type = specs.validate_item_type(item_type) + validated_fallback_bundle = specs.validate_bundle( + item_type, fallback_bundle) validated_product_bundle = ','.join( [validated_product_bundle, validated_fallback_bundle]) diff --git a/planet/specs.py b/planet/specs.py index 9c08a0bb8..e24312f94 100644 --- a/planet/specs.py +++ b/planet/specs.py @@ -55,15 +55,15 @@ def __str__(self): return f'{self.field_name} - \'{self.value}\' is not one of {self.opts}.' -def validate_bundle(bundle): - supported = get_product_bundles() - return _validate_field(bundle, supported, 'product_bundle') +def validate_bundle(item_type, bundle): + all_product_bundles = get_product_bundles() + validate_supported_bundles(item_type, bundle, all_product_bundles) + return _validate_field(bundle, all_product_bundles, 'product_bundle') -def validate_item_type(item_type, bundle): - validated_bundle = validate_bundle(bundle) - supported = get_item_types(validated_bundle) - return _validate_field(item_type, supported, 'item_type') +def validate_item_type(item_type): + supported_item_types = get_item_types() + return _validate_field(item_type, supported_item_types, 'item_type') def validate_order_type(order_type): @@ -92,6 +92,25 @@ def _validate_field(value, supported, field_name): return value +def validate_supported_bundles(item_type, bundle, all_product_bundles): + spec = _get_product_bundle_spec() + + supported_bundles = [] + for product_bundle in all_product_bundles: + availible_item_types = set( + spec['bundles'][product_bundle]['assets'].keys()) + if item_type.lower() in [x.lower() for x in availible_item_types]: + supported_bundles.append(product_bundle) + + return _validate_field(bundle, supported_bundles, 'bundle') + + +def _get_product_bundle_spec(): + with open(DATA_DIR / PRODUCT_BUNDLE_SPEC_NAME) as f: + data = json.load(f) + return data + + def get_match(test_entry, spec_entries, field_name): '''Find and return matching spec entry regardless of capitalization. @@ -107,10 +126,22 @@ def get_match(test_entry, spec_entries, field_name): return match -def get_product_bundles(): +def get_product_bundles(item_type=None): '''Get product bundles supported by Orders API.''' spec = _get_product_bundle_spec() - return spec['bundles'].keys() + + if item_type: + all_product_bundles = get_product_bundles() + + supported_bundles = [] + for product_bundle in all_product_bundles: + availible_item_types = set( + spec['bundles'][product_bundle]['assets'].keys()) + if item_type.lower() in [x.lower() for x in availible_item_types]: + supported_bundles.append(product_bundle) + else: + supported_bundles = spec['bundles'].keys() + return supported_bundles def get_item_types(product_bundle=None): @@ -127,9 +158,3 @@ def get_item_types(product_bundle=None): for bundle in get_product_bundles())) return item_types - - -def _get_product_bundle_spec(): - with open(DATA_DIR / PRODUCT_BUNDLE_SPEC_NAME) as f: - data = json.load(f) - return data diff --git a/tests/integration/test_orders_cli.py b/tests/integration/test_orders_cli.py index f47c6bb13..3610ad9e2 100644 --- a/tests/integration/test_orders_cli.py +++ b/tests/integration/test_orders_cli.py @@ -53,7 +53,6 @@ def stac_json(): def test_cli_orders_list_basic(invoke, order_descriptions): next_page_url = TEST_ORDERS_URL + '/blob/?page_marker=IAmATest' order1, order2, order3 = order_descriptions - page1_response = { "_links": { "_self": "string", "next": next_page_url @@ -462,10 +461,10 @@ def test_cli_orders_request_basic_success(expected_ids, stac_json): result = invoke([ 'request', + 'PSOrthoTile', + 'analytic', '--name=test', f'--id={id_string}', - '--bundle=analytic', - '--item-type=PSOrthoTile' ]) assert not result.exception @@ -486,24 +485,43 @@ def test_cli_orders_request_basic_success(expected_ids, def test_cli_orders_request_item_type_invalid(invoke): result = invoke([ 'request', + 'invalid' + 'analytic', '--name=test', '--id=4500474_2133707_2021-05-20_2419', - '--bundle=analytic', - '--item-type=invalid' ]) assert result.exit_code == 2 - assert 'Error: Invalid value: item_type' in result.output + assert "Usage: main orders request [OPTIONS] ITEM_TYPE BUNDLE" in result.output -def test_cli_orders_request_id_empty(invoke): +def test_cli_orders_request_product_bundle_invalid(invoke): result = invoke([ 'request', + 'PSScene' + 'invalid', '--name=test', - '--id=', - '--bundle=analytic', - '--item-type=PSOrthoTile', + '--id=4500474_2133707_2021-05-20_2419', ]) assert result.exit_code == 2 + assert "Usage: main orders request [OPTIONS] ITEM_TYPE BUNDLE" in result.output + + +def test_cli_orders_request_product_bundle_incompatible(invoke): + result = invoke([ + 'request', + 'PSScene', + 'analytic', + '--name=test', + '--id=4500474_2133707_2021-05-20_2419', + ]) + assert result.exit_code == 2 + assert "Usage: main orders request [OPTIONS] ITEM_TYPE BUNDLE" in result.output + + +def test_cli_orders_request_id_empty(invoke): + result = invoke( + ['request', 'PSOrthoTile', 'analytic', '--name=test', '--id=']) + assert result.exit_code == 2 assert 'Entry cannot be an empty string.' in result.output @@ -520,10 +538,10 @@ def test_cli_orders_request_clip_success(geom_fixture, result = invoke([ 'request', + 'PSOrthoTile', + 'analytic', '--name=test', '--id=4500474_2133707_2021-05-20_2419', - '--bundle=analytic', - '--item-type=PSOrthoTile', f'--clip={json.dumps(geom)}', ]) assert result.exit_code == 0 @@ -550,10 +568,10 @@ def test_cli_orders_request_clip_success(geom_fixture, def test_cli_orders_request_clip_invalid_geometry(invoke, point_geom_geojson): result = invoke([ 'request', + 'PSOrthoTile', + 'analytic', '--name=test', '--id=4500474_2133707_2021-05-20_2419', - '--bundle=analytic', - '--item-type=PSOrthoTile', f'--clip={json.dumps(point_geom_geojson)}' ]) assert result.exit_code == 2 @@ -567,10 +585,10 @@ def test_cli_orders_request_both_clip_and_tools(invoke, geom_geojson): # option values are valid json result = invoke([ 'request', + 'PSOrthoTile', + 'analytic', '--name=test', '--id=4500474_2133707_2021-05-20_2419', - '--bundle=analytic', - '--item-type=PSOrthoTile', f'--clip={json.dumps(geom_geojson)}', f'--tools={json.dumps(geom_geojson)}' ]) @@ -592,10 +610,10 @@ def test_cli_orders_request_cloudconfig(invoke, stac_json): result = invoke([ 'request', + 'PSOrthoTile', + 'analytic', '--name=test', '--id=4500474_2133707_2021-05-20_2419', - '--bundle=analytic', - '--item-type=PSOrthoTile', f'--cloudconfig={json.dumps(config_json)}', ]) assert result.exit_code == 0 @@ -619,10 +637,10 @@ def test_cli_orders_request_cloudconfig(invoke, stac_json): def test_cli_orders_request_email(invoke, stac_json): result = invoke([ 'request', + 'PSOrthoTile', + 'analytic', '--name=test', '--id=4500474_2133707_2021-05-20_2419', - '--bundle=analytic', - '--item-type=PSOrthoTile', '--email' ]) assert result.exit_code == 0 @@ -650,10 +668,10 @@ def test_cli_orders_request_tools(invoke, geom_geojson, stac_json): result = invoke([ 'request', + 'PSOrthoTile', + 'analytic', '--name=test', '--id=4500474_2133707_2021-05-20_2419', - '--bundle=analytic', - '--item-type=PSOrthoTile', f'--tools={json.dumps(tools_json)}' ]) @@ -678,10 +696,10 @@ def test_cli_orders_request_no_stac(invoke): result = invoke([ 'request', + 'PSOrthoTile', + 'analytic', '--name=test', '--id=4500474_2133707_2021-05-20_2419', - '--bundle=analytic', - '--item-type=PSOrthoTile', '--no-stac' ]) diff --git a/tests/unit/test_data_item_type.py b/tests/unit/test_data_callbacks.py similarity index 100% rename from tests/unit/test_data_item_type.py rename to tests/unit/test_data_callbacks.py diff --git a/tests/unit/test_specs.py b/tests/unit/test_specs.py index e7c17bf73..f326f7696 100644 --- a/tests/unit/test_specs.py +++ b/tests/unit/test_specs.py @@ -21,6 +21,37 @@ LOGGER = logging.getLogger(__name__) TEST_PRODUCT_BUNDLE = 'visual' +ALL_PRODUCT_BUNDLES = [ + 'analytic', + 'analytic_udm2', + 'analytic_3b_udm2', + 'analytic_5b', + 'analytic_5b_udm2', + 'analytic_8b_udm2', + 'visual', + 'uncalibrated_dn', + 'uncalibrated_dn_udm2', + 'basic_analytic', + 'basic_analytic_udm2', + 'basic_analytic_8b_udm2', + 'basic_uncalibrated_dn', + 'basic_uncalibrated_dn_udm2', + 'analytic_sr', + 'analytic_sr_udm2', + 'analytic_8b_sr_udm2', + 'basic_uncalibrated_dn_nitf', + 'basic_uncalibrated_dn_nitf_udm2', + 'basic_analytic_nitf', + 'basic_analytic_nitf_udm2', + 'basic_panchromatic', + 'basic_panchromatic_dn', + 'panchromatic', + 'panchromatic_dn', + 'panchromatic_dn_udm2', + 'pansharpened', + 'pansharpened_udm2', + 'basic_l1a_dn' +] # must be a valid item type for TEST_PRODUCT_BUNDLE TEST_ITEM_TYPE = 'PSScene' ALL_ITEM_TYPES = [ @@ -54,26 +85,26 @@ def test_get_type_match(): def test_validate_bundle_supported(): - assert 'analytic' == specs.validate_bundle('ANALYTIC') + assert 'visual' == specs.validate_bundle(TEST_ITEM_TYPE, 'VISUAL') def test_validate_bundle_notsupported(): with pytest.raises(specs.SpecificationException): - specs.validate_bundle('notsupported') + specs.validate_bundle(TEST_ITEM_TYPE, 'notsupported') -def test_validate_item_type_supported(): - assert 'PSOrthoTile' == specs.validate_item_type('psorthotile', 'analytic') +def test_validate_bundle_notsupported_item_type(): + with pytest.raises(specs.SpecificationException): + specs.validate_item_type('wha') -def test_validate_item_type_notsupported_bundle(): - with pytest.raises(specs.SpecificationException): - specs.validate_item_type('psorthotile', 'wha') +def test_validate_item_type_supported(): + assert 'PSOrthoTile' == specs.validate_item_type('psorthotile') def test_validate_item_type_notsupported_itemtype(): with pytest.raises(specs.SpecificationException): - specs.validate_item_type('notsupported', 'analytic') + specs.validate_item_type('notsupported') def test_validate_order_type_supported(): @@ -103,11 +134,17 @@ def test_validate_file_format_notsupported(): specs.validate_archive_type('notsupported') -def test_get_product_bundles(): - bundles = specs.get_product_bundles() +def test_get_product_bundles_with_item_type(): + bundles = specs.get_product_bundles(item_type=TEST_ITEM_TYPE) assert TEST_PRODUCT_BUNDLE in bundles +def test_get_product_bundles_without_item_type(): + bundles = specs.get_product_bundles() + for bundle in bundles: + assert bundle in ALL_PRODUCT_BUNDLES + + def test_get_item_types_with_bundle(): item_types = specs.get_item_types(product_bundle=TEST_PRODUCT_BUNDLE) assert TEST_ITEM_TYPE in item_types @@ -117,3 +154,16 @@ def test_get_item_types_without_bundle(): item_types = specs.get_item_types() for item in item_types: assert item in ALL_ITEM_TYPES + + +def test_validate_supported_bundles_success(): + validated_bundle = specs.validate_supported_bundles( + TEST_ITEM_TYPE, TEST_PRODUCT_BUNDLE, ALL_PRODUCT_BUNDLES) + assert validated_bundle in ALL_PRODUCT_BUNDLES + + +def test_validate_supported_bundles_fail(): + with pytest.raises(specs.SpecificationException): + specs.validate_supported_bundles(TEST_ITEM_TYPE, + 'analytic', + ALL_PRODUCT_BUNDLES)