kleinkram 0.44.0.dev20250409080103__py3-none-any.whl → 0.44.1.dev20250409111712__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of kleinkram might be problematic. Click here for more details.

@@ -17,6 +17,7 @@ from uuid import UUID
17
17
  import boto3.s3.transfer
18
18
  import botocore.config
19
19
  import httpx
20
+ from httpx import RemoteProtocolError
20
21
  from rich.console import Console
21
22
  from tqdm import tqdm
22
23
 
@@ -44,6 +45,9 @@ MAX_UPLOAD_RETRIES = 3
44
45
  S3_MAX_RETRIES = 60 # same as frontend
45
46
  S3_READ_TIMEOUT = 60 * 5 # 5 minutes
46
47
 
48
+ RETRY_BACKOFF_BASE = 2 # exponential backoff base
49
+ MAX_RETRIES = 5
50
+
47
51
 
48
52
  class UploadCredentials(NamedTuple):
49
53
  access_key: str
@@ -264,23 +268,69 @@ def _get_file_download(client: AuthenticatedClient, id: UUID) -> str:
264
268
  def _url_download(
265
269
  url: str, *, path: Path, size: int, overwrite: bool = False, verbose: bool = False
266
270
  ) -> None:
267
- if path.exists() and not overwrite:
268
- raise FileExistsError(f"file already exists: {path}")
269
-
270
- with httpx.stream("GET", url, timeout=S3_READ_TIMEOUT) as response:
271
- response.raise_for_status()
272
- with open(path, "wb") as f:
273
- with tqdm(
274
- total=size,
275
- desc=f"downloading {path.name}",
276
- unit="B",
277
- unit_scale=True,
278
- leave=False,
279
- disable=not verbose,
280
- ) as pbar:
281
- for chunk in response.iter_bytes(chunk_size=DOWNLOAD_CHUNK_SIZE):
282
- f.write(chunk)
283
- pbar.update(len(chunk))
271
+ if path.exists():
272
+ if overwrite:
273
+ path.unlink()
274
+ downloaded = 0
275
+ else:
276
+ downloaded = path.stat().st_size
277
+ if downloaded >= size:
278
+ raise FileExistsError(f"file already exists and is complete: {path}")
279
+ else:
280
+ downloaded = 0
281
+
282
+ attempt = 0
283
+ while downloaded < size:
284
+ try:
285
+ headers = {"Range": f"bytes={downloaded}-"}
286
+ with httpx.stream(
287
+ "GET", url, headers=headers, timeout=S3_READ_TIMEOUT
288
+ ) as response:
289
+ # Accept both 206 Partial Content and 200 OK if starting from 0
290
+ if not (
291
+ response.status_code == 206
292
+ or (downloaded == 0 and response.status_code == 200)
293
+ ):
294
+ response.raise_for_status()
295
+ raise RuntimeError(
296
+ f"Expected 206 Partial Content, got {response.status_code}"
297
+ )
298
+
299
+ mode = "ab" if downloaded > 0 else "wb"
300
+ with open(path, mode) as f:
301
+ with tqdm(
302
+ total=size,
303
+ initial=downloaded,
304
+ desc=f"downloading {path.name}",
305
+ unit="B",
306
+ unit_scale=True,
307
+ leave=False,
308
+ disable=not verbose,
309
+ ) as pbar:
310
+ for chunk in response.iter_bytes(
311
+ chunk_size=DOWNLOAD_CHUNK_SIZE
312
+ ):
313
+ attempt = 0 # reset attempt counter on successful download of non-empty chunk
314
+ if not chunk:
315
+ break
316
+ f.write(chunk)
317
+ downloaded += len(chunk)
318
+ pbar.update(len(chunk))
319
+ break # download complete
320
+ except RemoteProtocolError as e:
321
+ logger.info(f"Error: {e}, retrying...")
322
+ attempt += 1
323
+ if attempt > MAX_RETRIES:
324
+ raise RuntimeError(
325
+ f"Download failed after {MAX_RETRIES} retries due to RemoteProtocolError"
326
+ ) from e
327
+ if verbose:
328
+ print(
329
+ f"RemoteProtocolError on attempt {attempt}/{MAX_RETRIES}, retrying after backoff..."
330
+ )
331
+ sleep(RETRY_BACKOFF_BASE**attempt)
332
+ except Exception as e:
333
+ raise RuntimeError(f"Failed to download: {e}") from e
284
334
 
285
335
 
286
336
  class DownloadState(Enum):
