machineconfig 1.96__py3-none-any.whl → 2.0__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 (164) hide show
  1. machineconfig/cluster/cloud_manager.py +22 -26
  2. machineconfig/cluster/data_transfer.py +2 -2
  3. machineconfig/cluster/distribute.py +0 -2
  4. machineconfig/cluster/file_manager.py +4 -4
  5. machineconfig/cluster/job_params.py +1 -1
  6. machineconfig/cluster/loader_runner.py +8 -8
  7. machineconfig/cluster/remote_machine.py +4 -4
  8. machineconfig/cluster/script_execution.py +2 -2
  9. machineconfig/cluster/sessions_managers/archive/create_zellij_template.py +1 -1
  10. machineconfig/cluster/sessions_managers/enhanced_command_runner.py +23 -23
  11. machineconfig/cluster/sessions_managers/wt_local.py +78 -76
  12. machineconfig/cluster/sessions_managers/wt_local_manager.py +91 -91
  13. machineconfig/cluster/sessions_managers/wt_remote.py +39 -39
  14. machineconfig/cluster/sessions_managers/wt_remote_manager.py +94 -91
  15. machineconfig/cluster/sessions_managers/wt_utils/layout_generator.py +56 -54
  16. machineconfig/cluster/sessions_managers/wt_utils/process_monitor.py +49 -49
  17. machineconfig/cluster/sessions_managers/wt_utils/remote_executor.py +18 -18
  18. machineconfig/cluster/sessions_managers/wt_utils/session_manager.py +42 -42
  19. machineconfig/cluster/sessions_managers/wt_utils/status_reporter.py +36 -36
  20. machineconfig/cluster/sessions_managers/zellij_local.py +43 -46
  21. machineconfig/cluster/sessions_managers/zellij_local_manager.py +139 -120
  22. machineconfig/cluster/sessions_managers/zellij_remote.py +35 -35
  23. machineconfig/cluster/sessions_managers/zellij_remote_manager.py +33 -33
  24. machineconfig/cluster/sessions_managers/zellij_utils/example_usage.py +15 -15
  25. machineconfig/cluster/sessions_managers/zellij_utils/layout_generator.py +25 -26
  26. machineconfig/cluster/sessions_managers/zellij_utils/process_monitor.py +49 -49
  27. machineconfig/cluster/sessions_managers/zellij_utils/remote_executor.py +5 -5
  28. machineconfig/cluster/sessions_managers/zellij_utils/session_manager.py +15 -15
  29. machineconfig/cluster/sessions_managers/zellij_utils/status_reporter.py +11 -11
  30. machineconfig/cluster/templates/utils.py +3 -3
  31. machineconfig/jobs/__pycache__/__init__.cpython-311.pyc +0 -0
  32. machineconfig/jobs/python/__pycache__/__init__.cpython-311.pyc +0 -0
  33. machineconfig/jobs/python/__pycache__/python_ve_symlink.cpython-311.pyc +0 -0
  34. machineconfig/jobs/python/check_installations.py +8 -9
  35. machineconfig/jobs/python/python_cargo_build_share.py +2 -2
  36. machineconfig/jobs/python/vscode/link_ve.py +7 -7
  37. machineconfig/jobs/python/vscode/select_interpreter.py +7 -7
  38. machineconfig/jobs/python/vscode/sync_code.py +5 -5
  39. machineconfig/jobs/python_custom_installers/archive/ngrok.py +2 -2
  40. machineconfig/jobs/python_custom_installers/dev/aider.py +3 -3
  41. machineconfig/jobs/python_custom_installers/dev/alacritty.py +3 -3
  42. machineconfig/jobs/python_custom_installers/dev/brave.py +3 -3
  43. machineconfig/jobs/python_custom_installers/dev/bypass_paywall.py +5 -5
  44. machineconfig/jobs/python_custom_installers/dev/code.py +3 -3
  45. machineconfig/jobs/python_custom_installers/dev/cursor.py +9 -9
  46. machineconfig/jobs/python_custom_installers/dev/docker_desktop.py +4 -4
  47. machineconfig/jobs/python_custom_installers/dev/espanso.py +4 -4
  48. machineconfig/jobs/python_custom_installers/dev/goes.py +4 -4
  49. machineconfig/jobs/python_custom_installers/dev/lvim.py +4 -4
  50. machineconfig/jobs/python_custom_installers/dev/nerdfont.py +3 -3
  51. machineconfig/jobs/python_custom_installers/dev/redis.py +3 -3
  52. machineconfig/jobs/python_custom_installers/dev/wezterm.py +3 -3
  53. machineconfig/jobs/python_custom_installers/dev/winget.py +27 -27
  54. machineconfig/jobs/python_custom_installers/docker.py +3 -3
  55. machineconfig/jobs/python_custom_installers/gh.py +7 -7
  56. machineconfig/jobs/python_custom_installers/hx.py +1 -1
  57. machineconfig/jobs/python_custom_installers/warp-cli.py +3 -3
  58. machineconfig/jobs/python_generic_installers/config.json +412 -389
  59. machineconfig/jobs/python_windows_installers/dev/config.json +1 -1
  60. machineconfig/logger.py +50 -0
  61. machineconfig/profile/__pycache__/__init__.cpython-311.pyc +0 -0
  62. machineconfig/profile/__pycache__/create.cpython-311.pyc +0 -0
  63. machineconfig/profile/__pycache__/shell.cpython-311.pyc +0 -0
  64. machineconfig/profile/create.py +23 -16
  65. machineconfig/profile/create_hardlinks.py +8 -8
  66. machineconfig/profile/shell.py +41 -37
  67. machineconfig/scripts/__pycache__/__init__.cpython-311.pyc +0 -0
  68. machineconfig/scripts/__pycache__/__init__.cpython-313.pyc +0 -0
  69. machineconfig/scripts/linux/devops +2 -2
  70. machineconfig/scripts/linux/fire +1 -0
  71. machineconfig/scripts/linux/fire_agents +0 -1
  72. machineconfig/scripts/linux/mcinit +27 -0
  73. machineconfig/scripts/python/__pycache__/__init__.cpython-311.pyc +0 -0
  74. machineconfig/scripts/python/__pycache__/__init__.cpython-313.pyc +0 -0
  75. machineconfig/scripts/python/__pycache__/croshell.cpython-311.pyc +0 -0
  76. machineconfig/scripts/python/__pycache__/devops.cpython-311.pyc +0 -0
  77. machineconfig/scripts/python/__pycache__/devops.cpython-313.pyc +0 -0
  78. machineconfig/scripts/python/__pycache__/devops_update_repos.cpython-311.pyc +0 -0
  79. machineconfig/scripts/python/__pycache__/devops_update_repos.cpython-313.pyc +0 -0
  80. machineconfig/scripts/python/__pycache__/fire_agents.cpython-311.pyc +0 -0
  81. machineconfig/scripts/python/__pycache__/fire_jobs.cpython-311.pyc +0 -0
  82. machineconfig/scripts/python/__pycache__/repos.cpython-311.pyc +0 -0
  83. machineconfig/scripts/python/ai/__pycache__/init.cpython-311.pyc +0 -0
  84. machineconfig/scripts/python/ai/__pycache__/mcinit.cpython-311.pyc +0 -0
  85. machineconfig/scripts/python/ai/chatmodes/Thinking-Beast-Mode.chatmode.md +337 -0
  86. machineconfig/scripts/python/ai/chatmodes/Ultimate-Transparent-Thinking-Beast-Mode.chatmode.md +644 -0
  87. machineconfig/scripts/python/ai/chatmodes/deepResearch.chatmode.md +81 -0
  88. machineconfig/scripts/python/ai/configs/.gemini/settings.json +81 -0
  89. machineconfig/scripts/python/ai/instructions/python/dev.instructions.md +45 -0
  90. machineconfig/scripts/python/ai/mcinit.py +103 -0
  91. machineconfig/scripts/python/ai/prompts/allLintersAndTypeCheckers.prompt.md +5 -0
  92. machineconfig/scripts/python/ai/prompts/research-report-skeleton.prompt.md +38 -0
  93. machineconfig/scripts/python/ai/scripts/lint_and_type_check.sh +47 -0
  94. machineconfig/scripts/python/archive/tmate_conn.py +5 -5
  95. machineconfig/scripts/python/archive/tmate_start.py +3 -3
  96. machineconfig/scripts/python/choose_wezterm_theme.py +2 -2
  97. machineconfig/scripts/python/cloud_copy.py +19 -18
  98. machineconfig/scripts/python/cloud_mount.py +9 -7
  99. machineconfig/scripts/python/cloud_repo_sync.py +11 -11
  100. machineconfig/scripts/python/cloud_sync.py +1 -1
  101. machineconfig/scripts/python/croshell.py +14 -14
  102. machineconfig/scripts/python/devops.py +6 -6
  103. machineconfig/scripts/python/devops_add_identity.py +8 -6
  104. machineconfig/scripts/python/devops_add_ssh_key.py +18 -18
  105. machineconfig/scripts/python/devops_backup_retrieve.py +13 -13
  106. machineconfig/scripts/python/devops_devapps_install.py +3 -3
  107. machineconfig/scripts/python/devops_update_repos.py +1 -1
  108. machineconfig/scripts/python/dotfile.py +2 -2
  109. machineconfig/scripts/python/fire_agents.py +183 -41
  110. machineconfig/scripts/python/fire_jobs.py +17 -11
  111. machineconfig/scripts/python/ftpx.py +2 -2
  112. machineconfig/scripts/python/gh_models.py +94 -94
  113. machineconfig/scripts/python/helpers/__pycache__/__init__.cpython-311.pyc +0 -0
  114. machineconfig/scripts/python/helpers/__pycache__/cloud_helpers.cpython-311.pyc +0 -0
  115. machineconfig/scripts/python/helpers/__pycache__/helpers2.cpython-311.pyc +0 -0
  116. machineconfig/scripts/python/helpers/__pycache__/helpers4.cpython-311.pyc +0 -0
  117. machineconfig/scripts/python/helpers/cloud_helpers.py +3 -3
  118. machineconfig/scripts/python/helpers/helpers2.py +1 -1
  119. machineconfig/scripts/python/helpers/helpers4.py +8 -6
  120. machineconfig/scripts/python/helpers/helpers5.py +7 -7
  121. machineconfig/scripts/python/helpers/repo_sync_helpers.py +1 -1
  122. machineconfig/scripts/python/mount_nfs.py +3 -2
  123. machineconfig/scripts/python/mount_nw_drive.py +4 -4
  124. machineconfig/scripts/python/mount_ssh.py +3 -2
  125. machineconfig/scripts/python/repos.py +8 -8
  126. machineconfig/scripts/python/scheduler.py +1 -1
  127. machineconfig/scripts/python/start_slidev.py +8 -7
  128. machineconfig/scripts/python/start_terminals.py +1 -1
  129. machineconfig/scripts/python/viewer.py +40 -40
  130. machineconfig/scripts/python/wifi_conn.py +65 -66
  131. machineconfig/scripts/python/wsl_windows_transfer.py +1 -1
  132. machineconfig/scripts/windows/mcinit.ps1 +4 -0
  133. machineconfig/settings/linters/.ruff.toml +2 -2
  134. machineconfig/settings/shells/ipy/profiles/default/startup/playext.py +71 -71
  135. machineconfig/settings/shells/wt/settings.json +8 -8
  136. machineconfig/setup_linux/web_shortcuts/tmp.sh +2 -0
  137. machineconfig/setup_windows/wt_and_pwsh/set_pwsh_theme.py +10 -7
  138. machineconfig/setup_windows/wt_and_pwsh/set_wt_settings.py +9 -7
  139. machineconfig/utils/ai/browser_user_wrapper.py +5 -5
  140. machineconfig/utils/ai/generate_file_checklist.py +11 -12
  141. machineconfig/utils/ai/url2md.py +1 -1
  142. machineconfig/utils/cloud/onedrive/setup_oauth.py +4 -4
  143. machineconfig/utils/cloud/onedrive/transaction.py +129 -129
  144. machineconfig/utils/code.py +13 -6
  145. machineconfig/utils/installer.py +51 -53
  146. machineconfig/utils/installer_utils/installer_abc.py +21 -10
  147. machineconfig/utils/installer_utils/installer_class.py +42 -16
  148. machineconfig/utils/io_save.py +3 -15
  149. machineconfig/utils/options.py +10 -3
  150. machineconfig/utils/path.py +5 -0
  151. machineconfig/utils/path_reduced.py +201 -149
  152. machineconfig/utils/procs.py +23 -23
  153. machineconfig/utils/scheduling.py +11 -12
  154. machineconfig/utils/ssh.py +270 -0
  155. machineconfig/utils/terminal.py +180 -0
  156. machineconfig/utils/utils.py +1 -2
  157. machineconfig/utils/utils2.py +43 -0
  158. machineconfig/utils/utils5.py +163 -34
  159. machineconfig/utils/ve.py +2 -2
  160. {machineconfig-1.96.dist-info → machineconfig-2.0.dist-info}/METADATA +13 -8
  161. {machineconfig-1.96.dist-info → machineconfig-2.0.dist-info}/RECORD +163 -144
  162. machineconfig/cluster/self_ssh.py +0 -57
  163. {machineconfig-1.96.dist-info → machineconfig-2.0.dist-info}/WHEEL +0 -0
  164. {machineconfig-1.96.dist-info → machineconfig-2.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,180 @@
1
+
2
+ from machineconfig.utils.path_reduced import P, OPLike
3
+ import subprocess
4
+ from typing import Any, BinaryIO, Optional, Union
5
+ import platform
6
+ import sys
7
+ import os
8
+ from typing import Literal, TypeAlias
9
+ from dataclasses import dataclass
10
+
11
+ SHELLS: TypeAlias = Literal["default", "cmd", "powershell", "pwsh", "bash"] # pwsh.exe is PowerShell (community) and powershell.exe is Windows Powershell (msft)
12
+ CONSOLE: TypeAlias = Literal["wt", "cmd"]
13
+ MACHINE: TypeAlias = Literal["Windows", "Linux", "Darwin"]
14
+
15
+
16
+ @dataclass
17
+ class STD:
18
+ stdin: str
19
+ stdout: str
20
+ stderr: str
21
+ returncode: int
22
+
23
+
24
+ class Response:
25
+ @staticmethod
26
+ def from_completed_process(cp: subprocess.CompletedProcess[str]):
27
+ resp = Response(cmd=cp.args)
28
+ resp.output.stdout = cp.stdout
29
+ resp.output.stderr = cp.stderr
30
+ resp.output.returncode = cp.returncode
31
+ return resp
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
+ def __call__(self, *args: Any, **kwargs: Any) -> Optional[str]:
38
+ _ = args, kwargs
39
+ return self.op.rstrip() if type(self.op) is str else None
40
+ @property
41
+ def op(self) -> str: return self.output.stdout
42
+ @property
43
+ def ip(self) -> str: return self.output.stdin
44
+ @property
45
+ def err(self) -> str: return self.output.stderr
46
+ @property
47
+ def returncode(self) -> int: return self.output.returncode
48
+ def op2path(self, strict_returncode: bool = True, strict_err: bool = False) -> Union[P, None]:
49
+ if self.is_successful(strict_returcode=strict_returncode, strict_err=strict_err): return P(self.op.rstrip())
50
+ return None
51
+ def op_if_successfull_or_default(self, strict_returcode: bool = True, strict_err: bool = False) -> Optional[str]: return self.op if self.is_successful(strict_returcode=strict_returcode, strict_err=strict_err) else None
52
+ def is_successful(self, strict_returcode: bool = True, strict_err: bool = False) -> bool:
53
+ return ((self.returncode in {0, None}) if strict_returcode else True) and (self.err == "" if strict_err else True)
54
+ def capture(self):
55
+ for key in ["stdin", "stdout", "stderr"]:
56
+ val: Optional[BinaryIO] = self.std[key]
57
+ if val is not None and val.readable():
58
+ self.output.__dict__[key] = val.read().decode().rstrip()
59
+ return self
60
+ def print_if_unsuccessful(self, desc: str = "TERMINAL CMD", strict_err: bool = False, strict_returncode: bool = False, assert_success: bool = False):
61
+ success = self.is_successful(strict_err=strict_err, strict_returcode=strict_returncode)
62
+ if assert_success: assert success, self.print(capture=False, desc=desc)
63
+ if success:
64
+ print(f"✅ {desc} completed successfully")
65
+ else:
66
+ self.print(capture=False, desc=desc)
67
+ return self
68
+ def print(self, desc: str = "TERMINAL CMD", capture: bool = True):
69
+ if capture: self.capture()
70
+ from rich import console
71
+ con = console.Console()
72
+ from rich.panel import Panel
73
+ from rich.text import Text # from rich.syntax import Syntax; syntax = Syntax(my_code, "python", theme="monokai", line_numbers=True)
74
+ tmp1 = Text("đŸ“Ĩ Input Command:\n")
75
+ tmp1.stylize("u bold blue")
76
+ tmp2 = Text("\n📤 Terminal Response:\n")
77
+ tmp2.stylize("u bold blue")
78
+ list_str = [f"{f' {idx} - {key} '}".center(40, "═") + f"\n{val}" for idx, (key, val) in enumerate(self.output.__dict__.items())]
79
+ txt = tmp1 + Text(str(self.input), style="white") + tmp2 + Text("\n".join(list_str), style="white")
80
+ con.print(Panel(txt, title=f"đŸ–Ĩī¸ {self.desc}", subtitle=f"📋 {desc}", width=150, style="bold cyan on black"))
81
+ return self
82
+
83
+ # DEPRECATED: Use subprocess.run directly instead of Terminal class.
84
+ # The Terminal class has been replaced with inline subprocess calls to underlying primitives.
85
+ # This file is kept for reference but should not be used.
86
+
87
+ class Terminal:
88
+ def __init__(self, stdout: Optional[int] = subprocess.PIPE, stderr: Optional[int] = subprocess.PIPE, stdin: Optional[int] = subprocess.PIPE, elevated: bool = False):
89
+ self.machine: str = platform.system()
90
+ self.elevated: bool = elevated
91
+ self.stdout = stdout
92
+ self.stderr = stderr
93
+ self.stdin = stdin
94
+ # def set_std_system(self): self.stdout = sys.stdout; self.stderr = sys.stderr; self.stdin = sys.stdin
95
+ def set_std_pipe(self):
96
+ self.stdout = subprocess.PIPE
97
+ self.stderr = subprocess.PIPE
98
+ self.stdin = subprocess.PIPE
99
+ def set_std_null(self):
100
+ self.stdout, self.stderr, self.stdin = subprocess.DEVNULL, subprocess.DEVNULL, subprocess.DEVNULL # Equivalent to `echo 'foo' &> /dev/null`
101
+ def run(self, *cmds: str, shell: Optional[SHELLS] = "default", check: bool = False, ip: Optional[str] = None) -> Response: # Runs SYSTEM commands like subprocess.run
102
+ """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.
103
+ * 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.
104
+ * `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.
105
+ * `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`"""
106
+ my_list = list(cmds) # `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.
107
+ if self.machine == "Windows" and shell in {"powershell", "pwsh"}: my_list = [shell, "-Command"] + my_list # alternatively, one can run "cmd"
108
+ if self.elevated is False or self.is_user_admin():
109
+ 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)
110
+ else:
111
+ resp = __import__("ctypes").windll.shell32.ShellExecuteW(None, "runas", sys.executable, " ".join(sys.argv), None, 1)
112
+ return Response.from_completed_process(resp)
113
+ def run_script(self, script: str, shell: SHELLS = "default", verbose: bool = False):
114
+ if self.machine == "Linux": script = "#!/bin/bash" + "\n" + script # `source` is only available in bash.
115
+ script_file = P.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])
116
+ if shell == "default":
117
+ if self.machine == "Windows":
118
+ start_cmd = "powershell" # default shell on Windows is cmd which is not very useful. (./source is not available)
119
+ full_command: Union[list[str], str] = [start_cmd, str(script_file)] # shell=True will cause this to be a string anyway (with space separation)
120
+ else:
121
+ start_cmd = "bash"
122
+ full_command = f"{start_cmd} {script_file}" # full_command = [start_cmd, str(script_file)]
123
+ else: full_command = f"{shell} {script_file}" # full_command = [shell, str(tmp_file)]
124
+ if verbose:
125
+ desc="Script to be executed:"
126
+ if platform.system() == "Windows": lexer = "powershell"
127
+ elif platform.system() == "Linux": lexer = "sh"
128
+ elif platform.system() == "Darwin": lexer = "sh" # macOS uses similar shell to Linux
129
+ else: raise NotImplementedError(f"Platform {platform.system()} not supported.")
130
+ from rich.console import Console
131
+ from rich.panel import Panel
132
+ from rich.syntax import Syntax
133
+ import rich.progress as pb
134
+ console = Console()
135
+ console.print(Panel(Syntax(code=script, lexer=lexer), title=f"📄 {desc}"), style="bold red")
136
+ with pb.Progress(transient=True) as progress:
137
+ _task = progress.add_task(f"Running Script @ {script_file}", total=None)
138
+ resp = subprocess.run(full_command, stderr=self.stderr, stdin=self.stdin, stdout=self.stdout, text=True, shell=True, check=False)
139
+ else: resp = subprocess.run(full_command, stderr=self.stderr, stdin=self.stdin, stdout=self.stdout, text=True, shell=True, check=False)
140
+ return Response.from_completed_process(resp)
141
+ 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.
142
+ 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 "")
143
+ py_script = P.tmpfile(name="tmp_python_script", suffix=".py", folder="tmp_scripts/terminal")
144
+ py_script.write_text(f"""print(r'''{script}''')""" + "\n" + script)
145
+ print(f"""🚀 [ASYNC PYTHON SCRIPT] Script URI:
146
+ {py_script.absolute().as_uri()}""")
147
+ print("Script to be executed asyncronously: ", py_script.absolute().as_uri())
148
+ shell_script = f"""
149
+ {f'cd {wdir}' if wdir is not None else ''}
150
+ {'ipython' if ipython else 'python'} {'-i' if interactive else ''} {py_script}
151
+ """
152
+ shell_script = P.tmpfile(name="tmp_shell_script", suffix=".sh" if self.machine == "Linux" else ".ps1", folder="tmp_scripts/shell").write_text(shell_script)
153
+ if shell is None and self.machine == "Windows": shell = "pwsh"
154
+ window = "start" if new_window and self.machine == "Windows" else ""
155
+ os.system(f"{window} {terminal} {shell} {shell_script}")
156
+ @staticmethod
157
+ def is_user_admin() -> bool: # adopted from: https://stackoverflow.com/questions/19672352/how-to-run-script-with-elevated-privilege-on-windows"""
158
+ if os.name == 'nt':
159
+ try: return __import__("ctypes").windll.shell32.IsUserAnAdmin()
160
+ except Exception:
161
+ import traceback
162
+ traceback.print_exc()
163
+ print("Admin check failed, assuming not an admin.")
164
+ return False
165
+ else:
166
+ return os.getuid() == 0 # Check for root on Posix
167
+ # @staticmethod
168
+ # def run_as_admin(file: PLike, params: Any, wait: bool = False):
169
+ # proce_info = install_n_import(library="win32com", package="pywin32", fromlist=["shell.shell.ShellExecuteEx"]).shell.shell.ShellExecuteEx(lpVerb='runas', lpFile=file, lpParameters=params)
170
+ # # TODO update PATH for this to take effect immediately.
171
+ # if wait: time.sleep(1)
172
+ # return proce_info
173
+
174
+ @staticmethod
175
+ def get_header(wdir: OPLike, toolbox: bool): return f"""
176
+ # >> Code prepended
177
+ {"from crocodile.toolbox import *" if toolbox else "# No toolbox import."}
178
+ {'''sys.path.insert(0, r'{wdir}') ''' if wdir is not None else "# No path insertion."}
179
+ # >> End of header, start of script passed
180
+ """
@@ -3,7 +3,6 @@ Utils
3
3
  """
4
4
 
5
5
  from machineconfig.utils.path_reduced import P as PathExtended
6
- # import crocodile.environment as env
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
@@ -50,7 +49,7 @@ DEFAULTS_PATH = PathExtended.home().joinpath("dotfiles/machineconfig/defaults.in
50
49
 
51
50
 
52
51
 
53
- def check_dotfiles_version_is_beyond(commit_dtm: str, update: bool=False):
52
+ def check_dotfiles_version_is_beyond(commit_dtm: str, update: bool) -> bool:
54
53
  dotfiles_path = str(PathExtended.home().joinpath("dotfiles"))
55
54
  from git import Repo
56
55
  repo = Repo(path=dotfiles_path)
@@ -2,6 +2,9 @@
2
2
 
3
3
  from pathlib import Path
4
4
  from typing import Optional, Any
5
+ # import time
6
+ # from typing import Callable, Literal, TypeVar, ParamSpec
7
+
5
8
 
6
9
  def randstr(length: int = 10, lower: bool = True, upper: bool = True, digits: bool = True, punctuation: bool = False, safe: bool = False, noun: bool = False) -> str:
7
10
  if safe:
@@ -40,3 +43,43 @@ def pprint(obj: dict[Any, Any], title: str) -> None:
40
43
  inspect(type("TempStruct", (object,), obj)(), value=False, title=title, docs=False, dunder=False, sort=False)
41
44
  def get_repr(obj: dict[Any, Any], sep: str = "\n", justify: int = 15, quotes: bool = False):
42
45
  return sep.join([f"{key:>{justify}} = {repr(val) if quotes else val}" for key, val in obj.items()])
46
+
47
+
48
+ # T = TypeVar('T')
49
+ # PS = ParamSpec('PS')
50
+
51
+ # class RepeatUntilNoException:
52
+ # """
53
+ # Repeat function calling if it raised an exception and/or exceeded the timeout, for a maximum of `retry` times.
54
+ # * Alternative: `https://github.com/jd/tenacity`
55
+ # """
56
+ # def __init__(self, retry: int, sleep: float, timeout: Optional[float] = None, scaling: Literal["linear", "exponential"] = "exponential"):
57
+ # self.retry = retry
58
+ # self.sleep = sleep
59
+ # self.timeout = timeout
60
+ # self.scaling: Literal["linear", "exponential"] = scaling
61
+ # def __call__(self, func: Callable[PS, T]) -> Callable[PS, T]:
62
+ # from functools import wraps
63
+ # if self.timeout is not None:
64
+ # import warpt_time_decorator
65
+ # func = wrapt_timeout_decorator.timeout(self.timeout)(func)
66
+ # @wraps(wrapped=func)
67
+ # def wrapper(*args: PS.args, **kwargs: PS.kwargs):
68
+ # t0 = time.time()
69
+ # for idx in range(self.retry):
70
+ # try:
71
+ # return func(*args, **kwargs)
72
+ # except Exception as ex:
73
+ # match self.scaling:
74
+ # case "linear":
75
+ # sleep_time = self.sleep * (idx + 1)
76
+ # case "exponential":
77
+ # sleep_time = self.sleep * (idx + 1)**2
78
+ # print(f"""đŸ’Ĩ [RETRY] Function {func.__name__} call failed with error:
79
+ # {ex}
80
+ # Retry count: {idx}/{self.retry}. Sleeping for {sleep_time} seconds.
81
+ # Total elapsed time: {time.time() - t0:0.1f} seconds.""")
82
+ # print(f"""đŸ’Ĩ Robust call of `{func}` failed with ```{ex}```.\nretrying {idx}/{self.retry} more times after sleeping for {sleep_time} seconds.\nTotal wait time so far {time.time() - t0: 0.1f} seconds.""")
83
+ # time.sleep(sleep_time)
84
+ # raise RuntimeError(f"đŸ’Ĩ Robust call failed after {self.retry} retries and total wait time of {time.time() - t0: 0.1f} seconds.\n{func=}\n{args=}\n{kwargs=}")
85
+ # return wrapper
@@ -1,39 +1,52 @@
1
1
 
