Skip to content

Commit d6ded9c

Browse files
authored
Merge pull request #186 from planetlabs/orders-cli-support
orders API functionality
2 parents fc151c2 + af14594 commit d6ded9c

File tree

9 files changed

+451
-10
lines changed

9 files changed

+451
-10
lines changed

docs/source/cli/examples.rst

+103
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,109 @@ Requesting a resource for a feature in a collection::
189189
planet analytics collections features get target-quad <collection_id or subscription_id> <feature_id>
190190
planet analytics collections features get source-image-info <collection_id or subscription_id> <feature_id>
191191

192+
Orders Examples
193+
-----------------
194+
195+
List all recent orders for the authenticated user::
196+
197+
planet orders list
198+
199+
Get the status of a single order by Order ID::
200+
201+
planet orders get <order ID>
202+
203+
Note that you may want to parse the JSON that's output into a more human
204+
readable format. The cli does not directly provide options for this, but is
205+
meant to be easily interoperable with other tools, e.g. `jq
206+
<https://stedolan.github.io/jq/>`_.
207+
208+
To cancel a running order by given order ID::
209+
210+
planet orders cancel <order ID>
211+
212+
To download an order to your local machine::
213+
214+
planet orders download <order ID>
215+
216+
Optionally, a `--dest <path to destination>` flag may be specified too.
217+
218+
Creating an Order
219+
..................
220+
221+
The minimal command to create a simple order looks something like::
222+
223+
planet orders create --name "my order" \
224+
--id 20151119_025740_0c74,20151119_025741_0c74 \
225+
--bundle visual --item-type psscene3band
226+
227+
If no toolchain or delivery details are specified, a basic order with download
228+
delivery will be placed for the requested bundle including the item id(s) specified.
229+
230+
Additionally, optional toolchain & delivery details can be provided on the
231+
command line, e.g.:::
232+
233+
planet orders create --name "my order" \
234+
--id 20151119_025740_0c74,20151119_025741_0c74 \
235+
--bundle visual --item-type psscene3band --zip order --email
236+
237+
This places the same order as above, and will also provide a .zip archive
238+
download link for the full order, as well as email notification.
239+
240+
The Orders API allows you to specify a toolchain of operations to be performed
241+
on your order prior to download. To read more about tools & toolchains, visit
242+
`the docs <https://developers.planet.com/docs/orders/tools-toolchains/>`_ .
243+
244+
To add tool operations to your order, use the `--tools` option to specify a
245+
json-formatted file containing an array (list) of the desired tools an their
246+
settings.
247+
248+
.. note:: The json-formatted file must be formatted as an array (enclosed in square brackets), even if only specifying a single tool
249+
250+
For example, to apply the 3 tools `TOAR -> Reproject -> Tile` in sequence to an
251+
order, you would create a `.json` file similar to the following::
252+
253+
[
254+
{
255+
"toar": {
256+
"scale_factor": 10000
257+
}
258+
},
259+
{
260+
"reproject": {
261+
"projection": "WGS84",
262+
"kernel": "cubic"
263+
}
264+
},
265+
{
266+
"tile": {
267+
"tile_size": 1232,
268+
"origin_x": -180,
269+
"origin_y": -90,
270+
"pixel_size": 0.000027056277056,
271+
"name_template": "C1232_30_30_{tilex:04d}_{tiley:04d}"
272+
}
273+
}
274+
]
275+
276+
277+
Similarly, you can also specify cloud delivery options on an order create
278+
command with the `--cloudconfig <path to json file>` option. In this case, the
279+
json file should contain the required credentials for your desired cloud
280+
storage destination, for example::
281+
282+
{
283+
"amazon_s3":{
284+
"bucket":"foo-bucket",
285+
"aws_region":"us-east-2",
286+
"aws_access_key_id":"",
287+
"aws_secret_access_key":"",
288+
"path_prefix":""
289+
}
290+
291+
You can find complete documentation of Orders API cloud storage delivery and
292+
required credentials `in the docs here
293+
<https://developers.planet.com/docs/orders/ordering-delivery/#delivery-to-cloud-storage_1>`_.
294+
192295
Integration With Other Tools
193296
----------------------------
194297

planet/api/client.py

+77
Original file line numberDiff line numberDiff line change
@@ -495,3 +495,80 @@ def get_associated_resource_for_analytic_feature(self,
495495
resource_type))
496496
response = self._get(url).get_body()
497497
return response
498+
499+
def get_orders(self):
500+
'''Get information for all pending and completed order requests for
501+
the current user.
502+
503+
:returns: :py:Class:`planet.api.models.Orders`
504+
'''
505+
506+
# TODO filter 'completed orders', 'in progress orders', 'all orders'?
507+
url = self._url('compute/ops/orders/v2')
508+
orders = (self._get(url, models.Orders).get_body())
509+
return orders
510+
511+
def get_individual_order(self, order_id):
512+
'''Get order request details by Order ID.
513+
514+
:param order_id str: The ID of the Order
515+
:returns: :py:Class:`planet.api.models.Order`
516+
:raises planet.api.exceptions.APIException: On API error.
517+
'''
518+
url = self._url('compute/ops/orders/v2/{}'.format(order_id))
519+
return self._get(url, models.Order).get_body()
520+
521+
def cancel_order(self, order_id):
522+
'''Cancel a running order by Order ID.
523+
524+
:param order_id str: The ID of the Order to cancel
525+
:returns: :py:Class:`planet.api.models.Order`
526+
:raises planet.api.exceptions.APIException: On API error.
527+
'''
528+
url = self._url('compute/ops/orders/v2/{}'.format(order_id))
529+
return self.dispatcher.response(models.Request(url, self.auth,
530+
body_type=models.Order,
531+
method='PUT')
532+
).get_body()
533+
534+
def create_order(self, request):
535+
'''Create an order.
536+
537+
:param asset:
538+
:returns: :py:Class:`planet.api.models.Response` containing a
539+
:py:Class:`planet.api.models.Body` of the asset.
540+
:raises planet.api.exceptions.APIException: On API error.
541+
'''
542+
url = self._url('compute/ops/orders/v2')
543+
body = json.dumps(request)
544+
return self.dispatcher.response(models.Request(url, self.auth,
545+
body_type=models.Order,
546+
data=body,
547+
method='POST')
548+
).get_body()
549+
550+
def download_order(self, order_id, callback=None):
551+
'''Download all items in an order.
552+
553+
:param order_id: ID of order to download
554+
:returns: :py:Class:`planet.api.models.Response` containing a
555+
:py:Class:`planet.api.models.Body` of the asset.
556+
:raises planet.api.exceptions.APIException: On API error.
557+
'''
558+
559+
url = self._url('compute/ops/orders/v2/{}'.format(order_id))
560+
561+
order = self._get(url, models.Order).get_body()
562+
locations = order.get_locations()
563+
return self._get(locations, models.JSON, callback=callback)
564+
565+
def download_location(self, location, callback=None):
566+
'''Download an item in an order.
567+
568+
:param location: location URL of item
569+
:returns: :py:Class:`planet.api.models.Response` containing a
570+
:py:Class:`planet.api.models.Body` of the asset.
571+
:raises planet.api.exceptions.APIException: On API error.
572+
'''
573+
574+
return self._get(location, models.JSON, callback=callback)

