Skip to content

Commit 8b4530a

Browse files
committed
initial mosaics cli + async client
1 parent 2bfd1b7 commit 8b4530a

File tree

8 files changed

+718
-4
lines changed

8 files changed

+718
-4
lines changed

docs/python/sdk-reference.md

Lines changed: 4 additions & 2 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
@@ -37,5 +41,3 @@ title: Python SDK API Reference
3741
## ::: planet.reporting
3842
rendering:
3943
show_root_full_path: false
40-
41-

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 "Global Basemap Series"
4+
planet mosaics series list --name-contains=Global | jq .[].name
5+
6+
echo -e "\nLatest Global Monthly"
7+
planet mosaics series list-mosaics "Global Monthly" --latest --pretty
8+
9+
echo -e "\nHow Many Quads?"
10+
planet mosaics search 09462e5a-2af0-4de3-a710-e9010d8d4e58 --bbox=-100,40,-100,40.1
11+
12+
echo -e "\nWhat Scenes Contributed to Quad?"
13+
planet mosaics contributions 09462e5a-2af0-4de3-a710-e9010d8d4e58 455-1273
14+
15+
echo -e "\nDownload Them!"
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
@@ -16,7 +16,7 @@
1616
from . import data_filter, order_request, reporting, subscription_request
1717
from .__version__ import __version__ # NOQA
1818
from .auth import Auth
19-
from .clients import DataClient, FeaturesClient, OrdersClient, SubscriptionsClient # NOQA
19+
from .clients import DataClient, FeaturesClient, MosaicsClient, OrdersClient, SubscriptionsClient # NOQA
2020
from .io import collect
2121
from .sync import Planet
2222

@@ -26,6 +26,7 @@
2626
'DataClient',
2727
'data_filter',
2828
'FeaturesClient',
29+
'MosaicsClient',
2930
'OrdersClient',
3031
'order_request',
3132
'Planet',

planet/cli/cli.py

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

2121
import planet
22+
from planet.cli import mosaics
2223

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

@@ -79,3 +80,4 @@ def _configure_logging(verbosity):
7980
main.add_command(subscriptions.subscriptions) # type: ignore
8081
main.add_command(collect.collect) # type: ignore
8182
main.add_command(features.features)
83+
main.add_command(mosaics.mosaics)

