machineconfig 2.0__py3-none-any.whl → 2.2__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 (253) 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 +5 -6
  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 +27 -46
  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 +2 -1
  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/archive/ngrok.py +13 -13
  48. machineconfig/jobs/python_custom_installers/dev/aider.py +7 -15
  49. machineconfig/jobs/python_custom_installers/dev/alacritty.py +9 -18
  50. machineconfig/jobs/python_custom_installers/dev/brave.py +10 -19
  51. machineconfig/jobs/python_custom_installers/dev/bypass_paywall.py +8 -15
  52. machineconfig/jobs/python_custom_installers/dev/code.py +12 -32
  53. machineconfig/jobs/python_custom_installers/dev/cursor.py +3 -14
  54. machineconfig/jobs/python_custom_installers/dev/docker_desktop.py +8 -7
  55. machineconfig/jobs/python_custom_installers/dev/espanso.py +15 -19
  56. machineconfig/jobs/python_custom_installers/dev/goes.py +5 -12
  57. machineconfig/jobs/python_custom_installers/dev/lvim.py +9 -17
  58. machineconfig/jobs/python_custom_installers/dev/nerdfont.py +12 -19
  59. machineconfig/jobs/python_custom_installers/dev/redis.py +12 -20
  60. machineconfig/jobs/python_custom_installers/dev/wezterm.py +12 -19
  61. machineconfig/jobs/python_custom_installers/dev/winget.py +5 -23
  62. machineconfig/jobs/python_custom_installers/docker.py +12 -21
  63. machineconfig/jobs/python_custom_installers/gh.py +11 -19
  64. machineconfig/jobs/python_custom_installers/hx.py +32 -16
  65. machineconfig/jobs/python_custom_installers/warp-cli.py +12 -20
  66. machineconfig/jobs/python_generic_installers/__pycache__/__init__.cpython-313.pyc +0 -0
  67. machineconfig/jobs/python_generic_installers/config.json +1 -1
  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 +38 -26
  74. machineconfig/profile/create_hardlinks.py +29 -20
  75. machineconfig/profile/shell.py +56 -32
  76. machineconfig/scripts/__init__.py +0 -2
  77. machineconfig/scripts/__pycache__/__init__.cpython-313.pyc +0 -0
  78. machineconfig/scripts/cloud/init.sh +2 -2
  79. machineconfig/scripts/linux/checkout_versions +1 -1
  80. machineconfig/scripts/linux/choose_wezterm_theme +1 -1
  81. machineconfig/scripts/linux/cloud_copy +1 -1
  82. machineconfig/scripts/linux/cloud_manager +1 -1
  83. machineconfig/scripts/linux/cloud_mount +1 -1
  84. machineconfig/scripts/linux/cloud_repo_sync +1 -1
  85. machineconfig/scripts/linux/cloud_sync +1 -1
  86. machineconfig/scripts/linux/croshell +1 -1
  87. machineconfig/scripts/linux/devops +7 -7
  88. machineconfig/scripts/linux/fire +1 -1
  89. machineconfig/scripts/linux/fire_agents +3 -2
  90. machineconfig/scripts/linux/ftpx +1 -1
  91. machineconfig/scripts/linux/gh_models +1 -1
  92. machineconfig/scripts/linux/kill_process +1 -1
  93. machineconfig/scripts/linux/mcinit +1 -1
  94. machineconfig/scripts/linux/repos +1 -1
  95. machineconfig/scripts/linux/scheduler +1 -1
  96. machineconfig/scripts/linux/start_slidev +1 -1
  97. machineconfig/scripts/linux/start_terminals +1 -1
  98. machineconfig/scripts/linux/url2md +1 -1
  99. machineconfig/scripts/linux/warp-cli.sh +122 -0
  100. machineconfig/scripts/linux/wifi_conn +1 -1
  101. machineconfig/scripts/python/__pycache__/__init__.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_agents.cpython-313.pyc +0 -0
  106. machineconfig/scripts/python/ai/__init__.py +0 -0
  107. machineconfig/scripts/python/ai/generate_files.py +83 -0
  108. machineconfig/scripts/python/ai/instructions/python/dev.instructions.md +2 -2
  109. machineconfig/scripts/python/ai/mcinit.py +14 -7
  110. machineconfig/scripts/python/ai/scripts/lint_and_type_check.sh +10 -5
  111. machineconfig/scripts/python/archive/tmate_conn.py +5 -5
  112. machineconfig/scripts/python/archive/tmate_start.py +7 -7
  113. machineconfig/scripts/python/choose_wezterm_theme.py +35 -32
  114. machineconfig/scripts/python/cloud_copy.py +23 -14
  115. machineconfig/scripts/python/cloud_mount.py +36 -24
  116. machineconfig/scripts/python/cloud_repo_sync.py +40 -27
  117. machineconfig/scripts/python/cloud_sync.py +4 -4
  118. machineconfig/scripts/python/croshell.py +40 -29
  119. machineconfig/scripts/python/devops.py +45 -27
  120. machineconfig/scripts/python/devops_add_identity.py +15 -25
  121. machineconfig/scripts/python/devops_add_ssh_key.py +8 -8
  122. machineconfig/scripts/python/devops_backup_retrieve.py +18 -16
  123. machineconfig/scripts/python/devops_devapps_install.py +25 -20
  124. machineconfig/scripts/python/devops_update_repos.py +232 -59
  125. machineconfig/scripts/python/dotfile.py +17 -15
  126. machineconfig/scripts/python/fire_agents.py +48 -22
  127. machineconfig/scripts/python/fire_jobs.py +93 -58
  128. machineconfig/scripts/python/ftpx.py +26 -15
  129. machineconfig/scripts/python/get_zellij_cmd.py +8 -7
  130. machineconfig/scripts/python/helpers/cloud_helpers.py +33 -28
  131. machineconfig/scripts/python/helpers/helpers2.py +27 -16
  132. machineconfig/scripts/python/helpers/helpers4.py +45 -32
  133. machineconfig/scripts/python/helpers/helpers5.py +1 -1
  134. machineconfig/scripts/python/helpers/repo_sync_helpers.py +32 -10
  135. machineconfig/scripts/python/mount_nfs.py +9 -16
  136. machineconfig/scripts/python/mount_nw_drive.py +10 -5
  137. machineconfig/scripts/python/mount_ssh.py +9 -7
  138. machineconfig/scripts/python/repos.py +216 -58
  139. machineconfig/scripts/python/snapshot.py +0 -1
  140. machineconfig/scripts/python/start_slidev.py +11 -6
  141. machineconfig/scripts/python/start_terminals.py +22 -16
  142. machineconfig/scripts/python/viewer_template.py +0 -1
  143. machineconfig/scripts/python/wifi_conn.py +49 -75
  144. machineconfig/scripts/python/wsl_windows_transfer.py +9 -7
  145. machineconfig/scripts/windows/checkout_version.ps1 +1 -3
  146. machineconfig/scripts/windows/choose_wezterm_theme.ps1 +1 -3
  147. machineconfig/scripts/windows/cloud_copy.ps1 +2 -6
  148. machineconfig/scripts/windows/cloud_manager.ps1 +1 -1
  149. machineconfig/scripts/windows/cloud_repo_sync.ps1 +1 -2
  150. machineconfig/scripts/windows/cloud_sync.ps1 +2 -2
  151. machineconfig/scripts/windows/croshell.ps1 +2 -2
  152. machineconfig/scripts/windows/devops.ps1 +1 -4
  153. machineconfig/scripts/windows/dotfile.ps1 +1 -3
  154. machineconfig/scripts/windows/fire.ps1 +1 -1
  155. machineconfig/scripts/windows/ftpx.ps1 +2 -2
  156. machineconfig/scripts/windows/gpt.ps1 +1 -1
  157. machineconfig/scripts/windows/kill_process.ps1 +1 -2
  158. machineconfig/scripts/windows/mcinit.ps1 +1 -1
  159. machineconfig/scripts/windows/mount_nfs.ps1 +1 -1
  160. machineconfig/scripts/windows/mount_ssh.ps1 +1 -1
  161. machineconfig/scripts/windows/pomodoro.ps1 +1 -1
  162. machineconfig/scripts/windows/py2exe.ps1 +1 -3
  163. machineconfig/scripts/windows/repos.ps1 +1 -1
  164. machineconfig/scripts/windows/scheduler.ps1 +1 -1
  165. machineconfig/scripts/windows/snapshot.ps1 +2 -2
  166. machineconfig/scripts/windows/start_slidev.ps1 +1 -1
  167. machineconfig/scripts/windows/start_terminals.ps1 +1 -1
  168. machineconfig/scripts/windows/wifi_conn.ps1 +1 -1
  169. machineconfig/scripts/windows/wsl_windows_transfer.ps1 +1 -3
  170. machineconfig/settings/lf/linux/lfrc +2 -1
  171. machineconfig/settings/linters/.ruff_cache/.gitignore +2 -0
  172. machineconfig/settings/linters/.ruff_cache/CACHEDIR.TAG +1 -0
  173. machineconfig/settings/lvim/windows/archive/config_additional.lua +1 -1
  174. machineconfig/settings/svim/linux/init.toml +1 -1
  175. machineconfig/settings/svim/windows/init.toml +1 -1
  176. machineconfig/setup_linux/web_shortcuts/croshell.sh +3 -52
  177. machineconfig/setup_linux/web_shortcuts/interactive.sh +6 -6
  178. machineconfig/setup_linux/web_shortcuts/ssh.sh +0 -4
  179. machineconfig/setup_windows/web_shortcuts/all.ps1 +2 -2
  180. machineconfig/setup_windows/web_shortcuts/ascii_art.ps1 +1 -1
  181. machineconfig/setup_windows/web_shortcuts/croshell.ps1 +1 -1
  182. machineconfig/setup_windows/web_shortcuts/interactive.ps1 +5 -5
  183. machineconfig/setup_windows/wt_and_pwsh/install_fonts.ps1 +51 -15
  184. machineconfig/setup_windows/wt_and_pwsh/set_pwsh_theme.py +58 -13
  185. machineconfig/setup_windows/wt_and_pwsh/set_wt_settings.py +45 -37
  186. machineconfig/utils/ai/generate_file_checklist.py +8 -10
  187. machineconfig/utils/ai/url2md.py +4 -2
  188. machineconfig/utils/cloud/onedrive/setup_oauth.py +1 -0
  189. machineconfig/utils/cloud/onedrive/transaction.py +63 -98
  190. machineconfig/utils/code.py +62 -41
  191. machineconfig/utils/installer.py +29 -35
  192. machineconfig/utils/installer_utils/installer_abc.py +11 -11
  193. machineconfig/utils/installer_utils/installer_class.py +155 -74
  194. machineconfig/utils/links.py +112 -31
  195. machineconfig/utils/notifications.py +211 -0
  196. machineconfig/utils/options.py +41 -42
  197. machineconfig/utils/path.py +13 -6
  198. machineconfig/utils/path_reduced.py +614 -311
  199. machineconfig/utils/procs.py +48 -42
  200. machineconfig/utils/scheduling.py +0 -1
  201. machineconfig/utils/source_of_truth.py +27 -0
  202. machineconfig/utils/ssh.py +146 -85
  203. machineconfig/utils/terminal.py +84 -37
  204. machineconfig/utils/upgrade_packages.py +91 -0
  205. machineconfig/utils/utils2.py +39 -50
  206. machineconfig/utils/utils5.py +195 -116
  207. machineconfig/utils/ve.py +13 -5
  208. {machineconfig-2.0.dist-info → machineconfig-2.2.dist-info}/METADATA +14 -13
  209. {machineconfig-2.0.dist-info → machineconfig-2.2.dist-info}/RECORD +212 -237
  210. machineconfig/jobs/__pycache__/__init__.cpython-311.pyc +0 -0
  211. machineconfig/jobs/python/__pycache__/__init__.cpython-311.pyc +0 -0
  212. machineconfig/jobs/python/__pycache__/python_ve_symlink.cpython-311.pyc +0 -0
  213. machineconfig/jobs/python/archive/python_tools.txt +0 -12
  214. machineconfig/jobs/python/vscode/__pycache__/select_interpreter.cpython-311.pyc +0 -0
  215. machineconfig/jobs/python_custom_installers/__pycache__/__init__.cpython-311.pyc +0 -0
  216. machineconfig/jobs/python_generic_installers/__pycache__/__init__.cpython-311.pyc +0 -0
  217. machineconfig/jobs/python_generic_installers/update.py +0 -3
  218. machineconfig/jobs/python_linux_installers/__pycache__/__init__.cpython-311.pyc +0 -0
  219. machineconfig/profile/__pycache__/__init__.cpython-311.pyc +0 -0
  220. machineconfig/profile/__pycache__/create.cpython-311.pyc +0 -0
  221. machineconfig/profile/__pycache__/shell.cpython-311.pyc +0 -0
  222. machineconfig/scripts/__pycache__/__init__.cpython-311.pyc +0 -0
  223. machineconfig/scripts/linux/activate_ve +0 -87
  224. machineconfig/scripts/python/__pycache__/__init__.cpython-311.pyc +0 -0
  225. machineconfig/scripts/python/__pycache__/cloud_copy.cpython-311.pyc +0 -0
  226. machineconfig/scripts/python/__pycache__/cloud_mount.cpython-311.pyc +0 -0
  227. machineconfig/scripts/python/__pycache__/cloud_sync.cpython-311.pyc +0 -0
  228. machineconfig/scripts/python/__pycache__/croshell.cpython-311.pyc +0 -0
  229. machineconfig/scripts/python/__pycache__/devops.cpython-311.pyc +0 -0
  230. machineconfig/scripts/python/__pycache__/devops_backup_retrieve.cpython-311.pyc +0 -0
  231. machineconfig/scripts/python/__pycache__/devops_devapps_install.cpython-311.pyc +0 -0
  232. machineconfig/scripts/python/__pycache__/devops_update_repos.cpython-311.pyc +0 -0
  233. machineconfig/scripts/python/__pycache__/fire_agents.cpython-311.pyc +0 -0
  234. machineconfig/scripts/python/__pycache__/fire_jobs.cpython-311.pyc +0 -0
  235. machineconfig/scripts/python/__pycache__/fire_jobs.cpython-313.pyc +0 -0
  236. machineconfig/scripts/python/__pycache__/get_zellij_cmd.cpython-311.pyc +0 -0
  237. machineconfig/scripts/python/__pycache__/repos.cpython-311.pyc +0 -0
  238. machineconfig/scripts/python/ai/__pycache__/init.cpython-311.pyc +0 -0
  239. machineconfig/scripts/python/ai/__pycache__/mcinit.cpython-311.pyc +0 -0
  240. machineconfig/scripts/python/helpers/__pycache__/__init__.cpython-311.pyc +0 -0
  241. machineconfig/scripts/python/helpers/__pycache__/__init__.cpython-313.pyc +0 -0
  242. machineconfig/scripts/python/helpers/__pycache__/cloud_helpers.cpython-311.pyc +0 -0
  243. machineconfig/scripts/python/helpers/__pycache__/helpers2.cpython-311.pyc +0 -0
  244. machineconfig/scripts/python/helpers/__pycache__/helpers4.cpython-311.pyc +0 -0
  245. machineconfig/scripts/python/helpers/__pycache__/helpers4.cpython-313.pyc +0 -0
  246. machineconfig/scripts/python/helpers/__pycache__/repo_sync_helpers.cpython-311.pyc +0 -0
  247. machineconfig/scripts/windows/activate_ve.ps1 +0 -54
  248. machineconfig/setup_linux/web_shortcuts/all.sh +0 -48
  249. machineconfig/setup_linux/web_shortcuts/update_system.sh +0 -48
  250. machineconfig/utils/utils.py +0 -95
  251. /machineconfig/setup_linux/web_shortcuts/{tmp.sh → android.sh} +0 -0
  252. {machineconfig-2.0.dist-info → machineconfig-2.2.dist-info}/WHEEL +0 -0
  253. {machineconfig-2.0.dist-info → machineconfig-2.2.dist-info}/top_level.txt +0 -0
