Skip to content

Commit 94b1fd2

Browse files
authored
Merge pull request #1131 from planetlabs/mosaics-api
initial mosaics cli + async client
2 parents 36a554b + e22b178 commit 94b1fd2

File tree

12 files changed

+1558
-2
lines changed

12 files changed

+1558
-2
lines changed

docs/python/sdk-reference.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ title: Python SDK API Reference
1010
rendering:
1111
show_root_full_path: false
1212

13+
## ::: planet.MosaicsClient
14+
rendering:
15+
show_root_full_path: false
16+
1317
## ::: planet.OrdersClient
1418
rendering:
1519
show_root_full_path: false

examples/mosaics-cli.sh

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/bin/bash
2+
3+
echo -e "List the mosaic series that have the word Global in their name"
4+
planet mosaics series list --name-contains=Global | jq .[].name
5+
6+
echo -e "\nWhat is the latest mosaic in the series named Global Monthly, with output indented"
7+
planet mosaics series list-mosaics "Global Monthly" --latest --pretty
8+
9+
echo -e "\nHow many quads are in the mosaic with this ID (name also accepted!)?"
10+
planet mosaics search 09462e5a-2af0-4de3-a710-e9010d8d4e58 --bbox=-100,40,-100,40.1 | jq .[].id
11+
12+
echo -e "\nWhat scenes contributed to this quad in the mosaic with this ID (name also accepted)?"
13+
planet mosaics contributions 09462e5a-2af0-4de3-a710-e9010d8d4e58 455-1273
14+
15+
echo -e "\nDownload them to a directory named quads!"
16+
planet mosaics download 09462e5a-2af0-4de3-a710-e9010d8d4e58 --bbox=-100,40,-100,40.1 --output-dir=quads

planet/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from .__version__ import __version__ # NOQA
1818
from .auth import Auth
1919
from .auth_builtins import PlanetOAuthScopes
20-
from .clients import DataClient, FeaturesClient, OrdersClient, SubscriptionsClient # NOQA
20+
from .clients import DataClient, FeaturesClient, MosaicsClient, OrdersClient, SubscriptionsClient # NOQA
2121
from .io import collect
2222
from .sync import Planet
2323

@@ -28,6 +28,7 @@
2828
'DataClient',
2929
'data_filter',
3030
'FeaturesClient',
31+
'MosaicsClient',
3132
'OrdersClient',
3233
'order_request',
3334
'Planet',

planet/cli/cli.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import planet_auth_utils
2222
import planet
23+
from planet.cli import mosaics
2324

2425
from . import auth, cmds, collect, data, orders, subscriptions, features
2526

@@ -128,6 +129,7 @@ def _configure_logging(verbosity):
128129
main.add_command(subscriptions.subscriptions) # type: ignore
129130
main.add_command(collect.collect) # type: ignore
130131
main.add_command(features.features)
132+
main.add_command(mosaics.mosaics)
131133

132134
if __name__ == "__main__":
133135
main() # pylint: disable=E1120