@@ -320,7 +370,7 @@ def download_file(
320
370
 
321
371
  elif verbose:
322
372
  tqdm.write(
323
- styled_string(f"overwriting {path}, hash missmatch", style="yellow")
373
+ styled_string(f"overwriting {path}, hash mismatch", style="yellow")
324
374
  )
325
375
 
326
376
  elif not overwrite and file.size is not None:
@@ -328,9 +378,7 @@ def download_file(
328
378
 
329
379
  elif verbose:
330
380
  tqdm.write(
331
- styled_string(
332
- f"overwriting {path}, file size missmatch", style="yellow"
333
- )
381
+ styled_string(f"overwriting {path}, file size mismatch", style="yellow")
334
382
  )
335
383
 
336
384
  # request a download url
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kleinkram
3
- Version: 0.44.0.dev20250409080103
3
+ Version: 0.44.1.dev20250409111712
4
4
  Summary: give me your bags
5
5
  Author: Cyrill Püntener, Dominique Garmier, Johann Schwabe
6
6
  Author-email: pucyril@ethz.ch, dgarmier@ethz.ch, jschwab@ethz.ch
@@ -15,7 +15,7 @@ kleinkram/wrappers.py,sha256=ZScoEov5Q6D2rvaJJ8E-4f58P_NGWrGc9mRPYxSqOC0,13127
15
15
  kleinkram/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
16
  kleinkram/api/client.py,sha256=VwuT97_WdbDpcVGwMXB0fRnUoQnUSf7BOP5eXUFokfI,5932
17
17
  kleinkram/api/deser.py,sha256=xRpYUFKZ0Luoo7XyAtYblJvprmpjNSZOiFVnFKmOzcM,4819
18
- kleinkram/api/file_transfer.py,sha256=GYO5CaTQjpSjNg3Nc28kavqfdEy2pgPRkPsnPDSFUPw,18093
18
+ kleinkram/api/file_transfer.py,sha256=9UMEBZP7IV75IHg_tBEnBMQTsT_SIBjG0HDHijAfXco,20094
19
19
  kleinkram/api/pagination.py,sha256=P_zPsBKlMWkmAv-YfUNHaGW-XLB_4U8BDMrKyiDFIXk,1370
20
20
  kleinkram/api/query.py,sha256=9Exi4hJR7Ml38_zjAcOvSEoIAxZLlpM6QwwzO9fs5Gk,3293
21
21
  kleinkram/api/routes.py,sha256=x0IfzQO5RAC3rohDSpdUNlVOWl221gh4e0fbMKfGrQA,12251
@@ -43,8 +43,8 @@ tests/test_printing.py,sha256=Jz1AjqmqBRjp1JLm6H1oVJyvGaMPlahVXdKnd7UDQFc,2231
43
43
  tests/test_query.py,sha256=fExmCKXLA7-9j2S2sF_sbvRX_2s6Cp3a7OTcqE25q9g,3864
44
44
  tests/test_utils.py,sha256=eUBYrn3xrcgcaxm1X4fqZaX4tRvkbI6rh6BUbNbu9T0,4784
45
45
  tests/test_wrappers.py,sha256=TbcTyO2L7fslbzgfDdcVZkencxNQ8cGPZm_iB6c9d6Q,2673
46
- kleinkram-0.44.0.dev20250409080103.dist-info/METADATA,sha256=slvYitYbGRQnyaZ7BweL9HMUpcMWQUbWgcUdNYeqIJU,2825
47
- kleinkram-0.44.0.dev20250409080103.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
48
- kleinkram-0.44.0.dev20250409080103.dist-info/entry_points.txt,sha256=SaB2l5aqhSr8gmaMw2kvQU90a8Bnl7PedU8cWYxkfYo,46
49
- kleinkram-0.44.0.dev20250409080103.dist-info/top_level.txt,sha256=N3-sJagEHu1Tk1X6Dx1X1q0pLDNbDZpLzRxVftvepds,24
50
- kleinkram-0.44.0.dev20250409080103.dist-info/RECORD,,
46
+ kleinkram-0.44.1.dev20250409111712.dist-info/METADATA,sha256=2IKtDT39yl-yI4fvJqCsR2oSfLXtAtBp4As7vMQauAE,2825
47
+ kleinkram-0.44.1.dev20250409111712.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
48
+ kleinkram-0.44.1.dev20250409111712.dist-info/entry_points.txt,sha256=SaB2l5aqhSr8gmaMw2kvQU90a8Bnl7PedU8cWYxkfYo,46
49
+ kleinkram-0.44.1.dev20250409111712.dist-info/top_level.txt,sha256=N3-sJagEHu1Tk1X6Dx1X1q0pLDNbDZpLzRxVftvepds,24
50
+ kleinkram-0.44.1.dev20250409111712.dist-info/RECORD,,