@@ -1,37 +1,44 @@
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
8
+ # from machineconfig.utils.ve import get_ve_activate_line
9
+
9
10
 
10
11
  @dataclass
11
12
  class Scout:
12
- source_full: P
13
- source_rel2home: P
13
+ source_full: PathExtended
14
+ source_rel2home: PathExtended
14
15
  exists: bool
15
16
  is_dir: bool
16
- files: Optional[List[P]]
17
+ files: Optional[List[PathExtended]]
18
+
19
+
17
20
  def scout(source: PLike, z: bool = False, r: bool = False) -> Scout:
18
- source_full = P(source).expanduser().absolute()
21
+ source_full = PathExtended(source).expanduser().absolute()
19
22
  source_rel2home = source_full.collapseuser()
20
23
  exists = source_full.exists()
21
24
  is_dir = source_full.is_dir() if exists else False
22
25
  if z and exists:
23
- try: source_full = source_full.zip()
26
+ try:
27
+ source_full = source_full.zip()
24
28
  except Exception as ex:
25
29
  raise Exception(f"Could not zip {source_full} due to {ex}") from ex # type: ignore # pylint: disable=W0719
26
30
  source_rel2home = source_full.zip()
27
31
  if r and exists and is_dir:
28
32
  files = [item.collapseuser() for item in source_full.search(folders=False, r=True)]
