From 891b2bb4cef2afbcc8a7f7baee230cafbf594a4e Mon Sep 17 00:00:00 2001 From: Adrian Sonnenschein Date: Thu, 8 Feb 2024 07:22:07 -0500 Subject: [PATCH] Revert "Revert "Subscriptions API Hosting Support for Sentinel Hub"" --- planet/cli/subscriptions.py | 7 ++- planet/subscription_request.py | 41 +++++++++++++--- tests/integration/test_subscriptions_api.py | 12 +++++ tests/integration/test_subscriptions_cli.py | 7 ++- tests/unit/test_subscription_request.py | 53 +++++++++++++++++++++ 5 files changed, 112 insertions(+), 8 deletions(-) diff --git a/planet/cli/subscriptions.py b/planet/cli/subscriptions.py index b3bfe2ba0..70b7dad7d 100644 --- a/planet/cli/subscriptions.py +++ b/planet/cli/subscriptions.py @@ -249,7 +249,6 @@ async def list_subscription_results_cmd(ctx, help='Source JSON. Can be a string, filename, or - for stdin.') @click.option( '--delivery', - required=True, type=types.JSON(), help=("Delivery configuration, including credentials for a cloud " "storage provider, to enable cloud delivery of data. Can be a " @@ -262,6 +261,10 @@ async def list_subscription_results_cmd(ctx, '--tools', type=types.JSON(), help='Toolchain JSON. Can be a string, filename, or - for stdin.') +@click.option( + '--hosting', + type=types.JSON(), + help='Hosting JSON. Can be a string, a filename, or - for stdin.') @click.option( '--clip-to-source', is_flag=True, @@ -273,6 +276,7 @@ def request(name, delivery, notifications, tools, + hosting, clip_to_source, pretty): """Generate a subscriptions request. @@ -287,6 +291,7 @@ def request(name, delivery, notifications=notifications, tools=tools, + hosting=hosting, clip_to_source=clip_to_source) echo_json(res, pretty) diff --git a/planet/subscription_request.py b/planet/subscription_request.py index fd1273c1c..520e34113 100644 --- a/planet/subscription_request.py +++ b/planet/subscription_request.py @@ -49,9 +49,10 @@ def build_request(name: str, source: Mapping, - delivery: Mapping, + delivery: Optional[Mapping] = None, notifications: Optional[Mapping] = None, tools: Optional[List[Mapping]] = None, + hosting: Optional[Mapping] = None, clip_to_source: Optional[bool] = False) -> dict: """Construct a Subscriptions API request. @@ -65,6 +66,7 @@ def build_request(name: str, notifications: Specify notifications via email/webhook. tools: Tools to apply to the products. The order of operation is determined by the service. + hosting: A hosting destination e.g. Sentinel Hub. clip_to_source: whether to clip to the source geometry or not (the default). If True a clip configuration will be added to the list of requested tools unless an existing clip tool @@ -106,17 +108,20 @@ def build_request(name: str, delivery = amazon_s3(ACCESS_KEY_ID, SECRET_ACCESS_KEY, "test", "us-east-1") + hosting = sentinel_hub("2716077c-191e-4e47-9e3f-01c9c429f88d") + subscription_request = build_request( - "test_subscription", source=source, delivery=delivery + "test_subscription", source=source, delivery=delivery, hosting=hosting ) ``` """ - # Because source and delivery are Mappings we must make copies for + # Because source is a Mapping we must make copies for # the function's return value. dict() shallow copies a Mapping # and returns a new dict. - details = { - "name": name, "source": dict(source), "delivery": dict(delivery) - } + details = {"name": name, "source": dict(source)} + + if delivery: + details['delivery'] = dict(delivery) if notifications: details['notifications'] = dict(notifications) @@ -145,6 +150,9 @@ def build_request(name: str, details['tools'] = tool_list + if hosting: + details['hosting'] = dict(hosting) + return details @@ -735,3 +743,24 @@ def cloud_filter_tool( } return _tool("cloud_filter", result) + + +def _hosting(type: str, parameters: dict) -> dict: + return {"type": type, "parameters": parameters} + + +def sentinel_hub(collection_id: Optional[str]) -> dict: + """Specify a Sentinel Hub hosting destination. + + Requires the user to have a Sentinel Hub account linked with their Planet + account. Subscriptions API will create a new collection to deliver data to + if collection_id is omitted from the request. + + Parameters: + collection_id: Sentinel Hub collection + """ + + parameters = {} + if collection_id: + parameters['collection_id'] = collection_id + return _hosting("sentinel_hub", parameters) diff --git a/tests/integration/test_subscriptions_api.py b/tests/integration/test_subscriptions_api.py index aef5fa8b4..7fbc9bab2 100644 --- a/tests/integration/test_subscriptions_api.py +++ b/tests/integration/test_subscriptions_api.py @@ -204,6 +204,18 @@ async def test_create_subscription_success(): assert sub['name'] == 'test' +@pytest.mark.anyio +@create_mock +async def test_create_subscription_with_hosting_success(): + """Subscription is created, description has the expected items.""" + async with Session() as session: + client = SubscriptionsClient(session, base_url=TEST_URL) + sub = await client.create_subscription({ + 'name': 'test', 'source': 'test', 'hosting': 'yes, please' + }) + assert sub['name'] == 'test' + + @pytest.mark.anyio @failing_api_mock async def test_cancel_subscription_failure(): diff --git a/tests/integration/test_subscriptions_cli.py b/tests/integration/test_subscriptions_cli.py index 3ac230ee6..25b462dd3 100644 --- a/tests/integration/test_subscriptions_cli.py +++ b/tests/integration/test_subscriptions_cli.py @@ -95,11 +95,16 @@ def test_subscriptions_create_failure(invoke): # It must be updated when we begin to test against a more strict # imitation of the Planet Subscriptions API. GOOD_SUB_REQUEST = {'name': 'lol', 'delivery': True, 'source': 'wut'} +GOOD_SUB_REQUEST_WITH_HOSTING = { + 'name': 'lol', 'source': 'wut', 'hosting': True +} @pytest.mark.parametrize('cmd_arg, runner_input', [('-', json.dumps(GOOD_SUB_REQUEST)), - (json.dumps(GOOD_SUB_REQUEST), None)]) + (json.dumps(GOOD_SUB_REQUEST), None), + ('-', json.dumps(GOOD_SUB_REQUEST_WITH_HOSTING)), + (json.dumps(GOOD_SUB_REQUEST_WITH_HOSTING), None)]) @create_mock def test_subscriptions_create_success(invoke, cmd_arg, runner_input): """Subscriptions creation succeeds with a valid subscription request.""" diff --git a/tests/unit/test_subscription_request.py b/tests/unit/test_subscription_request.py index ef4e43488..2aa12f081 100644 --- a/tests/unit/test_subscription_request.py +++ b/tests/unit/test_subscription_request.py @@ -119,6 +119,59 @@ def test_build_request_clip_to_source_failure(geom_geojson): ) +def test_build_request_host_sentinel_hub_with_collection(geom_geojson): + source = { + "type": "catalog", + "parameters": { + "geometry": geom_geojson, + "start_time": "2021-03-01T00:00:00Z", + "end_time": "2023-11-01T00:00:00Z", + "rrule": "FREQ=MONTHLY;BYMONTH=3,4,5,6,7,8,9,10", + "item_types": ["PSScene"], + "asset_types": ["ortho_analytic_4b"] + } + } + + hosting = {"type": "sentinel-hub"} + + res = subscription_request.build_request('test', + source=source, + hosting=hosting) + + expected = {"name": "test", "source": source, "hosting": hosting} + + assert res == expected + + +def test_build_request_host_sentinel_hub_no_collection(geom_geojson): + source = { + "type": "catalog", + "parameters": { + "geometry": geom_geojson, + "start_time": "2021-03-01T00:00:00Z", + "end_time": "2023-11-01T00:00:00Z", + "rrule": "FREQ=MONTHLY;BYMONTH=3,4,5,6,7,8,9,10", + "item_types": ["PSScene"], + "asset_types": ["ortho_analytic_4b"] + } + } + + hosting = { + "type": "sentinel-hub", + "parameters": { + "collection_id": "4c9af036-4274-4a97-bf0d-eb2a7853330d" + } + } + + res = subscription_request.build_request('test', + source=source, + hosting=hosting) + + expected = {"name": "test", "source": source, "hosting": hosting} + + assert res == expected + + def test_catalog_source_success(geom_geojson): res = subscription_request.catalog_source( item_types=["PSScene"],