14
14
15
15
import asyncio
16
16
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
18
18
from planet .clients .base import _BaseClient
19
19
from planet .constants import PLANET_BASE_URL
20
- from planet .exceptions import MissingResource
20
+ from planet .exceptions import ClientError , MissingResource
21
21
from planet .http import Session
22
22
from planet .models import GeoInterface , Mosaic , Paged , Quad , Response , Series , StreamingBody
23
23
from uuid import UUID
28
28
29
29
Number = Union [int , float ]
30
30
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
+ """
32
36
33
37
34
38
class _SeriesPage (Paged ):
@@ -121,18 +125,16 @@ async def _resolve_mosaic(self, mosaic: Union[Mosaic, str]) -> Mosaic:
121
125
async def get_mosaic (self , name_or_id : str ) -> Mosaic :
122
126
"""Get the API representation of a mosaic by name or id.
123
127
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
127
130
"""
128
131
return Mosaic (await self ._get (name_or_id , "mosaics" , _MosaicsPage ))
129
132
130
133
async def get_series (self , name_or_id : str ) -> Series :
131
134
"""Get the API representation of a series by name or id.
132
135
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
136
138
"""
137
139
return Series (await self ._get (name_or_id , "series" , _SeriesPage ))
138
140
@@ -148,7 +150,7 @@ async def list_series(
148
150
149
151
Example:
150
152
151
- ```
153
+ ```python
152
154
series = await client.list_series()
153
155
async for s in series:
154
156
print(s)
@@ -184,7 +186,7 @@ async def list_mosaics(
184
186
185
187
Example:
186
188
187
- ```
189
+ ```python
188
190
mosaics = await client.list_mosaics()
189
191
async for m in mosaics:
190
192
print(m)
@@ -221,7 +223,7 @@ async def list_series_mosaics(
221
223
222
224
Example:
223
225
224
- ```
226
+ ```python
225
227
mosaics = await client.list_series_mosaics("d5098531-aa4f-4ff9-a9d5-74ad4a6301e5")
226
228
async for m in mosaics:
227
229
print(m)
@@ -250,26 +252,86 @@ async def list_series_mosaics(
250
252
async for item in _MosaicsPage (resp , self ._session .request ):
251
253
yield Mosaic (item )
252
254
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 ]:
261
294
"""
262
295
List the a mosaic's quads.
263
296
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
+
264
307
Example:
265
308
266
- ```
309
+ List the quad at a single point (note the extent has the same corners)
310
+
311
+ ```python
267
312
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] )
269
314
async for q in quads:
270
315
print(q)
271
316
```
272
317
"""
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 :
273
335
mosaic = await self ._resolve_mosaic (mosaic )
274
336
if geometry :
275
337
if isinstance (geometry , GeoInterface ):
@@ -279,21 +341,16 @@ async def list_quads(self,
279
341
minimal ,
280
342
summary )
281
343
else :
282
- if bbox is None :
344
+ if not bbox :
283
345
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
297
354
298
355
async def _quads_geometry (self ,
299
356
mosaic : Mosaic ,
@@ -305,6 +362,10 @@ async def _quads_geometry(self,
305
362
params ["minimal" ] = "true"
306
363
if summary :
307
364
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"
308
369
mosaic_id = mosaic ["id" ]
309
370
return await self ._session .request (
310
371
method = "POST" ,
@@ -338,7 +399,7 @@ async def get_quad(self, mosaic: Union[Mosaic, str], quad_id: str) -> Quad:
338
399
339
400
Example:
340
401
341
- ```
402
+ ```python
342
403
quad = await client.get_quad("d5098531-aa4f-4ff9-a9d5-74ad4a6301e5", "1234-5678")
343
404
print(quad)
344
405
```
@@ -357,7 +418,7 @@ async def get_quad_contributions(self, quad: Quad) -> list[dict]:
357
418
358
419
Example:
359
420
360
- ```
421
+ ```python
361
422
quad = await client.get_quad("d5098531-aa4f-4ff9-a9d5-74ad4a6301e5", "1234-5678")
362
423
contributions = await client.get_quad_contributions(quad)
363
424
print(contributions)
@@ -381,19 +442,26 @@ async def download_quad(self,
381
442
382
443
Example:
383
444
384
- ```
445
+ ```python
385
446
quad = await client.get_quad("d5098531-aa4f-4ff9-a9d5-74ad4a6301e5", "1234-5678")
386
447
await client.download_quad(quad)
387
448
```
388
449
"""
389
450
url = quad ["_links" ]["download" ]
390
451
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
391
459
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 )
397
465
398
466
async def download_quads (self ,
399
467
/ ,
@@ -409,13 +477,18 @@ async def download_quads(self,
409
477
"""
410
478
Download a mosaics' quads to a directory.
411
479
480
+ Raises:
481
+ ClientError: if `geometry` or `bbox` is not specified.
482
+
412
483
Example:
413
484
414
- ```
485
+ ```python
415
486
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 ))
417
488
```
418
489
"""
490
+ if not any ((bbox , geometry )):
491
+ raise ClientError ("bbox or geometry is required" )
419
492
jobs = []
420
493
mosaic = await self ._resolve_mosaic (mosaic )
421
494
directory = directory or mosaic ["name" ]
0 commit comments