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.

Files changed (235) hide show
  1. machineconfig/cluster/cloud_manager.py +0 -3
  2. machineconfig/cluster/data_transfer.py +0 -1
  3. machineconfig/cluster/file_manager.py +0 -1
  4. machineconfig/cluster/job_params.py +0 -3
  5. machineconfig/cluster/loader_runner.py +0 -3
  6. machineconfig/cluster/remote_machine.py +0 -1
  7. machineconfig/cluster/script_notify_upon_completion.py +0 -1
  8. machineconfig/cluster/sessions_managers/archive/create_zellij_template.py +3 -5
  9. machineconfig/cluster/sessions_managers/archive/session_managers.py +0 -1
  10. machineconfig/cluster/sessions_managers/enhanced_command_runner.py +17 -57
  11. machineconfig/cluster/sessions_managers/wt_local.py +36 -110
  12. machineconfig/cluster/sessions_managers/wt_local_manager.py +42 -112
  13. machineconfig/cluster/sessions_managers/wt_remote.py +23 -30
  14. machineconfig/cluster/sessions_managers/wt_remote_manager.py +20 -62
  15. machineconfig/cluster/sessions_managers/wt_utils/layout_generator.py +10 -15
  16. machineconfig/cluster/sessions_managers/wt_utils/process_monitor.py +27 -127
  17. machineconfig/cluster/sessions_managers/wt_utils/remote_executor.py +10 -43
  18. machineconfig/cluster/sessions_managers/wt_utils/session_manager.py +22 -101
  19. machineconfig/cluster/sessions_managers/wt_utils/status_reporter.py +11 -39
  20. machineconfig/cluster/sessions_managers/zellij_local.py +49 -102
  21. machineconfig/cluster/sessions_managers/zellij_local_manager.py +34 -78
  22. machineconfig/cluster/sessions_managers/zellij_remote.py +17 -24
  23. machineconfig/cluster/sessions_managers/zellij_remote_manager.py +7 -13
  24. machineconfig/cluster/sessions_managers/zellij_utils/example_usage.py +4 -2
  25. machineconfig/cluster/sessions_managers/zellij_utils/layout_generator.py +6 -6
  26. machineconfig/cluster/sessions_managers/zellij_utils/process_monitor.py +18 -88
  27. machineconfig/cluster/sessions_managers/zellij_utils/remote_executor.py +2 -6
  28. machineconfig/cluster/sessions_managers/zellij_utils/session_manager.py +12 -40
  29. machineconfig/cluster/sessions_managers/zellij_utils/status_reporter.py +3 -2
  30. machineconfig/cluster/templates/cli_click.py +0 -1
  31. machineconfig/cluster/templates/cli_gooey.py +0 -2
  32. machineconfig/cluster/templates/cli_trogon.py +0 -1
  33. machineconfig/cluster/templates/run_cloud.py +0 -1
  34. machineconfig/cluster/templates/run_cluster.py +0 -1
  35. machineconfig/cluster/templates/run_remote.py +0 -1
  36. machineconfig/cluster/templates/utils.py +26 -10
  37. machineconfig/jobs/__pycache__/__init__.cpython-313.pyc +0 -0
  38. machineconfig/jobs/linux/msc/cli_agents.sh +16 -0
  39. machineconfig/jobs/python/check_installations.py +1 -0
  40. machineconfig/jobs/python/create_bootable_media.py +0 -2
  41. machineconfig/jobs/python/python_ve_symlink.py +9 -11
  42. machineconfig/jobs/python/tasks.py +0 -1
  43. machineconfig/jobs/python/vscode/api.py +5 -5
  44. machineconfig/jobs/python/vscode/link_ve.py +13 -14
  45. machineconfig/jobs/python/vscode/select_interpreter.py +21 -22
  46. machineconfig/jobs/python/vscode/sync_code.py +9 -13
  47. machineconfig/jobs/python_custom_installers/__pycache__/__init__.cpython-313.pyc +0 -0
  48. machineconfig/jobs/python_custom_installers/archive/ngrok.py +13 -13
  49. machineconfig/jobs/python_custom_installers/dev/aider.py +7 -15
  50. machineconfig/jobs/python_custom_installers/dev/alacritty.py +9 -18
  51. machineconfig/jobs/python_custom_installers/dev/brave.py +10 -19
  52. machineconfig/jobs/python_custom_installers/dev/bypass_paywall.py +8 -15
  53. machineconfig/jobs/python_custom_installers/dev/code.py +14 -21
  54. machineconfig/jobs/python_custom_installers/dev/cursor.py +3 -14
  55. machineconfig/jobs/python_custom_installers/dev/docker_desktop.py +8 -7
  56. machineconfig/jobs/python_custom_installers/dev/espanso.py +15 -19
  57. machineconfig/jobs/python_custom_installers/dev/goes.py +5 -12
  58. machineconfig/jobs/python_custom_installers/dev/lvim.py +9 -17
  59. machineconfig/jobs/python_custom_installers/dev/nerdfont.py +12 -19
  60. machineconfig/jobs/python_custom_installers/dev/redis.py +12 -20
  61. machineconfig/jobs/python_custom_installers/dev/wezterm.py +12 -19
  62. machineconfig/jobs/python_custom_installers/dev/winget.py +5 -23
  63. machineconfig/jobs/python_custom_installers/docker.py +12 -21
  64. machineconfig/jobs/python_custom_installers/gh.py +11 -19
  65. machineconfig/jobs/python_custom_installers/hx.py +32 -16
  66. machineconfig/jobs/python_custom_installers/warp-cli.py +12 -20
  67. machineconfig/jobs/python_generic_installers/__pycache__/__init__.cpython-313.pyc +0 -0
  68. machineconfig/jobs/python_linux_installers/__pycache__/__init__.cpython-313.pyc +0 -0
  69. machineconfig/jobs/windows/archive/archive_pygraphviz.ps1 +1 -1
  70. machineconfig/jobs/windows/msc/cli_agents.bat +0 -0
  71. machineconfig/jobs/windows/msc/cli_agents.ps1 +0 -0
  72. machineconfig/jobs/windows/start_terminal.ps1 +1 -1
  73. machineconfig/profile/create.py +29 -22
  74. machineconfig/profile/create_hardlinks.py +26 -19
  75. machineconfig/profile/shell.py +51 -28
  76. machineconfig/scripts/__pycache__/__init__.cpython-313.pyc +0 -0
  77. machineconfig/scripts/cloud/init.sh +2 -2
  78. machineconfig/scripts/linux/checkout_versions +1 -1
  79. machineconfig/scripts/linux/choose_wezterm_theme +1 -1
  80. machineconfig/scripts/linux/cloud_copy +1 -1
  81. machineconfig/scripts/linux/cloud_manager +1 -1
  82. machineconfig/scripts/linux/cloud_mount +1 -1
  83. machineconfig/scripts/linux/cloud_repo_sync +1 -1
  84. machineconfig/scripts/linux/cloud_sync +1 -1
  85. machineconfig/scripts/linux/croshell +1 -1
  86. machineconfig/scripts/linux/devops +4 -6
  87. machineconfig/scripts/linux/fire +1 -1
  88. machineconfig/scripts/linux/fire_agents +3 -2
  89. machineconfig/scripts/linux/ftpx +1 -1
  90. machineconfig/scripts/linux/gh_models +1 -1
  91. machineconfig/scripts/linux/kill_process +1 -1
  92. machineconfig/scripts/linux/mcinit +1 -1
  93. machineconfig/scripts/linux/repos +1 -1
  94. machineconfig/scripts/linux/scheduler +1 -1
  95. machineconfig/scripts/linux/start_slidev +1 -1
  96. machineconfig/scripts/linux/start_terminals +1 -1
  97. machineconfig/scripts/linux/url2md +1 -1
  98. machineconfig/scripts/linux/warp-cli.sh +122 -0
  99. machineconfig/scripts/linux/wifi_conn +1 -1
  100. machineconfig/scripts/python/__pycache__/__init__.cpython-313.pyc +0 -0
  101. machineconfig/scripts/python/__pycache__/croshell.cpython-313.pyc +0 -0
  102. machineconfig/scripts/python/__pycache__/devops.cpython-313.pyc +0 -0
  103. machineconfig/scripts/python/__pycache__/devops_devapps_install.cpython-313.pyc +0 -0
  104. machineconfig/scripts/python/__pycache__/devops_update_repos.cpython-313.pyc +0 -0
  105. machineconfig/scripts/python/__pycache__/fire_jobs.cpython-313.pyc +0 -0
  106. machineconfig/scripts/python/ai/__init__.py +0 -0
  107. machineconfig/scripts/python/ai/__pycache__/__init__.cpython-313.pyc +0 -0
  108. machineconfig/scripts/python/ai/__pycache__/generate_files.cpython-313.pyc +0 -0
  109. machineconfig/scripts/python/ai/__pycache__/mcinit.cpython-313.pyc +0 -0
  110. machineconfig/scripts/python/ai/generate_files.py +84 -0
  111. machineconfig/scripts/python/ai/instructions/python/dev.instructions.md +2 -2
  112. machineconfig/scripts/python/ai/mcinit.py +7 -3
  113. machineconfig/scripts/python/ai/scripts/lint_and_type_check.sh +10 -5
  114. machineconfig/scripts/python/cloud_copy.py +1 -1
  115. machineconfig/scripts/python/cloud_mount.py +1 -1
  116. machineconfig/scripts/python/cloud_repo_sync.py +4 -4
  117. machineconfig/scripts/python/croshell.py +5 -3
  118. machineconfig/scripts/python/devops_add_identity.py +1 -1
  119. machineconfig/scripts/python/devops_add_ssh_key.py +1 -1
  120. machineconfig/scripts/python/devops_backup_retrieve.py +1 -1
  121. machineconfig/scripts/python/devops_update_repos.py +140 -52
  122. machineconfig/scripts/python/dotfile.py +1 -1
  123. machineconfig/scripts/python/fire_agents.py +28 -9
  124. machineconfig/scripts/python/fire_jobs.py +3 -4
  125. machineconfig/scripts/python/ftpx.py +2 -1
  126. machineconfig/scripts/python/helpers/__pycache__/__init__.cpython-313.pyc +0 -0
  127. machineconfig/scripts/python/helpers/__pycache__/helpers4.cpython-313.pyc +0 -0
  128. machineconfig/scripts/python/helpers/helpers2.py +2 -2
  129. machineconfig/scripts/python/helpers/helpers4.py +1 -2
  130. machineconfig/scripts/python/helpers/repo_sync_helpers.py +1 -1
  131. machineconfig/scripts/python/mount_nfs.py +1 -1
  132. machineconfig/scripts/python/mount_ssh.py +1 -1
  133. machineconfig/scripts/python/repos.py +1 -1
  134. machineconfig/scripts/python/start_slidev.py +1 -1
  135. machineconfig/scripts/python/wsl_windows_transfer.py +1 -1
  136. machineconfig/scripts/windows/checkout_version.ps1 +1 -3
  137. machineconfig/scripts/windows/choose_wezterm_theme.ps1 +1 -3
  138. machineconfig/scripts/windows/cloud_copy.ps1 +2 -6
  139. machineconfig/scripts/windows/cloud_manager.ps1 +1 -1
  140. machineconfig/scripts/windows/cloud_repo_sync.ps1 +1 -2
  141. machineconfig/scripts/windows/cloud_sync.ps1 +2 -2
  142. machineconfig/scripts/windows/croshell.ps1 +2 -2
  143. machineconfig/scripts/windows/devops.ps1 +1 -4
  144. machineconfig/scripts/windows/dotfile.ps1 +1 -3
  145. machineconfig/scripts/windows/fire.ps1 +1 -1
  146. machineconfig/scripts/windows/ftpx.ps1 +2 -2
  147. machineconfig/scripts/windows/gpt.ps1 +1 -1
  148. machineconfig/scripts/windows/kill_process.ps1 +1 -2
  149. machineconfig/scripts/windows/mcinit.ps1 +1 -1
  150. machineconfig/scripts/windows/mount_nfs.ps1 +1 -1
  151. machineconfig/scripts/windows/mount_ssh.ps1 +1 -1
  152. machineconfig/scripts/windows/pomodoro.ps1 +1 -1
  153. machineconfig/scripts/windows/py2exe.ps1 +1 -3
  154. machineconfig/scripts/windows/repos.ps1 +1 -1
  155. machineconfig/scripts/windows/scheduler.ps1 +1 -1
  156. machineconfig/scripts/windows/snapshot.ps1 +2 -2
  157. machineconfig/scripts/windows/start_slidev.ps1 +1 -1
  158. machineconfig/scripts/windows/start_terminals.ps1 +1 -1
  159. machineconfig/scripts/windows/wifi_conn.ps1 +1 -1
  160. machineconfig/scripts/windows/wsl_windows_transfer.ps1 +1 -3
  161. machineconfig/settings/lf/linux/lfrc +1 -1
  162. machineconfig/settings/linters/.ruff_cache/.gitignore +2 -0
  163. machineconfig/settings/linters/.ruff_cache/CACHEDIR.TAG +1 -0
  164. machineconfig/settings/lvim/windows/archive/config_additional.lua +1 -1
  165. machineconfig/settings/svim/linux/init.toml +1 -1
  166. machineconfig/settings/svim/windows/init.toml +1 -1
  167. machineconfig/setup_linux/web_shortcuts/croshell.sh +0 -54
  168. machineconfig/setup_linux/web_shortcuts/interactive.sh +6 -6
  169. machineconfig/setup_windows/web_shortcuts/all.ps1 +2 -2
  170. machineconfig/setup_windows/web_shortcuts/ascii_art.ps1 +1 -1
  171. machineconfig/setup_windows/web_shortcuts/croshell.ps1 +1 -1
  172. machineconfig/setup_windows/web_shortcuts/interactive.ps1 +5 -5
  173. machineconfig/setup_windows/wt_and_pwsh/install_fonts.ps1 +51 -15
  174. machineconfig/setup_windows/wt_and_pwsh/set_pwsh_theme.py +66 -12
  175. machineconfig/setup_windows/wt_and_pwsh/set_wt_settings.py +44 -36
  176. machineconfig/utils/ai/generate_file_checklist.py +8 -10
  177. machineconfig/utils/ai/url2md.py +4 -2
  178. machineconfig/utils/cloud/onedrive/setup_oauth.py +1 -0
  179. machineconfig/utils/cloud/onedrive/transaction.py +63 -98
  180. machineconfig/utils/code.py +60 -39
  181. machineconfig/utils/installer.py +27 -33
  182. machineconfig/utils/installer_utils/installer_abc.py +8 -7
  183. machineconfig/utils/installer_utils/installer_class.py +149 -70
  184. machineconfig/utils/links.py +22 -11
  185. machineconfig/utils/notifications.py +197 -0
  186. machineconfig/utils/options.py +29 -23
  187. machineconfig/utils/path.py +13 -6
  188. machineconfig/utils/path_reduced.py +485 -216
  189. machineconfig/utils/procs.py +47 -41
  190. machineconfig/utils/scheduling.py +0 -1
  191. machineconfig/utils/ssh.py +157 -76
  192. machineconfig/utils/terminal.py +82 -37
  193. machineconfig/utils/utils.py +12 -10
  194. machineconfig/utils/utils2.py +38 -48
  195. machineconfig/utils/utils5.py +183 -116
  196. machineconfig/utils/ve.py +9 -4
  197. {machineconfig-2.0.dist-info → machineconfig-2.1.dist-info}/METADATA +3 -2
  198. {machineconfig-2.0.dist-info → machineconfig-2.1.dist-info}/RECORD +200 -217
  199. machineconfig/jobs/__pycache__/__init__.cpython-311.pyc +0 -0
  200. machineconfig/jobs/python/__pycache__/__init__.cpython-311.pyc +0 -0
  201. machineconfig/jobs/python/__pycache__/python_ve_symlink.cpython-311.pyc +0 -0
  202. machineconfig/jobs/python/archive/python_tools.txt +0 -12
  203. machineconfig/jobs/python/vscode/__pycache__/select_interpreter.cpython-311.pyc +0 -0
  204. machineconfig/jobs/python_custom_installers/__pycache__/__init__.cpython-311.pyc +0 -0
  205. machineconfig/jobs/python_generic_installers/__pycache__/__init__.cpython-311.pyc +0 -0
  206. machineconfig/jobs/python_generic_installers/update.py +0 -3
  207. machineconfig/jobs/python_linux_installers/__pycache__/__init__.cpython-311.pyc +0 -0
  208. machineconfig/profile/__pycache__/__init__.cpython-311.pyc +0 -0
  209. machineconfig/profile/__pycache__/create.cpython-311.pyc +0 -0
  210. machineconfig/profile/__pycache__/shell.cpython-311.pyc +0 -0
  211. machineconfig/scripts/__pycache__/__init__.cpython-311.pyc +0 -0
  212. machineconfig/scripts/linux/activate_ve +0 -87
  213. machineconfig/scripts/python/__pycache__/__init__.cpython-311.pyc +0 -0
  214. machineconfig/scripts/python/__pycache__/cloud_copy.cpython-311.pyc +0 -0
  215. machineconfig/scripts/python/__pycache__/cloud_mount.cpython-311.pyc +0 -0
  216. machineconfig/scripts/python/__pycache__/cloud_sync.cpython-311.pyc +0 -0
  217. machineconfig/scripts/python/__pycache__/croshell.cpython-311.pyc +0 -0
  218. machineconfig/scripts/python/__pycache__/devops.cpython-311.pyc +0 -0
  219. machineconfig/scripts/python/__pycache__/devops_backup_retrieve.cpython-311.pyc +0 -0
  220. machineconfig/scripts/python/__pycache__/devops_devapps_install.cpython-311.pyc +0 -0
  221. machineconfig/scripts/python/__pycache__/devops_update_repos.cpython-311.pyc +0 -0
  222. machineconfig/scripts/python/__pycache__/fire_agents.cpython-311.pyc +0 -0
  223. machineconfig/scripts/python/__pycache__/fire_jobs.cpython-311.pyc +0 -0
  224. machineconfig/scripts/python/__pycache__/get_zellij_cmd.cpython-311.pyc +0 -0
  225. machineconfig/scripts/python/__pycache__/repos.cpython-311.pyc +0 -0
  226. machineconfig/scripts/python/ai/__pycache__/init.cpython-311.pyc +0 -0
  227. machineconfig/scripts/python/ai/__pycache__/mcinit.cpython-311.pyc +0 -0
  228. machineconfig/scripts/python/helpers/__pycache__/__init__.cpython-311.pyc +0 -0
  229. machineconfig/scripts/python/helpers/__pycache__/cloud_helpers.cpython-311.pyc +0 -0
  230. machineconfig/scripts/python/helpers/__pycache__/helpers2.cpython-311.pyc +0 -0
  231. machineconfig/scripts/python/helpers/__pycache__/helpers4.cpython-311.pyc +0 -0
  232. machineconfig/scripts/python/helpers/__pycache__/repo_sync_helpers.cpython-311.pyc +0 -0
  233. machineconfig/scripts/windows/activate_ve.ps1 +0 -54
  234. {machineconfig-2.0.dist-info → machineconfig-2.1.dist-info}/WHEEL +0 -0
  235. {machineconfig-2.0.dist-info → machineconfig-2.1.dist-info}/top_level.txt +0 -0