planet/cli/mosaics.py

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
import asyncio
2+
from contextlib import asynccontextmanager
3+
4+
import click
5+
6+
from planet.cli.cmds import command
7+
from planet.cli.io import echo_json
8+
from planet.cli.session import CliSession
9+
from planet.cli.types import BoundingBox, DateTime, Geometry
10+
from planet.cli.validators import check_geom
11+
from planet.clients.mosaics import MosaicsClient
12+
13+
14+
@asynccontextmanager
15+
async def client(ctx):
16+
async with CliSession() as sess:
17+
cl = MosaicsClient(sess, base_url=ctx.obj['BASE_URL'])
18+
yield cl
19+
20+
21+
include_links = click.option("--links",
22+
is_flag=True,
23+
help=("If enabled, include API links"))
24+
25+
name_contains = click.option(
26+
"--name-contains",
27+
type=str,
28+
help=("Match if the name contains text, case-insensitive"))
29+
30+
bbox = click.option('--bbox',
31+
type=BoundingBox(),
32+
help=("Region to download as comma-delimited strings: "
33+
" lon_min,lat_min,lon_max,lat_max"))
34+
35+
interval = click.option("--interval",
36+
type=str,
37+
help=("Match this interval, e.g. 1 mon"))
38+
39+
acquired_gt = click.option("--acquired_gt",
40+
type=DateTime(),
41+
help=("Imagery acquisition after than this date"))
42+
43+
acquired_lt = click.option("--acquired_lt",
44+
type=DateTime(),
45+
help=("Imagery acquisition before than this date"))
46+
47+
geometry = click.option('--geometry',
48+
type=Geometry(),
49+
callback=check_geom,
50+
help=("A geojson geometry to search with. "
51+
"Can be a string, filename, or - for stdin."))
52+
53+
54+
def _strip_links(resource):
55+
if isinstance(resource, dict):
56+
resource.pop("_links", None)
57+
return resource
58+
59+
60+
async def _output(result, pretty, include_links=False):
61+
if asyncio.iscoroutine(result):
62+
result = await result
63+
if not include_links:
64+
_strip_links(result)
65+
echo_json(result, pretty)
66+
else:
67+
results = [_strip_links(r) async for r in result]
68+
echo_json(results, pretty)
69+
70+
71+
@click.group() # type: ignore
72+
@click.pass_context
73+
@click.option('-u',
74+
'--base-url',
75+
default=None,
76+
help='Assign custom base Mosaics API URL.')
77+
def mosaics(ctx, base_url):
78+
"""Commands for interacting with the Mosaics API"""
79+
ctx.obj['BASE_URL'] = base_url
80+
81+
82+
@mosaics.group() # type: ignore
83+
def series():
84+
"""Commands for interacting with Mosaic Series through the Mosaics API"""
85+
86+
87+
@command(mosaics, name="contributions")
88+
@click.argument("name_or_id")
89+
@click.argument("quad")
90+
async def quad_contributions(ctx, name_or_id, quad, pretty):
91+
'''Get contributing scenes for a quad in a mosaic specified by name or ID
92+
93+
Example:
94+
95+
planet mosaics contribution global_monthly_2025_04_mosaic 575-1300
96+
'''
97+
async with client(ctx) as cl:
98+
item = await cl.get_quad(name_or_id, quad)
99+
await _output(cl.get_quad_contributions(item), pretty)
100+
101+
102+
@command(mosaics, name="info")
103+
@click.argument("name_or_id", required=True)
104+
@include_links
105+
async def mosaic_info(ctx, name_or_id, pretty, links):
106+
"""Get information for a mosaic specified by name or ID
107+
108+
Example:
109+
110+
planet mosaics info global_monthly_2025_04_mosaic
111+
"""
112+
async with client(ctx) as cl:
113+
await _output(cl.get_mosaic(name_or_id), pretty, links)
114+
115+
116+
@command(mosaics, name="list")
117+
@name_contains
118+
@interval
119+
@acquired_gt
120+
@acquired_lt
121+
@include_links
122+
async def mosaics_list(ctx,
123+
name_contains,
124+
interval,
125+
acquired_gt,
126+
acquired_lt,
127+
pretty,
128+
links):
129+
"""List information for all available mosaics
130+
131+
Example:
132+
133+
planet mosaics list --name-contains global_monthly
134+
"""
135+
async with client(ctx) as cl:
136+
await _output(
137+
cl.list_mosaics(name_contains=name_contains,
138+
interval=interval,
139+
acquired_gt=acquired_gt,
140+
acquired_lt=acquired_lt),
141+
pretty,
142+
links)
143+
144+
145+
@command(series, name="info")
146+
@click.argument("name_or_id", required=True)
147+
@include_links
148+
async def series_info(ctx, name_or_id, pretty, links):
149+
"""Get information for a series specified by name or ID
150+
151+
Example:
152+
153+
planet series info "Global Quarterly"
154+
"""
155+
async with client(ctx) as cl:
156+
await _output(cl.get_series(name_or_id), pretty, links)
157+
158+
159+
@command(series, name="list")
160+
@name_contains
161+
@interval
162+
@acquired_gt
163+
@acquired_lt
164+
@include_links
165+
async def series_list(ctx,
166+
name_contains,
167+
interval,
168+
acquired_gt,
169+
acquired_lt,
170+
pretty,
171+
links):
172+
"""List information for available series
173+
174+
Example:
175+
176+
planet mosaics series list --name-contains=Global
177+
"""
178+
async with client(ctx) as cl:
179+
await _output(
180+
cl.list_series(
181+
name_contains=name_contains,
182+
interval=interval,
183+
acquired_gt=acquired_gt,
184+
acquired_lt=acquired_lt,
185+
),
186+
pretty,
187+
links)
188+
189+
190+
@command(series, name="list-mosaics")
191+
@click.argument("name_or_id", required=True)
192+
@click.option("--latest",
193+
is_flag=True,
194+
help=("Get the latest mosaic in the series"))
195+
@acquired_gt
196+
@acquired_lt
197+
@include_links
198+
async def list_series_mosaics(ctx,
199+
name_or_id,
200+
acquired_gt,
201+
acquired_lt,
202+
latest,
203+
pretty,
204+
links):
205+
"""List mosaics in a series specified by name or ID
206+
207+
Example:
208+
209+
planet mosaics series list-mosaics global_monthly_2025_04_mosaic
210+
"""
211+
async with client(ctx) as cl:
212+
await _output(
213+
cl.list_series_mosaics(name_or_id,
214+
acquired_gt=acquired_gt,
215+
acquired_lt=acquired_lt,
216+
latest=latest),
217+
pretty,
218+
links)
219+
220+
221+
@command(mosaics, name="search")
222+
@click.argument("name_or_id", required=True)
223+
@bbox
224+
@geometry
225+
@click.option("--summary",
226+
is_flag=True,
227+
help=("Get a count of how many quads would be returned"))
228+
@include_links
229+
async def list_quads(ctx, name_or_id, bbox, geometry, summary, pretty, links):
230+
"""Search quads in a mosaic specified by name or ID
231+
232+
Example:
233+
234+
planet mosaics search global_monthly_2025_04_mosaic --bbox -100,40,-100,41
235+
"""
236+
async with client(ctx) as cl:
237+
if summary:
238+
result = cl.summarize_quads(name_or_id,
239+
bbox=bbox,
240+
geometry=geometry)
241+
else:
242+
result = cl.list_quads(name_or_id,
243+
minimal=False,
244+
bbox=bbox,
245+
geometry=geometry)
246+
await _output(result, pretty, links)
247+
248+
249+
@command(mosaics, name="download")
250+
@click.argument("name_or_id", required=True)
251+
@click.option('--output-dir',
252+
help=('Directory for file download. Defaults to mosaic name'),
253+
type=click.Path(exists=True,
254+
resolve_path=True,
255+
writable=True,
256+
file_okay=False))
257+
@bbox
258+
@geometry
259+
async def download(ctx, name_or_id, output_dir, bbox, geometry, **kwargs):
260+
"""Download quads from a mosaic by name or ID
261+
262+
Example:
263+
264+
planet mosaics search global_monthly_2025_04_mosaic --bbox -100,40,-100,41
265+
"""
266+
quiet = ctx.obj['QUIET']
267+
async with client(ctx) as cl:
268+
await cl.download_quads(name_or_id,
269+
bbox=bbox,
270+
geometry=geometry,
271+
directory=output_dir,
272+
progress_bar=not quiet)

