materials-commons-cli 2.3.0b9__tar.gz → 2.3.0b10__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.
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/PKG-INFO +1 -1
- materials_commons_cli-2.3.0b10/materials_commons/cli/__init__.py +1 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/old/functions.py +9 -4
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/parser.py +3 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/reconcile3.py +51 -9
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/server/downloader/file_download_manager.py +2 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/server/downloader/file_downloader.py +59 -3
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/server/uploader/file_uploader.py +55 -4
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/subcommands/up.py +1 -0
- materials_commons_cli-2.3.0b10/materials_commons/cli/terminal_progress.py +45 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons_cli.egg-info/PKG-INFO +1 -1
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons_cli.egg-info/SOURCES.txt +1 -0
- materials_commons_cli-2.3.0b9/materials_commons/cli/__init__.py +0 -1
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/LICENSE +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/README.md +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/LICENSE.txt +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/async_reconciler.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/config.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/downloader.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/filedb.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/local_project.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/models.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/old/__init__.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/old/cloned_project.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/old/exceptions.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/old/file_functions.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/old/globus.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/old/list_objects.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/old/print_formatter.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/old/sqltable.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/old/tmp_functions.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/old/tree_functions.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/old/treedb.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/old/user_config.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/reconcile2.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/requests.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/run.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/server/__init__.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/server/command_handlers/__init__.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/server/command_handlers/admin_handler_lookup.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/server/command_handlers/download_handler_lookup.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/server/command_handlers/list_handler_lookup.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/server/command_handlers/multi_handler_lookup.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/server/command_handlers/protocol.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/server/command_handlers/search_find_handler_lookup.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/server/command_handlers/upload_handler_lookup.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/server/db/__init__.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/server/db/db_manager.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/server/downloader/__init__.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/server/indexer/__init__.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/server/indexer/file_index_manager.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/server/local_rest_server.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/server/ocommand_handlers.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/server/project_filedbs.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/server/projects.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/server/service_container.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/server/service_runtime.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/server/uploader/__init__.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/server/uploader/client_api.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/server/uploader/file_upload_manager.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/server/websocket_server.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/subcommands/__init__.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/subcommands/clone.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/subcommands/config.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/subcommands/dataset.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/subcommands/down.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/subcommands/down2.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/subcommands/expt.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/subcommands/fetch.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/subcommands/globus.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/subcommands/init.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/subcommands/ls.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/subcommands/ls2.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/subcommands/mcp.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/subcommands/mkdir.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/subcommands/mv.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/subcommands/proj.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/subcommands/remote.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/subcommands/rm.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/subcommands/runners/__init__.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/subcommands/runners/download_globus_runner.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/subcommands/runners/download_v1_runner.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/subcommands/scan.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/subcommands/server.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/subcommands/versions.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/walk.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons_cli.egg-info/dependency_links.txt +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons_cli.egg-info/entry_points.txt +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons_cli.egg-info/not-zip-safe +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons_cli.egg-info/requires.txt +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons_cli.egg-info/top_level.txt +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/setup.cfg +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/setup.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/test/test_api.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/test/test_functions.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/test/test_mc_dataset.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/test/test_mc_down.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/test/test_mc_mkdir.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/test/test_mc_mv.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/test/test_mc_proj.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/test/test_mc_rm.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/test/test_mc_up.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/test/test_treecompare_no_cache.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/test/test_treecompare_with_cache.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/test/test_treedb.py +0 -0
- {materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/test/test_user_config.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '2.3.0b10'
|
|
@@ -144,28 +144,33 @@ def format_time(time_value, fmt="%Y %b %d %H:%M:%S") -> str:
|
|
|
144
144
|
return str(type(time_value))
|
|
145
145
|
|
|
146
146
|
|
|
147
|
-
def checksum(path, chunk_size=
|
|
148
|
-
"""Generate MD5 checksum for the file at
|
|
147
|
+
def checksum(path, chunk_size=1024*1024, progress_callback=None):
|
|
148
|
+
"""Generate MD5 checksum for the file at path
|
|
149
149
|
|
|
150
150
|
Args:
|
|
151
151
|
path (str): Path to file
|
|
152
152
|
chunk_size (int): Size of chunks to read (default 8192 bytes)
|
|
153
|
+
progress_callback (callable): Callback function to report progress as progress_callback(bytes_read)
|
|
153
154
|
|
|
154
155
|
Returns:
|
|
155
156
|
str: MD5 checksum hexdigest
|
|
156
157
|
"""
|
|
157
158
|
md5 = hashlib.md5()
|
|
159
|
+
bytes_read = 0
|
|
158
160
|
with open(path, 'rb') as f:
|
|
159
161
|
while True:
|
|
160
162
|
chunk = f.read(chunk_size)
|
|
161
163
|
if not chunk:
|
|
162
164
|
break
|
|
163
165
|
md5.update(chunk)
|
|
166
|
+
bytes_read += len(chunk)
|
|
167
|
+
if progress_callback:
|
|
168
|
+
progress_callback(bytes_read)
|
|
164
169
|
return md5.hexdigest()
|
|
165
170
|
|
|
166
171
|
|
|
167
|
-
async def checksum_async(path, chunk_size=
|
|
168
|
-
"""Generate MD5 checksum for the file at
|
|
172
|
+
async def checksum_async(path, chunk_size=1024*1024):
|
|
173
|
+
"""Generate MD5 checksum for the file at path"""
|
|
169
174
|
return await asyncio.to_thread(checksum, path, chunk_size)
|
|
170
175
|
|
|
171
176
|
|
{materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/parser.py
RENAMED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
import json
|
|
3
|
+
import logging
|
|
3
4
|
import os
|
|
4
5
|
import sys
|
|
5
6
|
from datetime import datetime, timedelta
|
|
@@ -144,6 +145,8 @@ def check_package_version():
|
|
|
144
145
|
|
|
145
146
|
|
|
146
147
|
def main(argv=None, working_dir=None):
|
|
148
|
+
logger = logging.getLogger(__name__)
|
|
149
|
+
logger.setLevel(logging.WARNING)
|
|
147
150
|
if argv is None:
|
|
148
151
|
argv = sys.argv
|
|
149
152
|
if working_dir is None:
|
{materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/reconcile3.py
RENAMED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import time
|
|
4
5
|
from dataclasses import replace
|
|
5
6
|
from typing import Literal, Optional
|
|
6
7
|
|
|
@@ -9,6 +10,7 @@ from materials_commons.api import models as mcmodel
|
|
|
9
10
|
from materials_commons.cli.local_project import LocalProject
|
|
10
11
|
from materials_commons.cli.models import FileRecord, Observation, FileDecision
|
|
11
12
|
from materials_commons.cli.old.functions import checksum
|
|
13
|
+
from materials_commons.cli.terminal_progress import TerminalProgress
|
|
12
14
|
|
|
13
15
|
Action = Literal["skip", "upload", "download", "conflict", "adopt", "db_update"]
|
|
14
16
|
ObservationState = Literal["neither", "local_only", "remote_only", "both"]
|
|
@@ -141,12 +143,12 @@ class SingleFileReconciler:
|
|
|
141
143
|
if observation.local_entry_matches_record() and observation.file_record.local_checksum:
|
|
142
144
|
return self._upload(record, "local only")
|
|
143
145
|
else:
|
|
144
|
-
local_checksum = await
|
|
146
|
+
local_checksum = await self._checksum_with_feedback(observation)
|
|
145
147
|
record = replace(record, local_checksum=local_checksum)
|
|
146
148
|
reason = "local only - update local record"
|
|
147
149
|
return self._upload(record, reason)
|
|
148
150
|
else:
|
|
149
|
-
local_checksum = await
|
|
151
|
+
local_checksum = await self._checksum_with_feedback(observation)
|
|
150
152
|
record = replace(record, local_checksum=local_checksum)
|
|
151
153
|
reason = "local only - add local record"
|
|
152
154
|
return self._upload(record, reason)
|
|
@@ -157,13 +159,13 @@ class SingleFileReconciler:
|
|
|
157
159
|
if observation.local_entry_matches_record():
|
|
158
160
|
return self._skip(record, "local entry matches existing record; no remote file to download")
|
|
159
161
|
else:
|
|
160
|
-
local_checksum = await
|
|
162
|
+
local_checksum = await self._checksum_with_feedback(observation)
|
|
161
163
|
record = replace(record, local_checksum=local_checksum)
|
|
162
164
|
reason = "local only, uploadable - update local record"
|
|
163
165
|
return self._db_update(record, reason)
|
|
164
166
|
else:
|
|
165
167
|
# No record in the project database
|
|
166
|
-
local_checksum = await
|
|
168
|
+
local_checksum = await self._checksum_with_feedback(observation)
|
|
167
169
|
record = replace(record, local_checksum=local_checksum)
|
|
168
170
|
reason = "local only, uploadable - add local record"
|
|
169
171
|
return self._db_update(record, reason)
|
|
@@ -265,14 +267,14 @@ class SingleFileReconciler:
|
|
|
265
267
|
# No action needed, local has been uploaded, but remote changed, and we could download it.
|
|
266
268
|
return self._skip(updated_record, "local matches remote, but remote file differs (downloadable)")
|
|
267
269
|
# Local entry is different. Compute new checksum and upload.
|
|
268
|
-
local_checksum = await
|
|
270
|
+
local_checksum = await self._checksum_with_feedback(observation)
|
|
269
271
|
updated_record = replace(updated_record, local_checksum=local_checksum)
|
|
270
272
|
return self._upload(updated_record, "local file changed")
|
|
271
273
|
|
|
272
274
|
async def _reconcile_both_sides_upload_no_record(self, observation: Observation,
|
|
273
275
|
updated_record: FileRecord) -> FileDecision:
|
|
274
276
|
# No file record. Let's compute the checksum and see if that matches the remote
|
|
275
|
-
local_checksum = await
|
|
277
|
+
local_checksum = await self._checksum_with_feedback(observation)
|
|
276
278
|
if local_checksum == observation.remote_entry.checksum:
|
|
277
279
|
return self._db_update(updated_record, "local matches remote, add file record")
|
|
278
280
|
updated_record = replace(updated_record, local_checksum=local_checksum)
|
|
@@ -304,7 +306,7 @@ class SingleFileReconciler:
|
|
|
304
306
|
else:
|
|
305
307
|
# The file record and the local file don't match. We need to compute the checksum for the
|
|
306
308
|
# local to determine if the local file has ever been uploaded.
|
|
307
|
-
local_checksum = await
|
|
309
|
+
local_checksum = await self._checksum_with_feedback(observation)
|
|
308
310
|
# Check if the local checksum matches the file record checksum
|
|
309
311
|
if local_checksum == observation.file_record.local_checksum:
|
|
310
312
|
# The checksum matches, so this file has been uploaded. So we can safely download the remote.
|
|
@@ -328,7 +330,7 @@ class SingleFileReconciler:
|
|
|
328
330
|
"""
|
|
329
331
|
# There is no file_record; that means we don't know the state of the local file. First, lets compute
|
|
330
332
|
# the checksum of the local file.
|
|
331
|
-
local_checksum = await
|
|
333
|
+
local_checksum = await self._checksum_with_feedback(observation)
|
|
332
334
|
if local_checksum == observation.remote_entry.checksum:
|
|
333
335
|
# Need to create a file_record
|
|
334
336
|
return self._db_update(updated_record, "local file matches remote checksum")
|
|
@@ -372,7 +374,8 @@ class SingleFileReconciler:
|
|
|
372
374
|
|
|
373
375
|
# Get previous versions and look for a match
|
|
374
376
|
try:
|
|
375
|
-
previous_versions = await asyncio.to_thread(self._proj.remote.get_file_versions, self._proj.id,
|
|
377
|
+
previous_versions = await asyncio.to_thread(self._proj.remote.get_file_versions, self._proj.id,
|
|
378
|
+
remote_file_id)
|
|
376
379
|
except Exception as e:
|
|
377
380
|
previous_versions = None
|
|
378
381
|
|
|
@@ -432,3 +435,42 @@ class SingleFileReconciler:
|
|
|
432
435
|
|
|
433
436
|
def _db_update(self, record: FileRecord, reason: str) -> FileDecision:
|
|
434
437
|
return self._decision("db_update", record, reason, updated=True)
|
|
438
|
+
|
|
439
|
+
async def _checksum_with_feedback(self, observation: Observation) -> str:
|
|
440
|
+
path = observation.local_entry.path
|
|
441
|
+
total_bytes = observation.local_entry.size
|
|
442
|
+
progress_id = f"checksum:{path.as_posix()}"
|
|
443
|
+
loop = asyncio.get_running_loop()
|
|
444
|
+
|
|
445
|
+
def make_line(bytes_read: int, status: str = "computing checksum") -> str:
|
|
446
|
+
percent = 100.0 if total_bytes == 0 else min((bytes_read / total_bytes) * 100.0, 100.0)
|
|
447
|
+
return (
|
|
448
|
+
f"{path.as_posix()} | "
|
|
449
|
+
f"checksum | "
|
|
450
|
+
f"{bytes_read} / {total_bytes} bytes | "
|
|
451
|
+
f"{percent:6.2f}% | "
|
|
452
|
+
f"{status}"
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
last_render_at = 0.0
|
|
456
|
+
|
|
457
|
+
def progress_callback(bytes_read: int) -> None:
|
|
458
|
+
nonlocal last_render_at
|
|
459
|
+
|
|
460
|
+
now = time.monotonic()
|
|
461
|
+
if now - last_render_at < 1.0 and bytes_read < total_bytes:
|
|
462
|
+
return
|
|
463
|
+
|
|
464
|
+
last_render_at = now
|
|
465
|
+
line = make_line(bytes_read)
|
|
466
|
+
asyncio.run_coroutine_threadsafe(
|
|
467
|
+
TerminalProgress.render(progress_id, line),
|
|
468
|
+
loop,
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
await TerminalProgress.render(progress_id, make_line(0))
|
|
472
|
+
|
|
473
|
+
result = await asyncio.to_thread(checksum, path.as_posix(), 1024 * 1024, progress_callback)
|
|
474
|
+
|
|
475
|
+
await TerminalProgress.render(progress_id, make_line(total_bytes, "checksum complete"))
|
|
476
|
+
return result
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
from typing import Dict, Any, Optional, Callable
|
|
3
|
+
import logging
|
|
3
4
|
|
|
4
5
|
from materials_commons.cli.requests import DownloadRequest
|
|
5
6
|
from materials_commons.cli.server.downloader.file_downloader import FileDownloader, logger
|
|
6
7
|
|
|
8
|
+
logger.setLevel(logging.WARNING)
|
|
7
9
|
|
|
8
10
|
class FileDownloadManager:
|
|
9
11
|
"""Manages multiple concurrent file downloads"""
|
|
@@ -13,6 +13,7 @@ import requests
|
|
|
13
13
|
from materials_commons.cli.old.functions import checksum
|
|
14
14
|
from materials_commons.cli.requests import DownloadRequest, DBWriteRequest
|
|
15
15
|
from materials_commons.cli.server import projects
|
|
16
|
+
from materials_commons.cli.terminal_progress import TerminalProgress
|
|
16
17
|
|
|
17
18
|
logger = logging.getLogger(__name__)
|
|
18
19
|
|
|
@@ -154,25 +155,35 @@ class FileDownloader:
|
|
|
154
155
|
|
|
155
156
|
async def _download_with_ranges(self, resume_from: int = 0) -> bool:
|
|
156
157
|
"""Download the file using HTTP Range requests with streaming"""
|
|
158
|
+
|
|
159
|
+
# Get the event loop so we can schedule progress rendering. Since the routine
|
|
160
|
+
# _download_with_ranges_blocking is not async it will need the loop do the
|
|
161
|
+
# progress. We have to get the async loop here in the async routine, and then
|
|
162
|
+
# pass it to the blocking download routine.
|
|
163
|
+
loop = asyncio.get_running_loop()
|
|
157
164
|
try:
|
|
158
|
-
|
|
165
|
+
await self._render_progress(status="starting")
|
|
166
|
+
success = await asyncio.to_thread(self._download_with_ranges_blocking, resume_from, loop)
|
|
159
167
|
|
|
160
168
|
if not success:
|
|
161
169
|
await self._save_metadata()
|
|
170
|
+
await self._render_progress(status="failed downloading")
|
|
171
|
+
return False
|
|
162
172
|
|
|
163
173
|
return success
|
|
164
174
|
|
|
165
175
|
except requests.exceptions.RequestException as e:
|
|
166
176
|
logger.error(f"HTTP request failed: {e}")
|
|
167
177
|
await self._save_metadata()
|
|
178
|
+
await self._render_progress(status=f"failed with HTTP exception: {e}")
|
|
168
179
|
return False
|
|
169
180
|
except Exception as e:
|
|
170
181
|
logger.error(f"Download error: {e}")
|
|
171
182
|
await self._save_metadata()
|
|
183
|
+
await self._render_progress(status=f"failed with exception: {e}")
|
|
172
184
|
return False
|
|
173
185
|
|
|
174
|
-
def _download_with_ranges_blocking(self, resume_from: int = 0) -> bool:
|
|
175
|
-
"Blocking download. Called from asyncio.to_thread()"
|
|
186
|
+
def _download_with_ranges_blocking(self, resume_from: int = 0, loop: Optional[asyncio.AbstractEventLoop] = None) -> bool:
|
|
176
187
|
"""Blocking download implementation. Runs in a worker thread."""
|
|
177
188
|
headers = {
|
|
178
189
|
'Authorization': f'Bearer {self.apitoken}',
|
|
@@ -213,6 +224,26 @@ class FileDownloader:
|
|
|
213
224
|
|
|
214
225
|
# Open the file in append mode if resuming, write mode otherwise
|
|
215
226
|
mode = 'ab' if resume_from > 0 else 'wb'
|
|
227
|
+
last_progress_render_at = 0.0
|
|
228
|
+
progress_interval_seconds = 0.25
|
|
229
|
+
|
|
230
|
+
def schedule_progress_render(status: str = "downloading", force: bool = False) -> None:
|
|
231
|
+
nonlocal last_progress_render_at
|
|
232
|
+
|
|
233
|
+
if loop is None:
|
|
234
|
+
return
|
|
235
|
+
|
|
236
|
+
now = time.monotonic()
|
|
237
|
+
if not force and now - last_progress_render_at < progress_interval_seconds:
|
|
238
|
+
return
|
|
239
|
+
|
|
240
|
+
last_progress_render_at = now
|
|
241
|
+
asyncio.run_coroutine_threadsafe(
|
|
242
|
+
self._render_progress(self.bytes_received, self.expected_size, status=status),
|
|
243
|
+
loop,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
schedule_progress_render(status="downloading", force=True)
|
|
216
247
|
|
|
217
248
|
with open(self.part_file, mode) as f:
|
|
218
249
|
for chunk in response.iter_content(chunk_size=self.chunk_size):
|
|
@@ -222,6 +253,7 @@ class FileDownloader:
|
|
|
222
253
|
|
|
223
254
|
if self.cancelled:
|
|
224
255
|
logger.info("Download cancelled")
|
|
256
|
+
schedule_progress_render(status="cancelled", force=True)
|
|
225
257
|
return False
|
|
226
258
|
|
|
227
259
|
if chunk:
|
|
@@ -229,6 +261,8 @@ class FileDownloader:
|
|
|
229
261
|
f.write(chunk)
|
|
230
262
|
self.bytes_received += len(chunk)
|
|
231
263
|
|
|
264
|
+
schedule_progress_render()
|
|
265
|
+
|
|
232
266
|
# Progress callback
|
|
233
267
|
if self.progress_callback:
|
|
234
268
|
self.progress_callback(self.bytes_received, self.expected_size)
|
|
@@ -237,6 +271,8 @@ class FileDownloader:
|
|
|
237
271
|
if self.bytes_received % (self.chunk_size * 10) == 0:
|
|
238
272
|
self._save_metadata_blocking()
|
|
239
273
|
|
|
274
|
+
schedule_progress_render(status="download complete", force=True)
|
|
275
|
+
|
|
240
276
|
return True
|
|
241
277
|
|
|
242
278
|
finally:
|
|
@@ -313,3 +349,23 @@ class FileDownloader:
|
|
|
313
349
|
"""Cancel the download"""
|
|
314
350
|
self.cancelled = True
|
|
315
351
|
logger.info(f"Download cancelled: {self.file_path}")
|
|
352
|
+
|
|
353
|
+
async def _render_progress(self,
|
|
354
|
+
bytes_received: Optional[int] = None,
|
|
355
|
+
total_bytes: Optional[int] = None,
|
|
356
|
+
status: str = "downloading") -> None:
|
|
357
|
+
"""Render this download's progress on a stable terminal row."""
|
|
358
|
+
bytes_received = self.bytes_received if bytes_received is None else bytes_received
|
|
359
|
+
total_bytes = self.expected_size if total_bytes is None else total_bytes
|
|
360
|
+
|
|
361
|
+
percent = 100.0 if total_bytes == 0 else min((bytes_received / total_bytes) * 100.0, 100.0)
|
|
362
|
+
|
|
363
|
+
line = (
|
|
364
|
+
f"{self.file_path.as_posix()} | "
|
|
365
|
+
f"download | "
|
|
366
|
+
f"{bytes_received} / {total_bytes} bytes | "
|
|
367
|
+
f"{percent:6.2f}% | "
|
|
368
|
+
f"{status}"
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
await TerminalProgress.render(f"download:{self.transfer_id}", line)
|
|
@@ -1,23 +1,32 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import hashlib
|
|
3
3
|
import logging
|
|
4
|
+
import sys
|
|
4
5
|
import time
|
|
5
6
|
import uuid
|
|
6
7
|
from dataclasses import replace
|
|
7
8
|
from datetime import datetime, timezone
|
|
8
9
|
from typing import Optional, Callable, Dict, Any
|
|
10
|
+
from materials_commons.cli.terminal_progress import TerminalProgress, format_bytes
|
|
9
11
|
|
|
10
12
|
import aiofiles
|
|
11
13
|
|
|
12
|
-
from materials_commons.cli.filedb import FileIndexDB
|
|
13
14
|
from materials_commons.cli.requests import UploadRequest, DBWriteRequest
|
|
14
15
|
|
|
15
16
|
logger = logging.getLogger(__name__)
|
|
17
|
+
logger.setLevel(logging.WARNING)
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
class FileUploader:
|
|
19
21
|
"""Handles upload of a single file via websocket using queue pattern"""
|
|
20
22
|
|
|
23
|
+
# These class variables are used to track progress of file uploads. They are shared across all
|
|
24
|
+
# instances of FileUploader. The lock keeps different writers to the terminal from interfering
|
|
25
|
+
# with each other.
|
|
26
|
+
_progress_lock = asyncio.Lock()
|
|
27
|
+
_progress_rows: Dict[str, int] = {}
|
|
28
|
+
_progress_row_count = 0
|
|
29
|
+
|
|
21
30
|
def __init__(
|
|
22
31
|
self,
|
|
23
32
|
ws_send_queue: asyncio.Queue,
|
|
@@ -40,6 +49,8 @@ class FileUploader:
|
|
|
40
49
|
self.bytes_sent = 0
|
|
41
50
|
self.paused = False
|
|
42
51
|
self.cancelled = False
|
|
52
|
+
self.upload_started_at = time.monotonic()
|
|
53
|
+
self._last_progress_render_at = 0.0
|
|
43
54
|
|
|
44
55
|
# These fields are referenced multiple times. They are pulled out to make code more readable.
|
|
45
56
|
self.file_path = upload_request.observation.path
|
|
@@ -66,20 +77,25 @@ class FileUploader:
|
|
|
66
77
|
print(f"Skipping {self.file_path.as_posix()} (empty file)...")
|
|
67
78
|
return True
|
|
68
79
|
|
|
80
|
+
await self._render_progress(status="starting")
|
|
81
|
+
|
|
69
82
|
# Step 1: Initialize the transfer
|
|
70
83
|
if not await self._send_transfer_init():
|
|
84
|
+
await self._render_progress(status="failed on initialization")
|
|
71
85
|
return False
|
|
72
86
|
|
|
73
87
|
# Step 2: Wait for acceptance
|
|
74
88
|
if not await self._wait_for_acceptance():
|
|
75
89
|
if self._already_uploaded:
|
|
76
90
|
logger.info(f"File {self.file_path} already uploaded")
|
|
77
|
-
|
|
91
|
+
await self._render_progress(self.file_size, self.file_size, status="already uploaded")
|
|
78
92
|
return True
|
|
93
|
+
await self._render_progress(status="failed on acceptance")
|
|
79
94
|
return False
|
|
80
95
|
|
|
81
96
|
# # Step 3: Send file chunks
|
|
82
97
|
if not await self._send_chunks2():
|
|
98
|
+
await self._render_progress(status="failed on chunk sending")
|
|
83
99
|
return False
|
|
84
100
|
|
|
85
101
|
# ### New
|
|
@@ -99,19 +115,21 @@ class FileUploader:
|
|
|
99
115
|
|
|
100
116
|
# Step 4: Send completion
|
|
101
117
|
if not await self._send_transfer_complete():
|
|
118
|
+
await self._render_progress(status="failed on completion")
|
|
102
119
|
return False
|
|
103
120
|
|
|
104
121
|
# Step 5: Wait for finalization
|
|
105
122
|
if not await self._wait_for_finalization():
|
|
123
|
+
await self._render_progress(status="failed on finalization")
|
|
106
124
|
return False
|
|
107
125
|
|
|
108
126
|
logger.info(f"File upload complete for {self.file_path}")
|
|
109
|
-
|
|
127
|
+
await self._render_progress(self.file_size, self.file_size, status="complete")
|
|
110
128
|
return True
|
|
111
129
|
|
|
112
130
|
except Exception as e:
|
|
113
131
|
logger.error(f"Error uploading file {self.file_path}: {e}")
|
|
114
|
-
|
|
132
|
+
await self._render_progress(status=f"error: {e}")
|
|
115
133
|
return False
|
|
116
134
|
|
|
117
135
|
async def resume(self, resume_from_byte: int, resume_from_chunk: int) -> bool:
|
|
@@ -323,6 +341,8 @@ class FileUploader:
|
|
|
323
341
|
|
|
324
342
|
self.bytes_sent = bytes_received
|
|
325
343
|
|
|
344
|
+
await self._render_progress(self.bytes_sent, self.file_size, status="uploading")
|
|
345
|
+
|
|
326
346
|
# Progress callback outside lock
|
|
327
347
|
if self.progress_callback:
|
|
328
348
|
self.progress_callback(self.bytes_sent, self.file_size)
|
|
@@ -389,6 +409,8 @@ class FileUploader:
|
|
|
389
409
|
self.bytes_sent = ack_msg["payload"]["bytes_received"]
|
|
390
410
|
sequence += 1
|
|
391
411
|
|
|
412
|
+
await self._render_progress(self.bytes_sent, self.file_size, status="uploading")
|
|
413
|
+
|
|
392
414
|
# Progress callback
|
|
393
415
|
if self.progress_callback:
|
|
394
416
|
self.progress_callback(self.bytes_sent, self.file_size)
|
|
@@ -473,6 +495,35 @@ class FileUploader:
|
|
|
473
495
|
self.cancelled = True
|
|
474
496
|
logger.info(f"Upload cancelled: {self.file_path.name}")
|
|
475
497
|
|
|
498
|
+
async def _render_progress(self,
|
|
499
|
+
bytes_sent: Optional[int] = None,
|
|
500
|
+
total_bytes: Optional[int] = None,
|
|
501
|
+
status: str = "uploading") -> None:
|
|
502
|
+
"""Render this upload's progress on a stable terminal row."""
|
|
503
|
+
|
|
504
|
+
bytes_sent = self.bytes_sent if bytes_sent is None else bytes_sent
|
|
505
|
+
total_bytes = self.file_size if total_bytes is None else total_bytes
|
|
506
|
+
|
|
507
|
+
force_render = status != "uploading" or bytes_sent >= total_bytes
|
|
508
|
+
now = time.monotonic()
|
|
509
|
+
if not force_render and now - self._last_progress_render_at < 0.25:
|
|
510
|
+
return
|
|
511
|
+
self._last_progress_render_at = now
|
|
512
|
+
|
|
513
|
+
percent = 100.0 if total_bytes == 0 else min((bytes_sent / total_bytes) * 100.0, 100.0)
|
|
514
|
+
elapsed = max(time.monotonic() - self.upload_started_at, 0.001)
|
|
515
|
+
rate = bytes_sent / elapsed
|
|
516
|
+
|
|
517
|
+
line = (
|
|
518
|
+
f"{self.file_path.as_posix()} | "
|
|
519
|
+
f"{format_bytes(bytes_sent)} / {format_bytes(total_bytes)} | "
|
|
520
|
+
f"{percent:6.2f}% | "
|
|
521
|
+
f"{format_bytes(rate)}/s | "
|
|
522
|
+
f"{status}"
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
await TerminalProgress.render(self.transfer_id, line)
|
|
526
|
+
|
|
476
527
|
def _calculate_md5(self, chunk_size=8192) -> str:
|
|
477
528
|
"""Calculate md5 hash of file"""
|
|
478
529
|
md5_hash = hashlib.md5()
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import sys
|
|
3
|
+
from typing import Dict
|
|
4
|
+
|
|
5
|
+
def format_bytes(value: float) -> str:
|
|
6
|
+
"""Format bytes as a human-readable value."""
|
|
7
|
+
units = ("B", "KB", "MB", "GB", "TB")
|
|
8
|
+
size = float(value)
|
|
9
|
+
|
|
10
|
+
for unit in units:
|
|
11
|
+
if size < 1024.0 or unit == units[-1]:
|
|
12
|
+
return f"{size:.1f} {unit}"
|
|
13
|
+
size /= 1024.0
|
|
14
|
+
|
|
15
|
+
return f"{size:.1f} TB"
|
|
16
|
+
|
|
17
|
+
class TerminalProgress:
|
|
18
|
+
_lock = asyncio.Lock()
|
|
19
|
+
_rows: Dict[str, int] = {}
|
|
20
|
+
_row_count = 0
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
async def render(cls, progress_id: str, line: str):
|
|
24
|
+
if not sys.stdout.isatty():
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
async with cls._lock:
|
|
28
|
+
if progress_id not in cls._rows:
|
|
29
|
+
cls._rows[progress_id] = cls._row_count
|
|
30
|
+
cls._row_count += 1
|
|
31
|
+
print()
|
|
32
|
+
|
|
33
|
+
row = cls._rows[progress_id]
|
|
34
|
+
rows_from_cursor = cls._row_count - row
|
|
35
|
+
|
|
36
|
+
terminal_width = 120
|
|
37
|
+
if len(line) > terminal_width:
|
|
38
|
+
line = line[:terminal_width - 3] + "..."
|
|
39
|
+
|
|
40
|
+
sys.stdout.write("\033[s")
|
|
41
|
+
sys.stdout.write(f"\033[{rows_from_cursor}A")
|
|
42
|
+
sys.stdout.write("\r\033[K")
|
|
43
|
+
sys.stdout.write(line)
|
|
44
|
+
sys.stdout.write("\033[u")
|
|
45
|
+
sys.stdout.flush()
|
|
@@ -15,6 +15,7 @@ materials_commons/cli/reconcile2.py
|
|
|
15
15
|
materials_commons/cli/reconcile3.py
|
|
16
16
|
materials_commons/cli/requests.py
|
|
17
17
|
materials_commons/cli/run.py
|
|
18
|
+
materials_commons/cli/terminal_progress.py
|
|
18
19
|
materials_commons/cli/walk.py
|
|
19
20
|
materials_commons/cli/old/__init__.py
|
|
20
21
|
materials_commons/cli/old/cloned_project.py
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = '2.3.0b9'
|
|
File without changes
|
|
File without changes
|
{materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/LICENSE.txt
RENAMED
|
File without changes
|
|
File without changes
|
{materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/config.py
RENAMED
|
File without changes
|
{materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/downloader.py
RENAMED
|
File without changes
|
{materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/filedb.py
RENAMED
|
File without changes
|
|
File without changes
|
{materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/models.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/old/globus.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/old/treedb.py
RENAMED
|
File without changes
|
|
File without changes
|
{materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/reconcile2.py
RENAMED
|
File without changes
|
{materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/requests.py
RENAMED
|
File without changes
|
{materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/run.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/materials_commons/cli/walk.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/test/test_treecompare_no_cache.py
RENAMED
|
File without changes
|
{materials_commons_cli-2.3.0b9 → materials_commons_cli-2.3.0b10}/test/test_treecompare_with_cache.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|