kleinkram 0.44.0.dev20250409080532__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):
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.dev20250409080532
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=9YLEpbR77LTmK5YKPGC1Eil1_HnyuXOdW1Q1bRd0Wnk,18053
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.dev20250409080532.dist-info/METADATA,sha256=XmWS4nDjTzSptkV6eW9mFzVnT9NOoMncz6BPcsRnce4,2825
47
- kleinkram-0.44.0.dev20250409080532.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
48
- kleinkram-0.44.0.dev20250409080532.dist-info/entry_points.txt,sha256=SaB2l5aqhSr8gmaMw2kvQU90a8Bnl7PedU8cWYxkfYo,46
49
- kleinkram-0.44.0.dev20250409080532.dist-info/top_level.txt,sha256=N3-sJagEHu1Tk1X6Dx1X1q0pLDNbDZpLzRxVftvepds,24
50
- kleinkram-0.44.0.dev20250409080532.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():