29
- else: files = None
33
+ else:
34
+ files = None
30
35
  return Scout(source_full=source_full, source_rel2home=source_rel2home, exists=exists, is_dir=is_dir, files=files)
31
36
 
32
37
 
33
38
  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
39
+ def __init__(
40
+ 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
41
+ ): # https://stackoverflow.com/questions/51027192/execute-command-script-using-different-shell-in-ssh-paramiko
35
42
  self.pwd = pwd
36
43
  self.ve = ve
37
44
  self.compress = compress # Defaults: (1) use localhost if nothing provided.
@@ -44,26 +51,33 @@ class SSH: # inferior alternative: https://github.com/fabric/fabric
44
51
  import platform
45
52
  import paramiko # type: ignore
46
53
  import getpass
54
+
47
55
  if isinstance(host, str):
48
56
  try:
49
57
  import paramiko.config as pconfig
50
- config = pconfig.SSHConfig.from_path(str(P.home().joinpath(".ssh/config")))
58
+
59
+ config = pconfig.SSHConfig.from_path(str(PathExtended.home().joinpath(".ssh/config")))
51
60
  config_dict = config.lookup(host)
52
61
  self.hostname = config_dict["hostname"]
53
62
  self.username = config_dict["user"]
54
63
  self.host = host
55
64
  self.port = int(config_dict.get("port", port))
