kleinkram 0.43.4.dev20250401070240__py3-none-any.whl → 0.43.4.dev20250401085004__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.

@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import signal
3
4
  import logging
4
5
  import sys
5
6
  from concurrent.futures import Future
@@ -7,7 +8,7 @@ from concurrent.futures import ThreadPoolExecutor
7
8
  from concurrent.futures import as_completed
8
9
  from enum import Enum
9
10
  from pathlib import Path
10
- from time import monotonic
11
+ from time import monotonic, sleep
11
12
  from typing import Dict
12
13
  from typing import NamedTuple
13
14
  from typing import Optional
@@ -39,6 +40,7 @@ UPLOAD_CANCEL = "/files/cancelUpload"
39
40
  DOWNLOAD_CHUNK_SIZE = 1024 * 1024 * 16
40
41
  DOWNLOAD_URL = "/files/download"
41
42
 
43
+ MAX_UPLOAD_RETRIES = 3
42
44
  S3_MAX_RETRIES = 60 # same as frontend
43
45
  S3_READ_TIMEOUT = 60 * 5 # 5 minutes
44
46
 
@@ -66,8 +68,8 @@ def _cancel_file_upload(
66
68
  client: AuthenticatedClient, file_id: UUID, mission_id: UUID
67
69
  ) -> None:
68
70
  data = {
69
- "uuid": [str(file_id)],
70
- "missionUUID": str(mission_id),
71
+ "uuids": [str(file_id)],
72
+ "missionUuid": str(mission_id),
71
73
  }
72
74
  resp = client.post(UPLOAD_CANCEL, json=data)
73
75
  resp.raise_for_status()
@@ -151,6 +153,37 @@ class UploadState(Enum):
151
153
  CANCELED = 3
152
154
 
153
155
 
156
+ def _get_upload_credentials_with_retry(
157
+ client, pbar, filename, mission_id, max_attempts=5
158
+ ):
159
+ """
160
+ Retrieves upload credentials with retry logic.
161
+
162
+ Args:
163
+ client: The client object used for retrieving credentials.
164
+ filename: The internal filename.
165
+ mission_id: The mission ID.
166
+ max_attempts: Maximum number of retry attempts.
167
+
168
+ Returns:
169
+ The upload credentials or None if retrieval fails after all attempts.
170
+ """
171
+ attempt = 0
172
+ while attempt < max_attempts:
173
+ creds = _get_upload_creditials(
174
+ client, internal_filename=filename, mission_id=mission_id
175
+ )
176
+ if creds is not None:
177
+ return creds
178
+
179
+ attempt += 1
180
+ if attempt < max_attempts:
181
+ delay = 2**attempt # Exponential backoff (2, 4, 8, 16...)
182
+ sleep(delay)
183
+
184
+ return None
185
+
186
+
154
187
  # TODO: i dont want to handle errors at this level