2
- from typing import Callable, Optional, Union, Any
3
- from logging import Logger as Log
4
- from machineconfig.utils.utils2 import randstr, get_repr
2
+ from typing import Callable, Optional, Union, Any, NoReturn, TypeVar, Protocol, List
3
+ import logging
5
4
  import time
6
- from datetime import datetime, timezone
5
+ from datetime import datetime, timezone, timedelta
6
+
7
+
8
+ class LoggerTemplate(Protocol):
9
+ handlers: List[logging.Handler]
10
+ def debug(self, msg: str) -> None:
11
+ pass # 10
12
+ def info(self, msg: str) -> None:
13
+ pass # 20
14
+ def warning(self, msg: str) -> None:
15
+ pass # 30
16
+ def error(self, msg: str) -> None:
17
+ pass # 40
18
+ def critical(self, msg: str) -> None:
19
+ pass # 50
20
+ def fatal(self, msg: str) -> None:
21
+ pass # 50
7
22
 
8
23
 
9
24
  class Scheduler:
10
- def __init__(self, routine: Callable[['Scheduler'], Any],
11
- wait_ms: int,
25
+ def __init__(self, routine: Callable[['Scheduler'], Any], wait_ms: int,
26
+ logger: LoggerTemplate, sess_stats: Optional[Callable[['Scheduler'], dict[str, Any]]] = None,
12
27
  exception_handler: Optional[Callable[[Union[Exception, KeyboardInterrupt], str, 'Scheduler'], Any]] = None,
13
- logger: Optional[Log] = None,
14
- sess_stats: Optional[Callable[['Scheduler'], dict[str, Any]]] = None,
15
- max_cycles: int = 1_000_000_000,
16
- records: Optional[list[list[Any]]] = None):
28
+ max_cycles: int = 1_000_000_000, records: Optional[list[list[Any]]] = None):
17
29
  self.routine = routine # main routine to be repeated every `wait` time period
