machineconfig 6.61__py3-none-any.whl → 6.62__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.

@@ -183,7 +183,7 @@ def ftpx(
183
183
  padding=(1, 2),
184
184
  )
185
185
  )
186
- received_file = ssh.copy_from_here(source_path=resolved_source, target_path=resolved_target, compress_with_zip=zipFirst, recursive=recursive, overwrite_existing=False)
186
+ received_file = ssh.copy_from_here(source_path=resolved_source, target_rel2home=resolved_target, compress_with_zip=zipFirst, recursive=recursive, overwrite_existing=False)
187
187
 
188
188
  if source_is_remote and isinstance(received_file, PathExtended):
189
189
  console.print(
@@ -1,7 +1,7 @@
1
1
 
2
2
 
3
3
  import typer
4
- from typing import Annotated, Optional
4
+ from typing import Annotated, Optional, TypedDict
5
5
  from pathlib import Path
6
6
  import subprocess
7
7
  import requests
@@ -115,3 +115,28 @@ def merge_pdfs(
115
115
  from machineconfig.utils.code import run_shell_script, get_uv_command_executing_python_script
116
116
  uv_command, _py_file = get_uv_command_executing_python_script(python_script=code, uv_with=["pypdf"], uv_project_dir=None)
117
117
  run_shell_script(uv_command)
118
+
119
+
120
+ class MachineSpecs(TypedDict):
121
+ system: str
122
+ distro: str
123
+ home_dir: str
124
+ def get_machine_specs() -> MachineSpecs:
125
+ """Write print and return the local machine specs."""
126
+ import platform
127
+ UV_RUN_CMD = "$HOME/.local/bin/uv run" if platform.system() != "Windows" else """& "$env:USERPROFILE/.local/bin/uv" run"""
128
+ command = f"""{UV_RUN_CMD} --with distro python -c "import distro; print(distro.name(pretty=True))" """
129
+ import subprocess
130
+ distro = subprocess.run(command, shell=True, capture_output=True, text=True).stdout.strip()
131
+ specs: MachineSpecs = {
132
+ "system": platform.system(),
133
+ "distro": distro,
134
+ "home_dir": str(Path.home()),
135
+ }
136
+ print(specs)
137
+ from machineconfig.utils.source_of_truth import CONFIG_ROOT
138
+ path = CONFIG_ROOT.joinpath("machine_specs.json")
139
+ CONFIG_ROOT.mkdir(parents=True, exist_ok=True)
140
+ import json
141
+ path.write_text(json.dumps(specs, indent=4), encoding="utf-8")
142
+ return specs
@@ -1,6 +1,6 @@
1
1
 
2
2
 
3
- from machineconfig.scripts.python.helpers_devops.cli_utils import download, merge_pdfs
3
+ from machineconfig.scripts.python.helpers_devops.cli_utils import download, merge_pdfs, get_machine_specs
4
4
  import typer
5
5
 
6
6
 
@@ -11,6 +11,8 @@ def get_app() -> typer.Typer:
11
11
  app.command(name="d", no_args_is_help=True, hidden=True)(download)
12
12
  app.command(name="merge-pdfs", no_args_is_help=True, help="[m] Merge two PDF files into one.")(merge_pdfs)
13
13
  app.command(name="m", no_args_is_help=True, hidden=True)(merge_pdfs)
14
+ app.command(name="get-machine-specs", no_args_is_help=False, help="[g] Get machine specifications.")(get_machine_specs)
15
+ app.command(name="g", no_args_is_help=False, hidden=True)(get_machine_specs)
14
16
  return app
15
17
 
16
18
 
@@ -1,16 +1,16 @@
1
- from typing import Callable, Optional, Any, Union
1
+ from typing import Callable, Optional, Any, Union, cast
2
2
  import os
3
3
  from pathlib import Path
4
4
  import platform
5
+ from machineconfig.scripts.python.helpers_devops.cli_utils import MachineSpecs
5
6
  import rich.console
6
- from machineconfig.utils.terminal import Response, MACHINE
7
- from machineconfig.utils.accessories import pprint
8
-
7
+ from machineconfig.utils.terminal import Response
8
+ from machineconfig.utils.accessories import pprint, randstr
9
+ from machineconfig.utils.meta import lambda_to_python_script
9
10
  UV_RUN_CMD = "$HOME/.local/bin/uv run" if platform.system() != "Windows" else """& "$env:USERPROFILE/.local/bin/uv" run"""
10
11
  MACHINECONFIG_VERSION = "machineconfig>=6.61"
11
12
  DEFAULT_PICKLE_SUBDIR = "tmp_results/tmp_scripts/ssh"
12
13
 
13
-
14
14
  class SSH:
15
15
  def __init__(
16
16
  self, host: Optional[str], username: Optional[str], hostname: Optional[str], ssh_key_path: Optional[str], password: Optional[str], port: int, enable_compression: bool):
@@ -22,7 +22,6 @@ class SSH:
22
22
  self.username: str
23
23
  self.port: int = port
24
24
  self.proxycommand: Optional[str] = None
25
- import platform
26
25
  import paramiko # type: ignore
27
26
  import getpass
28
27
 
@@ -109,13 +108,15 @@ class SSH:
109
108
  def view_bar(self, transferred: int, total: int) -> None:
110
109
  if self.progress and self.task is not None:
111
110
  self.progress.update(self.task, completed=transferred, total=total)
112
-
113
111
  self.tqdm_wrap = RichProgressWrapper
114
- self._local_distro: Optional[str] = None
115
- self._remote_distro: Optional[str] = None
116
- self._remote_machine: Optional[MACHINE] = None
112
+ from machineconfig.scripts.python.helpers_devops.cli_utils import get_machine_specs
113
+ self.local_specs: MachineSpecs = get_machine_specs()
114
+ resp = self.run_shell(command=f"""~/.local/bin/utils get-machine-specs """, verbose_output=False, description="Getting remote machine specs", strict_stderr=False, strict_return_code=False)
115
+ import json
116
+ json_str = resp.op
117
+ print(f"Remote machine specs JSON: {resp}")
118
+ self.remote_specs: MachineSpecs = cast(MachineSpecs, json.loads(json_str))
117
119
  self.terminal_responses: list[Response] = []
118
- self.platform = platform
119
120
 
120
121
  def __enter__(self) -> "SSH":
121
122
  return self
@@ -126,34 +127,11 @@ class SSH:
126
127
  self.sftp.close()
127
128
  self.sftp = None
128
129
  self.ssh.close()
129
- def get_remote_machine(self) -> MACHINE:
130
- if self._remote_machine is None:
131
- windows_test1 = self.run_shell(command="$env:OS", verbose_output=False, description="Testing Remote OS Type", strict_stderr=False, strict_return_code=False).op
132
- windows_test2 = self.run_shell(command="echo %OS%", verbose_output=False, description="Testing Remote OS Type Again", strict_stderr=False, strict_return_code=False).op
133
- if windows_test1 == "Windows_NT" or windows_test2 == "Windows_NT":
134
- self._remote_machine = "Windows"
135
- else:
136
- self._remote_machine = "Linux"
137
- return self._remote_machine
138
- def get_local_distro(self) -> str:
139
- if self._local_distro is None:
140
- command = f"""{UV_RUN_CMD} --with distro python -c "import distro; print(distro.name(pretty=True))" """
141
- import subprocess
142
- res = subprocess.run(command, shell=True, capture_output=True, text=True).stdout.strip()
143
- self._local_distro = res
144
- return res
145
- return self._local_distro
146
- def get_remote_distro(self) -> str:
147
- if self._remote_distro is None:
148
- command_str = f"""{UV_RUN_CMD} --with distro python -c "import distro; print(distro.name(pretty=True))" """
149
- res = self.run_shell(command=command_str, verbose_output=True, description="", strict_stderr=False, strict_return_code=False)
150
- self._remote_distro = res.op_if_successfull_or_default() or ""
151
- return self._remote_distro
152
130
  def restart_computer(self) -> Response:
153
- return self.run_shell(command="Restart-Computer -Force" if self.get_remote_machine() == "Windows" else "sudo reboot", verbose_output=True, description="", strict_stderr=False, strict_return_code=False)
131
+ return self.run_shell(command="Restart-Computer -Force" if self.remote_specs["system"] == "Windows" else "sudo reboot", verbose_output=True, description="", strict_stderr=False, strict_return_code=False)
154
132
  def send_ssh_key(self) -> Response:
155
- self.copy_from_here(source_path=Path("~/.ssh/id_rsa.pub"), target_path=None, compress_with_zip=False, recursive=False, overwrite_existing=False)
156
- if self.get_remote_machine() != "Windows":
133
+ self.copy_from_here(source_path="~/.ssh/id_rsa.pub", target_rel2home=None, compress_with_zip=False, recursive=False, overwrite_existing=False)
134
+ if self.remote_specs["system"] != "Windows":
157
135
  raise RuntimeError("send_ssh_key is only supported for Windows remote machines")
158
136
  code_url = "https://raw.githubusercontent.com/thisismygitrepo/machineconfig/refs/heads/main/src/machineconfig/setup_windows/openssh-server_add-sshkey.ps1"
159
137
  import urllib.request
@@ -162,17 +140,17 @@ class SSH:
162
140
  return self.run_shell(command=code, verbose_output=True, description="", strict_stderr=False, strict_return_code=False)
163
141
 
164
142
  def get_remote_repr(self, add_machine: bool = False) -> str:
165
- return f"{self.username}@{self.hostname}:{self.port}" + (f" [{self.get_remote_machine()}][{self.get_remote_distro()}]" if add_machine else "")
143
+ return f"{self.username}@{self.hostname}:{self.port}" + (f" [{self.remote_specs['system']}][{self.remote_specs['distro']}]" if add_machine else "")
166
144
  def get_local_repr(self, add_machine: bool = False) -> str:
167
145
  import getpass
168
- return f"{getpass.getuser()}@{self.platform.node()}" + (f" [{self.platform.system()}][{self.get_local_distro()}]" if add_machine else "")
146
+ return f"{getpass.getuser()}@{platform.node()}" + (f" [{platform.system()}][{self.local_specs['distro']}]" if add_machine else "")
169
147
  def get_ssh_conn_str(self, command: str) -> str:
170
148
  return "ssh " + (f" -i {self.ssh_key_path}" if self.ssh_key_path else "") + self.get_remote_repr(add_machine=False).replace(":", " -p ") + (f" -t {command} " if command != "" else " ")
171
149
  def __repr__(self) -> str:
172
150
  return f"local {self.get_local_repr(add_machine=True)} >>> SSH TO >>> remote {self.get_remote_repr(add_machine=True)}"
173
151
 
174
152
  def run_locally(self, command: str) -> Response:
175
- print(f"""💻 [LOCAL EXECUTION] Running command on node: {self.platform.node()} Command: {command}""")
153
+ print(f"""💻 [LOCAL EXECUTION] Running command on node: {self.local_specs['system']} Command: {command}""")
176
154
  res = Response(cmd=command)
177
155
  res.output.returncode = os.system(command)
178
156
  return res
@@ -184,15 +162,14 @@ class SSH:
184
162
  res.print()
185
163
  else:
186
164
  res.capture().print_if_unsuccessful(desc=description, strict_err=strict_stderr, strict_returncode=strict_return_code, assert_success=False)
187
- self.terminal_responses.append(res)
165
+ # self.terminal_responses.append(res)
188
166
  return res
189
167
  def run_py(self, python_code: str, uv_with: Optional[list[str]], uv_project_dir: Optional[str],
190
168
  description: str, verbose_output: bool, strict_stderr: bool, strict_return_code: bool) -> Response:
191
- from machineconfig.utils.accessories import randstr
192
169
  py_path = Path.home().joinpath(f"{DEFAULT_PICKLE_SUBDIR}/runpy_{randstr()}.py")
193
170
  py_path.parent.mkdir(parents=True, exist_ok=True)
194
171
  py_path.write_text(python_code, encoding="utf-8")
195
- self.copy_from_here(source_path=py_path, target_path=None, compress_with_zip=False, recursive=False, overwrite_existing=False)
172
+ self.copy_from_here(source_path=str(py_path), target_rel2home=None, compress_with_zip=False, recursive=False, overwrite_existing=False)
196
173
  if uv_with is not None and len(uv_with) > 0:
197
174
  with_clause = " --with " + '"' + ",".join(uv_with) + '"'
198
175
  else:
@@ -205,7 +182,6 @@ class SSH:
205
182
  return self.run_shell(command=uv_cmd, verbose_output=verbose_output, description=description or f"run_py on {self.get_remote_repr(add_machine=False)}", strict_stderr=strict_stderr, strict_return_code=strict_return_code)
206
183
 
207
184
  def run_py_func(self, func: Callable[..., Any], uv_with: Optional[list[str]], uv_project_dir: Optional[str]) -> Response:
208
- from machineconfig.utils.meta import lambda_to_python_script
209
185
  command = lambda_to_python_script(lmb=func, in_global=True)
210
186
  return self.run_py(python_code=command, uv_with=uv_with, uv_project_dir=uv_project_dir,
211
187
  description=f"run_py_func {func.__name__} on {self.get_remote_repr(add_machine=False)}",
@@ -218,77 +194,45 @@ class SSH:
218
194
  local_path.parent.mkdir(parents=True, exist_ok=True)
219
195
  self.sftp.get(remotepath=remote_path, localpath=str(local_path))
220
196
 
221
- def _create_remote_target_dir(self, target_path: Union[str, Path], overwrite_existing: bool) -> str:
197
+ def create_dir(self, path_rel2home: str, overwrite_existing: bool) -> None:
222
198
  """Helper to create a directory on remote machine and return its path."""
223
- def create_target_dir(target_dir_path: str, overwrite: bool, json_output_path: str) -> str:
199
+ def create_target_dir(target_rel2home: str, overwrite: bool) -> None:
224
200
  from pathlib import Path
225
201
  import shutil
226
- import json
227
- directory_path = Path(target_dir_path).expanduser()
202
+ directory_path = Path(target_rel2home).expanduser()
228
203
  if overwrite and directory_path.exists():
229
204
  if directory_path.is_dir():
230
205
  shutil.rmtree(directory_path)
231
206
  else:
232
207
  directory_path.unlink()
233
208
  directory_path.parent.mkdir(parents=True, exist_ok=True)
234
- result_path_posix = directory_path.as_posix()
235
- json_result_path = Path(json_output_path)
236
- json_result_path.parent.mkdir(parents=True, exist_ok=True)
237
- json_result_path.write_text(json.dumps(result_path_posix, indent=2), encoding="utf-8")
238
- print(json_result_path.as_posix())
239
- return result_path_posix
240
- from machineconfig.utils.meta import lambda_to_python_script
241
- from machineconfig.utils.accessories import randstr
242
- remote_json_output = Path.home().joinpath(f"{DEFAULT_PICKLE_SUBDIR}/return_{randstr()}.json").as_posix()
243
- # command = function_to_script(func=create_target_dir, call_with_kwargs={"target_dir_path": Path(target_path).as_posix(), "overwrite": overwrite_existing, "json_output_path": remote_json_output})
244
- command = lambda_to_python_script(lmb=lambda: create_target_dir(target_dir_path=str(target_path), overwrite=overwrite_existing, json_output_path=remote_json_output), in_global=True)
245
- response = self.run_py(python_code=command, uv_with=[MACHINECONFIG_VERSION], uv_project_dir=None,
246
- description=f"Creating target directory `{Path(target_path).parent.as_posix()}` @ {self.get_remote_repr(add_machine=False)}",
247
- verbose_output=False, strict_stderr=False, strict_return_code=False)
248
- remote_json_path = response.op.strip()
249
- if not remote_json_path:
250
- raise RuntimeError(f"Failed to create target directory {target_path} - no response from remote")
251
- from machineconfig.utils.accessories import randstr
252
- local_json = Path.home().joinpath(f"{DEFAULT_PICKLE_SUBDIR}/local_{randstr()}.json")
253
- self._simple_sftp_get(remote_path=remote_json_path, local_path=local_json)
254
- import json
255
- try:
256
- result = json.loads(local_json.read_text(encoding="utf-8"))
257
- except (json.JSONDecodeError, FileNotFoundError) as err:
258
- raise RuntimeError(f"Failed to create target directory {target_path} - invalid JSON response: {err}") from err
259
- finally:
260
- if local_json.exists():
261
- local_json.unlink()
262
- assert isinstance(result, str), f"Failed to create target directory {target_path} on remote"
263
- return result
264
-
209
+ return
210
+ command = lambda_to_python_script(lmb=lambda: create_target_dir(target_rel2home=path_rel2home, overwrite=overwrite_existing), in_global=True)
211
+ tmp_py_file = Path.home().joinpath(f"{DEFAULT_PICKLE_SUBDIR}/create_target_dir_{randstr()}.py")
212
+ tmp_py_file.parent.mkdir(parents=True, exist_ok=True)
213
+ tmp_py_file.write_text(command, encoding="utf-8")
214
+ self.copy_from_here(source_path=str(tmp_py_file), target_rel2home=None, compress_with_zip=False, recursive=False, overwrite_existing=True)
215
+ self.run_shell(command=f"""{UV_RUN_CMD} python {tmp_py_file.as_posix()}""", verbose_output=False, description=f"Creating target dir {path_rel2home}", strict_stderr=True, strict_return_code=True)
265
216
 
266
- def copy_from_here(self, source_path: Union[str, Path], target_path: Optional[Union[str, Path]], compress_with_zip: bool, recursive: bool, overwrite_existing: bool) -> Path:
267
- if self.sftp is None:
268
- raise RuntimeError(f"SFTP connection not available for {self.hostname}. Cannot transfer files.")
269
-
217
+ def copy_from_here(self, source_path: str, target_rel2home: Optional[str], compress_with_zip: bool, recursive: bool, overwrite_existing: bool) -> None:
218
+ if self.sftp is None: raise RuntimeError(f"SFTP connection not available for {self.hostname}. Cannot transfer files.")
270
219
  source_obj = Path(source_path).expanduser().absolute()
271
- if not source_obj.exists():
272
- raise RuntimeError(f"SSH Error: source `{source_obj}` does not exist!")
273
-
274
- if target_path is None:
275
- try:
276
- target_path_relative = source_obj.relative_to(Path.home())
277
- target_path = Path("~") / target_path_relative
220
+ if not source_obj.exists(): raise RuntimeError(f"SSH Error: source `{source_obj}` does not exist!")
221
+ if target_rel2home is None:
222
+ try: target_rel2home = str(source_obj.relative_to(Path.home()))
278
223
  except ValueError:
279
224
  raise RuntimeError(f"If target is not specified, source must be relative to home directory, but got: {source_obj}")
280
-
281
225
  if not compress_with_zip and source_obj.is_dir():
282
226
  if not recursive:
283
227
  raise RuntimeError(f"SSH Error: source `{source_obj}` is a directory! Set `recursive=True` for recursive sending or `compress_with_zip=True` to zip it first.")
284
228
  file_paths_to_upload: list[Path] = [file_path for file_path in source_obj.rglob("*") if file_path.is_file()]
285
- remote_root = self._create_remote_target_dir(target_path=target_path, overwrite_existing=overwrite_existing)
229
+ # self.create_dir(path_rel2home=target_rel2home, overwrite_existing=overwrite_existing)
286
230
  for idx, file_path in enumerate(file_paths_to_upload):
287
231
  print(f" {idx + 1:03d}. {file_path}")
288
232
  for file_path in file_paths_to_upload:
289
- remote_file_target = Path(remote_root).joinpath(file_path.relative_to(source_obj))
290
- self.copy_from_here(source_path=file_path, target_path=remote_file_target, compress_with_zip=False, recursive=False, overwrite_existing=overwrite_existing)
291
- return Path(remote_root)
233
+ remote_file_target = Path(target_rel2home).joinpath(file_path.relative_to(source_obj))
234
+ self.copy_from_here(source_path=str(file_path), target_rel2home=str(remote_file_target), compress_with_zip=False, recursive=False, overwrite_existing=overwrite_existing)
235
+ return None
292
236
  if compress_with_zip:
293
237
  print("🗜️ ZIPPING ...")
294
238
  import shutil
@@ -298,16 +242,14 @@ class SSH:
298
242
  else:
299
243
  shutil.make_archive(str(zip_path), "zip", source_obj.parent, source_obj.name)
300
244
  source_obj = Path(str(zip_path) + ".zip")
301
- if not str(target_path).endswith(".zip"):
302
- target_path = Path(str(target_path) + ".zip")
303
- remotepath_str = self._create_remote_target_dir(target_path=target_path, overwrite_existing=overwrite_existing)
304
- remotepath = Path(remotepath_str)
305
- print(f"""📤 [SFTP UPLOAD] Sending file: {repr(source_obj)} ==> Remote Path: {remotepath.as_posix()}""")
245
+ if not target_rel2home.endswith(".zip"): target_rel2home = target_rel2home + ".zip"
246
+ # remotepath_str = self.create_dir(target_path=target_path, overwrite_existing=overwrite_existing)
247
+ print(f"""📤 [SFTP UPLOAD] Sending file: {repr(source_obj)} ==> Remote Path: {target_rel2home}""")
306
248
  try:
307
249
  with self.tqdm_wrap(ascii=True, unit="b", unit_scale=True) as pbar:
308
250
  if self.sftp is None: # type: ignore[unreachable]
309
251
  raise RuntimeError(f"SFTP connection lost for {self.hostname}")
310
- self.sftp.put(localpath=str(source_obj), remotepath=remotepath.as_posix(), callback=pbar.view_bar) # type: ignore
252
+ self.sftp.put(localpath=str(source_obj), remotepath=str(Path(self.remote_specs["home_dir"]).joinpath(target_rel2home)), callback=pbar.view_bar)
311
253
  except Exception:
312
254
  if compress_with_zip and source_obj.exists() and str(source_obj).endswith("_archive.zip"):
313
255
  source_obj.unlink()
@@ -325,11 +267,14 @@ class SSH:
325
267
  with zipfile.ZipFile(archive_path, "r") as archive_handle:
326
268
  archive_handle.extractall(extraction_directory)
327
269
  archive_path.unlink()
328
- from machineconfig.utils.meta import lambda_to_python_script
329
- command = lambda_to_python_script(lmb=lambda: unzip_archive(zip_file_path=remotepath.as_posix(), overwrite_flag=overwrite_existing), in_global=True)
330
- _resp = self.run_py(python_code=command, uv_with=[MACHINECONFIG_VERSION], uv_project_dir=None, description=f"UNZIPPING {remotepath.as_posix()}", verbose_output=False, strict_stderr=True, strict_return_code=True)
270
+ command = lambda_to_python_script(lmb=lambda: unzip_archive(zip_file_path=str(Path(self.remote_specs["home_dir"]).joinpath(target_rel2home)), overwrite_flag=overwrite_existing), in_global=True)
271
+ tmp_py_file = Path.home().joinpath(f"{DEFAULT_PICKLE_SUBDIR}/create_target_dir_{randstr()}.py")
272
+ tmp_py_file.parent.mkdir(parents=True, exist_ok=True)
273
+ tmp_py_file.write_text(command, encoding="utf-8")
274
+ transferred_py_file = self.copy_from_here(source_path=str(tmp_py_file), target_rel2home=None, compress_with_zip=True, recursive=False, overwrite_existing=True)
275
+ self.run_shell(command=f"""{UV_RUN_CMD} python {transferred_py_file}""", verbose_output=False, description=f"UNZIPPING {target_rel2home}", strict_stderr=True, strict_return_code=True)
331
276
  source_obj.unlink()
332
- return source_obj
277
+ return None
333
278
 
334
279
  def _check_remote_is_dir(self, source_path: Union[str, Path]) -> bool:
335
280
  """Helper to check if a remote path is a directory."""
@@ -342,16 +287,13 @@ class SSH:
342
287
  json_result_path.write_text(json.dumps(is_directory, indent=2), encoding="utf-8")
343
288
  print(json_result_path.as_posix())
344
289
  return is_directory
345
-
346
- from machineconfig.utils.meta import lambda_to_python_script
347
- from machineconfig.utils.accessories import randstr
348
290
  remote_json_output = Path.home().joinpath(f"{DEFAULT_PICKLE_SUBDIR}/return_{randstr()}.json").as_posix()
349
291
  command = lambda_to_python_script(lmb=lambda: check_is_dir(path_to_check=str(source_path), json_output_path=remote_json_output), in_global=True)
350
292
  response = self.run_py(python_code=command, uv_with=[MACHINECONFIG_VERSION], uv_project_dir=None, description=f"Check if source `{source_path}` is a dir", verbose_output=False, strict_stderr=False, strict_return_code=False)
351
293
  remote_json_path = response.op.strip()
352
294
  if not remote_json_path:
353
295
  raise RuntimeError(f"Failed to check if {source_path} is directory - no response from remote")
354
- from machineconfig.utils.accessories import randstr
296
+
355
297
  local_json = Path.home().joinpath(f"{DEFAULT_PICKLE_SUBDIR}/local_{randstr()}.json")
356
298
  self._simple_sftp_get(remote_path=remote_json_path, local_path=local_json)
357
299
  import json
@@ -377,15 +319,15 @@ class SSH:
377
319
  print(json_result_path.as_posix())
378
320
  return expanded_path_posix
379
321
 
380
- from machineconfig.utils.meta import lambda_to_python_script
381
- from machineconfig.utils.accessories import randstr
322
+
323
+
382
324
  remote_json_output = Path.home().joinpath(f"{DEFAULT_PICKLE_SUBDIR}/return_{randstr()}.json").as_posix()
383
325
  command = lambda_to_python_script(lmb=lambda: expand_source(path_to_expand=str(source_path), json_output_path=remote_json_output), in_global=True)
384
326
  response = self.run_py(python_code=command, uv_with=[MACHINECONFIG_VERSION], uv_project_dir=None, description="Resolving source path by expanding user", verbose_output=False, strict_stderr=False, strict_return_code=False)
385
327
  remote_json_path = response.op.strip()
386
328
  if not remote_json_path:
387
329
  raise RuntimeError(f"Could not resolve source path {source_path} - no response from remote")
388
- from machineconfig.utils.accessories import randstr
330
+
389
331
  local_json = Path.home().joinpath(f"{DEFAULT_PICKLE_SUBDIR}/local_{randstr()}.json")
390
332
  self._simple_sftp_get(remote_path=remote_json_path, local_path=local_json)
391
333
  import json
@@ -426,15 +368,15 @@ class SSH:
426
368
  print(json_result_path.as_posix())
427
369
  return file_paths_list
428
370
 
429
- from machineconfig.utils.meta import lambda_to_python_script
430
- from machineconfig.utils.accessories import randstr
371
+
372
+
431
373
  remote_json_output = Path.home().joinpath(f"{DEFAULT_PICKLE_SUBDIR}/return_{randstr()}.json").as_posix()
432
374
  command = lambda_to_python_script(lmb=lambda: search_files(directory_path=expanded_source, json_output_path=remote_json_output), in_global=True)
433
375
  response = self.run_py(python_code=command, uv_with=[MACHINECONFIG_VERSION], uv_project_dir=None, description="Searching for files in source", verbose_output=False, strict_stderr=False, strict_return_code=False)
434
376
  remote_json_path = response.op.strip()
435
377
  if not remote_json_path:
436
378
  raise RuntimeError(f"Could not resolve source path {source} - no response from remote")
437
- from machineconfig.utils.accessories import randstr
379
+
438
380
  local_json = Path.home().joinpath(f"{DEFAULT_PICKLE_SUBDIR}/local_{randstr()}.json")
439
381
  self._simple_sftp_get(remote_path=remote_json_path, local_path=local_json)
440
382
  import json
@@ -464,15 +406,15 @@ class SSH:
464
406
  except ValueError:
465
407
  raise RuntimeError(f"Source path must be relative to home directory: {source_absolute_path}")
466
408
 
467
- from machineconfig.utils.meta import lambda_to_python_script
468
- from machineconfig.utils.accessories import randstr
409
+
410
+
469
411
  remote_json_output = Path.home().joinpath(f"{DEFAULT_PICKLE_SUBDIR}/return_{randstr()}.json").as_posix()
470
412
  command = lambda_to_python_script(lmb=lambda: collapse_to_home_dir(absolute_path=expanded_source, json_output_path=remote_json_output), in_global=True)
471
413
  response = self.run_py(python_code=command, uv_with=[MACHINECONFIG_VERSION], uv_project_dir=None, description="Finding default target via relative source path", verbose_output=False, strict_stderr=False, strict_return_code=False)
472
414
  remote_json_path_dir = response.op.strip()
473
415
  if not remote_json_path_dir:
474
416
  raise RuntimeError("Could not resolve target path - no response from remote")
475
- from machineconfig.utils.accessories import randstr
417
+
476
418
  local_json_dir = Path.home().joinpath(f"{DEFAULT_PICKLE_SUBDIR}/local_{randstr()}.json")
477
419
  self._simple_sftp_get(remote_path=remote_json_path_dir, local_path=local_json_dir)
478
420
  import json
@@ -516,15 +458,15 @@ class SSH:
516
458
  print(json_result_path.as_posix())
517
459
  return zip_file_path
518
460
 
519
- from machineconfig.utils.meta import lambda_to_python_script
520
- from machineconfig.utils.accessories import randstr
461
+
462
+
521
463
  remote_json_output = Path.home().joinpath(f"{DEFAULT_PICKLE_SUBDIR}/return_{randstr()}.json").as_posix()
522
464
  command = lambda_to_python_script(lmb=lambda: zip_source(path_to_zip=expanded_source, json_output_path=remote_json_output), in_global=True)
523
465
  response = self.run_py(python_code=command, uv_with=[MACHINECONFIG_VERSION], uv_project_dir=None, description=f"Zipping source file {source}", verbose_output=False, strict_stderr=False, strict_return_code=False)
524
466
  remote_json_path = response.op.strip()
525
467
  if not remote_json_path:
526
468
  raise RuntimeError(f"Could not zip {source} - no response from remote")
527
- from machineconfig.utils.accessories import randstr
469
+
528
470
  local_json = Path.home().joinpath(f"{DEFAULT_PICKLE_SUBDIR}/local_{randstr()}.json")
529
471
  self._simple_sftp_get(remote_path=remote_json_path, local_path=local_json)
530
472
  import json
@@ -555,15 +497,15 @@ class SSH:
555
497
  except ValueError:
556
498
  raise RuntimeError(f"Source path must be relative to home directory: {source_absolute_path}")
557
499
 
558
- from machineconfig.utils.meta import lambda_to_python_script
559
- from machineconfig.utils.accessories import randstr
500
+
501
+
560
502
  remote_json_output = Path.home().joinpath(f"{DEFAULT_PICKLE_SUBDIR}/return_{randstr()}.json").as_posix()
561
503
  command = lambda_to_python_script(lmb=lambda: collapse_to_home(absolute_path=expanded_source, json_output_path=remote_json_output), in_global=True)
562
504
  response = self.run_py(python_code=command, uv_with=[MACHINECONFIG_VERSION], uv_project_dir=None, description="Finding default target via relative source path", verbose_output=False, strict_stderr=False, strict_return_code=False)
563
505
  remote_json_path = response.op.strip()
564
506
  if not remote_json_path:
565
507
  raise RuntimeError("Could not resolve target path - no response from remote")
566
- from machineconfig.utils.accessories import randstr
508
+
567
509
  local_json = Path.home().joinpath(f"{DEFAULT_PICKLE_SUBDIR}/local_{randstr()}.json")
568
510
  self._simple_sftp_get(remote_path=remote_json_path, local_path=local_json)
569
511
  import json
@@ -613,10 +555,12 @@ class SSH:
613
555
  else:
614
556
  file_or_dir_path.unlink()
615
557
 
616
- from machineconfig.utils.meta import lambda_to_python_script
558
+
617
559
  command = lambda_to_python_script(lmb=lambda: delete_temp_zip(path_to_delete=expanded_source), in_global=True)
618
560
  self.run_py(python_code=command, uv_with=[MACHINECONFIG_VERSION], uv_project_dir=None, description="Cleaning temp zip files @ remote.", verbose_output=False, strict_stderr=True, strict_return_code=True)
619
561
 
620
562
  print("\n")
621
563
  return target_obj
622
564
 
565
+ if __name__ == "__main__":
566
+ ssh = SSH(host="p51s", username=None, hostname=None, ssh_key_path=None, password=None, port=22, enable_compression=False)
@@ -35,7 +35,8 @@ class Response:
35
35
  def __call__(self, *args: Any, **kwargs: Any) -> Optional[str]:
36
36
  _ = args, kwargs
37
37
  return self.op.rstrip() if type(self.op) is str else None
38
-
38
+ def __repr__(self) -> str:
39
+ return f"Response({self.input=}, {self.output=}, {self.desc=}, {self.op=}, {self.ip=}, {self.err=}, {self.returncode=})"
39
40
  @property
40
41
  def op(self) -> str:
41
42
  return self.output.stdout
@@ -97,115 +98,3 @@ class Response:
97
98
  txt = tmp1 + Text(str(self.input), style="white") + tmp2 + Text("\n".join(list_str), style="white")
98
99
  con.print(Panel(txt, title=f"🖥️ {self.desc}", subtitle=f"📋 {desc}", width=150, style="bold cyan on black"))
99
100
  return self
100
-
101
-
102
- '''
103
- class Terminal:
104
- def __init__(self, stdout: Optional[int] = subprocess.PIPE, stderr: Optional[int] = subprocess.PIPE, stdin: Optional[int] = subprocess.PIPE, elevated: bool = False):
105
- self.machine: str = platform.system()
106
- self.elevated: bool = elevated
107
- self.stdout = stdout
108
- self.stderr = stderr
109
- self.stdin = stdin
110
-
111
- # def set_std_system(self): self.stdout = sys.stdout; self.stderr = sys.stderr; self.stdin = sys.stdin
112
- # def set_std_pipe(self):
113
- # self.stdout = subprocess.PIPE
114
- # self.stderr = subprocess.PIPE
115
- # self.stdin = subprocess.PIPE
116
-
117
- # def set_std_null(self):
118
- # self.stdout, self.stderr, self.stdin = subprocess.DEVNULL, subprocess.DEVNULL, subprocess.DEVNULL # Equivalent to `echo 'foo' &> /dev/null`
119
-
120
- def run(self, *cmds: str, shell: Optional[SHELLS] = "default", check: bool = False, ip: Optional[str] = None) -> Response: # Runs SYSTEM commands like subprocess.run
121
- """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.
122
- * 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.
123
- * `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.
124
- * `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`"""
125
- my_list = list(
126
- cmds
127
- ) # `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.
128
- if self.machine == "Windows" and shell in {"powershell", "pwsh"}:
129
- my_list = [shell, "-Command"] + my_list # alternatively, one can run "cmd"
130
- if self.elevated is False or self.is_user_admin():
131
- 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)
132
- else:
133
- resp = __import__("ctypes").windll.shell32.ShellExecuteW(None, "runas", sys.executable, " ".join(sys.argv), None, 1)
134
- return Response.from_completed_process(resp)
135
- @staticmethod
136
- def is_user_admin() -> bool: # adopted from: https://stackoverflow.com/questions/19672352/how-to-run-script-with-elevated-privilege-on-windows"""
137
- if os.name == "nt":
138
- try:
139
- return __import__("ctypes").windll.shell32.IsUserAnAdmin()
140
- except Exception:
141
- import traceback
142
-
143
- traceback.print_exc()
144
- print("Admin check failed, assuming not an admin.")
145
- return False
146
- else:
147
- return os.getuid() == 0 # Check for root on Posix
148
- '''
149
-
150
- # def run_shell_script(self, script: str, shell: SHELLS = "default", verbose: bool = False):
151
- # if self.machine == "Linux":
152
- # script = "#!/bin/bash" + "\n" + script # `source` is only available in bash.
153
- # script_file = PathExtended.tmpfile(name="tmp_shell_script", suffix=".ps1" if self.machine == "Windows" else ".sh", folder="tmp_scripts")
154
- # script_file.write_text(script, newline={"Windows": None, "Linux": "\n"}[self.machine])
155
- # if shell == "default":
156
- # if self.machine == "Windows":
157
- # start_cmd = "powershell" # default shell on Windows is cmd which is not very useful. (./source is not available)
158
- # full_command: Union[list[str], str] = [start_cmd, str(script_file)] # shell=True will cause this to be a string anyway (with space separation)
159
- # else:
160
- # start_cmd = "bash"
161
- # full_command = f"{start_cmd} {script_file}" # full_command = [start_cmd, str(script_file)]
162
- # else:
163
- # full_command = f"{shell} {script_file}" # full_command = [shell, str(tmp_file)]
164
- # if verbose:
165
- # desc = "Script to be executed:"
166
- # if platform.system() == "Windows":
167
- # lexer = "powershell"
168
- # elif platform.system() == "Linux":
169
- # lexer = "sh"
170
- # elif platform.system() == "Darwin":
171
- # lexer = "sh" # macOS uses similar shell to Linux
172
- # else:
173
- # raise NotImplementedError(f"Platform {platform.system()} not supported.")
174
- # from rich.console import Console
175
- # from rich.panel import Panel
176
- # from rich.syntax import Syntax
177
- # import rich.progress as pb
178
-
179
- # console = Console()
180
- # console.print(Panel(Syntax(code=script, lexer=lexer), title=f"📄 {desc}"), style="bold red")
181
- # with pb.Progress(transient=True) as progress:
182
- # _task = progress.add_task(f"Running Script @ {script_file}", total=None)
183
- # resp = subprocess.run(full_command, stderr=self.stderr, stdin=self.stdin, stdout=self.stdout, text=True, shell=True, check=False)
184
- # else:
185
- # resp = subprocess.run(full_command, stderr=self.stderr, stdin=self.stdin, stdout=self.stdout, text=True, shell=True, check=False)
186
- # return Response.from_completed_process(resp)
187
-
188
- # 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.
189
- # 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 "")
190
- # py_script = PathExtended.tmpfile(name="tmp_python_script", suffix=".py", folder="tmp_scripts/terminal")
191
- # py_script.write_text(f"""print(r'''{script}''')""" + "\n" + script)
192
- # print(f"""🚀 [ASYNC PYTHON SCRIPT] Script URI:
193
- # {py_script.absolute().as_uri()}""")
194
- # print("Script to be executed asyncronously: ", py_script.absolute().as_uri())
195
- # shell_script = f"""
196
- # {f"cd {wdir}" if wdir is not None else ""}
197
- # {"ipython" if ipython else "python"} {"-i" if interactive else ""} {py_script}
198
- # """
199
- # shell_path = PathExtended.tmpfile(name="tmp_shell_script", suffix=".sh" if self.machine == "Linux" else ".ps1", folder="tmp_scripts/shell")
200
- # shell_path.write_text(shell_script)
201
- # if shell is None and self.machine == "Windows":
202
- # shell = "pwsh"
203
- # window = "start" if new_window and self.machine == "Windows" else ""
204
- # os.system(f"{window} {terminal} {shell} {shell_script}")
205
-
206
- # @staticmethod
207
- # def run_as_admin(file: PLike, params: Any, wait: bool = False):
208
- # proce_info = install_n_import(library="win32com", package="pywin32", fromlist=["shell.shell.ShellExecuteEx"]).shell.shell.ShellExecuteEx(lpVerb='runas', lpFile=file, lpParameters=params)
209
- # # TODO update PATH for this to take effect immediately.
210
- # if wait: time.sleep(1)
211
- # return proce_info
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: machineconfig
3
- Version: 6.61
3
+ Version: 6.62
4
4
  Summary: Dotfiles management package
5
5
  Author-email: Alex Al-Saffar <programmer@usa.com>
6
6
  License: Apache 2.0
@@ -127,10 +127,10 @@ machineconfig/scripts/python/devops.py,sha256=Lv4d-UlyOREj4VTcu_pxswYo54Mawe3XGe
127
127
  machineconfig/scripts/python/devops_navigator.py,sha256=5Cm384D4S8_GsvMzTwr0C16D0ktf8_5Mk5bEJncwDO8,237
128
128
  machineconfig/scripts/python/entry.py,sha256=liCf186Msa6R-EjcQ0I6K80Km7Wi8qckJB6rXIHeHU0,2488
129
129
  machineconfig/scripts/python/fire_jobs.py,sha256=My7sFn1R2vh21uIHGfNppgX99WTEitCFgJ1MSasBUOQ,13597
130
- machineconfig/scripts/python/ftpx.py,sha256=vm4QNJA0z1Vu-85wFliGNoDHMZZ-Yy8zQACL6x7Wo6U,9760
130
+ machineconfig/scripts/python/ftpx.py,sha256=A13hL_tDYfcsaK9PkshK-0lrUS6KPhPCtwqWtLSo6IM,9764
131
131
  machineconfig/scripts/python/interactive.py,sha256=zt3g6nGKR_Y5A57UnR4Y5-JpLzrpnCOSaqU1bnaikK0,11666
132
132
  machineconfig/scripts/python/sessions.py,sha256=UERxO472EDtN7nKHEULbn6G3S5PJIpsDG9Gq3TlByqI,9823
133
- machineconfig/scripts/python/utils.py,sha256=rlrTomPlvhzaMf2UWzVeyO_kTwNWFj1iASXIF2g9cx8,710
133
+ machineconfig/scripts/python/utils.py,sha256=c9HsKG40i5ggwqKuS3O-LCScuFpmxMVKqWpsFHx2dJc,934
134
134
  machineconfig/scripts/python/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
135
135
  machineconfig/scripts/python/ai/generate_files.py,sha256=VfjKdwgF8O6E4oiRtfWNliibLmmwGe7f9ld6wpOsXTw,14498
136
136
  machineconfig/scripts/python/ai/initai.py,sha256=9SZtWOcRuwk8ZU3wHOfPzjInERD79ZTYFY8tVACgza4,2260
@@ -187,7 +187,7 @@ machineconfig/scripts/python/helpers_devops/cli_repos.py,sha256=Xwkv1adqHZvTfRSP
187
187
  machineconfig/scripts/python/helpers_devops/cli_self.py,sha256=0-R7YDIQS2cH2n9C7lShFoBRwbytQz3BCuHGQrus7FU,6225
188
188
  machineconfig/scripts/python/helpers_devops/cli_share_server.py,sha256=q9pFJ6AxPuygMr3onMNOKEuuQHbVE_6Qoyo7xRT5FX0,4196
189
189
  machineconfig/scripts/python/helpers_devops/cli_terminal.py,sha256=k_PzXaiGyE0vXr0Ii1XcJz2A7UvyPJrR31TRWt4RKRI,6019
190
- machineconfig/scripts/python/helpers_devops/cli_utils.py,sha256=sCYKR6bXzr6DgVnQrCa_pjKdc7tOOY3T8h1A41UURBU,5066
190
+ machineconfig/scripts/python/helpers_devops/cli_utils.py,sha256=jN_GeZyy-9tAJoBoASsmn9_tI7ZgkgXp7Ijgi2x_ssw,6053
191
191
  machineconfig/scripts/python/helpers_devops/devops_backup_retrieve.py,sha256=Dn8luB6QJzxKiiFSC-NMqiYddWZoca3A8eOjMYZDzTc,5598
192
192
  machineconfig/scripts/python/helpers_devops/devops_status.py,sha256=PJVPhfhXq8der6Xd-_fjZfnizfM-RGfJApkRGhGBmNo,20525
193
193
  machineconfig/scripts/python/helpers_devops/devops_update_repos.py,sha256=kSln8_-Wn7Qu0NaKdt-QTN_bBVyTIAWHH8xVYKK-vCM,10133
@@ -407,8 +407,8 @@ machineconfig/utils/procs.py,sha256=YPA_vEYQGwPd_o_Lc6nOTBo5BrB1tSs8PJ42XiGpenM,
407
407
  machineconfig/utils/scheduler.py,sha256=44CASABJg3epccxhAwv2CX7TVgZh6zVy3K4vqHKTuf4,14228
408
408
  machineconfig/utils/scheduling.py,sha256=6x5zLA7sY5gohrEtN6zGrXIqNFasMoyBfwLcOjrjiME,11109
409
409
  machineconfig/utils/source_of_truth.py,sha256=ZAnCRltiM07ig--P6g9_6nEAvNFC4X4ERFTVcvpIYsE,764
410
- machineconfig/utils/ssh.py,sha256=R2yOmz1HXwPpW1XlhzOZbneu3xejjoUoAIT1QnQ-ecA,39382
411
- machineconfig/utils/terminal.py,sha256=IlmOByfQG-vjhaFFxxzU5rWzP5_qUzmalRfuey3PAmc,11801
410
+ machineconfig/utils/ssh.py,sha256=lTvhFr9A2yNe6MbtcBMSl_d3wjeIfrkMUrREgMcZ7zk,35791
411
+ machineconfig/utils/terminal.py,sha256=VDgsjTjBmMGgZN0YIc0pJ8YksLDrBtiXON1EThy7_is,4264
412
412
  machineconfig/utils/tst.py,sha256=6u1GI49NdcpxH2BYGAusNfY5q9G_ytCGVzFM5b6HYpM,674
413
413
  machineconfig/utils/upgrade_packages.py,sha256=mSFyKvB3JhHte_x1dtmEgrJZCAXgTUQoaJUSx1OXQ3Y,4145
414
414
  machineconfig/utils/ve.py,sha256=L-6PBXnQGXThiwWgheJMQoisAZOZA6SVCbGw2J-GFnI,2414
@@ -436,8 +436,8 @@ machineconfig/utils/schemas/installer/installer_types.py,sha256=QClRY61QaduBPJoS
436
436
  machineconfig/utils/schemas/layouts/layout_types.py,sha256=TcqlZdGVoH8htG5fHn1KWXhRdPueAcoyApppZsPAPto,2020
437
437
  machineconfig/utils/schemas/repos/repos_types.py,sha256=ECVr-3IVIo8yjmYmVXX2mnDDN1SLSwvQIhx4KDDQHBQ,405
438
438
  machineconfig/utils/ssh_utils/utils.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
439
- machineconfig-6.61.dist-info/METADATA,sha256=W4hUI9UB2KnHVsMoV1OKaTStImXnD7iWzZ2tIkJGucs,2928
440
- machineconfig-6.61.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
441
- machineconfig-6.61.dist-info/entry_points.txt,sha256=NTW7hbUlpt5Vx9DdQrONLkYMCuBXpvYh1dt0AtlGxeI,466
442
- machineconfig-6.61.dist-info/top_level.txt,sha256=porRtB8qms8fOIUJgK-tO83_FeH6Bpe12oUVC670teA,14
443
- machineconfig-6.61.dist-info/RECORD,,
439
+ machineconfig-6.62.dist-info/METADATA,sha256=VjucUVQDh7RyqFcRHJooUz1f528jVpPQ6_ilmkmPrVo,2928
440
+ machineconfig-6.62.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
441
+ machineconfig-6.62.dist-info/entry_points.txt,sha256=NTW7hbUlpt5Vx9DdQrONLkYMCuBXpvYh1dt0AtlGxeI,466
442
+ machineconfig-6.62.dist-info/top_level.txt,sha256=porRtB8qms8fOIUJgK-tO83_FeH6Bpe12oUVC670teA,14
443
+ machineconfig-6.62.dist-info/RECORD,,