materials-commons-cli 2.2.3__tar.gz → 2.3.0b2__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.2.3 → materials_commons_cli-2.3.0b2}/PKG-INFO +1 -1
- materials_commons_cli-2.3.0b2/materials_commons/cli/__init__.py +1 -0
- materials_commons_cli-2.3.0b2/materials_commons/cli/async_reconciler.py +104 -0
- materials_commons_cli-2.3.0b2/materials_commons/cli/config.py +224 -0
- materials_commons_cli-2.3.0b2/materials_commons/cli/downloader.py +116 -0
- materials_commons_cli-2.3.0b2/materials_commons/cli/filedb.py +242 -0
- materials_commons_cli-2.3.0b2/materials_commons/cli/local_project.py +88 -0
- materials_commons_cli-2.3.0b2/materials_commons/cli/models.py +417 -0
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/parser.py +11 -7
- materials_commons_cli-2.3.0b2/materials_commons/cli/reconcile2.py +478 -0
- materials_commons_cli-2.3.0b2/materials_commons/cli/reconcile3.py +434 -0
- materials_commons_cli-2.3.0b2/materials_commons/cli/requests.py +44 -0
- materials_commons_cli-2.3.0b2/materials_commons/cli/run.py +65 -0
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/server/downloader/file_download_manager.py +17 -17
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/server/downloader/file_downloader.py +93 -69
- materials_commons_cli-2.3.0b2/materials_commons/cli/server/indexer/file_index_manager.py +82 -0
- materials_commons_cli-2.2.3/materials_commons/cli/server/command_handlers.py → materials_commons_cli-2.3.0b2/materials_commons/cli/server/ocommand_handlers.py +210 -13
- materials_commons_cli-2.3.0b2/materials_commons/cli/server/project_filedbs.py +50 -0
- materials_commons_cli-2.3.0b2/materials_commons/cli/server/projects.py +234 -0
- materials_commons_cli-2.3.0b2/materials_commons/cli/server/service_container.py +97 -0
- materials_commons_cli-2.3.0b2/materials_commons/cli/server/service_runtime.py +96 -0
- materials_commons_cli-2.2.3/materials_commons/cli/server/uploader/api.py → materials_commons_cli-2.3.0b2/materials_commons/cli/server/uploader/client_api.py +33 -26
- materials_commons_cli-2.2.3/materials_commons/cli/server/uploader/file_transfer_manager.py → materials_commons_cli-2.3.0b2/materials_commons/cli/server/uploader/file_upload_manager.py +28 -13
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/server/uploader/file_uploader.py +67 -27
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/server/websocket_server.py +84 -73
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/subcommands/clone.py +1 -4
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/subcommands/config.py +1 -4
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/subcommands/dataset.py +4 -5
- materials_commons_cli-2.3.0b2/materials_commons/cli/subcommands/down.py +133 -0
- materials_commons_cli-2.3.0b2/materials_commons/cli/subcommands/down2.py +29 -0
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/subcommands/expt.py +2 -3
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/subcommands/fetch.py +3 -5
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/subcommands/globus.py +4 -6
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/subcommands/init.py +2 -4
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/subcommands/ls.py +14 -6
- materials_commons_cli-2.3.0b2/materials_commons/cli/subcommands/ls2.py +110 -0
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/subcommands/mkdir.py +3 -4
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/subcommands/mv.py +4 -6
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/subcommands/proj.py +4 -7
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/subcommands/remote.py +2 -3
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/subcommands/rm.py +3 -5
- materials_commons_cli-2.3.0b2/materials_commons/cli/subcommands/scan.py +145 -0
- materials_commons_cli-2.3.0b2/materials_commons/cli/subcommands/server.py +66 -0
- materials_commons_cli-2.3.0b2/materials_commons/cli/subcommands/up.py +242 -0
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/subcommands/versions.py +4 -4
- materials_commons_cli-2.3.0b2/materials_commons/cli/walk.py +258 -0
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons_cli.egg-info/PKG-INFO +1 -1
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons_cli.egg-info/SOURCES.txt +18 -15
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/test/test_api.py +2 -4
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/test/test_functions.py +2 -2
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/test/test_mc_dataset.py +2 -7
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/test/test_mc_down.py +2 -4
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/test/test_mc_mkdir.py +5 -5
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/test/test_mc_mv.py +4 -6
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/test/test_mc_proj.py +3 -3
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/test/test_mc_rm.py +4 -6
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/test/test_mc_up.py +2 -3
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/test/test_treecompare_no_cache.py +2 -3
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/test/test_treecompare_with_cache.py +4 -5
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/test/test_treedb.py +4 -3
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/test/test_user_config.py +1 -2
- materials_commons_cli-2.2.3/materials_commons/cli/__init__.py +0 -1
- materials_commons_cli-2.2.3/materials_commons/cli/cloned_project.py +0 -224
- materials_commons_cli-2.2.3/materials_commons/cli/exceptions.py +0 -12
- materials_commons_cli-2.2.3/materials_commons/cli/file_functions.py +0 -138
- materials_commons_cli-2.2.3/materials_commons/cli/filedb.py +0 -110
- materials_commons_cli-2.2.3/materials_commons/cli/functions.py +0 -745
- materials_commons_cli-2.2.3/materials_commons/cli/globus.py +0 -418
- materials_commons_cli-2.2.3/materials_commons/cli/list_objects.py +0 -404
- materials_commons_cli-2.2.3/materials_commons/cli/print_formatter.py +0 -105
- materials_commons_cli-2.2.3/materials_commons/cli/server/indexer/file_index_manager.py +0 -84
- materials_commons_cli-2.2.3/materials_commons/cli/server/projects.py +0 -88
- materials_commons_cli-2.2.3/materials_commons/cli/sqltable.py +0 -213
- materials_commons_cli-2.2.3/materials_commons/cli/subcommands/down.py +0 -340
- materials_commons_cli-2.2.3/materials_commons/cli/subcommands/scan.py +0 -83
- materials_commons_cli-2.2.3/materials_commons/cli/subcommands/server.py +0 -75
- materials_commons_cli-2.2.3/materials_commons/cli/subcommands/up.py +0 -227
- materials_commons_cli-2.2.3/materials_commons/cli/tmp_functions.py +0 -41
- materials_commons_cli-2.2.3/materials_commons/cli/tree_functions.py +0 -1737
- materials_commons_cli-2.2.3/materials_commons/cli/treedb.py +0 -646
- materials_commons_cli-2.2.3/materials_commons/cli/user_config.py +0 -216
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/LICENSE +0 -0
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/README.md +0 -0
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/LICENSE.txt +0 -0
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/server/__init__.py +0 -0
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/server/downloader/__init__.py +0 -0
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/server/indexer/__init__.py +0 -0
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/server/local_rest_server.py +0 -0
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/server/uploader/__init__.py +0 -0
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/subcommands/__init__.py +0 -0
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons_cli.egg-info/dependency_links.txt +0 -0
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons_cli.egg-info/entry_points.txt +0 -0
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons_cli.egg-info/not-zip-safe +0 -0
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons_cli.egg-info/requires.txt +0 -0
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons_cli.egg-info/top_level.txt +0 -0
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/setup.cfg +0 -0
- {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/setup.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '2.3.0b2'
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from datetime import timezone
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional, AsyncIterator
|
|
5
|
+
|
|
6
|
+
from materials_commons.cli.filedb import FileIndexDB
|
|
7
|
+
from materials_commons.cli.local_project import LocalProject
|
|
8
|
+
from materials_commons.cli.models import FileState, FileDecision, Observation, RemoteFileEntry
|
|
9
|
+
from materials_commons.cli.reconcile3 import SingleFileReconciler, ReconcileMode
|
|
10
|
+
from materials_commons.cli.server import projects
|
|
11
|
+
from materials_commons.cli.walk import ListDirFunc, IgnoreFunc, async_walk, path_to_local_file_entry
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AsyncReconciler:
|
|
15
|
+
def __init__(self,
|
|
16
|
+
proj: LocalProject,
|
|
17
|
+
db: FileIndexDB,
|
|
18
|
+
reconcile_mode: ReconcileMode,
|
|
19
|
+
# recompute_checksum: bool = True,
|
|
20
|
+
max_concurrent: int = 10):
|
|
21
|
+
self.proj = proj
|
|
22
|
+
self.db = db
|
|
23
|
+
# self.recompute_checksum = recompute_checksum
|
|
24
|
+
self.max_concurrent = max_concurrent
|
|
25
|
+
self.reconcile_mode = reconcile_mode
|
|
26
|
+
self.reconciler = SingleFileReconciler(proj=self.proj, mode=reconcile_mode)
|
|
27
|
+
|
|
28
|
+
async def walk(self, path: str | Path, listdir_fn: ListDirFunc, recursive: bool = False,
|
|
29
|
+
ignore_fn: Optional[IgnoreFunc] = None) -> AsyncIterator[tuple[Path, dict[str, FileState]]]:
|
|
30
|
+
sem = asyncio.Semaphore(self.max_concurrent)
|
|
31
|
+
|
|
32
|
+
async def single_entry_reconcile(obs: Observation) -> FileDecision:
|
|
33
|
+
async with sem:
|
|
34
|
+
return await self.reconciler.reconcile_file(obs)
|
|
35
|
+
|
|
36
|
+
async for current_path, observations in async_walk(path, recursive=recursive, listdir_fn=listdir_fn,
|
|
37
|
+
ignore_fn=ignore_fn):
|
|
38
|
+
path_states: dict[str, FileState] = {}
|
|
39
|
+
remote_dir = projects.local_to_remote_project_path(Path(self.proj.local_path), Path(current_path))
|
|
40
|
+
|
|
41
|
+
# Create a map of file_records to name
|
|
42
|
+
file_records = await self.db.get_files_by_dir(remote_dir.as_posix())
|
|
43
|
+
file_records_map = {file_record.name: file_record for file_record in file_records}
|
|
44
|
+
|
|
45
|
+
# Go through all the observations and match to the file records
|
|
46
|
+
for obs in observations:
|
|
47
|
+
obs.file_record = file_records_map.get(obs.name, None)
|
|
48
|
+
path_states[obs.name] = FileState(observation=obs)
|
|
49
|
+
|
|
50
|
+
results = await asyncio.gather(
|
|
51
|
+
*[single_entry_reconcile(state.observation) for state in path_states.values()],
|
|
52
|
+
return_exceptions=True)
|
|
53
|
+
|
|
54
|
+
# At this point path_entries contains entries in one of 4 states:
|
|
55
|
+
# 1. Both remote and local entries exist
|
|
56
|
+
# 2. Only remote entry exists
|
|
57
|
+
# 3. Only local entry exists
|
|
58
|
+
# 4. Lookup failed, and we got an exception
|
|
59
|
+
|
|
60
|
+
for state, result in zip(path_states.values(), results):
|
|
61
|
+
if isinstance(result, Exception):
|
|
62
|
+
state.exception = result
|
|
63
|
+
else:
|
|
64
|
+
state.file_decision = result
|
|
65
|
+
|
|
66
|
+
yield current_path, path_states
|
|
67
|
+
|
|
68
|
+
async def reconcile_file(self, path: str | Path) -> FileState:
|
|
69
|
+
remote_project_path = projects.local_to_remote_project_path(Path(self.proj.local_path), Path(path))
|
|
70
|
+
file_record = await self.db.get_file_by_path(remote_project_path.as_posix())
|
|
71
|
+
try:
|
|
72
|
+
mc_remote_file = await asyncio.to_thread(self.proj.remote.get_file_by_path, self.proj.id,
|
|
73
|
+
remote_project_path.as_posix())
|
|
74
|
+
except Exception as e:
|
|
75
|
+
mc_remote_file = None
|
|
76
|
+
local_file_entry = path_to_local_file_entry(Path(path))
|
|
77
|
+
if mc_remote_file:
|
|
78
|
+
remote_file_entry = RemoteFileEntry(
|
|
79
|
+
path=Path(mc_remote_file.path),
|
|
80
|
+
name=mc_remote_file.name,
|
|
81
|
+
kind="file",
|
|
82
|
+
size=mc_remote_file.size,
|
|
83
|
+
mtime_ns=int(mc_remote_file.updated_at.replace(tzinfo=timezone.utc).timestamp() * 1_000_000_000),
|
|
84
|
+
ctime_ns=int(mc_remote_file.created_at.replace(tzinfo=timezone.utc).timestamp() * 1_000_000_000),
|
|
85
|
+
remote_file_id=getattr(mc_remote_file, "id", None),
|
|
86
|
+
checksum=getattr(mc_remote_file, "checksum", None),
|
|
87
|
+
raw=mc_remote_file,
|
|
88
|
+
)
|
|
89
|
+
else:
|
|
90
|
+
remote_file_entry = None
|
|
91
|
+
|
|
92
|
+
obs = Observation(
|
|
93
|
+
remote_entry=remote_file_entry,
|
|
94
|
+
local_entry=local_file_entry,
|
|
95
|
+
file_record=file_record,
|
|
96
|
+
local_path=Path(path),
|
|
97
|
+
remote_path=remote_project_path,
|
|
98
|
+
)
|
|
99
|
+
decision = await self.reconciler.reconcile_file(obs)
|
|
100
|
+
|
|
101
|
+
return FileState(
|
|
102
|
+
observation=obs,
|
|
103
|
+
file_decision=decision,
|
|
104
|
+
)
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import getpass
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
from dataclasses import asdict, dataclass, field
|
|
6
|
+
from os.path import join
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
from materials_commons.api.client import Client
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def default_config_dir() -> str:
|
|
14
|
+
user = getpass.getuser()
|
|
15
|
+
return join(os.path.expanduser("~" + user), ".materialscommons")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def resolve_config_file(
|
|
19
|
+
config_dir_path: Optional[str],
|
|
20
|
+
config_file_name: str,
|
|
21
|
+
) -> str:
|
|
22
|
+
if not config_dir_path:
|
|
23
|
+
config_dir_path = default_config_dir()
|
|
24
|
+
return join(config_dir_path, config_file_name)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def default_data() -> Dict[str, Any]:
|
|
28
|
+
return {
|
|
29
|
+
"apikey": None,
|
|
30
|
+
"mcurl": None,
|
|
31
|
+
"email": None,
|
|
32
|
+
"remotes": [],
|
|
33
|
+
"globus": {},
|
|
34
|
+
"client_uuid": None,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def load_json(path: str) -> Dict[str, Any]:
|
|
39
|
+
if os.path.exists(path):
|
|
40
|
+
with open(path, "r") as f:
|
|
41
|
+
return json.load(f)
|
|
42
|
+
return default_data()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def apply_environment(data: Dict[str, Any]) -> Dict[str, Any]:
|
|
46
|
+
data = dict(data)
|
|
47
|
+
|
|
48
|
+
env_apikey = os.environ.get("MC_API_KEY")
|
|
49
|
+
env_mcurl = os.environ.get("MC_API_URL")
|
|
50
|
+
env_email = os.environ.get("MC_API_EMAIL")
|
|
51
|
+
|
|
52
|
+
if env_apikey:
|
|
53
|
+
data["apikey"] = env_apikey
|
|
54
|
+
if env_mcurl:
|
|
55
|
+
data["mcurl"] = env_mcurl
|
|
56
|
+
if env_email:
|
|
57
|
+
data["email"] = env_email
|
|
58
|
+
|
|
59
|
+
return data
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def apply_overrides(
|
|
63
|
+
data: Dict[str, Any],
|
|
64
|
+
override_config: Optional[Dict[str, Any]],
|
|
65
|
+
) -> Dict[str, Any]:
|
|
66
|
+
data = dict(data)
|
|
67
|
+
if override_config:
|
|
68
|
+
data.update(override_config)
|
|
69
|
+
return data
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def normalize_default_remote(data: Dict[str, Any]) -> Dict[str, Any]:
|
|
73
|
+
data = dict(data)
|
|
74
|
+
|
|
75
|
+
if data.get("mcurl") and data.get("apikey") and data.get("email"):
|
|
76
|
+
data["default_remote"] = {
|
|
77
|
+
"mcurl": data.get("mcurl"),
|
|
78
|
+
"email": data.get("email"),
|
|
79
|
+
"mcapikey": data.get("apikey"),
|
|
80
|
+
}
|
|
81
|
+
elif "default_remote" not in data:
|
|
82
|
+
data["default_remote"] = {
|
|
83
|
+
"mcurl": data.get("mcurl"),
|
|
84
|
+
"email": "__default__",
|
|
85
|
+
"mcapikey": data.get("apikey"),
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return data
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(eq=True)
|
|
92
|
+
class RemoteConfig:
|
|
93
|
+
mcurl: Optional[str] = None
|
|
94
|
+
email: Optional[str] = None
|
|
95
|
+
mcapikey: Optional[str] = None
|
|
96
|
+
|
|
97
|
+
def get_params(self) -> Dict[str, Optional[str]]:
|
|
98
|
+
return {"apikey": self.mcapikey}
|
|
99
|
+
|
|
100
|
+
def make_client(self) -> Client:
|
|
101
|
+
return Client(self.mcapikey, self.mcurl)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass
|
|
105
|
+
class GlobusConfig:
|
|
106
|
+
transfer_rt: Optional[str] = None
|
|
107
|
+
endpoint_id: Optional[str] = None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass
|
|
111
|
+
class Config:
|
|
112
|
+
"""Configuration variables."""
|
|
113
|
+
|
|
114
|
+
default_remote: RemoteConfig = field(default_factory=RemoteConfig)
|
|
115
|
+
remotes: List[RemoteConfig] = field(default_factory=list)
|
|
116
|
+
globus: GlobusConfig = field(default_factory=GlobusConfig)
|
|
117
|
+
developer_mode: bool = False
|
|
118
|
+
REST_logging: bool = False
|
|
119
|
+
client_uuid: Optional[str] = None
|
|
120
|
+
config_file: Optional[str] = None
|
|
121
|
+
|
|
122
|
+
@classmethod
|
|
123
|
+
def load(
|
|
124
|
+
cls,
|
|
125
|
+
config_dir_path: Optional[str] = None,
|
|
126
|
+
config_file_name: str = "config.json",
|
|
127
|
+
override_config: Optional[Dict[str, Any]] = None,
|
|
128
|
+
) -> "Config":
|
|
129
|
+
config_file = resolve_config_file(config_dir_path, config_file_name)
|
|
130
|
+
|
|
131
|
+
data = load_json(config_file)
|
|
132
|
+
data = apply_environment(data)
|
|
133
|
+
data = apply_overrides(data, override_config)
|
|
134
|
+
data = normalize_default_remote(data)
|
|
135
|
+
|
|
136
|
+
return cls(
|
|
137
|
+
default_remote=RemoteConfig(**data.get("default_remote", {})),
|
|
138
|
+
remotes=[RemoteConfig(**kwargs) for kwargs in data.get("remotes", [])],
|
|
139
|
+
globus=GlobusConfig(**data.get("globus", {})),
|
|
140
|
+
developer_mode=data.get("developer_mode", False),
|
|
141
|
+
REST_logging=data.get("REST_logging", False),
|
|
142
|
+
client_uuid=data.get("client_uuid"),
|
|
143
|
+
config_file=config_file,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
147
|
+
return {
|
|
148
|
+
"default_remote": asdict(self.default_remote),
|
|
149
|
+
"remotes": [asdict(value) for value in self.remotes],
|
|
150
|
+
"globus": asdict(self.globus),
|
|
151
|
+
"developer_mode": self.developer_mode,
|
|
152
|
+
"REST_logging": self.REST_logging,
|
|
153
|
+
"client_uuid": self.client_uuid or "",
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
def save(self) -> None:
|
|
157
|
+
if not self.config_file:
|
|
158
|
+
raise ValueError("Cannot save Config without config_file")
|
|
159
|
+
|
|
160
|
+
config_dir_path = os.path.dirname(self.config_file)
|
|
161
|
+
if config_dir_path and not os.path.exists(config_dir_path):
|
|
162
|
+
os.makedirs(config_dir_path, exist_ok=True)
|
|
163
|
+
|
|
164
|
+
with open(self.config_file, "w") as f:
|
|
165
|
+
json.dump(self.to_dict(), f, indent=2)
|
|
166
|
+
|
|
167
|
+
os.chmod(self.config_file, 0o600)
|
|
168
|
+
|
|
169
|
+
def find_remote(
|
|
170
|
+
self,
|
|
171
|
+
email: Optional[str],
|
|
172
|
+
mcurl: Optional[str],
|
|
173
|
+
) -> Optional[RemoteConfig]:
|
|
174
|
+
for remote in self.remotes:
|
|
175
|
+
if remote.email == email and remote.mcurl == mcurl:
|
|
176
|
+
return remote
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def get_remote_config_and_login_if_necessary(
|
|
181
|
+
email: Optional[str] = None,
|
|
182
|
+
mcurl: Optional[str] = None,
|
|
183
|
+
) -> RemoteConfig:
|
|
184
|
+
"""Prompt for login if remote is not stored in Config."""
|
|
185
|
+
config = Config.load()
|
|
186
|
+
existing_remote = config.find_remote(email=email, mcurl=mcurl)
|
|
187
|
+
|
|
188
|
+
if existing_remote is not None:
|
|
189
|
+
return existing_remote
|
|
190
|
+
|
|
191
|
+
remote_config = RemoteConfig(mcurl=mcurl, email=email)
|
|
192
|
+
|
|
193
|
+
while True:
|
|
194
|
+
try:
|
|
195
|
+
print("Login to:", email, mcurl)
|
|
196
|
+
password = getpass.getpass(prompt="password: ")
|
|
197
|
+
remote_config.mcapikey = Client.get_apikey(email, password, mcurl)
|
|
198
|
+
break
|
|
199
|
+
except requests.exceptions.HTTPError as e:
|
|
200
|
+
print(str(e))
|
|
201
|
+
if not re.search("Bad Request for url", str(e)):
|
|
202
|
+
raise
|
|
203
|
+
print("Wrong password for " + email + " at " + mcurl)
|
|
204
|
+
except requests.exceptions.ConnectionError as e:
|
|
205
|
+
print("Could not connect to " + mcurl)
|
|
206
|
+
raise e
|
|
207
|
+
|
|
208
|
+
config.remotes.append(remote_config)
|
|
209
|
+
config.save()
|
|
210
|
+
|
|
211
|
+
print()
|
|
212
|
+
print("Added APIKey for", email, "at", mcurl, "to", config.config_file)
|
|
213
|
+
print()
|
|
214
|
+
|
|
215
|
+
return remote_config
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def make_client_and_login_if_necessary(
|
|
219
|
+
email: Optional[str] = None,
|
|
220
|
+
mcurl: Optional[str] = None,
|
|
221
|
+
) -> Client:
|
|
222
|
+
"""Make Client, prompting for login if remote is not stored in Config."""
|
|
223
|
+
remote_config = get_remote_config_and_login_if_necessary(mcurl=mcurl, email=email)
|
|
224
|
+
return remote_config.make_client()
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from igittigitt import IgnoreParser, igittigitt
|
|
5
|
+
|
|
6
|
+
from materials_commons.cli.async_reconciler import AsyncReconciler
|
|
7
|
+
from materials_commons.cli.filedb import FileIndexDB
|
|
8
|
+
from materials_commons.cli.local_project import LocalProject
|
|
9
|
+
from materials_commons.cli.requests import DownloadRequest
|
|
10
|
+
from materials_commons.cli.server.service_container import ServiceContainer
|
|
11
|
+
from materials_commons.cli.server.service_runtime import ServiceRuntime
|
|
12
|
+
from materials_commons.cli.walk import make_merged_listdir_func
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Downloader:
|
|
18
|
+
def __init__(self, proj: LocalProject, db: FileIndexDB, ignore_parser: IgnoreParser, container: ServiceContainer,
|
|
19
|
+
service_runtime: ServiceRuntime):
|
|
20
|
+
self.proj = proj
|
|
21
|
+
self.db = db
|
|
22
|
+
self.container = container
|
|
23
|
+
self.service_runtime = service_runtime
|
|
24
|
+
self.ignore_parser = ignore_parser
|
|
25
|
+
self.async_reconciler = AsyncReconciler(db=db, proj=proj, reconcile_mode="download")
|
|
26
|
+
self.listdir_fn = make_merged_listdir_func(proj)
|
|
27
|
+
self.transfer_ids = []
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
async def init(cls, proj: LocalProject):
|
|
31
|
+
db = await proj.get_filedb()
|
|
32
|
+
|
|
33
|
+
# Setup ignore parser
|
|
34
|
+
ignore_parser = igittigitt.IgnoreParser()
|
|
35
|
+
ignore_parser.parse_rule_files(base_dir=proj.local_path, filename=".mcignore", add_default_patterns=False)
|
|
36
|
+
|
|
37
|
+
# Start services
|
|
38
|
+
container = ServiceContainer.create()
|
|
39
|
+
service_runtime = ServiceRuntime(container)
|
|
40
|
+
await service_runtime.start(db_manager=True, file_download_manager=True)
|
|
41
|
+
|
|
42
|
+
return cls(proj=proj, db=db, ignore_parser=ignore_parser, container=container, service_runtime=service_runtime)
|
|
43
|
+
|
|
44
|
+
async def run(self, paths: list[str | Path], recursive: bool = False, force: bool = False):
|
|
45
|
+
try:
|
|
46
|
+
for path in paths:
|
|
47
|
+
p = Path(path)
|
|
48
|
+
if p.is_dir():
|
|
49
|
+
# Walk and download local dir
|
|
50
|
+
await self._download_dir(p, recursive=recursive)
|
|
51
|
+
elif p.is_file():
|
|
52
|
+
# Download local file
|
|
53
|
+
await self._download_file(p)
|
|
54
|
+
else:
|
|
55
|
+
# If we are here, then the path entry is a remote path. We look up the path to
|
|
56
|
+
# determine the type of download to perform.
|
|
57
|
+
remote_path = self.proj.to_remote_path(p)
|
|
58
|
+
try:
|
|
59
|
+
f = self.proj.remote.get_file_by_path(self.proj.id, remote_path.as_posix())
|
|
60
|
+
except Exception as e:
|
|
61
|
+
# logger.error(f"Error getting file by path {remote_path}: {e}")
|
|
62
|
+
print(f"No such remote file {p}")
|
|
63
|
+
continue
|
|
64
|
+
|
|
65
|
+
if f is None:
|
|
66
|
+
continue
|
|
67
|
+
|
|
68
|
+
if f.mime_type == "directory":
|
|
69
|
+
# Remote entry is a directory
|
|
70
|
+
await self._download_dir(p, recursive=recursive, force=force)
|
|
71
|
+
else:
|
|
72
|
+
# Remote entry is a file
|
|
73
|
+
await self._download_file(p, force=force)
|
|
74
|
+
|
|
75
|
+
except Exception as e:
|
|
76
|
+
logger.error(f"Unexpected error during download: {e}")
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
finally:
|
|
80
|
+
await self.container.file_download_manager.wait_all()
|
|
81
|
+
await self.service_runtime.stop(drain=True)
|
|
82
|
+
await self.db.close()
|
|
83
|
+
|
|
84
|
+
async def _download_file(self, path: Path, force: bool = False) -> None:
|
|
85
|
+
file_state = await self.async_reconciler.reconcile_file(path)
|
|
86
|
+
if file_state.exception:
|
|
87
|
+
logger.error(f"Error encountered while processing {path}: {file_state.exception}")
|
|
88
|
+
print(f"Error encountered while processing {path}: {file_state.exception}")
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
if file_state.file_decision.action == "download" or force:
|
|
92
|
+
download_request = DownloadRequest(observation=file_state.observation,
|
|
93
|
+
updated_record=file_state.file_decision.updated_record,
|
|
94
|
+
project=self.proj)
|
|
95
|
+
print(f"Downloading {path}...")
|
|
96
|
+
transfer_id = await self.container.file_download_manager.download_file(download_request)
|
|
97
|
+
self.transfer_ids.append(transfer_id)
|
|
98
|
+
else:
|
|
99
|
+
print(f"Skipping download of {path} because {file_state.file_decision.reason}")
|
|
100
|
+
|
|
101
|
+
async def _download_dir(self, path: Path, recursive: bool = False, force: bool = False):
|
|
102
|
+
async for current_path, path_entries in self.async_reconciler.walk(path=path, listdir_fn=self.listdir_fn,
|
|
103
|
+
recursive=recursive, ignore_fn=None):
|
|
104
|
+
for entry_name in sorted(path_entries):
|
|
105
|
+
file_state = path_entries[entry_name]
|
|
106
|
+
if file_state.exception:
|
|
107
|
+
logger.error(f"Error encountered while processing {entry_name}: {file_state.exception}")
|
|
108
|
+
print(f"Error encountered while processing {entry_name}: {file_state.exception}")
|
|
109
|
+
continue
|
|
110
|
+
if file_state.file_decision.action == "download" or force:
|
|
111
|
+
download_request = DownloadRequest(observation=file_state.observation,
|
|
112
|
+
updated_record=file_state.file_decision.updated_record,
|
|
113
|
+
project=self.proj)
|
|
114
|
+
print(f"Downloading {entry_name} to {current_path / entry_name}...")
|
|
115
|
+
transfer_id = await self.container.file_download_manager.download_file(download_request)
|
|
116
|
+
self.transfer_ids.append(transfer_id)
|