Skip to content

Commit 01ba3c4

Browse files
committed
some tweaks
- make separate summarize_quads function - use quad ID for download file name to avoid hitting download endpoint to determine name from content-disposition headers - add full_extent option for explicitly using the mosaic bbox for listing (rather than defaulting when bbox/geometry not provided) - required bbox or geometry for downloading - minor doc fixes add language for styling code blocks
1 parent a4d35f0 commit 01ba3c4

File tree

4 files changed

+173
-76
lines changed

4 files changed

+173
-76
lines changed

planet/cli/mosaics.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -234,14 +234,16 @@ async def list_quads(ctx, name_or_id, bbox, geometry, summary, pretty, links):
234234
planet mosaics search global_monthly_2025_04_mosaic --bbox -100,40,-100,41
235235
"""
236236
async with client(ctx) as cl:
237-
await _output(
238-
cl.list_quads(name_or_id,
239-
minimal=False,
240-
bbox=bbox,
241-
geometry=geometry,
242-
summary=summary),
243-
pretty,
244-
links)
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)
245247

246248

247249
@command(mosaics, name="download")

planet/clients/mosaics.py

Lines changed: 95 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@
1414

1515
import asyncio
1616
from pathlib import Path
17-
from typing import AsyncIterator, Optional, Tuple, Type, TypeVar, Union, cast
17+
from typing import AsyncIterator, Optional, Sequence, Tuple, Type, TypeVar, Union, cast
1818
from planet.clients.base import _BaseClient
1919
from planet.constants import PLANET_BASE_URL
20-
from planet.exceptions import MissingResource
20+
from planet.exceptions import ClientError, MissingResource
2121
from planet.http import Session
2222
from planet.models import GeoInterface, Mosaic, Paged, Quad, Response, Series, StreamingBody
2323
from uuid import UUID
@@ -28,7 +28,11 @@
2828

2929
Number = Union[int, float]
3030

31-
BBox = Tuple[Number, Number, Number, Number]
31+
BBox = Sequence[Number]
32+
"""BBox is a rectangular area described by 2 corners
33+
where the positional meaning in the sequence is
34+
left, bottom, right, and top, respectively
35+
"""
3236

3337

3438
class _SeriesPage(Paged):
@@ -121,18 +125,16 @@ async def _resolve_mosaic(self, mosaic: Union[Mosaic, str]) -> Mosaic:
121125
async def get_mosaic(self, name_or_id: str) -> Mosaic:
122126
"""Get the API representation of a mosaic by name or id.
123127
124-
:param name str: The name or id of the mosaic
125-
:returns: dict or None (if searching by name)
126-
:raises planet.api.exceptions.APIException: On API error.
128+
Parameters:
129+
name_or_id: The name or id of the mosaic
127130
"""
128131
return Mosaic(await self._get(name_or_id, "mosaics", _MosaicsPage))
129132

130133
async def get_series(self, name_or_id: str) -> Series:
131134
"""Get the API representation of a series by name or id.
132135
133-
:param name str: The name or id of the series
134-
:returns: dict or None (if searching by name)
135-
:raises planet.api.exceptions.APIException: On API error.
136+
Parameters:
137+
name_or_id: The name or id of the mosaic
136138
"""
137139
return Series(await self._get(name_or_id, "series", _SeriesPage))
138140

@@ -148,7 +150,7 @@ async def list_series(
148150
149151
Example:
150152
151-
```
153+
```python
152154
series = await client.list_series()
153155
async for s in series:
154156
print(s)
@@ -184,7 +186,7 @@ async def list_mosaics(
184186
185187
Example:
186188
187-
```
189+
```python
188190
mosaics = await client.list_mosaics()
189191
async for m in mosaics:
190192
print(m)
@@ -221,7 +223,7 @@ async def list_series_mosaics(
221223
222224
Example:
223225
224-
```
226+
```python
225227
mosaics = await client.list_series_mosaics("d5098531-aa4f-4ff9-a9d5-74ad4a6301e5")
226228
async for m in mosaics:
227229
print(m)
@@ -250,26 +252,76 @@ async def list_series_mosaics(
250252
async for item in _MosaicsPage(resp, self._session.request):
251253
yield Mosaic(item)
252254

255+
async def summarize_quads(self,
256+
/,
257+
mosaic: Union[Mosaic, str],
258+
*,
259+
bbox: Optional[BBox] = None,
260+
geometry: Optional[Union[dict, GeoInterface]] = None) -> dict:
261+
"""
262+
Get a summary of a quad list for a mosaic.
263+
264+
If the bbox or geometry is not provided, the entire list is considered.
265+
266+
Examples:
267+
268+
Get the total number of quads in the mosaic.
269+
270+
```python
271+
mosaic = await client.get_mosaic("d5098531-aa4f-4ff9-a9d5-74ad4a6301e5")
272+
summary = await client.summarize_quads(mosaic)
273+
print(summary["total_quads"])
274+
```
275+
"""
276+
resp = await self._list_quads(mosaic, minimal=True, bbox=bbox, geometry=geometry, summary=True)
277+
return resp.json()["summary"]
278+
253279
async def list_quads(self,
254280
/,
255281
mosaic: Union[Mosaic, str],
256282
*,
257283
minimal: bool = False,
284+
full_extent: bool = False,
258285
bbox: Optional[BBox] = None,
259-
geometry: Optional[Union[dict, GeoInterface]] = None,
260-
summary: bool = False) -> AsyncIterator[Quad]:
286+
geometry: Optional[Union[dict, GeoInterface]] = None) -> AsyncIterator[Quad]:
261287
"""
262288
List the a mosaic's quads.
263289
290+
Parameters:
291+
mosaic: the mosaic to list
292+
minimal: if False, response includes full metadata
293+
full_extent: if True, the mosaic's extent will be used to list
294+
bbox: only quads intersecting the bbox will be listed
295+
geometry: only quads intersecting the geometry will be listed
296+
297+
Raises:
298+
ClientError: if `geometry`, `bbox` or `full_extent` is not specified.
299+
264300
Example:
265301
266-
```
302+
List the quad at a single point (note the extent has the same corners)
303+
304+
```python
267305
mosaic = await client.get_mosaic("d5098531-aa4f-4ff9-a9d5-74ad4a6301e5")
268-
quads = await client.list_quads(mosaic)
306+
quads = await client.list_quads(mosaic, bbox=[-100, 40, -100, 40])
269307
async for q in quads:
270308
print(q)
271309
```
272310
"""
311+
if not any((geometry, bbox, full_extent)):
312+
raise ClientError("one of: geometry, bbox, full_extent required")
313+
resp = await self._list_quads(mosaic, minimal=minimal, bbox=bbox, geometry=geometry)
314+
async for item in _QuadsPage(resp, self._session.request):
315+
yield Quad(item)
316+
317+
async def _list_quads(self,
318+
/,
319+
mosaic: Union[Mosaic, str],
320+
*,
321+
minimal: bool = False,
322+
bbox: Optional[BBox] = None,
323+
geometry: Optional[Union[dict, GeoInterface]] = None,
324+
summary: bool = False) -> Response:
273325
mosaic = await self._resolve_mosaic(mosaic)
274326
if geometry:
275327
if isinstance(geometry, GeoInterface):
@@ -288,12 +340,7 @@ async def list_quads(self,
288340
else:
289341
search = bbox
290342
resp = await self._quads_bbox(mosaic, search, minimal, summary)
291-
# kinda yucky - yields a different "shaped" dict
292-
if summary:
293-
yield resp.json()["summary"]
294-
return
295-
async for item in _QuadsPage(resp, self._session.request):
296-
yield Quad(item)
343+
return resp
297344

298345
async def _quads_geometry(self,
299346
mosaic: Mosaic,
@@ -305,6 +352,10 @@ async def _quads_geometry(self,
305352
params["minimal"] = "true"
306353
if summary:
307354
params["summary"] = "true"
355+
# this could be fixed in the API ...
356+
# for a summary, we don't need to get any listings
357+
# zero is ignored, but in case that gets rejected, just use 1
358+
params["_page_size"] = 1
308359
mosaic_id = mosaic["id"]
309360
return await self._session.request(
310361
method="POST",
@@ -338,7 +389,7 @@ async def get_quad(self, mosaic: Union[Mosaic, str], quad_id: str) -> Quad:
338389
339390
Example:
340391
341-
```
392+
```python
342393
quad = await client.get_quad("d5098531-aa4f-4ff9-a9d5-74ad4a6301e5", "1234-5678")
343394
print(quad)
344395
```
@@ -357,7 +408,7 @@ async def get_quad_contributions(self, quad: Quad) -> list[dict]:
357408
358409
Example:
359410
360-
```
411+
```python
361412
quad = await client.get_quad("d5098531-aa4f-4ff9-a9d5-74ad4a6301e5", "1234-5678")
362413
contributions = await client.get_quad_contributions(quad)
363414
print(contributions)
@@ -381,19 +432,26 @@ async def download_quad(self,
381432
382433
Example:
383434
384-
```
435+
```python
385436
quad = await client.get_quad("d5098531-aa4f-4ff9-a9d5-74ad4a6301e5", "1234-5678")
386437
await client.download_quad(quad)
387438
```
388439
"""
389440
url = quad["_links"]["download"]
390441
Path(directory).mkdir(exist_ok=True, parents=True)
442+
dest = Path(directory, quad["id"] + ".tif")
443+
# this avoids a request to the download endpoint which would
444+
# get counted as a download even if only the headers were read
445+
# and the response content is ignored (like if when the file
446+
# exists and overwrite is False)
447+
if dest.exists() and not overwrite:
448+
return
391449
async with self._session.stream(method='GET', url=url) as resp:
392-
body = StreamingBody(resp)
393-
dest = Path(directory, body.name)
394-
await body.write(dest,
395-
overwrite=overwrite,
396-
progress_bar=progress_bar)
450+
await StreamingBody(resp).write(
451+
dest,
452+
# pass along despite our manual handling
453+
overwrite=overwrite,
454+
progress_bar=progress_bar)
397455

398456
async def download_quads(self,
399457
/,
@@ -409,13 +467,18 @@ async def download_quads(self,
409467
"""
410468
Download a mosaics' quads to a directory.
411469
470+
Raises:
471+
ClientError: if `geometry` or `bbox` is not specified.
472+
412473
Example:
413474
414-
```
475+
```python
415476
mosaic = await cl.get_mosaic(name)
416-
client.download_quads(mosaic, bbox=(-100, 40, -100, 41))
477+
client.download_quads(mosaic, bbox=(-100, 40, -100, 40))
417478
```
418479
"""
480+
if not any((bbox, geometry)):
481+
raise ClientError("bbox or geometry is required")
419482
jobs = []
420483
mosaic = await self._resolve_mosaic(mosaic)
421484
directory = directory or mosaic["name"]

0 commit comments

Comments
 (0)