Skip to content

initial mosaics cli + async client #1131

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/python/sdk-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ title: Python SDK API Reference
rendering:
show_root_full_path: false

## ::: planet.MosaicsClient
rendering:
show_root_full_path: false

## ::: planet.OrdersClient
rendering:
show_root_full_path: false
Expand Down Expand Up @@ -37,5 +41,3 @@ title: Python SDK API Reference
## ::: planet.reporting
rendering:
show_root_full_path: false


16 changes: 16 additions & 0 deletions examples/mosaics-cli.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/bin/bash

echo -e "List the mosaic series that have the word Global in their name"
planet mosaics series list --name-contains=Global | jq .[].name

echo -e "\nWhat is the latest mosaic in the series named Global Monthly, with output indented"
planet mosaics series list-mosaics "Global Monthly" --latest --pretty

echo -e "\nHow many quads are in the mosaic with this ID (name also accepted!)?"
planet mosaics search 09462e5a-2af0-4de3-a710-e9010d8d4e58 --bbox=-100,40,-100,40.1 | jq .[].id

echo -e "\nWhat scenes contributed to this quad in the mosaic with this ID (name also accepted)?"
planet mosaics contributions 09462e5a-2af0-4de3-a710-e9010d8d4e58 455-1273

echo -e "\nDownload them to a directory named quads!"
planet mosaics download 09462e5a-2af0-4de3-a710-e9010d8d4e58 --bbox=-100,40,-100,40.1 --output-dir=quads
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious if we think users might need guard rails for this. Could this be a significant enough amount of data to warrant a confirmation step?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this was one of the issues (1) I raised in the MR description.

3 changes: 2 additions & 1 deletion planet/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from . import data_filter, order_request, reporting, subscription_request
from .__version__ import __version__ # NOQA
from .auth import Auth
from .clients import DataClient, FeaturesClient, OrdersClient, SubscriptionsClient # NOQA
from .clients import DataClient, FeaturesClient, MosaicsClient, OrdersClient, SubscriptionsClient # NOQA
from .io import collect
from .sync import Planet

Expand All @@ -26,6 +26,7 @@
'DataClient',
'data_filter',
'FeaturesClient',
'MosaicsClient',
'OrdersClient',
'order_request',
'Planet',
Expand Down
2 changes: 2 additions & 0 deletions planet/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import click

import planet
from planet.cli import mosaics

from . import auth, collect, data, orders, subscriptions, features

Expand Down Expand Up @@ -79,3 +80,4 @@ def _configure_logging(verbosity):
main.add_command(subscriptions.subscriptions) # type: ignore
main.add_command(collect.collect) # type: ignore
main.add_command(features.features)
main.add_command(mosaics.mosaics)
270 changes: 270 additions & 0 deletions planet/cli/mosaics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
import asyncio
from contextlib import asynccontextmanager

import click

from planet.cli.cmds import command
from planet.cli.io import echo_json
from planet.cli.session import CliSession
from planet.cli.types import BoundingBox, DateTime, Geometry
from planet.cli.validators import check_geom
from planet.clients.mosaics import MosaicsClient


@asynccontextmanager
async def client(ctx):
async with CliSession() as sess:
cl = MosaicsClient(sess, base_url=ctx.obj['BASE_URL'])
yield cl


include_links = click.option("--links",
is_flag=True,
help=("If enabled, include API links"))

name_contains = click.option(
"--name-contains",
type=str,
help=("Match if the name contains text, case-insensitive"))

bbox = click.option('--bbox',
type=BoundingBox(),
help=("Region to download as comma-delimited strings: "
" lon_min,lat_min,lon_max,lat_max"))

interval = click.option("--interval",
type=str,
help=("Match this interval, e.g. 1 mon"))

acquired_gt = click.option("--acquired_gt",
type=DateTime(),
help=("Imagery acquisition after than this date"))

acquired_lt = click.option("--acquired_lt",
type=DateTime(),
help=("Imagery acquisition before than this date"))

geometry = click.option('--geometry',
type=Geometry(),
callback=check_geom,
help=("A geojson geometry to search with. "
"Can be a string, filename, or - for stdin."))


def _strip_links(resource):
if isinstance(resource, dict):
resource.pop("_links", None)
return resource


async def _output(result, pretty, include_links=False):
if asyncio.iscoroutine(result):
result = await result
if not include_links:
_strip_links(result)
echo_json(result, pretty)
else:
results = [_strip_links(r) async for r in result]
echo_json(results, pretty)


@click.group() # type: ignore
@click.pass_context
@click.option('-u',
'--base-url',
default=None,
help='Assign custom base Mosaics API URL.')
def mosaics(ctx, base_url):
"""Commands for interacting with the Mosaics API"""
ctx.obj['BASE_URL'] = base_url


@mosaics.group() # type: ignore
def series():
"""Commands for interacting with Mosaic Series through the Mosaics API"""


@command(mosaics, name="contributions")
@click.argument("name_or_id")
@click.argument("quad")
async def quad_contributions(ctx, name_or_id, quad, pretty):
'''Get contributing scenes for a quad in a mosaic specified by name or ID

Example:

planet mosaics contribution global_monthly_2025_04_mosaic 575-1300
'''
async with client(ctx) as cl:
item = await cl.get_quad(name_or_id, quad)
await _output(cl.get_quad_contributions(item), pretty)


