14
14
# the License.
15
15
"""Functionality for interacting with the orders api"""
16
16
import asyncio
17
+ import hashlib
18
+ import json
17
19
import logging
18
- from pathlib import Path
20
+ import re
19
21
import time
20
- from typing import AsyncIterator , Callable , List , Optional
21
22
import uuid
22
- import json
23
- import hashlib
23
+ from pathlib import Path
24
+ from typing import AsyncIterator , Callable , List , Optional
24
25
26
+ import stamina
25
27
from tqdm .asyncio import tqdm
26
28
27
29
from .. import exceptions
28
30
from ..constants import PLANET_BASE_URL
29
- from ..http import Session
31
+ from ..http import RETRY_EXCEPTIONS , Session
30
32
from ..models import Paged
31
33
32
34
BASE_URL = f'{ PLANET_BASE_URL } /compute/ops'
@@ -232,6 +234,7 @@ async def aggregated_order_stats(self) -> dict:
232
234
response = await self ._session .request (method = 'GET' , url = url )
233
235
return response .json ()
234
236
237
+ @stamina .retry (on = tuple (RETRY_EXCEPTIONS ))
235
238
async def download_asset (self ,
236
239
location : str ,
237
240
filename : Optional [str ] = None ,
@@ -257,31 +260,49 @@ async def download_asset(self,
257
260
limit is exceeded.
258
261
259
262
"""
260
- response = await self ._session .request (method = 'GET' , url = location )
261
- filename = filename or response .filename
262
- length = response .length
263
- if not filename :
264
- raise exceptions .ClientError (
265
- f'Could not determine filename at { location } ' )
263
+ async with self ._session ._limiter , self ._session ._client .stream ('GET' , location ) as resp :
264
+ content_length = int (resp .headers .get ('content-length' ))
265
+
266
+ # Fall back to content-disposition for a filename.
267
+ if not filename :
268
+ try :
269
+ content_disposition = resp .headers ['content-disposition' ]
270
+ match = re .search ('filename="?([^"]+)"?' ,
271
+ content_disposition )
272
+ filename = match .group (1 ) # type: ignore
273
+ except (AttributeError , KeyError ) as err :
274
+ raise exceptions .ClientError (
275
+ f'Could not determine filename at { location } ' ) from err
276
+
277
+ dl_path = Path (directory , filename )
278
+ dl_path .parent .mkdir (exist_ok = True , parents = True )
279
+ LOGGER .info (f'Downloading { dl_path } ' )
266
280
267
- dl_path = Path (directory , filename )
268
- dl_path .parent .mkdir (exist_ok = True , parents = True )
269
- LOGGER .info (f'Downloading { dl_path } ' )
270
-
271
- try :
272
- mode = 'wb' if overwrite else 'xb'
273
- with open (dl_path , mode ) as fp :
274
- with tqdm (total = length ,
275
- unit_scale = True ,
276
- unit_divisor = 1024 * 1024 ,
277
- unit = 'B' ,
278
- desc = str (filename ),
279
- disable = not progress_bar ) as progress :
280
- await self ._session .write (location , fp , progress .update )
281
- except FileExistsError :
282
- LOGGER .info (f'File { dl_path } exists, not overwriting' )
283
-
284
- return dl_path
281
+ try :
282
+ mode = 'wb' if overwrite else 'xb'
283
+ with dl_path .open (mode ) as fp :
284
+ with tqdm (total = content_length ,
285
+ unit_scale = True ,
286
+ unit_divisor = 1024 * 1024 ,
287
+ unit = 'B' ,
288
+ desc = str (filename ),
289
+ disable = not progress_bar ) as progress :
290
+
291
+ previous = resp .num_bytes_downloaded
292
+
293
+ # Size from boto3.s3.transfer.TransferConfig
294
+ # io_chunksize. Planet assets are generally
295
+ # several MB or more.
296
+ async for chunk in resp .aiter_bytes (chunk_size = 262144 ):
297
+ fp .write (chunk )
298
+ current = resp .num_bytes_downloaded
299
+ progress .update (current - previous )
300
+ previous = current
301
+
302
+ except FileExistsError :
303
+ LOGGER .info (f'File { dl_path } exists, not overwriting' )
304
+
305
+ return dl_path
285
306
286
307
async def download_order (self ,
287
308
order_id : str ,
0 commit comments