Skip to content

Commit

Permalink
Merge pull request #971 from planetlabs/issue967
Browse files Browse the repository at this point in the history
Add a clip_to_source kwarg to subscript_request.build_request
  • Loading branch information
sgillies authored Jul 7, 2023
2 parents 4ad5a45 + c93e074 commit 79d9a3c
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 38 deletions.
7 changes: 7 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
97 changes: 72 additions & 25 deletions planet/subscription_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -45,13 +45,42 @@


def build_request(name: str,
source: dict,
delivery: dict,
notifications: Optional[dict] = None,
tools: Optional[List[dict]] = None) -> dict:
"""Prepare a subscriptions request.
source: Mapping,
delivery: Mapping,
notifications: Optional[Mapping] = None,
tools: Optional[List[Mapping]] = None,
clip_to_source=False) -> dict:
"""Construct a Subscriptions API request.
The return value can be passed to
[planet.clients.subscriptions.SubscriptionsClient.create_subscription][].
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: Not all data layers support clipping, please
consult the Product reference before using this option.
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.
Returns:
A Python dict representation of a Subscriptions API request for
a new subscription.
Raises:
ClientError when a valid Subscriptions API request can't be
constructed.
Examples:
```python
>>> from datetime import datetime
>>> from planet.subscription_request import (
Expand All @@ -72,36 +101,54 @@ def build_request(name: str,
... ACCESS_KEY_ID, SECRET_ACCESS_KEY, "test", "us-east-1")
...
>>> subscription_request = build_request(
... 'test_subscription', source, delivery)
... 'test_subscription', source=source, delivery=delivery)
...
```
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}
# Because source and delivery are Mappings 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)
}

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 is True a clip configuration will be added
# to the list of requested tools unless an existing clip tool
# exists. In that case an exception is raised. 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.
if clip_to_source:
if any(tool.get('type', None) == 'clip' for tool in tool_list):
raise ClientError(
"clip_to_source option conflicts with a configured clip tool."
)
else:
tool_list.append({
'type': 'clip',
'parameters': {
'aoi': source['parameters']['geometry']
}
})

details['tools'] = tool_list

return details


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:
Expand Down Expand Up @@ -142,7 +189,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:
Expand All @@ -151,7 +198,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:
Expand Down Expand Up @@ -348,7 +395,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
Expand All @@ -370,12 +417,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:
Expand Down
3 changes: 3 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 3 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
continue

install_requires = [
'click>=8.0.0',
# click 8.1.4 breaks our mypy check, see
# https://github.com/pallets/click/issues/2558.
'click>8.0,<8.1.4',
'geojson',
'httpx>=0.23.0',
'jsonschema',
Expand Down
22 changes: 11 additions & 11 deletions tests/integration/test_data_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
54 changes: 53 additions & 1 deletion tests/unit/test_subscription_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,59 @@ def test_build_request_success(geom_geojson):
assert res == expected


def test_build_request_clip_to_source_success(geom_geojson):
"""Without a clip tool we can clip to source."""
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"]
}
}
req = subscription_request.build_request(
'test',
source=source,
delivery={},
tools=[{
'type': 'hammer'
}],
clip_to_source=True,
)
assert req["tools"][1]["type"] == "clip"
assert req["tools"][1]["parameters"]["aoi"] == geom_geojson


def test_build_request_clip_to_source_failure(geom_geojson):
"""With a clip tool we can not clip to source."""
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"]
}
}
with pytest.raises(exceptions.ClientError):
subscription_request.build_request(
'test',
source=source,
delivery={},
tools=[{
'type': 'clip'
}, {
'type': 'hammer'
}],
clip_to_source=True,
)


def test_catalog_source_success(geom_geojson):
res = subscription_request.catalog_source(
item_types=["PSScene"],
Expand Down Expand Up @@ -230,7 +283,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

Expand Down

0 comments on commit 79d9a3c

Please sign in to comment.