Skip to content

Commit 86ea523

Browse files
authored
Merge pull request #585 from planetlabs/cli_data_stats_#488
CLI data stats #488
2 parents fb3481a + d3d56bd commit 86ea523

File tree

5 files changed

+101
-8
lines changed

5 files changed

+101
-8
lines changed

design-docs/CLI-Data.md

-3
Original file line numberDiff line numberDiff line change
@@ -726,9 +726,6 @@ ITEM_TYPES - string. Comma-separated item type identifier(s).
726726
INTERVAL - string. The size of the histogram buckets (<hour, day, week, month, year>)
727727
FILTER - string. A full JSON description filter. Supports file and stdin.
728728
729-
Options:
730-
--utc-offset - string. A "ISO 8601 UTC offset" (e.g. +01:00 or -08:00) that can be used to adjust the buckets to a users time zone.
731-
732729
Output:
733730
A full JSON description of the returned statistics result.
734731
```

planet/cli/data.py

+23-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import click
1919

2020
from planet import data_filter, DataClient
21-
from planet.clients.data import SEARCH_SORT, SEARCH_SORT_DEFAULT
21+
from planet.clients.data import SEARCH_SORT, SEARCH_SORT_DEFAULT, STATS_INTERVAL
2222

2323
from . import types
2424
from .cmds import coro, translate_exceptions
@@ -48,6 +48,7 @@ def data(ctx, base_url):
4848
ctx.obj['BASE_URL'] = base_url
4949

5050

51+
# TODO: filter().
5152
def geom_to_filter(ctx, param, value: Optional[dict]) -> Optional[dict]:
5253
return data_filter.geometry_filter(value) if value else None
5354

@@ -295,6 +296,27 @@ async def search_create(ctx, name, item_types, filter, daily_email, pretty):
295296
echo_json(items, pretty)
296297

297298

299+
@data.command()
300+
@click.pass_context
301+
@translate_exceptions
302+
@coro
303+
@click.argument("item_types", type=types.CommaSeparatedString())
304+
@click.argument('interval', type=click.Choice(STATS_INTERVAL))
305+
@click.argument("filter", type=types.JSON(), default="-", required=False)
306+
async def stats(ctx, item_types, interval, filter):
307+
"""Get a bucketed histogram of items matching the filter.
308+
309+
This function returns a bucketed histogram of results based on the
310+
item_types, interval, and json filter specified (using file or stdin).
311+
312+
"""
313+
async with data_client(ctx) as cl:
314+
items = await cl.get_stats(item_types=item_types,
315+
interval=interval,
316+
search_filter=filter)
317+
echo_json(items)
318+
319+
298320
@data.command()
299321
@click.pass_context
300322
@translate_exceptions
@@ -319,4 +341,3 @@ async def search_get(ctx, search_id, pretty):
319341
# TODO: asset_activate()".
320342
# TODO: asset_wait()".
321343
# TODO: asset_download()".
322-
# TODO: stats()".

planet/clients/data.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ async def get_stats(self,
336336
interval: The size of the histogram date buckets.
337337
338338
Returns:
339-
Returns a date bucketed histogram of items matching the filter.
339+
A full JSON description of the returned statistics result histogram.
340340
341341
Raises:
342342
planet.exceptions.APIError: On API error.
@@ -348,11 +348,13 @@ async def get_stats(self,
348348
f'{interval} must be one of {STATS_INTERVAL}')
349349

350350
url = f'{self._base_url}{STATS_PATH}'
351+
351352
request_json = {
352353
'interval': interval,
353354
'filter': search_filter,
354355
'item_types': item_types
355356
}
357+
356358
request = self._request(url, method='POST', json=request_json)
357359
response = await self._do_request(request)
358360
return response.json()

tests/integration/test_data_api.py

-2
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,6 @@ async def test_get_search_success(search_id, search_result, session):
229229
get_url = f'{TEST_SEARCHES_URL}/{search_id}'
230230
mock_resp = httpx.Response(HTTPStatus.OK, json=search_result)
231231
respx.get(get_url).return_value = mock_resp
232-
233232
cl = DataClient(session, base_url=TEST_URL)
234233
search = await cl.get_search(search_id)
235234
assert search_result == search
@@ -246,7 +245,6 @@ async def test_get_search_id_doesnt_exist(search_id, session):
246245
}
247246
mock_resp = httpx.Response(404, json=resp)
248247
respx.get(get_url).return_value = mock_resp
249-
250248
cl = DataClient(session, base_url=TEST_URL)
251249

252250
with pytest.raises(exceptions.MissingResource):

tests/integration/test_data_cli.py

+75
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
TEST_URL = 'https://api.planet.com/data/v1'
3030
TEST_QUICKSEARCH_URL = f'{TEST_URL}/quick-search'
3131
TEST_SEARCHES_URL = f'{TEST_URL}/searches'
32+
TEST_STATS_URL = f'{TEST_URL}/stats'
3233

3334

3435
@pytest.fixture
@@ -581,6 +582,80 @@ def test_search_create_daily_email(invoke, search_result):
581582
assert json.loads(result.output) == search_result
582583

583584

585+
@respx.mock
586+
@pytest.mark.asyncio
587+
@pytest.mark.parametrize("filter", ['{1:1}', '{"foo"}'])
588+
def test_data_stats_invalid_filter(invoke, filter):
589+
"""Test for planet data stats. Test with multiple item_types.
590+
Test should fail as filter does not contain valid JSON."""
591+
mock_resp = httpx.Response(HTTPStatus.OK,
592+
json={'features': [{
593+
"key": "value"
594+
}]})
595+
respx.post(TEST_STATS_URL).return_value = mock_resp
596+
interval = "hour"
597+
item_type = 'PSScene'
598+
runner = CliRunner()
599+
result = invoke(["stats", item_type, interval, filter], runner=runner)
600+
assert result.exit_code == 2
601+
602+
603+
@respx.mock
604+
@pytest.mark.parametrize(
605+
"item_types", ['PSScene', 'SkySatScene', ('PSScene', 'SkySatScene')])
606+
@pytest.mark.parametrize("interval, exit_code", [(None, 1), ('hou', 2),
607+
('hour', 0)])
608+
def test_data_stats_invalid_interval(invoke, item_types, interval, exit_code):
609+
"""Test for planet data stats. Test with multiple item_types.
610+
Test should succeed with valid interval, and fail with invalid interval."""
611+
filter = {
612+
"type": "DateRangeFilter",
613+
"field_name": "acquired",
614+
"config": {
615+
"gt": "2019-12-31T00:00:00Z", "lte": "2020-01-31T00:00:00Z"
616+
}
617+
}
618+
619+
mock_resp = httpx.Response(HTTPStatus.OK,
620+
json={'features': [{
621+
"key": "value"
622+
}]})
623+
respx.post(TEST_STATS_URL).return_value = mock_resp
624+
625+
runner = CliRunner()
626+
result = invoke(["stats", item_types, interval, json.dumps(filter)],
627+
runner=runner)
628+
629+
assert result.exit_code == exit_code
630+
631+
632+
@respx.mock
633+
@pytest.mark.parametrize(
634+
"item_types", ['PSScene', 'SkySatScene', ('PSScene', 'SkySatScene')])
635+
@pytest.mark.parametrize("interval", ['hour', 'day', 'week', 'month', 'year'])
636+
def test_data_stats_success(invoke, item_types, interval):
637+
"""Test for planet data stats. Test with multiple item_types.
638+
Test should succeed as filter contains valid JSON, item_types, and intervals."""
639+
filter = {
640+
"type": "DateRangeFilter",
641+
"field_name": "acquired",
642+
"config": {
643+
"gt": "2019-12-31T00:00:00Z", "lte": "2020-01-31T00:00:00Z"
644+
}
645+
}
646+
647+
mock_resp = httpx.Response(HTTPStatus.OK,
648+
json={'features': [{
649+
"key": "value"
650+
}]})
651+
respx.post(TEST_STATS_URL).return_value = mock_resp
652+
653+
runner = CliRunner()
654+
result = invoke(["stats", item_types, interval, json.dumps(filter)],
655+
runner=runner)
656+
assert result.exit_code == 0
657+
658+
584659
# TODO: basic test for "planet data filter".
585660

586661

0 commit comments

Comments
 (0)