<NME> orders.py
<BEF> # Copyright 2022 Planet Labs PBC.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Orders API CLI"""
from contextlib import asynccontextmanager
import json
import logging
from os import path
import re

import click

import planet
from planet import OrdersClient, Session  # allow mocking
from .session import CliSession

LOGGER = logging.getLogger(__name__)


pretty = click.option('--pretty', is_flag=True, help='Format JSON output.')


def _split(value):
    '''return input split on any whitespace or comma'''
    return re.split(r'\s+|,', value)


def read(value, split=False):
    '''Get the value of an option interpreting as a file implicitly or
    explicitly and falling back to the value if not explicitly specified.
    If the value is '@name', then a file must exist with name and the returned
    value will be the contents of that file. If the value is '@-' or '-', then
    stdin will be read and returned as the value. Finally, if a file exists
    with the provided value, that file will be read. Otherwise, the value
    will be returned.
    '''
    v = str(value)
    retval = value
    if v == '-':
        fname = '-' if v == '-' else v[1:]
        try:
            with click.open_file(fname) as fp:
                if not fp.isatty():
                    retval = fp.read()
                else:
                    retval = None
        # @todo better to leave as IOError and let caller handle it
        # to better report in context of call (e.g. the option/type)
        except IOError as ioe:
            # if explicit and problems, raise
            if v[0] == '@':
                raise click.ClickException(str(ioe))
    elif path.exists(v) and path.isfile(v):
        retval = json.loads(open(retval).read())
    if retval and split and type(retval) != tuple:
        retval = _split(retval.strip())
    if v == '-':
        retval = json.loads(retval)
    return retval


@asynccontextmanager
    async with orders_client(ctx) as cl:
        json_resp = await cl.cancel_order(str(order_id))

    click.echo(json_resp)


@orders.command()
@click.pass_context
@translate_exceptions
@coro
@click.argument('order_id', type=click.UUID)
@click.option('--delay',
              type=int,
              default=5,
              help='Time (in seconds) between polls.')
@click.option('--max-attempts',
              type=int,
              default=200,
              show_default=True,
              help='Maximum number of polls. Set to zero for no limit.')
@click.option('--state',
              help='State prior to a final state that will end polling.',
              type=click.Choice(planet.clients.orders.ORDER_STATE_SEQUENCE,
                                case_sensitive=False))
async def wait(ctx, order_id, delay, max_attempts, state):
    """Wait until order reaches desired state.

    Reports the state of the order on the last poll.

    This function polls the Orders API to determine the order state, with
    the specified delay between each polling attempt, until the
    order reaches a final state or earlier state, if specified.
    If the maximum number of attempts is reached before polling is
    complete, an exception is raised. Setting --max-attempts to zero will
    result in no limit on the number of attempts.

    Setting --delay to zero results in no delay between polling attempts.
    This will likely result in throttling by the Orders API, which has
    a rate limit of 10 requests per second. If many orders are being
    polled asynchronously, consider increasing the delay to avoid
    throttling.

    By default, polling completes when the order reaches a final state.
    If --state is specified, polling will complete when the specified earlier
    state is reached or passed.
    """
    quiet = ctx.obj['QUIET']
    async with orders_client(ctx) as cl:
        with planet.reporting.StateBar(order_id=order_id,
                                       disable=quiet) as bar:
            state = await cl.wait(str(order_id),
                                  state=state,
                                  delay=delay,
                                  max_attempts=max_attempts,
                                  callback=bar.update_state)
    click.echo(state)


@orders.command()
@click.pass_context
@translate_exceptions
@coro
@click.argument('order_id', type=click.UUID)
@click.option('--checksum',
              default=None,
              type=click.Choice(['MD5', 'SHA256'], case_sensitive=False),
              help=('Verify that checksums match.'))
@click.option('--directory',
              default='.',
              help=('Base directory for file download.'),
              type=click.Path(exists=True,
                              resolve_path=True,
                              writable=True,
                              file_okay=False))
@click.option('--overwrite',
              is_flag=True,
              default=False,
              help=('Overwrite files if they already exist.'))
async def download(ctx, order_id, overwrite, directory, checksum):
    """Download order by order ID.

    If --checksum is provided, the associated checksums given in the manifest
    are compared against the downloaded files to verify that they match.

    If --checksum is provided, files are already downloaded, and --overwrite is
    not specified, this will simply validate the checksums of the files against
    the manifest.
    """
    quiet = ctx.obj['QUIET']
    async with orders_client(ctx) as cl:
        await cl.download_order(
            str(order_id),
            directory=Path(directory),
            overwrite=overwrite,
            progress_bar=not quiet,
        )
        if checksum:
            cl.validate_checksum(Path(directory, str(order_id)), checksum)


