kleinkram 0.44.0.dev20250409080103__py3-none-any.whl → 0.44.1__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.

@@ -44,6 +44,9 @@ MAX_UPLOAD_RETRIES = 3
44
44
  S3_MAX_RETRIES = 60 # same as frontend
45
45
  S3_READ_TIMEOUT = 60 * 5 # 5 minutes
46
46
 
47
+ RETRY_BACKOFF_BASE = 2 # exponential backoff base
48
+ MAX_RETRIES = 5
49
+
47
50
 
48
51
  class UploadCredentials(NamedTuple):
49
52
  access_key: str
@@ -264,23 +267,67 @@ def _get_file_download(client: AuthenticatedClient, id: UUID) -> str:
264
267
  def _url_download(
265
268
  url: str, *, path: Path, size: int, overwrite: bool = False, verbose: bool = False
266
269
  ) -> 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))
270
+ if path.exists():
271
+ if overwrite:
272
+ path.unlink()
273
+ downloaded = 0
274
+ else:
275
+ downloaded = path.stat().st_size
276
+ if downloaded >= size:
277
+ raise FileExistsError(f"file already exists and is complete: {path}")
278
+ else:
279
+ downloaded = 0
280
+
281
+ attempt = 0
282
+ while downloaded < size:
283
+ try:
284
+ headers = {"Range": f"bytes={downloaded}-"}
285
+ with httpx.stream(
286
+ "GET", url, headers=headers, timeout=S3_READ_TIMEOUT
287
+ ) as response:
288
+ # Accept both 206 Partial Content and 200 OK if starting from 0
289
+ if not (
290
+ response.status_code == 206
291
+ or (downloaded == 0 and response.status_code == 200)
292
+ ):
293
+ response.raise_for_status()
294
+ raise RuntimeError(
295
+ f"Expected 206 Partial Content, got {response.status_code}"
296
+ )
297
+
298
+ mode = "ab" if downloaded > 0 else "wb"
299
+ with open(path, mode) as f:
300
+ with tqdm(
301
+ total=size,
302
+ initial=downloaded,
303
+ desc=f"downloading {path.name}",
304
+ unit="B",
305
+ unit_scale=True,
306
+ leave=False,
307
+ disable=not verbose,
308
+ ) as pbar:
309
+ for chunk in response.iter_bytes(
310
+ chunk_size=DOWNLOAD_CHUNK_SIZE
311
+ ):
312
+ attempt = 0 # reset attempt counter on successful download of non-empty chunk
313
+ if not chunk:
314
+ break
315
+ f.write(chunk)
316
+ downloaded += len(chunk)
317
+ pbar.update(len(chunk))
318
+ break # download complete
319
+ except Exception as e:
320
+ logger.info(f"Error: {e}, retrying...")
321
+ attempt += 1
322
+ if attempt > MAX_RETRIES:
323
+ raise RuntimeError(
324
+ f"Download failed after {MAX_RETRIES} retries due to {e}"
325
+ ) from e
326
+ if verbose:
327
+ print(
328
+ f"{e} on attempt {attempt}/{MAX_RETRIES}, retrying after backoff..."
329
+ )
330
+ sleep(RETRY_BACKOFF_BASE**attempt)
284
331
 
285
332
 
286
333
  class DownloadState(Enum):