planet/cli/types.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,14 @@ def convert(self, value, param, ctx) -> datetime:
140140
self.fail(str(e))
141141

142142
return value
143+
144+
145+
class BoundingBox(click.ParamType):
146+
name = 'bbox'
147+
148+
def convert(self, val, param, ctx):
149+
try:
150+
xmin, ymin, xmax, ymax = map(float, val.split(','))
151+
except (TypeError, ValueError):
152+
raise click.BadParameter('Invalid bounding box')
153+
return (xmin, ymin, xmax, ymax)

planet/clients/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,23 @@
1414
# limitations under the License.
1515
from .data import DataClient
1616
from .features import FeaturesClient
17+
from .mosaics import MosaicsClient
1718
from .orders import OrdersClient
1819
from .subscriptions import SubscriptionsClient
1920

2021
__all__ = [
21-
'DataClient', 'FeaturesClient', 'OrdersClient', 'SubscriptionsClient'
22+
'DataClient',
23+
'FeaturesClient',
24+
'MosaicsClient',
25+
'OrdersClient',
26+
'SubscriptionsClient'
2227
]
2328

2429
# Organize client classes by their module name to allow lookup.
2530
_client_directory = {
2631
'data': DataClient,
2732
'features': FeaturesClient,
33+
'mosaics': MosaicsClient,
2834
'orders': OrdersClient,
2935
'subscriptions': SubscriptionsClient
3036
}

0 commit comments

Comments
 (0)