Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a planetary_variable_source subscription helper #976

Merged
merged 3 commits into from
Jul 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
2.1.0 (TBD)

Added:
- A subscription_request.planetary_variable_source function has been added
(#976).
- 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.
the subscription's source geometry. This is a preview of the default behavior
sgillies marked this conversation as resolved.
Show resolved Hide resolved
of the next version of the Subscriptions API (#971).

2.0.3 (2023-06-28)

Expand Down
133 changes: 110 additions & 23 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, Mapping
from typing import Any, Dict, Optional, List, Literal, Mapping

from . import geojson, specs
from .exceptions import ClientError
Expand Down Expand Up @@ -49,7 +49,7 @@ def build_request(name: str,
delivery: Mapping,
notifications: Optional[Mapping] = None,
tools: Optional[List[Mapping]] = None,
clip_to_source=False) -> dict:
clip_to_source: Optional[bool] = False) -> dict:
"""Construct a Subscriptions API request.

The return value can be passed to
Expand All @@ -73,12 +73,12 @@ def build_request(name: str,
behavior.

Returns:
A Python dict representation of a Subscriptions API request for
a new subscription.
dict: a representation of a Subscriptions API request for
a new subscription.

Raises:
ClientError when a valid Subscriptions API request can't be
constructed.
ClientError: when a valid Subscriptions API request can't be
constructed.

Examples:
```python
Expand Down Expand Up @@ -152,27 +152,34 @@ def catalog_source(
end_time: Optional[datetime] = None,
rrule: Optional[str] = None,
) -> dict:
"""Catalog subscription source.
"""Construct a Catalog subscription source.

The return value can be passed to
[planet.subscription_request.build_request][].

Parameters:
item_types: The class of spacecraft and processing level of the
subscription's matching items, e.g. PSScene.
asset_types: The data products which will be delivered for all subscription
matching items. An item will only match and deliver if all specified
asset types are published for that item.
geometry: The area of interest of the subscription that will be used to
determine matches.
start_time: The start time of the subscription. This time can be in the
past or future.
filter: The filter criteria based on item-level metadata.
end_time: The end time of the subscription. This time can be in the past or
future, and must be after the start_time.
rrule: The recurrence rule, given in iCalendar RFC 5545 format. Only
monthly recurrences are supported at this time.
item_types: The class of spacecraft and processing level of the
subscription's matching items, e.g. PSScene.
asset_types: The data products which will be delivered for all
subscription matching items. An item will only match and
deliver if all specified asset types are published for that
item.
geometry: The area of interest of the subscription that will be
used to determine matches.
start_time: The start time of the subscription. This time can be
in the past or future.
filter: The filter criteria based on item-level metadata.
end_time: The end time of the subscription. This time can be in
the past or future, and must be after the start_time.
rrule: The recurrence rule, given in iCalendar RFC 5545 format.
Only monthly recurrences are supported at this time.

Returns:
dict: a representation of a subscription source.

Raises:
planet.exceptions.ClientError: If start_time or end_time are not valid
datetimes
ClientError: if a source can not be
configured.
"""
if len(item_types) > 1:
raise ClientError(
Expand Down Expand Up @@ -212,6 +219,86 @@ def catalog_source(
return {"type": "catalog", "parameters": parameters}


def planetary_variable_source(
var_type: Literal["biomass_proxy",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just wanted to point out that internally we are already using more sources for upcoming forest products.

I'm not sure if there is a way to not duplicate these lists.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cpaulik I'm going off what we have at https://developers.planet.com/docs/subscriptions/pvs-subs/#planetary-variables-types-and-ids. Do you know if we publish a machine-readable version of this table for customers?

I'm using the Literal type hints here so that developers get value hints from their IDEs. The SDK does not at this time validate the values at runtime. Thus you can use any values you want, including unavailable PV types and ids. See for example https://github.com/planetlabs/planet-client-python/pull/982/files#diff-661d60b3722e519e66594c89a655b8aa191b919ac27c98ecffde58f984ab120eR355.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You sparked my curiosity. Since our API docs spell them out I did some digging and found that https://api.planet.com/subscriptions/v1/spec includes the sources types under schemas.PVSourceTypes

"land_surface_temperature",
"soil_water_content",
"vegetation_optical_depth"],
var_id: str,
geometry: Mapping,
start_time: datetime,
end_time: Optional[datetime] = None,
) -> dict:
"""Construct a Planetary Variable subscription source.

Planetary Variables come in 4 types and are further subdivided
within these types. See [Subscribing to Planetary
Variables](https://developers.planet.com/docs/subscriptions/pvs-subs/#planetary-variables-types-and-ids)
for details.

The return value can be passed to
[planet.subscription_request.build_request][].
sgillies marked this conversation as resolved.
Show resolved Hide resolved

Note: this function does not validate variable types and ids.

Parameters:
var_type: one of "biomass_proxy", "land_surface_temperature",
"soil_water_content", or "vegetation_optical_depth".
var_id: a value such as "SWC-AMSR2-C_V1.0_100" for soil water
content derived from AMSR2 C band.
sgillies marked this conversation as resolved.
Show resolved Hide resolved
geometry: The area of interest of the subscription that will be
used to determine matches.
start_time: The start time of the subscription. This time can be
in the past or future.
end_time: The end time of the subscription. This time can be in
the past or future, and must be after the start_time.

Returns:
dict: a representation of a subscription source.

Raises:
ClientError: if a source can not be
configured.

Examples:
```python
>>> source = planetary_variable_source(
... "soil_water_content",
... "SWC-AMSR2-C_V1.0_100",
... geometry={
... "type": "Polygon",
... "coordinates": [[[37.791595458984375, 14.84923123791421],
... [37.90214538574219, 14.84923123791421],
... [37.90214538574219, 14.945448293647944],
... [37.791595458984375, 14.945448293647944],
... [37.791595458984375, 14.84923123791421]]]
... },
... start_time=datetime(2021, 3, 1)
... )
>>> request = build_request(source=source, ...)
```
"""
# TODO: validation of variable types and ids.

parameters = {
"id": var_id,
"geometry": geojson.as_geom(dict(geometry)),
}

try:
parameters['start_time'] = _datetime_to_rfc3339(start_time)
except AttributeError:
raise ClientError('Could not convert start_time to an iso string')

if end_time:
try:
parameters['end_time'] = _datetime_to_rfc3339(end_time)
except AttributeError:
raise ClientError('Could not convert end_time to an iso string')

return {"type": var_type, "parameters": parameters}


def _datetime_to_rfc3339(value: datetime) -> str:
"""Converts the datetime to an RFC3339 string"""
iso = value.isoformat()
Expand Down
20 changes: 20 additions & 0 deletions tests/unit/test_subscription_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,3 +346,23 @@ def test_toar_tool_success():

expected = {"type": "toar", "parameters": {"scale_factor": 12345}}
assert res == expected


def test_pv_source_success(geom_geojson):
"""Configure a planetary variable subscription source."""
# NOTE: this function does not yet validate type and id.
# The nonsense values are intended to fail when the function does
# add validation.
source = subscription_request.planetary_variable_source(
"var1",
"VAR1-abcd",
kevinlacaille marked this conversation as resolved.
Show resolved Hide resolved
geometry=geom_geojson,
start_time=datetime(2021, 3, 1),
end_time=datetime(2021, 3, 2),
)

assert source["type"] == "var1"
params = source["parameters"]
assert params["id"] == "VAR1-abcd"
assert params["geometry"] == geom_geojson
assert params["start_time"].startswith("2021-03-01")