diff --git a/docs/source/cli/examples.rst b/docs/source/cli/examples.rst
index b5759b1be..903e37ad7 100644
--- a/docs/source/cli/examples.rst
+++ b/docs/source/cli/examples.rst
@@ -61,7 +61,7 @@ meant to be easily interoperable with other tools, e.g. `jq
`_. For example, we could output just the name
and date range of each mosaic with::
- planet mosaics list | jq -r '.mosaics[] | [.name, .first_acquired, .last_acquired] | @tsv'
+ planet mosaics list | jq -r '.mosaics[] | [.name, .first_acquired, .last_acquired] | @tsv'
Get basic information for a specific mosaic::
@@ -73,7 +73,7 @@ list all quads. Keep in mind that there may be millions for a global mosaic.)::
planet mosaics search global_monthly_2018_09_mosaic --limit=10
Find all quads inside a particular area of interest::
-
+
planet mosaics search global_monthly_2018_09_mosaic --bbox=-95.5,29.6,-95.3,29.8
Note that the format of ``--bbox`` is "xmin,ymin,xmax,ymax", so longitude comes
@@ -193,7 +193,7 @@ Orders Examples
-----------------
List all recent orders for the authenticated user::
-
+
planet orders list
Get the status of a single order by Order ID::
@@ -203,17 +203,17 @@ Get the status of a single order by Order ID::
Note that you may want to parse the JSON that's output into a more human
readable format. The cli does not directly provide options for this, but is
meant to be easily interoperable with other tools, e.g. `jq
-`_.
+`_.
To cancel a running order by given order ID::
planet orders cancel
-To download an order to your local machine::
+To download an order to your local machine::
- planet orders download
+ planet orders download
-Optionally, a `--dest ` flag may be specified too.
+Optionally, a `--dest ` flag may be specified too.
Creating an Order
..................
@@ -227,19 +227,148 @@ The minimal command to create a simple order looks something like::
If no toolchain or delivery details are specified, a basic order with download
delivery will be placed for the requested bundle including the item id(s) specified.
-Additionally, optional toolchain & delivery details can be provided on the
-command line, e.g.:::
+In the place of `--id`, you can insert a Data search string. This will populate
+the list of IDs from a search. For example::
+
+ planet orders create --name "my order" \
+ --ids_from_search $'--item-type PSScene3Band --date acquired gt 2017-02-14 --date acquired lt 2017-03-14 --limit 6 --geom \'{
+ "type": "FeatureCollection",
+ "features": [
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ [
+ [
+ -116.40701293945311,
+ 43.061363052307875
+ ],
+ [
+ -116.4451217651367,
+ 43.05032512283074
+ ],
+ [
+ -116.4320755004883,
+ 43.017450433440814
+ ],
+ [
+ -116.37508392333984,
+ 43.01092359150748
+ ],
+ [
+ -116.3393783569336,
+ 43.03677585761058
+ ],
+ [
+ -116.35894775390624,
+ 43.06186472916744
+ ],
+ [
+ -116.40701293945311,
+ 43.061363052307875
+ ]
+ ]
+ ]
+ }
+ }
+ ]
+ }\'' \
+ --bundle visual \
+ --item-type psscene3band \
+ --zip bundle --email \
+ --clip '{
+ "type": "Polygon",
+ "coordinates": [
+ [
+ [
+ -116.40701293945311,
+ 43.061363052307875
+ ],
+ [
+ -116.4451217651367,
+ 43.05032512283074
+ ],
+ [
+ -116.4320755004883,
+ 43.017450433440814
+ ],
+ [
+ -116.37508392333984,
+ 43.01092359150748
+ ],
+ [
+ -116.3393783569336,
+ 43.03677585761058
+ ],
+ [
+ -116.35894775390624,
+ 43.06186472916744
+ ],
+ [
+ -116.40701293945311,
+ 43.061363052307875
+ ]
+ ]
+ ]
+ }'
+
+Note that `--ids_from_search` is passed as a string value.
+
+Additionally, optional toolchain & delivery details can be provided on the command line, e.g.::
planet orders create --name "my order" \
--id 20151119_025740_0c74,20151119_025741_0c74 \
--bundle visual --item-type psscene3band --zip order --email
This places the same order as above, and will also provide a .zip archive
-download link for the full order, as well as email notification.
+download link for the full order, as well as email notification. If you change
+`--zip order` to `--zip bundle`, the individual bundles will be zipped rather
+than the full order.
+
+You can also clip the items in an order by providing a GeoJSON AOI Geometry
+with the `--clip` parameter::
+
+ planet orders create --name "my order" ... \
+ --clip '{
+ "type": "Polygon",
+ "coordinates": [
+ [
+ [
+ -163.828125,
+ -44.59046718130883
+ ],
+ [
+ 181.7578125,
+ -44.59046718130883
+ ],
+ [
+ 181.7578125,
+ 78.42019327591201
+ ],
+ [
+ -163.828125,
+ 78.42019327591201
+ ],
+ [
+ -163.828125,
+ -44.59046718130883
+ ]
+ ]
+ ]
+ }'
+
+Alternatively, you can specify a file that contains your GeoJSON AOI using the
+`@` notation, e.g. `--clip @path/to/aoi.json`.
+
+It should be noted that if the clip AOI you specify does not intersect with the
+items in `--id` or `--ids_from_search` you may end up with a zero result order.
+If some of the items intersect, you will receive those items.
The Orders API allows you to specify a toolchain of operations to be performed
on your order prior to download. To read more about tools & toolchains, visit
-`the docs `_ .
+`the docs `_ .
To add tool operations to your order, use the `--tools` option to specify a
json-formatted file containing an array (list) of the desired tools an their
@@ -271,7 +400,7 @@ order, you would create a `.json` file similar to the following::
"name_template": "C1232_30_30_{tilex:04d}_{tiley:04d}"
}
}
- ]
+ ]
Similarly, you can also specify cloud delivery options on an order create
@@ -279,8 +408,8 @@ command with the `--cloudconfig ` option. In this case, the
json file should contain the required credentials for your desired cloud
storage destination, for example::
- {
- "amazon_s3":{
+ {
+ "amazon_s3":{
"bucket":"foo-bucket",
"aws_region":"us-east-2",
"aws_access_key_id":"",
diff --git a/planet/scripts/types.py b/planet/scripts/types.py
index a06d414cc..072341003 100644
--- a/planet/scripts/types.py
+++ b/planet/scripts/types.py
@@ -309,3 +309,28 @@ def convert(self, val, param, ctx):
for date in dates:
if date != '..' and strp_lenient(date) is None:
raise click.BadParameter('Invalid date: {}'.format(date))
+
+
+class ClipAOI(click.ParamType):
+ name = 'clip'
+
+ def convert(self, val, param, ctx):
+ val = read(val)
+ if not val:
+ return []
+ try:
+ json.loads(val)
+ except ValueError:
+ raise click.BadParameter('invalid GeoJSON')
+ return val
+
+
+class RequiredUnless(click.Option):
+ def __init__(self, *args, **kwargs):
+ self.this_opt_exists = kwargs.pop('this_opt_exists')
+ super(RequiredUnless, self).__init__(*args, **kwargs)
+
+ def handle_parse_result(self, ctx, opts, args):
+ if self.name not in opts and self.this_opt_exists not in opts:
+ raise click.UsageError('{} is required.'.format(self.name))
+ return super(RequiredUnless, self).handle_parse_result(ctx, opts, args)
diff --git a/planet/scripts/util.py b/planet/scripts/util.py
index da802808c..5fb109459 100644
--- a/planet/scripts/util.py
+++ b/planet/scripts/util.py
@@ -100,6 +100,7 @@ def create_order_request(**kwargs):
email = kwargs.get('email')
archive = kwargs.get('zip')
config = kwargs.get('cloudconfig')
+ clip = kwargs.get('clip')
tools = kwargs.get('tools')
request = {'name': kwargs.get('name'),
@@ -108,20 +109,21 @@ def create_order_request(**kwargs):
'product_bundle': bundle}
],
'tools': [
- ],
- 'delivery': {
- },
- 'notifications': {
+ ],
+ 'delivery': {
+ },
+ 'notifications': {
'email': email
- },
- }
+ },
+ }
if archive is not None:
request["delivery"]["archive_filename"] = "{{name}}_{{order_id}}.zip"
request["delivery"]["archive_type"] = "zip"
- # TODO verify this is correct req format for order vs bundle zip
- if archive == "bundle":
+ # If single_archive is not set, each bundle will be zipped, as opposed
+ # to the entire order.
+ if archive == "order":
request["delivery"]["single_archive"] = True
if config:
@@ -129,9 +131,13 @@ def create_order_request(**kwargs):
conf = json.load(f)
request["delivery"].update(conf)
- # TODO determine reasonable interfaces for SOME tools via CLI;
- # e.g., clip via provided geojson AOI
- # for now we can punt by pointing users to doc examples for copy-pasting
+ # NOTE clip is the only tool that currently can be specified via CLI param.
+ # A full tool chain can be specified via JSON file, so that will overwrite
+ # clip if both are present. TODO add other common tools as params.
+ if clip and not tools:
+ toolchain = [{'clip': {'aoi': json.loads(clip)}}]
+ request['tools'].extend(toolchain)
+
if tools:
with open(tools, 'r') as f:
toolchain = json.load(f)
@@ -346,3 +352,11 @@ def downloader_output(dl, disable_ansi=False):
if termui.WIN and not disable_ansi:
logging.getLogger('').setLevel(logging.INFO)
return Output(thread, dl)
+
+
+def ids_from_search_response(resp):
+ ret = []
+ r = json.loads(resp)
+ for feature in r['features']:
+ ret.append(feature['id'])
+ return ','.join(ret)
diff --git a/planet/scripts/v1.py b/planet/scripts/v1.py
index 1670a8d08..28f073eaf 100644
--- a/planet/scripts/v1.py
+++ b/planet/scripts/v1.py
@@ -13,6 +13,8 @@
# limitations under the License.
import click
+from click.testing import CliRunner
+
from itertools import chain
import json
from .cli import (
@@ -34,7 +36,9 @@
BoundingBox,
metavar_docs,
DateInterval,
- ItemType
+ ItemType,
+ RequiredUnless,
+ ClipAOI,
)
from .util import (
call_and_wrap,
@@ -45,7 +49,8 @@
echo_json_response,
read,
search_req_from_opts,
- create_order_request
+ create_order_request,
+ ids_from_search_response,
)
from planet.api.utils import (
handle_interrupt
@@ -162,16 +167,16 @@ 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(
+ '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)
@@ -271,8 +276,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)
@@ -322,15 +327,15 @@ 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(
+ 'Location to download files to'), type=click.Path(
exists=True, resolve_path=True, writable=True, file_okay=False
))
@limit_option(None)
@@ -574,17 +579,17 @@ def features():
@features.command('list')
@click.argument('subscription_id')
@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 results published before the item with the provided ID.'
@@ -607,17 +612,17 @@ def list_features(subscription_id, pretty, limit, rbox, bbox, time_range,
@features.command('list-all')
@click.argument('subscription_id')
@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 results published before the item with the provided ID.'
@@ -642,7 +647,7 @@ def list_features_all(subscription_id, pretty, rbox, bbox, time_range, before,
@click.argument('subscription_id')
@click.argument('feature_id')
@click.option('--dest', default='.', help=(
- 'Location to download files to'), type=click.Path(
+ 'Location to download files to'), type=click.Path(
exists=True, resolve_path=True, writable=True, file_okay=False
))
@pretty
@@ -711,8 +716,14 @@ def cancel_order(order_id, pretty):
@click.option('--name', required=True)
-@click.option('--id', required=True,
- help='One or more comma-separated item IDs')
+@click.option('--id', help='One or more comma-separated item IDs',
+ cls=RequiredUnless, this_opt_exists='ids_from_search')
+# Note: This is passed as a string, because --item-type is a required field for
+# both 'data search' and 'orders create'.
+@click.option('--ids_from_search',
+ help='Embedded data search')
+@click.option('--clip', type=ClipAOI(),
+ help='Provide a GeoJSON AOI Geometry for clipping')
@click.option('--email', default=False, is_flag=True,
help='Send email notification when Order is complete')
@click.option('--zip', type=click.Choice(['order', 'bundle']),
@@ -735,6 +746,16 @@ def cancel_order(order_id, pretty):
@pretty
def create_order(pretty, **kwargs):
'''Create an order'''
+ ids_from_search = kwargs.get('ids_from_search')
+ if ids_from_search is not None:
+ runner = CliRunner()
+ resp = runner.invoke(quick_search, ids_from_search).output
+ try:
+ id_list = ids_from_search_response(resp)
+ except ValueError:
+ raise click.ClickException('ids_from_search, {}'.format(resp))
+ kwargs['id'] = id_list
+ del kwargs['ids_from_search']
cl = clientv1()
request = create_order_request(**kwargs)
echo_json_response(call_and_wrap(cl.create_order, request), pretty)
@@ -743,12 +764,12 @@ def create_order(pretty, **kwargs):
@orders.command('download')
@click.argument('order_id', type=click.UUID)
@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
- ))
+))
@pretty
def download_order(order_id, dest, quiet, pretty):
'''Download an order by given order ID'''