1414
1515import asyncio
1616from 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
1818from planet .clients .base import _BaseClient
1919from planet .constants import PLANET_BASE_URL
20- from planet .exceptions import MissingResource
20+ from planet .exceptions import ClientError , MissingResource
2121from planet .http import Session
2222from planet .models import GeoInterface , Mosaic , Paged , Quad , Response , Series , StreamingBody
2323from uuid import UUID
2828
2929Number = 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
3438class _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