planet/cli/mosaics.py

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
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 result is None:
64+
raise click.ClickException("not found")
65+
if not include_links:
66+
_strip_links(result)
67+
echo_json(result, pretty)
68+
else:
69+
results = [_strip_links(r) async for r in result]
70+
echo_json(results, pretty)
71+
72+
73+
@click.group() # type: ignore
74+
@click.pass_context
75+
@click.option('-u',
76+
'--base-url',
77+
default=None,
78+
help='Assign custom base Mosaics API URL.')
79+
def mosaics(ctx, base_url):
80+
"""Commands for interacting with the Mosaics API"""
81+
ctx.obj['BASE_URL'] = base_url
82+
83+
84+
@mosaics.group() # type: ignore
85+
def series():
86+
"""Commands for interacting with Mosaic Series through the Mosaics API"""
87+
88+
89+
@command(mosaics, name="contributions")
90+
@click.argument("name")
91+
@click.argument("quad")
92+
async def quad_contributions(ctx, name, quad, pretty):
93+
'''Get contributing scenes for a mosaic quad
94+
95+
Example:
96+
97+
planet mosaics contribution global_monthly_2025_04_mosaic 575-1300
98+
'''
99+
async with client(ctx) as cl:
100+
item = await cl.get_quad(name, quad)
101+
await _output(cl.get_quad_contributions(item), pretty)
102+
103+
104+
@command(mosaics, name="info")
105+
@click.argument("name", required=True)
106+
@include_links
107+
async def mosaic_info(ctx, name, pretty, links):
108+
"""Get information for a specific mosaic
109+
110+
Example:
111+
112+
planet mosaics info global_monthly_2025_04_mosaic
113+
"""
114+
async with client(ctx) as cl:
115+
await _output(cl.get_mosaic(name), pretty, links)
116+
117+
118+
@command(mosaics, name="list")
119+
@name_contains
120+
@interval
121+
@acquired_gt
122+
@acquired_lt
123+
@include_links
124+
async def mosaics_list(ctx,
125+
name_contains,
126+
interval,
127+
acquired_gt,
128+
acquired_lt,
129+
pretty,
130+
links):
131+
"""List all mosaics
132+
133+
Example:
134+
135+
planet mosaics info global_monthly_2025_04_mosaic
136+
"""
137+
async with client(ctx) as cl:
138+
await _output(
139+
cl.list_mosaics(name_contains=name_contains,
140+
interval=interval,
141+
acquired_gt=acquired_gt,
142+
acquired_lt=acquired_lt),
143+
pretty,
144+
links)
145+
146+
147+
@command(series, name="info")
148+
@click.argument("name", required=True)
149+
@include_links
150+
async def series_info(ctx, name, pretty, links):
151+
"""Get information for a specific series
152+
153+
Example:
154+
155+
planet series info "Global Quarterly"
156+
"""
157+
async with client(ctx) as cl:
158+
await _output(cl.get_series(name), pretty, links)
159+
160+
161+
@command(series, name="list")
162+
@name_contains
163+
@interval
164+
@acquired_gt
165+
@acquired_lt
166+
@include_links
167+
async def series_list(ctx,
168+
name_contains,
169+
interval,
170+
acquired_gt,
171+
acquired_lt,
172+
pretty,
173+
links):
174+
"""List series
175+
176+
Example:
177+
178+
planet mosaics series list --name-contains=Global
179+
"""
180+
async with client(ctx) as cl:
181+
await _output(
182+
cl.list_series(
183+
name_contains,
184+
interval,
185+
acquired_gt,
186+
acquired_lt,
187+
),
188+
pretty,
189+
links)
190+
191+
192+
@command(series, name="list-mosaics")
193+
@click.argument("name", required=True)
194+
@click.option("--latest",
195+
is_flag=True,
196+
help=("Get the latest mosaic in the series"))
197+
@acquired_gt
198+
@acquired_lt
199+
@include_links
200+
async def list_series_mosaics(ctx,
201+
name,
202+
acquired_gt,
203+
acquired_lt,
204+
latest,
205+
links,
206+
pretty):
207+
"""List mosaics in a series
208+
209+
Example:
210+
211+
planet mosaics series list-mosaics global_monthly_2025_04_mosaic
212+
"""
213+
async with client(ctx) as cl:
214+
await _output(
215+
cl.list_series_mosaics(name,
216+
acquired_gt=acquired_gt,
217+
acquired_lt=acquired_lt,
218+
latest=latest),
219+
pretty,
220+
links)
221+
222+
223+
@command(mosaics, name="search")
224+
@click.argument("name", required=True)
225+
@bbox
226+
@geometry
227+
@click.option("--summary",
228+
is_flag=True,
229+
help=("Get a count of how many quads would be returned"))
230+
@include_links
231+
async def list_quads(ctx, name, bbox, geometry, summary, links, pretty):
232+
"""Search quads
233+
234+
Example:
235+
236+
planet mosaics search global_monthly_2025_04_mosaic --bbox -100,40,-100,41
237+
"""
238+
async with client(ctx) as cl:
239+
mosaic = await cl.get_mosaic(name)
240+
if mosaic is None:
241+
raise click.ClickException("No mosaic named " + name)
242+
await _output(
243+
cl.list_quads(mosaic,
244+
minimal=False,
245+
bbox=bbox,
246+
geometry=geometry,
247+
summary=summary),
248+
pretty,
249+
links)
250+
251+
252+
@command(mosaics, name="download")
253+
@click.argument("name", required=True)
254+
@click.option('--output-dir',
255+
default='.',
256+
help=('Directory for file download.'),
257+
type=click.Path(exists=True,
258+
resolve_path=True,
259+
writable=True,
260+
file_okay=False))
261+
@bbox
262+
@geometry
263+
async def download(ctx, name, output_dir, bbox, geometry, **kwargs):
264+
"""Download quads from a mosaic
265+
266+
Example:
267+
268+
planet mosaics search global_monthly_2025_04_mosaic --bbox -100,40,-100,41
269+
"""
270+
quiet = ctx.obj['QUIET']
271+
async with client(ctx) as cl:
272+
mosaic = await cl.get_mosaic(name)
273+
if mosaic is None:
274+
raise click.ClickException("No mosaic named " + name)
275+
await cl.download_quads(mosaic,
276+
bbox=bbox,
277+
geometry=geometry,
278+
directory=output_dir,
279+
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)