Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
11 changes: 11 additions & 0 deletions planet/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,14 @@
PLANET_BASE_URL = 'https://api.planet.com'

SECRET_FILE_PATH = Path(os.path.expanduser('~')) / '.planet.json'

# Tool weights define the required processing order for subscription tools
_SUBSCRIPTION_TOOL_WEIGHT = {
"harmonize": 1,
"toar": 2,
"clip": 3,
"reproject": 3,
"bandmath": 3,
"cloud_filter": 4,
"file_format": 4,
}
58 changes: 54 additions & 4 deletions planet/subscription_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

from . import geojson, specs
from .clients.destinations import DEFAULT_DESTINATION_REF
from .constants import _SUBSCRIPTION_TOOL_WEIGHT
from .exceptions import ClientError

NOTIFICATIONS_TOPICS = ('delivery.success',
Expand Down Expand Up @@ -48,6 +49,54 @@
REPROJECT_KERNEL_DEFAULT = 'near'


def _validate_tool_order(tool_list: List[dict]) -> None:
"""Validate that tools are ordered according to their processing weights.

Args:
tool_list: List of tool configurations to validate.

Raises:
ClientError: If tools are not in the correct order.
"""
for i in range(1, len(tool_list)):
prev_type = tool_list[i - 1].get('type')
curr_type = tool_list[i].get('type')
prev_weight = _SUBSCRIPTION_TOOL_WEIGHT.get(prev_type)
curr_weight = _SUBSCRIPTION_TOOL_WEIGHT.get(curr_type)

if prev_weight is not None and curr_weight is not None:
if prev_weight > curr_weight:
raise ClientError(
f"Tools must be ordered according to their processing order. "
f"Tool '{prev_type}' cannot come before tool '{curr_type}'."
)


def _insert_clip_tool(tool_list: List[dict]) -> None:
"""Insert clip tool at the correct position in the tool list.

The clip tool is inserted based on its position in the _SUBSCRIPTION_TOOL_WEIGHT
dictionary, ensuring it comes before any tool that appears after it in the
processing order.
Comment thread
asonnenschein marked this conversation as resolved.
Outdated

Args:
tool_list: List of tool configurations (modified in place).
"""
# Create a position mapping from the _SUBSCRIPTION_TOOL_WEIGHT dictionary
tool_order = {name: idx for idx, name in enumerate(_SUBSCRIPTION_TOOL_WEIGHT.keys())}
clip_position = tool_order['clip']
insert_index = len(tool_list) # default to end

for i, tool in enumerate(tool_list):
tool_type = tool.get('type')
if tool_type in tool_order:
if tool_order[tool_type] > clip_position:
insert_index = i
break
Comment thread
asonnenschein marked this conversation as resolved.
Outdated

tool_list.insert(insert_index, {'type': 'clip', 'parameters': {}})


def build_request(name: str,
source: Mapping,
delivery: Optional[Mapping] = None,
Expand Down Expand Up @@ -128,16 +177,17 @@ def build_request(name: str,
if tools or clip_to_source:
tool_list = [dict(tool) for tool in (tools or [])]

# 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.
# Validate that input tool_list is in correct order
_validate_tool_order(tool_list)
Comment thread
asonnenschein marked this conversation as resolved.

# If clip_to_source is True, insert clip at correct position
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': {}})
_insert_clip_tool(tool_list)

details['tools'] = tool_list

Expand Down
187 changes: 187 additions & 0 deletions tests/unit/test_subscription_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -688,3 +688,190 @@ def test_default_destination_path_prefix_success():
"ref": "pl:destinations/default", "path_prefix": "my/prefix"
}
}


@pytest.mark.parametrize(
"input_tools,expected_order",
[
# Test valid ordering is maintained
(
[
{"type": "harmonize"},
{"type": "clip"},
{"type": "file_format"}
],
["harmonize", "clip", "file_format"]
),
# Test all tools in correct order
(
[
{"type": "harmonize"},
{"type": "toar"},
{"type": "clip"},
{"type": "reproject"},
{"type": "bandmath"},
{"type": "cloud_filter"},
{"type": "file_format"}
],
["harmonize", "toar", "clip", "reproject", "bandmath", "cloud_filter", "file_format"]
),
# Test tools with same weight can be in any order
(
[
{"type": "reproject"},
{"type": "clip"},
{"type": "bandmath"}
],
["reproject", "clip", "bandmath"]
),
# Test different order for same weight tools
(
[
{"type": "clip"},
{"type": "bandmath"},
{"type": "reproject"}
],
["clip", "bandmath", "reproject"]
),
# Test single tool
(
[{"type": "clip"}],
["clip"]
),
# Test weight 4 tools can be in any order
(
[
{"type": "file_format"},
{"type": "cloud_filter"}
],
["file_format", "cloud_filter"]
),
]
)
def test_build_request_tool_order_maintained(geom_geojson, input_tools, expected_order):
"""Test that valid tool ordering is maintained."""
source = {
"parameters": {
"geometry": geom_geojson,
"start_time": "2021-03-01T00:00:00Z",
"item_types": ["PSScene"],
"asset_types": ["ortho_analytic_4b"]
}
}

req = subscription_request.build_request(
'test',
source=source,
delivery={},
tools=input_tools
)

actual_order = [tool["type"] for tool in req["tools"]]
assert actual_order == expected_order


@pytest.mark.parametrize(
"invalid_tools",
[
# clip before harmonize (weight 3 before weight 1)
[{"type": "clip"}, {"type": "harmonize"}],
# file_format before toar (weight 4 before weight 2)
[{"type": "file_format"}, {"type": "toar"}],
# cloud_filter before clip (weight 4 before weight 3)
[{"type": "cloud_filter"}, {"type": "clip"}],
# Multiple out of order
[{"type": "file_format"}, {"type": "harmonize"}, {"type": "clip"}],
]
)
def test_build_request_invalid_tool_order(geom_geojson, invalid_tools):
"""Test that invalid tool ordering raises an error."""
source = {
"parameters": {
"geometry": geom_geojson,
"start_time": "2021-03-01T00:00:00Z",
"item_types": ["PSScene"],
"asset_types": ["ortho_analytic_4b"]
}
}

with pytest.raises(exceptions.ClientError, match="Tools must be ordered according to their processing order"):
subscription_request.build_request(
'test',
source=source,
delivery={},
tools=invalid_tools
)


@pytest.mark.parametrize(
"existing_tools,expected_order",
[
# Clip added to empty list
(
[],
["clip"]
),
# Clip inserted before higher weight tools (weight 4)
(
[{"type": "file_format"}, {"type": "cloud_filter"}],
["clip", "file_format", "cloud_filter"]
),
# Clip inserted after lower weight tools (weight 1, 2)
(
[{"type": "harmonize"}, {"type": "toar"}],
["harmonize", "toar", "clip"]
),
# Clip inserted between lower and higher weight tools
(
[
{"type": "harmonize"},
{"type": "toar"},
{"type": "file_format"}
],
["harmonize", "toar", "clip", "file_format"]
),
# Clip inserted before tools of same weight (weight 3)
(
[{"type": "reproject"}, {"type": "bandmath"}],
["clip", "reproject", "bandmath"]
),
# Clip inserted at end when all tools have lower weight
(
[{"type": "harmonize"}, {"type": "toar"}],
["harmonize", "toar", "clip"]
),
# Complex case with all tool types in correct order except clip
(
[
{"type": "harmonize"},
{"type": "toar"},
{"type": "reproject"},
{"type": "bandmath"},
{"type": "cloud_filter"},
{"type": "file_format"}
],
["harmonize", "toar", "clip", "reproject", "bandmath", "cloud_filter", "file_format"]
),
]
)
def test_build_request_clip_to_source_insertion(geom_geojson, existing_tools, expected_order):
"""Test that clip tool is inserted at correct position when clip_to_source=True."""
source = {
"parameters": {
"geometry": geom_geojson,
"start_time": "2021-03-01T00:00:00Z",
"item_types": ["PSScene"],
"asset_types": ["ortho_analytic_4b"]
}
}

req = subscription_request.build_request(
'test',
source=source,
delivery={},
tools=existing_tools,
clip_to_source=True
)

actual_order = [tool["type"] for tool in req["tools"]]
assert actual_order == expected_order
Loading