From 58dbb73f806bc0be2eee5830c05cc024c59eb398 Mon Sep 17 00:00:00 2001 From: Agata Date: Mon, 1 Apr 2019 16:06:00 -0700 Subject: [PATCH 01/10] Stub out analytics entrypoints --- planet/api/client.py | 34 +++++++++++++++++++++++++++++ planet/scripts/v1.py | 52 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) diff --git a/planet/api/client.py b/planet/api/client.py index 2807a66de..dcda151a6 100644 --- a/planet/api/client.py +++ b/planet/api/client.py @@ -344,3 +344,37 @@ def download_quad(self, quad, callback=None): ''' download_url = quad['_links']['download'] return self._get(download_url, models.Body, callback=callback) + + def check_analytics_connection(self): + ''' + Proof of concept that we can use the Analytics API from here. Probably should be deleted before the final version. + :return: + ''' + return self._get(self._url('analytics/health')).get_body() + + def list_analytic_subsriptions(self): + ''' + Get subscriptions that the authenticated user has access to + :return: + ''' + raise NotImplementedError() + + def list_analytic_subscription_features(self, subscription_id): + ''' + List features for an analytic subscription. + :param subscription_id: + :return: + ''' + # TODO add filter args + raise NotImplementedError() + + def get_associated_resource_for_analytic_feature(self, subscription_id, feature_id, resource_type): + ''' + Get resource assocated with some feature in an analytic subscription. + :param subscription_id: + :param feature_id: + :param resource_type: + :return: + ''' + # TODO can this be chained with item downloader? + raise NotImplementedError() diff --git a/planet/scripts/v1.py b/planet/scripts/v1.py index 299cd4b8d..0411a12d3 100644 --- a/planet/scripts/v1.py +++ b/planet/scripts/v1.py @@ -318,3 +318,55 @@ def download_quads(name, bbox, rbox, quiet, dest, limit): # invoke the function within an interrupt handler that will shut everything # down properly handle_interrupt(dl.shutdown, dl.download, items, [], dest) + + +@cli.group('analytics') +def analytics(): + '''Commands for interacting with the Analytics Feed API''' + pass + + +@analytics.command('check_connection') +@pretty +def health(pretty): + ''' + Check that we can connect to the API + :param pretty: + :return: + ''' + cl = clientv1() + response = cl.check_analytics_connection() + echo_json_response(response, pretty) + + +@analytics.group('subscriptions') +def subscriptions(): + '''Commands for interacting with the Analytics Feed API for subscriptions''' + pass + + +@subscriptions.command('list') +@pretty +@limit_option +def list_subscriptions(pretty, limit): + cl = clientv1() + # TODO Something with auth? + response = cl.list_analytic_subsriptions() + echo_json_response(response, pretty) + + +@analytics.group('features') +def features(): + '''Commands for interacting with the Analytics Feed API for features''' + pass + + +@features.command('list') +@click.argument('subscription_id') +@pretty +@limit_option +def list_subscriptions(subscription_id, pretty, limit): + cl = clientv1() + # TODO Something with auth? + response = cl.list_analytic_subscription_features(subscription_id, limit) + echo_json_response(response, pretty) From d9ed5a6b2c0550a9dbfce76615c77031cbf91ac3 Mon Sep 17 00:00:00 2001 From: Agata Date: Tue, 2 Apr 2019 11:00:35 -0700 Subject: [PATCH 02/10] Flesh out analytics CLI entrypoints and client funcs --- planet/api/client.py | 58 +++++++++++++++++++-------- planet/scripts/cli.py | 11 ++++++ planet/scripts/types.py | 13 +++++++ planet/scripts/v1.py | 86 ++++++++++++++++++++++++++++++++++------- 4 files changed, 138 insertions(+), 30 deletions(-) diff --git a/planet/api/client.py b/planet/api/client.py index dcda151a6..46d48fc4a 100644 --- a/planet/api/client.py +++ b/planet/api/client.py @@ -348,33 +348,59 @@ def download_quad(self, quad, callback=None): def check_analytics_connection(self): ''' Proof of concept that we can use the Analytics API from here. Probably should be deleted before the final version. - :return: + :returns: :py:Class:`planet.api.models.JSON` ''' - return self._get(self._url('analytics/health')).get_body() + return self._get(self._url('health')).get_body() - def list_analytic_subsriptions(self): + def list_analytic_subsriptions(self, feed_id, limit, before): ''' Get subscriptions that the authenticated user has access to - :return: + :param limit int: Limit number of subscriptions returned. API default is 250 if not provided. + :param before str: When paginating, provide the identifier for last subscription on previous page. + :raises planet.api.exceptions.APIException: On API error. + :returns: :py:Class:`planet.api.models.JSON` + ''' + params = {'limit': limit, 'feed_id': feed_id, 'before': before} + return self._get(self._url('subscriptions'), params=params).get_body() + + def get_subscription_info(self, subscription_id): + ''' + Get the information describing a specific subscription. + :param subscription_id: + :raises planet.api.exceptions.APIException: On API error. + :returns: :py:Class:`planet.api.models.JSON` ''' - raise NotImplementedError() + return self._get(self._url('subscriptions/{}'.format(subscription_id))).get_body() - def list_analytic_subscription_features(self, subscription_id): + def list_analytic_subscription_features(self, subscription_id, limit, bbox, time_range, before, after): ''' List features for an analytic subscription. :param subscription_id: - :return: + :param limit int: Limit number of features returned. API default is 250 if not provided. + :param before str: Get features published before the item with the provided ID. + :param after str: Get features published after the item with the provided ID. + :param time_range str: ISO format datetime interval. + :param bbox tuple: A lon_min, lat_min, lon_max, lat_max area to search + :raises planet.api.exceptions.APIException: On API error. + :returns: :py:Class:`planet.api.models.JSON` ''' - # TODO add filter args - raise NotImplementedError() + params = {'limit': limit, 'bbox': bbox, 'time': time_range, 'before': before, 'after': after} + return self._get(self._url('collections/{}/items'.format(subscription_id)), params=params).get_body() def get_associated_resource_for_analytic_feature(self, subscription_id, feature_id, resource_type): ''' - Get resource assocated with some feature in an analytic subscription. - :param subscription_id: - :param feature_id: - :param resource_type: - :return: + Get resource assocated with some feature in an analytic subscription. Response might be JSON or a TIF, depending + on requested resource. + :param subscription_id str: ID of subscription + :param feature_id str: ID of feature + :param resource_type str: Type of resource to request. + :raises planet.api.exceptions.APIException: On API error or resource type unavailable. + :returns: :py:Class:`planet.api.models.JSON` for resource type `source-image-info`, but can also return + :py:Class:`planet.api.models.Response` containing a :py:Class:`planet.api.models.Body` of the + resource. ''' - # TODO can this be chained with item downloader? - raise NotImplementedError() + url = self._url('collections/{}/items/{}/resources/{}'.format(subscription_id, feature_id, resource_type)) + response = self._get(url).get_body() + return response + + diff --git a/planet/scripts/cli.py b/planet/scripts/cli.py index 89d187e05..c9d795e3f 100644 --- a/planet/scripts/cli.py +++ b/planet/scripts/cli.py @@ -28,6 +28,17 @@ def clientv1(): return api.ClientV1(**client_params) +def analytics_client_v1(): + # Non-default analytics base URL doesn't have the analytics postfix + client = api.ClientV1(**client_params) + if not client.base_url.endswith('/'): + client.base_url = client.base_url + '/' + if client.base_url == 'https://api.planet.com/': + client.base_url = 'https://api.planet.com/analytics/' + + return client + + def configure_logging(verbosity): '''configure logging via verbosity level of between 0 and 2 corresponding to log levels warning, info and debug respectfully.''' diff --git a/planet/scripts/types.py b/planet/scripts/types.py index 7bccaf161..3bfc3ace8 100644 --- a/planet/scripts/types.py +++ b/planet/scripts/types.py @@ -285,3 +285,16 @@ def convert(self, val, param, ctx): except (TypeError, ValueError): raise click.BadParameter('Invalid bounding box') return (xmin, ymin, xmax, ymax) + + +class DateInterval(click.ParamType): + name = 'date interval' + + def convert(self, val, param, ctx): + dates = val.split('/') + if len(dates) > 2: + raise click.BadParameter('Too many dates') + + for date in dates: + if date != '..' and strp_lenient(date) is None: + raise click.BadParameter('Invalid date: {}'.format(date)) diff --git a/planet/scripts/v1.py b/planet/scripts/v1.py index 0411a12d3..f7a456f3b 100644 --- a/planet/scripts/v1.py +++ b/planet/scripts/v1.py @@ -17,7 +17,8 @@ import json from .cli import ( cli, - clientv1 + clientv1, + analytics_client_v1 ) from .opts import ( asset_type_option, @@ -31,7 +32,8 @@ from .types import ( AssetTypePerm, BoundingBox, - metavar_docs + metavar_docs, + DateInterval ) from .util import ( call_and_wrap, @@ -47,6 +49,7 @@ handle_interrupt ) from planet.api import downloader +from planet.api.utils import write_to_file filter_opts_epilog = '\nFilter Formats:\n\n' + \ @@ -334,7 +337,8 @@ def health(pretty): :param pretty: :return: ''' - cl = clientv1() + cl = analytics_client_v1() + click.echo('Using base URL: {}'.format(cl.base_url)) response = cl.check_analytics_connection() echo_json_response(response, pretty) @@ -346,15 +350,29 @@ def subscriptions(): @subscriptions.command('list') +@click.option('--feed-id', type=str) +@limit_option(250) # Analytics API default +@click.option('--before', type=str, help= + 'When paginating, provide the identifier for last subscription on previous page.' + ) @pretty -@limit_option -def list_subscriptions(pretty, limit): - cl = clientv1() - # TODO Something with auth? - response = cl.list_analytic_subsriptions() +def list_subscriptions(pretty, limit, feed_id, before): + '''List all subscriptions user has access to.''' + cl = analytics_client_v1() + response = cl.list_analytic_subsriptions(feed_id, limit, before) echo_json_response(response, pretty) +@subscriptions.command('describe') +@click.argument('subscription_id') +@pretty +def get_subscription_info(subscription_id, pretty): + '''Get metadata for specific subscription.''' + cl = analytics_client_v1() + sub_info = cl.get_subscription_info(subscription_id) + echo_json_response(sub_info, pretty) + + @analytics.group('features') def features(): '''Commands for interacting with the Analytics Feed API for features''' @@ -363,10 +381,50 @@ def features(): @features.command('list') @click.argument('subscription_id') +@limit_option(250) # Analytics API default +@click.option('--bbox', type=BoundingBox(), help=( + 'Region to query as a comma-delimited string:' + ' lon_min,lat_min,lon_max,lat_max' +)) +@click.option('--rbox', type=BoundingBox(), help='Alias for --bbox') +@click.option('--time-range', type=DateInterval(), help=( + 'Time interval. Can be open or closed interval, start times are inclusive and end times are exclusive:' + '2019-01-01T00:00:00.00Z/2019-02-01T00:00:00.00Z (Closed interval for January 2019),' + '2019-01-01T00:00:00.00Z/.. (Open interval for all items since the start of January 2019),' + '2019-01-01T00:00:00.00Z (instant)' +)) +@click.option('--before', type=str, help='Get features published before the item with the provided ID.') +@click.option('--after', type=str, help='Get features published after the item with the provided ID.') @pretty -@limit_option -def list_subscriptions(subscription_id, pretty, limit): - cl = clientv1() - # TODO Something with auth? - response = cl.list_analytic_subscription_features(subscription_id, limit) - echo_json_response(response, pretty) +def list_features(subscription_id, pretty, limit, rbox, bbox, time_range, before, after): + '''Request feature list for a particular subscription.''' + cl = analytics_client_v1() + bbox = bbox or rbox + features = cl.list_analytic_subscription_features(subscription_id, limit, bbox, time_range, before, after) + echo_json_response(features, pretty) + + +@features.command('get') +@click.argument('resource_type', type=click.Choice(['source-image-info', 'target-quad', 'source-quad'])) +@click.argument('subscription_id') +@click.argument('feature_id') +@click.option('--dest', default='.', help=( + 'Location to download files to'), type=click.Path( + exists=True, resolve_path=True, writable=True, file_okay=False +)) +@pretty +def get_associated_resource(subscription_id, feature_id, resource_type, pretty, dest): + '''Request resources associated with a particular subscription/feature combination.''' + cl = analytics_client_v1() + if resource_type in ['target-quad', 'source-quad']: + click.echo('Requesting {} for {}/{} selected destination directory is: {}'.format(resource_type, subscription_id, feature_id, dest)) + + resource = cl.get_associated_resource_for_analytic_feature(subscription_id, feature_id, resource_type) + + if resource_type == 'source-image-info': + echo_json_response(resource, pretty) + + if resource_type in ['target-quad', 'source-quad']: + writer = write_to_file(dest, None) + writer(resource) + click.echo('{} written, available at: {}/{}'.format(resource_type, dest, resource.name)) From c7a2234d53c6a8d9d68f2c362f26d29de1afa964 Mon Sep 17 00:00:00 2001 From: Agata Date: Wed, 3 Apr 2019 16:03:03 -0700 Subject: [PATCH 03/10] Separate out analytics configuration to allow for using analytics alongside mosaics, even in AF next --- planet/api/client.py | 58 ++++++++++++++++++++++++-- planet/scripts/cli.py | 19 ++++++--- planet/scripts/v1.py | 97 +++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 160 insertions(+), 14 deletions(-) diff --git a/planet/api/client.py b/planet/api/client.py index 46d48fc4a..8cf3acc7d 100644 --- a/planet/api/client.py +++ b/planet/api/client.py @@ -347,20 +347,28 @@ def download_quad(self, quad, callback=None): def check_analytics_connection(self): ''' - Proof of concept that we can use the Analytics API from here. Probably should be deleted before the final version. + Validate that we can use the Analytics API. Useful to test connectivity to test environments. :returns: :py:Class:`planet.api.models.JSON` ''' return self._get(self._url('health')).get_body() + def wfs_conformance(self): + ''' + Details about WFS3 conformance + :returns: :py:Class:`planet.api.models.JSON` + ''' + return self._get(self._url('conformance')).get_body() + def list_analytic_subsriptions(self, feed_id, limit, before): ''' Get subscriptions that the authenticated user has access to :param limit int: Limit number of subscriptions returned. API default is 250 if not provided. :param before str: When paginating, provide the identifier for last subscription on previous page. + :param feed_id str: Return subscriptions associated with a particular feed only. :raises planet.api.exceptions.APIException: On API error. :returns: :py:Class:`planet.api.models.JSON` ''' - params = {'limit': limit, 'feed_id': feed_id, 'before': before} + params = {'limit': limit, 'feedID': feed_id, 'before': before} return self._get(self._url('subscriptions'), params=params).get_body() def get_subscription_info(self, subscription_id): @@ -372,7 +380,47 @@ def get_subscription_info(self, subscription_id): ''' return self._get(self._url('subscriptions/{}'.format(subscription_id))).get_body() - def list_analytic_subscription_features(self, subscription_id, limit, bbox, time_range, before, after): + def list_analytic_feeds(self, limit, before, stats): + ''' + Get collections that the authenticated user has access to + :param limit int: Limit number of subscriptions returned. API default is 250 if not provided. + :param before str: When paginating, provide the identifier for last subscription on previous page. + :raises planet.api.exceptions.APIException: On API error. + :returns: :py:Class:`planet.api.models.JSON` + ''' + params = {'limit': limit, 'before': before, 'stats': stats} + return self._get(self._url('feeds'), params=params).get_body() + + def get_feed_info(self, feed_id): + ''' + Get the information describing a specific collection. + :param subscription_id: + :raises planet.api.exceptions.APIException: On API error. + :returns: :py:Class:`planet.api.models.JSON` + ''' + return self._get(self._url('feeds/{}'.format(feed_id))).get_body() + + def list_analytic_collections(self, limit, before): + ''' + Get collections that the authenticated user has access to + :param limit int: Limit number of subscriptions returned. API default is 250 if not provided. + :param before str: When paginating, provide the identifier for last subscription on previous page. + :raises planet.api.exceptions.APIException: On API error. + :returns: :py:Class:`planet.api.models.JSON` + ''' + params = {'limit': limit, 'before': before} + return self._get(self._url('collections'), params=params).get_body() + + def get_collection_info(self, subscription_id): + ''' + Get the information describing a specific collection. + :param subscription_id: + :raises planet.api.exceptions.APIException: On API error. + :returns: :py:Class:`planet.api.models.JSON` + ''' + return self._get(self._url('collections/{}'.format(subscription_id))).get_body() + + def list_collection_features(self, subscription_id, limit, bbox, time_range, before, after): ''' List features for an analytic subscription. :param subscription_id: @@ -384,7 +432,9 @@ def list_analytic_subscription_features(self, subscription_id, limit, bbox, time :raises planet.api.exceptions.APIException: On API error. :returns: :py:Class:`planet.api.models.JSON` ''' - params = {'limit': limit, 'bbox': bbox, 'time': time_range, 'before': before, 'after': after} + params = {'limit': limit, 'time': time_range, 'before': before, 'after': after} + if bbox: + params['bbox'] = ','.join([str(b) for b in bbox]) return self._get(self._url('collections/{}/items'.format(subscription_id)), params=params).get_body() def get_associated_resource_for_analytic_feature(self, subscription_id, feature_id, resource_type): diff --git a/planet/scripts/cli.py b/planet/scripts/cli.py index c9d795e3f..d57ce9eb7 100644 --- a/planet/scripts/cli.py +++ b/planet/scripts/cli.py @@ -25,17 +25,19 @@ def clientv1(): + client_params.pop('analytics_base_url', None) return api.ClientV1(**client_params) def analytics_client_v1(): # Non-default analytics base URL doesn't have the analytics postfix - client = api.ClientV1(**client_params) - if not client.base_url.endswith('/'): - client.base_url = client.base_url + '/' - if client.base_url == 'https://api.planet.com/': - client.base_url = 'https://api.planet.com/analytics/' + analytics_params = dict(**client_params) + if client_params.get('analytics_base_url') is not None: + analytics_params['base_url'] = analytics_params.pop('analytics_base_url') + else: + analytics_params['base_url'] = 'https://api.planet.com/analytics/' + client = api.ClientV1(**analytics_params) return client @@ -73,8 +75,11 @@ def configure_logging(verbosity): @click.option('-u', '--base-url', envvar='PL_API_BASE_URL', help='Change the base Planet API URL or ENV PL_API_BASE_URL' ' - Default https://api.planet.com/') +@click.option('-u', '--analytics-base-url', envvar='PL_ANALYTICS_API_BASE_URL', + help='Change the base Planet API URL or ENV PL_ANALYTICS_API_BASE_URL' + ' - Default https://api.planet.com/analytics') @click.version_option(version=__version__, message='%(version)s') -def cli(context, verbose, api_key, base_url, workers): +def cli(context, verbose, api_key, base_url, analytics_base_url, workers): '''Planet API Client''' configure_logging(verbose) @@ -84,6 +89,8 @@ def cli(context, verbose, api_key, base_url, workers): client_params['workers'] = workers if base_url: client_params['base_url'] = base_url + if analytics_base_url: + client_params['analytics_base_url'] = analytics_base_url @cli.command('help') diff --git a/planet/scripts/v1.py b/planet/scripts/v1.py index f7a456f3b..c1cc98892 100644 --- a/planet/scripts/v1.py +++ b/planet/scripts/v1.py @@ -48,7 +48,7 @@ from planet.api.utils import ( handle_interrupt ) -from planet.api import downloader +from planet.api import downloader, filters from planet.api.utils import write_to_file @@ -329,7 +329,7 @@ def analytics(): pass -@analytics.command('check_connection') +@analytics.command('check-connection') @pretty def health(pretty): ''' @@ -343,6 +343,49 @@ def health(pretty): echo_json_response(response, pretty) +@analytics.command('wfs-conformance') +@pretty +def conformance(pretty): + ''' + Details about WFS3 conformance. + :param pretty: + :return: + ''' + cl = analytics_client_v1() + response = cl.wfs_conformance() + echo_json_response(response, pretty) + + +@analytics.group('feeds') +def feeds(): + '''Commands for interacting with the Analytics Feed API for collections''' + pass + + +@feeds.command('list') +@limit_option(250) # Analytics API default +@click.option('--before', type=str, help= + 'When paginating, provide the identifier for last collection on previous page.' + ) +@click.option('--stats', is_flag=True, default=False) +@pretty +def list_feeds(pretty, limit, before, stats): + '''List all subscriptions user has access to.''' + cl = analytics_client_v1() + response = cl.list_analytic_feeds(limit, before, stats) + echo_json_response(response, pretty) + + +@feeds.command('describe') +@click.argument('feed_id') +@pretty +def get_feed_info(feed_id, pretty): + '''Get metadata for specific feed.''' + cl = analytics_client_v1() + sub_info = cl.get_feed_info(feed_id) + echo_json_response(sub_info, pretty) + + @analytics.group('subscriptions') def subscriptions(): '''Commands for interacting with the Analytics Feed API for subscriptions''' @@ -373,7 +416,53 @@ def get_subscription_info(subscription_id, pretty): echo_json_response(sub_info, pretty) -@analytics.group('features') +@analytics.group('collections') +def collections(): + '''Commands for interacting with the Analytics Feed API for collections''' + pass + + +@collections.command('list') +@limit_option(250) # Analytics API default +@click.option('--before', type=str, help= + 'When paginating, provide the identifier for last collection on previous page.' + ) +@pretty +def list_collections(pretty, limit, before): + '''List all collections user has access to.''' + cl = analytics_client_v1() + response = cl.list_analytic_collections(limit, before) + echo_json_response(response, pretty) + + +@collections.command('describe') +@click.argument('subscription_id') +@pretty +def get_collection_info(subscription_id, pretty): + '''Get metadata for specific collection.''' + cl = analytics_client_v1() + sub_info = cl.get_collection_info(subscription_id) + echo_json_response(sub_info, pretty) + + +@collections.command('resource-types') +@click.argument('subscription_id') +@pretty +def get_resource_types(subscription_id, pretty): + '''Get available resource types.''' + cl = analytics_client_v1() + # Assumes that all features in a collection have the same list of associated resource types + features = cl.list_collection_features(subscription_id, 1, None, None, None, None) + feature_list = features.get()['features'] + if not feature_list: + click.ClickException('No features found, cannot determine resource types.').show() + click.Abort() + types = {item['rel'] for item in features.get()['features'][0]['links']} + types.remove('self') + click.echo('Found resource types: {}'.format(list(types))) + + +@collections.group('features') def features(): '''Commands for interacting with the Analytics Feed API for features''' pass @@ -400,7 +489,7 @@ def list_features(subscription_id, pretty, limit, rbox, bbox, time_range, before '''Request feature list for a particular subscription.''' cl = analytics_client_v1() bbox = bbox or rbox - features = cl.list_analytic_subscription_features(subscription_id, limit, bbox, time_range, before, after) + features = cl.list_collection_features(subscription_id, limit, bbox, time_range, before, after) echo_json_response(features, pretty) From ef726a53e32c12a4c8ad8885063d764e0c3044c5 Mon Sep 17 00:00:00 2001 From: Agata Date: Fri, 5 Apr 2019 14:14:13 -0700 Subject: [PATCH 04/10] Add additional mosaics entrypoints to list mosaics for a series, as well mosaics for AF resources --- planet/api/client.py | 21 ++++++++- planet/scripts/v1.py | 106 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 121 insertions(+), 6 deletions(-) diff --git a/planet/api/client.py b/planet/api/client.py index 8cf3acc7d..bfbd0961d 100644 --- a/planet/api/client.py +++ b/planet/api/client.py @@ -272,13 +272,30 @@ def get_assets_by_id(self, item_type, id): url = 'data/v1/item-types/%s/items/%s/assets' % (item_type, id) return self._get(url).get_body() - def get_mosaics(self): + def get_mosaic_series(self, series_id): + '''Get information pertaining to a mosaics series + :returns: :py:Class:`planet.api.models.JSON` + ''' + url = self._url('basemaps/v1/series/{}'.format(series_id)) + return self._get(url, models.JSON).get_body() + + def get_mosaics_for_series(self, series_id): + '''Get list of mosaics available for a series + :returns: :py:Class:`planet.api.models.Mosaics` + ''' + url = self._url('basemaps/v1/series/{}/mosaics'.format(series_id)) + return self._get(url, models.Mosaics).get_body() + + def get_mosaics(self, name_contains=None): '''Get information for all mosaics accessible by the current user. :returns: :py:Class:`planet.api.models.Mosaics` ''' + params = {} + if name_contains: + params['name__contains'] = name_contains url = self._url('basemaps/v1/mosaics') - return self._get(url, models.Mosaics).get_body() + return self._get(url, models.Mosaics, params=params).get_body() def get_mosaic_by_name(self, name): '''Get the API representation of a mosaic by name. diff --git a/planet/scripts/v1.py b/planet/scripts/v1.py index c1cc98892..06bf0136c 100644 --- a/planet/scripts/v1.py +++ b/planet/scripts/v1.py @@ -230,12 +230,36 @@ def mosaics(): pass +@mosaics.group('series') +def series(): + '''Commands for interacting with Mosaic Series through the Mosaics API''' + pass + + +@series.command('describe') +@click.argument('series_id') +@pretty +def describe(series_id, pretty): + cl = clientv1() + echo_json_response(call_and_wrap(cl.get_mosaic_series, series_id), pretty) + + +@series.command('list-mosaics') +@click.argument('series_id') +@pretty +def list_mosaics_for_series(series_id, pretty): + client = clientv1() + series = client.get_mosaics_for_series(series_id) + echo_json_response(series, pretty) + + @mosaics.command('list') +@click.option('--prefix', default=None) @pretty -def list_mosaics(pretty): +def list_mosaics(pretty, prefix): '''List information for all available mosaics''' cl = clientv1() - echo_json_response(call_and_wrap(cl.get_mosaics), pretty) + echo_json_response(call_and_wrap(cl.get_mosaics, prefix), pretty) @mosaics.command('search') @@ -376,14 +400,38 @@ def list_feeds(pretty, limit, before, stats): echo_json_response(response, pretty) +@feeds.command('list-mosaics') +@click.argument('feed_id') +def get_mosaic_list_for_feed(feed_id): + '''List mosaics linked to feed''' + analytics_client = analytics_client_v1() + feed_info = analytics_client.get_feed_info(feed_id).get() + + for type_ in ['target', 'source']: + feed_image_conf = feed_info.get(type_) + + if feed_image_conf['type'] != 'mosaic': + click.ClickException('The {} for this feed is not a mosaic type, cannot list mosaics.'.format(type_)) + continue + + mosaic_series = feed_image_conf['config']['series_id'] + + client = clientv1() + mosaics = client.get_mosaics_for_series(mosaic_series) + + click.echo('{} mosaics:'.format(type_)) + for mosaic in mosaics.get()['mosaics']: + click.echo('\t{}'.format(mosaic['name'])) + + @feeds.command('describe') @click.argument('feed_id') @pretty def get_feed_info(feed_id, pretty): '''Get metadata for specific feed.''' cl = analytics_client_v1() - sub_info = cl.get_feed_info(feed_id) - echo_json_response(sub_info, pretty) + feed_info = cl.get_feed_info(feed_id) + echo_json_response(feed_info, pretty) @analytics.group('subscriptions') @@ -406,6 +454,31 @@ def list_subscriptions(pretty, limit, feed_id, before): echo_json_response(response, pretty) +@subscriptions.command('list-mosaics') +@click.argument('subscription_id') +def get_mosaic_list_for_feed(subscription_id): + '''List mosaics linked to feed''' + analytics_client = analytics_client_v1() + sub_info = analytics_client.get_subscription_info(subscription_id).get() + feed_info = analytics_client.get_feed_info(sub_info['feedID']).get() + + for type_ in ['target', 'source']: + feed_image_conf = feed_info.get(type_) + + if feed_image_conf['type'] != 'mosaic': + click.ClickException('The {} for this feed is not a mosaic type, cannot list mosaics.'.format(type_)) + continue + + mosaic_series = feed_image_conf['config']['series_id'] + + client = clientv1() + mosaics = client.get_mosaics_for_series(mosaic_series) + + click.echo('{} mosaics:'.format(type_)) + for mosaic in mosaics.get()['mosaics']: + click.echo('\t{}'.format(mosaic['name'])) + + @subscriptions.command('describe') @click.argument('subscription_id') @pretty @@ -435,6 +508,31 @@ def list_collections(pretty, limit, before): echo_json_response(response, pretty) +@collections.command('list-mosaics') +@click.argument('subscription_id') +def get_mosaic_list_for_feed(subscription_id): + '''List mosaics linked to feed''' + analytics_client = analytics_client_v1() + sub_info = analytics_client.get_subscription_info(subscription_id).get() + feed_info = analytics_client.get_feed_info(sub_info['feedID']).get() + + for type_ in ['target', 'source']: + feed_image_conf = feed_info.get(type_) + + if feed_image_conf['type'] != 'mosaic': + click.ClickException('The {} for this feed is not a mosaic type, cannot list mosaics.'.format(type_)) + continue + + mosaic_series = feed_image_conf['config']['series_id'] + + client = clientv1() + mosaics = client.get_mosaics_for_series(mosaic_series) + + click.echo('{} mosaics:'.format(type_)) + for mosaic in mosaics.get()['mosaics']: + click.echo('\t{}'.format(mosaic['name'])) + + @collections.command('describe') @click.argument('subscription_id') @pretty From 7abcf1f36c3826bf21a4e21135b867cbc0c9516c Mon Sep 17 00:00:00 2001 From: Agata Date: Wed, 3 Jul 2019 11:08:27 -0700 Subject: [PATCH 05/10] Misc fixup for flake8 and test dependencies --- planet/api/client.py | 87 ++++++++++++++++++--------- planet/scripts/cli.py | 13 ++-- planet/scripts/v1.py | 136 ++++++++++++++++++++++++++---------------- setup.py | 2 + 4 files changed, 152 insertions(+), 86 deletions(-) diff --git a/planet/api/client.py b/planet/api/client.py index bfbd0961d..33f780ee9 100644 --- a/planet/api/client.py +++ b/planet/api/client.py @@ -23,6 +23,7 @@ class _Base(object): '''High-level access to Planet's API.''' + def __init__(self, api_key=None, base_url='https://api.planet.com/', workers=4): ''' @@ -364,7 +365,8 @@ def download_quad(self, quad, callback=None): def check_analytics_connection(self): ''' - Validate that we can use the Analytics API. Useful to test connectivity to test environments. + Validate that we can use the Analytics API. Useful to test connectivity + to test environments. :returns: :py:Class:`planet.api.models.JSON` ''' return self._get(self._url('health')).get_body() @@ -379,9 +381,12 @@ def wfs_conformance(self): def list_analytic_subsriptions(self, feed_id, limit, before): ''' Get subscriptions that the authenticated user has access to - :param limit int: Limit number of subscriptions returned. API default is 250 if not provided. - :param before str: When paginating, provide the identifier for last subscription on previous page. - :param feed_id str: Return subscriptions associated with a particular feed only. + :param limit int: Limit number of subscriptions returned. API default + is 250 if not provided. + :param before str: When paginating, provide the identifier for last + subscription on previous page. + :param feed_id str: Return subscriptions associated with a particular + feed only. :raises planet.api.exceptions.APIException: On API error. :returns: :py:Class:`planet.api.models.JSON` ''' @@ -395,13 +400,16 @@ def get_subscription_info(self, subscription_id): :raises planet.api.exceptions.APIException: On API error. :returns: :py:Class:`planet.api.models.JSON` ''' - return self._get(self._url('subscriptions/{}'.format(subscription_id))).get_body() + url = 'subscriptions/{}'.format(subscription_id) + return self._get(self._url(url)).get_body() def list_analytic_feeds(self, limit, before, stats): ''' Get collections that the authenticated user has access to - :param limit int: Limit number of subscriptions returned. API default is 250 if not provided. - :param before str: When paginating, provide the identifier for last subscription on previous page. + :param limit int: Limit number of subscriptions returned. API default + is 250 if not provided. + :param before str: When paginating, provide the identifier for last + subscription on previous page. :raises planet.api.exceptions.APIException: On API error. :returns: :py:Class:`planet.api.models.JSON` ''' @@ -420,8 +428,10 @@ def get_feed_info(self, feed_id): def list_analytic_collections(self, limit, before): ''' Get collections that the authenticated user has access to - :param limit int: Limit number of subscriptions returned. API default is 250 if not provided. - :param before str: When paginating, provide the identifier for last subscription on previous page. + :param limit int: Limit number of subscriptions returned. API default + is 250 if not provided. + :param before str: When paginating, provide the identifier for last + subscription on previous page. :raises planet.api.exceptions.APIException: On API error. :returns: :py:Class:`planet.api.models.JSON` ''' @@ -435,39 +445,62 @@ def get_collection_info(self, subscription_id): :raises planet.api.exceptions.APIException: On API error. :returns: :py:Class:`planet.api.models.JSON` ''' - return self._get(self._url('collections/{}'.format(subscription_id))).get_body() + url = 'collections/{}'.format(subscription_id) + return self._get(self._url(url)).get_body() - def list_collection_features(self, subscription_id, limit, bbox, time_range, before, after): + def list_collection_features(self, + subscription_id, + limit, + bbox, + time_range, + before, + after): ''' List features for an analytic subscription. :param subscription_id: - :param limit int: Limit number of features returned. API default is 250 if not provided. - :param before str: Get features published before the item with the provided ID. - :param after str: Get features published after the item with the provided ID. + :param limit int: Limit number of features returned. API default is 250 + if not provided. + :param before str: Get features published before the item with the + provided ID. + :param after str: Get features published after the item with the + provided ID. :param time_range str: ISO format datetime interval. :param bbox tuple: A lon_min, lat_min, lon_max, lat_max area to search :raises planet.api.exceptions.APIException: On API error. :returns: :py:Class:`planet.api.models.JSON` ''' - params = {'limit': limit, 'time': time_range, 'before': before, 'after': after} + params = { + 'limit': limit, + 'time': time_range, + 'before': before, + 'after': after + } if bbox: params['bbox'] = ','.join([str(b) for b in bbox]) - return self._get(self._url('collections/{}/items'.format(subscription_id)), params=params).get_body() + return self._get( + self._url('collections/{}/items'.format(subscription_id)), + params=params).get_body() - def get_associated_resource_for_analytic_feature(self, subscription_id, feature_id, resource_type): + def get_associated_resource_for_analytic_feature(self, + subscription_id, + feature_id, + resource_type): ''' - Get resource assocated with some feature in an analytic subscription. Response might be JSON or a TIF, depending - on requested resource. + Get resource assocated with some feature in an analytic subscription. + Response might be JSON or a TIF, depending on requested resource. :param subscription_id str: ID of subscription :param feature_id str: ID of feature :param resource_type str: Type of resource to request. - :raises planet.api.exceptions.APIException: On API error or resource type unavailable. - :returns: :py:Class:`planet.api.models.JSON` for resource type `source-image-info`, but can also return - :py:Class:`planet.api.models.Response` containing a :py:Class:`planet.api.models.Body` of the - resource. - ''' - url = self._url('collections/{}/items/{}/resources/{}'.format(subscription_id, feature_id, resource_type)) + :raises planet.api.exceptions.APIException: On API error or resource + type unavailable. + :returns: :py:Class:`planet.api.models.JSON` for resource type + `source-image-info`, but can also return + :py:Class:`planet.api.models.Response` containing a + :py:Class:`planet.api.models.Body` of the resource. + ''' + url = self._url( + 'collections/{}/items/{}/resources/{}'.format(subscription_id, + feature_id, + resource_type)) response = self._get(url).get_body() return response - - diff --git a/planet/scripts/cli.py b/planet/scripts/cli.py index d57ce9eb7..9a634cfbe 100644 --- a/planet/scripts/cli.py +++ b/planet/scripts/cli.py @@ -31,13 +31,13 @@ def clientv1(): def analytics_client_v1(): # Non-default analytics base URL doesn't have the analytics postfix - analytics_params = dict(**client_params) + params = dict(**client_params) if client_params.get('analytics_base_url') is not None: - analytics_params['base_url'] = analytics_params.pop('analytics_base_url') + params['base_url'] = params.pop('analytics_base_url') else: - analytics_params['base_url'] = 'https://api.planet.com/analytics/' + params['base_url'] = 'https://api.planet.com/analytics/' - client = api.ClientV1(**analytics_params) + client = api.ClientV1(**params) return client @@ -76,8 +76,9 @@ def configure_logging(verbosity): help='Change the base Planet API URL or ENV PL_API_BASE_URL' ' - Default https://api.planet.com/') @click.option('-u', '--analytics-base-url', envvar='PL_ANALYTICS_API_BASE_URL', - help='Change the base Planet API URL or ENV PL_ANALYTICS_API_BASE_URL' - ' - Default https://api.planet.com/analytics') + help=('Change the base Planet API URL or ENV ' + 'PL_ANALYTICS_API_BASE_URL' + ' - Default https://api.planet.com/analytics')) @click.version_option(version=__version__, message='%(version)s') def cli(context, verbose, api_key, base_url, analytics_base_url, workers): '''Planet API Client''' diff --git a/planet/scripts/v1.py b/planet/scripts/v1.py index 06bf0136c..502000b1c 100644 --- a/planet/scripts/v1.py +++ b/planet/scripts/v1.py @@ -48,10 +48,9 @@ from planet.api.utils import ( handle_interrupt ) -from planet.api import downloader, filters +from planet.api import downloader from planet.api.utils import write_to_file - filter_opts_epilog = '\nFilter Formats:\n\n' + \ '\n'.join(['%s\n\n%s' % (k, v.replace(' ', '') .replace('``', '\'')) @@ -156,17 +155,17 @@ def _disable_item_type(ctx, param, value): @click.option('--search-id', is_eager=True, callback=_disable_item_type, type=str, help='Use the specified search') @click.option('--dry-run', is_flag=True, help=( - 'Only report the number of items that would be downloaded.' + 'Only report the number of items that would be downloaded.' )) @click.option('--activate-only', is_flag=True, help=( - 'Only activate the items. Outputs URLS for downloading.' + 'Only activate the items. Outputs URLS for downloading.' )) @click.option('--quiet', is_flag=True, help=( - 'Disable ANSI control output' + 'Disable ANSI control output' )) @click.option('--dest', default='.', help=( - 'Location to download files to'), type=click.Path( - exists=True, resolve_path=True, writable=True, file_okay=False)) + 'Location to download files to'), type=click.Path( + exists=True, resolve_path=True, writable=True, file_okay=False)) @limit_option(None) @data.command('download', epilog=filter_opts_epilog) def download(asset_type, dest, limit, sort, search_id, dry_run, activate_only, @@ -265,8 +264,8 @@ def list_mosaics(pretty, prefix): @mosaics.command('search') @click.argument('name') @click.option('--bbox', type=BoundingBox(), help=( - 'Region to query as a comma-delimited string:' - ' lon_min,lat_min,lon_max,lat_max' + 'Region to query as a comma-delimited string:' + ' lon_min,lat_min,lon_max,lat_max' )) @click.option('--rbox', type=BoundingBox(), help='Alias for --bbox') @limit_option(None) @@ -316,16 +315,16 @@ def quad_contributions(name, quad, pretty): @mosaics.command('download') @click.argument('name') @click.option('--bbox', type=BoundingBox(), help=( - 'Region to download as a comma-delimited string:' - ' lon_min,lat_min,lon_max,lat_max' + 'Region to download as a comma-delimited string:' + ' lon_min,lat_min,lon_max,lat_max' )) @click.option('--rbox', type=BoundingBox(), help='Alias for --bbox') @click.option('--quiet', is_flag=True, help=( - 'Disable ANSI control output' + 'Disable ANSI control output' )) @click.option('--dest', default='.', help=( - 'Location to download files to'), type=click.Path( - exists=True, resolve_path=True, writable=True, file_okay=False + 'Location to download files to'), type=click.Path( + exists=True, resolve_path=True, writable=True, file_okay=False )) @limit_option(None) def download_quads(name, bbox, rbox, quiet, dest, limit): @@ -356,11 +355,7 @@ def analytics(): @analytics.command('check-connection') @pretty def health(pretty): - ''' - Check that we can connect to the API - :param pretty: - :return: - ''' + '''Check that we can connect to the API''' cl = analytics_client_v1() click.echo('Using base URL: {}'.format(cl.base_url)) response = cl.check_analytics_connection() @@ -388,8 +383,9 @@ def feeds(): @feeds.command('list') @limit_option(250) # Analytics API default -@click.option('--before', type=str, help= - 'When paginating, provide the identifier for last collection on previous page.' +@click.option('--before', type=str, help=('When paginating, provide the ' + 'identifier for last collection on' + 'previous page.') ) @click.option('--stats', is_flag=True, default=False) @pretty @@ -411,7 +407,8 @@ def get_mosaic_list_for_feed(feed_id): feed_image_conf = feed_info.get(type_) if feed_image_conf['type'] != 'mosaic': - click.ClickException('The {} for this feed is not a mosaic type, cannot list mosaics.'.format(type_)) + msg_format = 'The {} for this feed is not a mosaic type.' + click.ClickException(msg_format.format(type_)) continue mosaic_series = feed_image_conf['config']['series_id'] @@ -436,15 +433,18 @@ def get_feed_info(feed_id, pretty): @analytics.group('subscriptions') def subscriptions(): - '''Commands for interacting with the Analytics Feed API for subscriptions''' + ''' + Commands for interacting with the Analytics Feed API for subscriptions + ''' pass @subscriptions.command('list') @click.option('--feed-id', type=str) @limit_option(250) # Analytics API default -@click.option('--before', type=str, help= - 'When paginating, provide the identifier for last subscription on previous page.' +@click.option('--before', type=str, help=('When paginating, provide the ' + 'identifier for last subscription on' + 'previous page.') ) @pretty def list_subscriptions(pretty, limit, feed_id, before): @@ -456,7 +456,7 @@ def list_subscriptions(pretty, limit, feed_id, before): @subscriptions.command('list-mosaics') @click.argument('subscription_id') -def get_mosaic_list_for_feed(subscription_id): +def get_mosaic_list_for_subscription(subscription_id): '''List mosaics linked to feed''' analytics_client = analytics_client_v1() sub_info = analytics_client.get_subscription_info(subscription_id).get() @@ -466,7 +466,8 @@ def get_mosaic_list_for_feed(subscription_id): feed_image_conf = feed_info.get(type_) if feed_image_conf['type'] != 'mosaic': - click.ClickException('The {} for this feed is not a mosaic type, cannot list mosaics.'.format(type_)) + msg_format = 'The {} for this feed is not a mosaic type.' + click.ClickException(msg_format.format(type_)) continue mosaic_series = feed_image_conf['config']['series_id'] @@ -497,8 +498,9 @@ def collections(): @collections.command('list') @limit_option(250) # Analytics API default -@click.option('--before', type=str, help= - 'When paginating, provide the identifier for last collection on previous page.' +@click.option('--before', type=str, help=('When paginating, provide the ' + 'identifier for last collection on ' + 'previous page.') ) @pretty def list_collections(pretty, limit, before): @@ -510,7 +512,7 @@ def list_collections(pretty, limit, before): @collections.command('list-mosaics') @click.argument('subscription_id') -def get_mosaic_list_for_feed(subscription_id): +def get_mosaic_list_for_collection(subscription_id): '''List mosaics linked to feed''' analytics_client = analytics_client_v1() sub_info = analytics_client.get_subscription_info(subscription_id).get() @@ -520,7 +522,8 @@ def get_mosaic_list_for_feed(subscription_id): feed_image_conf = feed_info.get(type_) if feed_image_conf['type'] != 'mosaic': - click.ClickException('The {} for this feed is not a mosaic type, cannot list mosaics.'.format(type_)) + msg_format = 'The {} for this feed is not a mosaic type.' + click.ClickException(msg_format.format(type_)) continue mosaic_series = feed_image_conf['config']['series_id'] @@ -549,11 +552,18 @@ def get_collection_info(subscription_id, pretty): def get_resource_types(subscription_id, pretty): '''Get available resource types.''' cl = analytics_client_v1() - # Assumes that all features in a collection have the same list of associated resource types - features = cl.list_collection_features(subscription_id, 1, None, None, None, None) + # Assumes that all features in a collection have the same list of + # associated resource types + features = cl.list_collection_features(subscription_id, + 1, + None, + None, + None, + None) feature_list = features.get()['features'] if not feature_list: - click.ClickException('No features found, cannot determine resource types.').show() + click.ClickException( + 'No features found, cannot determine resource types.').show() click.Abort() types = {item['rel'] for item in features.get()['features'][0]['links']} types.remove('self') @@ -570,43 +580,59 @@ def features(): @click.argument('subscription_id') @limit_option(250) # Analytics API default @click.option('--bbox', type=BoundingBox(), help=( - 'Region to query as a comma-delimited string:' - ' lon_min,lat_min,lon_max,lat_max' + 'Region to query as a comma-delimited string:' + ' lon_min,lat_min,lon_max,lat_max' )) @click.option('--rbox', type=BoundingBox(), help='Alias for --bbox') @click.option('--time-range', type=DateInterval(), help=( - 'Time interval. Can be open or closed interval, start times are inclusive and end times are exclusive:' - '2019-01-01T00:00:00.00Z/2019-02-01T00:00:00.00Z (Closed interval for January 2019),' - '2019-01-01T00:00:00.00Z/.. (Open interval for all items since the start of January 2019),' - '2019-01-01T00:00:00.00Z (instant)' + 'Time interval. Can be open or closed interval, start times are ' + 'inclusive and end times are exclusive: ' + '2019-01-01T00:00:00.00Z/2019-02-01T00:00:00.00Z (Closed interval for ' + 'January 2019), 2019-01-01T00:00:00.00Z/.. (Open interval for all ' + 'items since the start of January 2019), 2019-01-01T00:00:00.00Z ' + '(instant)' )) -@click.option('--before', type=str, help='Get features published before the item with the provided ID.') -@click.option('--after', type=str, help='Get features published after the item with the provided ID.') +@click.option('--before', type=str, + help='Get features published before specified item.') +@click.option('--after', type=str, + help='Get features published after specified item.') @pretty -def list_features(subscription_id, pretty, limit, rbox, bbox, time_range, before, after): +def list_features(subscription_id, pretty, limit, rbox, bbox, time_range, + before, after): '''Request feature list for a particular subscription.''' cl = analytics_client_v1() bbox = bbox or rbox - features = cl.list_collection_features(subscription_id, limit, bbox, time_range, before, after) + features = cl.list_collection_features(subscription_id, limit, bbox, + time_range, before, after) echo_json_response(features, pretty) @features.command('get') -@click.argument('resource_type', type=click.Choice(['source-image-info', 'target-quad', 'source-quad'])) +@click.argument('resource_type', type=click.Choice( + ['source-image-info', 'target-quad', 'source-quad'])) @click.argument('subscription_id') @click.argument('feature_id') @click.option('--dest', default='.', help=( - 'Location to download files to'), type=click.Path( - exists=True, resolve_path=True, writable=True, file_okay=False + 'Location to download files to'), type=click.Path( + exists=True, resolve_path=True, writable=True, file_okay=False )) @pretty -def get_associated_resource(subscription_id, feature_id, resource_type, pretty, dest): - '''Request resources associated with a particular subscription/feature combination.''' +def get_associated_resource(subscription_id, feature_id, resource_type, pretty, + dest): + '''Request resources for a particular subscription/feature combination.''' cl = analytics_client_v1() if resource_type in ['target-quad', 'source-quad']: - click.echo('Requesting {} for {}/{} selected destination directory is: {}'.format(resource_type, subscription_id, feature_id, dest)) - - resource = cl.get_associated_resource_for_analytic_feature(subscription_id, feature_id, resource_type) + msg_format = 'Requesting {} for {}/{}, destination directory is: {}' + click.echo(msg_format.format( + resource_type, + subscription_id, + feature_id, + dest + )) + + resource = cl.get_associated_resource_for_analytic_feature(subscription_id, + feature_id, + resource_type) if resource_type == 'source-image-info': echo_json_response(resource, pretty) @@ -614,4 +640,8 @@ def get_associated_resource(subscription_id, feature_id, resource_type, pretty, if resource_type in ['target-quad', 'source-quad']: writer = write_to_file(dest, None) writer(resource) - click.echo('{} written, available at: {}/{}'.format(resource_type, dest, resource.name)) + click.echo('{} written, available at: {}/{}'.format( + resource_type, + dest, + resource.name + )) diff --git a/setup.py b/setup.py index 54306c75f..7af3a8025 100644 --- a/setup.py +++ b/setup.py @@ -47,6 +47,8 @@ 'pytest-cov', 'sphinx', 'wheel', + 'mock', + 'requests-mock', ] setup(name='planet', From b1ccd0b4c5cef900509e86e7f8f9321682589e08 Mon Sep 17 00:00:00 2001 From: Agata Date: Mon, 8 Jul 2019 15:21:12 -0700 Subject: [PATCH 06/10] Adding auto-pagination for analytics commands --- planet/api/client.py | 71 ++++++++++++++++---------------------------- planet/api/models.py | 49 ++++++++++++++++++++++++++++-- planet/scripts/v1.py | 50 ++++++++++--------------------- 3 files changed, 88 insertions(+), 82 deletions(-) diff --git a/planet/api/client.py b/planet/api/client.py index 33f780ee9..0c11ec765 100644 --- a/planet/api/client.py +++ b/planet/api/client.py @@ -378,20 +378,17 @@ def wfs_conformance(self): ''' return self._get(self._url('conformance')).get_body() - def list_analytic_subsriptions(self, feed_id, limit, before): + def list_analytic_subsriptions(self, feed_id): ''' Get subscriptions that the authenticated user has access to - :param limit int: Limit number of subscriptions returned. API default - is 250 if not provided. - :param before str: When paginating, provide the identifier for last - subscription on previous page. :param feed_id str: Return subscriptions associated with a particular feed only. :raises planet.api.exceptions.APIException: On API error. - :returns: :py:Class:`planet.api.models.JSON` + :returns: :py:Class:`planet.api.models.Subscriptions` ''' - params = {'limit': limit, 'feedID': feed_id, 'before': before} - return self._get(self._url('subscriptions'), params=params).get_body() + params = {'feedID': feed_id} + url = self._url('subscriptions') + return self._get(url, models.Subscriptions, params=params).get_body() def get_subscription_info(self, subscription_id): ''' @@ -400,21 +397,18 @@ def get_subscription_info(self, subscription_id): :raises planet.api.exceptions.APIException: On API error. :returns: :py:Class:`planet.api.models.JSON` ''' - url = 'subscriptions/{}'.format(subscription_id) - return self._get(self._url(url)).get_body() + url = self._url('subscriptions/{}'.format(subscription_id)) + return self._get(url, models.JSON).get_body() - def list_analytic_feeds(self, limit, before, stats): + def list_analytic_feeds(self, stats): ''' Get collections that the authenticated user has access to - :param limit int: Limit number of subscriptions returned. API default - is 250 if not provided. - :param before str: When paginating, provide the identifier for last - subscription on previous page. :raises planet.api.exceptions.APIException: On API error. - :returns: :py:Class:`planet.api.models.JSON` + :returns: :py:Class:`planet.api.models.Feeds` ''' - params = {'limit': limit, 'before': before, 'stats': stats} - return self._get(self._url('feeds'), params=params).get_body() + params = {'stats': stats} + url = self._url('feeds') + return self._get(url, models.Feeds, params=params).get_body() def get_feed_info(self, feed_id): ''' @@ -423,20 +417,19 @@ def get_feed_info(self, feed_id): :raises planet.api.exceptions.APIException: On API error. :returns: :py:Class:`planet.api.models.JSON` ''' - return self._get(self._url('feeds/{}'.format(feed_id))).get_body() + url = self._url('feeds/{}'.format(feed_id)) + return self._get(url, models.JSON).get_body() - def list_analytic_collections(self, limit, before): + def list_analytic_collections(self): ''' Get collections that the authenticated user has access to - :param limit int: Limit number of subscriptions returned. API default - is 250 if not provided. - :param before str: When paginating, provide the identifier for last - subscription on previous page. :raises planet.api.exceptions.APIException: On API error. - :returns: :py:Class:`planet.api.models.JSON` + :returns: :py:Class:`planet.api.models.WFS3Collections` ''' - params = {'limit': limit, 'before': before} - return self._get(self._url('collections'), params=params).get_body() + params = {} + url = self._url('collections') + return self._get(url, models.WFS3Collections, + params=params).get_body() def get_collection_info(self, subscription_id): ''' @@ -446,47 +439,35 @@ def get_collection_info(self, subscription_id): :returns: :py:Class:`planet.api.models.JSON` ''' url = 'collections/{}'.format(subscription_id) - return self._get(self._url(url)).get_body() + return self._get(self._url(url), models.JSON).get_body() def list_collection_features(self, subscription_id, - limit, bbox, time_range, - before, - after): + ): ''' List features for an analytic subscription. :param subscription_id: - :param limit int: Limit number of features returned. API default is 250 - if not provided. - :param before str: Get features published before the item with the - provided ID. - :param after str: Get features published after the item with the - provided ID. :param time_range str: ISO format datetime interval. :param bbox tuple: A lon_min, lat_min, lon_max, lat_max area to search :raises planet.api.exceptions.APIException: On API error. - :returns: :py:Class:`planet.api.models.JSON` + :returns: :py:Class:`planet.api.models.WFS3Features` ''' params = { - 'limit': limit, 'time': time_range, - 'before': before, - 'after': after } if bbox: params['bbox'] = ','.join([str(b) for b in bbox]) - return self._get( - self._url('collections/{}/items'.format(subscription_id)), - params=params).get_body() + url = self._url('collections/{}/items'.format(subscription_id)) + return self._get(url, models.WFS3Features, params=params).get_body() def get_associated_resource_for_analytic_feature(self, subscription_id, feature_id, resource_type): ''' - Get resource assocated with some feature in an analytic subscription. + Get resource associated with some feature in an analytic subscription. Response might be JSON or a TIF, depending on requested resource. :param subscription_id str: ID of subscription :param feature_id str: ID of feature diff --git a/planet/api/models.py b/planet/api/models.py index 8314b3213..aac336d86 100644 --- a/planet/api/models.py +++ b/planet/api/models.py @@ -184,9 +184,9 @@ class Paged(JSON): def next(self): links = self.get()[self.LINKS_KEY] - next = links.get(self.NEXT_KEY, None) - if next: - request = Request(next, self._request.auth, body_type=type(self)) + next_ = links.get(self.NEXT_KEY, None) + if next_: + request = Request(next_, self._request.auth, body_type=type(self)) return self._dispatcher.response(request).get_body() def _pages(self): @@ -249,6 +249,7 @@ def _json_stream(self, limit): } +# GeoJSON feature class Features(Paged): def _json_stream(self, limit): @@ -275,3 +276,45 @@ class Mosaics(Paged): class MosaicQuads(Paged): ITEM_KEY = 'items' + + +class AnalyticsPaged(Paged): + LINKS_KEY = 'links' + NEXT_KEY = 'next' + ITEM_KEY = 'data' + + def next(self): + links = self.get()[self.LINKS_KEY] + next_ = None + for link in links: + if link['rel'] == self.NEXT_KEY: + next_ = link['href'] + if next_: + request = Request(next_, self._request.auth, body_type=type(self)) + return self._dispatcher.response(request).get_body() + + +# The analytics API returns two conceptual types of objects: WFS3-compliant +# objects and everything else. There may be some overlap (ex. subscriptions and +# collections). +class Feeds(AnalyticsPaged): + pass + + +class Subscriptions(AnalyticsPaged): + pass + + +class WFS3Paged(AnalyticsPaged): + pass + + +class WFS3Collections(AnalyticsPaged): + ITEM_KEY = 'collections' + + +class WFS3Features(AnalyticsPaged): + # Explicitly disambiguate between WFS3 and GeoJSON features because the + # differences in the structure of the response envelope result in paging + # slightly differently. + ITEM_KEY = 'features' diff --git a/planet/scripts/v1.py b/planet/scripts/v1.py index 502000b1c..b6cdfff70 100644 --- a/planet/scripts/v1.py +++ b/planet/scripts/v1.py @@ -382,18 +382,14 @@ def feeds(): @feeds.command('list') -@limit_option(250) # Analytics API default -@click.option('--before', type=str, help=('When paginating, provide the ' - 'identifier for last collection on' - 'previous page.') - ) +@limit_option(None) @click.option('--stats', is_flag=True, default=False) @pretty -def list_feeds(pretty, limit, before, stats): +def list_feeds(pretty, limit, stats): '''List all subscriptions user has access to.''' cl = analytics_client_v1() - response = cl.list_analytic_feeds(limit, before, stats) - echo_json_response(response, pretty) + response = cl.list_analytic_feeds(stats) + echo_json_response(response, pretty, limit) @feeds.command('list-mosaics') @@ -441,17 +437,13 @@ def subscriptions(): @subscriptions.command('list') @click.option('--feed-id', type=str) -@limit_option(250) # Analytics API default -@click.option('--before', type=str, help=('When paginating, provide the ' - 'identifier for last subscription on' - 'previous page.') - ) +@limit_option(None) @pretty -def list_subscriptions(pretty, limit, feed_id, before): +def list_subscriptions(pretty, limit, feed_id): '''List all subscriptions user has access to.''' cl = analytics_client_v1() - response = cl.list_analytic_subsriptions(feed_id, limit, before) - echo_json_response(response, pretty) + response = cl.list_analytic_subsriptions(feed_id) + echo_json_response(response, pretty, limit) @subscriptions.command('list-mosaics') @@ -497,17 +489,13 @@ def collections(): @collections.command('list') -@limit_option(250) # Analytics API default -@click.option('--before', type=str, help=('When paginating, provide the ' - 'identifier for last collection on ' - 'previous page.') - ) +@limit_option(None) @pretty -def list_collections(pretty, limit, before): +def list_collections(pretty, limit): '''List all collections user has access to.''' cl = analytics_client_v1() - response = cl.list_analytic_collections(limit, before) - echo_json_response(response, pretty) + response = cl.list_analytic_collections() + echo_json_response(response, pretty, limit) @collections.command('list-mosaics') @@ -578,7 +566,7 @@ def features(): @features.command('list') @click.argument('subscription_id') -@limit_option(250) # Analytics API default +@limit_option(None) @click.option('--bbox', type=BoundingBox(), help=( 'Region to query as a comma-delimited string:' ' lon_min,lat_min,lon_max,lat_max' @@ -592,19 +580,13 @@ def features(): 'items since the start of January 2019), 2019-01-01T00:00:00.00Z ' '(instant)' )) -@click.option('--before', type=str, - help='Get features published before specified item.') -@click.option('--after', type=str, - help='Get features published after specified item.') @pretty -def list_features(subscription_id, pretty, limit, rbox, bbox, time_range, - before, after): +def list_features(subscription_id, pretty, limit, rbox, bbox, time_range): '''Request feature list for a particular subscription.''' cl = analytics_client_v1() bbox = bbox or rbox - features = cl.list_collection_features(subscription_id, limit, bbox, - time_range, before, after) - echo_json_response(features, pretty) + features = cl.list_collection_features(subscription_id, bbox, time_range) + echo_json_response(features, pretty, limit) @features.command('get') From b2ffa89aff30f4abe9b9117fd7759c4fa9486938 Mon Sep 17 00:00:00 2001 From: Agata Date: Mon, 8 Jul 2019 17:40:35 -0700 Subject: [PATCH 07/10] unit tests for analytics commands --- planet/api/client.py | 2 +- planet/scripts/v1.py | 17 +++--- tests/fixtures/af_features.json | 63 ++++++++++++++++++++ tests/fixtures/feeds.json | 41 +++++++++++++ tests/fixtures/subscriptions.json | 59 ++++++++++++++++++ tests/test_client.py | 63 ++++++++++++++++++++ tests/test_models.py | 64 ++++++++++++++------ tests/test_v1_cli.py | 99 +++++++++++++++++++++++++++++++ 8 files changed, 381 insertions(+), 27 deletions(-) create mode 100644 tests/fixtures/af_features.json create mode 100644 tests/fixtures/feeds.json create mode 100644 tests/fixtures/subscriptions.json diff --git a/planet/api/client.py b/planet/api/client.py index 0c11ec765..53cd8ec52 100644 --- a/planet/api/client.py +++ b/planet/api/client.py @@ -378,7 +378,7 @@ def wfs_conformance(self): ''' return self._get(self._url('conformance')).get_body() - def list_analytic_subsriptions(self, feed_id): + def list_analytic_subscriptions(self, feed_id): ''' Get subscriptions that the authenticated user has access to :param feed_id str: Return subscriptions associated with a particular diff --git a/planet/scripts/v1.py b/planet/scripts/v1.py index b6cdfff70..506218bf4 100644 --- a/planet/scripts/v1.py +++ b/planet/scripts/v1.py @@ -383,7 +383,8 @@ def feeds(): @feeds.command('list') @limit_option(None) -@click.option('--stats', is_flag=True, default=False) +@click.option('--stats', is_flag=True, default=False, + help='Include feed stats') @pretty def list_feeds(pretty, limit, stats): '''List all subscriptions user has access to.''' @@ -442,7 +443,7 @@ def subscriptions(): def list_subscriptions(pretty, limit, feed_id): '''List all subscriptions user has access to.''' cl = analytics_client_v1() - response = cl.list_analytic_subsriptions(feed_id) + response = cl.list_analytic_subscriptions(feed_id) echo_json_response(response, pretty, limit) @@ -543,9 +544,6 @@ def get_resource_types(subscription_id, pretty): # Assumes that all features in a collection have the same list of # associated resource types features = cl.list_collection_features(subscription_id, - 1, - None, - None, None, None) feature_list = features.get()['features'] @@ -554,8 +552,13 @@ def get_resource_types(subscription_id, pretty): 'No features found, cannot determine resource types.').show() click.Abort() types = {item['rel'] for item in features.get()['features'][0]['links']} - types.remove('self') - click.echo('Found resource types: {}'.format(list(types))) + + # The client and API only support these three, but there may be more link + # types, ex. to things like tiles + supported_types = {'source-image-info', 'target-quad', 'source-quad'} + + found_types = types.intersection(supported_types) + click.echo('Found resource types: {}'.format(list(found_types))) @collections.group('features') diff --git a/tests/fixtures/af_features.json b/tests/fixtures/af_features.json new file mode 100644 index 000000000..38fe099c1 --- /dev/null +++ b/tests/fixtures/af_features.json @@ -0,0 +1,63 @@ +{ + "features": [ + { + "created": "2019-06-21T06:17:17.503Z", + "geometry": { + "coordinates": [ + [ + [ + -5.36217571105314, + 36.089533763333 + ], + [ + -5.36213229651505, + 36.0880835477 + ], + [ + -5.36370663936721, + 36.0880524885812 + ], + [ + -5.36375008277508, + 36.0895027025726 + ], + [ + -5.36217571105314, + 36.089533763333 + ] + ] + ], + "type": "Polygon" + }, + "id": "feature-id", + "links": [ + { + "href": "https://api.planet.com/analytics/collections/collection-id/items/feature-id", + "rel": "self" + }, + { + "href": "https://api.planet.com/analytics/collections/collection-id/items/feature-id/resources/source-image-info", + "rel": "source-image-info" + }, + { + "href": "https://tiles.planet.com/data/v1/PSScene3Band/scene-id/{z}/{x}/{y}.png?api_key=key", + "rel": "source-tiles" + } + ], + "properties": { + "observed": "2019-06-15T10:37:54.572192Z", + "score": 0.9949000477790833, + "source_asset_type": "visual", + "source_cloud_cover": 0, + "source_item": "scene-id", + "source_item_id": "scene-id", + "source_item_type": "PSScene3Band", + "xmax_px": 7961, + "xmin_px": 7913, + "ymax_px": 1869, + "ymin_px": 1815 + }, + "type": "Feature" + } + ] +} diff --git a/tests/fixtures/feeds.json b/tests/fixtures/feeds.json new file mode 100644 index 000000000..92c91ac4d --- /dev/null +++ b/tests/fixtures/feeds.json @@ -0,0 +1,41 @@ +{ + "data": [ + { + "created": "2019-06-25T00:23:31.033Z", + "description": "Hello World\n", + "id": "feed_identifier", + "interval": 60, + "links": [ + { + "href": "https://api.planet.com/analytics/feeds/feed_identifier", + "rel": "self" + }, + { + "href": "https://api.planet.com/analytics/feeds", + "rel": "feeds" + } + ], + "process": { + "actions": [ + ], + "annotations": { + "service": "sif" + }, + "args": {}, + "namespace": "{{ .Namespace }}" + }, + "source": { + "config": { + "series_id": "mosaic_series_id" + }, + "type": "mosaic" + }, + "status": "active", + "target": { + "type": "collection" + }, + "title": "Feed Title", + "updated": "2019-06-25T00:23:31.033Z" + } + ] +} diff --git a/tests/fixtures/subscriptions.json b/tests/fixtures/subscriptions.json new file mode 100644 index 000000000..a7757f1f3 --- /dev/null +++ b/tests/fixtures/subscriptions.json @@ -0,0 +1,59 @@ +{ + "data": [ + { + "created": "2019-06-25T00:27:06.365Z", + "description": "Subscription Description", + "endTime": "2019-07-15T00:00:00.000Z", + "feedID": "5125e592-64c2-48db-a354-dd0128fd4fd2", + "geometry": { + "coordinates": [ + [ + [ + -105.10620117187499, + 40.513277131087484 + ], + [ + -105.10620117187499, + 40.58997103470645 + ], + [ + -104.96475219726562, + 40.58997103470645 + ], + [ + -104.96475219726562, + 40.513277131087484 + ], + [ + -105.10620117187499, + 40.513277131087484 + ] + ] + ], + "type": "Polygon" + }, + "id": "subscription_identifier", + "links": [ + { + "href": "https://api.planet.com/analytics/subscriptions/subscription_identifier", + "rel": "self" + }, + { + "href": "https://api.planet.com/analytics/collections/subscription_identifier/items", + "rel": "results" + }, + { + "href": "https://api.planet.com/analytics/feeds/feed_identifier", + "rel": "feed" + }, + { + "href": "https://api.planet.com/analytics/subscriptions", + "rel": "subscriptions" + } + ], + "startTime": "2019-03-01T00:00:00.000Z", + "title": "Subscription Title", + "updated": "2019-07-09T00:34:22.096Z" + } + ] +} diff --git a/tests/test_client.py b/tests/test_client.py index 2bad84a3c..25a3f49c4 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -196,3 +196,66 @@ def match(req): return body['filter']['config'] == {'gt': '1970-01-01T00:00:00Z'} assert_simple_request(url, client.stats, ({'request': True},), method='post', match=match) + + +def test_list_analytic_feeds(client): + url = client.base_url + 'feeds' + assert_simple_request(url, + client.list_analytic_feeds, + (False,)) + + +def test_get_feed_info(client): + url = client.base_url + 'feeds/feed-id' + assert_simple_request(url, + client.get_feed_info, + ('feed-id',)) + + +@pytest.mark.parametrize('feed_id', [None, 'feed-id']) +def test_list_analytic_subscriptions(client, feed_id): + url = client.base_url + 'subscriptions' + assert_simple_request(url, + client.list_analytic_subscriptions, + (feed_id,)) + + +def test_get_subscription_info(client): + url = client.base_url + 'subscriptions/sub-id' + assert_simple_request(url, + client.get_subscription_info, + ('sub-id',)) + + +def test_list_collections(client): + url = client.base_url + 'collections' + assert_simple_request(url, + client.list_analytic_collections, + ()) + + +def test_get_collection_info(client): + url = client.base_url + 'collections/sub-id' + assert_simple_request(url, + client.get_collection_info, + ('sub-id',)) + + +def test_list_features(client): + url = client.base_url + 'collections/sub-id/items' + assert_simple_request(url, + client.list_collection_features, + ('sub-id', None, None)) + + +# only testing source-image-info here, the other types output a file +def test_get_associated_resource(client): + sid = 'sub-id' + fid = 'feature-id' + rid = 'source-image-info' + url = client.base_url + 'collections/{}/items/{}/resources/{}'.format( + sid, fid, rid + ) + assert_simple_request(url, + client.get_associated_resource_for_analytic_feature, + (sid, fid, rid)) diff --git a/tests/test_models.py b/tests/test_models.py index 4a5b81b30..a2347dcdc 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -14,10 +14,8 @@ import io import json -from planet.api.models import Features -from planet.api.models import Paged -from planet.api.models import Request -from planet.api.models import Response +import pytest +from planet.api.models import Features, Paged, Request, Response, WFS3Features from mock import MagicMock # try: # from StringIO import StringIO as Buffy @@ -33,24 +31,37 @@ def mock_http_response(json, iter_content=None): return m -def make_page(cnt, start, key, next): +def make_page(cnt, start, key, next, af): '''fake paged content''' - return start + cnt, { - '_links': { - '_next': next - }, - key: [{ - 'thingee': start + t - } for t in range(cnt)] - } - - -def make_pages(cnt, num, key): + if af: + envelope = { + 'links': [{ + 'href': next, + 'rel': 'next', + 'title': 'NEXT' + }], + key: [{ + 'thingee': start + t + } for t in range(cnt)] + } + else: + envelope = { + '_links': { + '_next': next + }, + key: [{ + 'thingee': start + t + } for t in range(cnt)] + } + return start + cnt, envelope + + +def make_pages(cnt, num, key, af): '''generator of 'cnt' pages containing 'num' content''' start = 0 for p in range(num): next = 'page %d' % (p + 1,) if p + 1 < num else None - start, page = make_page(cnt, start, key, next) + start, page = make_page(cnt, start, key, next, af=af) yield page @@ -58,12 +69,12 @@ class Thingees(Paged): ITEM_KEY = 'thingees' -def thingees(cnt, num, key='thingees', body=Thingees): +def thingees(cnt, num, key='thingees', body=Thingees, af=False): req = Request('url', 'auth') dispatcher = MagicMock(name='dispatcher', ) # make 5 pages with 5 items on each page - pages = make_pages(5, 5, key=key) + pages = make_pages(5, 5, key=key, af=af) # initial the paged object with the first page paged = body(req, mock_http_response(json=next(pages)), dispatcher) # the remaining 4 get used here @@ -123,3 +134,18 @@ def test_features(): features_json = json.loads(buf.getvalue()) assert features_json['type'] == 'FeatureCollection' assert len(features_json['features']) == 13 + + +@pytest.mark.parametrize('limit', [None, 13]) +def test_wf3_features(limit): + pages = 5 + page_size = 6 + num_items = pages * page_size + features = thingees(page_size, pages, + body=WFS3Features, + key='features', + af=True) + buf = io.StringIO() + features.json_encode(buf, limit) + features_json = json.loads(buf.getvalue()) + assert len(features_json['features']) == limit if limit else num_items diff --git a/tests/test_v1_cli.py b/tests/test_v1_cli.py index 838ff07f9..d3e8bb26c 100644 --- a/tests/test_v1_cli.py +++ b/tests/test_v1_cli.py @@ -9,6 +9,7 @@ from planet.api import ClientV1 from planet.api import models import pytest +from _common import read_fixture # have to clear in case key is picked up via env if api.auth.ENV_KEY in os.environ: @@ -27,6 +28,13 @@ def client(): yield client +@pytest.fixture(scope="module") +def analytics_client(): + client = MagicMock(name='analytics_client', spec=ClientV1) + with patch('planet.scripts.v1.analytics_client_v1', lambda: client): + yield client + + def assert_success(result, expected_output, exit_code=0): if result.exception: print(result.output) @@ -260,3 +268,94 @@ def test_geom_filter(runner, client): args, kw = client.create_search.call_args req = args[0] assert req['filter']['config'][0]['type'] == 'GeometryFilter' + + +# For analytics entrypoints, only testing those entrypoints that have any logic +# beyond just "make client function call" + +def test_get_mosaics_list_for_feed(runner, analytics_client, client): + feeds_blob = json.loads(read_fixture('feeds.json')) + mosaics_blob = json.loads(read_fixture('list-mosaics.json')) + + configure_response(analytics_client.get_feed_info, + json.dumps(feeds_blob['data'][0])) + configure_response(client.get_mosaics_for_series, + json.dumps(mosaics_blob)) + + expected = 'source mosaics:\n\tcolor_balance_mosaic\n' + assert_success( + runner.invoke(main, [ + 'analytics', 'feeds', 'list-mosaics', 'feed-id' + ]), + expected + ) + + +def test_get_mosaics_list_for_subscription(runner, analytics_client, client): + sub_blob = json.loads(read_fixture('subscriptions.json')) + feeds_blob = json.loads(read_fixture('feeds.json')) + mosaics_blob = json.loads(read_fixture('list-mosaics.json')) + + configure_response(analytics_client.get_subscription_info, + json.dumps(sub_blob['data'][0])) + configure_response(analytics_client.get_feed_info, + json.dumps(feeds_blob['data'][0])) + configure_response(client.get_mosaics_for_series, + json.dumps(mosaics_blob)) + + expected = 'source mosaics:\n\tcolor_balance_mosaic\n' + assert_success( + runner.invoke(main, [ + 'analytics', 'subscriptions', 'list-mosaics', 'sub-id' + ]), + expected + ) + + +def test_get_mosaics_list_for_collection(runner, analytics_client, client): + sub_blob = json.loads(read_fixture('subscriptions.json')) + feeds_blob = json.loads(read_fixture('feeds.json')) + mosaics_blob = read_fixture('list-mosaics.json') + + configure_response(analytics_client.get_subscription_info, + json.dumps(sub_blob['data'][0])) + configure_response(analytics_client.get_feed_info, + json.dumps(feeds_blob['data'][0])) + configure_response(client.get_mosaics_for_series, + mosaics_blob) + + expected = 'source mosaics:\n\tcolor_balance_mosaic\n' + assert_success( + runner.invoke(main, [ + 'analytics', 'collections', 'list-mosaics', 'collection-id' + ]), + expected + ) + + +def test_get_collection_resource_types(runner, analytics_client): + feature_blob = read_fixture('af_features.json') + configure_response(analytics_client.list_collection_features, + feature_blob) + + expected_output = "Found resource types: ['source-image-info']\n" + assert_success( + runner.invoke(main, [ + 'analytics', 'collections', 'resource-types', 'collection-id' + ]), + expected_output + ) + + +def test_get_associated_resource(runner, analytics_client): + configure_response( + analytics_client.get_associated_resource_for_analytic_feature, + '{"chowda":true}', + ) + assert_success( + runner.invoke(main, [ + 'analytics', 'collections', 'features', 'get', 'source-image-info', + 'sub-id', 'feature-id' + ]), + '{"chowda":true}\n' + ) From 5694ed872c13dbb8e7b2d777c6eb057368f12e5d Mon Sep 17 00:00:00 2001 From: Agata Date: Wed, 10 Jul 2019 16:03:59 -0700 Subject: [PATCH 08/10] bump version --- CHANGES.txt | 6 +++++- planet/api/__version__.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index df98c16b3..9ab2b45ca 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,8 @@ -1.3.4 (2019-06-27) +1.3.0 (2019-07-10) +------------------------------------- +- Added support for Analytic Feeds + +1.2.4 (2019-06-27) ------------------------------------- - Add new UDM2 and related asset types diff --git a/planet/api/__version__.py b/planet/api/__version__.py index daab838ba..19b4f1d60 100644 --- a/planet/api/__version__.py +++ b/planet/api/__version__.py @@ -1 +1 @@ -__version__ = '1.2.4' +__version__ = '1.3.0' From 7cb9a9ab6c5a8f81137a535adeb2d6f1e71a4f02 Mon Sep 17 00:00:00 2001 From: Agata Date: Wed, 10 Jul 2019 16:44:42 -0700 Subject: [PATCH 09/10] Add comment to explain why the get resource entrypoint won't work in AF next. --- planet/scripts/v1.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/planet/scripts/v1.py b/planet/scripts/v1.py index 506218bf4..58d1d0963 100644 --- a/planet/scripts/v1.py +++ b/planet/scripts/v1.py @@ -605,6 +605,9 @@ def list_features(subscription_id, pretty, limit, rbox, bbox, time_range): def get_associated_resource(subscription_id, feature_id, resource_type, pretty, dest): '''Request resources for a particular subscription/feature combination.''' + # Note that this command will not work for a custom analytics URL, as the + # underlying API call is a redirect to the Data API and Mosaics API. + # See https://github.com/kennethreitz/requests/issues/2949 for more info. cl = analytics_client_v1() if resource_type in ['target-quad', 'source-quad']: msg_format = 'Requesting {} for {}/{}, destination directory is: {}' From ba9a1c1da14cafd4fd8ff2969cf7692031854b6c Mon Sep 17 00:00:00 2001 From: Agata Date: Thu, 11 Jul 2019 15:46:53 -0700 Subject: [PATCH 10/10] Fixing incorrect shorthand CLI arg --- planet/scripts/cli.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/planet/scripts/cli.py b/planet/scripts/cli.py index 9a634cfbe..7b2991df9 100644 --- a/planet/scripts/cli.py +++ b/planet/scripts/cli.py @@ -75,7 +75,8 @@ def configure_logging(verbosity): @click.option('-u', '--base-url', envvar='PL_API_BASE_URL', help='Change the base Planet API URL or ENV PL_API_BASE_URL' ' - Default https://api.planet.com/') -@click.option('-u', '--analytics-base-url', envvar='PL_ANALYTICS_API_BASE_URL', +@click.option('-au', '--analytics-base-url', + envvar='PL_ANALYTICS_API_BASE_URL', help=('Change the base Planet API URL or ENV ' 'PL_ANALYTICS_API_BASE_URL' ' - Default https://api.planet.com/analytics'))