@@ -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 = [{'pid': pid, 'files': files} for pid, files in res.items()]
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('UTC'))
61
- create_time_local = create_time_utc.astimezone(timezone('Australia/Adelaide'))
62
-
63
- process_info.append({
64
- 'pid': proc.pid,
65
- 'name': proc.name(),
66
- 'username': proc.username(),
67
- 'cpu_percent': proc.cpu_percent(),
68
- 'memory_usage_mb': mem_usage_mb,
69
- 'status': proc.status(),
70
- 'create_time': create_time_local,
71
- 'command': " ".join(proc.cmdline())
72
- })
73
- except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): pass
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['memory_usage_mb'], reverse=True)
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 = ['PID', 'Name', 'Username', 'CPU%', 'Memory(MB)', 'Status', 'Create Time', 'Command']
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['create_time'].strftime('%Y-%m-%d %H:%M:%S')
97
+ create_time_str = process["create_time"].strftime("%Y-%m-%d %H:%M:%S")
95
98
  # Truncate command if too long
96
- command = process['command'][:47] + "..." if len(process['command']) > 50 else process['command']
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['pid'] for p in selected_processes])
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['pid'] for p in target_processes]) if input("\n⚠️ Confirm termination? y/[n] ").lower() == "y" else None
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['name'] == name]
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['create_time'])
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['pid'] for p in filtered_processes])
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('names, pids and commands cannot all be None')
159
- if names is None: names = []
160
- if pids is None: pids = []
161
- if commands is None: commands = []
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['name'] == name]
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['pid']).kill()
170
- print(f'💀 Killed process {name} with PID {process["pid"]}. It lived {get_age(process["create_time"])}. RIP 🪦💐')
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'❓ No process with PID {pid} found')
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['command']]
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['pid']).kill()
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('UTC'))
205
- create_time_local = create_time_utc.astimezone(timezone('Australia/Adelaide'))
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('Australia/Adelaide'))
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__ == '__main__':
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:
@@ -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 P, PLike, OPLike
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: P
13
- source_rel2home: P
12
+ source_full: PathExtended
13
+ source_rel2home: PathExtended
14
14
  exists: bool