56
65
  tmp = config_dict.get("identityfile", sshkey)
57
- if isinstance(tmp, list): sshkey = tmp[0]
58
- else: sshkey = tmp
66
+ if isinstance(tmp, list):
67
+ sshkey = tmp[0]
68
+ else:
69
+ sshkey = tmp
59
70
  self.proxycommand = config_dict.get("proxycommand", None)
60
71
  if sshkey is not None:
61
72
  tmp = config.lookup("*").get("identityfile", sshkey)
62
- if isinstance(tmp, list): sshkey = tmp[0]
63
- else: sshkey = tmp
73
+ if isinstance(tmp, list):
74
+ sshkey = tmp[0]
75
+ else:
76
+ sshkey = tmp
64
77
  except (FileNotFoundError, KeyError):
65
78
  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("@")
79
+ if "@" in host:
80
+ self.username, self.hostname = host.split("@")
67
81
  else:
68
82
  self.username = username or getpass.getuser()
69
83
  self.hostname = host
@@ -77,12 +91,11 @@ class SSH: # inferior alternative: https://github.com/fabric/fabric
77
91
  print(f"Provided values: host={host}, username={username}, hostname={hostname}")
78
92
  raise ValueError("Either host or username and hostname must be provided.")
79
93
 
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
94
+ 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
95
  self.ssh = paramiko.SSHClient()