@orders.command()
@click.pass_context
@translate_exceptions
@coro
@click.argument("request", type=types.JSON(), default="-", required=False)
@pretty
async def create(ctx, request: str, pretty):
    '''Create an order.

    This command outputs the created order description, optionally
    pretty-printed.

    REQUEST is the full description of the order to be created. It must be JSON
    and can be specified a json string, filename, or '-' for stdin.
    '''
    async with orders_client(ctx) as cl:
        order = await cl.create_order(request)

    echo_json(order, pretty)


@orders.command()
@click.pass_context
@translate_exceptions
@coro
@click.argument('item_type',
                metavar='ITEM_TYPE',
                type=click.Choice(planet.specs.get_item_types(),
                                  case_sensitive=False))
@click.argument('bundle',
                metavar='BUNDLE',
                type=click.Choice(planet.specs.get_product_bundles(),
                                  case_sensitive=False))
@click.option('--name',
              required=True,
              help='Order name. Does not need to be unique.',
              type=click.STRING)
@click.option('--id',
              help='One or more comma-separated item IDs.',
              type=types.CommaSeparatedString(),
              required=True)
@click.option('--clip',
              type=types.JSON(),
              help="""Clip feature GeoJSON. Can be a json string, filename,
              or '-' for stdin.""")
@click.option(
    '--tools',
    type=types.JSON(),
    help="""Toolchain JSON. Can be a json string, filename, or '-' for
    stdin.""")
@click.option('--email',
              default=False,
              is_flag=True,
              help='Send email notification when order is complete.')
@click.option(
    '--cloudconfig',
    type=types.JSON(),
    help="""Credentials for cloud storage provider to enable cloud delivery of
    data. Can be a json string, filename, or '-' for stdin.""")
@click.option(
    '--stac/--no-stac',
    default=True,
    is_flag=True,
    help="""Include or exclude metadata in SpatioTemporal Asset Catalog (STAC)
    format. Not specifying either defaults to including it (--stac).""")
@pretty
async def request(ctx,
                  item_type,
                  bundle,
                  name,
                  id,
                  clip,
                  tools,
                  email,
                  cloudconfig,
                  stac,
                  pretty):
    """Generate an order request.

    This command provides support for building an order description used
    in creating an order. It outputs the order request, optionally pretty-
    printed.
    """
    try:
        product = planet.order_request.product(id, bundle, item_type)
    except planet.specs.SpecificationException as e:
        raise click.BadParameter(e)

    if email:
        notifications = planet.order_request.notifications(email=email)
    else:
        notifications = None

    if clip and tools:
        raise click.BadParameter("Specify only one of '--clip' or '--tools'")
    elif clip:
        try:
            clip = planet.geojson.as_polygon(clip)
        except planet.exceptions.GeoJSONError as e:
@click.pass_context
@translate_exceptions
@coro
@click.argument('request', default='-', required=False)
@pretty
async def create(ctx, request, pretty):
    '''  Create an order.
        delivery = None

    if stac:
        stac_json = {'stac': {}}
    else:
        stac_json = {}
        Order request as stdin, str, or file name. Full description of order
        to be created.
    '''
    request_json = read(request)
    async with orders_client(ctx) as cl:
        order = await cl.create_order(request_json)

    echo_json(order, pretty)


<MSG> Replaced stdin function with click functionality + new stdin function.

<DFF> @@ -16,9 +16,11 @@ from contextlib import asynccontextmanager
 import json
 import logging
 from os import path
-import re
+# import re
+from typing import List
 
 import click
+from yaml import parse
 
 import planet
 from planet import OrdersClient, Session  # allow mocking
@@ -30,43 +32,71 @@ LOGGER = logging.getLogger(__name__)
 pretty = click.option('--pretty', is_flag=True, help='Format JSON output.')
 
 
