Skip to content

Commit 79d9a3c

Browse files
authored
Merge pull request #971 from planetlabs/issue967
Add a clip_to_source kwarg to subscript_request.build_request
2 parents 4ad5a45 + c93e074 commit 79d9a3c

File tree

6 files changed

+149
-38
lines changed

6 files changed

+149
-38
lines changed

CHANGES.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
2.1.0 (TBD)
2+
3+
Added:
4+
- The subscription_request.build_request function has a new option to clip to
5+
the subscription's source geometry. This is a preview of the default
6+
behavior of the next version of the Subscriptions API.
7+
18
2.0.3 (2023-06-28)
29

310
Changed:

planet/subscription_request.py

Lines changed: 72 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# the License.
1414
"""Functionality for preparing subscription requests."""
1515
from datetime import datetime
16-
from typing import Any, Dict, Optional, List
16+
from typing import Any, Dict, Optional, List, Mapping
1717

1818
from . import geojson, specs
1919
from .exceptions import ClientError
@@ -45,13 +45,42 @@
4545

4646

4747
def build_request(name: str,
48-
source: dict,
49-
delivery: dict,
50-
notifications: Optional[dict] = None,
51-
tools: Optional[List[dict]] = None) -> dict:
52-
"""Prepare a subscriptions request.
48+
source: Mapping,
49+
delivery: Mapping,
50+
notifications: Optional[Mapping] = None,
51+
tools: Optional[List[Mapping]] = None,
52+
clip_to_source=False) -> dict:
53+
"""Construct a Subscriptions API request.
5354
55+
The return value can be passed to
56+
[planet.clients.subscriptions.SubscriptionsClient.create_subscription][].
5457
58+
Parameters:
59+
name: Name of the subscription.
60+
source: A source for the subscription, i.e. catalog.
61+
delivery: A delivery mechanism e.g. GCS, AWS, Azure, or OCS.
62+
notifications: Specify notifications via email/webhook.
63+
tools: Tools to apply to the products. The order of operation
64+
is determined by the service.
65+
clip_to_source: whether to clip to the source geometry or not
66+
(the default). If True a clip configuration will be added to
67+
the list of requested tools unless an existing clip tool
68+
exists. NOTE: Not all data layers support clipping, please
69+
consult the Product reference before using this option.
70+
NOTE: the next version of the Subscription API will remove
71+
the clip tool option and always clip to the source geometry.
72+
Thus this is a preview of the next API version's default
73+
behavior.
74+
75+
Returns:
76+
A Python dict representation of a Subscriptions API request for
77+
a new subscription.
78+
79+
Raises:
80+
ClientError when a valid Subscriptions API request can't be
81+
constructed.
82+
83+
Examples:
5584
```python
5685
>>> from datetime import datetime
5786
>>> from planet.subscription_request import (
@@ -72,36 +101,54 @@ def build_request(name: str,
72101
... ACCESS_KEY_ID, SECRET_ACCESS_KEY, "test", "us-east-1")
73102
...
74103
>>> subscription_request = build_request(
75-
... 'test_subscription', source, delivery)
104+
... 'test_subscription', source=source, delivery=delivery)
76105
...
77106
78107
```
79-
80-
Parameters:
81-
name: Name of the subscription.
82-
source: A source for the subscription, i.e. catalog.
83-
delivery: A delivery mechanism e.g. GCS, AWS, Azure, or OCS.
84-
notifications: Specify notifications via email/webhook.
85-
tools: Tools to apply to the products. Order defines
86-
the toolchain order of operatations.
87108
"""
88-
details = {"name": name, "source": source, "delivery": delivery}
109+
# Because source and delivery are Mappings we must make copies for
110+
# the function's return value. dict() shallow copies a Mapping
111+
# and returns a new dict.
112+
details = {
113+
"name": name, "source": dict(source), "delivery": dict(delivery)
114+
}
89115

90116
if notifications:
91-
details['notifications'] = notifications
117+
details['notifications'] = dict(notifications)
92118

93119
if tools:
94-
details['tools'] = tools
120+
tool_list = [dict(tool) for tool in tools]
121+
122+
# If clip_to_source is True a clip configuration will be added
123+
# to the list of requested tools unless an existing clip tool
124+
# exists. In that case an exception is raised. NOTE: the next
125+
# version of the Subscription API will remove the clip tool
126+
# option and always clip to the source geometry. Thus this is a
127+
# preview of the next API version's default behavior.
128+
if clip_to_source:
129+
if any(tool.get('type', None) == 'clip' for tool in tool_list):
130+
raise ClientError(
131+
"clip_to_source option conflicts with a configured clip tool."
132+
)
133+
else:
134+
tool_list.append({
135+
'type': 'clip',
136+
'parameters': {
137+
'aoi': source['parameters']['geometry']
138+
}
139+
})
140+
141+
details['tools'] = tool_list
95142

96143
return details
97144

98145

99146
def catalog_source(
100147
item_types: List[str],
101148
asset_types: List[str],
102-
geometry: dict,
149+
geometry: Mapping,
103150
start_time: datetime,
104-
filter: Optional[dict] = None,
151+
filter: Optional[Mapping] = None,
105152
end_time: Optional[datetime] = None,
106153
rrule: Optional[str] = None,
107154
) -> dict:
@@ -142,7 +189,7 @@ def catalog_source(
142189
parameters = {
143190
"item_types": item_types,
144191
"asset_types": asset_types,
145-
"geometry": geojson.as_geom(geometry),
192+
"geometry": geojson.as_geom(dict(geometry)),
146193
}
147194

148195
try:
@@ -151,7 +198,7 @@ def catalog_source(
151198
raise ClientError('Could not convert start_time to an iso string')
152199

153200
if filter:
154-
parameters['filter'] = filter
201+
parameters['filter'] = dict(filter)
155202

156203
if end_time:
157204
try:
@@ -348,7 +395,7 @@ def band_math_tool(b1: str,
348395
return _tool('bandmath', parameters)
349396

350397

351-
def clip_tool(aoi: dict) -> dict:
398+
def clip_tool(aoi: Mapping) -> dict:
352399
"""Specify a subscriptions API clip tool.
353400
354401
Imagery and udm files will be clipped to your area of interest. nodata
@@ -370,12 +417,12 @@ def clip_tool(aoi: dict) -> dict:
370417
"""
371418
valid_types = ['Polygon', 'MultiPolygon']
372419

373-
geom = geojson.as_geom(aoi)
420+
geom = geojson.as_geom(dict(aoi))
374421
if geom['type'].lower() not in [v.lower() for v in valid_types]:
375422
raise ClientError(
376423
f'Invalid geometry type: {geom["type"]} is not in {valid_types}.')
377424

378-
return _tool('clip', {'aoi': aoi})
425+
return _tool('clip', {'aoi': geom})
379426

380427

381428
def file_format_tool(file_format: str) -> dict:

setup.cfg

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,6 @@ fail_under = 98
1717
[yapf]
1818
based_on_style = pep8
1919
split_all_top_level_comma_separated_values=true
20+
21+
[flake8]
22+
ignore = E126,E501,W50

setup.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@
2424
continue
2525

2626
install_requires = [
27-
'click>=8.0.0',
27+
# click 8.1.4 breaks our mypy check, see
28+
# https://github.com/pallets/click/issues/2558.
29+
'click>8.0,<8.1.4',
2830
'geojson',
2931
'httpx>=0.23.0',
3032
'jsonschema',

tests/integration/test_data_api.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -558,17 +558,17 @@ async def test_run_search_doesnotexist(session):
558558
async def test_get_stats_success(search_filter, session):
559559

560560
page_response = {
561-
"buckets": [{
562-
"count": 433638, "start_time": "2022-01-01T00:00:00.000000Z"
563-
},
564-
{
565-
"count": 431924,
566-
"start_time": "2022-01-02T00:00:00.000000Z"
567-
},
568-
{
569-
"count": 417138,
570-
"start_time": "2022-01-03T00:00:00.000000Z"
571-
}]
561+
"buckets": [
562+
{
563+
"count": 433638, "start_time": "2022-01-01T00:00:00.000000Z"
564+
},
565+
{
566+
"count": 431924, "start_time": "2022-01-02T00:00:00.000000Z"
567+
},
568+
{
569+
"count": 417138, "start_time": "2022-01-03T00:00:00.000000Z"
570+
},
571+
],
572572
}
573573
mock_resp = httpx.Response(HTTPStatus.OK, json=page_response)
574574
respx.post(TEST_STATS_URL).return_value = mock_resp

tests/unit/test_subscription_request.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,59 @@ def test_build_request_success(geom_geojson):
6565
assert res == expected
6666

6767

68+
def test_build_request_clip_to_source_success(geom_geojson):
69+
"""Without a clip tool we can clip to source."""
70+
source = {
71+
"type": "catalog",
72+
"parameters": {
73+
"geometry": geom_geojson,
74+
"start_time": "2021-03-01T00:00:00Z",
75+
"end_time": "2023-11-01T00:00:00Z",
76+
"rrule": "FREQ=MONTHLY;BYMONTH=3,4,5,6,7,8,9,10",
77+
"item_types": ["PSScene"],
78+
"asset_types": ["ortho_analytic_4b"]
79+
}
80+
}
81+
req = subscription_request.build_request(
82+
'test',
83+
source=source,
84+
delivery={},
85+
tools=[{
86+
'type': 'hammer'
87+
}],
88+
clip_to_source=True,
89+
)
90+
assert req["tools"][1]["type"] == "clip"
91+
assert req["tools"][1]["parameters"]["aoi"] == geom_geojson
92+
93+
94+
def test_build_request_clip_to_source_failure(geom_geojson):
95+
"""With a clip tool we can not clip to source."""
96+
source = {
97+
"type": "catalog",
98+
"parameters": {
99+
"geometry": geom_geojson,
100+
"start_time": "2021-03-01T00:00:00Z",
101+
"end_time": "2023-11-01T00:00:00Z",
102+
"rrule": "FREQ=MONTHLY;BYMONTH=3,4,5,6,7,8,9,10",
103+
"item_types": ["PSScene"],
104+
"asset_types": ["ortho_analytic_4b"]
105+
}
106+
}
107+
with pytest.raises(exceptions.ClientError):
108+
subscription_request.build_request(
109+
'test',
110+
source=source,
111+
delivery={},
112+
tools=[{
113+
'type': 'clip'
114+
}, {
115+
'type': 'hammer'
116+
}],
117+
clip_to_source=True,
118+
)
119+
120+
68121
def test_catalog_source_success(geom_geojson):
69122
res = subscription_request.catalog_source(
70123
item_types=["PSScene"],
@@ -230,7 +283,6 @@ def test_band_math_tool_invalid_pixel_type():
230283

231284
def test_clip_tool_success(geom_geojson):
232285
res = subscription_request.clip_tool(geom_geojson)
233-
234286
expected = {"type": "clip", "parameters": {"aoi": geom_geojson}}
235287
assert res == expected
236288

0 commit comments

Comments
 (0)