82
96
  self.ssh.load_system_host_keys()
83
97
  self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
84
98
  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")
85
-
86
99
  sock = paramiko.ProxyCommand(self.proxycommand) if self.proxycommand is not None else None
87
100
  try:
88
101
  if pwd is None:
@@ -91,38 +104,36 @@ 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
101
-
102
- try: self.sftp: Optional[paramiko.SFTPClient] = self.ssh.open_sftp()
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
+ try:
113
+ self.sftp: Optional[paramiko.SFTPClient] = self.ssh.open_sftp()
103
114
  except Exception as err:
104
115
  self.sftp = None
105
116
  print(f"""⚠️ WARNING: Failed to open SFTP connection to {hostname}.
106
117
  Error Details: {err}\nData transfer may be affected!""")
118
+
107
119
  def view_bar(slf: Any, a: Any, b: Any):
108
120
  slf.total = int(b)
109
121
  slf.update(int(a - slf.n)) # update pbar with increment
110
122
  from tqdm import tqdm
111
- self.tqdm_wrap = type('TqdmWrap', (tqdm,), {'view_bar': view_bar})
123
+ self.tqdm_wrap = type("TqdmWrap", (tqdm,), {"view_bar": view_bar})
112
124
  self._local_distro: Optional[str] = None
113
125
  self._remote_distro: Optional[str] = None
