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.
- machineconfig/cluster/cloud_manager.py +22 -29
- machineconfig/cluster/data_transfer.py +2 -3
- machineconfig/cluster/distribute.py +0 -2
- machineconfig/cluster/file_manager.py +4 -5
- machineconfig/cluster/job_params.py +1 -4
- machineconfig/cluster/loader_runner.py +8 -11
- machineconfig/cluster/remote_machine.py +4 -5
- machineconfig/cluster/script_execution.py +2 -2
- machineconfig/cluster/script_notify_upon_completion.py +0 -1
- machineconfig/cluster/sessions_managers/archive/create_zellij_template.py +4 -6
- machineconfig/cluster/sessions_managers/archive/session_managers.py +0 -1
- machineconfig/cluster/sessions_managers/enhanced_command_runner.py +35 -75
- machineconfig/cluster/sessions_managers/wt_local.py +113 -185
- machineconfig/cluster/sessions_managers/wt_local_manager.py +127 -197
- machineconfig/cluster/sessions_managers/wt_remote.py +60 -67
- machineconfig/cluster/sessions_managers/wt_remote_manager.py +110 -149
- machineconfig/cluster/sessions_managers/wt_utils/layout_generator.py +61 -64
- machineconfig/cluster/sessions_managers/wt_utils/process_monitor.py +72 -172
- machineconfig/cluster/sessions_managers/wt_utils/remote_executor.py +27 -60
- machineconfig/cluster/sessions_managers/wt_utils/session_manager.py +58 -137
- machineconfig/cluster/sessions_managers/wt_utils/status_reporter.py +46 -74
- machineconfig/cluster/sessions_managers/zellij_local.py +91 -147
- machineconfig/cluster/sessions_managers/zellij_local_manager.py +165 -190
- machineconfig/cluster/sessions_managers/zellij_remote.py +51 -58
- machineconfig/cluster/sessions_managers/zellij_remote_manager.py +40 -46
- machineconfig/cluster/sessions_managers/zellij_utils/example_usage.py +19 -17
- machineconfig/cluster/sessions_managers/zellij_utils/layout_generator.py +30 -31
- machineconfig/cluster/sessions_managers/zellij_utils/process_monitor.py +64 -134
- machineconfig/cluster/sessions_managers/zellij_utils/remote_executor.py +7 -11
- machineconfig/cluster/sessions_managers/zellij_utils/session_manager.py +27 -55
- machineconfig/cluster/sessions_managers/zellij_utils/status_reporter.py +14 -13
- machineconfig/cluster/templates/cli_click.py +0 -1
- machineconfig/cluster/templates/cli_gooey.py +0 -2
- machineconfig/cluster/templates/cli_trogon.py +0 -1
- machineconfig/cluster/templates/run_cloud.py +0 -1
- machineconfig/cluster/templates/run_cluster.py +0 -1
- machineconfig/cluster/templates/run_remote.py +0 -1
- machineconfig/cluster/templates/utils.py +27 -11
- machineconfig/jobs/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/jobs/linux/msc/cli_agents.sh +16 -0
- machineconfig/jobs/python/check_installations.py +9 -9
- machineconfig/jobs/python/create_bootable_media.py +0 -2
- machineconfig/jobs/python/python_cargo_build_share.py +2 -2
- machineconfig/jobs/python/python_ve_symlink.py +9 -11
- machineconfig/jobs/python/tasks.py +0 -1
- machineconfig/jobs/python/vscode/api.py +5 -5
- machineconfig/jobs/python/vscode/link_ve.py +20 -21
- machineconfig/jobs/python/vscode/select_interpreter.py +28 -29
- machineconfig/jobs/python/vscode/sync_code.py +14 -18
- machineconfig/jobs/python_custom_installers/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/jobs/python_custom_installers/archive/ngrok.py +15 -15
- machineconfig/jobs/python_custom_installers/dev/aider.py +10 -18
- machineconfig/jobs/python_custom_installers/dev/alacritty.py +12 -21
- machineconfig/jobs/python_custom_installers/dev/brave.py +13 -22
- machineconfig/jobs/python_custom_installers/dev/bypass_paywall.py +13 -20
- machineconfig/jobs/python_custom_installers/dev/code.py +17 -24
- machineconfig/jobs/python_custom_installers/dev/cursor.py +10 -21
- machineconfig/jobs/python_custom_installers/dev/docker_desktop.py +12 -11
- machineconfig/jobs/python_custom_installers/dev/espanso.py +19 -23
- machineconfig/jobs/python_custom_installers/dev/goes.py +9 -16
- machineconfig/jobs/python_custom_installers/dev/lvim.py +13 -21
- machineconfig/jobs/python_custom_installers/dev/nerdfont.py +15 -22
- machineconfig/jobs/python_custom_installers/dev/redis.py +15 -23
- machineconfig/jobs/python_custom_installers/dev/wezterm.py +15 -22
- machineconfig/jobs/python_custom_installers/dev/winget.py +32 -50
- machineconfig/jobs/python_custom_installers/docker.py +15 -24
- machineconfig/jobs/python_custom_installers/gh.py +18 -26
- machineconfig/jobs/python_custom_installers/hx.py +33 -17
- machineconfig/jobs/python_custom_installers/warp-cli.py +15 -23
- machineconfig/jobs/python_generic_installers/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/jobs/python_generic_installers/config.json +412 -389
- machineconfig/jobs/python_linux_installers/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/jobs/python_windows_installers/dev/config.json +1 -1
- machineconfig/jobs/windows/archive/archive_pygraphviz.ps1 +1 -1
- machineconfig/jobs/windows/msc/cli_agents.bat +0 -0
- machineconfig/jobs/windows/msc/cli_agents.ps1 +0 -0
- machineconfig/jobs/windows/start_terminal.ps1 +1 -1
- machineconfig/logger.py +50 -0
- machineconfig/profile/create.py +50 -36
- machineconfig/profile/create_hardlinks.py +33 -26
- machineconfig/profile/shell.py +87 -60
- machineconfig/scripts/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/scripts/cloud/init.sh +2 -2
- machineconfig/scripts/linux/checkout_versions +1 -1
- machineconfig/scripts/linux/choose_wezterm_theme +1 -1
- machineconfig/scripts/linux/cloud_copy +1 -1
- machineconfig/scripts/linux/cloud_manager +1 -1
- machineconfig/scripts/linux/cloud_mount +1 -1
- machineconfig/scripts/linux/cloud_repo_sync +1 -1
- machineconfig/scripts/linux/cloud_sync +1 -1
- machineconfig/scripts/linux/croshell +1 -1
- machineconfig/scripts/linux/devops +3 -5
- machineconfig/scripts/linux/fire +2 -1
- machineconfig/scripts/linux/fire_agents +3 -3
- machineconfig/scripts/linux/ftpx +1 -1
- machineconfig/scripts/linux/gh_models +1 -1
- machineconfig/scripts/linux/kill_process +1 -1
- machineconfig/scripts/linux/mcinit +2 -2
- machineconfig/scripts/linux/repos +1 -1
- machineconfig/scripts/linux/scheduler +1 -1
- machineconfig/scripts/linux/start_slidev +1 -1
- machineconfig/scripts/linux/start_terminals +1 -1
- machineconfig/scripts/linux/url2md +1 -1
- machineconfig/scripts/linux/warp-cli.sh +122 -0
- machineconfig/scripts/linux/wifi_conn +1 -1
- machineconfig/scripts/python/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/croshell.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/devops.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/devops_devapps_install.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/devops_update_repos.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/fire_jobs.cpython-313.pyc +0 -0
- machineconfig/scripts/python/ai/__init__.py +0 -0
- machineconfig/scripts/python/ai/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/scripts/python/ai/__pycache__/generate_files.cpython-313.pyc +0 -0
- machineconfig/scripts/python/ai/__pycache__/mcinit.cpython-313.pyc +0 -0
- machineconfig/scripts/python/ai/chatmodes/Thinking-Beast-Mode.chatmode.md +337 -0
- machineconfig/scripts/python/ai/chatmodes/Ultimate-Transparent-Thinking-Beast-Mode.chatmode.md +644 -0
- machineconfig/scripts/python/ai/chatmodes/deepResearch.chatmode.md +81 -0
- machineconfig/scripts/python/ai/configs/.gemini/settings.json +81 -0
- machineconfig/scripts/python/ai/generate_files.py +84 -0
- machineconfig/scripts/python/ai/instructions/python/dev.instructions.md +45 -0
- machineconfig/scripts/python/ai/mcinit.py +107 -0
- machineconfig/scripts/python/ai/prompts/allLintersAndTypeCheckers.prompt.md +5 -0
- machineconfig/scripts/python/ai/prompts/research-report-skeleton.prompt.md +38 -0
- machineconfig/scripts/python/ai/scripts/lint_and_type_check.sh +52 -0
- machineconfig/scripts/python/archive/tmate_conn.py +5 -5
- machineconfig/scripts/python/archive/tmate_start.py +3 -3
- machineconfig/scripts/python/choose_wezterm_theme.py +2 -2
- machineconfig/scripts/python/cloud_copy.py +20 -19
- machineconfig/scripts/python/cloud_mount.py +10 -8
- machineconfig/scripts/python/cloud_repo_sync.py +15 -15
- machineconfig/scripts/python/cloud_sync.py +1 -1
- machineconfig/scripts/python/croshell.py +18 -16
- machineconfig/scripts/python/devops.py +6 -6
- machineconfig/scripts/python/devops_add_identity.py +9 -7
- machineconfig/scripts/python/devops_add_ssh_key.py +19 -19
- machineconfig/scripts/python/devops_backup_retrieve.py +14 -14
- machineconfig/scripts/python/devops_devapps_install.py +3 -3
- machineconfig/scripts/python/devops_update_repos.py +141 -53
- machineconfig/scripts/python/dotfile.py +3 -3
- machineconfig/scripts/python/fire_agents.py +202 -41
- machineconfig/scripts/python/fire_jobs.py +20 -21
- machineconfig/scripts/python/ftpx.py +4 -3
- machineconfig/scripts/python/gh_models.py +94 -94
- machineconfig/scripts/python/helpers/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/scripts/python/helpers/__pycache__/helpers4.cpython-313.pyc +0 -0
- machineconfig/scripts/python/helpers/cloud_helpers.py +3 -3
- machineconfig/scripts/python/helpers/helpers2.py +3 -3
- machineconfig/scripts/python/helpers/helpers4.py +8 -7
- machineconfig/scripts/python/helpers/helpers5.py +7 -7
- machineconfig/scripts/python/helpers/repo_sync_helpers.py +2 -2
- machineconfig/scripts/python/mount_nfs.py +4 -3
- machineconfig/scripts/python/mount_nw_drive.py +4 -4
- machineconfig/scripts/python/mount_ssh.py +4 -3
- machineconfig/scripts/python/repos.py +9 -9
- machineconfig/scripts/python/scheduler.py +1 -1
- machineconfig/scripts/python/start_slidev.py +9 -8
- machineconfig/scripts/python/start_terminals.py +1 -1
- machineconfig/scripts/python/viewer.py +40 -40
- machineconfig/scripts/python/wifi_conn.py +65 -66
- machineconfig/scripts/python/wsl_windows_transfer.py +2 -2
- machineconfig/scripts/windows/checkout_version.ps1 +1 -3
- machineconfig/scripts/windows/choose_wezterm_theme.ps1 +1 -3
- machineconfig/scripts/windows/cloud_copy.ps1 +2 -6
- machineconfig/scripts/windows/cloud_manager.ps1 +1 -1
- machineconfig/scripts/windows/cloud_repo_sync.ps1 +1 -2
- machineconfig/scripts/windows/cloud_sync.ps1 +2 -2
- machineconfig/scripts/windows/croshell.ps1 +2 -2
- machineconfig/scripts/windows/devops.ps1 +1 -4
- machineconfig/scripts/windows/dotfile.ps1 +1 -3
- machineconfig/scripts/windows/fire.ps1 +1 -1
- machineconfig/scripts/windows/ftpx.ps1 +2 -2
- machineconfig/scripts/windows/gpt.ps1 +1 -1
- machineconfig/scripts/windows/kill_process.ps1 +1 -2
- machineconfig/scripts/windows/mcinit.ps1 +2 -2
- machineconfig/scripts/windows/mount_nfs.ps1 +1 -1
- machineconfig/scripts/windows/mount_ssh.ps1 +1 -1
- machineconfig/scripts/windows/pomodoro.ps1 +1 -1
- machineconfig/scripts/windows/py2exe.ps1 +1 -3
- machineconfig/scripts/windows/repos.ps1 +1 -1
- machineconfig/scripts/windows/scheduler.ps1 +1 -1
- machineconfig/scripts/windows/snapshot.ps1 +2 -2
- machineconfig/scripts/windows/start_slidev.ps1 +1 -1
- machineconfig/scripts/windows/start_terminals.ps1 +1 -1
- machineconfig/scripts/windows/wifi_conn.ps1 +1 -1
- machineconfig/scripts/windows/wsl_windows_transfer.ps1 +1 -3
- machineconfig/settings/lf/linux/lfrc +1 -1
- machineconfig/settings/linters/.ruff.toml +2 -2
- machineconfig/settings/linters/.ruff_cache/.gitignore +2 -0
- machineconfig/settings/linters/.ruff_cache/CACHEDIR.TAG +1 -0
- machineconfig/settings/lvim/windows/archive/config_additional.lua +1 -1
- machineconfig/settings/shells/ipy/profiles/default/startup/playext.py +71 -71
- machineconfig/settings/shells/wt/settings.json +8 -8
- machineconfig/settings/svim/linux/init.toml +1 -1
- machineconfig/settings/svim/windows/init.toml +1 -1
- machineconfig/setup_linux/web_shortcuts/croshell.sh +0 -54
- machineconfig/setup_linux/web_shortcuts/interactive.sh +6 -6
- machineconfig/setup_linux/web_shortcuts/tmp.sh +2 -0
- machineconfig/setup_windows/web_shortcuts/all.ps1 +2 -2
- machineconfig/setup_windows/web_shortcuts/ascii_art.ps1 +1 -1
- machineconfig/setup_windows/web_shortcuts/croshell.ps1 +1 -1
- machineconfig/setup_windows/web_shortcuts/interactive.ps1 +5 -5
- machineconfig/setup_windows/wt_and_pwsh/install_fonts.ps1 +51 -15
- machineconfig/setup_windows/wt_and_pwsh/set_pwsh_theme.py +75 -18
- machineconfig/setup_windows/wt_and_pwsh/set_wt_settings.py +52 -42
- machineconfig/utils/ai/browser_user_wrapper.py +5 -5
- machineconfig/utils/ai/generate_file_checklist.py +19 -22
- machineconfig/utils/ai/url2md.py +5 -3
- machineconfig/utils/cloud/onedrive/setup_oauth.py +5 -4
- machineconfig/utils/cloud/onedrive/transaction.py +192 -227
- machineconfig/utils/code.py +71 -43
- machineconfig/utils/installer.py +77 -85
- machineconfig/utils/installer_utils/installer_abc.py +29 -17
- machineconfig/utils/installer_utils/installer_class.py +188 -83
- machineconfig/utils/io_save.py +3 -15
- machineconfig/utils/links.py +22 -11
- machineconfig/utils/notifications.py +197 -0
- machineconfig/utils/options.py +38 -25
- machineconfig/utils/path.py +18 -6
- machineconfig/utils/path_reduced.py +637 -316
- machineconfig/utils/procs.py +69 -63
- machineconfig/utils/scheduling.py +11 -13
- machineconfig/utils/ssh.py +351 -0
- machineconfig/utils/terminal.py +225 -0
- machineconfig/utils/utils.py +13 -12
- machineconfig/utils/utils2.py +43 -10
- machineconfig/utils/utils5.py +242 -46
- machineconfig/utils/ve.py +11 -6
- {machineconfig-1.97.dist-info ā machineconfig-2.1.dist-info}/METADATA +15 -9
- {machineconfig-1.97.dist-info ā machineconfig-2.1.dist-info}/RECORD +232 -235
- machineconfig/cluster/self_ssh.py +0 -57
- machineconfig/jobs/__pycache__/__init__.cpython-311.pyc +0 -0
- machineconfig/jobs/python/__pycache__/__init__.cpython-311.pyc +0 -0
- machineconfig/jobs/python/archive/python_tools.txt +0 -12
- machineconfig/jobs/python/vscode/__pycache__/select_interpreter.cpython-311.pyc +0 -0
- machineconfig/jobs/python_custom_installers/__pycache__/__init__.cpython-311.pyc +0 -0
- machineconfig/jobs/python_generic_installers/__pycache__/__init__.cpython-311.pyc +0 -0
- machineconfig/jobs/python_generic_installers/update.py +0 -3
- machineconfig/jobs/python_linux_installers/__pycache__/__init__.cpython-311.pyc +0 -0
- machineconfig/profile/__pycache__/__init__.cpython-311.pyc +0 -0
- machineconfig/profile/__pycache__/create.cpython-311.pyc +0 -0
- machineconfig/profile/__pycache__/shell.cpython-311.pyc +0 -0
- machineconfig/scripts/__pycache__/__init__.cpython-311.pyc +0 -0
- machineconfig/scripts/linux/activate_ve +0 -87
- machineconfig/scripts/python/__pycache__/__init__.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/cloud_copy.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/cloud_mount.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/cloud_sync.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/croshell.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/devops.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/devops_backup_retrieve.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/devops_devapps_install.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/devops_update_repos.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/fire_agents.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/fire_jobs.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/get_zellij_cmd.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/repos.cpython-311.pyc +0 -0
- machineconfig/scripts/python/ai/__pycache__/init.cpython-311.pyc +0 -0
- machineconfig/scripts/python/ai/init.py +0 -56
- machineconfig/scripts/python/ai/rules/python/dev.md +0 -31
- machineconfig/scripts/python/helpers/__pycache__/__init__.cpython-311.pyc +0 -0
- machineconfig/scripts/python/helpers/__pycache__/cloud_helpers.cpython-311.pyc +0 -0
- machineconfig/scripts/python/helpers/__pycache__/helpers2.cpython-311.pyc +0 -0
- machineconfig/scripts/python/helpers/__pycache__/helpers4.cpython-311.pyc +0 -0
- machineconfig/scripts/python/helpers/__pycache__/repo_sync_helpers.cpython-311.pyc +0 -0
- machineconfig/scripts/windows/activate_ve.ps1 +0 -54
- {machineconfig-1.97.dist-info ā machineconfig-2.1.dist-info}/WHEEL +0 -0
- {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
|
+
"""
|
machineconfig/utils/utils.py
CHANGED
|
@@ -2,18 +2,17 @@
|
|
|
2
2
|
Utils
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
from machineconfig.utils.path_reduced import
|
|
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
|
|
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
|
-
_ =
|
|
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 =
|
|
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
|
|
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
|