-def _split(value):
-    '''return input split on any whitespace or comma'''
-    return re.split(r'\s+|,', value)
-
-
-def read(value, split=False):
-    '''Get the value of an option interpreting as a file implicitly or
-    explicitly and falling back to the value if not explicitly specified.
-    If the value is '@name', then a file must exist with name and the returned
-    value will be the contents of that file. If the value is '@-' or '-', then
-    stdin will be read and returned as the value. Finally, if a file exists
-    with the provided value, that file will be read. Otherwise, the value
-    will be returned.
-    '''
-    v = str(value)
-    retval = value
-    if v == '-':
-        fname = '-' if v == '-' else v[1:]
+# def _split(value):
+#     '''return input split on any whitespace or comma'''
+#     return re.split(r'\s+|,', value)
+
+
+# def read(value, split=False):
+#     '''Get the value of an option interpreting as a file implicitly or
+#     explicitly and falling back to the value if not explicitly specified.
+#     If the value is '@name', then a file must exist with name and the returned
+#     value will be the contents of that file. If the value is '@-' or '-', then
+#     stdin will be read and returned as the value. Finally, if a file exists
+#     with the provided value, that file will be read. Otherwise, the value
+#     will be returned.
+#     '''
+#     v = str(value)
+#     retval = value
+#     if v == '-':
+#         fname = '-' if v == '-' else v[1:]
+#         try:
+#             with click.open_file(fname) as fp:
+#                 if not fp.isatty():
+#                     retval = fp.read()
+#                 else:
+#                     retval = None
+#         # @todo better to leave as IOError and let caller handle it
+#         # to better report in context of call (e.g. the option/type)
+#         except IOError as ioe:
+#             # if explicit and problems, raise
+#             if v[0] == '@':
+#                 raise click.ClickException(str(ioe))
+#     elif path.exists(v) and path.isfile(v):
+#         retval = json.loads(open(retval).read())
+#     if retval and split and type(retval) != tuple:
+#         retval = _split(retval.strip())
+#     if v == '-':
+#         retval = json.loads(retval)
+#     return retval
+
+def parse_item_types(ctx, param, value: str) -> List[str]:
+    """Turn a string of comma-separated names into a list of names."""
+    # Note: we could also normalize case and validate the names against
+    # our schema here.
+    return [part.strip() for part in value.split(",")]
+
+
+def parse_filter(ctx, param, value: str) -> dict:
+    """Turn filter JSON into a dict."""
+    # read filter using raw json
+    if path.exists(value) and path.isfile(value):
         try:
-            with click.open_file(fname) as fp:
-                if not fp.isatty():
-                    retval = fp.read()
-                else:
-                    retval = None
-        # @todo better to leave as IOError and let caller handle it
-        # to better report in context of call (e.g. the option/type)
-        except IOError as ioe:
-            # if explicit and problems, raise
-            if v[0] == '@':
-                raise click.ClickException(str(ioe))
-    elif path.exists(v) and path.isfile(v):
-        retval = json.loads(open(retval).read())
-    if retval and split and type(retval) != tuple:
-        retval = _split(retval.strip())
-    if v == '-':
-        retval = json.loads(retval)
-    return retval
+            json_value = json.loads(open(value).read())
+        except json.decoder.JSONDecodeError:
+            raise click.ClickException('File does not contain valid json.')
+        return json_value
+    # read filter using click pipe option
+    elif value == '-':
+        try:
+            with click.open_file(value) as f:
+                json_value = json.load(f)
+        except json.decoder.JSONDecodeError:
+            raise click.ClickException('File does not contain valid json.')
+        return json_value
+    else:
+        raise click.ClickException(
+            'Please pass filter using filename or STDIN.')
 
 
 @asynccontextmanager
@@ -272,7 +302,7 @@ def read_file_json(ctx, param, value):
 @click.pass_context
 @translate_exceptions
 @coro
-@click.argument('request', default='-', required=False)
+@click.argument('request', required=True, callback=parse_filter)
 @pretty
 async def create(ctx, request, pretty):
     '''  Create an order.
@@ -285,9 +315,10 @@ async def create(ctx, request, pretty):
         Order request as stdin, str, or file name. Full description of order
         to be created.
     '''
-    request_json = read(request)
+    # if not filter.endswith(".json") | filter != '-':
+    #     raise click.BadParameter("Please pass filter using filename or STDIN.")
     async with orders_client(ctx) as cl:
-        order = await cl.create_order(request_json)
+        order = await cl.create_order(request)
 
     echo_json(order, pretty)
 

{'addition_count': 71, 'commit_subject': 'Replaced stdin function with click functionality + new stdin function.', 'deletion_count': 40, 'file_extension': '.py', 'lang': 'py', 'license': 'apache-2.0', 'repo_name': 'planetlabs/planet-client-python'}