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 , Tuple , 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,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