18
- self.logger = logger if logger is not None else Log(name="SchedLogger_" + randstr(noun=True))
30
+ self.logger = logger
19
31
  self.exception_handler = exception_handler if exception_handler is not None else self.default_exception_handler
20
32
  self.records: list[list[Any]] = records if records is not None else []
21
33
  self.wait_ms = wait_ms # wait period between routine cycles.
22
34
  self.cycle: int = 0
23
35
  self.max_cycles: int = max_cycles
24
- self.sess_start_time_ms: int
36
+ self.sess_start_utc_ms: int
25
37
  self.sess_stats = sess_stats or (lambda _sched: {})
26
- def __repr__(self): return f"Scheduler with {self.cycle} cycles ran so far. Last cycle was at {self.sess_start_time_ms}."
38
+ def __repr__(self): return f"Scheduler with {self.cycle} cycles ran so far. Last cycle was at {self.sess_start_utc_ms}."
27
39
  def run(self, max_cycles: Optional[int]=None, until_ms: Optional[int]=None):
28
40
  if max_cycles is not None:
29
41
  self.max_cycles = max_cycles
30
42
  if until_ms is None:
31
- until_ms = 1_000_000_000_000
32
- self.sess_start_time_ms = time.time_ns() // 1_000_000
43
+ until_ms = 1_000_000_000_000_000
44
+ self.sess_start_utc_ms = time.time_ns() // 1_000_000
33
45
  while (time.time_ns() // 1_000_000) < until_ms and self.cycle < self.max_cycles:
34
46
  # 1- Time before Ops, and Opening Message
35
47
  time1_ms = time.time_ns() // 1_000_000
36
- self.logger.info(f"Starting Cycle {str(self.cycle).zfill(5)}. Total Run Time = {str(time1_ms - self.sess_start_time_ms).split('.', maxsplit=1)[0]}. UTC🕜 {datetime.now(tz=timezone.utc).strftime('%d %H:%M:%S')}")
48
+ duration = timedelta(milliseconds=time1_ms - self.sess_start_utc_ms)
49
+ self.logger.info(f"Starting Cycle {str(self.cycle).zfill(5)}. Total Run Time = {str(duration).split('.', maxsplit=1)[0]}. UTC🕜 {datetime.now(tz=timezone.utc).strftime('%d %H:%M:%S')}")
37
50
  try:
38
51
  self.routine(self)
39
52
  except Exception as ex:
@@ -41,44 +54,160 @@ class Scheduler:
41
54
  time2_ms = time.time_ns() // 1_000_000
42
55
  time_left_ms = int(self.wait_ms - (time2_ms - time1_ms)) # 4- Conclude Message
43
56
  self.cycle += 1
44
- self.logger.info(f"Finishing Cycle {str(self.cycle - 1).zfill(5)} in {str((time2_ms - time1_ms)*0.001).split('.', maxsplit=1)[0]}s. Sleeping for {self.wait_ms*0.001:0.1f}s ({time_left_ms*0.001:0.1}s left)\n" + "-" * 100)
57
+ self.logger.info(f"Finishing Cycle {str(self.cycle - 1).zfill(5)} in {str((time2_ms - time1_ms)*0.001).split('.', maxsplit=1)[0]}s. Sleeping for {self.wait_ms*0.001:0.1f}s ({time_left_ms*0.001:0.1f}s left)\n" + "-" * 100)
45
58
  try: time.sleep(time_left_ms*0.001 if time_left_ms > 0 else 0.0) # # 5- Sleep. consider replacing by Asyncio.sleep
46
59
  except KeyboardInterrupt as ex:
47
60
  self.exception_handler(ex, "sleep", self)
48
61
  return # that's probably the only kind of exception that can rise during sleep.
49
62
  self.record_session_end(reason=f"Reached maximum number of cycles ({self.max_cycles})" if self.cycle >= self.max_cycles else f"Reached due stop time ({until_ms})")
50
- def get_records_df(self):
51
- import polars as pl
52
- columns = ["start", "finish", "duration", "cycles", "termination reason", "logfile"] + list(self.sess_stats(self).keys())
53
- return pl.DataFrame(self.records, schema=columns)
63
+ def get_records_df(self) -> List[dict[str, Any]]:
64
+ columns = ["start", "finish", "duration", "cycles", "termination reason"] + list(self.sess_stats(self).keys())
65
+ return [dict(zip(columns, row)) for row in self.records]
54
66
  def record_session_end(self, reason: str):
55
- import polars as pl
56
67
  end_time_ms = time.time_ns() // 1_000_000
57
- duration_ms = end_time_ms - self.sess_start_time_ms
68
+ duration_ms = end_time_ms - self.sess_start_utc_ms
58
69
  sess_stats = self.sess_stats(self)
59
- self.records.append([self.sess_start_time_ms, end_time_ms, duration_ms, self.cycle, reason,
70
+ self.records.append([self.sess_start_utc_ms, end_time_ms, duration_ms, self.cycle, reason,
60
71
  # self.logger.file_path
61
72
  ] + list(sess_stats.values()))
62
- summ = {"start time": f"{str(self.sess_start_time_ms)}",
73
+ records_df = self.get_records_df()
74
+ total_cycles = sum(row["cycles"] for row in records_df)
75
+ summ = {"start time": f"{str(self.sess_start_utc_ms)}",
63
76
  "finish time": f"{str(end_time_ms)}.",
64
77
  "duration": f"{str(duration_ms)} | wait time {self.wait_ms/1_000: 0.1f}s",
65
- "cycles ran": f"{self.cycle} | Lifetime cycles = {self.get_records_df().select(pl.col('cycles').sum()).item()}",
78
+ "cycles ran": f"{self.cycle} | Lifetime cycles = {total_cycles}",
66
79
  "termination reason": reason,
67
80
  # "logfile": self.logger.file_path
68
81
  }
69
82
  summ.update(sess_stats)
83
+ from machineconfig.utils.utils2 import get_repr
70
84
  tmp = get_repr(summ)
71
85
  self.logger.critical("\n--> Scheduler has finished running a session. \n" + tmp + "\n" + "-" * 100)
72
- df = self.get_records_df()
73
- df = df.with_columns([
74
- pl.col("start").map_elements(lambda x: str(x).split(".", maxsplit=1)[0], return_dtype=pl.String),
75
- pl.col("finish").map_elements(lambda x: str(x).split(".", maxsplit=1)[0], return_dtype=pl.String),
76
- pl.col("duration").map_elements(lambda x: str(x).split(".", maxsplit=1)[0], return_dtype=pl.String)
77
- ])
78
- self.logger.critical("\n--> Logger history.\n" + str(df))
86
+ # Format records as table
87
+ if records_df:
88
+ headers = list(records_df[0].keys())
89
+ # Process start, finish, duration to strings without milliseconds
90
+ processed_records = []
91
+ for row in records_df:
92
+ processed = row.copy()
93
+ processed["start"] = str(row["start"]).split(".", maxsplit=1)[0]
94
+ processed["finish"] = str(row["finish"]).split(".", maxsplit=1)[0]
95
+ processed["duration"] = str(row["duration"]).split(".", maxsplit=1)[0]
96
+ processed_records.append(processed)
97
+ # Simple aligned table formatting
98
+ max_lengths = {col: max(len(str(row.get(col, ""))) for row in processed_records) for col in headers}
99
+ table_lines = ["| " + " | ".join(col.ljust(max_lengths[col]) for col in headers) + " |"]
100
+ table_lines.append("|" + "-+-".join("-" * max_lengths[col] for col in headers) + "|")
101
+ for row in processed_records:
102
+ table_lines.append("| " + " | ".join(str(row.get(col, "")).ljust(max_lengths[col]) for col in headers) + " |")
103
+ table_str = "\n".join(table_lines)
104
+ else:
105
+ table_str = "No records available."
106
+ self.logger.critical("\n--> Logger history.\n" + table_str)
79
107
  return self
80
108
  def default_exception_handler(self, ex: Union[Exception, KeyboardInterrupt], during: str, sched: 'Scheduler') -> None: # user decides on handling and continue, terminate, save checkpoint, etc. # Use signal library.
81
109
  print(sched)
82
110
  self.record_session_end(reason=f"during {during}, " + str(ex))
83
- self.logger.exception(ex)
111
+ self.logger.fatal(str(ex))
84
112
  raise ex
113
+
114
+
115
+ T = TypeVar('T')
116
+ T2 = TypeVar('T2')
117
+ class PrintFunc(Protocol):
118
+ def __call__(self, msg: str) -> Union[NoReturn, None]: ...
119
+
120
+
121
+ # class CacheV2(Generic[T]):
122
+ # def __init__(self, source_func: Callable[[], T],
123
+ # expire_ms: int, logger: Optional[PrintFunc] = None, path: OPLike = None,
124
+ # saver: Callable[[T, PLike], Any] = Save.pickle, reader: Callable[[PLike], T] = Read.pickle, name: Optional[str] = None) -> None:
125
+ # self.cache: Optional[T] = None
126
+ # self.source_func = source_func
127
+ # self.path: Optional[P] = P(path) if path else None
128
+ # self.time_produced = time.time_ns() // 1_000_000
129
+ # self.save = saver
130
+ # self.reader = reader
131
+ # self.logger = logger
132
+ # self.expire = expire_ms # in milliseconds
133
+ # self.name = name if isinstance(name, str) else str(self.source_func)
134
+ # @property
135
+ # def age(self):
136
+ # if self.path is None:
137
+ # return time.time_ns() // 1_000_000 - self.time_produced
138
+ # return time.time_ns() // 1_000_000 - int(self.path.stat().st_mtime * 1000)
139
+ # def __setstate__(self, state: dict[str, Any]) -> None:
140
+ # self.__dict__.update(state)
141
+ # self.path = P.home() / self.path if self.path is not None else self.path
142
+ # def __getstate__(self) -> dict[str, Any]:
143
+ # state = self.__dict__.copy()
144
+ # state["path"] = self.path.relative_to(P.home()) if self.path is not None else state["path"]
145
+ # return state
146
+ # def __call__(self, fresh: bool = False) -> T:
147
+ # if fresh or self.cache is None:
148
+ # if not fresh and self.path is not None and self.path.exists():
149
+ # age = time.time_ns() // 1_000_000 - int(self.path.stat().st_mtime * 1000)
150
+ # msg1 = f"""
151
+ # đŸ“Ļ ════════════════════ CACHE V2 OPERATION ════════════════════
152
+ # 🔄 {self.name} cache: Reading cached values from `{self.path}`
153
+ # âąī¸ Lag = {age} ms
154
+ # ════════════════════════════════════════════════════════════════"""
155
+ # try:
156
+ # self.cache = self.reader(self.path)
157
+ # except Exception as ex:
158
+ # if self.logger:
159
+ # msg2 = f"""
160
+ # ❌ ════════════════════ CACHE V2 ERROR ════════════════════
161
+ # âš ī¸ {self.name} cache: Cache file is corrupted
162
+ # 🔍 Error: {ex}
163
+ # ════════════════════════════════════════════════════════════"""
164
+ # self.logger(msg1 + msg2)
165
+ # self.cache = self.source_func()
166
+ # self.save(self.cache, self.path)
167
+ # return self.cache
168
+ # return self(fresh=False)
169
+ # else:
170
+ # if self.logger:
171
+ # self.logger(f"""
172
+ # 🆕 ════════════════════ NEW CACHE V2 ════════════════════
173
+ # 🔄 {self.name} cache: Populating fresh cache from source func
174
+ # â„šī¸ Reason: Previous cache never existed or there was an explicit fresh order
175
+ # ════════════════════════════════════════════════════════════""")
176
+ # self.cache = self.source_func()
177
+ # if self.path is None:
178
+ # self.time_produced = time.time_ns() // 1_000_000
179
+ # else:
180
+ # self.save(self.cache, self.path)
181
+ # else:
182
+ # try:
183
+ # age = self.age
184
+ # except AttributeError:
185
+ # self.cache = None
186
+ # return self(fresh=fresh)
187
+ # if age > self.expire:
188
+ # if self.logger:
189
+ # self.logger(f"""
190
+ # 🔄 ════════════════════ CACHE V2 UPDATE ════════════════════
191
+ # âš ī¸ {self.name} cache: Updating cache from source func
192
+ # âąī¸ Age = {age} ms > {self.expire} ms
193
+ # ════════════════════════════════════════════════════════════""")
194
+ # self.cache = self.source_func()
195
+ # if self.path is None:
196
+ # self.time_produced = time.time_ns() // 1_000_000
197
+ # else:
198
+ # self.save(self.cache, self.path)
199
+ # else:
200
+ # if self.logger:
201
+ # self.logger(f"""
202
+ # ✅ ════════════════════ USING CACHE V2 ════════════════════
203
+ # đŸ“Ļ {self.name} cache: Using cached values
204
+ # âąī¸ Lag = {age} ms
205
+ # ════════════════════════════════════════════════════════════""")
206
+ # return self.cache
207
+ # @staticmethod
208
+ # def as_decorator(expire: int = 60000, logger: Optional[PrintFunc] = None, path: OPLike = None,
209
+ # name: Optional[str] = None):
210
+ # def decorator(source_func: Callable[[], T2]) -> CacheV2['T2']:
211
+ # res = CacheV2(source_func=source_func, expire_ms=expire, logger=logger, path=path, name=name)
212
+ # return res
213
+ # return decorator
machineconfig/utils/ve.py CHANGED
@@ -19,10 +19,10 @@ def get_ve_path_and_ipython_profile(init_path: PathExtended) -> tuple[Optional[s
19
19
  ipy_profile = ini["specs"]["ipy_profile"]
20
20
  print(f"✨ Using IPython profile: {ipy_profile}")
21
21
  if ipy_profile is None and tmp.joinpath(".ipy_profile").exists():
22
- ipy_profile = tmp.joinpath(".ipy_profile").read_text().rstrip()
22
+ ipy_profile = tmp.joinpath(".ipy_profile").read_text(encoding="utf-8").rstrip()
23
23
  print(f"✨ Using IPython profile: {ipy_profile}. This is based on this file {tmp.joinpath('.ipy_profile')}")
24
24
  if ve_path is None and tmp.joinpath(".ve_path").exists():
25
- ve_path = tmp.joinpath(".ve_path").read_text().rstrip().replace("\n", "")
25
+ ve_path = tmp.joinpath(".ve_path").read_text(encoding="utf-8").rstrip().replace("\n", "")
26
26
  print(f"🔮 Using Virtual Environment found @ {tmp}/.ve_path: {ve_path}")
27
27
  if ve_path is None and tmp.joinpath(".venv").exists():
28
28
  print(f"🔮 Using Virtual Environment found @ {tmp}/.venv")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: machineconfig
3
- Version: 1.96
3
+ Version: 2.0
4
4
  Summary: Dotfiles management package
5
5
  Author-email: Alex Al-Saffar <programmer@usa.com>
6
6
  License: Apache 2.0
@@ -9,22 +9,27 @@ Project-URL: Bug Tracker, https://github.com/thisismygitrepo/machineconfig/issue
9
9
  Classifier: Programming Language :: Python :: 3
10
10
  Classifier: License :: OSI Approved :: Apache Software License
11
11
  Classifier: Operating System :: OS Independent
12
- Requires-Python: >=3.11
12
+ Requires-Python: >=3.13
13
13
  Description-Content-Type: text/markdown
14
- Requires-Dist: crocodile
15
14
  Requires-Dist: rich>=14.0.0
16
15
  Requires-Dist: paramiko>=3.5.1
17
16
  Requires-Dist: psutil>=7.0.0
18
- Requires-Dist: openai>=1.75.0
19
- Requires-Dist: nbformat>=5.10.4
20
17
  Requires-Dist: fire>=0.7.0
21
18
  Requires-Dist: pydantic>=2.11.3
22
- Requires-Dist: clipboard>=0.0.4
23
19
  Requires-Dist: gitpython>=3.1.44
24
- Requires-Dist: pudb>=2024.1.3
25
20
  Requires-Dist: pyfzf>=0.3.1
26
- Requires-Dist: call-function-with-timeout>=1.1.1
27
21
  Requires-Dist: rclone-python>=0.1.23
22
+ Requires-Dist: pytz>=2025.2
23
+ Requires-Dist: tomli>=2.2.1
24
+ Requires-Dist: toml>=0.10.2
25
+ Requires-Dist: pyyaml>=6.0.2
26
+ Requires-Dist: pyjson5>=1.6.9
27
+ Requires-Dist: requests>=2.32.5
28
+ Requires-Dist: tqdm>=4.67.1
29
+ Requires-Dist: joblib>=1.5.2
30
+ Requires-Dist: randomname>=0.2.1
31
+ Requires-Dist: cryptography>=44.0.2
32
+ Requires-Dist: tenacity>=9.1.2
28
33
  Provides-Extra: windows
29
34
  Requires-Dist: pywin32; extra == "windows"
30
35
  Provides-Extra: docs