planet/api/downloader.py

+48-1
Original file line numberDiff line numberDiff line change
@@ -504,13 +504,60 @@ def stats(self):
504504
return stats
505505

506506

507-
def create(client, mosaic=False, **kw):
507+
class _OrderDownloadStage(_DStage):
508+
def _task(self, t):
509+
return t
510+
511+
def _do(self, task):
512+
func = self._write_tracker(task, None)
513+
writer = write_to_file(self._dest, func, overwrite=False)
514+
self._downloads += 1
515+
self._results.put((task,
516+
self._client.download_location(task, writer)))
517+
518+
519+
class _OrderDownloader(_Downloader):
520+
def activate(self, items, asset_types):
521+
pass
522+
523+
def _init(self, items, asset_types, dest):
524+
client = self._client
525+
dstage = _OrderDownloadStage(items, client, asset_types, dest)
526+
self._dest = dest
527+
self._stages.append(dstage)
528+
self._apply_opts(vars())
529+
self._completed = 0
530+
531+
def stats(self):
532+
stats = {
533+
'paging': False,
534+
'activating': 0,
535+
'pending': 0,
536+
'complete': 0,
537+
'downloading': 0,
538+
'downloaded': '0.0MB',
539+
}
540+
if not self._stages:
541+
return stats
542+
543+
dstage = self._stages[0]
544+
mb_written = '%.2fMB' % (dstage._written / float(1024**2))
545+
stats['downloading'] = dstage._downloads - self._completed
546+
stats['downloaded'] = mb_written
547+
stats['pending'] = dstage.work()
548+
stats['complete'] = self._completed
549+
return stats
550+
551+
552+
def create(client, mosaic=False, order=False, **kw):
508553
'''Create a Downloader with the provided client.
509554
510555
:param mosaic bool: If True, the Downloader will fetch mosaic quads.
511556
:returns: :py:Class:`planet.api.downloader.Downloader`
512557
'''
513558
if mosaic:
514559
return _MosaicDownloader(client, **kw)
560+
elif order:
561+
return _OrderDownloader(client, **kw)
515562
else:
516563
return _Downloader(client, **kw)

planet/api/models.py

