kleinkram 0.44.0.dev20250409080532__tar.gz → 0.44.1.dev20250414055151__tar.gz

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.

Files changed (57) hide show
  1. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/PKG-INFO +1 -1
  2. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/kleinkram/api/file_transfer.py +67 -17
  3. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/kleinkram/printing.py +2 -2
  4. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/kleinkram/utils.py +2 -2
  5. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/kleinkram.egg-info/PKG-INFO +1 -1
  6. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/setup.cfg +1 -1
  7. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/tests/test_printing.py +8 -8
  8. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/README.md +0 -0
  9. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/kleinkram/__init__.py +0 -0
  10. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/kleinkram/__main__.py +0 -0
  11. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/kleinkram/_version.py +0 -0
  12. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/kleinkram/api/__init__.py +0 -0
  13. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/kleinkram/api/client.py +0 -0
  14. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/kleinkram/api/deser.py +0 -0
  15. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/kleinkram/api/pagination.py +0 -0
  16. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/kleinkram/api/query.py +0 -0
  17. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/kleinkram/api/routes.py +0 -0
  18. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/kleinkram/auth.py +0 -0
  19. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/kleinkram/cli/__init__.py +0 -0
  20. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/kleinkram/cli/_download.py +0 -0
  21. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/kleinkram/cli/_endpoint.py +0 -0
  22. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/kleinkram/cli/_file.py +0 -0
  23. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/kleinkram/cli/_list.py +0 -0
  24. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/kleinkram/cli/_mission.py +0 -0
  25. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/kleinkram/cli/_project.py +0 -0
  26. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/kleinkram/cli/_upload.py +0 -0
  27. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/kleinkram/cli/_verify.py +0 -0
  28. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/kleinkram/cli/app.py +0 -0
  29. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/kleinkram/cli/error_handling.py +0 -0
  30. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/kleinkram/config.py +0 -0
  31. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/kleinkram/core.py +0 -0
  32. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/kleinkram/errors.py +0 -0
  33. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/kleinkram/main.py +0 -0
  34. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/kleinkram/models.py +0 -0
  35. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/kleinkram/py.typed +0 -0
  36. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/kleinkram/types.py +0 -0
  37. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/kleinkram/wrappers.py +0 -0
  38. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/kleinkram.egg-info/SOURCES.txt +0 -0
  39. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/kleinkram.egg-info/dependency_links.txt +0 -0
  40. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/kleinkram.egg-info/entry_points.txt +0 -0
  41. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/kleinkram.egg-info/requires.txt +0 -0
  42. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/kleinkram.egg-info/top_level.txt +0 -0
  43. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/pyproject.toml +0 -0
  44. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/requirements.txt +0 -0
  45. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/setup.py +0 -0
  46. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/testing/__init__.py +0 -0
  47. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/testing/backend_fixtures.py +0 -0
  48. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/tests/__init__.py +0 -0
  49. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/tests/conftest.py +0 -0
  50. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/tests/test_config.py +0 -0
  51. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/tests/test_core.py +0 -0
  52. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/tests/test_end_to_end.py +0 -0
  53. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/tests/test_error_handling.py +0 -0
  54. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/tests/test_fixtures.py +0 -0
  55. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/tests/test_query.py +0 -0
  56. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/tests/test_utils.py +0 -0
  57. {kleinkram-0.44.0.dev20250409080532 → kleinkram-0.44.1.dev20250414055151}/tests/test_wrappers.py +0 -0
@@ -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.dev20250414055151
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
@@ -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):
@@ -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
@@ -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.dev20250414055151
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
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = kleinkram
3
- version = 0.44.0-dev20250409080532
3
+ version = 0.44.1-dev20250414055151
4
4
  description = give me your bags
5
5
  long_description = file: README.md
6
6
  long_description_content_type = text/markdown
@@ -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():