diff --git a/CHANGES.txt b/CHANGES.txt index e0f441e7..44c93ccf 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,10 @@ +2.1.0 (TBD) + +Added: +- The subscription_request.build_request function has a new option to clip to + the subscription's source geometry. This is a preview of the default + behavior of the next version of the Subscriptions API. + 2.0.3 (2023-06-28) Changed: diff --git a/planet/cli/subscriptions.py b/planet/cli/subscriptions.py index a10b9c67..11f37b11 100644 --- a/planet/cli/subscriptions.py +++ b/planet/cli/subscriptions.py @@ -205,10 +205,10 @@ async def list_subscription_results_cmd(ctx, '--notifications', type=types.JSON(), help='Notifications JSON. Can be a string, filename, or - for stdin.') -@click.option('--tools', - type=types.JSON(), - help='Toolchain JSON. Can be a string, filename, or - for stdin.' - ) +@click.option( + '--tools', + type=types.JSON(), + help='Toolchain JSON. Can be a string, filename, or - for stdin.') @pretty def request(name, source, delivery, notifications, tools, pretty): """Generate a subscriptions request.""" @@ -247,10 +247,10 @@ def request(name, source, delivery, notifications, tools, pretty): @click.option('--rrule', type=str, help='iCalendar recurrance rule to specify recurrances.') -@click.option('--filter', - type=types.JSON(), - help='Search filter. Can be a string, filename, or - for stdin.' - ) +@click.option( + '--filter', + type=types.JSON(), + help='Search filter. Can be a string, filename, or - for stdin.') @pretty def request_catalog(item_types, asset_types, diff --git a/planet/subscription_request.py b/planet/subscription_request.py index a4f2f26d..059c2607 100644 --- a/planet/subscription_request.py +++ b/planet/subscription_request.py @@ -13,7 +13,7 @@ # the License. """Functionality for preparing subscription requests.""" from datetime import datetime -from typing import Any, Dict, Optional, List +from typing import Any, Dict, Optional, List, Mapping from . import geojson, specs from .exceptions import ClientError @@ -45,12 +45,27 @@ def build_request(name: str, - source: dict, - delivery: dict, - notifications: Optional[dict] = None, - tools: Optional[List[dict]] = None) -> dict: + source: Mapping, + delivery: Mapping, + notifications: Optional[Mapping] = None, + tools: Optional[List[Mapping]] = None, + clip_to_source=False) -> dict: """Prepare a subscriptions request. + Parameters: + name: Name of the subscription. + source: A source for the subscription, i.e. catalog. + delivery: A delivery mechanism e.g. GCS, AWS, Azure, or OCS. + notifications: Specify notifications via email/webhook. + tools: Tools to apply to the products. The order of operation + is determined by the service. + 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 + exists. NOTE: the next version of the Subscription API will + remove the clip tool option and always clip to the source + geometry. Thus this is a preview of the next API version's + default behavior. ```python >>> from datetime import datetime @@ -77,21 +92,26 @@ def build_request(name: str, ``` - Parameters: - name: Name of the subscription. - source: A source for the subscription, i.e. catalog. - delivery: A delivery mechanism e.g. GCS, AWS, Azure, or OCS. - notifications: Specify notifications via email/webhook. - tools: Tools to apply to the products. Order defines - the toolchain order of operatations. """ - details = {"name": name, "source": source, "delivery": delivery} + details = { + "name": name, "source": dict(source), "delivery": dict(delivery) + } if notifications: - details['notifications'] = notifications + details['notifications'] = dict(notifications) if tools: - details['tools'] = tools + tool_list = [dict(tool) for tool in tools] + if clip_to_source and not any( + tool.get('type', None) == 'clip' for tool in tool_list): + tool_list.append({ + 'type': 'clip', + 'parameters': { + 'aoi': source['parameters']['geometry'] + } + }) + + details['tools'] = tool_list return details @@ -99,9 +119,9 @@ def build_request(name: str, def catalog_source( item_types: List[str], asset_types: List[str], - geometry: dict, + geometry: Mapping, start_time: datetime, - filter: Optional[dict] = None, + filter: Optional[Mapping] = None, end_time: Optional[datetime] = None, rrule: Optional[str] = None, ) -> dict: @@ -142,7 +162,7 @@ def catalog_source( parameters = { "item_types": item_types, "asset_types": asset_types, - "geometry": geojson.as_geom(geometry), + "geometry": geojson.as_geom(dict(geometry)), } try: @@ -151,7 +171,7 @@ def catalog_source( raise ClientError('Could not convert start_time to an iso string') if filter: - parameters['filter'] = filter + parameters['filter'] = dict(filter) if end_time: try: @@ -348,7 +368,7 @@ def band_math_tool(b1: str, return _tool('bandmath', parameters) -def clip_tool(aoi: dict) -> dict: +def clip_tool(aoi: Mapping) -> dict: '''Specify a subscriptions API clip tool. Imagery and udm files will be clipped to your area of interest. nodata @@ -370,12 +390,12 @@ def clip_tool(aoi: dict) -> dict: ''' valid_types = ['Polygon', 'MultiPolygon'] - geom = geojson.as_geom(aoi) + geom = geojson.as_geom(dict(aoi)) if geom['type'].lower() not in [v.lower() for v in valid_types]: raise ClientError( f'Invalid geometry type: {geom["type"]} is not in {valid_types}.') - return _tool('clip', {'aoi': aoi}) + return _tool('clip', {'aoi': geom}) def file_format_tool(file_format: str) -> dict: diff --git a/setup.cfg b/setup.cfg index b9e07839..e24c32da 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,3 +17,6 @@ fail_under = 98 [yapf] based_on_style = pep8 split_all_top_level_comma_separated_values=true + +[flake8] +ignore = E126,E501,W50 \ No newline at end of file diff --git a/tests/integration/test_data_api.py b/tests/integration/test_data_api.py index 8ae40ef5..7a57f539 100644 --- a/tests/integration/test_data_api.py +++ b/tests/integration/test_data_api.py @@ -558,17 +558,17 @@ async def test_run_search_doesnotexist(session): async def test_get_stats_success(search_filter, session): page_response = { - "buckets": [{ - "count": 433638, "start_time": "2022-01-01T00:00:00.000000Z" - }, - { - "count": 431924, - "start_time": "2022-01-02T00:00:00.000000Z" - }, - { - "count": 417138, - "start_time": "2022-01-03T00:00:00.000000Z" - }] + "buckets": [ + { + "count": 433638, "start_time": "2022-01-01T00:00:00.000000Z" + }, + { + "count": 431924, "start_time": "2022-01-02T00:00:00.000000Z" + }, + { + "count": 417138, "start_time": "2022-01-03T00:00:00.000000Z" + }, + ], } mock_resp = httpx.Response(HTTPStatus.OK, json=page_response) respx.post(TEST_STATS_URL).return_value = mock_resp @@ -875,11 +875,11 @@ async def _stream_img(): @respx.mock @pytest.mark.anyio -@pytest.mark.parametrize("hashes_match, md5_entry, expectation", - [(True, True, does_not_raise()), - (False, True, pytest.raises(exceptions.ClientError)), - (True, False, pytest.raises(exceptions.ClientError))] - ) +@pytest.mark.parametrize( + "hashes_match, md5_entry, expectation", + [(True, True, does_not_raise()), + (False, True, pytest.raises(exceptions.ClientError)), + (True, False, pytest.raises(exceptions.ClientError))]) async def test_validate_checksum(hashes_match, md5_entry, expectation, tmpdir): test_bytes = b'foo bar' testfile = Path(tmpdir / 'test.txt') diff --git a/tests/unit/test_subscription_request.py b/tests/unit/test_subscription_request.py index 5eae0542..82347000 100644 --- a/tests/unit/test_subscription_request.py +++ b/tests/unit/test_subscription_request.py @@ -65,6 +65,31 @@ def test_build_request_success(geom_geojson): assert res == expected +def test_build_request_clip_to_source(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"] + } + } + res = subscription_request.build_request( + 'test', + source=source, + delivery={}, + tools=[{ + 'type': 'hammer' + }], + clip_to_source=True, + ) + assert res["tools"][1]["type"] == "clip" + assert res["tools"][1]["parameters"]["aoi"] == geom_geojson + + def test_catalog_source_success(geom_geojson): res = subscription_request.catalog_source( item_types=["PSScene"], @@ -230,7 +255,6 @@ def test_band_math_tool_invalid_pixel_type(): def test_clip_tool_success(geom_geojson): res = subscription_request.clip_tool(geom_geojson) - expected = {"type": "clip", "parameters": {"aoi": geom_geojson}} assert res == expected