+34
Original file line numberDiff line numberDiff line change
@@ -318,3 +318,37 @@ class WFS3Features(AnalyticsPaged):
318318
# differences in the structure of the response envelope result in paging
319319
# slightly differently.
320320
ITEM_KEY = 'features'
321+
322+
323+
class Orders(Paged):
324+
ITEM_KEY = 'orders'
325+
326+
327+
class Order(JSON):
328+
LINKS_KEY = '_links'
329+
RESULTS_KEY = 'results'
330+
LOCATION_KEY = 'location'
331+
332+
def get_results(self):
333+
links = self.get()[self.LINKS_KEY]
334+
results = links.get(self.RESULTS_KEY, None)
335+
return results
336+
337+
def get_locations(self):
338+
results = self.get_results()
339+
locations = [r[self.LOCATION_KEY] for r in results]
340+
return locations
341+
342+
def items_iter(self, limit):
343+
'''Get an iterator of the 'items' in each order.
344+
The iterator yields the individual items in the order.
345+
346+
:param int limit: The number of 'items' to limit to.
347+
:return: iter of items in page
348+
'''
349+
350+
locations = iter(self.get_locations())
351+
352+
# if limit is not None:
353+
# locations = itertools.islice(locations, limit)
354+
return locations

planet/scripts/item_asset_types.py

+21
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
_item_types = None
99
_asset_types = None
10+
_bundles = None
1011

1112
# Default values here are used as a fallback
1213
# In case the API fails to respond or takes too long.
@@ -63,6 +64,18 @@
6364
'udm2', 'visual', 'visual_xml'
6465
]
6566

67+
DEFAULT_BUNDLES = [u'all', u'all_udm2', u'analytic', u'analytic_sr',
68+
u'analytic_sr_udm2', u'analytic_udm2', u'basic_analytic',
69+
u'basic_analytic_nitf', u'basic_analytic_nitf_udm2',
70+
u'basic_analytic_udm2', u'basic_panchromatic',
71+
u'basic_panchromatic_dn', u'basic_uncalibrated_dn',
72+
u'basic_uncalibrated_dn_nitf',
73+
u'basic_uncalibrated_dn_nitf_udm2',
74+
u'basic_uncalibrated_dn_udm2', u'panchromatic',
75+
u'panchromatic_dn', u'panchromatic_dn_udm2',
76+
u'pansharpened', u'pansharpened_udm2', u'uncalibrated_dn',
77+
u'uncalibrated_dn_udm2', u'visual']
78+
6679

6780
def _get_json_or_raise(url, timeout=11):
6881
api_key = find_api_key()
@@ -89,3 +102,11 @@ def get_asset_types():
89102
data = _get_json_or_raise(ASSET_TYPE_URL)
90103
_asset_types = [a['id'] for a in data['asset_types']]
91104
return _asset_types
105+
106+
107+
def get_bundles():
108+
global _bundles
109+
if _bundles is None:
110+
_bundles = DEFAULT_BUNDLES
111+
# TODO if/when bundles defs are served by API we can grab them here
112+
return _bundles

planet/scripts/opts.py

+9-2
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
NumberIn,
2525
Range,
2626
SortSpec,
27-
StringIn
27+
StringIn,
28+
Bundle
2829
)
2930

3031

@@ -77,11 +78,17 @@ def limit_option(default):
7778
)
7879

7980
item_type_option = click.option(
80-
'--item-type', multiple=True, required=True, type=ItemType(), help=(
81+
'--item-type', multiple=False, required=True, type=ItemType(), help=(
8182
'Specify item type(s)'
8283
)
8384
)
8485

86+
bundle_option = click.option(
87+
'--bundle', multiple=False, required=True, type=Bundle(), help=(
88+
'Specify bundle(s)'
89+
)
90+
)
91+
8592
asset_type_option = click.option(
8693
'--asset-type', multiple=True, required=True, type=AssetType(), help=(
8794
'Specify asset type(s)'

planet/scripts/types.py

+12-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from planet.api.utils import geometry_from_json
2626
from planet.api.utils import strp_lenient
2727
from planet.scripts.item_asset_types import get_item_types, get_asset_types, \
28-
DEFAULT_ITEM_TYPES, DEFAULT_ASSET_TYPES
28+
get_bundles, DEFAULT_ITEM_TYPES, DEFAULT_ASSET_TYPES, DEFAULT_BUNDLES
2929

3030

3131
metavar_docs = {
@@ -118,6 +118,17 @@ def get_remote_choices(self):
118118
return self.choices
119119

120120

121+
class Bundle(_LenientChoice):
122+
name = 'bundle'
123+
allow_all = True
124+
125+
def __init__(self):
126+
_LenientChoice.__init__(self, DEFAULT_BUNDLES)
127+
128+
def get_remote_choices(self):
129+
return get_bundles()
130+
131+
121132
class ItemType(_LenientChoice):
122133
name = 'item-type'
123134
allow_all = True

0 commit comments

Comments
 (0)