Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 8b4530a

Browse files
committedMay 12, 2025·
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
}

‎planet/clients/mosaics.py

Lines changed: 397 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,397 @@
1+
import asyncio
2+
from pathlib import Path
3+
from typing import AsyncIterator, Awaitable, Optional, Tuple, Type, TypeVar, Union, cast
4+
from planet.constants import PLANET_BASE_URL
5+
from planet.http import Session
6+
from planet.models import Paged, Response, StreamingBody
7+
from uuid import UUID
8+
9+
BASE_URL = f'{PLANET_BASE_URL}/basemaps/v1'
10+
11+
T = TypeVar("T")
12+
13+
Number = Union[int, float]
14+
15+
BBox = Tuple[Number, Number, Number, Number]
16+
17+
18+
class Series(Paged):
19+
ITEMS_KEY = 'series'
20+
NEXT_KEY = '_next'
21+
22+
23+
class Mosaics(Paged):
24+
ITEMS_KEY = 'mosaics'
25+
NEXT_KEY = '_next'
26+
27+
28+
class MosaicQuads(Paged):
29+
ITEMS_KEY = 'items'
30+
NEXT_KEY = '_next'
31+
32+
33+
def _is_uuid(val: str) -> bool:
34+
try:
35+
UUID(val)
36+
return True
37+
except ValueError:
38+
return False
39+
40+
41+
class MosaicsClient:
42+
"""High-level asynchronous access to Planet's Mosaics API.
43+
44+
Example:
45+
```python
46+
>>> import asyncio
47+
>>> from planet import Session
48+
>>>
49+
>>> async def main():
50+
... async with Session() as sess:
51+
... cl = sess.client('data')
52+
... # use client here
53+
...
54+
>>> asyncio.run(main())
55+
```
56+
"""
57+
58+
def __init__(self, session: Session, base_url: Optional[str] = None):
59+
"""
60+
Parameters:
61+
session: Open session connected to server.
62+
base_url: The base URL to use. Defaults to production orders API
63+
base url.
64+
"""
65+
self._session = session
66+
67+
self._base_url = base_url or BASE_URL
68+
if self._base_url.endswith('/'):
69+
self._base_url = self._base_url[:-1]
70+
71+
def _call_sync(self, f: Awaitable[T]) -> T:
72+
"""block on an async function call, using the call_sync method of the session"""
73+
return self._session._call_sync(f)
74+
75+
def _url(self, path: str) -> str:
76+
return f"{BASE_URL}/{path}"
77+
78+
async def _get_by_name(self, path: str, pager: Type[Paged],
79+
name: str) -> Optional[dict]:
80+
response = await self._session.request(
81+
method='GET',
82+
url=self._url(path),
83+
params={
84+
"name__is": name,
85+
},
86+
)
87+
listing = response.json()[pager.ITEMS_KEY]
88+
return listing[0] if listing else None
89+
90+
async def _get_by_id(self, path: str, id: str) -> dict:
91+
response = await self._session.request(method="GET",
92+
url=self._url(f"{path}/{id}"))
93+
return response.json()
94+
95+
async def _get(self, name_or_id: str, path: str,
96+
pager: Type[Paged]) -> Optional[dict]:
97+
if _is_uuid(name_or_id):
98+
return await self._get_by_id(path, name_or_id)
99+
return await self._get_by_name(path, pager, name_or_id)
100+
101+
async def get_mosaic(self, name_or_id: str) -> Optional[dict]:
102+
"""Get the API representation of a mosaic by name or id.
103+
104+
:param name str: The name or id of the mosaic
105+
:returns: dict or None (if searching by name)
106+
:raises planet.api.exceptions.APIException: On API error.
107+
"""
108+
return await self._get(name_or_id, "mosaics", Mosaics)
109+
110+
async def get_series(self, name_or_id: str) -> Optional[dict]:
111+
"""Get the API representation of a series by name or id.
112+
113+
:param name str: The name or id of the series
114+
:returns: dict or None (if searching by name)
115+
:raises planet.api.exceptions.APIException: On API error.
116+
"""
117+
return await self._get(name_or_id, "series", Series)
118+
119+
async def list_series(
120+
self,
121+
name_contains: Optional[str] = None,
122+
interval: Optional[str] = None,
123+
acquired_gt: Optional[str] = None,
124+
acquired_lt: Optional[str] = None) -> AsyncIterator[dict]:
125+
"""
126+
List the series you have access to.
127+
128+
Example:
129+
130+
```
131+
series = await client.list_series()
132+
async for s in series:
133+
print(s)
134+
```
135+
"""
136+
params = {}
137+
if name_contains:
138+
params["name__contains"] = name_contains
139+
if interval:
140+
params["interval"] = interval
141+
if acquired_gt:
142+
params["acquired__gt"] = acquired_gt
143+
if acquired_lt:
144+
params["acquired__lt"] = acquired_lt
145+
resp = await self._session.request(
146+
method='GET',
147+
url=self._url("series"),
148+
params=params,
149+
)
150+
async for item in Series(resp, self._session.request):
151+
yield item
152+
153+
async def list_mosaics(
154+
self,
155+
name_contains: Optional[str] = None,
156+
interval: Optional[str] = None,
157+
acquired_gt: Optional[str] = None,
158+
acquired_lt: Optional[str] = None,
159+
latest: bool = False,
160+
) -> AsyncIterator[dict]:
161+
"""
162+
List the mosaics you have access to.
163+
164+
Example:
165+
166+
```
167+
mosaics = await client.list_mosacis()
168+
async for m in mosaics:
169+
print(m)
170+
```
171+
"""
172+
params = {}
173+
if name_contains:
174+
params["name__contains"] = name_contains
175+
if interval:
176+
params["interval"] = interval
177+
if acquired_gt:
178+
params["acquired__gt"] = acquired_gt
179+
if acquired_lt:
180+
params["acquired__lt"] = acquired_lt
181+
if latest:
182+
params["latest"] = "yes"
183+
resp = await self._session.request(
184+
method='GET',
185+
url=self._url("mosaics"),
186+
params=params,
187+
)
188+
async for item in Mosaics(resp, self._session.request):
189+
yield item
190+
191+
async def list_series_mosaics(
192+
self,
193+
name_or_id: str,
194+
acquired_gt: Optional[str] = None,
195+
acquired_lt: Optional[str] = None,
196+
latest: bool = False,
197+
) -> AsyncIterator[dict]:
198+
"""
199+
List the mosaics in a series.
200+
201+
Example:
202+
203+
```
204+
mosaics = await client.list_series_mosaics("d5098531-aa4f-4ff9-a9d5-74ad4a6301e5")
205+
async for m in mosaics:
206+
print(m)
207+
```
208+
"""
209+
if not _is_uuid(name_or_id):
210+
series = await self._get_by_name("series", Series, name_or_id)
211+
if series is None:
212+
return
213+
name_or_id = series["id"]
214+
params = {}
215+
if acquired_gt:
216+
params["acquired__gt"] = acquired_gt
217+
if acquired_lt:
218+
params["acquired__lt"] = acquired_lt
219+
if latest:
220+
params["latest"] = "yes"
221+
resp = await self._session.request(
222+
method="GET",
223+
url=self._url(f"series/{name_or_id}/mosaics"),
224+
params=params,
225+
)
226+
async for item in Mosaics(resp, self._session.request):
227+
yield item
228+
229+
async def list_quads(self,
230+
mosaic: dict,
231+
minimal: bool = False,
232+
bbox: Optional[BBox] = None,
233+
geometry: Optional[dict] = None,
234+
summary: bool = False) -> AsyncIterator[dict]:
235+
"""
236+
List the a mosaic's quads.
237+
238+
Example:
239+
240+
```
241+
mosaic = await client.get_mosaic("d5098531-aa4f-4ff9-a9d5-74ad4a6301e5")
242+
quads = await client.list_quads(mosaic)
243+
async for q in quads:
244+
print(q)
245+
```
246+
"""
247+
if geometry:
248+
resp = await self._quads_geometry(mosaic,
249+
geometry,
250+
minimal,
251+
summary)
252+
else:
253+
if bbox is None:
254+
xmin, ymin, xmax, ymax = cast(BBox, mosaic['bbox'])
255+
search = (max(-180, xmin),
256+
max(-85, ymin),
257+
min(180, xmax),
258+
min(85, ymax))
259+
else:
260+
search = bbox
261+
resp = await self._quads_bbox(mosaic, search, minimal, summary)
262+
# kinda yucky - yields a different "shaped" dict
263+
if summary:
264+
yield resp.json()["summary"]
265+
return
266+
async for item in MosaicQuads(resp, self._session.request):
267+
yield item
268+
269+
async def _quads_geometry(self,
270+
mosaic: dict,
271+
geometry: dict,
272+
minimal: bool,
273+
summary: bool) -> Response:
274+
params = {}
275+
if minimal:
276+
params["minimal"] = "true"
277+
if summary:
278+
params["summary"] = "true"
279+
mosaic_id = mosaic["id"]
280+
return await self._session.request(
281+
method="POST",
282+
url=self._url(f"mosaics/{mosaic_id}/quads/search"),
283+
params=params,
284+
json=geometry,
285+
)
286+
287+
async def _quads_bbox(self,
288+
mosaic: dict,
289+
bbox: BBox,
290+
minimal: bool,
291+
summary: bool) -> Response:
292+
quads_template = mosaic["_links"]["quads"]
293+
# this is fully qualified URL, so don't use self._url
294+
url = quads_template.replace("{lx},{ly},{ux},{uy}",
295+
",".join([str(f) for f in bbox]))
296+
# params will overwrite the templated query
297+
if minimal:
298+
url += "&minimal=true"
299+
if summary:
300+
url += "&summary=true"
301+
return await self._session.request(
302+
method="GET",
303+
url=url,
304+
)
305+
306+
async def get_quad(self, name_or_id: str, quad_id: str) -> dict:
307+
"""
308+
Get a mosaic's quad information.
309+
310+
Example:
311+
312+
```
313+
quad = await client.get_quad("d5098531-aa4f-4ff9-a9d5-74ad4a6301e5", "1234-5678")
314+
print(quad)
315+
```
316+
"""
317+
if not _is_uuid(name_or_id):
318+
mosaic = await self.get_mosaic(name_or_id)
319+
if mosaic is None:
320+
return {}
321+
name_or_id = cast(str, mosaic["id"])
322+
resp = await self._session.request(
323+
method="GET",
324+
url=self._url(f"mosaics/{name_or_id}/quads/{quad_id}"),
325+
)
326+
return resp.json()
327+
328+
async def get_quad_contributions(self, quad: dict) -> list[dict]:
329+
"""
330+
Get a mosaic's quad information.
331+
332+
Example:
333+
334+
```
335+
quad = await client.get_quad("d5098531-aa4f-4ff9-a9d5-74ad4a6301e5", "1234-5678")
336+
contributions = await client.get_quad_contributions(quad)
337+
print(contributions)
338+
```
339+
"""
340+
resp = await self._session.request(
341+
"GET",
342+
quad["_links"]["items"],
343+
)
344+
return resp.json()["items"]
345+
346+
async def download_quad(self,
347+
quad: dict,
348+
directory,
349+
overwrite: bool = False,
350+
progress_bar=False):
351+
url = quad["_links"]["download"]
352+
Path(directory).mkdir(exist_ok=True, parents=True)
353+
async with self._session.stream(method='GET', url=url) as resp:
354+
body = StreamingBody(resp)
355+
dest = Path(directory, body.name)
356+
await body.write(dest,
357+
overwrite=overwrite,
358+
progress_bar=progress_bar)
359+
"""
360+
Download a quad to a directory.
361+
362+
Example:
363+
364+
```
365+
quad = await client.get_quad("d5098531-aa4f-4ff9-a9d5-74ad4a6301e5", "1234-5678")
366+
await client.download_quad(quad, ".")
367+
```
368+
"""
369+
370+
async def download_quads(self,
371+
mosaic: dict,
372+
directory: str,
373+
bbox: Optional[BBox] = None,
374+
geometry: Optional[dict] = None,
375+
progress_bar: bool = False,
376+
concurrency: int = 4):
377+
"""
378+
Download a mosaics' quads to a directory.
379+
380+
Example:
381+
382+
```
383+
mosaic = await cl.get_mosaic(name)
384+
client.download_quads(mosaic, '.', bbox=(-100, 40, -100, 41))
385+
```
386+
"""
387+
jobs = []
388+
async for q in self.list_quads(mosaic,
389+
minimal=True,
390+
bbox=bbox,
391+
geometry=geometry):
392+
jobs.append(
393+
self.download_quad(q, directory, progress_bar=progress_bar))
394+
if len(jobs) == concurrency:
395+
await asyncio.gather(*jobs)
396+
jobs = []
397+
await asyncio.gather(*jobs)

0 commit comments

Comments
 (0)
Please sign in to comment.