Skip to content

Commit

Permalink
create top level geom filter in data api
Browse files Browse the repository at this point in the history
allow feat refs for it
  • Loading branch information
Andy Gaither committed Apr 18, 2024
1 parent f94d68c commit 8a9c4b6
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 12 deletions.
30 changes: 24 additions & 6 deletions planet/cli/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import click

from planet.reporting import AssetStatusBar
from planet import data_filter, DataClient, exceptions
from planet import data_filter, DataClient, exceptions, geojson
from planet.clients.data import (SEARCH_SORT,
LIST_SEARCH_TYPE,
LIST_SEARCH_TYPE_DEFAULT,
Expand Down Expand Up @@ -81,6 +81,11 @@ def check_item_types(ctx, param, item_types) -> Optional[List[dict]]:
raise click.BadParameter(str(e))


def check_geom(ctx, param, geometry: Optional[dict]) -> Optional[dict]:
"""Validates geometry as GeoJSON or feature ref(s)."""
return geojson.as_geom_or_ref(geometry) if geometry else None


def check_item_type(ctx, param, item_type) -> Optional[List[dict]]:
"""Validates the item type provided by comparing it to all supported
item types."""
Expand Down Expand Up @@ -281,6 +286,7 @@ def filter(ctx,
@click.argument("item_types",
type=types.CommaSeparatedString(),
callback=check_item_types)
@click.option("--geom", type=types.JSON(), callback=check_geom)
@click.option('--filter',
type=types.JSON(),
help="""Apply specified filter to search. Can be a json string,
Expand All @@ -293,7 +299,7 @@ def filter(ctx,
show_default=True,
help='Field and direction to order results by.')
@pretty
async def search(ctx, item_types, filter, limit, name, sort, pretty):
async def search(ctx, item_types, geom, filter, limit, name, sort, pretty):
"""Execute a structured item search.
This function outputs a series of GeoJSON descriptions, one for each of the
Expand All @@ -311,6 +317,7 @@ async def search(ctx, item_types, filter, limit, name, sort, pretty):
async with data_client(ctx) as cl:

async for item in cl.search(item_types,
geometry=geom,
search_filter=filter,
name=name,
sort=sort,
Expand All @@ -325,6 +332,7 @@ async def search(ctx, item_types, filter, limit, name, sort, pretty):
@click.argument("item_types",
type=types.CommaSeparatedString(),
callback=check_item_types)
@click.option("--geom", type=types.JSON(), callback=check_geom)
@click.option(
'--filter',
type=types.JSON(),
Expand All @@ -339,7 +347,13 @@ async def search(ctx, item_types, filter, limit, name, sort, pretty):
is_flag=True,
help='Send a daily email when new results are added.')
@pretty
async def search_create(ctx, item_types, filter, name, daily_email, pretty):
async def search_create(ctx,
item_types,
geom,
filter,
name,
daily_email,
pretty):
"""Create a new saved structured item search.
This function outputs a full JSON description of the created search,
Expand All @@ -349,6 +363,7 @@ async def search_create(ctx, item_types, filter, name, daily_email, pretty):
"""
async with data_client(ctx) as cl:
items = await cl.create_search(item_types=item_types,
geometry=geom,
search_filter=filter,
name=name,
enable_email=daily_email)
Expand Down Expand Up @@ -485,6 +500,7 @@ async def search_delete(ctx, search_id):
type=str,
required=True,
help='Name of the saved search.')
@click.option("--geom", type=types.JSON(), callback=check_geom, default=None)
@click.option('--daily-email',
is_flag=True,
help='Send a daily email when new results are added.')
Expand All @@ -493,6 +509,7 @@ async def search_update(ctx,
search_id,
item_types,
filter,
geom,
name,
daily_email,
pretty):
Expand All @@ -504,9 +521,10 @@ async def search_update(ctx,
async with data_client(ctx) as cl:
items = await cl.update_search(search_id,
item_types,
filter,
name,
daily_email)
search_filter=filter,
name=name,
geometry=geom,
enable_email=daily_email)
echo_json(items, pretty)


Expand Down
18 changes: 17 additions & 1 deletion planet/clients/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from ..http import Session
from ..models import Paged, StreamingBody
from ..specs import validate_data_item_type
from ..geojson import as_geom_or_ref

BASE_URL = f'{PLANET_BASE_URL}/data/v1/'
SEARCHES_PATH = '/searches'
Expand Down Expand Up @@ -112,6 +113,7 @@ def _item_url(self, item_type, item_id):

async def search(self,
item_types: List[str],
geometry: Optional[dict] = None,
search_filter: Optional[dict] = None,
name: Optional[str] = None,
sort: Optional[str] = None,
Expand All @@ -134,6 +136,8 @@ async def search(self,
sort: Field and direction to order results by. Valid options are
given in SEARCH_SORT.
name: The name of the saved search.
geometry: GeoJSON, a feature reference or a list of feature
references
limit: Maximum number of results to return. When set to 0, no
maximum is applied.
Expand All @@ -149,6 +153,9 @@ async def search(self,

item_types = [validate_data_item_type(item) for item in item_types]
request_json = {'filter': search_filter, 'item_types': item_types}

if geometry:
request_json['geometry'] = as_geom_or_ref(geometry)
if name:
request_json['name'] = name

Expand All @@ -159,7 +166,6 @@ async def search(self,
raise exceptions.ClientError(
f'{sort} must be one of {SEARCH_SORT}')
params['_sort'] = sort

response = await self._session.request(method='POST',
url=url,
json=request_json,
Expand All @@ -171,6 +177,7 @@ async def create_search(self,
item_types: List[str],
search_filter: dict,
name: str,
geometry: Optional[dict] = None,
enable_email: bool = False) -> dict:
"""Create a new saved structured item search.
Expand All @@ -192,6 +199,7 @@ async def create_search(self,
Parameters:
item_types: The item types to include in the search.
geometry: A feature reference or a GeoJSON
search_filter: Structured search criteria.
name: The name of the saved search.
enable_email: Send a daily email when new results are added.
Expand All @@ -205,12 +213,15 @@ async def create_search(self,
url = self._searches_url()

item_types = [validate_data_item_type(item) for item in item_types]

request = {
'name': name,
'filter': search_filter,
'item_types': item_types,
'__daily_email_enabled': enable_email
}
if geometry:
request['geometry'] = as_geom_or_ref(geometry)

response = await self._session.request(method='POST',
url=url,
Expand All @@ -222,12 +233,14 @@ async def update_search(self,
item_types: List[str],
search_filter: dict,
name: str,
geometry: Optional[dict] = None,
enable_email: bool = False) -> dict:
"""Update an existing saved search.
Parameters:
search_id: Saved search identifier.
item_types: The item types to include in the search.
geometry: A feature reference or a GeoJSON
search_filter: Structured search criteria.
name: The name of the saved search.
enable_email: Send a daily email when new results are added.
Expand All @@ -238,12 +251,15 @@ async def update_search(self,
url = f'{self._searches_url()}/{search_id}'

item_types = [validate_data_item_type(item) for item in item_types]

request = {
'name': name,
'filter': search_filter,
'item_types': item_types,
'__daily_email_enabled': enable_email
}
if geometry:
request['geometry'] = geometry

response = await self._session.request(method='PUT',
url=url,
Expand Down
54 changes: 52 additions & 2 deletions tests/integration/test_data_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,52 @@ async def test_search_name(item_descriptions, search_response, session):
assert items_list == item_descriptions


@respx.mock
@pytest.mark.anyio
@pytest.mark.parametrize("geom_fixture", [('geom_geojson'),
('geom_reference')])
async def test_search_geometry(geom_fixture,
item_descriptions,
session,
request):

quick_search_url = f'{TEST_URL}/quick-search'
next_page_url = f'{TEST_URL}/blob/?page_marker=IAmATest'

item1, item2, item3 = item_descriptions
page1_response = {
"_links": {
"_next": next_page_url
}, "features": [item1, item2]
}
mock_resp1 = httpx.Response(HTTPStatus.OK, json=page1_response)
respx.post(quick_search_url).return_value = mock_resp1

page2_response = {"_links": {"_self": next_page_url}, "features": [item3]}
mock_resp2 = httpx.Response(HTTPStatus.OK, json=page2_response)
respx.get(next_page_url).return_value = mock_resp2

cl = DataClient(session, base_url=TEST_URL)
geom = request.getfixturevalue(geom_fixture)
items_list = [
i async for i in cl.search(
['PSScene'], name='quick_search', geometry=geom)
]
# check that request is correct
expected_request = {
"item_types": ["PSScene"],
"geometry": geom,
"filter": data_filter.empty_filter(),
"name": "quick_search"
}
actual_body = json.loads(respx.calls[0].request.content)

assert actual_body == expected_request

# check that all of the items were returned unchanged
assert items_list == item_descriptions


@respx.mock
@pytest.mark.anyio
async def test_search_filter(item_descriptions,
Expand Down Expand Up @@ -197,7 +243,10 @@ async def test_search_sort(item_descriptions,
cl = DataClient(session, base_url=TEST_URL)

# run through the iterator to actually initiate the call
[i async for i in cl.search(['PSScene'], search_filter, sort=sort)]
[
i async for i in cl.search(
['PSScene'], search_filter=search_filter, sort=sort)
]


@respx.mock
Expand All @@ -218,7 +267,8 @@ async def test_search_limit(item_descriptions,

cl = DataClient(session, base_url=TEST_URL)
items_list = [
i async for i in cl.search(['PSScene'], search_filter, limit=2)
i async for i in cl.search(
['PSScene'], search_filter=search_filter, limit=2)
]

# check only the first two results were returned
Expand Down
22 changes: 19 additions & 3 deletions tests/integration/test_data_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -415,9 +415,7 @@ def test_data_filter_update(invoke, assert_and_filters_equal):


@respx.mock
@pytest.mark.parametrize("item_types, expect_success",
[('PSScene', True), ('SkySatScene', True),
('PSScene, SkySatScene', True), ('INVALID', False)])
@pytest.mark.parametrize("item_types, expect_success", [('PSScene', True)])
def test_data_search_cmd_item_types(item_types, expect_success, invoke):
"""Test for planet data search_quick item types, valid and invalid."""
mock_resp = httpx.Response(HTTPStatus.OK,
Expand All @@ -435,6 +433,24 @@ def test_data_search_cmd_item_types(item_types, expect_success, invoke):
assert result.exit_code == 2


@respx.mock
@pytest.mark.parametrize("geom_fixture",
[('geom_geojson'), ('feature_geojson'),
('featurecollection_geojson'), ('geom_reference')])
def test_data_search_cmd_top_level_geom(geom_fixture, request, invoke):
"""Ensure that all GeoJSON forms of describing a geometry are handled
and all result in the same, valid GeometryFilter being created"""
mock_resp = httpx.Response(HTTPStatus.OK,
json={'features': [{
"key": "value"
}]})
respx.post(TEST_QUICKSEARCH_URL).return_value = mock_resp
geom = request.getfixturevalue(geom_fixture)

result = invoke(["search", 'PSScene', f"--geom={json.dumps(geom)}"])
assert result.exit_code == 0


@respx.mock
@pytest.mark.parametrize("filter", ['{1:1}', '{"foo"}'])
def test_data_search_cmd_filter_invalid_json(invoke, filter):
Expand Down

0 comments on commit 8a9c4b6

Please sign in to comment.