@@ -320,7 +367,7 @@ def download_file(
320
367
 
321
368
  elif verbose:
322
369
  tqdm.write(
323
- styled_string(f"overwriting {path}, hash missmatch", style="yellow")
370
+ styled_string(f"overwriting {path}, hash mismatch", style="yellow")
324
371
  )
325
372
 
326
373
  elif not overwrite and file.size is not None:
@@ -328,9 +375,7 @@ def download_file(
328
375
 
329
376
  elif verbose:
330
377
  tqdm.write(
331
- styled_string(
332
- f"overwriting {path}, file size missmatch", style="yellow"
333
- )
378
+ styled_string(f"overwriting {path}, file size mismatch", style="yellow")
334
379
  )
335
380
 
336
381
  # request a download url
kleinkram/printing.py CHANGED
@@ -85,8 +85,8 @@ def format_bytes(size: int) -> str:
85
85
  index = 0
86
86
 
87
87
  fsize: float = size
88
- while fsize >= 1024 and index < len(units) - 1:
89
- fsize /= 1024.0
88
+ while fsize >= 1000 and index < len(units) - 1:
89
+ fsize /= 1000.0
90
90
  index += 1
91
91
 
92
92
  # Format to 2 decimal places if needed
kleinkram/utils.py CHANGED
@@ -229,10 +229,10 @@ def format_bytes(size_bytes: int | float, speed: bool = False) -> str:
229
229
  return "0 B/s" if speed else "0 B"
230
230
 
231
231
  units = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]
232
- power = math.floor(math.log(size_bytes, 1024))
232
+ power = math.floor(math.log(size_bytes, 1000))
233
233
  unit_index = min(power, len(units) - 1)
234
234
 
235
- value = size_bytes / (1024**unit_index)
235
+ value = size_bytes / (1000**unit_index)
236
236
 
237
237
  unit_suffix = "/s" if speed else ""
238
238
  return f"{value:.2f} {units[unit_index]}{unit_suffix}"
@@ -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
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
@@ -7,15 +7,15 @@ kleinkram/core.py,sha256=COJcUx8tP6EQIyg4OiVprJfQUCMbiq2QGB9xZ2CPUCo,9621
7
7
  kleinkram/errors.py,sha256=qa98YvhDbLqX60P8bcMcFmHy4HxgYNlSROXud8Kj-P4,965
8
8
  kleinkram/main.py,sha256=BTE0mZN__xd46wBhFi6iBlK9eGGQvJ1LdUMsbnysLi0,172
9
9
  kleinkram/models.py,sha256=MkWdGIWuCSnyg45DbdEUhtGIprwPYwKppYumHkRAS18,1870
10
- kleinkram/printing.py,sha256=q29iYGkBUCmeJNzNacLHRP-8BqF2UhoXirwX7ja7a1k,12212
10
+ kleinkram/printing.py,sha256=gVLQPmAhyO1Fc5kKLBC77KvoEguBaAReTU3h-QQCx58,12212
11
11
  kleinkram/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
12
  kleinkram/types.py,sha256=nfDjj8TB1Jn5vqO0Xg6qhLOuKom9DDhe62BrngqnVGM,185
13
- kleinkram/utils.py,sha256=AsKZxEGStn03L2tqqiMVCQCrDyl8HhwOfpa3no4dfYc,6767
13
+ kleinkram/utils.py,sha256=lNtmjKTZj4ANndERqfL3QD8VI2B4ZavdW66lxNDz_IY,6767
14
14
  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=eHJlK1FY0hHBmFHAves_p49hi4Mmpdjy1NNLeyLvK0w,19917
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
@@ -39,12 +39,12 @@ tests/test_core.py,sha256=tVvmC4yBkE00VspoowhfdNAUiBsmkIJQodaC3jjUqqM,5623
39
39
  tests/test_end_to_end.py,sha256=0W5pUES5hek-pXq4NZtpPZqKTORkGCRsDv5_D3rDMjY,3372
40
40
  tests/test_error_handling.py,sha256=qPSMKF1qsAHyUME0-krxbIrk38iGKkhAyAah-KwN4NE,1300
41
41
  tests/test_fixtures.py,sha256=UlPmGbEsGvrDPsaStGMRjNvrVPGjCqOB0RMfLJq2VRA,1071
42
- tests/test_printing.py,sha256=Jz1AjqmqBRjp1JLm6H1oVJyvGaMPlahVXdKnd7UDQFc,2231
42
+ tests/test_printing.py,sha256=kPzpIQOtQJ9yQ32mM8cMGDVOGsbrZZLQhfsXN1Pe68Q,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.dist-info/METADATA,sha256=yX6R8JZh8xG0FcfjE8fUaKOAfPoAxbg4TV_UN6k73ac,2807
47
+ kleinkram-0.44.1.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
48
+ kleinkram-0.44.1.dist-info/entry_points.txt,sha256=SaB2l5aqhSr8gmaMw2kvQU90a8Bnl7PedU8cWYxkfYo,46
49
+ kleinkram-0.44.1.dist-info/top_level.txt,sha256=N3-sJagEHu1Tk1X6Dx1X1q0pLDNbDZpLzRxVftvepds,24
50
+ kleinkram-0.44.1.dist-info/RECORD,,
tests/test_printing.py CHANGED
@@ -15,14 +15,14 @@ from kleinkram.printing import parse_metadata_value
15
15
  def test_format_bytes():
16
16
  assert format_bytes(0) == "0 B"
17
17
  assert format_bytes(1) == "1 B"
18
- assert format_bytes(1000) == "1000 B"
19
- assert format_bytes(1024) == "1.00 KB"
20
- assert format_bytes(1025) == "1.00 KB"
21
- assert format_bytes(2048) == "2.00 KB"
22
- assert format_bytes(2**20) == "1.00 MB"
23
- assert format_bytes(2**30) == "1.00 GB"
24
- assert format_bytes(2**40) == "1.00 TB"
25
- assert format_bytes(2**50) == "1.00 PB"
18
+ assert format_bytes(999) == "999 B"
19
+ assert format_bytes(1000) == "1.00 KB"
20
+ assert format_bytes(1001) == "1.00 KB"
21
+ assert format_bytes(2000) == "2.00 KB"
22
+ assert format_bytes(10**6) == "1.00 MB"
23
+ assert format_bytes(10**9) == "1.00 GB"
24
+ assert format_bytes(10**12) == "1.00 TB"
25
+ assert format_bytes(10**15) == "1.00 PB"
26
26
 
27
27
 
28
28
  def test_add_placeholder_row():