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 , Tuple , 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,76 @@ async def list_series_mosaics(
250
252
async for item in _MosaicsPage (resp , self ._session .request ):
251
253
yield Mosaic (item )
252
254
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
+
253
279
async def list_quads (self ,
254
280
/ ,
255
281
mosaic : Union [Mosaic , str ],
256
282
* ,
257
283
minimal : bool = False ,
284
+ full_extent : bool = False ,
258
285
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 ]:
261
287
"""
262
288
List the a mosaic's quads.
263
289
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
+
264
300
Example:
265
301
266
- ```
302
+ List the quad at a single point (note the extent has the same corners)
303
+
304
+ ```python
267
305
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] )
269
307
async for q in quads:
270
308
print(q)
271
309
```
272
310
"""
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 :
273
325
mosaic = await self ._resolve_mosaic (mosaic )
274
326
if geometry :
275
327
if isinstance (geometry , GeoInterface ):
@@ -288,12 +340,7 @@ async def list_quads(self,
288
340
else :
289
341
search = bbox
290
342
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
297
344
298
345
async def _quads_geometry (self ,
299
346
mosaic : Mosaic ,
@@ -305,6 +352,10 @@ async def _quads_geometry(self,
305
352
params ["minimal" ] = "true"
306
353
if summary :
307
354
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
308
359
mosaic_id = mosaic ["id" ]
309
360
return await self ._session .request (
310
361
method = "POST" ,
@@ -338,7 +389,7 @@ async def get_quad(self, mosaic: Union[Mosaic, str], quad_id: str) -> Quad:
338
389
339
390
Example:
340
391
341
- ```
392
+ ```python
342
393
quad = await client.get_quad("d5098531-aa4f-4ff9-a9d5-74ad4a6301e5", "1234-5678")
343
394
print(quad)
344
395
```
@@ -357,7 +408,7 @@ async def get_quad_contributions(self, quad: Quad) -> list[dict]:
357
408
358
409
Example:
359
410
360
- ```
411
+ ```python
361
412
quad = await client.get_quad("d5098531-aa4f-4ff9-a9d5-74ad4a6301e5", "1234-5678")
362
413
contributions = await client.get_quad_contributions(quad)
363
414
print(contributions)
@@ -381,19 +432,26 @@ async def download_quad(self,
381
432
382
433
Example:
383
434
384
- ```
435
+ ```python
385
436
quad = await client.get_quad("d5098531-aa4f-4ff9-a9d5-74ad4a6301e5", "1234-5678")
386
437
await client.download_quad(quad)
387
438
```
388
439
"""
389
440
url = quad ["_links" ]["download" ]
390
441
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
391
449
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 )
397
455
398
456
async def download_quads (self ,
399
457
/ ,
@@ -409,13 +467,18 @@ async def download_quads(self,
409
467
"""
410
468
Download a mosaics' quads to a directory.
411
469
470
+ Raises:
471
+ ClientError: if `geometry` or `bbox` is not specified.
472
+
412
473
Example:
413
474
414
- ```
475
+ ```python
415
476
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 ))
417
478
```
418
479
"""
480
+ if not any ((bbox , geometry )):
481
+ raise ClientError ("bbox or geometry is required" )
419
482
jobs = []
420
483
mosaic = await self ._resolve_mosaic (mosaic )
421
484
directory = directory or mosaic ["name" ]
0 commit comments