@command(mosaics, name="info")
@click.argument("name_or_id", required=True)
@include_links
async def mosaic_info(ctx, name_or_id, pretty, links):
"""Get information for a mosaic specified by name or ID

Example:

planet mosaics info global_monthly_2025_04_mosaic
"""
async with client(ctx) as cl:
await _output(cl.get_mosaic(name_or_id), pretty, links)


@command(mosaics, name="list")
@name_contains
@interval
@acquired_gt
@acquired_lt
@include_links
async def mosaics_list(ctx,
name_contains,
interval,
acquired_gt,
acquired_lt,
pretty,
links):
"""List information for all available mosaics

Example:

planet mosaics list --name-contains global_monthly
"""
async with client(ctx) as cl:
await _output(
cl.list_mosaics(name_contains=name_contains,
interval=interval,
acquired_gt=acquired_gt,
acquired_lt=acquired_lt),
pretty,
links)


@command(series, name="info")
@click.argument("name_or_id", required=True)
@include_links
async def series_info(ctx, name_or_id, pretty, links):
"""Get information for a series specified by name or ID

Example:

planet series info "Global Quarterly"
"""
async with client(ctx) as cl:
await _output(cl.get_series(name_or_id), pretty, links)


@command(series, name="list")
@name_contains
@interval
@acquired_gt
@acquired_lt
@include_links
async def series_list(ctx,
name_contains,
interval,
acquired_gt,
acquired_lt,
pretty,
links):
"""List information for available series

Example:

planet mosaics series list --name-contains=Global
"""
async with client(ctx) as cl:
await _output(
cl.list_series(
name_contains=name_contains,
interval=interval,
acquired_gt=acquired_gt,
acquired_lt=acquired_lt,
),
pretty,
links)


@command(series, name="list-mosaics")
@click.argument("name_or_id", required=True)
@click.option("--latest",
is_flag=True,
help=("Get the latest mosaic in the series"))
@acquired_gt
@acquired_lt
@include_links
async def list_series_mosaics(ctx,
name_or_id,
acquired_gt,
acquired_lt,
latest,
pretty,
links):
"""List mosaics in a series specified by name or ID

Example:

planet mosaics series list-mosaics global_monthly_2025_04_mosaic
"""
async with client(ctx) as cl:
await _output(
cl.list_series_mosaics(name_or_id,
acquired_gt=acquired_gt,
acquired_lt=acquired_lt,
latest=latest),
pretty,
links)


@command(mosaics, name="search")
@click.argument("name_or_id", required=True)
@bbox
@geometry
@click.option("--summary",
is_flag=True,
help=("Get a count of how many quads would be returned"))
@include_links
async def list_quads(ctx, name_or_id, bbox, geometry, summary, pretty, links):
"""Search quads in a mosaic specified by name or ID

Example:

planet mosaics search global_monthly_2025_04_mosaic --bbox -100,40,-100,41
"""
async with client(ctx) as cl:
await _output(
cl.list_quads(name_or_id,
minimal=False,
bbox=bbox,
geometry=geometry,
summary=summary),
pretty,
links)


@command(mosaics, name="download")
@click.argument("name_or_id", required=True)
@click.option('--output-dir',
help=('Directory for file download. Defaults to mosaic name'),
type=click.Path(exists=True,
resolve_path=True,
writable=True,
file_okay=False))
@bbox
@geometry
async def download(ctx, name_or_id, output_dir, bbox, geometry, **kwargs):
"""Download quads from a mosaic by name or ID

Example:

planet mosaics search global_monthly_2025_04_mosaic --bbox -100,40,-100,41
"""
quiet = ctx.obj['QUIET']
async with client(ctx) as cl:
await cl.download_quads(name_or_id,
bbox=bbox,
geometry=geometry,
directory=output_dir,
progress_bar=not quiet)
11 changes: 11 additions & 0 deletions planet/cli/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,14 @@ def convert(self, value, param, ctx) -> datetime:
self.fail(str(e))

return value


class BoundingBox(click.ParamType):
name = 'bbox'

def convert(self, val, param, ctx):
try:
xmin, ymin, xmax, ymax = map(float, val.split(','))
except (TypeError, ValueError):
raise click.BadParameter('Invalid bounding box')
return (xmin, ymin, xmax, ymax)
8 changes: 7 additions & 1 deletion planet/clients/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,23 @@
# limitations under the License.
from .data import DataClient
from .features import FeaturesClient
from .mosaics import MosaicsClient
from .orders import OrdersClient
from .subscriptions import SubscriptionsClient

__all__ = [
'DataClient', 'FeaturesClient', 'OrdersClient', 'SubscriptionsClient'
'DataClient',
'FeaturesClient',
'MosaicsClient',
'OrdersClient',
'SubscriptionsClient'
]

# Organize client classes by their module name to allow lookup.
_client_directory = {
'data': DataClient,
'features': FeaturesClient,
'mosaics': MosaicsClient,
'orders': OrdersClient,
'subscriptions': SubscriptionsClient
}
Loading