155
188
  def upload_file(
156
189
  client: AuthenticatedClient,
@@ -161,40 +194,54 @@ def upload_file(
161
194
  verbose: bool = False,
162
195
  s3_endpoint: Optional[str] = None,
163
196
  ) -> Tuple[UploadState, int]:
164
- """\
197
+ """
165
198
  returns UploadState and bytes uploaded (0 if not uploaded)
199
+ Retries up to 3 times on failure.
166
200
  """
167
201
  if s3_endpoint is None:
168
202
  s3_endpoint = get_config().endpoint.s3
169
203
 
170
204
  total_size = path.stat().st_size
171
- with tqdm(
172
- total=total_size,
173
- unit="B",
174
- unit_scale=True,
175
- desc=f"uploading {path}...",
176
- leave=False,
177
- disable=not verbose,
178
- ) as pbar:
179
- # get per file upload credentials
180
- creds = _get_upload_creditials(
181
- client, internal_filename=filename, mission_id=mission_id
182
- )
183
- if creds is None:
184
- return UploadState.EXISTS, 0
205
+ for attempt in range(MAX_UPLOAD_RETRIES):
206
+ with tqdm(
207
+ total=total_size,
208
+ unit="B",
209
+ unit_scale=True,
210
+ desc=f"uploading {path}...",
211
+ leave=False,
212
+ disable=not verbose,
213
+ ) as pbar:
214
+
215
+ # get per file upload credentials
216
+ creds = _get_upload_credentials_with_retry(
217
+ client, pbar, filename, mission_id, max_attempts=5 if attempt > 0 else 1
218
+ )
219
+
220
+ if creds is None:
221
+ return UploadState.EXISTS, 0
185
222
 
186
- try:
187
- _s3_upload(path, endpoint=s3_endpoint, credentials=creds, pbar=pbar)
188
- except Exception as e:
189
- logger.error(format_traceback(e))
190
223
  try:
191
- _cancel_file_upload(client, creds.file_id, mission_id)
192
- except Exception as cancel_e:
193
- logger.error(f"Failed to cancel upload for {creds.file_id}: {cancel_e}")
194
- raise e from e
195
- else:
196
- _confirm_file_upload(client, creds.file_id, b64_md5(path))
197
- return UploadState.UPLOADED, total_size
224
+ _s3_upload(path, endpoint=s3_endpoint, credentials=creds, pbar=pbar)
225
+ except Exception as e:
226
+ logger.error(format_traceback(e))
227
+ try:
228
+ _cancel_file_upload(client, creds.file_id, mission_id)
229
+ except Exception as cancel_e:
230
+ logger.error(
231
+ f"Failed to cancel upload for {creds.file_id}: {cancel_e}"
232
+ )
233
+
234
+ if attempt < 2: # Retry if not the last attempt
235
+ pbar.update(0)
236
+ logger.error(f"Retrying upload for {attempt + 1}")
237
+ continue
238
+ else:
239
+ logger.error(f"Cancelling upload for {attempt}")
240
+ raise e from e
241
+
242
+ else:
243
+ _confirm_file_upload(client, creds.file_id, b64_md5(path))
244
+ return UploadState.UPLOADED, total_size
198
245
 
199
246
 
200
247
  def _get_file_download(client: AuthenticatedClient, id: UUID) -> str:
@@ -420,6 +467,9 @@ def upload_files(
420
467
  ) as pbar:
421
468
  start = monotonic()
422
469
  futures: Dict[Future[Tuple[UploadState, int]], Path] = {}
470
+
471
+ skipped_files = 0
472
+ failed_files = 0
423
473
  with ThreadPoolExecutor(max_workers=n_workers) as executor:
424
474
  for name, path in files.items():
425
475
  if not path.is_file():
@@ -441,6 +491,16 @@ def upload_files(
441
491
 
442
492
  total_uploaded_bytes = 0
443
493
  for future in as_completed(futures):
494
+
495
+ if future.exception():
496
+ failed_files += 1
497
+
498
+ if (
499
+ future.exception() is None
500
+ and future.result()[0] == UploadState.EXISTS
501
+ ):
502
+ skipped_files += 1
503
+
444
504
  path = futures[future]
445
505
  uploaded_bytes = _upload_handler(future, path, verbose=verbose)
446
506
  total_uploaded_bytes += uploaded_bytes
@@ -455,6 +515,16 @@ def upload_files(
455
515
  console.print(f"Total uploaded: {format_bytes(total_uploaded_bytes)}")
456
516
  console.print(f"Average speed: {format_bytes(avg_speed_bps, speed=True)}")
457
517
 
518
+ if failed_files > 0:
519
+ console.print(
520
+ f"\nUploaded {len(files) - failed_files - skipped_files} files, {skipped_files} skipped, {failed_files} uploads failed",
521
+ style="red",
522
+ )
523
+ else:
524
+ console.print(
525
+ f"\nUploaded {len(files) - skipped_files} files, {skipped_files} skipped"
526
+ )
527
+
458
528
 
459
529
  def download_files(
460
530
  client: AuthenticatedClient,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kleinkram
3
- Version: 0.43.4.dev20250401070240
3
+ Version: 0.43.4.dev20250401085004
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=4xXU43eNnvMG2sssU330MmTLSSRdurOpnZ-zNGOGmt0,11342
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=_uYYJs1iND4YNpg8_-VUo6zu1DuHV8Or2KkGSVAAL0o,15278
18
+ kleinkram/api/file_transfer.py,sha256=smzT_gQjmJHeVYdlfmpCH6CRS_Swx-Kp-3Q8lvTNrqQ,17452
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.43.4.dev20250401070240.dist-info/METADATA,sha256=4tOEmzhTgN0OyfKC1ICUlsV6zuXsawwUCBLIFaHsLzA,2825
47
- kleinkram-0.43.4.dev20250401070240.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
48
- kleinkram-0.43.4.dev20250401070240.dist-info/entry_points.txt,sha256=SaB2l5aqhSr8gmaMw2kvQU90a8Bnl7PedU8cWYxkfYo,46
49
- kleinkram-0.43.4.dev20250401070240.dist-info/top_level.txt,sha256=N3-sJagEHu1Tk1X6Dx1X1q0pLDNbDZpLzRxVftvepds,24
50
- kleinkram-0.43.4.dev20250401070240.dist-info/RECORD,,
46
+ kleinkram-0.43.4.dev20250401085004.dist-info/METADATA,sha256=LHc7Ma6iRyefhO0E8zMhCB4Dp2vm_AXHcxTTJIP-Tfg,2825
47
+ kleinkram-0.43.4.dev20250401085004.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
48
+ kleinkram-0.43.4.dev20250401085004.dist-info/entry_points.txt,sha256=SaB2l5aqhSr8gmaMw2kvQU90a8Bnl7PedU8cWYxkfYo,46
49
+ kleinkram-0.43.4.dev20250401085004.dist-info/top_level.txt,sha256=N3-sJagEHu1Tk1X6Dx1X1q0pLDNbDZpLzRxVftvepds,24
50
+ kleinkram-0.43.4.dev20250401085004.dist-info/RECORD,,