114
126
  self._remote_machine: Optional[MACHINE] = None
115
127
  self.terminal_responses: list[Response] = []
116
128
  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)
121
129
  def get_remote_machine(self) -> MACHINE:
122
130
  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"
131
+ 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":
132
+ self._remote_machine = "Windows"
133
+ else:
134
+ self._remote_machine = "Linux"
125
135
  return self._remote_machine # echo %OS% TODO: uname on linux
136
+
126
137
  def get_local_distro(self) -> str:
127
138
  if self._local_distro is None:
128
139
  command = """uv run --with distro python -c "import distro; print(distro.name(pretty=True))" """
@@ -133,105 +144,150 @@ class SSH: # inferior alternative: https://github.com/fabric/fabric
133
144
  return self._local_distro
134
145
  def get_remote_distro(self):
135
146
  if self._remote_distro is None:
136
- self._remote_distro = self.run_py("print(install_n_import('distro').name(pretty=True))", verbose=False).op_if_successfull_or_default() or ""
137
- # q.run("""~/.local/bin/uv run --with distro python -c "import distro; print(distro.name(pretty=True))" """)
147
+ res = self.run("""~/.local/bin/uv run --with distro python -c "import distro; print(distro.name(pretty=True))" """)
148
+ self._remote_distro = res.op_if_successfull_or_default() or ""
138
149
  return self._remote_distro
139
- def restart_computer(self): self.run("Restart-Computer -Force" if self.get_remote_machine() == "Windows" else "sudo reboot")
150
+
151
+ def restart_computer(self):
152
+ self.run("Restart-Computer -Force" if self.get_remote_machine() == "Windows" else "sudo reboot")
153
+
140
154
  def send_ssh_key(self):
141
155
  self.copy_from_here("~/.ssh/id_rsa.pub")
142
156
  assert self.get_remote_machine() == "Windows"
143
157
  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")
158
+ code = PathExtended(code_url).download().read_text(encoding="utf-8")
145
159
  self.run(code)
160
+
146
161
  def copy_env_var(self, name: str):
147
162
  assert self.get_remote_machine() == "Linux"
148
163
  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 "")
164
+
165
+ def get_remote_repr(self, add_machine: bool = False) -> str:
166
+ return f"{self.username}@{self.hostname}:{self.port}" + (f" [{self.get_remote_machine()}][{self.get_remote_distro()}]" if add_machine else "")
167
+
150
168
  def get_local_repr(self, add_machine: bool = False) -> str:
151
169
  import getpass
170
+
152
171
  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)}"
172
+
173
+ def __repr__(self):
174
+ return f"local {self.get_local_repr(add_machine=True)} >>> SSH TO >>> remote {self.get_remote_repr(add_machine=True)}"
175
+
154
176
  def run_locally(self, command: str):
155
177
  print(f"""💻 [LOCAL EXECUTION] Running command on node: {self.platform.node()} Command: {command}""")
156
178
  res = Response(cmd=command)
157
179
  res.output.returncode = os.system(command)
158
180
  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 ' ')
