machineconfig 2.0__py3-none-any.whl → 2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of machineconfig might be problematic. Click here for more details.
- machineconfig/cluster/cloud_manager.py +0 -3
- machineconfig/cluster/data_transfer.py +0 -1
- machineconfig/cluster/file_manager.py +0 -1
- machineconfig/cluster/job_params.py +0 -3
- machineconfig/cluster/loader_runner.py +0 -3
- machineconfig/cluster/remote_machine.py +0 -1
- machineconfig/cluster/script_notify_upon_completion.py +0 -1
- machineconfig/cluster/sessions_managers/archive/create_zellij_template.py +3 -5
- machineconfig/cluster/sessions_managers/archive/session_managers.py +0 -1
- machineconfig/cluster/sessions_managers/enhanced_command_runner.py +17 -57
- machineconfig/cluster/sessions_managers/wt_local.py +36 -110
- machineconfig/cluster/sessions_managers/wt_local_manager.py +42 -112
- machineconfig/cluster/sessions_managers/wt_remote.py +23 -30
- machineconfig/cluster/sessions_managers/wt_remote_manager.py +20 -62
- machineconfig/cluster/sessions_managers/wt_utils/layout_generator.py +10 -15
- machineconfig/cluster/sessions_managers/wt_utils/process_monitor.py +27 -127
- machineconfig/cluster/sessions_managers/wt_utils/remote_executor.py +10 -43
- machineconfig/cluster/sessions_managers/wt_utils/session_manager.py +22 -101
- machineconfig/cluster/sessions_managers/wt_utils/status_reporter.py +11 -39
- machineconfig/cluster/sessions_managers/zellij_local.py +49 -102
- machineconfig/cluster/sessions_managers/zellij_local_manager.py +34 -78
- machineconfig/cluster/sessions_managers/zellij_remote.py +17 -24
- machineconfig/cluster/sessions_managers/zellij_remote_manager.py +7 -13
- machineconfig/cluster/sessions_managers/zellij_utils/example_usage.py +4 -2
- machineconfig/cluster/sessions_managers/zellij_utils/layout_generator.py +6 -6
- machineconfig/cluster/sessions_managers/zellij_utils/process_monitor.py +18 -88
- machineconfig/cluster/sessions_managers/zellij_utils/remote_executor.py +2 -6
- machineconfig/cluster/sessions_managers/zellij_utils/session_manager.py +12 -40
- machineconfig/cluster/sessions_managers/zellij_utils/status_reporter.py +3 -2
- machineconfig/cluster/templates/cli_click.py +0 -1
- machineconfig/cluster/templates/cli_gooey.py +0 -2
- machineconfig/cluster/templates/cli_trogon.py +0 -1
- machineconfig/cluster/templates/run_cloud.py +0 -1
- machineconfig/cluster/templates/run_cluster.py +0 -1
- machineconfig/cluster/templates/run_remote.py +0 -1
- machineconfig/cluster/templates/utils.py +26 -10
- machineconfig/jobs/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/jobs/linux/msc/cli_agents.sh +16 -0
- machineconfig/jobs/python/check_installations.py +1 -0
- machineconfig/jobs/python/create_bootable_media.py +0 -2
- machineconfig/jobs/python/python_ve_symlink.py +9 -11
- machineconfig/jobs/python/tasks.py +0 -1
- machineconfig/jobs/python/vscode/api.py +5 -5
- machineconfig/jobs/python/vscode/link_ve.py +13 -14
- machineconfig/jobs/python/vscode/select_interpreter.py +21 -22
- machineconfig/jobs/python/vscode/sync_code.py +9 -13
- machineconfig/jobs/python_custom_installers/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/jobs/python_custom_installers/archive/ngrok.py +13 -13
- machineconfig/jobs/python_custom_installers/dev/aider.py +7 -15
- machineconfig/jobs/python_custom_installers/dev/alacritty.py +9 -18
- machineconfig/jobs/python_custom_installers/dev/brave.py +10 -19
- machineconfig/jobs/python_custom_installers/dev/bypass_paywall.py +8 -15
- machineconfig/jobs/python_custom_installers/dev/code.py +14 -21
- machineconfig/jobs/python_custom_installers/dev/cursor.py +3 -14
- machineconfig/jobs/python_custom_installers/dev/docker_desktop.py +8 -7
- machineconfig/jobs/python_custom_installers/dev/espanso.py +15 -19
- machineconfig/jobs/python_custom_installers/dev/goes.py +5 -12
- machineconfig/jobs/python_custom_installers/dev/lvim.py +9 -17
- machineconfig/jobs/python_custom_installers/dev/nerdfont.py +12 -19
- machineconfig/jobs/python_custom_installers/dev/redis.py +12 -20
- machineconfig/jobs/python_custom_installers/dev/wezterm.py +12 -19
- machineconfig/jobs/python_custom_installers/dev/winget.py +5 -23
- machineconfig/jobs/python_custom_installers/docker.py +12 -21
- machineconfig/jobs/python_custom_installers/gh.py +11 -19
- machineconfig/jobs/python_custom_installers/hx.py +32 -16
- machineconfig/jobs/python_custom_installers/warp-cli.py +12 -20
- machineconfig/jobs/python_generic_installers/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/jobs/python_linux_installers/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/jobs/windows/archive/archive_pygraphviz.ps1 +1 -1
- machineconfig/jobs/windows/msc/cli_agents.bat +0 -0
- machineconfig/jobs/windows/msc/cli_agents.ps1 +0 -0
- machineconfig/jobs/windows/start_terminal.ps1 +1 -1
- machineconfig/profile/create.py +29 -22
- machineconfig/profile/create_hardlinks.py +26 -19
- machineconfig/profile/shell.py +51 -28
- machineconfig/scripts/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/scripts/cloud/init.sh +2 -2
- machineconfig/scripts/linux/checkout_versions +1 -1
- machineconfig/scripts/linux/choose_wezterm_theme +1 -1
- machineconfig/scripts/linux/cloud_copy +1 -1
- machineconfig/scripts/linux/cloud_manager +1 -1
- machineconfig/scripts/linux/cloud_mount +1 -1
- machineconfig/scripts/linux/cloud_repo_sync +1 -1
- machineconfig/scripts/linux/cloud_sync +1 -1
- machineconfig/scripts/linux/croshell +1 -1
- machineconfig/scripts/linux/devops +4 -6
- machineconfig/scripts/linux/fire +1 -1
- machineconfig/scripts/linux/fire_agents +3 -2
- machineconfig/scripts/linux/ftpx +1 -1
- machineconfig/scripts/linux/gh_models +1 -1
- machineconfig/scripts/linux/kill_process +1 -1
- machineconfig/scripts/linux/mcinit +1 -1
- machineconfig/scripts/linux/repos +1 -1
- machineconfig/scripts/linux/scheduler +1 -1
- machineconfig/scripts/linux/start_slidev +1 -1
- machineconfig/scripts/linux/start_terminals +1 -1
- machineconfig/scripts/linux/url2md +1 -1
- machineconfig/scripts/linux/warp-cli.sh +122 -0
- machineconfig/scripts/linux/wifi_conn +1 -1
- machineconfig/scripts/python/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/croshell.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/devops.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/devops_devapps_install.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/devops_update_repos.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/fire_jobs.cpython-313.pyc +0 -0
- machineconfig/scripts/python/ai/__init__.py +0 -0
- machineconfig/scripts/python/ai/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/scripts/python/ai/__pycache__/generate_files.cpython-313.pyc +0 -0
- machineconfig/scripts/python/ai/__pycache__/mcinit.cpython-313.pyc +0 -0
- machineconfig/scripts/python/ai/generate_files.py +84 -0
- machineconfig/scripts/python/ai/instructions/python/dev.instructions.md +2 -2
- machineconfig/scripts/python/ai/mcinit.py +7 -3
- machineconfig/scripts/python/ai/scripts/lint_and_type_check.sh +10 -5
- machineconfig/scripts/python/cloud_copy.py +1 -1
- machineconfig/scripts/python/cloud_mount.py +1 -1
- machineconfig/scripts/python/cloud_repo_sync.py +4 -4
- machineconfig/scripts/python/croshell.py +5 -3
- machineconfig/scripts/python/devops_add_identity.py +1 -1
- machineconfig/scripts/python/devops_add_ssh_key.py +1 -1
- machineconfig/scripts/python/devops_backup_retrieve.py +1 -1
- machineconfig/scripts/python/devops_update_repos.py +140 -52
- machineconfig/scripts/python/dotfile.py +1 -1
- machineconfig/scripts/python/fire_agents.py +28 -9
- machineconfig/scripts/python/fire_jobs.py +3 -4
- machineconfig/scripts/python/ftpx.py +2 -1
- machineconfig/scripts/python/helpers/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/scripts/python/helpers/__pycache__/helpers4.cpython-313.pyc +0 -0
- machineconfig/scripts/python/helpers/helpers2.py +2 -2
- machineconfig/scripts/python/helpers/helpers4.py +1 -2
- machineconfig/scripts/python/helpers/repo_sync_helpers.py +1 -1
- machineconfig/scripts/python/mount_nfs.py +1 -1
- machineconfig/scripts/python/mount_ssh.py +1 -1
- machineconfig/scripts/python/repos.py +1 -1
- machineconfig/scripts/python/start_slidev.py +1 -1
- machineconfig/scripts/python/wsl_windows_transfer.py +1 -1
- machineconfig/scripts/windows/checkout_version.ps1 +1 -3
- machineconfig/scripts/windows/choose_wezterm_theme.ps1 +1 -3
- machineconfig/scripts/windows/cloud_copy.ps1 +2 -6
- machineconfig/scripts/windows/cloud_manager.ps1 +1 -1
- machineconfig/scripts/windows/cloud_repo_sync.ps1 +1 -2
- machineconfig/scripts/windows/cloud_sync.ps1 +2 -2
- machineconfig/scripts/windows/croshell.ps1 +2 -2
- machineconfig/scripts/windows/devops.ps1 +1 -4
- machineconfig/scripts/windows/dotfile.ps1 +1 -3
- machineconfig/scripts/windows/fire.ps1 +1 -1
- machineconfig/scripts/windows/ftpx.ps1 +2 -2
- machineconfig/scripts/windows/gpt.ps1 +1 -1
- machineconfig/scripts/windows/kill_process.ps1 +1 -2
- machineconfig/scripts/windows/mcinit.ps1 +1 -1
- machineconfig/scripts/windows/mount_nfs.ps1 +1 -1
- machineconfig/scripts/windows/mount_ssh.ps1 +1 -1
- machineconfig/scripts/windows/pomodoro.ps1 +1 -1
- machineconfig/scripts/windows/py2exe.ps1 +1 -3
- machineconfig/scripts/windows/repos.ps1 +1 -1
- machineconfig/scripts/windows/scheduler.ps1 +1 -1
- machineconfig/scripts/windows/snapshot.ps1 +2 -2
- machineconfig/scripts/windows/start_slidev.ps1 +1 -1
- machineconfig/scripts/windows/start_terminals.ps1 +1 -1
- machineconfig/scripts/windows/wifi_conn.ps1 +1 -1
- machineconfig/scripts/windows/wsl_windows_transfer.ps1 +1 -3
- machineconfig/settings/lf/linux/lfrc +1 -1
- machineconfig/settings/linters/.ruff_cache/.gitignore +2 -0
- machineconfig/settings/linters/.ruff_cache/CACHEDIR.TAG +1 -0
- machineconfig/settings/lvim/windows/archive/config_additional.lua +1 -1
- machineconfig/settings/svim/linux/init.toml +1 -1
- machineconfig/settings/svim/windows/init.toml +1 -1
- machineconfig/setup_linux/web_shortcuts/croshell.sh +0 -54
- machineconfig/setup_linux/web_shortcuts/interactive.sh +6 -6
- machineconfig/setup_windows/web_shortcuts/all.ps1 +2 -2
- machineconfig/setup_windows/web_shortcuts/ascii_art.ps1 +1 -1
- machineconfig/setup_windows/web_shortcuts/croshell.ps1 +1 -1
- machineconfig/setup_windows/web_shortcuts/interactive.ps1 +5 -5
- machineconfig/setup_windows/wt_and_pwsh/install_fonts.ps1 +51 -15
- machineconfig/setup_windows/wt_and_pwsh/set_pwsh_theme.py +66 -12
- machineconfig/setup_windows/wt_and_pwsh/set_wt_settings.py +44 -36
- machineconfig/utils/ai/generate_file_checklist.py +8 -10
- machineconfig/utils/ai/url2md.py +4 -2
- machineconfig/utils/cloud/onedrive/setup_oauth.py +1 -0
- machineconfig/utils/cloud/onedrive/transaction.py +63 -98
- machineconfig/utils/code.py +60 -39
- machineconfig/utils/installer.py +27 -33
- machineconfig/utils/installer_utils/installer_abc.py +8 -7
- machineconfig/utils/installer_utils/installer_class.py +149 -70
- machineconfig/utils/links.py +22 -11
- machineconfig/utils/notifications.py +197 -0
- machineconfig/utils/options.py +29 -23
- machineconfig/utils/path.py +13 -6
- machineconfig/utils/path_reduced.py +485 -216
- machineconfig/utils/procs.py +47 -41
- machineconfig/utils/scheduling.py +0 -1
- machineconfig/utils/ssh.py +157 -76
- machineconfig/utils/terminal.py +82 -37
- machineconfig/utils/utils.py +12 -10
- machineconfig/utils/utils2.py +38 -48
- machineconfig/utils/utils5.py +183 -116
- machineconfig/utils/ve.py +9 -4
- {machineconfig-2.0.dist-info → machineconfig-2.1.dist-info}/METADATA +3 -2
- {machineconfig-2.0.dist-info → machineconfig-2.1.dist-info}/RECORD +200 -217
- machineconfig/jobs/__pycache__/__init__.cpython-311.pyc +0 -0
- machineconfig/jobs/python/__pycache__/__init__.cpython-311.pyc +0 -0
- machineconfig/jobs/python/__pycache__/python_ve_symlink.cpython-311.pyc +0 -0
- machineconfig/jobs/python/archive/python_tools.txt +0 -12
- machineconfig/jobs/python/vscode/__pycache__/select_interpreter.cpython-311.pyc +0 -0
- machineconfig/jobs/python_custom_installers/__pycache__/__init__.cpython-311.pyc +0 -0
- machineconfig/jobs/python_generic_installers/__pycache__/__init__.cpython-311.pyc +0 -0
- machineconfig/jobs/python_generic_installers/update.py +0 -3
- machineconfig/jobs/python_linux_installers/__pycache__/__init__.cpython-311.pyc +0 -0
- machineconfig/profile/__pycache__/__init__.cpython-311.pyc +0 -0
- machineconfig/profile/__pycache__/create.cpython-311.pyc +0 -0
- machineconfig/profile/__pycache__/shell.cpython-311.pyc +0 -0
- machineconfig/scripts/__pycache__/__init__.cpython-311.pyc +0 -0
- machineconfig/scripts/linux/activate_ve +0 -87
- machineconfig/scripts/python/__pycache__/__init__.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/cloud_copy.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/cloud_mount.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/cloud_sync.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/croshell.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/devops.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/devops_backup_retrieve.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/devops_devapps_install.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/devops_update_repos.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/fire_agents.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/fire_jobs.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/get_zellij_cmd.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/repos.cpython-311.pyc +0 -0
- machineconfig/scripts/python/ai/__pycache__/init.cpython-311.pyc +0 -0
- machineconfig/scripts/python/ai/__pycache__/mcinit.cpython-311.pyc +0 -0
- machineconfig/scripts/python/helpers/__pycache__/__init__.cpython-311.pyc +0 -0
- machineconfig/scripts/python/helpers/__pycache__/cloud_helpers.cpython-311.pyc +0 -0
- machineconfig/scripts/python/helpers/__pycache__/helpers2.cpython-311.pyc +0 -0
- machineconfig/scripts/python/helpers/__pycache__/helpers4.cpython-311.pyc +0 -0
- machineconfig/scripts/python/helpers/__pycache__/repo_sync_helpers.cpython-311.pyc +0 -0
- machineconfig/scripts/windows/activate_ve.ps1 +0 -54
- {machineconfig-2.0.dist-info → machineconfig-2.1.dist-info}/WHEEL +0 -0
- {machineconfig-2.0.dist-info → machineconfig-2.1.dist-info}/top_level.txt +0 -0
machineconfig/utils/procs.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
"""Procs
|
|
2
|
-
|
|
1
|
+
"""Procs"""
|
|
2
|
+
|
|
3
3
|
import psutil
|
|
4
4
|
from tqdm import tqdm
|
|
5
5
|
from pytz import timezone
|
|
@@ -29,7 +29,7 @@ def get_processes_accessing_file(path: str):
|
|
|
29
29
|
if len(tmp) > 0:
|
|
30
30
|
res[proc.pid] = tmp
|
|
31
31
|
# Convert to list of dictionaries for consistent data structure
|
|
32
|
-
result_data = [{
|
|
32
|
+
result_data = [{"pid": pid, "files": files} for pid, files in res.items()]
|
|
33
33
|
console.print(Panel(f"✅ Found {len(res)} processes accessing the specified file", title="[bold blue]Process Info[/bold blue]", border_style="blue"))
|
|
34
34
|
return result_data
|
|
35
35
|
|
|
@@ -44,7 +44,7 @@ def kill_process(name: str):
|
|
|
44
44
|
killed = True
|
|
45
45
|
if not killed:
|
|
46
46
|
print(f"❓ No process with name '{name}' was found")
|
|
47
|
-
print(f"{'─'*80}\n")
|
|
47
|
+
print(f"{'─' * 80}\n")
|
|
48
48
|
|
|
49
49
|
|
|
50
50
|
class ProcessManager:
|
|
@@ -57,23 +57,26 @@ class ProcessManager:
|
|
|
57
57
|
try:
|
|
58
58
|
mem_usage_mb = proc.memory_info().rss / (1024 * 1024)
|
|
59
59
|
# Convert create_time to local timezone
|
|
60
|
-
create_time_utc = datetime.fromtimestamp(proc.create_time(), tz=timezone(
|
|
61
|
-
create_time_local = create_time_utc.astimezone(timezone(
|
|
62
|
-
|
|
63
|
-
process_info.append(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
60
|
+
create_time_utc = datetime.fromtimestamp(proc.create_time(), tz=timezone("UTC"))
|
|
61
|
+
create_time_local = create_time_utc.astimezone(timezone("Australia/Adelaide"))
|
|
62
|
+
|
|
63
|
+
process_info.append(
|
|
64
|
+
{
|
|
65
|
+
"pid": proc.pid,
|
|
66
|
+
"name": proc.name(),
|
|
67
|
+
"username": proc.username(),
|
|
68
|
+
"cpu_percent": proc.cpu_percent(),
|
|
69
|
+
"memory_usage_mb": mem_usage_mb,
|
|
70
|
+
"status": proc.status(),
|
|
71
|
+
"create_time": create_time_local,
|
|
72
|
+
"command": " ".join(proc.cmdline()),
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
|
76
|
+
pass
|
|
74
77
|
|
|
75
78
|
# Sort by memory usage (descending)
|
|
76
|
-
process_info.sort(key=lambda x: x[
|
|
79
|
+
process_info.sort(key=lambda x: x["memory_usage_mb"], reverse=True)
|
|
77
80
|
self.data = process_info
|
|
78
81
|
console.print(Panel(f"✅ Process Manager initialized with {len(process_info)} processes", title="[bold blue]Process Info[/bold blue]", border_style="blue"))
|
|
79
82
|
|
|
@@ -83,7 +86,7 @@ class ProcessManager:
|
|
|
83
86
|
return ""
|
|
84
87
|
|
|
85
88
|
# Create header
|
|
86
|
-
_headers = [
|
|
89
|
+
_headers = ["PID", "Name", "Username", "CPU%", "Memory(MB)", "Status", "Create Time", "Command"]
|
|
87
90
|
header_line = f"{'PID':<8} {'Name':<20} {'Username':<12} {'CPU%':<8} {'Memory(MB)':<12} {'Status':<12} {'Create Time':<20} {'Command':<50}"
|
|
88
91
|
separator = "-" * len(header_line)
|
|
89
92
|
|
|
@@ -91,9 +94,9 @@ class ProcessManager:
|
|
|
91
94
|
|
|
92
95
|
for process in self.data:
|
|
93
96
|
# Format create_time as string
|
|
94
|
-
create_time_str = process[
|
|
97
|
+
create_time_str = process["create_time"].strftime("%Y-%m-%d %H:%M:%S")
|
|
95
98
|
# Truncate command if too long
|
|
96
|
-
command = process[
|
|
99
|
+
command = process["command"][:47] + "..." if len(process["command"]) > 50 else process["command"]
|
|
97
100
|
|
|
98
101
|
line = f"{process['pid']:<8} {process['name'][:19]:<20} {process['username'][:11]:<12} {process['cpu_percent']:<8.1f} {process['memory_usage_mb']:<12.2f} {process['status'][:11]:<12} {create_time_str:<20} {command:<50}"
|
|
99
102
|
lines.append(line)
|
|
@@ -123,7 +126,7 @@ class ProcessManager:
|
|
|
123
126
|
|
|
124
127
|
kill_all = input("\n⚠️ Confirm killing ALL selected processes? y/[n] ").lower() == "y"
|
|
125
128
|
if kill_all:
|
|
126
|
-
self.kill(pids=[p[
|
|
129
|
+
self.kill(pids=[p["pid"] for p in selected_processes])
|
|
127
130
|
return
|
|
128
131
|
|
|
129
132
|
kill_by_index = input("\n🔫 Kill by index? (enter numbers separated by spaces, e.g. '1 4') or [n] to cancel: ")
|
|
@@ -132,7 +135,7 @@ class ProcessManager:
|
|
|
132
135
|
target_processes = [selected_processes[i] for i in indices]
|
|
133
136
|
for idx2, process in enumerate(target_processes):
|
|
134
137
|
pprint(process, f"🎯 Target Process {idx2}")
|
|
135
|
-
_ = self.kill(pids=[p[
|
|
138
|
+
_ = self.kill(pids=[p["pid"] for p in target_processes]) if input("\n⚠️ Confirm termination? y/[n] ").lower() == "y" else None
|
|
136
139
|
console.print(Panel("🔔 No processes were terminated.", title="[bold blue]Process Info[/bold blue]", border_style="blue"))
|
|
137
140
|
|
|
138
141
|
def filter_and_kill(self, name: Optional[str] = None):
|
|
@@ -141,12 +144,12 @@ class ProcessManager:
|
|
|
141
144
|
console.print(Panel(title, title="[bold blue]Process Info[/bold blue]", border_style="blue"))
|
|
142
145
|
|
|
143
146
|
# Filter processes by name
|
|
144
|
-
filtered_processes = [p for p in self.data if p[
|
|
147
|
+
filtered_processes = [p for p in self.data if p["name"] == name]
|
|
145
148
|
# Sort by create_time (ascending)
|
|
146
|
-
filtered_processes.sort(key=lambda x: x[
|
|
149
|
+
filtered_processes.sort(key=lambda x: x["create_time"])
|
|
147
150
|
|
|
148
151
|
print(f"🎯 Found {len(filtered_processes)} processes matching name: '{name}'")
|
|
149
|
-
self.kill(pids=[p[
|
|
152
|
+
self.kill(pids=[p["pid"] for p in filtered_processes])
|
|
150
153
|
console.print(Panel("", title="[bold blue]Process Info[/bold blue]", border_style="blue"))
|
|
151
154
|
|
|
152
155
|
def kill(self, names: Optional[list[str]] = None, pids: Optional[list[int]] = None, commands: Optional[list[str]] = None):
|
|
@@ -155,19 +158,22 @@ class ProcessManager:
|
|
|
155
158
|
console.print(Panel(title, title="[bold blue]Process Info[/bold blue]", border_style="blue"))
|
|
156
159
|
if names is None and pids is None and commands is None:
|
|
157
160
|
print("❌ Error: No termination targets specified (names, pids, or commands)")
|
|
158
|
-
raise ValueError(
|
|
159
|
-
if names is None:
|
|
160
|
-
|
|
161
|
-
if
|
|
161
|
+
raise ValueError("names, pids and commands cannot all be None")
|
|
162
|
+
if names is None:
|
|
163
|
+
names = []
|
|
164
|
+
if pids is None:
|
|
165
|
+
pids = []
|
|
166
|
+
if commands is None:
|
|
167
|
+
commands = []
|
|
162
168
|
|
|
163
169
|
killed_count = 0
|
|
164
170
|
|
|
165
171
|
for name in names:
|
|
166
|
-
matching_processes = [p for p in self.data if p[
|
|
172
|
+
matching_processes = [p for p in self.data if p["name"] == name]
|
|
167
173
|
if len(matching_processes) > 0:
|
|
168
174
|
for process in matching_processes:
|
|
169
|
-
psutil.Process(process[
|
|
170
|
-
print(f
|
|
175
|
+
psutil.Process(process["pid"]).kill()
|
|
176
|
+
print(f"💀 Killed process {name} with PID {process['pid']}. It lived {get_age(process['create_time'])}. RIP 🪦💐")
|
|
171
177
|
killed_count += 1
|
|
172
178
|
else:
|
|
173
179
|
print(f'❓ No process named "{name}" found')
|
|
@@ -181,13 +187,13 @@ class ProcessManager:
|
|
|
181
187
|
print(f'💀 Killed process with PID {pid} and name "{proc_name}". It lived {proc_lifetime}. RIP 🪦💐')
|
|
182
188
|
killed_count += 1
|
|
183
189
|
except psutil.NoSuchProcess:
|
|
184
|
-
print(f
|
|
190
|
+
print(f"❓ No process with PID {pid} found")
|
|
185
191
|
|
|
186
192
|
for command in commands:
|
|
187
|
-
matching_processes = [p for p in self.data if command in p[
|
|
193
|
+
matching_processes = [p for p in self.data if command in p["command"]]
|
|
188
194
|
if len(matching_processes) > 0:
|
|
189
195
|
for process in matching_processes:
|
|
190
|
-
psutil.Process(process[
|
|
196
|
+
psutil.Process(process["pid"]).kill()
|
|
191
197
|
print(f'💀 Killed process with "{command}" in its command & PID {process["pid"]}. It lived {get_age(process["create_time"])}. RIP 🪦💐')
|
|
192
198
|
killed_count += 1
|
|
193
199
|
else:
|
|
@@ -201,13 +207,13 @@ def get_age(create_time: Any) -> str:
|
|
|
201
207
|
try:
|
|
202
208
|
if isinstance(create_time, (int, float)):
|
|
203
209
|
# Handle timestampz
|
|
204
|
-
create_time_utc = datetime.fromtimestamp(create_time, tz=timezone(
|
|
205
|
-
create_time_local = create_time_utc.astimezone(timezone(
|
|
210
|
+
create_time_utc = datetime.fromtimestamp(create_time, tz=timezone("UTC"))
|
|
211
|
+
create_time_local = create_time_utc.astimezone(timezone("Australia/Adelaide"))
|
|
206
212
|
else:
|
|
207
213
|
# Already a datetime object
|
|
208
214
|
create_time_local = create_time
|
|
209
215
|
|
|
210
|
-
now_local = datetime.now(tz=timezone(
|
|
216
|
+
now_local = datetime.now(tz=timezone("Australia/Adelaide"))
|
|
211
217
|
age = now_local - create_time_local
|
|
212
218
|
return str(age)
|
|
213
219
|
except Exception as e:
|
|
@@ -224,5 +230,5 @@ def get_age(create_time: Any) -> str:
|
|
|
224
230
|
return f"unknown due to {ee} and {e}"
|
|
225
231
|
|
|
226
232
|
|
|
227
|
-
if __name__ ==
|
|
233
|
+
if __name__ == "__main__":
|
|
228
234
|
pass
|
|
@@ -284,7 +284,6 @@
|
|
|
284
284
|
|
|
285
285
|
# print(f"Task: {task.name}")
|
|
286
286
|
|
|
287
|
-
# shell_script = get_shell_script_executing_python_file(python_file=str(task.task_root.joinpath("task.py")), ve_name=task.venv)
|
|
288
287
|
# shell_script_root = PathExtended.tmp().joinpath(f"tmp_scripts/scheduler/{task.name}")
|
|
289
288
|
# shell_script_root.mkdir(parents=True, exist_ok=True)
|
|
290
289
|
# try:
|
machineconfig/utils/ssh.py
CHANGED
|
@@ -1,37 +1,43 @@
|
|
|
1
|
-
|
|
2
1
|
from typing import Optional, Any, Union, List
|
|
3
2
|
import os
|
|
4
3
|
from dataclasses import dataclass
|
|
5
4
|
import rich.console
|
|
6
5
|
from machineconfig.utils.terminal import Terminal, Response, MACHINE
|
|
7
|
-
from machineconfig.utils.path_reduced import
|
|
6
|
+
from machineconfig.utils.path_reduced import PathExtended, PLike, OPLike
|
|
8
7
|
from machineconfig.utils.utils2 import pprint
|
|
9
8
|
|
|
9
|
+
|
|
10
10
|
@dataclass
|
|
11
11
|
class Scout:
|
|
12
|
-
source_full:
|
|
13
|
-
source_rel2home:
|
|
12
|
+
source_full: PathExtended
|
|
13
|
+
source_rel2home: PathExtended
|
|
14
14
|
exists: bool
|
|
15
15
|
is_dir: bool
|
|
16
|
-
files: Optional[List[
|
|
16
|
+
files: Optional[List[PathExtended]]
|
|
17
|
+
|
|
18
|
+
|
|
17
19
|
def scout(source: PLike, z: bool = False, r: bool = False) -> Scout:
|
|
18
|
-
source_full =
|
|
20
|
+
source_full = PathExtended(source).expanduser().absolute()
|
|
19
21
|
source_rel2home = source_full.collapseuser()
|
|
20
22
|
exists = source_full.exists()
|
|
21
23
|
is_dir = source_full.is_dir() if exists else False
|
|
22
24
|
if z and exists:
|
|
23
|
-
try:
|
|
25
|
+
try:
|
|
26
|
+
source_full = source_full.zip()
|
|
24
27
|
except Exception as ex:
|
|
25
28
|
raise Exception(f"Could not zip {source_full} due to {ex}") from ex # type: ignore # pylint: disable=W0719
|
|
26
29
|
source_rel2home = source_full.zip()
|
|
27
30
|
if r and exists and is_dir:
|
|
28
31
|
files = [item.collapseuser() for item in source_full.search(folders=False, r=True)]
|
|
29
|
-
else:
|
|
32
|
+
else:
|
|
33
|
+
files = None
|
|
30
34
|
return Scout(source_full=source_full, source_rel2home=source_rel2home, exists=exists, is_dir=is_dir, files=files)
|
|
31
35
|
|
|
32
36
|
|
|
33
37
|
class SSH: # inferior alternative: https://github.com/fabric/fabric
|
|
34
|
-
def __init__(
|
|
38
|
+
def __init__(
|
|
39
|
+
self, host: Optional[str] = None, username: Optional[str] = None, hostname: Optional[str] = None, sshkey: Optional[str] = None, pwd: Optional[str] = None, port: int = 22, ve: Optional[str] = ".venv", compress: bool = False
|
|
40
|
+
): # https://stackoverflow.com/questions/51027192/execute-command-script-using-different-shell-in-ssh-paramiko
|
|
35
41
|
self.pwd = pwd
|
|
36
42
|
self.ve = ve
|
|
37
43
|
self.compress = compress # Defaults: (1) use localhost if nothing provided.
|
|
@@ -44,26 +50,33 @@ class SSH: # inferior alternative: https://github.com/fabric/fabric
|
|
|
44
50
|
import platform
|
|
45
51
|
import paramiko # type: ignore
|
|
46
52
|
import getpass
|
|
53
|
+
|
|
47
54
|
if isinstance(host, str):
|
|
48
55
|
try:
|
|
49
56
|
import paramiko.config as pconfig
|
|
50
|
-
|
|
57
|
+
|
|
58
|
+
config = pconfig.SSHConfig.from_path(str(PathExtended.home().joinpath(".ssh/config")))
|
|
51
59
|
config_dict = config.lookup(host)
|
|
52
60
|
self.hostname = config_dict["hostname"]
|
|
53
61
|
self.username = config_dict["user"]
|
|
54
62
|
self.host = host
|
|
55
63
|
self.port = int(config_dict.get("port", port))
|
|
56
64
|
tmp = config_dict.get("identityfile", sshkey)
|
|
57
|
-
if isinstance(tmp, list):
|
|
58
|
-
|
|
65
|
+
if isinstance(tmp, list):
|
|
66
|
+
sshkey = tmp[0]
|
|
67
|
+
else:
|
|
68
|
+
sshkey = tmp
|
|
59
69
|
self.proxycommand = config_dict.get("proxycommand", None)
|
|
60
70
|
if sshkey is not None:
|
|
61
71
|
tmp = config.lookup("*").get("identityfile", sshkey)
|
|
62
|
-
if isinstance(tmp, list):
|
|
63
|
-
|
|
72
|
+
if isinstance(tmp, list):
|
|
73
|
+
sshkey = tmp[0]
|
|
74
|
+
else:
|
|
75
|
+
sshkey = tmp
|
|
64
76
|
except (FileNotFoundError, KeyError):
|
|
65
77
|
assert "@" in host or ":" in host, f"Host must be in the form of `username@hostname:port` or `username@hostname` or `hostname:port`, but it is: {host}"
|
|
66
|
-
if "@" in host:
|
|
78
|
+
if "@" in host:
|
|
79
|
+
self.username, self.hostname = host.split("@")
|
|
67
80
|
else:
|
|
68
81
|
self.username = username or getpass.getuser()
|
|
69
82
|
self.hostname = host
|
|
@@ -77,7 +90,7 @@ class SSH: # inferior alternative: https://github.com/fabric/fabric
|
|
|
77
90
|
print(f"Provided values: host={host}, username={username}, hostname={hostname}")
|
|
78
91
|
raise ValueError("Either host or username and hostname must be provided.")
|
|
79
92
|
|
|
80
|
-
self.sshkey = str(
|
|
93
|
+
self.sshkey = str(PathExtended(sshkey).expanduser().absolute()) if sshkey is not None else None # no need to pass sshkey if it was configured properly already
|
|
81
94
|
self.ssh = paramiko.SSHClient()
|
|
82
95
|
self.ssh.load_system_host_keys()
|
|
83
96
|
self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
@@ -91,147 +104,210 @@ class SSH: # inferior alternative: https://github.com/fabric/fabric
|
|
|
91
104
|
else:
|
|
92
105
|
allow_agent = False
|
|
93
106
|
look_for_keys = False
|
|
94
|
-
self.ssh.connect(hostname=self.hostname, username=self.username, password=self.pwd, port=self.port, key_filename=self.sshkey, compress=self.compress, sock=sock,
|
|
95
|
-
allow_agent=allow_agent, look_for_keys=look_for_keys) # type: ignore
|
|
107
|
+
self.ssh.connect(hostname=self.hostname, username=self.username, password=self.pwd, port=self.port, key_filename=self.sshkey, compress=self.compress, sock=sock, allow_agent=allow_agent, look_for_keys=look_for_keys) # type: ignore
|
|
96
108
|
except Exception as _err:
|
|
97
109
|
rich.console.Console().print_exception()
|
|
98
110
|
self.pwd = getpass.getpass(f"Enter password for {self.username}@{self.hostname}: ")
|
|
99
|
-
self.ssh.connect(hostname=self.hostname, username=self.username, password=self.pwd, port=self.port, key_filename=self.sshkey, compress=self.compress, sock=sock,
|
|
100
|
-
allow_agent=False,look_for_keys=False) # type: ignore
|
|
111
|
+
self.ssh.connect(hostname=self.hostname, username=self.username, password=self.pwd, port=self.port, key_filename=self.sshkey, compress=self.compress, sock=sock, allow_agent=False, look_for_keys=False) # type: ignore
|
|
101
112
|
|
|
102
|
-
try:
|
|
113
|
+
try:
|
|
114
|
+
self.sftp: Optional[paramiko.SFTPClient] = self.ssh.open_sftp()
|
|
103
115
|
except Exception as err:
|
|
104
116
|
self.sftp = None
|
|
105
117
|
print(f"""⚠️ WARNING: Failed to open SFTP connection to {hostname}.
|
|
106
118
|
Error Details: {err}\nData transfer may be affected!""")
|
|
119
|
+
|
|
107
120
|
def view_bar(slf: Any, a: Any, b: Any):
|
|
108
121
|
slf.total = int(b)
|
|
109
122
|
slf.update(int(a - slf.n)) # update pbar with increment
|
|
123
|
+
|
|
110
124
|
from tqdm import tqdm
|
|
111
|
-
|
|
125
|
+
|
|
126
|
+
self.tqdm_wrap = type("TqdmWrap", (tqdm,), {"view_bar": view_bar})
|
|
112
127
|
self._local_distro: Optional[str] = None
|
|
113
128
|
self._remote_distro: Optional[str] = None
|
|
114
129
|
self._remote_machine: Optional[MACHINE] = None
|
|
115
130
|
self.terminal_responses: list[Response] = []
|
|
116
131
|
self.platform = platform
|
|
117
|
-
self.remote_env_cmd = rf"""~/
|
|
118
|
-
self.local_env_cmd = rf"""~/
|
|
119
|
-
|
|
120
|
-
def
|
|
132
|
+
self.remote_env_cmd = rf"""~/code/machineconfig/{self.ve}/Scripts/Activate.ps1""" if self.get_remote_machine() == "Windows" else rf"""source ~/code/machineconfig/{self.ve}/bin/activate"""
|
|
133
|
+
self.local_env_cmd = rf"""~/code/machineconfig/{self.ve}/Scripts/Activate.ps1""" if self.platform.system() == "Windows" else rf"""source ~/code/machineconfig/{self.ve}/bin/activate""" # works for both cmd and pwsh
|
|
134
|
+
|
|
135
|
+
def __getstate__(self):
|
|
136
|
+
return {attr: self.__getattribute__(attr) for attr in ["username", "hostname", "host", "port", "sshkey", "compress", "pwd", "ve"]}
|
|
137
|
+
|
|
138
|
+
def __setstate__(self, state: dict[str, Any]):
|
|
139
|
+
SSH(**state)
|
|
140
|
+
|
|
121
141
|
def get_remote_machine(self) -> MACHINE:
|
|
122
142
|
if self._remote_machine is None:
|
|
123
|
-
if
|
|
124
|
-
|
|
143
|
+
if self.run("$env:OS", verbose=False, desc="Testing Remote OS Type").op == "Windows_NT" or self.run("echo %OS%", verbose=False, desc="Testing Remote OS Type Again").op == "Windows_NT":
|
|
144
|
+
self._remote_machine = "Windows"
|
|
145
|
+
else:
|
|
146
|
+
self._remote_machine = "Linux"
|
|
125
147
|
return self._remote_machine # echo %OS% TODO: uname on linux
|
|
148
|
+
|
|
126
149
|
def get_local_distro(self) -> str:
|
|
127
150
|
if self._local_distro is None:
|
|
128
151
|
command = """uv run --with distro python -c "import distro; print(distro.name(pretty=True))" """
|
|
129
152
|
import subprocess
|
|
153
|
+
|
|
130
154
|
res = subprocess.run(command, shell=True, capture_output=True, text=True).stdout.strip()
|
|
131
155
|
self._local_distro = res
|
|
132
156
|
return res
|
|
133
157
|
return self._local_distro
|
|
158
|
+
|
|
134
159
|
def get_remote_distro(self):
|
|
135
160
|
if self._remote_distro is None:
|
|
136
161
|
self._remote_distro = self.run_py("print(install_n_import('distro').name(pretty=True))", verbose=False).op_if_successfull_or_default() or ""
|
|
137
162
|
# q.run("""~/.local/bin/uv run --with distro python -c "import distro; print(distro.name(pretty=True))" """)
|
|
138
163
|
return self._remote_distro
|
|
139
|
-
|
|
164
|
+
|
|
165
|
+
def restart_computer(self):
|
|
166
|
+
self.run("Restart-Computer -Force" if self.get_remote_machine() == "Windows" else "sudo reboot")
|
|
167
|
+
|
|
140
168
|
def send_ssh_key(self):
|
|
141
169
|
self.copy_from_here("~/.ssh/id_rsa.pub")
|
|
142
170
|
assert self.get_remote_machine() == "Windows"
|
|
143
171
|
code_url = "https://raw.githubusercontent.com/thisismygitrepo/machineconfig/refs/heads/main/src/machineconfig/setup_windows/openssh-server_add-sshkey.ps1"
|
|
144
|
-
code =
|
|
172
|
+
code = PathExtended(code_url).download().read_text(encoding="utf-8")
|
|
145
173
|
self.run(code)
|
|
174
|
+
|
|
146
175
|
def copy_env_var(self, name: str):
|
|
147
176
|
assert self.get_remote_machine() == "Linux"
|
|
148
177
|
return self.run(f"{name} = {os.environ[name]}; export {name}")
|
|
149
|
-
|
|
178
|
+
|
|
179
|
+
def get_remote_repr(self, add_machine: bool = False) -> str:
|
|
180
|
+
return f"{self.username}@{self.hostname}:{self.port}" + (f" [{self.get_remote_machine()}][{self.get_remote_distro()}]" if add_machine else "")
|
|
181
|
+
|
|
150
182
|
def get_local_repr(self, add_machine: bool = False) -> str:
|
|
151
183
|
import getpass
|
|
184
|
+
|
|
152
185
|
return f"{getpass.getuser()}@{self.platform.node()}" + (f" [{self.platform.system()}][{self.get_local_distro()}]" if add_machine else "")
|
|
153
|
-
|
|
186
|
+
|
|
187
|
+
def __repr__(self):
|
|
188
|
+
return f"local {self.get_local_repr(add_machine=True)} >>> SSH TO >>> remote {self.get_remote_repr(add_machine=True)}"
|
|
189
|
+
|
|
154
190
|
def run_locally(self, command: str):
|
|
155
191
|
print(f"""💻 [LOCAL EXECUTION] Running command on node: {self.platform.node()} Command: {command}""")
|
|
156
192
|
res = Response(cmd=command)
|
|
157
193
|
res.output.returncode = os.system(command)
|
|
158
194
|
return res
|
|
159
|
-
|
|
195
|
+
|
|
196
|
+
def get_ssh_conn_str(self, cmd: str = ""):
|
|
197
|
+
return "ssh " + (f" -i {self.sshkey}" if self.sshkey else "") + self.get_remote_repr().replace(":", " -p ") + (f" -t {cmd} " if cmd != "" else " ")
|
|
198
|
+
|
|
160
199
|
# def open_console(self, cmd: str = '', new_window: bool = True, terminal: Optional[str] = None, shell: str = "pwsh"): Terminal().run_async(*(self.get_ssh_conn_str(cmd=cmd).split(" ")), new_window=new_window, terminal=terminal, shell=shell)
|
|
161
200
|
def run(self, cmd: str, verbose: bool = True, desc: str = "", strict_err: bool = False, strict_returncode: bool = False, env_prefix: bool = False) -> Response: # most central method.
|
|
162
201
|
cmd = (self.remote_env_cmd + "; " + cmd) if env_prefix else cmd
|
|
163
202
|
raw = self.ssh.exec_command(cmd)
|
|
164
203
|
res = Response(stdin=raw[0], stdout=raw[1], stderr=raw[2], cmd=cmd, desc=desc) # type: ignore
|
|
165
|
-
if not verbose:
|
|
166
|
-
|
|
204
|
+
if not verbose:
|
|
205
|
+
res.capture().print_if_unsuccessful(desc=desc, strict_err=strict_err, strict_returncode=strict_returncode, assert_success=False)
|
|
206
|
+
else:
|
|
207
|
+
res.print()
|
|
167
208
|
self.terminal_responses.append(res)
|
|
168
209
|
return res
|
|
210
|
+
|
|
169
211
|
def run_py(self, cmd: str, desc: str = "", return_obj: bool = False, verbose: bool = True, strict_err: bool = False, strict_returncode: bool = False) -> Union[Any, Response]:
|
|
170
212
|
assert '"' not in cmd, 'Avoid using `"` in your command. I dont know how to handle this when passing is as command to python in pwsh command.'
|
|
171
|
-
if not return_obj:
|
|
213
|
+
if not return_obj:
|
|
214
|
+
return self.run(
|
|
215
|
+
cmd=f"""{self.remote_env_cmd}; python -c "{Terminal.get_header(wdir=None, toolbox=True)}{cmd}\n""" + '"', desc=desc or f"run_py on {self.get_remote_repr()}", verbose=verbose, strict_err=strict_err, strict_returncode=strict_returncode
|
|
216
|
+
)
|
|
172
217
|
assert "obj=" in cmd, "The command sent to run_py must have `obj=` statement if return_obj is set to True"
|
|
173
|
-
source_file = self.run_py(f"""{cmd}\npath = Save.pickle(obj=obj, path=P.tmpfile(suffix='.pkl'))\nprint(path)""", desc=desc, verbose=verbose, strict_err=True, strict_returncode=True).op.split(
|
|
174
|
-
res = self.copy_to_here(source=source_file, target=
|
|
218
|
+
source_file = self.run_py(f"""{cmd}\npath = Save.pickle(obj=obj, path=P.tmpfile(suffix='.pkl'))\nprint(path)""", desc=desc, verbose=verbose, strict_err=True, strict_returncode=True).op.split("\n")[-1]
|
|
219
|
+
res = self.copy_to_here(source=source_file, target=PathExtended.tmpfile(suffix=".pkl"))
|
|
175
220
|
import pickle
|
|
221
|
+
|
|
176
222
|
res_bytes = res.read_bytes()
|
|
177
223
|
return pickle.loads(res_bytes)
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
224
|
+
|
|
225
|
+
def copy_from_here(self, source: PLike, target: OPLike = None, z: bool = False, r: bool = False, overwrite: bool = False, init: bool = True) -> Union[PathExtended, list[PathExtended]]:
|
|
226
|
+
if init:
|
|
227
|
+
print(f"{'⬆️' * 5} [SFTP UPLOAD] FROM `{source}` TO `{target}`") # TODO: using return_obj do all tests required in one go.
|
|
228
|
+
source_obj = PathExtended(source).expanduser().absolute()
|
|
229
|
+
if not source_obj.exists():
|
|
230
|
+
raise RuntimeError(f"Meta.SSH Error: source `{source_obj}` does not exist!")
|
|
182
231
|
if target is None:
|
|
183
|
-
target =
|
|
232
|
+
target = PathExtended(source_obj).expanduser().absolute().collapseuser(strict=True)
|
|
184
233
|
assert target.is_relative_to("~"), "If target is not specified, source must be relative to home."
|
|
185
|
-
if z:
|
|
234
|
+
if z:
|
|
235
|
+
target += ".zip"
|
|
186
236
|
if not z and source_obj.is_dir():
|
|
187
|
-
if r is False:
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
237
|
+
if r is False:
|
|
238
|
+
raise RuntimeError(f"Meta.SSH Error: source `{source_obj}` is a directory! either set `r=True` for recursive sending or raise `z=True` flag to zip it first.")
|
|
239
|
+
source_list: list[PathExtended] = source_obj.search("*", folders=False, files=True, r=True)
|
|
240
|
+
remote_root = (
|
|
241
|
+
self.run_py(
|
|
242
|
+
f"path=P(r'{PathExtended(target).as_posix()}').expanduser()\n{'path.delete(sure=True)' if overwrite else ''}\nprint(path.create())", desc=f"Creating Target directory `{PathExtended(target).as_posix()}` @ {self.get_remote_repr()}", verbose=False
|
|
243
|
+
).op
|
|
244
|
+
or ""
|
|
245
|
+
)
|
|
246
|
+
for idx, item in enumerate(source_list):
|
|
247
|
+
print(f" {idx + 1:03d}. {item}")
|
|
191
248
|
for item in source_list:
|
|
192
|
-
a__target =
|
|
249
|
+
a__target = PathExtended(remote_root).joinpath(item.relative_to(source_obj))
|
|
193
250
|
self.copy_from_here(source=item, target=a__target)
|
|
194
251
|
return list(source_list)
|
|
195
252
|
if z:
|
|
196
253
|
print("🗜️ ZIPPING ...")
|
|
197
|
-
source_obj =
|
|
198
|
-
remotepath =
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
254
|
+
source_obj = PathExtended(source_obj).expanduser().zip(content=True) # .append(f"_{randstr()}", inplace=True) # eventually, unzip will raise content flag, so this name doesn't matter.
|
|
255
|
+
remotepath = (
|
|
256
|
+
self.run_py(
|
|
257
|
+
f"path=P(r'{PathExtended(target).as_posix()}').expanduser()\n{'path.delete(sure=True)' if overwrite else ''}\nprint(path.parent.create())",
|
|
258
|
+
desc=f"Creating Target directory `{PathExtended(target).parent.as_posix()}` @ {self.get_remote_repr()}",
|
|
259
|
+
verbose=False,
|
|
260
|
+
).op
|
|
261
|
+
or ""
|
|
262
|
+
)
|
|
263
|
+
remotepath = PathExtended(remotepath.split("\n")[-1]).joinpath(PathExtended(target).name)
|
|
264
|
+
print(f"""📤 [SFTP UPLOAD] Sending file: {repr(PathExtended(source_obj))} ==> Remote Path: {remotepath.as_posix()}""")
|
|
265
|
+
with self.tqdm_wrap(ascii=True, unit="b", unit_scale=True) as pbar:
|
|
266
|
+
self.sftp.put(localpath=PathExtended(source_obj).expanduser(), remotepath=remotepath.as_posix(), callback=pbar.view_bar) # type: ignore # pylint: disable=E1129
|
|
202
267
|
if z:
|
|
203
268
|
_resp = self.run_py(f"""P(r'{remotepath.as_posix()}').expanduser().unzip(content=False, inplace=True, overwrite={overwrite})""", desc=f"UNZIPPING {remotepath.as_posix()}", verbose=False, strict_err=True, strict_returncode=True)
|
|
204
269
|
source_obj.delete(sure=True)
|
|
205
270
|
print("\n")
|
|
206
271
|
return source_obj
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
if
|
|
210
|
-
|
|
272
|
+
|
|
273
|
+
def copy_to_here(self, source: PLike, target: OPLike = None, z: bool = False, r: bool = False, init: bool = True) -> PathExtended:
|
|
274
|
+
if init:
|
|
275
|
+
print(f"{'⬇️' * 5} SFTP DOWNLOADING FROM `{source}` TO `{target}`")
|
|
276
|
+
if not z and self.run_py(f"print(P(r'{source}').expanduser().absolute().is_dir())", desc=f"Check if source `{source}` is a dir", verbose=False, strict_returncode=True, strict_err=True).op.split("\n")[-1] == "True":
|
|
277
|
+
if r is False:
|
|
278
|
+
raise RuntimeError(f"source `{source}` is a directory! either set r=True for recursive sending or raise zip_first flag.")
|
|
211
279
|
source_list = self.run_py(f"obj=P(r'{source}').search(folders=False, r=True).collapseuser(strict=False)", desc="Searching for files in source", return_obj=True, verbose=False)
|
|
212
280
|
assert isinstance(source_list, List), f"Could not resolve source path {source} due to error"
|
|
213
281
|
for file in source_list:
|
|
214
|
-
self.copy_to_here(source=file.as_posix(), target=
|
|
282
|
+
self.copy_to_here(source=file.as_posix(), target=PathExtended(target).joinpath(PathExtended(file).relative_to(source)) if target else None, r=False)
|
|
215
283
|
if z:
|
|
216
284
|
tmp: Response = self.run_py(f"print(P(r'{source}').expanduser().zip(inplace=False, verbose=False))", desc=f"Zipping source file {source}", verbose=False)
|
|
217
285
|
tmp2 = tmp.op2path(strict_returncode=True, strict_err=True)
|
|
218
|
-
if not isinstance(tmp2,
|
|
219
|
-
|
|
286
|
+
if not isinstance(tmp2, PathExtended):
|
|
287
|
+
raise RuntimeError(f"Could not zip {source} due to {tmp.err}")
|
|
288
|
+
else:
|
|
289
|
+
source = tmp2
|
|
220
290
|
if target is None:
|
|
221
|
-
tmpx = self.run_py(f"print(P(r'{
|
|
222
|
-
if isinstance(tmpx,
|
|
223
|
-
|
|
291
|
+
tmpx = self.run_py(f"print(P(r'{PathExtended(source).as_posix()}').collapseuser(strict=False).as_posix())", desc="Finding default target via relative source path", strict_returncode=True, strict_err=True, verbose=False).op2path()
|
|
292
|
+
if isinstance(tmpx, PathExtended):
|
|
293
|
+
target = tmpx
|
|
294
|
+
else:
|
|
295
|
+
raise RuntimeError(f"Could not resolve target path {target} due to error")
|
|
224
296
|
assert target.is_relative_to("~"), f"If target is not specified, source must be relative to home.\n{target=}"
|
|
225
|
-
target_obj =
|
|
297
|
+
target_obj = PathExtended(target).expanduser().absolute()
|
|
226
298
|
target_obj.parent.mkdir(parents=True, exist_ok=True)
|
|
227
|
-
if z and
|
|
299
|
+
if z and ".zip" not in target_obj.suffix:
|
|
300
|
+
target_obj += ".zip"
|
|
228
301
|
if "~" in str(source):
|
|
229
302
|
tmp3 = self.run_py(f"print(P(r'{source}').expanduser())", desc="# Resolving source path address by expanding user", strict_returncode=True, strict_err=True, verbose=False).op2path()
|
|
230
|
-
if isinstance(tmp3,
|
|
231
|
-
|
|
232
|
-
|
|
303
|
+
if isinstance(tmp3, PathExtended):
|
|
304
|
+
source = tmp3
|
|
305
|
+
else:
|
|
306
|
+
raise RuntimeError(f"Could not resolve source path {source} due to")
|
|
307
|
+
else:
|
|
308
|
+
source = PathExtended(source)
|
|
233
309
|
print(f"""📥 [DOWNLOAD] Receiving: {source} ==> Local Path: {target_obj}""")
|
|
234
|
-
with self.tqdm_wrap(ascii=True, unit=
|
|
310
|
+
with self.tqdm_wrap(ascii=True, unit="b", unit_scale=True) as pbar: # type: ignore # pylint: disable=E1129
|
|
235
311
|
assert self.sftp is not None, f"Could not establish SFTP connection to {self.hostname}."
|
|
236
312
|
self.sftp.get(remotepath=source.as_posix(), localpath=str(target_obj), callback=pbar.view_bar) # type: ignore
|
|
237
313
|
if z:
|
|
@@ -239,27 +315,32 @@ class SSH: # inferior alternative: https://github.com/fabric/fabric
|
|
|
239
315
|
self.run_py(f"P(r'{source.as_posix()}').delete(sure=True)", desc="Cleaning temp zip files @ remote.", strict_returncode=True, strict_err=True, verbose=False)
|
|
240
316
|
print("\n")
|
|
241
317
|
return target_obj
|
|
242
|
-
|
|
318
|
+
|
|
319
|
+
def receieve(self, source: PLike, target: OPLike = None, z: bool = False, r: bool = False) -> PathExtended:
|
|
243
320
|
scout = self.run_py(cmd=f"obj=scout(r'{source}', z={z}, r={r})", desc=f"Scouting source `{source}` path on remote", return_obj=True, verbose=False)
|
|
244
321
|
assert isinstance(scout, Scout)
|
|
245
322
|
if not z and scout.is_dir and scout.files is not None:
|
|
246
323
|
if r:
|
|
247
|
-
tmp: list[
|
|
324
|
+
tmp: list[PathExtended] = [self.receieve(source=file.as_posix(), target=PathExtended(target).joinpath(PathExtended(file).relative_to(source)) if target else None, r=False) for file in scout.files]
|
|
248
325
|
return tmp[0]
|
|
249
|
-
else:
|
|
326
|
+
else:
|
|
327
|
+
print("Source is a directory! either set `r=True` for recursive sending or raise `zip_first=True` flag.")
|
|
250
328
|
if target:
|
|
251
|
-
target =
|
|
329
|
+
target = PathExtended(target).expanduser().absolute()
|
|
252
330
|
else:
|
|
253
331
|
target = scout.source_rel2home.expanduser().absolute()
|
|
254
332
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
255
|
-
if z and
|
|
333
|
+
if z and ".zip" not in target.suffix:
|
|
334
|
+
target += ".zip"
|
|
256
335
|
source = scout.source_full
|
|
257
|
-
with self.tqdm_wrap(ascii=True, unit=
|
|
336
|
+
with self.tqdm_wrap(ascii=True, unit="b", unit_scale=True) as pbar:
|
|
337
|
+
self.sftp.get(remotepath=source.as_posix(), localpath=target.as_posix(), callback=pbar.view_bar) # type: ignore # pylint: disable=E1129
|
|
258
338
|
if z:
|
|
259
339
|
target = target.unzip(inplace=True, content=True)
|
|
260
340
|
self.run_py(f"P(r'{source.as_posix()}').delete(sure=True)", desc="Cleaning temp zip files @ remote.", strict_returncode=True, strict_err=True)
|
|
261
341
|
print("\n")
|
|
262
342
|
return target
|
|
343
|
+
|
|
263
344
|
# def print_summary(self):
|
|
264
345
|
# import polars as pl
|
|
265
346
|
# df = pl.DataFrame(List(self.terminal_responses).apply(lambda rsp: dict(desc=rsp.desc, err=rsp.err, returncode=rsp.returncode)).list)
|