Skip to content

Commit ce2bf64

Browse files
author
Andy Gaither
committed
create top level geom filter in data api
allow feat refs for it
1 parent f94d68c commit ce2bf64

File tree

4 files changed

+108
-9
lines changed

4 files changed

+108
-9
lines changed

planet/cli/data.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import click
2020

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

8383

84+
def check_geom(ctx, param, geometry: Optional[dict]) -> Optional[dict]:
85+
"""Validates geometry as GeoJSON or feature ref(s)."""
86+
return geojson.as_geom_or_ref(geometry) if geometry else None
87+
88+
8489
def check_item_type(ctx, param, item_type) -> Optional[List[dict]]:
8590
"""Validates the item type provided by comparing it to all supported
8691
item types."""
@@ -281,6 +286,7 @@ def filter(ctx,
281286
@click.argument("item_types",
282287
type=types.CommaSeparatedString(),
283288
callback=check_item_types)
289+
@click.option("--geom", type=types.JSON(), callback=check_geom)
284290
@click.option('--filter',
285291
type=types.JSON(),
286292
help="""Apply specified filter to search. Can be a json string,
@@ -293,7 +299,7 @@ def filter(ctx,
293299
show_default=True,
294300
help='Field and direction to order results by.')
295301
@pretty
296-
async def search(ctx, item_types, filter, limit, name, sort, pretty):
302+
async def search(ctx, item_types, geom, filter, limit, name, sort, pretty):
297303
"""Execute a structured item search.
298304
299305
This function outputs a series of GeoJSON descriptions, one for each of the
@@ -311,6 +317,7 @@ async def search(ctx, item_types, filter, limit, name, sort, pretty):
311317
async with data_client(ctx) as cl:
312318

313319
async for item in cl.search(item_types,
320+
geometry=geom,
314321
search_filter=filter,
315322
name=name,
316323
sort=sort,
@@ -325,6 +332,7 @@ async def search(ctx, item_types, filter, limit, name, sort, pretty):
325332
@click.argument("item_types",
326333
type=types.CommaSeparatedString(),
327334
callback=check_item_types)
335+
@click.option("--geom", type=types.JSON(), callback=check_geom)
328336
@click.option(
329337
'--filter',
330338
type=types.JSON(),
@@ -339,7 +347,13 @@ async def search(ctx, item_types, filter, limit, name, sort, pretty):
339347
is_flag=True,
340348
help='Send a daily email when new results are added.')
341349
@pretty
342-
async def search_create(ctx, item_types, filter, name, daily_email, pretty):
350+
async def search_create(ctx,
351+
item_types,
352+
geom,
353+
filter,
354+
name,
355+
daily_email,
356+
pretty):
343357
"""Create a new saved structured item search.
344358
345359
This function outputs a full JSON description of the created search,
@@ -349,6 +363,7 @@ async def search_create(ctx, item_types, filter, name, daily_email, pretty):
349363
"""
350364
async with data_client(ctx) as cl:
351365
items = await cl.create_search(item_types=item_types,
366+
geometry=geom,
352367
search_filter=filter,
353368
name=name,
354369
enable_email=daily_email)
@@ -492,6 +507,7 @@ async def search_delete(ctx, search_id):
492507
async def search_update(ctx,
493508
search_id,
494509
item_types,
510+
geometry,
495511
filter,
496512
name,
497513
daily_email,
@@ -504,6 +520,7 @@ async def search_update(ctx,
504520
async with data_client(ctx) as cl:
505521
items = await cl.update_search(search_id,
506522
item_types,
523+
geometry,
507524
filter,
508525
name,
509526
daily_email)

planet/clients/data.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from ..http import Session
2727
from ..models import Paged, StreamingBody
2828
from ..specs import validate_data_item_type
29+
from ..geojson import as_geom_or_ref
2930

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

113114
async def search(self,
114115
item_types: List[str],
116+
geometry: Optional[dict] = None,
115117
search_filter: Optional[dict] = None,
116118
name: Optional[str] = None,
117119
sort: Optional[str] = None,
@@ -134,6 +136,8 @@ async def search(self,
134136
sort: Field and direction to order results by. Valid options are
135137
given in SEARCH_SORT.
136138
name: The name of the saved search.
139+
geometry: GeoJSON, a feature reference or a list of feature
140+
references
137141
limit: Maximum number of results to return. When set to 0, no
138142
maximum is applied.
139143
@@ -149,6 +153,9 @@ async def search(self,
149153

150154
item_types = [validate_data_item_type(item) for item in item_types]
151155
request_json = {'filter': search_filter, 'item_types': item_types}
156+
157+
if geometry:
158+
request_json['geometry'] = as_geom_or_ref(geometry)
152159
if name:
153160
request_json['name'] = name
154161

@@ -159,7 +166,6 @@ async def search(self,
159166
raise exceptions.ClientError(
160167
f'{sort} must be one of {SEARCH_SORT}')
161168
params['_sort'] = sort
162-
163169
response = await self._session.request(method='POST',
164170
url=url,
165171
json=request_json,
@@ -169,6 +175,7 @@ async def search(self,
169175

170176
async def create_search(self,
171177
item_types: List[str],
178+
geometry: Optional[dict],
172179
search_filter: dict,
173180
name: str,
174181
enable_email: bool = False) -> dict:
@@ -192,6 +199,7 @@ async def create_search(self,
192199
193200
Parameters:
194201
item_types: The item types to include in the search.
202+
geometry: A feature reference or a GeoJSON
195203
search_filter: Structured search criteria.
196204
name: The name of the saved search.
197205
enable_email: Send a daily email when new results are added.
@@ -205,12 +213,15 @@ async def create_search(self,
205213
url = self._searches_url()
206214

207215
item_types = [validate_data_item_type(item) for item in item_types]
216+
208217
request = {
209218
'name': name,
210219
'filter': search_filter,
211220
'item_types': item_types,
212221
'__daily_email_enabled': enable_email
213222
}
223+
if geometry:
224+
request['geometry'] = as_geom_or_ref(geometry)
214225

215226
response = await self._session.request(method='POST',
216227
url=url,
@@ -220,6 +231,7 @@ async def create_search(self,
220231
async def update_search(self,
221232
search_id: str,
222233
item_types: List[str],
234+
geometry: Optional[dict],
223235
search_filter: dict,
224236
name: str,
225237
enable_email: bool = False) -> dict:
@@ -228,6 +240,7 @@ async def update_search(self,
228240
Parameters:
229241
search_id: Saved search identifier.
230242
item_types: The item types to include in the search.
243+
geometry: A feature reference or a GeoJSON
231244
search_filter: Structured search criteria.
232245
name: The name of the saved search.
233246
enable_email: Send a daily email when new results are added.
@@ -238,12 +251,15 @@ async def update_search(self,
238251
url = f'{self._searches_url()}/{search_id}'
239252

240253
item_types = [validate_data_item_type(item) for item in item_types]
254+
241255
request = {
242256
'name': name,
243257
'filter': search_filter,
244258
'item_types': item_types,
245259
'__daily_email_enabled': enable_email
246260
}
261+
if geometry:
262+
request['geometry'] = geometry
247263

248264
response = await self._session.request(method='PUT',
249265
url=url,

tests/integration/test_data_api.py

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,52 @@ async def test_search_name(item_descriptions, search_response, session):
140140
assert items_list == item_descriptions
141141

142142

143+
@respx.mock
144+
@pytest.mark.anyio
145+
@pytest.mark.parametrize("geom_fixture", [('geom_geojson'),
146+
('geom_reference')])
147+
async def test_search_geometry(geom_fixture,
148+
item_descriptions,
149+
session,
150+
request):
151+
152+
quick_search_url = f'{TEST_URL}/quick-search'
153+
next_page_url = f'{TEST_URL}/blob/?page_marker=IAmATest'
154+
155+
item1, item2, item3 = item_descriptions
156+
page1_response = {
157+
"_links": {
158+
"_next": next_page_url
159+
}, "features": [item1, item2]
160+
}
161+
mock_resp1 = httpx.Response(HTTPStatus.OK, json=page1_response)
162+
respx.post(quick_search_url).return_value = mock_resp1
163+
164+
page2_response = {"_links": {"_self": next_page_url}, "features": [item3]}
165+
mock_resp2 = httpx.Response(HTTPStatus.OK, json=page2_response)
166+
respx.get(next_page_url).return_value = mock_resp2
167+
168+
cl = DataClient(session, base_url=TEST_URL)
169+
geom = request.getfixturevalue(geom_fixture)
170+
items_list = [
171+
i async for i in cl.search(
172+
['PSScene'], name='quick_search', geometry=geom)
173+
]
174+
# check that request is correct
175+
expected_request = {
176+
"item_types": ["PSScene"],
177+
"geometry": geom,
178+
"filter": data_filter.empty_filter(),
179+
"name": "quick_search"
180+
}
181+
actual_body = json.loads(respx.calls[0].request.content)
182+
183+
assert actual_body == expected_request
184+
185+
# check that all of the items were returned unchanged
186+
assert items_list == item_descriptions
187+
188+
143189
@respx.mock
144190
@pytest.mark.anyio
145191
async def test_search_filter(item_descriptions,
@@ -197,7 +243,10 @@ async def test_search_sort(item_descriptions,
197243
cl = DataClient(session, base_url=TEST_URL)
198244

199245
# run through the iterator to actually initiate the call
200-
[i async for i in cl.search(['PSScene'], search_filter, sort=sort)]
246+
[
247+
i async for i in cl.search(
248+
['PSScene'], search_filter=search_filter, sort=sort)
249+
]
201250

202251

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

219268
cl = DataClient(session, base_url=TEST_URL)
220269
items_list = [
221-
i async for i in cl.search(['PSScene'], search_filter, limit=2)
270+
i async for i in cl.search(
271+
['PSScene'], search_filter=search_filter, limit=2)
222272
]
223273

224274
# check only the first two results were returned

tests/integration/test_data_cli.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -415,9 +415,7 @@ def test_data_filter_update(invoke, assert_and_filters_equal):
415415

416416

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

437435

436+
@respx.mock
437+
@pytest.mark.parametrize("geom_fixture",
438+
[('geom_geojson'), ('feature_geojson'),
439+
('featurecollection_geojson'), ('geom_reference')])
440+
def test_data_search_cmd_top_level_geom(geom_fixture, request, invoke):
441+
"""Ensure that all GeoJSON forms of describing a geometry are handled
442+
and all result in the same, valid GeometryFilter being created"""
443+
mock_resp = httpx.Response(HTTPStatus.OK,
444+
json={'features': [{
445+
"key": "value"
446+
}]})
447+
respx.post(TEST_QUICKSEARCH_URL).return_value = mock_resp
448+
geom = request.getfixturevalue(geom_fixture)
449+
450+
result = invoke(["search", 'PSScene', f"--geom={json.dumps(geom)}"])
451+
assert result.exit_code == 0
452+
453+
438454
@respx.mock
439455
@pytest.mark.parametrize("filter", ['{1:1}', '{"foo"}'])
440456
def test_data_search_cmd_filter_invalid_json(invoke, filter):

0 commit comments

Comments
 (0)