machineconfig 1.97__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 (268) hide show
  1. machineconfig/cluster/cloud_manager.py +22 -29
  2. machineconfig/cluster/data_transfer.py +2 -3
  3. machineconfig/cluster/distribute.py +0 -2
  4. machineconfig/cluster/file_manager.py +4 -5
  5. machineconfig/cluster/job_params.py +1 -4
  6. machineconfig/cluster/loader_runner.py +8 -11
  7. machineconfig/cluster/remote_machine.py +4 -5
  8. machineconfig/cluster/script_execution.py +2 -2
  9. machineconfig/cluster/script_notify_upon_completion.py +0 -1
  10. machineconfig/cluster/sessions_managers/archive/create_zellij_template.py +4 -6
  11. machineconfig/cluster/sessions_managers/archive/session_managers.py +0 -1
  12. machineconfig/cluster/sessions_managers/enhanced_command_runner.py +35 -75
  13. machineconfig/cluster/sessions_managers/wt_local.py +113 -185
  14. machineconfig/cluster/sessions_managers/wt_local_manager.py +127 -197
  15. machineconfig/cluster/sessions_managers/wt_remote.py +60 -67
  16. machineconfig/cluster/sessions_managers/wt_remote_manager.py +110 -149
  17. machineconfig/cluster/sessions_managers/wt_utils/layout_generator.py +61 -64
  18. machineconfig/cluster/sessions_managers/wt_utils/process_monitor.py +72 -172
  19. machineconfig/cluster/sessions_managers/wt_utils/remote_executor.py +27 -60
  20. machineconfig/cluster/sessions_managers/wt_utils/session_manager.py +58 -137
  21. machineconfig/cluster/sessions_managers/wt_utils/status_reporter.py +46 -74
  22. machineconfig/cluster/sessions_managers/zellij_local.py +91 -147
  23. machineconfig/cluster/sessions_managers/zellij_local_manager.py +165 -190
  24. machineconfig/cluster/sessions_managers/zellij_remote.py +51 -58
  25. machineconfig/cluster/sessions_managers/zellij_remote_manager.py +40 -46
  26. machineconfig/cluster/sessions_managers/zellij_utils/example_usage.py +19 -17
  27. machineconfig/cluster/sessions_managers/zellij_utils/layout_generator.py +30 -31
  28. machineconfig/cluster/sessions_managers/zellij_utils/process_monitor.py +64 -134
  29. machineconfig/cluster/sessions_managers/zellij_utils/remote_executor.py +7 -11
  30. machineconfig/cluster/sessions_managers/zellij_utils/session_manager.py +27 -55
  31. machineconfig/cluster/sessions_managers/zellij_utils/status_reporter.py +14 -13
  32. machineconfig/cluster/templates/cli_click.py +0 -1
  33. machineconfig/cluster/templates/cli_gooey.py +0 -2
  34. machineconfig/cluster/templates/cli_trogon.py +0 -1
  35. machineconfig/cluster/templates/run_cloud.py +0 -1
  36. machineconfig/cluster/templates/run_cluster.py +0 -1
  37. machineconfig/cluster/templates/run_remote.py +0 -1
  38. machineconfig/cluster/templates/utils.py +27 -11
  39. machineconfig/jobs/__pycache__/__init__.cpython-313.pyc +0 -0
  40. machineconfig/jobs/linux/msc/cli_agents.sh +16 -0
  41. machineconfig/jobs/python/check_installations.py +9 -9
  42. machineconfig/jobs/python/create_bootable_media.py +0 -2
  43. machineconfig/jobs/python/python_cargo_build_share.py +2 -2
  44. machineconfig/jobs/python/python_ve_symlink.py +9 -11
  45. machineconfig/jobs/python/tasks.py +0 -1
  46. machineconfig/jobs/python/vscode/api.py +5 -5
  47. machineconfig/jobs/python/vscode/link_ve.py +20 -21
  48. machineconfig/jobs/python/vscode/select_interpreter.py +28 -29
  49. machineconfig/jobs/python/vscode/sync_code.py +14 -18
  50. machineconfig/jobs/python_custom_installers/__pycache__/__init__.cpython-313.pyc +0 -0
  51. machineconfig/jobs/python_custom_installers/archive/ngrok.py +15 -15
  52. machineconfig/jobs/python_custom_installers/dev/aider.py +10 -18
  53. machineconfig/jobs/python_custom_installers/dev/alacritty.py +12 -21
  54. machineconfig/jobs/python_custom_installers/dev/brave.py +13 -22
  55. machineconfig/jobs/python_custom_installers/dev/bypass_paywall.py +13 -20
  56. machineconfig/jobs/python_custom_installers/dev/code.py +17 -24
  57. machineconfig/jobs/python_custom_installers/dev/cursor.py +10 -21
  58. machineconfig/jobs/python_custom_installers/dev/docker_desktop.py +12 -11
  59. machineconfig/jobs/python_custom_installers/dev/espanso.py +19 -23
  60. machineconfig/jobs/python_custom_installers/dev/goes.py +9 -16
  61. machineconfig/jobs/python_custom_installers/dev/lvim.py +13 -21
  62. machineconfig/jobs/python_custom_installers/dev/nerdfont.py +15 -22
  63. machineconfig/jobs/python_custom_installers/dev/redis.py +15 -23
  64. machineconfig/jobs/python_custom_installers/dev/wezterm.py +15 -22
  65. machineconfig/jobs/python_custom_installers/dev/winget.py +32 -50
  66. machineconfig/jobs/python_custom_installers/docker.py +15 -24
  67. machineconfig/jobs/python_custom_installers/gh.py +18 -26
  68. machineconfig/jobs/python_custom_installers/hx.py +33 -17
  69. machineconfig/jobs/python_custom_installers/warp-cli.py +15 -23
  70. machineconfig/jobs/python_generic_installers/__pycache__/__init__.cpython-313.pyc +0 -0
  71. machineconfig/jobs/python_generic_installers/config.json +412 -389
  72. machineconfig/jobs/python_linux_installers/__pycache__/__init__.cpython-313.pyc +0 -0
  73. machineconfig/jobs/python_windows_installers/dev/config.json +1 -1
  74. machineconfig/jobs/windows/archive/archive_pygraphviz.ps1 +1 -1
  75. machineconfig/jobs/windows/msc/cli_agents.bat +0 -0
  76. machineconfig/jobs/windows/msc/cli_agents.ps1 +0 -0
  77. machineconfig/jobs/windows/start_terminal.ps1 +1 -1
  78. machineconfig/logger.py +50 -0
  79. machineconfig/profile/create.py +50 -36
  80. machineconfig/profile/create_hardlinks.py +33 -26
  81. machineconfig/profile/shell.py +87 -60
  82. machineconfig/scripts/__pycache__/__init__.cpython-313.pyc +0 -0
  83. machineconfig/scripts/cloud/init.sh +2 -2
  84. machineconfig/scripts/linux/checkout_versions +1 -1
  85. machineconfig/scripts/linux/choose_wezterm_theme +1 -1
  86. machineconfig/scripts/linux/cloud_copy +1 -1
  87. machineconfig/scripts/linux/cloud_manager +1 -1
  88. machineconfig/scripts/linux/cloud_mount +1 -1
  89. machineconfig/scripts/linux/cloud_repo_sync +1 -1
  90. machineconfig/scripts/linux/cloud_sync +1 -1
  91. machineconfig/scripts/linux/croshell +1 -1
  92. machineconfig/scripts/linux/devops +3 -5
  93. machineconfig/scripts/linux/fire +2 -1
  94. machineconfig/scripts/linux/fire_agents +3 -3
  95. machineconfig/scripts/linux/ftpx +1 -1
  96. machineconfig/scripts/linux/gh_models +1 -1
  97. machineconfig/scripts/linux/kill_process +1 -1
  98. machineconfig/scripts/linux/mcinit +2 -2
  99. machineconfig/scripts/linux/repos +1 -1
  100. machineconfig/scripts/linux/scheduler +1 -1
  101. machineconfig/scripts/linux/start_slidev +1 -1
  102. machineconfig/scripts/linux/start_terminals +1 -1
  103. machineconfig/scripts/linux/url2md +1 -1
  104. machineconfig/scripts/linux/warp-cli.sh +122 -0
  105. machineconfig/scripts/linux/wifi_conn +1 -1
  106. machineconfig/scripts/python/__pycache__/__init__.cpython-313.pyc +0 -0
  107. machineconfig/scripts/python/__pycache__/croshell.cpython-313.pyc +0 -0
  108. machineconfig/scripts/python/__pycache__/devops.cpython-313.pyc +0 -0
  109. machineconfig/scripts/python/__pycache__/devops_devapps_install.cpython-313.pyc +0 -0
  110. machineconfig/scripts/python/__pycache__/devops_update_repos.cpython-313.pyc +0 -0
  111. machineconfig/scripts/python/__pycache__/fire_jobs.cpython-313.pyc +0 -0
  112. machineconfig/scripts/python/ai/__init__.py +0 -0
  113. machineconfig/scripts/python/ai/__pycache__/__init__.cpython-313.pyc +0 -0
  114. machineconfig/scripts/python/ai/__pycache__/generate_files.cpython-313.pyc +0 -0
  115. machineconfig/scripts/python/ai/__pycache__/mcinit.cpython-313.pyc +0 -0
  116. machineconfig/scripts/python/ai/chatmodes/Thinking-Beast-Mode.chatmode.md +337 -0
  117. machineconfig/scripts/python/ai/chatmodes/Ultimate-Transparent-Thinking-Beast-Mode.chatmode.md +644 -0
  118. machineconfig/scripts/python/ai/chatmodes/deepResearch.chatmode.md +81 -0
  119. machineconfig/scripts/python/ai/configs/.gemini/settings.json +81 -0
  120. machineconfig/scripts/python/ai/generate_files.py +84 -0
  121. machineconfig/scripts/python/ai/instructions/python/dev.instructions.md +45 -0
  122. machineconfig/scripts/python/ai/mcinit.py +107 -0
  123. machineconfig/scripts/python/ai/prompts/allLintersAndTypeCheckers.prompt.md +5 -0
  124. machineconfig/scripts/python/ai/prompts/research-report-skeleton.prompt.md +38 -0
  125. machineconfig/scripts/python/ai/scripts/lint_and_type_check.sh +52 -0
  126. machineconfig/scripts/python/archive/tmate_conn.py +5 -5
  127. machineconfig/scripts/python/archive/tmate_start.py +3 -3
  128. machineconfig/scripts/python/choose_wezterm_theme.py +2 -2
  129. machineconfig/scripts/python/cloud_copy.py +20 -19
  130. machineconfig/scripts/python/cloud_mount.py +10 -8
  131. machineconfig/scripts/python/cloud_repo_sync.py +15 -15
  132. machineconfig/scripts/python/cloud_sync.py +1 -1
  133. machineconfig/scripts/python/croshell.py +18 -16
  134. machineconfig/scripts/python/devops.py +6 -6
  135. machineconfig/scripts/python/devops_add_identity.py +9 -7
  136. machineconfig/scripts/python/devops_add_ssh_key.py +19 -19
  137. machineconfig/scripts/python/devops_backup_retrieve.py +14 -14
  138. machineconfig/scripts/python/devops_devapps_install.py +3 -3
  139. machineconfig/scripts/python/devops_update_repos.py +141 -53
  140. machineconfig/scripts/python/dotfile.py +3 -3
  141. machineconfig/scripts/python/fire_agents.py +202 -41
  142. machineconfig/scripts/python/fire_jobs.py +20 -21
  143. machineconfig/scripts/python/ftpx.py +4 -3
  144. machineconfig/scripts/python/gh_models.py +94 -94
  145. machineconfig/scripts/python/helpers/__pycache__/__init__.cpython-313.pyc +0 -0
  146. machineconfig/scripts/python/helpers/__pycache__/helpers4.cpython-313.pyc +0 -0
  147. machineconfig/scripts/python/helpers/cloud_helpers.py +3 -3
  148. machineconfig/scripts/python/helpers/helpers2.py +3 -3
  149. machineconfig/scripts/python/helpers/helpers4.py +8 -7
  150. machineconfig/scripts/python/helpers/helpers5.py +7 -7
  151. machineconfig/scripts/python/helpers/repo_sync_helpers.py +2 -2
  152. machineconfig/scripts/python/mount_nfs.py +4 -3
  153. machineconfig/scripts/python/mount_nw_drive.py +4 -4
  154. machineconfig/scripts/python/mount_ssh.py +4 -3
  155. machineconfig/scripts/python/repos.py +9 -9
  156. machineconfig/scripts/python/scheduler.py +1 -1
  157. machineconfig/scripts/python/start_slidev.py +9 -8
  158. machineconfig/scripts/python/start_terminals.py +1 -1
  159. machineconfig/scripts/python/viewer.py +40 -40
  160. machineconfig/scripts/python/wifi_conn.py +65 -66
  161. machineconfig/scripts/python/wsl_windows_transfer.py +2 -2
  162. machineconfig/scripts/windows/checkout_version.ps1 +1 -3
  163. machineconfig/scripts/windows/choose_wezterm_theme.ps1 +1 -3
  164. machineconfig/scripts/windows/cloud_copy.ps1 +2 -6
  165. machineconfig/scripts/windows/cloud_manager.ps1 +1 -1
  166. machineconfig/scripts/windows/cloud_repo_sync.ps1 +1 -2
  167. machineconfig/scripts/windows/cloud_sync.ps1 +2 -2
  168. machineconfig/scripts/windows/croshell.ps1 +2 -2
  169. machineconfig/scripts/windows/devops.ps1 +1 -4
  170. machineconfig/scripts/windows/dotfile.ps1 +1 -3
  171. machineconfig/scripts/windows/fire.ps1 +1 -1
  172. machineconfig/scripts/windows/ftpx.ps1 +2 -2
  173. machineconfig/scripts/windows/gpt.ps1 +1 -1
  174. machineconfig/scripts/windows/kill_process.ps1 +1 -2
  175. machineconfig/scripts/windows/mcinit.ps1 +2 -2
  176. machineconfig/scripts/windows/mount_nfs.ps1 +1 -1
  177. machineconfig/scripts/windows/mount_ssh.ps1 +1 -1
  178. machineconfig/scripts/windows/pomodoro.ps1 +1 -1
  179. machineconfig/scripts/windows/py2exe.ps1 +1 -3
  180. machineconfig/scripts/windows/repos.ps1 +1 -1
  181. machineconfig/scripts/windows/scheduler.ps1 +1 -1
  182. machineconfig/scripts/windows/snapshot.ps1 +2 -2
  183. machineconfig/scripts/windows/start_slidev.ps1 +1 -1
  184. machineconfig/scripts/windows/start_terminals.ps1 +1 -1
  185. machineconfig/scripts/windows/wifi_conn.ps1 +1 -1
  186. machineconfig/scripts/windows/wsl_windows_transfer.ps1 +1 -3
  187. machineconfig/settings/lf/linux/lfrc +1 -1
  188. machineconfig/settings/linters/.ruff.toml +2 -2
  189. machineconfig/settings/linters/.ruff_cache/.gitignore +2 -0
  190. machineconfig/settings/linters/.ruff_cache/CACHEDIR.TAG +1 -0
  191. machineconfig/settings/lvim/windows/archive/config_additional.lua +1 -1
  192. machineconfig/settings/shells/ipy/profiles/default/startup/playext.py +71 -71
  193. machineconfig/settings/shells/wt/settings.json +8 -8
  194. machineconfig/settings/svim/linux/init.toml +1 -1
  195. machineconfig/settings/svim/windows/init.toml +1 -1
  196. machineconfig/setup_linux/web_shortcuts/croshell.sh +0 -54
  197. machineconfig/setup_linux/web_shortcuts/interactive.sh +6 -6
  198. machineconfig/setup_linux/web_shortcuts/tmp.sh +2 -0
  199. machineconfig/setup_windows/web_shortcuts/all.ps1 +2 -2
  200. machineconfig/setup_windows/web_shortcuts/ascii_art.ps1 +1 -1
  201. machineconfig/setup_windows/web_shortcuts/croshell.ps1 +1 -1
  202. machineconfig/setup_windows/web_shortcuts/interactive.ps1 +5 -5
  203. machineconfig/setup_windows/wt_and_pwsh/install_fonts.ps1 +51 -15
  204. machineconfig/setup_windows/wt_and_pwsh/set_pwsh_theme.py +75 -18
  205. machineconfig/setup_windows/wt_and_pwsh/set_wt_settings.py +52 -42
  206. machineconfig/utils/ai/browser_user_wrapper.py +5 -5
  207. machineconfig/utils/ai/generate_file_checklist.py +19 -22
  208. machineconfig/utils/ai/url2md.py +5 -3
  209. machineconfig/utils/cloud/onedrive/setup_oauth.py +5 -4
  210. machineconfig/utils/cloud/onedrive/transaction.py +192 -227
  211. machineconfig/utils/code.py +71 -43
  212. machineconfig/utils/installer.py +77 -85
  213. machineconfig/utils/installer_utils/installer_abc.py +29 -17
  214. machineconfig/utils/installer_utils/installer_class.py +188 -83
  215. machineconfig/utils/io_save.py +3 -15
  216. machineconfig/utils/links.py +22 -11
  217. machineconfig/utils/notifications.py +197 -0
  218. machineconfig/utils/options.py +38 -25
  219. machineconfig/utils/path.py +18 -6
  220. machineconfig/utils/path_reduced.py +637 -316
  221. machineconfig/utils/procs.py +69 -63
  222. machineconfig/utils/scheduling.py +11 -13
  223. machineconfig/utils/ssh.py +351 -0
  224. machineconfig/utils/terminal.py +225 -0
  225. machineconfig/utils/utils.py +13 -12
  226. machineconfig/utils/utils2.py +43 -10
  227. machineconfig/utils/utils5.py +242 -46
  228. machineconfig/utils/ve.py +11 -6
  229. {machineconfig-1.97.dist-info → machineconfig-2.1.dist-info}/METADATA +15 -9
  230. {machineconfig-1.97.dist-info → machineconfig-2.1.dist-info}/RECORD +232 -235
  231. machineconfig/cluster/self_ssh.py +0 -57
  232. machineconfig/jobs/__pycache__/__init__.cpython-311.pyc +0 -0
  233. machineconfig/jobs/python/__pycache__/__init__.cpython-311.pyc +0 -0
  234. machineconfig/jobs/python/archive/python_tools.txt +0 -12
  235. machineconfig/jobs/python/vscode/__pycache__/select_interpreter.cpython-311.pyc +0 -0
  236. machineconfig/jobs/python_custom_installers/__pycache__/__init__.cpython-311.pyc +0 -0
  237. machineconfig/jobs/python_generic_installers/__pycache__/__init__.cpython-311.pyc +0 -0
  238. machineconfig/jobs/python_generic_installers/update.py +0 -3
  239. machineconfig/jobs/python_linux_installers/__pycache__/__init__.cpython-311.pyc +0 -0
  240. machineconfig/profile/__pycache__/__init__.cpython-311.pyc +0 -0
  241. machineconfig/profile/__pycache__/create.cpython-311.pyc +0 -0
  242. machineconfig/profile/__pycache__/shell.cpython-311.pyc +0 -0
  243. machineconfig/scripts/__pycache__/__init__.cpython-311.pyc +0 -0
  244. machineconfig/scripts/linux/activate_ve +0 -87
  245. machineconfig/scripts/python/__pycache__/__init__.cpython-311.pyc +0 -0
  246. machineconfig/scripts/python/__pycache__/cloud_copy.cpython-311.pyc +0 -0
  247. machineconfig/scripts/python/__pycache__/cloud_mount.cpython-311.pyc +0 -0
  248. machineconfig/scripts/python/__pycache__/cloud_sync.cpython-311.pyc +0 -0
  249. machineconfig/scripts/python/__pycache__/croshell.cpython-311.pyc +0 -0
  250. machineconfig/scripts/python/__pycache__/devops.cpython-311.pyc +0 -0
  251. machineconfig/scripts/python/__pycache__/devops_backup_retrieve.cpython-311.pyc +0 -0
  252. machineconfig/scripts/python/__pycache__/devops_devapps_install.cpython-311.pyc +0 -0
  253. machineconfig/scripts/python/__pycache__/devops_update_repos.cpython-311.pyc +0 -0
  254. machineconfig/scripts/python/__pycache__/fire_agents.cpython-311.pyc +0 -0
  255. machineconfig/scripts/python/__pycache__/fire_jobs.cpython-311.pyc +0 -0
  256. machineconfig/scripts/python/__pycache__/get_zellij_cmd.cpython-311.pyc +0 -0
  257. machineconfig/scripts/python/__pycache__/repos.cpython-311.pyc +0 -0
  258. machineconfig/scripts/python/ai/__pycache__/init.cpython-311.pyc +0 -0
  259. machineconfig/scripts/python/ai/init.py +0 -56
  260. machineconfig/scripts/python/ai/rules/python/dev.md +0 -31
  261. machineconfig/scripts/python/helpers/__pycache__/__init__.cpython-311.pyc +0 -0
  262. machineconfig/scripts/python/helpers/__pycache__/cloud_helpers.cpython-311.pyc +0 -0
  263. machineconfig/scripts/python/helpers/__pycache__/helpers2.cpython-311.pyc +0 -0
  264. machineconfig/scripts/python/helpers/__pycache__/helpers4.cpython-311.pyc +0 -0
  265. machineconfig/scripts/python/helpers/__pycache__/repo_sync_helpers.cpython-311.pyc +0 -0
  266. machineconfig/scripts/windows/activate_ve.ps1 +0 -54
  267. {machineconfig-1.97.dist-info → machineconfig-2.1.dist-info}/WHEEL +0 -0
  268. {machineconfig-1.97.dist-info → machineconfig-2.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,351 @@
1
+ from typing import Optional, Any, Union, List
2
+ import os
3
+ from dataclasses import dataclass
4
+ import rich.console
5
+ from machineconfig.utils.terminal import Terminal, Response, MACHINE
6
+ from machineconfig.utils.path_reduced import PathExtended, PLike, OPLike
7
+ from machineconfig.utils.utils2 import pprint
8
+
9
+
10
+ @dataclass
11
+ class Scout:
12
+ source_full: PathExtended
13
+ source_rel2home: PathExtended
14
+ exists: bool
15
+ is_dir: bool
16
+ files: Optional[List[PathExtended]]
17
+
18
+
19
+ def scout(source: PLike, z: bool = False, r: bool = False) -> Scout:
20
+ source_full = PathExtended(source).expanduser().absolute()
21
+ source_rel2home = source_full.collapseuser()
22
+ exists = source_full.exists()
23
+ is_dir = source_full.is_dir() if exists else False
24
+ if z and exists:
25
+ try:
26
+ source_full = source_full.zip()
27
+ except Exception as ex:
28
+ raise Exception(f"Could not zip {source_full} due to {ex}") from ex # type: ignore # pylint: disable=W0719
29
+ source_rel2home = source_full.zip()
30
+ if r and exists and is_dir:
31
+ files = [item.collapseuser() for item in source_full.search(folders=False, r=True)]
32
+ else:
33
+ files = None
34
+ return Scout(source_full=source_full, source_rel2home=source_rel2home, exists=exists, is_dir=is_dir, files=files)
35
+
36
+
37
+ class SSH: # inferior alternative: https://github.com/fabric/fabric
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
41
+ self.pwd = pwd
42
+ self.ve = ve
43
+ self.compress = compress # Defaults: (1) use localhost if nothing provided.
44
+
45
+ self.host: Optional[str] = None
46
+ self.hostname: str
47
+ self.username: str
48
+ self.port: int = port
49
+ self.proxycommand: Optional[str] = None
50
+ import platform
51
+ import paramiko # type: ignore
52
+ import getpass
53
+
54
+ if isinstance(host, str):
55
+ try:
56
+ import paramiko.config as pconfig
57
+
58
+ config = pconfig.SSHConfig.from_path(str(PathExtended.home().joinpath(".ssh/config")))
59
+ config_dict = config.lookup(host)
60
+ self.hostname = config_dict["hostname"]
61
+ self.username = config_dict["user"]
62
+ self.host = host
63
+ self.port = int(config_dict.get("port", port))
64
+ tmp = config_dict.get("identityfile", sshkey)
65
+ if isinstance(tmp, list):
66
+ sshkey = tmp[0]
67
+ else:
68
+ sshkey = tmp
69
+ self.proxycommand = config_dict.get("proxycommand", None)
70
+ if sshkey is not None:
71
+ tmp = config.lookup("*").get("identityfile", sshkey)
72
+ if isinstance(tmp, list):
73
+ sshkey = tmp[0]
74
+ else:
75
+ sshkey = tmp
76
+ except (FileNotFoundError, KeyError):
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}"
78
+ if "@" in host:
79
+ self.username, self.hostname = host.split("@")
80
+ else:
81
+ self.username = username or getpass.getuser()
82
+ self.hostname = host
83
+ if ":" in self.hostname:
84
+ self.hostname, port_ = self.hostname.split(":")
85
+ self.port = int(port_)
86
+ elif username is not None and hostname is not None:
87
+ self.username, self.hostname = username, hostname
88
+ self.proxycommand = None
89
+ else:
90
+ print(f"Provided values: host={host}, username={username}, hostname={hostname}")
91
+ raise ValueError("Either host or username and hostname must be provided.")
92
+
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
94
+ self.ssh = paramiko.SSHClient()
95
+ self.ssh.load_system_host_keys()
96
+ self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
97
+ pprint(dict(host=self.host, hostname=self.hostname, username=self.username, password="***", port=self.port, key_filename=self.sshkey, ve=self.ve), title="SSHing To")
98
+
99
+ sock = paramiko.ProxyCommand(self.proxycommand) if self.proxycommand is not None else None
100
+ try:
101
+ if pwd is None:
102
+ allow_agent = True
103
+ look_for_keys = True
104
+ else:
105
+ allow_agent = False
106
+ look_for_keys = False
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
108
+ except Exception as _err:
109
+ rich.console.Console().print_exception()
110
+ self.pwd = getpass.getpass(f"Enter password for {self.username}@{self.hostname}: ")
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
112
+
113
+ try:
114
+ self.sftp: Optional[paramiko.SFTPClient] = self.ssh.open_sftp()
115
+ except Exception as err:
116
+ self.sftp = None
117
+ print(f"""āš ļø WARNING: Failed to open SFTP connection to {hostname}.
118
+ Error Details: {err}\nData transfer may be affected!""")
119
+
120
+ def view_bar(slf: Any, a: Any, b: Any):
121
+ slf.total = int(b)
122
+ slf.update(int(a - slf.n)) # update pbar with increment
123
+
124
+ from tqdm import tqdm
125
+
126
+ self.tqdm_wrap = type("TqdmWrap", (tqdm,), {"view_bar": view_bar})
127
+ self._local_distro: Optional[str] = None
128
+ self._remote_distro: Optional[str] = None
129
+ self._remote_machine: Optional[MACHINE] = None
130
+ self.terminal_responses: list[Response] = []
131
+ self.platform = platform
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
+
141
+ def get_remote_machine(self) -> MACHINE:
142
+ if self._remote_machine is None:
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"
147
+ return self._remote_machine # echo %OS% TODO: uname on linux
148
+
149
+ def get_local_distro(self) -> str:
150
+ if self._local_distro is None:
151
+ command = """uv run --with distro python -c "import distro; print(distro.name(pretty=True))" """
152
+ import subprocess
153
+
154
+ res = subprocess.run(command, shell=True, capture_output=True, text=True).stdout.strip()
155
+ self._local_distro = res
156
+ return res
157
+ return self._local_distro
158
+
159
+ def get_remote_distro(self):
160
+ if self._remote_distro is None:
161
+ self._remote_distro = self.run_py("print(install_n_import('distro').name(pretty=True))", verbose=False).op_if_successfull_or_default() or ""
162
+ # q.run("""~/.local/bin/uv run --with distro python -c "import distro; print(distro.name(pretty=True))" """)
163
+ return self._remote_distro
164
+
165
+ def restart_computer(self):
166
+ self.run("Restart-Computer -Force" if self.get_remote_machine() == "Windows" else "sudo reboot")
167
+
168
+ def send_ssh_key(self):
169
+ self.copy_from_here("~/.ssh/id_rsa.pub")
170
+ assert self.get_remote_machine() == "Windows"
171
+ code_url = "https://raw.githubusercontent.com/thisismygitrepo/machineconfig/refs/heads/main/src/machineconfig/setup_windows/openssh-server_add-sshkey.ps1"
172
+ code = PathExtended(code_url).download().read_text(encoding="utf-8")
173
+ self.run(code)
174
+
175
+ def copy_env_var(self, name: str):
176
+ assert self.get_remote_machine() == "Linux"
177
+ return self.run(f"{name} = {os.environ[name]}; export {name}")
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
+
182
+ def get_local_repr(self, add_machine: bool = False) -> str:
183
+ import getpass
184
+
185
+ return f"{getpass.getuser()}@{self.platform.node()}" + (f" [{self.platform.system()}][{self.get_local_distro()}]" if add_machine else "")
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
+
190
+ def run_locally(self, command: str):
191
+ print(f"""šŸ’» [LOCAL EXECUTION] Running command on node: {self.platform.node()} Command: {command}""")
192
+ res = Response(cmd=command)
193
+ res.output.returncode = os.system(command)
194
+ return res
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
+
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)
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.
201
+ cmd = (self.remote_env_cmd + "; " + cmd) if env_prefix else cmd
202
+ raw = self.ssh.exec_command(cmd)
203
+ res = Response(stdin=raw[0], stdout=raw[1], stderr=raw[2], cmd=cmd, desc=desc) # type: ignore
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()
208
+ self.terminal_responses.append(res)
209
+ return res
210
+
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]:
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.'
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
+ )
217
+ assert "obj=" in cmd, "The command sent to run_py must have `obj=` statement if return_obj is set to True"
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"))
220
+ import pickle
221
+
222
+ res_bytes = res.read_bytes()
223
+ return pickle.loads(res_bytes)
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!")
231
+ if target is None:
232
+ target = PathExtended(source_obj).expanduser().absolute().collapseuser(strict=True)
233
+ assert target.is_relative_to("~"), "If target is not specified, source must be relative to home."
234
+ if z:
235
+ target += ".zip"
236
+ if not z and source_obj.is_dir():
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}")
248
+ for item in source_list:
249
+ a__target = PathExtended(remote_root).joinpath(item.relative_to(source_obj))
250
+ self.copy_from_here(source=item, target=a__target)
251
+ return list(source_list)
252
+ if z:
253
+ print("šŸ—œļø ZIPPING ...")
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
267
+ if z:
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)
269
+ source_obj.delete(sure=True)
270
+ print("\n")
271
+ return source_obj
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.")
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)
280
+ assert isinstance(source_list, List), f"Could not resolve source path {source} due to error"
281
+ for file in source_list:
282
+ self.copy_to_here(source=file.as_posix(), target=PathExtended(target).joinpath(PathExtended(file).relative_to(source)) if target else None, r=False)
283
+ if z:
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)
285
+ tmp2 = tmp.op2path(strict_returncode=True, strict_err=True)
286
+ if not isinstance(tmp2, PathExtended):
287
+ raise RuntimeError(f"Could not zip {source} due to {tmp.err}")
288
+ else:
289
+ source = tmp2
290
+ if target is None:
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")
296
+ assert target.is_relative_to("~"), f"If target is not specified, source must be relative to home.\n{target=}"
297
+ target_obj = PathExtended(target).expanduser().absolute()
298
+ target_obj.parent.mkdir(parents=True, exist_ok=True)
299
+ if z and ".zip" not in target_obj.suffix:
300
+ target_obj += ".zip"
301
+ if "~" in str(source):
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()
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)
309
+ print(f"""šŸ“„ [DOWNLOAD] Receiving: {source} ==> Local Path: {target_obj}""")
310
+ with self.tqdm_wrap(ascii=True, unit="b", unit_scale=True) as pbar: # type: ignore # pylint: disable=E1129
311
+ assert self.sftp is not None, f"Could not establish SFTP connection to {self.hostname}."
312
+ self.sftp.get(remotepath=source.as_posix(), localpath=str(target_obj), callback=pbar.view_bar) # type: ignore
313
+ if z:
314
+ target_obj = target_obj.unzip(inplace=True, content=True)
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)
316
+ print("\n")
317
+ return target_obj
318
+
319
+ def receieve(self, source: PLike, target: OPLike = None, z: bool = False, r: bool = False) -> PathExtended:
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)
321
+ assert isinstance(scout, Scout)
322
+ if not z and scout.is_dir and scout.files is not None:
323
+ if r:
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]
325
+ return tmp[0]
326
+ else:
327
+ print("Source is a directory! either set `r=True` for recursive sending or raise `zip_first=True` flag.")
328
+ if target:
329
+ target = PathExtended(target).expanduser().absolute()
330
+ else:
331
+ target = scout.source_rel2home.expanduser().absolute()
332
+ target.parent.mkdir(parents=True, exist_ok=True)
333
+ if z and ".zip" not in target.suffix:
334
+ target += ".zip"
335
+ source = scout.source_full
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
338
+ if z:
339
+ target = target.unzip(inplace=True, content=True)
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)
341
+ print("\n")
342
+ return target
343
+
344
+ # def print_summary(self):
345
+ # import polars as pl
346
+ # df = pl.DataFrame(List(self.terminal_responses).apply(lambda rsp: dict(desc=rsp.desc, err=rsp.err, returncode=rsp.returncode)).list)
347
+ # print("\nSummary of operations performed:")
348
+ # print(df.to_pandas().to_markdown())
349
+ # if ((df.select('returncode').to_series().to_list()[2:] == [None] * (len(df) - 2)) and (df.select('err').to_series().to_list()[2:] == [''] * (len(df) - 2))): print("\nAll operations completed successfully.\n")
350
+ # else: print("\nSome operations failed. \n")
351
+ # return df
@@ -0,0 +1,225 @@
1
+ from machineconfig.utils.path_reduced import PathExtended, OPLike
2
+ import subprocess
3
+ from typing import Any, BinaryIO, Optional, Union
4
+ import platform
5
+ import sys
6
+ import os
7
+ from typing import Literal, TypeAlias
8
+ from dataclasses import dataclass
9
+
10
+ SHELLS: TypeAlias = Literal["default", "cmd", "powershell", "pwsh", "bash"] # pwsh.exe is PowerShell (community) and powershell.exe is Windows Powershell (msft)
11
+ CONSOLE: TypeAlias = Literal["wt", "cmd"]
12
+ MACHINE: TypeAlias = Literal["Windows", "Linux", "Darwin"]
13
+
14
+
15
+ @dataclass
16
+ class STD:
17
+ stdin: str
18
+ stdout: str
19
+ stderr: str
20
+ returncode: int
21
+
22
+
23
+ class Response:
24
+ @staticmethod
25
+ def from_completed_process(cp: subprocess.CompletedProcess[str]):
26
+ resp = Response(cmd=cp.args)
27
+ resp.output.stdout = cp.stdout
28
+ resp.output.stderr = cp.stderr
29
+ resp.output.returncode = cp.returncode
30
+ return resp
31
+
32
+ def __init__(self, stdin: Optional[BinaryIO] = None, stdout: Optional[BinaryIO] = None, stderr: Optional[BinaryIO] = None, cmd: Optional[str] = None, desc: str = ""):
33
+ self.std = dict(stdin=stdin, stdout=stdout, stderr=stderr)
34
+ self.output = STD(stdin="", stdout="", stderr="", returncode=0)
35
+ self.input = cmd
36
+ self.desc = desc # input command
37
+
38
+ def __call__(self, *args: Any, **kwargs: Any) -> Optional[str]:
39
+ _ = args, kwargs
40
+ return self.op.rstrip() if type(self.op) is str else None
41
+
42
+ @property
43
+ def op(self) -> str:
44
+ return self.output.stdout
45
+
46
+ @property
47
+ def ip(self) -> str:
48
+ return self.output.stdin
49
+
50
+ @property
51
+ def err(self) -> str:
52
+ return self.output.stderr
53
+
54
+ @property
55
+ def returncode(self) -> int:
56
+ return self.output.returncode
57
+
58
+ def op2path(self, strict_returncode: bool = True, strict_err: bool = False) -> Union[PathExtended, None]:
59
+ if self.is_successful(strict_returcode=strict_returncode, strict_err=strict_err):
60
+ return PathExtended(self.op.rstrip())
61
+ return None
62
+
63
+ def op_if_successfull_or_default(self, strict_returcode: bool = True, strict_err: bool = False) -> Optional[str]:
64
+ return self.op if self.is_successful(strict_returcode=strict_returcode, strict_err=strict_err) else None
65
+
66
+ def is_successful(self, strict_returcode: bool = True, strict_err: bool = False) -> bool:
67
+ return ((self.returncode in {0, None}) if strict_returcode else True) and (self.err == "" if strict_err else True)
68
+
69
+ def capture(self):
70
+ for key in ["stdin", "stdout", "stderr"]:
71
+ val: Optional[BinaryIO] = self.std[key]
72
+ if val is not None and val.readable():
73
+ self.output.__dict__[key] = val.read().decode().rstrip()
74
+ return self
75
+
76
+ def print_if_unsuccessful(self, desc: str = "TERMINAL CMD", strict_err: bool = False, strict_returncode: bool = False, assert_success: bool = False):
77
+ success = self.is_successful(strict_err=strict_err, strict_returcode=strict_returncode)
78
+ if assert_success:
79
+ assert success, self.print(capture=False, desc=desc)
80
+ if success:
81
+ print(f"āœ… {desc} completed successfully")
82
+ else:
83
+ self.print(capture=False, desc=desc)
84
+ return self
85
+
86
+ def print(self, desc: str = "TERMINAL CMD", capture: bool = True):
87
+ if capture:
88
+ self.capture()
89
+ from rich import console
90
+
91
+ con = console.Console()
92
+ from rich.panel import Panel
93
+ from rich.text import Text # from rich.syntax import Syntax; syntax = Syntax(my_code, "python", theme="monokai", line_numbers=True)
94
+
95
+ tmp1 = Text("šŸ“„ Input Command:\n")
96
+ tmp1.stylize("u bold blue")
97
+ tmp2 = Text("\nšŸ“¤ Terminal Response:\n")
98
+ tmp2.stylize("u bold blue")
99
+ list_str = [f"{f' {idx} - {key} '}".center(40, "═") + f"\n{val}" for idx, (key, val) in enumerate(self.output.__dict__.items())]
100
+ txt = tmp1 + Text(str(self.input), style="white") + tmp2 + Text("\n".join(list_str), style="white")
101
+ con.print(Panel(txt, title=f"šŸ–„ļø {self.desc}", subtitle=f"šŸ“‹ {desc}", width=150, style="bold cyan on black"))
102
+ return self
103
+
104
+
105
+ # DEPRECATED: Use subprocess.run directly instead of Terminal class.
106
+ # The Terminal class has been replaced with inline subprocess calls to underlying primitives.
107
+ # This file is kept for reference but should not be used.
108
+
109
+
110
+ class Terminal:
111
+ def __init__(self, stdout: Optional[int] = subprocess.PIPE, stderr: Optional[int] = subprocess.PIPE, stdin: Optional[int] = subprocess.PIPE, elevated: bool = False):
112
+ self.machine: str = platform.system()
113
+ self.elevated: bool = elevated
114
+ self.stdout = stdout
115
+ self.stderr = stderr
116
+ self.stdin = stdin
117
+
118
+ # def set_std_system(self): self.stdout = sys.stdout; self.stderr = sys.stderr; self.stdin = sys.stdin
119
+ def set_std_pipe(self):
120
+ self.stdout = subprocess.PIPE
121
+ self.stderr = subprocess.PIPE
122
+ self.stdin = subprocess.PIPE
123
+
124
+ def set_std_null(self):
125
+ self.stdout, self.stderr, self.stdin = subprocess.DEVNULL, subprocess.DEVNULL, subprocess.DEVNULL # Equivalent to `echo 'foo' &> /dev/null`
126
+
127
+ def run(self, *cmds: str, shell: Optional[SHELLS] = "default", check: bool = False, ip: Optional[str] = None) -> Response: # Runs SYSTEM commands like subprocess.run
128
+ """Blocking operation. Thus, if you start a shell via this method, it will run in the main and won't stop until you exit manually IF stdin is set to sys.stdin, otherwise it will run and close quickly. Other combinations of stdin, stdout can lead to funny behaviour like no output but accept input or opposite.
129
+ * This method is short for: res = subprocess.run("powershell command", capture_output=True, shell=True, text=True) and unlike os.system(cmd), subprocess.run(cmd) gives much more control over the output and input.
130
+ * `shell=True` loads up the profile of the shell called so more specific commands can be run. Importantly, on Windows, the `start` command becomes availalbe and new windows can be launched.
131
+ * `capture_output` prevents the stdout to redirect to the stdout of the script automatically, instead it will be stored in the Response object returned. # `capture_output=True` same as `stdout=subprocess.PIPE, stderr=subprocess.PIPE`"""
132
+ my_list = list(
133
+ cmds
134
+ ) # `subprocess.Popen` (process open) is the most general command. Used here to create asynchronous job. `subprocess.run` is a thin wrapper around Popen that makes it wait until it finishes the task. `suprocess.call` is an archaic command for pre-Python-3.5.
135
+ if self.machine == "Windows" and shell in {"powershell", "pwsh"}:
136
+ my_list = [shell, "-Command"] + my_list # alternatively, one can run "cmd"
137
+ if self.elevated is False or self.is_user_admin():
138
+ resp: subprocess.CompletedProcess[str] = subprocess.run(my_list, stderr=self.stderr, stdin=self.stdin, stdout=self.stdout, text=True, shell=True, check=check, input=ip)
139
+ else:
140
+ resp = __import__("ctypes").windll.shell32.ShellExecuteW(None, "runas", sys.executable, " ".join(sys.argv), None, 1)
141
+ return Response.from_completed_process(resp)
142
+
143
+ def run_script(self, script: str, shell: SHELLS = "default", verbose: bool = False):
144
+ if self.machine == "Linux":
145
+ script = "#!/bin/bash" + "\n" + script # `source` is only available in bash.
146
+ script_file = PathExtended.tmpfile(name="tmp_shell_script", suffix=".ps1" if self.machine == "Windows" else ".sh", folder="tmp_scripts").write_text(script, newline={"Windows": None, "Linux": "\n"}[self.machine])
147
+ if shell == "default":
148
+ if self.machine == "Windows":
149
+ start_cmd = "powershell" # default shell on Windows is cmd which is not very useful. (./source is not available)
150
+ full_command: Union[list[str], str] = [start_cmd, str(script_file)] # shell=True will cause this to be a string anyway (with space separation)
151
+ else:
152
+ start_cmd = "bash"
153
+ full_command = f"{start_cmd} {script_file}" # full_command = [start_cmd, str(script_file)]
154
+ else:
155
+ full_command = f"{shell} {script_file}" # full_command = [shell, str(tmp_file)]
156
+ if verbose:
157
+ desc = "Script to be executed:"
158
+ if platform.system() == "Windows":
159
+ lexer = "powershell"
160
+ elif platform.system() == "Linux":
161
+ lexer = "sh"
162
+ elif platform.system() == "Darwin":
163
+ lexer = "sh" # macOS uses similar shell to Linux
164
+ else:
165
+ raise NotImplementedError(f"Platform {platform.system()} not supported.")
166
+ from rich.console import Console
167
+ from rich.panel import Panel
168
+ from rich.syntax import Syntax
169
+ import rich.progress as pb
170
+
171
+ console = Console()
172
+ console.print(Panel(Syntax(code=script, lexer=lexer), title=f"šŸ“„ {desc}"), style="bold red")
173
+ with pb.Progress(transient=True) as progress:
174
+ _task = progress.add_task(f"Running Script @ {script_file}", total=None)
175
+ resp = subprocess.run(full_command, stderr=self.stderr, stdin=self.stdin, stdout=self.stdout, text=True, shell=True, check=False)
176
+ else:
177
+ resp = subprocess.run(full_command, stderr=self.stderr, stdin=self.stdin, stdout=self.stdout, text=True, shell=True, check=False)
178
+ return Response.from_completed_process(resp)
179
+
180
+ def run_py(self, script: str, wdir: OPLike = None, interactive: bool = True, ipython: bool = True, shell: Optional[str] = None, terminal: str = "", new_window: bool = True, header: bool = True): # async run, since sync run is meaningless.
181
+ script = (Terminal.get_header(wdir=wdir, toolbox=True) if header else "") + script + ("\nDisplayData.set_pandas_auto_width()\n" if terminal in {"wt", "powershell", "pwsh"} else "")
182
+ py_script = PathExtended.tmpfile(name="tmp_python_script", suffix=".py", folder="tmp_scripts/terminal")
183
+ py_script.write_text(f"""print(r'''{script}''')""" + "\n" + script)
184
+ print(f"""šŸš€ [ASYNC PYTHON SCRIPT] Script URI:
185
+ {py_script.absolute().as_uri()}""")
186
+ print("Script to be executed asyncronously: ", py_script.absolute().as_uri())
187
+ shell_script = f"""
188
+ {f"cd {wdir}" if wdir is not None else ""}
189
+ {"ipython" if ipython else "python"} {"-i" if interactive else ""} {py_script}
190
+ """
191
+ shell_script = PathExtended.tmpfile(name="tmp_shell_script", suffix=".sh" if self.machine == "Linux" else ".ps1", folder="tmp_scripts/shell").write_text(shell_script)
192
+ if shell is None and self.machine == "Windows":
193
+ shell = "pwsh"
194
+ window = "start" if new_window and self.machine == "Windows" else ""
195
+ os.system(f"{window} {terminal} {shell} {shell_script}")
196
+
197
+ @staticmethod
198
+ def is_user_admin() -> bool: # adopted from: https://stackoverflow.com/questions/19672352/how-to-run-script-with-elevated-privilege-on-windows"""
199
+ if os.name == "nt":
200
+ try:
201
+ return __import__("ctypes").windll.shell32.IsUserAnAdmin()
202
+ except Exception:
203
+ import traceback
204
+
205
+ traceback.print_exc()
206
+ print("Admin check failed, assuming not an admin.")
207
+ return False
208
+ else:
209
+ return os.getuid() == 0 # Check for root on Posix
210
+
211
+ # @staticmethod
212
+ # def run_as_admin(file: PLike, params: Any, wait: bool = False):
213
+ # proce_info = install_n_import(library="win32com", package="pywin32", fromlist=["shell.shell.ShellExecuteEx"]).shell.shell.ShellExecuteEx(lpVerb='runas', lpFile=file, lpParameters=params)
214
+ # # TODO update PATH for this to take effect immediately.
215
+ # if wait: time.sleep(1)
216
+ # return proce_info
217
+
218
+ @staticmethod
219
+ def get_header(wdir: OPLike, toolbox: bool):
220
+ return f"""
221
+ # >> Code prepended
222
+ {"from crocodile.toolbox import *" if toolbox else "# No toolbox import."}
223
+ {'''sys.path.insert(0, r'{wdir}') ''' if wdir is not None else "# No path insertion."}
224
+ # >> End of header, start of script passed
225
+ """
@@ -2,18 +2,17 @@
2
2
  Utils