15
15
  is_dir: bool
16
- files: Optional[List[P]]
16
+ files: Optional[List[PathExtended]]
17
+
18
+
17
19
  def scout(source: PLike, z: bool = False, r: bool = False) -> Scout:
18
- source_full = P(source).expanduser().absolute()
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: source_full = source_full.zip()
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: files = None
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__(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] = "ve", compress: bool = False): # https://stackoverflow.com/questions/51027192/execute-command-script-using-different-shell-in-ssh-paramiko
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
- config = pconfig.SSHConfig.from_path(str(P.home().joinpath(".ssh/config")))
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): sshkey = tmp[0]
58
- else: sshkey = tmp
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): sshkey = tmp[0]
63
- else: sshkey = tmp
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: self.username, self.hostname = host.split("@")
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(P(sshkey).expanduser().absolute()) if sshkey is not None else None # no need to pass sshkey if it was configured properly already
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: self.sftp: Optional[paramiko.SFTPClient] = self.ssh.open_sftp()
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
- self.tqdm_wrap = type('TqdmWrap', (tqdm,), {'view_bar': view_bar})
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"""~/venvs/{self.ve}/Scripts/Activate.ps1""" if self.get_remote_machine() == "Windows" else rf"""source ~/venvs/{self.ve}/bin/activate"""
118
- self.local_env_cmd = rf"""~/venvs/{self.ve}/Scripts/Activate.ps1""" if self.platform.system() == "Windows" else rf"""source ~/venvs/{self.ve}/bin/activate""" # works for both cmd and pwsh
119
- def __getstate__(self): return {attr: self.__getattribute__(attr) for attr in ["username", "hostname", "host", "port", "sshkey", "compress", "pwd", "ve"]}
120
- def __setstate__(self, state: dict[str, Any]): SSH(**state)
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 (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"): self._remote_machine = "Windows"
124
- else: self._remote_machine = "Linux"
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
- def restart_computer(self): self.run("Restart-Computer -Force" if self.get_remote_machine() == "Windows" else "sudo reboot")
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 = P(code_url).download().read_text(encoding="utf-8")
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
- def get_remote_repr(self, add_machine: bool = False) -> str: return f"{self.username}@{self.hostname}:{self.port}" + (f" [{self.get_remote_machine()}][{self.get_remote_distro()}]" if add_machine else "")
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
- def __repr__(self): return f"local {self.get_local_repr(add_machine=True)} >>> SSH TO >>> remote {self.get_remote_repr(add_machine=True)}"
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
- def get_ssh_conn_str(self, cmd: str = ""): return "ssh " + (f" -i {self.sshkey}" if self.sshkey else "") + self.get_remote_repr().replace(':', ' -p ') + (f' -t {cmd} ' if cmd != '' else ' ')
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: res.capture().print_if_unsuccessful(desc=desc, strict_err=strict_err, strict_returncode=strict_returncode, assert_success=False)
166
- else: res.print()
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: return self.run(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)
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('\n')[-1]
174
- res = self.copy_to_here(source=source_file, target=P.tmpfile(suffix='.pkl'))
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
- def copy_from_here(self, source: PLike, target: OPLike = None, z: bool = False, r: bool = False, overwrite: bool = False, init: bool = True) -> Union[P, list[P]]:
179
- if init: print(f"{'⬆️' * 5} [SFTP UPLOAD] FROM `{source}` TO `{target}`") # TODO: using return_obj do all tests required in one go.
180
- source_obj = P(source).expanduser().absolute()
181
- if not source_obj.exists(): raise RuntimeError(f"Meta.SSH Error: source `{source_obj}` does not exist!")
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 = P(source_obj).expanduser().absolute().collapseuser(strict=True)
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: target += ".zip"
234
+ if z:
235
+ target += ".zip"
186
236
  if not z and source_obj.is_dir():
187
- if r is False: 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.")
188
- source_list: list[P] = source_obj.search("*", folders=False, files=True, r=True)
189
- remote_root = self.run_py(f"path=P(r'{P(target).as_posix()}').expanduser()\n{'path.delete(sure=True)' if overwrite else ''}\nprint(path.create())", desc=f"Creating Target directory `{P(target).as_posix()}` @ {self.get_remote_repr()}", verbose=False).op or ''
190
- for idx, item in enumerate(source_list): print(f" {idx+1:03d}. {item}")
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 = P(remote_root).joinpath(item.relative_to(source_obj))
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 = P(source_obj).expanduser().zip(content=True) # .append(f"_{randstr()}", inplace=True) # eventually, unzip will raise content flag, so this name doesn't matter.
198
- remotepath = self.run_py(f"path=P(r'{P(target).as_posix()}').expanduser()\n{'path.delete(sure=True)' if overwrite else ''}\nprint(path.parent.create())", desc=f"Creating Target directory `{P(target).parent.as_posix()}` @ {self.get_remote_repr()}", verbose=False).op or ''
199
- remotepath = P(remotepath.split("\n")[-1]).joinpath(P(target).name)
200
- print(f"""📤 [SFTP UPLOAD] Sending file: {repr(P(source_obj))} ==> Remote Path: {remotepath.as_posix()}""")
201
- with self.tqdm_wrap(ascii=True, unit='b', unit_scale=True) as pbar: self.sftp.put(localpath=P(source_obj).expanduser(), remotepath=remotepath.as_posix(), callback=pbar.view_bar) # type: ignore # pylint: disable=E1129
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
- def copy_to_here(self, source: PLike, target: OPLike = None, z: bool = False, r: bool = False, init: bool = True) -> P:
208
- if init: print(f"{'⬇️' * 5} SFTP DOWNLOADING FROM `{source}` TO `{target}`")
209
- 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':
210
- if r is False: raise RuntimeError(f"source `{source}` is a directory! either set r=True for recursive sending or raise zip_first flag.")
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=P(target).joinpath(P(file).relative_to(source)) if target else None, r=False)
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, P): raise RuntimeError(f"Could not zip {source} due to {tmp.err}")
219
- else: source = tmp2
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'{P(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()
222
- if isinstance(tmpx, P): target = tmpx
223
- else: raise RuntimeError(f"Could not resolve target path {target} due to error")
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 = P(target).expanduser().absolute()
297
+ target_obj = PathExtended(target).expanduser().absolute()
226
298
  target_obj.parent.mkdir(parents=True, exist_ok=True)
227
- if z and '.zip' not in target_obj.suffix: target_obj += '.zip'
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, P): source = tmp3
231
- else: raise RuntimeError(f"Could not resolve source path {source} due to")
232
- else: source = P(source)
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='b', unit_scale=True) as pbar: # type: ignore # pylint: disable=E1129
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
- def receieve(self, source: PLike, target: OPLike = None, z: bool = False, r: bool = False) -> P:
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[P] = [self.receieve(source=file.as_posix(), target=P(target).joinpath(P(file).relative_to(source)) if target else None, r=False) for file in scout.files]
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: print("Source is a directory! either set `r=True` for recursive sending or raise `zip_first=True` flag.")
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 = P(target).expanduser().absolute()
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 '.zip' not in target.suffix: target += '.zip'
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='b', unit_scale=True) as pbar: self.sftp.get(remotepath=source.as_posix(), localpath=target.as_posix(), callback=pbar.view_bar) # type: ignore # pylint: disable=E1129
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)