160
- # 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
- 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
- cmd = (self.remote_env_cmd + "; " + cmd) if env_prefix else cmd
181
+ def get_ssh_conn_str(self, cmd: str = ""):
182
+ return "ssh " + (f" -i {self.sshkey}" if self.sshkey else "") + self.get_remote_repr().replace(":", " -p ") + (f" -t {cmd} " if cmd != "" else " ")
183
+ def run(self, cmd: str, verbose: bool = True, desc: str = "", strict_err: bool = False, strict_returncode: bool = False) -> Response:
163
184
  raw = self.ssh.exec_command(cmd)
164
185
  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()
186
+ if not verbose:
187
+ res.capture().print_if_unsuccessful(desc=desc, strict_err=strict_err, strict_returncode=strict_returncode, assert_success=False)
188
+ else:
189
+ res.print()
167
190
  self.terminal_responses.append(res)
168
191
  return res
169
192
  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
193
  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)
194
+ if not return_obj:
195
+ return self.run(
196
+ cmd=f"""uv run --with machineconfig -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)
172
197
  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'))
198
+ 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]
199
+ res = self.copy_to_here(source=source_file, target=PathExtended.tmpfile(suffix=".pkl"))
175
200
  import pickle
176
- res_bytes = res.read_bytes()
177
- 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!")
201
+ return pickle.loads(res.read_bytes())
202
+
203
+ 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]]:
204
+ if init:
205
+ print(f"{'⬆️' * 5} [SFTP UPLOAD] FROM `{source}` TO `{target}`") # TODO: using return_obj do all tests required in one go.
206
+ source_obj = PathExtended(source).expanduser().absolute()
207
+ if not source_obj.exists():
208
+ raise RuntimeError(f"Meta.SSH Error: source `{source_obj}` does not exist!")
182
209
  if target is None:
183
- target = P(source_obj).expanduser().absolute().collapseuser(strict=True)
210
+ target = PathExtended(source_obj).expanduser().absolute().collapseuser(strict=True)
184
211
  assert target.is_relative_to("~"), "If target is not specified, source must be relative to home."
185
- if z: target += ".zip"
212
+ if z:
213
+ target += ".zip"
186
214
  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}")
215
+ if r is False:
216
+ 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.")
217
+ source_list: list[PathExtended] = source_obj.search("*", folders=False, files=True, r=True)
218
+ remote_root = (
219
+ self.run_py(
220
+ f"path=P(r'{PathExtended(target).as_posix()}').expanduser()\n{'path.delete(sure=True)' if overwrite else ''}\nprint(path.create())",
221
+ desc=f"Creating Target directory `{PathExtended(target).as_posix()}` @ {self.get_remote_repr()}",
222
+ verbose=False,
223
+ ).op
224
+ or ""
225
+ )
226
+ for idx, item in enumerate(source_list):
227
+ print(f" {idx + 1:03d}. {item}")
191
228
  for item in source_list:
192
- a__target = P(remote_root).joinpath(item.relative_to(source_obj))
229
+ a__target = PathExtended(remote_root).joinpath(item.relative_to(source_obj))
193
230
  self.copy_from_here(source=item, target=a__target)
194
231
  return list(source_list)
195
232
  if z:
196
233
  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
234
+ 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.
235
+ remotepath = (
236
+ self.run_py(
237
+ f"path=P(r'{PathExtended(target).as_posix()}').expanduser()\n{'path.delete(sure=True)' if overwrite else ''}\nprint(path.parent.create())",
238
+ desc=f"Creating Target directory `{PathExtended(target).parent.as_posix()}` @ {self.get_remote_repr()}",
239
+ verbose=False,
240
+ ).op
241
+ or ""
242
+ )
243
+ remotepath = PathExtended(remotepath.split("\n")[-1]).joinpath(PathExtended(target).name)
244
+ print(f"""📤 [SFTP UPLOAD] Sending file: {repr(PathExtended(source_obj))} ==> Remote Path: {remotepath.as_posix()}""")
245
+ with self.tqdm_wrap(ascii=True, unit="b", unit_scale=True) as pbar:
246
+ self.sftp.put(localpath=PathExtended(source_obj).expanduser(), remotepath=remotepath.as_posix(), callback=pbar.view_bar) # type: ignore # pylint: disable=E1129
202
247
  if z:
203
248
  _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
249
  source_obj.delete(sure=True)
205
250
  print("\n")
206
251
  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.")
252
+
253
+ def copy_to_here(self, source: PLike, target: OPLike = None, z: bool = False, r: bool = False, init: bool = True) -> PathExtended:
254
+ if init:
255
+ print(f"{'⬇️' * 5} SFTP DOWNLOADING FROM `{source}` TO `{target}`")
256
+ 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":
257
+ if r is False:
258
+ raise RuntimeError(f"source `{source}` is a directory! either set r=True for recursive sending or raise zip_first flag.")
211
259
  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
260
  assert isinstance(source_list, List), f"Could not resolve source path {source} due to error"
213
261
  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)
262
+ self.copy_to_here(source=file.as_posix(), target=PathExtended(target).joinpath(PathExtended(file).relative_to(source)) if target else None, r=False)
215
263
  if z:
216
264
  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
265
  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
266
+ if not isinstance(tmp2, PathExtended):
267
+ raise RuntimeError(f"Could not zip {source} due to {tmp.err}")
268
+ else:
269
+ source = tmp2
220
270
  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")
271
+ 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()
272
+ if isinstance(tmpx, PathExtended):
273
+ target = tmpx
274
+ else:
275
+ raise RuntimeError(f"Could not resolve target path {target} due to error")
224
276
  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()
277
+ target_obj = PathExtended(target).expanduser().absolute()
226
278
  target_obj.parent.mkdir(parents=True, exist_ok=True)
227
- if z and '.zip' not in target_obj.suffix: target_obj += '.zip'
279
+ if z and ".zip" not in target_obj.suffix:
280
+ target_obj += ".zip"
228
281
  if "~" in str(source):
229
282
  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)
283
+ if isinstance(tmp3, PathExtended):
284
+ source = tmp3
285
+ else:
286
+ raise RuntimeError(f"Could not resolve source path {source} due to")
287
+ else:
288
+ source = PathExtended(source)
233
289
  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
290
+ with self.tqdm_wrap(ascii=True, unit="b", unit_scale=True) as pbar: # type: ignore # pylint: disable=E1129
235
291
  assert self.sftp is not None, f"Could not establish SFTP connection to {self.hostname}."
236
292
  self.sftp.get(remotepath=source.as_posix(), localpath=str(target_obj), callback=pbar.view_bar) # type: ignore
237
293
  if z:
@@ -239,27 +295,32 @@ class SSH: # inferior alternative: https://github.com/fabric/fabric
239
295
  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
296
  print("\n")
241
297
  return target_obj
242
- def receieve(self, source: PLike, target: OPLike = None, z: bool = False, r: bool = False) -> P:
298
+
299
+ def receieve(self, source: PLike, target: OPLike = None, z: bool = False, r: bool = False) -> PathExtended:
243
300
  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
301
  assert isinstance(scout, Scout)
245
302
  if not z and scout.is_dir and scout.files is not None:
246
303
  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]
304
+ 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
305
  return tmp[0]
249
- else: print("Source is a directory! either set `r=True` for recursive sending or raise `zip_first=True` flag.")
306
+ else:
307
+ print("Source is a directory! either set `r=True` for recursive sending or raise `zip_first=True` flag.")
250
308
  if target:
251
- target = P(target).expanduser().absolute()
309
+ target = PathExtended(target).expanduser().absolute()
252
310
  else:
253
311
  target = scout.source_rel2home.expanduser().absolute()
254
312
  target.parent.mkdir(parents=True, exist_ok=True)
255
- if z and '.zip' not in target.suffix: target += '.zip'
313
+ if z and ".zip" not in target.suffix:
314
+ target += ".zip"
256
315
  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
316
+ with self.tqdm_wrap(ascii=True, unit="b", unit_scale=True) as pbar:
317
+ self.sftp.get(remotepath=source.as_posix(), localpath=target.as_posix(), callback=pbar.view_bar) # type: ignore # pylint: disable=E1129
258
318
  if z:
259
319
  target = target.unzip(inplace=True, content=True)
260
320
  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
321
  print("\n")
262
322
  return target
323
+
263
324
  # def print_summary(self):
264
325
  # import polars as pl
265
326
  # df = pl.DataFrame(List(self.terminal_responses).apply(lambda rsp: dict(desc=rsp.desc, err=rsp.err, returncode=rsp.returncode)).list)