3
3
  """
4
4
 
5
- from machineconfig.utils.path_reduced import P as PathExtended
6
- # import crocodile.environment as env
5
+ from machineconfig.utils.path_reduced import PathExtended as PathExtended
7
6
  import machineconfig
8
7
  from machineconfig.utils.options import check_tool_exists, choose_cloud_interactively, choose_multiple_options, choose_one_option, choose_ssh_host, display_options
9
8
  from rich.console import Console
10
9
  from rich.panel import Panel
11
10
  from machineconfig.utils.links import build_links, symlink_copy, symlink_func
12
- from machineconfig.utils.code import get_shell_script_executing_python_file, get_shell_file_executing_python_script, write_shell_script_to_default_program_path, print_code, PROGRAM_PATH
11
+ from machineconfig.utils.code import write_shell_script_to_default_program_path, print_code, PROGRAM_PATH
13
12
  from machineconfig.utils.path import sanitize_path, match_file_name
14
13
 
15
14
  # Split into multiple assignments to fix incompatible tuple sizes
16
- _ = get_shell_script_executing_python_file, get_shell_file_executing_python_script, print_code, PROGRAM_PATH, display_options, write_shell_script_to_default_program_path
15
+ _ = print_code, PROGRAM_PATH, display_options, write_shell_script_to_default_program_path
17
16
  _ = build_links
18
17
  _ = symlink_copy
19
18
  _ = symlink_func
@@ -48,31 +47,34 @@ DEFAULTS_PATH = PathExtended.home().joinpath("dotfiles/machineconfig/defaults.in
48
47
  # else: print(f"\nāŒ ERROR | API request failed: {response.status_code}\n")
49
48
 
50
49
 
51
-
52
-
53
- def check_dotfiles_version_is_beyond(commit_dtm: str, update: bool=False):
50
+ def check_dotfiles_version_is_beyond(commit_dtm: str, update: bool) -> bool:
54
51
  dotfiles_path = str(PathExtended.home().joinpath("dotfiles"))
55
52
  from git import Repo
53
+
56
54
  repo = Repo(path=dotfiles_path)
57
55
  last_commit = repo.head.commit
58
56
  dtm = last_commit.committed_datetime
59
57
  from datetime import datetime # make it tz unaware
58
+
60
59
  dtm = datetime(dtm.year, dtm.month, dtm.day, dtm.hour, dtm.minute, dtm.second)
61
- res = dtm > datetime.fromisoformat(commit_dtm)
60
+ res = dtm > datetime.fromisoformat(commit_dtm)
62
61
  if res is False and update is True:
63
62
  console = Console()
64
63
  console.print(Panel(f"šŸ”„ UPDATE REQUIRED | Updating dotfiles because {dtm} < {datetime.fromisoformat(commit_dtm)}", border_style="bold blue", expand=False))
65
64
  from machineconfig.scripts.python.cloud_repo_sync import main
65
+
66
66
  main(cloud=None, path=dotfiles_path)
67
67
  return res
68
68
 
69
+
69
70
  def wait_for_jobs_to_finish(root: PathExtended, pattern: str, wait_for_n_jobs: int, max_wait_minutes: float) -> bool:
70
- wait_finished: bool=False
71
+ wait_finished: bool = False
71
72
  import time
73
+
72
74
  t0 = time.time()
73
75
  while not wait_finished:
74
76
  parts = root.search(pattern, folders=False, r=False)
75
- counter = len(parts)
77
+ counter = len(parts)
76
78
  if counter == wait_for_n_jobs:
77
79
  wait_finished = True
78
80
  console = Console()
@@ -89,8 +91,7 @@ def wait_for_jobs_to_finish(root: PathExtended, pattern: str, wait_for_n_jobs: i
89
91
  return False
90
92
 
91
93
 
92
-
93
- if __name__ == '__main__':
94
+ if __name__ == "__main__":
94
95
  # import typer
95
96
  # typer.run(check_tool_exists)
96
97
  pass