Skip to content

Commit 3f8222e

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 5f17137 commit 3f8222e

File tree

4 files changed

+212
-104
lines changed

4 files changed

+212
-104
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: 119 additions & 46 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, 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,86 @@ async def list_series_mosaics(
250252
async for item in _MosaicsPage(resp, self._session.request):
251253
yield Mosaic(item)
252254

253-
async def list_quads(self,
254-
/,
255-
mosaic: Union[Mosaic, str],
256-
*,
257-
minimal: bool = False,
258-
bbox: Optional[BBox] = None,
259-
geometry: Optional[Union[dict, GeoInterface]] = None,
260-
summary: bool = False) -> AsyncIterator[Quad]:
255+
async def summarize_quads(
256+
self,
257+
/,
258+
mosaic: Union[Mosaic, str],
259+
*,
260+
bbox: Optional[BBox] = None,
261+
geometry: Optional[Union[dict, GeoInterface]] = None) -> dict:
262+
"""
263+
Get a summary of a quad list for a mosaic.
264+
265+
If the bbox or geometry is not provided, the entire list is considered.
266+
267+
Examples:
268+
269+
Get the total number of quads in the mosaic.
270+
271+
```python
272+
mosaic = await client.get_mosaic("d5098531-aa4f-4ff9-a9d5-74ad4a6301e5")
273+
summary = await client.summarize_quads(mosaic)
274+
print(summary["total_quads"])
275+
```
276+
"""
277+
resp = await self._list_quads(mosaic,
278+
minimal=True,
279+
bbox=bbox,
280+
geometry=geometry,
281+
summary=True)
282+
return resp.json()["summary"]
283+
284+
async def list_quads(
285+
self,
286+
/,
287+
mosaic: Union[Mosaic, str],
288+
*,
289+
minimal: bool = False,
290+
full_extent: bool = False,
291+
bbox: Optional[BBox] = None,
292+
geometry: Optional[Union[dict, GeoInterface]] = None
293+
) -> AsyncIterator[Quad]:
261294
"""
262295
List the a mosaic's quads.
263296
297+
Parameters:
298+
mosaic: the mosaic to list
299+
minimal: if False, response includes full metadata
300+
full_extent: if True, the mosaic's extent will be used to list
301+
bbox: only quads intersecting the bbox will be listed
302+
geometry: only quads intersecting the geometry will be listed
303+
304+
Raises:
305+
ClientError: if `geometry`, `bbox` or `full_extent` is not specified.
306+
264307
Example:
265308
266-
```
309+
List the quad at a single point (note the extent has the same corners)
310+
311+
```python
267312
mosaic = await client.get_mosaic("d5098531-aa4f-4ff9-a9d5-74ad4a6301e5")
268-
quads = await client.list_quads(mosaic)
313+
quads = await client.list_quads(mosaic, bbox=[-100, 40, -100, 40])
269314
async for q in quads:
270315
print(q)
271316
```
272317
"""
318+
if not any((geometry, bbox, full_extent)):
319+
raise ClientError("one of: geometry, bbox, full_extent required")
320+
resp = await self._list_quads(mosaic,
321+
minimal=minimal,
322+
bbox=bbox,
323+
geometry=geometry)
324+
async for item in _QuadsPage(resp, self._session.request):
325+
yield Quad(item)
326+
327+
async def _list_quads(self,
328+
/,
329+
mosaic: Union[Mosaic, str],
330+
*,
331+
minimal: bool = False,
332+
bbox: Optional[BBox] = None,
333+
geometry: Optional[Union[dict, GeoInterface]] = None,
334+
summary: bool = False) -> Response:
273335
mosaic = await self._resolve_mosaic(mosaic)
274336
if geometry:
275337
if isinstance(geometry, GeoInterface):
@@ -279,21 +341,16 @@ async def list_quads(self,
279341
minimal,
280342
summary)
281343
else:
282-
if bbox is None:
344+
if not bbox:
283345
xmin, ymin, xmax, ymax = cast(BBox, mosaic['bbox'])
284-
search = (max(-180, xmin),
285-
max(-85, ymin),
286-
min(180, xmax),
287-
min(85, ymax))
288-
else:
289-
search = bbox
290-
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)
346+
bbox = [
347+
max(-180, xmin),
348+
max(-85, ymin),
349+
min(180, xmax),
350+
min(85, ymax)
351+
]
352+
resp = await self._quads_bbox(mosaic, bbox, minimal, summary)
353+
return resp
297354

298355
async def _quads_geometry(self,
299356
mosaic: Mosaic,
@@ -305,6 +362,10 @@ async def _quads_geometry(self,
305362
params["minimal"] = "true"
306363
if summary:
307364
params["summary"] = "true"
365+
# this could be fixed in the API ...
366+
# for a summary, we don't need to get any listings
367+
# zero is ignored, but in case that gets rejected, just use 1
368+
params["_page_size"] = "1"
308369
mosaic_id = mosaic["id"]
309370
return await self._session.request(
310371
method="POST",
@@ -338,7 +399,7 @@ async def get_quad(self, mosaic: Union[Mosaic, str], quad_id: str) -> Quad:
338399
339400
Example:
340401
341-
```
402+
```python
342403
quad = await client.get_quad("d5098531-aa4f-4ff9-a9d5-74ad4a6301e5", "1234-5678")
343404
print(quad)
344405
```
@@ -357,7 +418,7 @@ async def get_quad_contributions(self, quad: Quad) -> list[dict]:
357418
358419
Example:
359420
360-
```
421+
```python
361422
quad = await client.get_quad("d5098531-aa4f-4ff9-a9d5-74ad4a6301e5", "1234-5678")
362423
contributions = await client.get_quad_contributions(quad)
363424
print(contributions)
@@ -381,19 +442,26 @@ async def download_quad(self,
381442
382443
Example:
383444
384-
```
445+
```python
385446
quad = await client.get_quad("d5098531-aa4f-4ff9-a9d5-74ad4a6301e5", "1234-5678")
386447
await client.download_quad(quad)
387448
```
388449
"""
389450
url = quad["_links"]["download"]
390451
Path(directory).mkdir(exist_ok=True, parents=True)
452+
dest = Path(directory, quad["id"] + ".tif")
453+
# this avoids a request to the download endpoint which would
454+
# get counted as a download even if only the headers were read
455+
# and the response content is ignored (like if when the file
456+
# exists and overwrite is False)
457+
if dest.exists() and not overwrite:
458+
return
391459
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)
460+
await StreamingBody(resp).write(
461+
dest,
462+
# pass along despite our manual handling
463+
overwrite=overwrite,
464+
progress_bar=progress_bar)
397465

398466
async def download_quads(self,
399467
/,
@@ -409,13 +477,18 @@ async def download_quads(self,
409477
"""
410478
Download a mosaics' quads to a directory.
411479
480+
Raises:
481+
ClientError: if `geometry` or `bbox` is not specified.
482+
412483
Example:
413484
414-
```
485+
```python
415486
mosaic = await cl.get_mosaic(name)
416-
client.download_quads(mosaic, bbox=(-100, 40, -100, 41))
487+
client.download_quads(mosaic, bbox=(-100, 40, -100, 40))
417488
```
418489
"""
490+
if not any((bbox, geometry)):
491+
raise ClientError("bbox or geometry is required")
419492
jobs = []
420493
mosaic = await self._resolve_mosaic(mosaic)
421494
directory = directory or mosaic["name"]

0 commit comments

Comments
 (0)