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.
Files changed (97) hide show
  1. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/PKG-INFO +1 -1
  2. materials_commons_cli-2.3.0b2/materials_commons/cli/__init__.py +1 -0
  3. materials_commons_cli-2.3.0b2/materials_commons/cli/async_reconciler.py +104 -0
  4. materials_commons_cli-2.3.0b2/materials_commons/cli/config.py +224 -0
  5. materials_commons_cli-2.3.0b2/materials_commons/cli/downloader.py +116 -0
  6. materials_commons_cli-2.3.0b2/materials_commons/cli/filedb.py +242 -0
  7. materials_commons_cli-2.3.0b2/materials_commons/cli/local_project.py +88 -0
  8. materials_commons_cli-2.3.0b2/materials_commons/cli/models.py +417 -0
  9. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/parser.py +11 -7
  10. materials_commons_cli-2.3.0b2/materials_commons/cli/reconcile2.py +478 -0
  11. materials_commons_cli-2.3.0b2/materials_commons/cli/reconcile3.py +434 -0
  12. materials_commons_cli-2.3.0b2/materials_commons/cli/requests.py +44 -0
  13. materials_commons_cli-2.3.0b2/materials_commons/cli/run.py +65 -0
  14. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/server/downloader/file_download_manager.py +17 -17
  15. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/server/downloader/file_downloader.py +93 -69
  16. materials_commons_cli-2.3.0b2/materials_commons/cli/server/indexer/file_index_manager.py +82 -0
  17. 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
  18. materials_commons_cli-2.3.0b2/materials_commons/cli/server/project_filedbs.py +50 -0
  19. materials_commons_cli-2.3.0b2/materials_commons/cli/server/projects.py +234 -0
  20. materials_commons_cli-2.3.0b2/materials_commons/cli/server/service_container.py +97 -0
  21. materials_commons_cli-2.3.0b2/materials_commons/cli/server/service_runtime.py +96 -0
  22. 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
  23. 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
  24. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/server/uploader/file_uploader.py +67 -27
  25. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/server/websocket_server.py +84 -73
  26. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/subcommands/clone.py +1 -4
  27. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/subcommands/config.py +1 -4
  28. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/subcommands/dataset.py +4 -5
  29. materials_commons_cli-2.3.0b2/materials_commons/cli/subcommands/down.py +133 -0
  30. materials_commons_cli-2.3.0b2/materials_commons/cli/subcommands/down2.py +29 -0
  31. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/subcommands/expt.py +2 -3
  32. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/subcommands/fetch.py +3 -5
  33. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/subcommands/globus.py +4 -6
  34. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/subcommands/init.py +2 -4
  35. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/subcommands/ls.py +14 -6
  36. materials_commons_cli-2.3.0b2/materials_commons/cli/subcommands/ls2.py +110 -0
  37. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/subcommands/mkdir.py +3 -4
  38. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/subcommands/mv.py +4 -6
  39. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/subcommands/proj.py +4 -7
  40. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/subcommands/remote.py +2 -3
  41. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/subcommands/rm.py +3 -5
  42. materials_commons_cli-2.3.0b2/materials_commons/cli/subcommands/scan.py +145 -0
  43. materials_commons_cli-2.3.0b2/materials_commons/cli/subcommands/server.py +66 -0
  44. materials_commons_cli-2.3.0b2/materials_commons/cli/subcommands/up.py +242 -0
  45. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/subcommands/versions.py +4 -4
  46. materials_commons_cli-2.3.0b2/materials_commons/cli/walk.py +258 -0
  47. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons_cli.egg-info/PKG-INFO +1 -1
  48. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons_cli.egg-info/SOURCES.txt +18 -15
  49. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/test/test_api.py +2 -4
  50. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/test/test_functions.py +2 -2
  51. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/test/test_mc_dataset.py +2 -7
  52. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/test/test_mc_down.py +2 -4
  53. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/test/test_mc_mkdir.py +5 -5
  54. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/test/test_mc_mv.py +4 -6
  55. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/test/test_mc_proj.py +3 -3
  56. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/test/test_mc_rm.py +4 -6
  57. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/test/test_mc_up.py +2 -3
  58. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/test/test_treecompare_no_cache.py +2 -3
  59. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/test/test_treecompare_with_cache.py +4 -5
  60. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/test/test_treedb.py +4 -3
  61. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/test/test_user_config.py +1 -2
  62. materials_commons_cli-2.2.3/materials_commons/cli/__init__.py +0 -1
  63. materials_commons_cli-2.2.3/materials_commons/cli/cloned_project.py +0 -224
  64. materials_commons_cli-2.2.3/materials_commons/cli/exceptions.py +0 -12
  65. materials_commons_cli-2.2.3/materials_commons/cli/file_functions.py +0 -138
  66. materials_commons_cli-2.2.3/materials_commons/cli/filedb.py +0 -110
  67. materials_commons_cli-2.2.3/materials_commons/cli/functions.py +0 -745
  68. materials_commons_cli-2.2.3/materials_commons/cli/globus.py +0 -418
  69. materials_commons_cli-2.2.3/materials_commons/cli/list_objects.py +0 -404
  70. materials_commons_cli-2.2.3/materials_commons/cli/print_formatter.py +0 -105
  71. materials_commons_cli-2.2.3/materials_commons/cli/server/indexer/file_index_manager.py +0 -84
  72. materials_commons_cli-2.2.3/materials_commons/cli/server/projects.py +0 -88
  73. materials_commons_cli-2.2.3/materials_commons/cli/sqltable.py +0 -213
  74. materials_commons_cli-2.2.3/materials_commons/cli/subcommands/down.py +0 -340
  75. materials_commons_cli-2.2.3/materials_commons/cli/subcommands/scan.py +0 -83
  76. materials_commons_cli-2.2.3/materials_commons/cli/subcommands/server.py +0 -75
  77. materials_commons_cli-2.2.3/materials_commons/cli/subcommands/up.py +0 -227
  78. materials_commons_cli-2.2.3/materials_commons/cli/tmp_functions.py +0 -41
  79. materials_commons_cli-2.2.3/materials_commons/cli/tree_functions.py +0 -1737
  80. materials_commons_cli-2.2.3/materials_commons/cli/treedb.py +0 -646
  81. materials_commons_cli-2.2.3/materials_commons/cli/user_config.py +0 -216
  82. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/LICENSE +0 -0
  83. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/README.md +0 -0
  84. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/LICENSE.txt +0 -0
  85. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/server/__init__.py +0 -0
  86. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/server/downloader/__init__.py +0 -0
  87. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/server/indexer/__init__.py +0 -0
  88. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/server/local_rest_server.py +0 -0
  89. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/server/uploader/__init__.py +0 -0
  90. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons/cli/subcommands/__init__.py +0 -0
  91. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons_cli.egg-info/dependency_links.txt +0 -0
  92. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons_cli.egg-info/entry_points.txt +0 -0
  93. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons_cli.egg-info/not-zip-safe +0 -0
  94. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons_cli.egg-info/requires.txt +0 -0
  95. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/materials_commons_cli.egg-info/top_level.txt +0 -0
  96. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/setup.cfg +0 -0
  97. {materials_commons_cli-2.2.3 → materials_commons_cli-2.3.0b2}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: materials_commons_cli
3
- Version: 2.2.3
3
+ Version: 2.3.0b2
4
4
  Summary: Materials Commons CLI
5
5
  Home-page: https://materials-commons.github.io/materials-commons-cli/html/index.html
6
6
  Author: Materials Commons development team
@@ -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)