kleinkram 0.43.2.dev20250331113255__py3-none-any.whl → 0.43.2.dev20250331120423__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.
- kleinkram/api/file_transfer.py +122 -70
- kleinkram/utils.py +16 -0
- {kleinkram-0.43.2.dev20250331113255.dist-info → kleinkram-0.43.2.dev20250331120423.dist-info}/METADATA +1 -1
- {kleinkram-0.43.2.dev20250331113255.dist-info → kleinkram-0.43.2.dev20250331120423.dist-info}/RECORD +7 -7
- {kleinkram-0.43.2.dev20250331113255.dist-info → kleinkram-0.43.2.dev20250331120423.dist-info}/WHEEL +0 -0
- {kleinkram-0.43.2.dev20250331113255.dist-info → kleinkram-0.43.2.dev20250331120423.dist-info}/entry_points.txt +0 -0
- {kleinkram-0.43.2.dev20250331113255.dist-info → kleinkram-0.43.2.dev20250331120423.dist-info}/top_level.txt +0 -0
kleinkram/api/file_transfer.py
CHANGED
|
@@ -17,18 +17,18 @@ from uuid import UUID
|
|
|
17
17
|
import boto3.s3.transfer
|
|
18
18
|
import botocore.config
|
|
19
19
|
import httpx
|
|
20
|
-
from rich.console import Console
|
|
21
|
-
from tqdm import tqdm
|
|
22
|
-
|
|
23
20
|
from kleinkram.api.client import AuthenticatedClient
|
|
24
21
|
from kleinkram.config import get_config
|
|
25
22
|
from kleinkram.errors import AccessDenied
|
|
26
23
|
from kleinkram.models import File
|
|
27
24
|
from kleinkram.models import FileState
|
|
28
25
|
from kleinkram.utils import b64_md5
|
|
26
|
+
from kleinkram.utils import format_bytes
|
|
29
27
|
from kleinkram.utils import format_error
|
|
30
28
|
from kleinkram.utils import format_traceback
|
|
31
29
|
from kleinkram.utils import styled_string
|
|
30
|
+
from rich.console import Console
|
|
31
|
+
from tqdm import tqdm
|
|
32
32
|
|
|
33
33
|
logger = logging.getLogger(__name__)
|
|
34
34
|
|
|
@@ -160,9 +160,9 @@ def upload_file(
|
|
|
160
160
|
path: Path,
|
|
161
161
|
verbose: bool = False,
|
|
162
162
|
s3_endpoint: Optional[str] = None,
|
|
163
|
-
) -> UploadState:
|
|
163
|
+
) -> Tuple[UploadState, int]:
|
|
164
164
|
"""\
|
|
165
|
-
returns bytes uploaded
|
|
165
|
+
returns UploadState and bytes uploaded (0 if not uploaded)
|
|
166
166
|
"""
|
|
167
167
|
if s3_endpoint is None:
|
|
168
168
|
s3_endpoint = get_config().endpoint.s3
|
|
@@ -181,17 +181,20 @@ def upload_file(
|
|
|
181
181
|
client, internal_filename=filename, mission_id=mission_id
|
|
182
182
|
)
|
|
183
183
|
if creds is None:
|
|
184
|
-
return UploadState.EXISTS
|
|
184
|
+
return UploadState.EXISTS, 0
|
|
185
185
|
|
|
186
186
|
try:
|
|
187
187
|
_s3_upload(path, endpoint=s3_endpoint, credentials=creds, pbar=pbar)
|
|
188
188
|
except Exception as e:
|
|
189
189
|
logger.error(format_traceback(e))
|
|
190
|
-
|
|
191
|
-
|
|
190
|
+
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
|
|
192
195
|
else:
|
|
193
196
|
_confirm_file_upload(client, creds.file_id, b64_md5(path))
|
|
194
|
-
return UploadState.UPLOADED
|
|
197
|
+
return UploadState.UPLOADED, total_size
|
|
195
198
|
|
|
196
199
|
|
|
197
200
|
def _get_file_download(client: AuthenticatedClient, id: UUID) -> str:
|
|
@@ -203,7 +206,7 @@ def _get_file_download(client: AuthenticatedClient, id: UUID) -> str:
|
|
|
203
206
|
if 400 <= resp.status_code < 500:
|
|
204
207
|
raise AccessDenied(
|
|
205
208
|
f"Failed to download file: {resp.json()['message']}"
|
|
206
|
-
f"Status Code: {resp.status_code}",
|
|
209
|
+
f" Status Code: {resp.status_code}",
|
|
207
210
|
)
|
|
208
211
|
|
|
209
212
|
resp.raise_for_status()
|
|
@@ -218,6 +221,7 @@ def _url_download(
|
|
|
218
221
|
raise FileExistsError(f"file already exists: {path}")
|
|
219
222
|
|
|
220
223
|
with httpx.stream("GET", url) as response:
|
|
224
|
+
response.raise_for_status()
|
|
221
225
|
with open(path, "wb") as f:
|
|
222
226
|
with tqdm(
|
|
223
227
|
total=size,
|
|
@@ -247,12 +251,14 @@ def download_file(
|
|
|
247
251
|
path: Path,
|
|
248
252
|
overwrite: bool = False,
|
|
249
253
|
verbose: bool = False,
|
|
250
|
-
) -> DownloadState:
|
|
254
|
+
) -> Tuple[DownloadState, int]:
|
|
255
|
+
"""\
|
|
256
|
+
Returns DownloadState and bytes downloaded (file.size if successful or skipped ok, 0 otherwise)
|
|
257
|
+
"""
|
|
251
258
|
# skip files that are not ok on remote
|
|
252
259
|
if file.state != FileState.OK:
|
|
253
|
-
return DownloadState.SKIPPED_INVALID_REMOTE_STATE
|
|
260
|
+
return DownloadState.SKIPPED_INVALID_REMOTE_STATE, 0
|
|
254
261
|
|
|
255
|
-
# skip existing files depending on flags set
|
|
256
262
|
if path.exists():
|
|
257
263
|
local_hash = b64_md5(path)
|
|
258
264
|
if local_hash != file.hash and not overwrite and file.hash is not None:
|
|
@@ -270,17 +276,38 @@ def download_file(
|
|
|
270
276
|
# request a download url
|
|
271
277
|
download_url = _get_file_download(client, file.id)
|
|
272
278
|
|
|
273
|
-
# create parent directories
|
|
274
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
279
|
+
# create parent directories (moved earlier, before file open)
|
|
280
|
+
# path.parent.mkdir(parents=True, exist_ok=True)
|
|
275
281
|
|
|
276
282
|
# download the file and check the hash
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
283
|
+
try:
|
|
284
|
+
_url_download(
|
|
285
|
+
download_url,
|
|
286
|
+
path=path,
|
|
287
|
+
size=file.size,
|
|
288
|
+
overwrite=overwrite,
|
|
289
|
+
verbose=verbose,
|
|
290
|
+
)
|
|
291
|
+
except Exception as e:
|
|
292
|
+
logger.error(f"Error during download of {path}: {e}")
|
|
293
|
+
# Attempt to clean up potentially partial file
|
|
294
|
+
if path.exists():
|
|
295
|
+
try:
|
|
296
|
+
path.unlink()
|
|
297
|
+
logger.info(f"Removed potentially incomplete file {path}")
|
|
298
|
+
except OSError as unlink_e:
|
|
299
|
+
logger.error(f"Could not remove partial file {path}: {unlink_e}")
|
|
300
|
+
raise e # Re-raise to be caught by handler
|
|
301
|
+
|
|
280
302
|
observed_hash = b64_md5(path)
|
|
281
303
|
if file.hash is not None and observed_hash != file.hash:
|
|
282
|
-
|
|
283
|
-
|
|
304
|
+
# Download completed but hash failed
|
|
305
|
+
return (
|
|
306
|
+
DownloadState.DOWNLOADED_INVALID_HASH,
|
|
307
|
+
0,
|
|
308
|
+
) # 0 bytes considered successful transfer
|
|
309
|
+
# Hash matches or no remote hash to check against
|
|
310
|
+
return DownloadState.DOWNLOADED_OK, file.size
|
|
284
311
|
|
|
285
312
|
|
|
286
313
|
UPLOAD_STATE_COLOR = {
|
|
@@ -291,17 +318,20 @@ UPLOAD_STATE_COLOR = {
|
|
|
291
318
|
|
|
292
319
|
|
|
293
320
|
def _upload_handler(
|
|
294
|
-
future: Future[UploadState], path: Path, *, verbose: bool = False
|
|
321
|
+
future: Future[Tuple[UploadState, int]], path: Path, *, verbose: bool = False
|
|
295
322
|
) -> int:
|
|
323
|
+
"""Returns bytes uploaded successfully."""
|
|
324
|
+
state = UploadState.CANCELED # Default to canceled if exception occurs
|
|
325
|
+
size_bytes = 0
|
|
296
326
|
try:
|
|
297
|
-
state = future.result()
|
|
327
|
+
state, size_bytes = future.result()
|
|
298
328
|
except Exception as e:
|
|
299
329
|
logger.error(format_traceback(e))
|
|
300
330
|
if verbose:
|
|
301
331
|
tqdm.write(format_error(f"error uploading {path}", e))
|
|
302
332
|
else:
|
|
303
|
-
print(path.absolute(), file=sys.stderr)
|
|
304
|
-
return 0
|
|
333
|
+
print(f"ERROR: {path.absolute()}: {e}", file=sys.stderr)
|
|
334
|
+
return 0 # Return 0 bytes on error
|
|
305
335
|
|
|
306
336
|
if state == UploadState.UPLOADED:
|
|
307
337
|
msg = f"uploaded {path}"
|
|
@@ -312,11 +342,10 @@ def _upload_handler(
|
|
|
312
342
|
|
|
313
343
|
if verbose:
|
|
314
344
|
tqdm.write(styled_string(msg, style=UPLOAD_STATE_COLOR[state]))
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
print(path.absolute(), file=stream)
|
|
345
|
+
elif state != UploadState.UPLOADED:
|
|
346
|
+
print(f"SKIP/CANCEL: {path.absolute()}", file=sys.stderr)
|
|
318
347
|
|
|
319
|
-
return
|
|
348
|
+
return size_bytes
|
|
320
349
|
|
|
321
350
|
|
|
322
351
|
DOWNLOAD_STATE_COLOR = {
|
|
@@ -329,41 +358,48 @@ DOWNLOAD_STATE_COLOR = {
|
|
|
329
358
|
|
|
330
359
|
|
|
331
360
|
def _download_handler(
|
|
332
|
-
future: Future[DownloadState
|
|
361
|
+
future: Future[Tuple[DownloadState, int]],
|
|
362
|
+
file: File,
|
|
363
|
+
path: Path,
|
|
364
|
+
*,
|
|
365
|
+
verbose: bool = False,
|
|
333
366
|
) -> int:
|
|
367
|
+
"""Returns bytes downloaded/verified."""
|
|
368
|
+
state = DownloadState.DOWNLOADED_INVALID_HASH
|
|
369
|
+
size_bytes = 0
|
|
334
370
|
try:
|
|
335
|
-
state = future.result()
|
|
371
|
+
state, size_bytes = future.result()
|
|
336
372
|
except Exception as e:
|
|
337
373
|
logger.error(format_traceback(e))
|
|
338
374
|
if verbose:
|
|
339
|
-
tqdm.write(format_error(f"error
|
|
375
|
+
tqdm.write(format_error(f"error downloading {path}", e))
|
|
340
376
|
else:
|
|
341
|
-
print(path.absolute(), file=sys.stderr)
|
|
377
|
+
print(f"ERROR: {path.absolute()}: {e}", file=sys.stderr)
|
|
342
378
|
return 0
|
|
343
379
|
|
|
344
380
|
if state == DownloadState.DOWNLOADED_OK:
|
|
345
381
|
msg = f"downloaded {path}"
|
|
346
382
|
elif state == DownloadState.DOWNLOADED_INVALID_HASH:
|
|
347
|
-
msg = f"downloaded {path} failed hash check"
|
|
383
|
+
msg = f"downloaded {path} but failed hash check"
|
|
348
384
|
elif state == DownloadState.SKIPPED_OK:
|
|
349
|
-
msg = f"skipped {path} already downloaded"
|
|
385
|
+
msg = f"skipped {path} already downloaded (hash ok)"
|
|
350
386
|
elif state == DownloadState.SKIPPED_INVALID_HASH:
|
|
351
|
-
msg = f"skipped {path}
|
|
387
|
+
msg = f"skipped {path}, exists with hash mismatch (use --overwrite?)"
|
|
388
|
+
elif state == DownloadState.SKIPPED_INVALID_REMOTE_STATE:
|
|
389
|
+
msg = f"skipped {path}, remote file has invalid state ({file.state.value})"
|
|
352
390
|
else:
|
|
353
|
-
msg = f"skipped {path}
|
|
391
|
+
msg = f"skipped {path} with unknown state {state}"
|
|
354
392
|
|
|
355
393
|
if verbose:
|
|
356
|
-
tqdm.write(styled_string(msg, style=DOWNLOAD_STATE_COLOR
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
)
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
# number of bytes downloaded
|
|
366
|
-
return file.size if state == DownloadState.DOWNLOADED_OK else 0
|
|
394
|
+
tqdm.write(styled_string(msg, style=DOWNLOAD_STATE_COLOR.get(state, "red")))
|
|
395
|
+
elif state not in (DownloadState.DOWNLOADED_OK, DownloadState.SKIPPED_OK):
|
|
396
|
+
print(f"SKIP/FAIL: {path.absolute()} ({state.name})", file=sys.stderr)
|
|
397
|
+
|
|
398
|
+
return (
|
|
399
|
+
size_bytes
|
|
400
|
+
if state in (DownloadState.DOWNLOADED_OK, DownloadState.SKIPPED_OK)
|
|
401
|
+
else 0
|
|
402
|
+
)
|
|
367
403
|
|
|
368
404
|
|
|
369
405
|
def upload_files(
|
|
@@ -374,17 +410,25 @@ def upload_files(
|
|
|
374
410
|
verbose: bool = False,
|
|
375
411
|
n_workers: int = 2,
|
|
376
412
|
) -> None:
|
|
413
|
+
console = Console(file=sys.stderr)
|
|
377
414
|
with tqdm(
|
|
378
415
|
total=len(files),
|
|
379
416
|
unit="files",
|
|
380
|
-
desc="
|
|
417
|
+
desc="Uploading files",
|
|
381
418
|
disable=not verbose,
|
|
382
419
|
leave=False,
|
|
383
420
|
) as pbar:
|
|
384
421
|
start = monotonic()
|
|
385
|
-
futures: Dict[Future[UploadState], Path] = {}
|
|
422
|
+
futures: Dict[Future[Tuple[UploadState, int]], Path] = {}
|
|
386
423
|
with ThreadPoolExecutor(max_workers=n_workers) as executor:
|
|
387
424
|
for name, path in files.items():
|
|
425
|
+
if not path.is_file():
|
|
426
|
+
console.print(
|
|
427
|
+
f"[yellow]Skipping non-existent file: {path}[/yellow]"
|
|
428
|
+
)
|
|
429
|
+
pbar.update()
|
|
430
|
+
continue
|
|
431
|
+
|
|
388
432
|
future = executor.submit(
|
|
389
433
|
upload_file,
|
|
390
434
|
client=client,
|
|
@@ -395,19 +439,21 @@ def upload_files(
|
|
|
395
439
|
)
|
|
396
440
|
futures[future] = path
|
|
397
441
|
|
|
398
|
-
|
|
442
|
+
total_uploaded_bytes = 0
|
|
399
443
|
for future in as_completed(futures):
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
444
|
+
path = futures[future]
|
|
445
|
+
uploaded_bytes = _upload_handler(future, path, verbose=verbose)
|
|
446
|
+
total_uploaded_bytes += uploaded_bytes
|
|
403
447
|
pbar.update()
|
|
404
|
-
pbar.refresh()
|
|
405
448
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
449
|
+
end = monotonic()
|
|
450
|
+
elapsed_time = end - start
|
|
451
|
+
|
|
452
|
+
avg_speed_bps = total_uploaded_bytes / elapsed_time if elapsed_time > 0 else 0
|
|
453
|
+
|
|
454
|
+
console.print(f"Upload took {elapsed_time:.2f} seconds")
|
|
455
|
+
console.print(f"Total uploaded: {format_bytes(total_uploaded_bytes)}")
|
|
456
|
+
console.print(f"Average speed: {format_bytes(avg_speed_bps, speed=True)}")
|
|
411
457
|
|
|
412
458
|
|
|
413
459
|
def download_files(
|
|
@@ -418,16 +464,17 @@ def download_files(
|
|
|
418
464
|
overwrite: bool = False,
|
|
419
465
|
n_workers: int = 2,
|
|
420
466
|
) -> None:
|
|
467
|
+
console = Console(file=sys.stderr)
|
|
421
468
|
with tqdm(
|
|
422
469
|
total=len(files),
|
|
423
470
|
unit="files",
|
|
424
|
-
desc="
|
|
471
|
+
desc="Downloading files",
|
|
425
472
|
disable=not verbose,
|
|
426
473
|
leave=False,
|
|
427
474
|
) as pbar:
|
|
428
475
|
|
|
429
476
|
start = monotonic()
|
|
430
|
-
futures: Dict[Future[DownloadState], Tuple[File, Path]] = {}
|
|
477
|
+
futures: Dict[Future[Tuple[DownloadState, int]], Tuple[File, Path]] = {}
|
|
431
478
|
with ThreadPoolExecutor(max_workers=n_workers) as executor:
|
|
432
479
|
for path, file in files.items():
|
|
433
480
|
future = executor.submit(
|
|
@@ -440,16 +487,21 @@ def download_files(
|
|
|
440
487
|
)
|
|
441
488
|
futures[future] = (file, path)
|
|
442
489
|
|
|
443
|
-
|
|
490
|
+
total_downloaded_bytes = 0
|
|
444
491
|
for future in as_completed(futures):
|
|
445
492
|
file, path = futures[future]
|
|
446
|
-
|
|
447
|
-
|
|
493
|
+
downloaded_bytes = _download_handler(
|
|
494
|
+
future, file, path, verbose=verbose
|
|
495
|
+
)
|
|
496
|
+
total_downloaded_bytes += downloaded_bytes
|
|
448
497
|
pbar.update()
|
|
449
|
-
pbar.refresh()
|
|
450
498
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
499
|
+
end = monotonic()
|
|
500
|
+
elapsed_time = end - start
|
|
501
|
+
avg_speed_bps = total_downloaded_bytes / elapsed_time if elapsed_time > 0 else 0
|
|
502
|
+
|
|
503
|
+
console.print(f"Download took {elapsed_time:.2f} seconds")
|
|
504
|
+
console.print(
|
|
505
|
+
f"Total downloaded/verified: {format_bytes(total_downloaded_bytes)}"
|
|
506
|
+
)
|
|
507
|
+
console.print(f"Average speed: {format_bytes(avg_speed_bps, speed=True)}")
|
kleinkram/utils.py
CHANGED
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import base64
|
|
4
4
|
import hashlib
|
|
5
|
+
import math
|
|
5
6
|
import re
|
|
6
7
|
import string
|
|
7
8
|
import traceback
|
|
@@ -220,3 +221,18 @@ def parse_uuid_like(s: IdLike) -> UUID:
|
|
|
220
221
|
|
|
221
222
|
def parse_path_like(s: PathLike) -> Path:
|
|
222
223
|
return Path(s)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def format_bytes(size_bytes: int | float, speed: bool = False) -> str:
|
|
227
|
+
"""Formats a size in bytes into a human-readable string with appropriate units."""
|
|
228
|
+
if size_bytes == 0:
|
|
229
|
+
return "0 B/s" if speed else "0 B"
|
|
230
|
+
|
|
231
|
+
units = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]
|
|
232
|
+
power = math.floor(math.log(size_bytes, 1024))
|
|
233
|
+
unit_index = min(power, len(units) - 1)
|
|
234
|
+
|
|
235
|
+
value = size_bytes / (1024**unit_index)
|
|
236
|
+
|
|
237
|
+
unit_suffix = "/s" if speed else ""
|
|
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.43.2.
|
|
3
|
+
Version: 0.43.2.dev20250331120423
|
|
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
|
{kleinkram-0.43.2.dev20250331113255.dist-info → kleinkram-0.43.2.dev20250331120423.dist-info}/RECORD
RENAMED
|
@@ -10,12 +10,12 @@ kleinkram/models.py,sha256=8nJlPrKVLSmehspeuQSFV6nUo76JzehUn6KIZYH1xy4,1832
|
|
|
10
10
|
kleinkram/printing.py,sha256=_vIjs-lPVgv21ER5q5iYtx44OTs5y8wyd32w27WqocM,12161
|
|
11
11
|
kleinkram/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
12
|
kleinkram/types.py,sha256=nfDjj8TB1Jn5vqO0Xg6qhLOuKom9DDhe62BrngqnVGM,185
|
|
13
|
-
kleinkram/utils.py,sha256=
|
|
13
|
+
kleinkram/utils.py,sha256=AsKZxEGStn03L2tqqiMVCQCrDyl8HhwOfpa3no4dfYc,6767
|
|
14
14
|
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=
|
|
18
|
+
kleinkram/api/file_transfer.py,sha256=yuN6c43jdbUpOPOZI591XN2R2jWaA5px7YbRnaso5PI,15268
|
|
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.2.
|
|
47
|
-
kleinkram-0.43.2.
|
|
48
|
-
kleinkram-0.43.2.
|
|
49
|
-
kleinkram-0.43.2.
|
|
50
|
-
kleinkram-0.43.2.
|
|
46
|
+
kleinkram-0.43.2.dev20250331120423.dist-info/METADATA,sha256=YOiJZOo853MNLsw5Nj5gWUWvLw-9N0e6Tqq7Epu5fGE,2825
|
|
47
|
+
kleinkram-0.43.2.dev20250331120423.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
|
48
|
+
kleinkram-0.43.2.dev20250331120423.dist-info/entry_points.txt,sha256=SaB2l5aqhSr8gmaMw2kvQU90a8Bnl7PedU8cWYxkfYo,46
|
|
49
|
+
kleinkram-0.43.2.dev20250331120423.dist-info/top_level.txt,sha256=N3-sJagEHu1Tk1X6Dx1X1q0pLDNbDZpLzRxVftvepds,24
|
|
50
|
+
kleinkram-0.43.2.dev20250331120423.dist-info/RECORD,,
|
{kleinkram-0.43.2.dev20250331113255.dist-info → kleinkram-0.43.2.dev20250331120423.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|