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

@@ -1,50 +1,20 @@
1
- from typing import Optional, Any, Union, List
1
+ from typing import Optional, Any, Union
2
2
  import os
3
- from dataclasses import dataclass
3
+ from pathlib import Path
4
4
  import rich.console
5
5
  from machineconfig.utils.terminal import Response, MACHINE
6
- from machineconfig.utils.path_extended import PathExtended, PLike, OPLike
7
6
  from machineconfig.utils.accessories import pprint
8
- # from machineconfig.utils.ve import get_ve_activate_line
9
7
 
8
+ UV_RUN_CMD = "$HOME/.local/bin/uv run"
9
+ MACHINECONFIG_VERSION = "machineconfig>=5.67"
10
+ DEFAULT_PICKLE_SUBDIR = "tmp_results/tmp_scripts/ssh"
10
11
 
11
- def get_header(wdir: str): return f"""import sys; sys.path.insert(0, r'{wdir}')"""
12
12
 
13
-
14
- @dataclass
15
- class Scout:
16
- source_full: PathExtended
17
- source_rel2home: PathExtended
18
- exists: bool
19
- is_dir: bool
20
- files: Optional[List[PathExtended]]
21
-
22
-
23
- def scout(source: PLike, z: bool = False, r: bool = False) -> Scout:
24
- source_full = PathExtended(source).expanduser().absolute()
25
- source_rel2home = source_full.collapseuser()
26
- exists = source_full.exists()
27
- is_dir = source_full.is_dir() if exists else False
28
- if z and exists:
29
- try:
30
- source_full = source_full.zip()
31
- except Exception as ex:
32
- raise Exception(f"Could not zip {source_full} due to {ex}") from ex # type: ignore # pylint: disable=W0719
33
- source_rel2home = source_full.zip()
34
- if r and exists and is_dir:
35
- files = [item.collapseuser() for item in source_full.search(folders=False, r=True)]
36
- else:
37
- files = None
38
- return Scout(source_full=source_full, source_rel2home=source_rel2home, exists=exists, is_dir=is_dir, files=files)
39
-
40
-
41
- class SSH: # inferior alternative: https://github.com/fabric/fabric
13
+ class SSH:
42
14
  def __init__(
43
- 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
44
- ): # https://stackoverflow.com/questions/51027192/execute-command-script-using-different-shell-in-ssh-paramiko
45
- self.pwd = pwd
46
- self.ve = ve
47
- self.compress = compress # Defaults: (1) use localhost if nothing provided.
15
+ self, host: Optional[str], username: Optional[str], hostname: Optional[str], ssh_key_path: Optional[str], password: Optional[str], port: int, enable_compression: bool):
16
+ self.password = password
17
+ self.enable_compression = enable_compression
48
18
 
49
19
  self.host: Optional[str] = None
50
20
  self.hostname: str
@@ -59,24 +29,24 @@ class SSH: # inferior alternative: https://github.com/fabric/fabric
59
29
  try:
60
30
  import paramiko.config as pconfig
61
31
 
62
- config = pconfig.SSHConfig.from_path(str(PathExtended.home().joinpath(".ssh/config")))
32
+ config = pconfig.SSHConfig.from_path(str(Path.home().joinpath(".ssh/config")))
63
33
  config_dict = config.lookup(host)
64
34
  self.hostname = config_dict["hostname"]
65
35
  self.username = config_dict["user"]
66
36
  self.host = host
67
37
  self.port = int(config_dict.get("port", port))
68
- tmp = config_dict.get("identityfile", sshkey)
69
- if isinstance(tmp, list):
70
- sshkey = tmp[0]
38
+ identity_file_value = config_dict.get("identityfile", ssh_key_path)
39
+ if isinstance(identity_file_value, list):
40
+ ssh_key_path = identity_file_value[0]
71
41
  else:
72
- sshkey = tmp
42
+ ssh_key_path = identity_file_value
73
43
  self.proxycommand = config_dict.get("proxycommand", None)
74
- if sshkey is not None:
75
- tmp = config.lookup("*").get("identityfile", sshkey)
76
- if isinstance(tmp, list):
77
- sshkey = tmp[0]
44
+ if ssh_key_path is not None:
45
+ wildcard_identity_file = config.lookup("*").get("identityfile", ssh_key_path)
46
+ if isinstance(wildcard_identity_file, list):
47
+ ssh_key_path = wildcard_identity_file[0]
78
48
  else:
79
- sshkey = tmp
49
+ ssh_key_path = wildcard_identity_file
80
50
  except (FileNotFoundError, KeyError):
81
51
  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}"
82
52
  if "@" in host:
@@ -94,31 +64,29 @@ class SSH: # inferior alternative: https://github.com/fabric/fabric
94
64
  print(f"Provided values: host={host}, username={username}, hostname={hostname}")
95
65
  raise ValueError("Either host or username and hostname must be provided.")
96
66
 
97
- 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
67
+ self.ssh_key_path = str(Path(ssh_key_path).expanduser().absolute()) if ssh_key_path is not None else None
98
68
  self.ssh = paramiko.SSHClient()
99
69
  self.ssh.load_system_host_keys()
100
70
  self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
101
- 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")
71
+ pprint(dict(host=self.host, hostname=self.hostname, username=self.username, password="***", port=self.port, key_filename=self.ssh_key_path), title="SSHing To")
102
72
  sock = paramiko.ProxyCommand(self.proxycommand) if self.proxycommand is not None else None
103
73
  try:
104
- if pwd is None:
74
+ if password is None:
105
75
  allow_agent = True
106
76
  look_for_keys = True
107
77
  else:
108
78
  allow_agent = False
109
79
  look_for_keys = False
110
- 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
80
+ self.ssh.connect(hostname=self.hostname, username=self.username, password=self.password, port=self.port, key_filename=self.ssh_key_path, compress=self.enable_compression, sock=sock, allow_agent=allow_agent, look_for_keys=look_for_keys) # type: ignore
111
81
  except Exception as _err:
112
82
  rich.console.Console().print_exception()
113
- self.pwd = getpass.getpass(f"Enter password for {self.username}@{self.hostname}: ")
114
- 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
83
+ self.password = getpass.getpass(f"Enter password for {self.username}@{self.hostname}: ")
84
+ self.ssh.connect(hostname=self.hostname, username=self.username, password=self.password, port=self.port, key_filename=self.ssh_key_path, compress=self.enable_compression, sock=sock, allow_agent=False, look_for_keys=False) # type: ignore
115
85
  try:
116
86
  self.sftp: Optional[paramiko.SFTPClient] = self.ssh.open_sftp()
117
87
  except Exception as err:
118
88
  self.sftp = None
119
- print(f"""⚠️ WARNING: Failed to open SFTP connection to {hostname}.
120
- Error Details: {err}\nData transfer may be affected!""")
121
-
89
+ print(f"""⚠️ WARNING: Failed to open SFTP connection to {self.hostname}. Error Details: {err}\nData transfer may be affected!""")
122
90
  from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, FileSizeColumn, TransferSpeedColumn
123
91
 
124
92
  class RichProgressWrapper:
@@ -148,238 +116,489 @@ class SSH: # inferior alternative: https://github.com/fabric/fabric
148
116
  self.terminal_responses: list[Response] = []
149
117
  self.platform = platform
150
118
 
119
+ def __enter__(self) -> "SSH":
120
+ return self
121
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
122
+ self.close()
123
+ def close(self) -> None:
124
+ if self.sftp is not None:
125
+ self.sftp.close()
126
+ self.sftp = None
127
+ self.ssh.close()
151
128
  def get_remote_machine(self) -> MACHINE:
152
129
  if self._remote_machine is None:
153
- 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":
130
+ windows_test1 = self.run_shell(command="$env:OS", verbose_output=False, description="Testing Remote OS Type", strict_stderr=False, strict_return_code=False).op
131
+ 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
132
+ if windows_test1 == "Windows_NT" or windows_test2 == "Windows_NT":
154
133
  self._remote_machine = "Windows"
155
134
  else:
156
135
  self._remote_machine = "Linux"
157
- return self._remote_machine # echo %OS% TODO: uname on linux
158
-
136
+ return self._remote_machine
159
137
  def get_local_distro(self) -> str:
160
138
  if self._local_distro is None:
161
- command = """uv run --with distro python -c "import distro; print(distro.name(pretty=True))" """
139
+ command = f"""{UV_RUN_CMD} --with distro python -c "import distro; print(distro.name(pretty=True))" """
162
140
  import subprocess
163
141
  res = subprocess.run(command, shell=True, capture_output=True, text=True).stdout.strip()
164
142
  self._local_distro = res
165
143
  return res
166
144
  return self._local_distro
167
-
168
- def get_remote_distro(self):
145
+ def get_remote_distro(self) -> str:
169
146
  if self._remote_distro is None:
170
- res = self.run("""$HOME/.local/bin/uv run --with distro python -c "import distro; print(distro.name(pretty=True))" """)
147
+ command_str = f"""{UV_RUN_CMD} --with distro python -c "import distro; print(distro.name(pretty=True))" """
148
+ res = self.run_shell(command=command_str, verbose_output=True, description="", strict_stderr=False, strict_return_code=False)
171
149
  self._remote_distro = res.op_if_successfull_or_default() or ""
172
150
  return self._remote_distro
173
-
174
- def restart_computer(self):
175
- self.run("Restart-Computer -Force" if self.get_remote_machine() == "Windows" else "sudo reboot")
176
-
177
- def send_ssh_key(self):
178
- self.copy_from_here("~/.ssh/id_rsa.pub")
179
- assert self.get_remote_machine() == "Windows"
151
+ def restart_computer(self) -> Response:
152
+ 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)
153
+ def send_ssh_key(self) -> Response:
154
+ self.copy_from_here(source_path=Path("~/.ssh/id_rsa.pub"), target_path=None, compress_with_zip=False, recursive=False, overwrite_existing=False)
155
+ if self.get_remote_machine() != "Windows":
156
+ raise RuntimeError("send_ssh_key is only supported for Windows remote machines")
180
157
  code_url = "https://raw.githubusercontent.com/thisismygitrepo/machineconfig/refs/heads/main/src/machineconfig/setup_windows/openssh-server_add-sshkey.ps1"
181
- code = PathExtended(code_url).download().read_text(encoding="utf-8")
182
- self.run(code)
183
-
184
- def copy_env_var(self, name: str):
185
- assert self.get_remote_machine() == "Linux"
186
- return self.run(f"{name} = {os.environ[name]}; export {name}")
158
+ import urllib.request
159
+ with urllib.request.urlopen(code_url) as response:
160
+ code = response.read().decode("utf-8")
161
+ return self.run_shell(command=code, verbose_output=True, description="", strict_stderr=False, strict_return_code=False)
187
162
 
188
163
  def get_remote_repr(self, add_machine: bool = False) -> str:
189
164
  return f"{self.username}@{self.hostname}:{self.port}" + (f" [{self.get_remote_machine()}][{self.get_remote_distro()}]" if add_machine else "")
190
-
191
165
  def get_local_repr(self, add_machine: bool = False) -> str:
192
166
  import getpass
193
-
194
167
  return f"{getpass.getuser()}@{self.platform.node()}" + (f" [{self.platform.system()}][{self.get_local_distro()}]" if add_machine else "")
195
-
196
- def __repr__(self):
168
+ def get_ssh_conn_str(self, command: str) -> str:
169
+ 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 " ")
170
+ def __repr__(self) -> str:
197
171
  return f"local {self.get_local_repr(add_machine=True)} >>> SSH TO >>> remote {self.get_remote_repr(add_machine=True)}"
198
172
 
199
- def run_locally(self, command: str):
173
+ def run_locally(self, command: str) -> Response:
200
174
  print(f"""💻 [LOCAL EXECUTION] Running command on node: {self.platform.node()} Command: {command}""")
201
175
  res = Response(cmd=command)
202
176
  res.output.returncode = os.system(command)
203
177
  return res
204
178
 
205
- def get_ssh_conn_str(self, cmd: str = ""):
206
- return "ssh " + (f" -i {self.sshkey}" if self.sshkey else "") + self.get_remote_repr().replace(":", " -p ") + (f" -t {cmd} " if cmd != "" else " ")
207
179
 
208
- def run(self, cmd: str, verbose: bool = True, desc: str = "", strict_err: bool = False, strict_returncode: bool = False) -> Response:
209
- raw = self.ssh.exec_command(cmd)
210
- res = Response(stdin=raw[0], stdout=raw[1], stderr=raw[2], cmd=cmd, desc=desc) # type: ignore
211
- if not verbose:
212
- res.capture().print_if_unsuccessful(desc=desc, strict_err=strict_err, strict_returncode=strict_returncode, assert_success=False)
213
- else:
180
+ def run_shell(self, command: str, verbose_output: bool, description: str, strict_stderr: bool, strict_return_code: bool) -> Response:
181
+ raw = self.ssh.exec_command(command)
182
+ res = Response(stdin=raw[0], stdout=raw[1], stderr=raw[2], cmd=command, desc=description) # type: ignore
183
+ if verbose_output:
214
184
  res.print()
185
+ else:
186
+ res.capture().print_if_unsuccessful(desc=description, strict_err=strict_stderr, strict_returncode=strict_return_code, assert_success=False)
215
187
  self.terminal_responses.append(res)
216
188
  return res
217
- 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]:
189
+ def run_py(self, python_code: str, description: str, verbose_output: bool, strict_stderr: bool, strict_return_code: bool) -> Response:
218
190
  from machineconfig.utils.accessories import randstr
219
- from pathlib import Path
220
- cmd_path = Path.home().joinpath(f"tmp_results/tmp_scripts/ssh/runpy_{randstr()}.py")
191
+ cmd_path = Path.home().joinpath(f"{DEFAULT_PICKLE_SUBDIR}/runpy_{randstr()}.py")
221
192
  cmd_path.parent.mkdir(parents=True, exist_ok=True)
222
- if not return_obj:
223
- cmd_path.write_text(cmd, encoding="utf-8")
224
- self.copy_from_here(source=cmd_path, target=None)
225
- return self.run(
226
- cmd=f"""$HOME/.local/bin/uv run --with machineconfig>=5.65python {cmd_path.relative_to(Path.home())}""" + '"',
227
- desc=desc or f"run_py on {self.get_remote_repr()}",
228
- verbose=verbose,
229
- strict_err=strict_err,
230
- strict_returncode=strict_returncode,
231
- )
232
- assert "obj=" in cmd, "The command sent to run_py must have `obj=` statement if return_obj is set to True"
233
- return_path = cmd_path.parent.joinpath(f"return_obj_{randstr()}.pkl")
234
- def func(pkl_path_rel2_home: str) -> None:
235
- pickle_path_obj = Path.home().joinpath(pkl_path_rel2_home).expanduser().absolute()
236
- import pickle
237
- obj = globals().get("obj", None)
238
- pickle_path_obj.write_bytes(pickle.dumps(obj))
193
+ cmd_path.write_text(python_code, encoding="utf-8")
194
+ self.copy_from_here(source_path=cmd_path, target_path=None, compress_with_zip=False, recursive=False, overwrite_existing=False)
195
+ uv_cmd = f"""{UV_RUN_CMD} --with {MACHINECONFIG_VERSION} python {cmd_path.relative_to(Path.home())}"""
196
+ 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)
197
+
198
+ def _simple_sftp_get(self, remote_path: str, local_path: Path) -> None:
199
+ """Simple SFTP get without any recursion or path expansion - for internal use only."""
200
+ if self.sftp is None:
201
+ raise RuntimeError(f"SFTP connection not available for {self.hostname}")
202
+ local_path.parent.mkdir(parents=True, exist_ok=True)
203
+ self.sftp.get(remotepath=remote_path, localpath=str(local_path))
204
+
205
+ def _create_remote_target_dir(self, target_path: Union[str, Path], overwrite_existing: bool) -> str:
206
+ """Helper to create a directory on remote machine and return its path."""
207
+ def create_target_dir(target_dir_path: str, overwrite: bool, json_output_path: str) -> str:
208
+ from pathlib import Path
209
+ import shutil
210
+ import json
211
+ directory_path = Path(target_dir_path).expanduser()
212
+ if overwrite and directory_path.exists():
213
+ if directory_path.is_dir():
214
+ shutil.rmtree(directory_path)
215
+ else:
216
+ directory_path.unlink()
217
+ directory_path.parent.mkdir(parents=True, exist_ok=True)
218
+ result_path_posix = directory_path.as_posix()
219
+ json_result_path = Path(json_output_path)
220
+ json_result_path.parent.mkdir(parents=True, exist_ok=True)
221
+ json_result_path.write_text(json.dumps(result_path_posix, indent=2), encoding="utf-8")
222
+ print(json_result_path.as_posix())
223
+ return result_path_posix
239
224
  from machineconfig.utils.meta import function_to_script
240
- cmd_complement = function_to_script(func=func, call_with_kwargs={"pkl_path_rel2_home": return_path.relative_to(Path.home()).as_posix()})
241
- cmd_total = f"""{cmd}
242
- {cmd_complement}
243
- """
244
- cmd_path.write_text(cmd_total, encoding="utf-8")
245
- self.copy_from_here(source=cmd_path, target=None)
246
- _resp = self.run(f"""$HOME/.local/bin/uv run --with machineconfig>=5.65python {cmd_path}""", desc=desc, verbose=verbose, strict_err=True, strict_returncode=True).op.split("\n")[-1]
247
- res = self.copy_to_here(source=None, target=return_path)
248
- import pickle
249
- return pickle.loads(res.read_bytes())
250
-
251
- 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]]:
252
- if init:
253
- print(f"{'⬆️' * 5} [SFTP UPLOAD] FROM `{source}` TO `{target}`") # TODO: using return_obj do all tests required in one go.
254
- source_obj = PathExtended(source).expanduser().absolute()
225
+ from machineconfig.utils.accessories import randstr
226
+ remote_json_output = Path.home().joinpath(f"{DEFAULT_PICKLE_SUBDIR}/return_{randstr()}.json").as_posix()
227
+ 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})
228
+ response = self.run_py(python_code=command, description=f"Creating target directory `{Path(target_path).parent.as_posix()}` @ {self.get_remote_repr(add_machine=False)}", verbose_output=False, strict_stderr=False, strict_return_code=False)
229
+ remote_json_path = response.op.strip()
230
+ if not remote_json_path:
231
+ raise RuntimeError(f"Failed to create target directory {target_path} - no response from remote")
232
+ from machineconfig.utils.accessories import randstr
233
+ local_json = Path.home().joinpath(f"{DEFAULT_PICKLE_SUBDIR}/local_{randstr()}.json")
234
+ self._simple_sftp_get(remote_path=remote_json_path, local_path=local_json)
235
+ import json
236
+ try:
237
+ result = json.loads(local_json.read_text(encoding="utf-8"))
238
+ except (json.JSONDecodeError, FileNotFoundError) as err:
239
+ raise RuntimeError(f"Failed to create target directory {target_path} - invalid JSON response: {err}") from err
240
+ finally:
241
+ if local_json.exists():
242
+ local_json.unlink()
243
+ assert isinstance(result, str), f"Failed to create target directory {target_path} on remote"
244
+ return result
245
+
246
+
247
+ 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:
248
+ if self.sftp is None:
249
+ raise RuntimeError(f"SFTP connection not available for {self.hostname}. Cannot transfer files.")
250
+
251
+ source_obj = Path(source_path).expanduser().absolute()
255
252
  if not source_obj.exists():
256
- raise RuntimeError(f"Meta.SSH Error: source `{source_obj}` does not exist!")
257
- if target is None:
258
- target = PathExtended(source_obj).expanduser().absolute().collapseuser(strict=True)
259
- assert target.is_relative_to("~"), "If target is not specified, source must be relative to home."
260
- if z:
261
- target += ".zip"
262
- if not z and source_obj.is_dir():
263
- if r is False:
264
- 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.")
265
- source_list: list[PathExtended] = source_obj.search("*", folders=False, files=True, r=True)
266
- def func(item: PathExtended, overwrite: bool) -> None:
267
- from machineconfig.utils.path_extended import PathExtended as P
268
- path=P(r'{PathExtended(target).as_posix()}').expanduser()
269
- if overwrite: path.delete(sure=True)
270
- path.parent.mkdir(parents=True, exist_ok=True)
271
- from machineconfig.utils.meta import function_to_script
272
- command = function_to_script(func=func, call_with_kwargs={"item": PathExtended(target).as_posix(), "overwrite": overwrite})
273
- remote_root = (self.run_py(command,
274
- desc=f"Creating Target directory `{PathExtended(target).as_posix()}` @ {self.get_remote_repr()}",
275
- verbose=False,).op or "")
276
- for idx, item in enumerate(source_list):
277
- print(f" {idx + 1:03d}. {item}")
278
- for item in source_list:
279
- a__target = PathExtended(remote_root).joinpath(item.relative_to(source_obj))
280
- self.copy_from_here(source=item, target=a__target)
281
- return list(source_list)
282
- if z:
253
+ raise RuntimeError(f"SSH Error: source `{source_obj}` does not exist!")
254
+
255
+ if target_path is None:
256
+ try:
257
+ target_path_relative = source_obj.relative_to(Path.home())
258
+ target_path = Path("~") / target_path_relative
259
+ except ValueError:
260
+ raise RuntimeError(f"If target is not specified, source must be relative to home directory, but got: {source_obj}")
261
+
262
+ if not compress_with_zip and source_obj.is_dir():
263
+ if not recursive:
264
+ 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.")
265
+ file_paths_to_upload: list[Path] = [file_path for file_path in source_obj.rglob("*") if file_path.is_file()]
266
+ remote_root = self._create_remote_target_dir(target_path=target_path, overwrite_existing=overwrite_existing)
267
+ for idx, file_path in enumerate(file_paths_to_upload):
268
+ print(f" {idx + 1:03d}. {file_path}")
269
+ for file_path in file_paths_to_upload:
270
+ remote_file_target = Path(remote_root).joinpath(file_path.relative_to(source_obj))
271
+ self.copy_from_here(source_path=file_path, target_path=remote_file_target, compress_with_zip=False, recursive=False, overwrite_existing=overwrite_existing)
272
+ return Path(remote_root)
273
+ if compress_with_zip:
283
274
  print("🗜️ ZIPPING ...")
284
- 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.
285
- remotepath = (
286
- self.run_py(
287
- f"""
288
- path=P(r'{PathExtended(target).as_posix()}').expanduser()
289
- {'path.delete(sure=True)' if overwrite else ''}
290
- print(path.parent.create())""",
291
- desc=f"Creating Target directory `{PathExtended(target).parent.as_posix()}` @ {self.get_remote_repr()}",
292
- verbose=False,
293
- ).op
294
- or ""
295
- )
296
- remotepath = PathExtended(remotepath.split("\n")[-1]).joinpath(PathExtended(target).name)
297
- print(f"""📤 [SFTP UPLOAD] Sending file: {repr(PathExtended(source_obj))} ==> Remote Path: {remotepath.as_posix()}""")
298
- with self.tqdm_wrap(ascii=True, unit="b", unit_scale=True) as pbar:
299
- self.sftp.put(localpath=PathExtended(source_obj).expanduser(), remotepath=remotepath.as_posix(), callback=pbar.view_bar) # type: ignore # pylint: disable=E1129
300
- if z:
301
- _resp = self.run_py(f"""
302
- from machineconfig.utils.path_extended import PathExtended as P;
303
- 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)
304
- source_obj.delete(sure=True)
305
- print("\n")
275
+ import shutil
276
+ zip_path = Path(str(source_obj) + "_archive")
277
+ if source_obj.is_dir():
278
+ shutil.make_archive(str(zip_path), "zip", source_obj)
279
+ else:
280
+ shutil.make_archive(str(zip_path), "zip", source_obj.parent, source_obj.name)
281
+ source_obj = Path(str(zip_path) + ".zip")
282
+ if not str(target_path).endswith(".zip"):
283
+ target_path = Path(str(target_path) + ".zip")
284
+ remotepath_str = self._create_remote_target_dir(target_path=target_path, overwrite_existing=overwrite_existing)
285
+ remotepath = Path(remotepath_str)
286
+ print(f"""📤 [SFTP UPLOAD] Sending file: {repr(source_obj)} ==> Remote Path: {remotepath.as_posix()}""")
287
+ try:
288
+ with self.tqdm_wrap(ascii=True, unit="b", unit_scale=True) as pbar:
289
+ if self.sftp is None: # type: ignore[unreachable]
290
+ raise RuntimeError(f"SFTP connection lost for {self.hostname}")
291
+ self.sftp.put(localpath=str(source_obj), remotepath=remotepath.as_posix(), callback=pbar.view_bar) # type: ignore
292
+ except Exception:
293
+ if compress_with_zip and source_obj.exists() and str(source_obj).endswith("_archive.zip"):
294
+ source_obj.unlink()
295
+ raise
296
+
297
+ if compress_with_zip:
298
+ def unzip_archive(zip_file_path: str, overwrite_flag: bool) -> None:
299
+ from pathlib import Path
300
+ import shutil
301
+ import zipfile
302
+ archive_path = Path(zip_file_path).expanduser()
303
+ extraction_directory = archive_path.parent / archive_path.stem
304
+ if overwrite_flag and extraction_directory.exists():
305
+ shutil.rmtree(extraction_directory)
306
+ with zipfile.ZipFile(archive_path, "r") as archive_handle:
307
+ archive_handle.extractall(extraction_directory)
308
+ archive_path.unlink()
309
+ from machineconfig.utils.meta import function_to_script
310
+ command = function_to_script(func=unzip_archive, call_with_kwargs={"zip_file_path": remotepath.as_posix(), "overwrite_flag": overwrite_existing})
311
+ _resp = self.run_py(python_code=command, description=f"UNZIPPING {remotepath.as_posix()}", verbose_output=False, strict_stderr=True, strict_return_code=True)
312
+ source_obj.unlink()
313
+ print("\n")
306
314
  return source_obj
307
315
 
308
- def copy_to_here(self, source: PLike, target: OPLike = None, z: bool = False, r: bool = False, init: bool = True) -> PathExtended:
309
- if init:
316
+ def _check_remote_is_dir(self, source_path: Union[str, Path]) -> bool:
317
+ """Helper to check if a remote path is a directory."""
318
+ def check_is_dir(path_to_check: str, json_output_path: str) -> bool:
319
+ from pathlib import Path
320
+ import json
321
+ is_directory = Path(path_to_check).expanduser().absolute().is_dir()
322
+ json_result_path = Path(json_output_path)
323
+ json_result_path.parent.mkdir(parents=True, exist_ok=True)
324
+ json_result_path.write_text(json.dumps(is_directory, indent=2), encoding="utf-8")
325
+ print(json_result_path.as_posix())
326
+ return is_directory
327
+
328
+ from machineconfig.utils.meta import function_to_script
329
+ from machineconfig.utils.accessories import randstr
330
+ remote_json_output = Path.home().joinpath(f"{DEFAULT_PICKLE_SUBDIR}/return_{randstr()}.json").as_posix()
331
+ command = function_to_script(func=check_is_dir, call_with_kwargs={"path_to_check": str(source_path), "json_output_path": remote_json_output})
332
+ response = self.run_py(python_code=command, description=f"Check if source `{source_path}` is a dir", verbose_output=False, strict_stderr=False, strict_return_code=False)
333
+ remote_json_path = response.op.strip()
334
+ if not remote_json_path:
335
+ raise RuntimeError(f"Failed to check if {source_path} is directory - no response from remote")
336
+ from machineconfig.utils.accessories import randstr
337
+ local_json = Path.home().joinpath(f"{DEFAULT_PICKLE_SUBDIR}/local_{randstr()}.json")
338
+ self._simple_sftp_get(remote_path=remote_json_path, local_path=local_json)
339
+ import json
340
+ try:
341
+ result = json.loads(local_json.read_text(encoding="utf-8"))
342
+ except (json.JSONDecodeError, FileNotFoundError) as err:
343
+ raise RuntimeError(f"Failed to check if {source_path} is directory - invalid JSON response: {err}") from err
344
+ finally:
345
+ if local_json.exists():
346
+ local_json.unlink()
347
+ assert isinstance(result, bool), f"Failed to check if {source_path} is directory"
348
+ return result
349
+
350
+ def _expand_remote_path(self, source_path: Union[str, Path]) -> str:
351
+ """Helper to expand a path on the remote machine."""
352
+ def expand_source(path_to_expand: str, json_output_path: str) -> str:
353
+ from pathlib import Path
354
+ import json
355
+ expanded_path_posix = Path(path_to_expand).expanduser().absolute().as_posix()
356
+ json_result_path = Path(json_output_path)
357
+ json_result_path.parent.mkdir(parents=True, exist_ok=True)
358
+ json_result_path.write_text(json.dumps(expanded_path_posix, indent=2), encoding="utf-8")
359
+ print(json_result_path.as_posix())
360
+ return expanded_path_posix
361
+
362
+ from machineconfig.utils.meta import function_to_script
363
+ from machineconfig.utils.accessories import randstr
364
+ remote_json_output = Path.home().joinpath(f"{DEFAULT_PICKLE_SUBDIR}/return_{randstr()}.json").as_posix()
365
+ command = function_to_script(func=expand_source, call_with_kwargs={"path_to_expand": str(source_path), "json_output_path": remote_json_output})
366
+ response = self.run_py(python_code=command, description="Resolving source path by expanding user", verbose_output=False, strict_stderr=False, strict_return_code=False)
367
+ remote_json_path = response.op.strip()
368
+ if not remote_json_path:
369
+ raise RuntimeError(f"Could not resolve source path {source_path} - no response from remote")
370
+ from machineconfig.utils.accessories import randstr
371
+ local_json = Path.home().joinpath(f"{DEFAULT_PICKLE_SUBDIR}/local_{randstr()}.json")
372
+ self._simple_sftp_get(remote_path=remote_json_path, local_path=local_json)
373
+ import json
374
+ try:
375
+ result = json.loads(local_json.read_text(encoding="utf-8"))
376
+ except (json.JSONDecodeError, FileNotFoundError) as err:
377
+ raise RuntimeError(f"Could not resolve source path {source_path} - invalid JSON response: {err}") from err
378
+ finally:
379
+ if local_json.exists():
380
+ local_json.unlink()
381
+ assert isinstance(result, str), f"Could not resolve source path {source_path}"
382
+ return result
383
+
384
+ def copy_to_here(self, source: Union[str, Path], target: Optional[Union[str, Path]], compress_with_zip: bool = False, recursive: bool = False, internal_call: bool = False) -> Path:
385
+ if self.sftp is None:
386
+ raise RuntimeError(f"SFTP connection not available for {self.hostname}. Cannot transfer files.")
387
+
388
+ if not internal_call:
310
389
  print(f"{'⬇️' * 5} SFTP DOWNLOADING FROM `{source}` TO `{target}`")
311
- if not z and self.run_py(f"""
312
- from machineconfig.utils.path_extended import PathExtended as P
313
- 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":
314
- if r is False:
315
- raise RuntimeError(f"source `{source}` is a directory! either set r=True for recursive sending or raise zip_first flag.")
316
- source_list = self.run_py(f"""
317
- from machineconfig.utils.path_extended import PathExtended as P
318
- obj=P(r'{source}').search(folders=False, r=True).collapseuser(strict=False)
319
- """, desc="Searching for files in source", return_obj=True, verbose=False)
320
- assert isinstance(source_list, List), f"Could not resolve source path {source} due to error"
321
- for file in source_list:
322
- self.copy_to_here(source=file.as_posix(), target=PathExtended(target).joinpath(PathExtended(file).relative_to(source)) if target else None, r=False)
323
- if z:
324
- tmp: Response = self.run_py(f"from machineconfig.utils.path_extended import PathExtended as P; print(P(r'{source}').expanduser().zip(inplace=False, verbose=False))", desc=f"Zipping source file {source}", verbose=False)
325
- tmp2 = tmp.op2path(strict_returncode=True, strict_err=True)
326
- if not isinstance(tmp2, PathExtended):
327
- raise RuntimeError(f"Could not zip {source} due to {tmp.err}")
328
- else:
329
- source = tmp2
390
+
391
+ source_obj = Path(source)
392
+ expanded_source = self._expand_remote_path(source_path=source_obj)
393
+
394
+ if not compress_with_zip:
395
+ is_dir = self._check_remote_is_dir(source_path=expanded_source)
396
+
397
+ if is_dir:
398
+ if not recursive:
399
+ raise RuntimeError(f"SSH Error: source `{source_obj}` is a directory! Set recursive=True for recursive transfer or compress_with_zip=True to zip it.")
400
+
401
+ def search_files(directory_path: str, json_output_path: str) -> list[str]:
402
+ from pathlib import Path
403
+ import json
404
+ file_paths_list = [file_path.as_posix() for file_path in Path(directory_path).expanduser().absolute().rglob("*") if file_path.is_file()]
405
+ json_result_path = Path(json_output_path)
406
+ json_result_path.parent.mkdir(parents=True, exist_ok=True)
407
+ json_result_path.write_text(json.dumps(file_paths_list, indent=2), encoding="utf-8")
408
+ print(json_result_path.as_posix())
409
+ return file_paths_list
410
+
411
+ from machineconfig.utils.meta import function_to_script
412
+ from machineconfig.utils.accessories import randstr
413
+ remote_json_output = Path.home().joinpath(f"{DEFAULT_PICKLE_SUBDIR}/return_{randstr()}.json").as_posix()
414
+ command = function_to_script(func=search_files, call_with_kwargs={"directory_path": expanded_source, "json_output_path": remote_json_output})
415
+ response = self.run_py(python_code=command, description="Searching for files in source", verbose_output=False, strict_stderr=False, strict_return_code=False)
416
+ remote_json_path = response.op.strip()
417
+ if not remote_json_path:
418
+ raise RuntimeError(f"Could not resolve source path {source} - no response from remote")
419
+ from machineconfig.utils.accessories import randstr
420
+ local_json = Path.home().joinpath(f"{DEFAULT_PICKLE_SUBDIR}/local_{randstr()}.json")
421
+ self._simple_sftp_get(remote_path=remote_json_path, local_path=local_json)
422
+ import json
423
+ try:
424
+ source_list_str = json.loads(local_json.read_text(encoding="utf-8"))
425
+ except (json.JSONDecodeError, FileNotFoundError) as err:
426
+ raise RuntimeError(f"Could not resolve source path {source} - invalid JSON response: {err}") from err
427
+ finally:
428
+ if local_json.exists():
429
+ local_json.unlink()
430
+ assert isinstance(source_list_str, list), f"Could not resolve source path {source}"
431
+ file_paths_to_download = [Path(file_path_str) for file_path_str in source_list_str]
432
+
433
+ if target is None:
434
+ def collapse_to_home_dir(absolute_path: str, json_output_path: str) -> str:
435
+ from pathlib import Path
436
+ import json
437
+ source_absolute_path = Path(absolute_path).expanduser().absolute()
438
+ try:
439
+ relative_to_home = source_absolute_path.relative_to(Path.home())
440
+ collapsed_path_posix = (Path("~") / relative_to_home).as_posix()
441
+ json_result_path = Path(json_output_path)
442
+ json_result_path.parent.mkdir(parents=True, exist_ok=True)
443
+ json_result_path.write_text(json.dumps(collapsed_path_posix, indent=2), encoding="utf-8")
444
+ print(json_result_path.as_posix())
445
+ return collapsed_path_posix
446
+ except ValueError:
447
+ raise RuntimeError(f"Source path must be relative to home directory: {source_absolute_path}")
448
+
449
+ from machineconfig.utils.meta import function_to_script
450
+ from machineconfig.utils.accessories import randstr
451
+ remote_json_output = Path.home().joinpath(f"{DEFAULT_PICKLE_SUBDIR}/return_{randstr()}.json").as_posix()
452
+ command = function_to_script(func=collapse_to_home_dir, call_with_kwargs={"absolute_path": expanded_source, "json_output_path": remote_json_output})
453
+ response = self.run_py(python_code=command, description="Finding default target via relative source path", verbose_output=False, strict_stderr=False, strict_return_code=False)
454
+ remote_json_path_dir = response.op.strip()
455
+ if not remote_json_path_dir:
456
+ raise RuntimeError("Could not resolve target path - no response from remote")
457
+ from machineconfig.utils.accessories import randstr
458
+ local_json_dir = Path.home().joinpath(f"{DEFAULT_PICKLE_SUBDIR}/local_{randstr()}.json")
459
+ self._simple_sftp_get(remote_path=remote_json_path_dir, local_path=local_json_dir)
460
+ import json
461
+ try:
462
+ target_dir_str = json.loads(local_json_dir.read_text(encoding="utf-8"))
463
+ except (json.JSONDecodeError, FileNotFoundError) as err:
464
+ raise RuntimeError(f"Could not resolve target path - invalid JSON response: {err}") from err
465
+ finally:
466
+ if local_json_dir.exists():
467
+ local_json_dir.unlink()
468
+ assert isinstance(target_dir_str, str), "Could not resolve target path"
469
+ target = Path(target_dir_str)
470
+
471
+ target_dir = Path(target).expanduser().absolute()
472
+
473
+ for idx, file_path in enumerate(file_paths_to_download):
474
+ print(f" {idx + 1:03d}. {file_path}")
475
+
476
+ for file_path in file_paths_to_download:
477
+ local_file_target = target_dir.joinpath(Path(file_path).relative_to(expanded_source))
478
+ self.copy_to_here(source=file_path, target=local_file_target, compress_with_zip=False, recursive=False, internal_call=True)
479
+
480
+ return target_dir
481
+
482
+ if compress_with_zip:
483
+ print("🗜️ ZIPPING ...")
484
+ def zip_source(path_to_zip: str, json_output_path: str) -> str:
485
+ from pathlib import Path
486
+ import shutil
487
+ import json
488
+ source_to_compress = Path(path_to_zip).expanduser().absolute()
489
+ archive_base_path = source_to_compress.parent / (source_to_compress.name + "_archive")
490
+ if source_to_compress.is_dir():
491
+ shutil.make_archive(str(archive_base_path), "zip", source_to_compress)
492
+ else:
493
+ shutil.make_archive(str(archive_base_path), "zip", source_to_compress.parent, source_to_compress.name)
494
+ zip_file_path = str(archive_base_path) + ".zip"
495
+ json_result_path = Path(json_output_path)
496
+ json_result_path.parent.mkdir(parents=True, exist_ok=True)
497
+ json_result_path.write_text(json.dumps(zip_file_path, indent=2), encoding="utf-8")
498
+ print(json_result_path.as_posix())
499
+ return zip_file_path
500
+
501
+ from machineconfig.utils.meta import function_to_script
502
+ from machineconfig.utils.accessories import randstr
503
+ remote_json_output = Path.home().joinpath(f"{DEFAULT_PICKLE_SUBDIR}/return_{randstr()}.json").as_posix()
504
+ command = function_to_script(func=zip_source, call_with_kwargs={"path_to_zip": expanded_source, "json_output_path": remote_json_output})
505
+ response = self.run_py(python_code=command, description=f"Zipping source file {source}", verbose_output=False, strict_stderr=False, strict_return_code=False)
506
+ remote_json_path = response.op.strip()
507
+ if not remote_json_path:
508
+ raise RuntimeError(f"Could not zip {source} - no response from remote")
509
+ from machineconfig.utils.accessories import randstr
510
+ local_json = Path.home().joinpath(f"{DEFAULT_PICKLE_SUBDIR}/local_{randstr()}.json")
511
+ self._simple_sftp_get(remote_path=remote_json_path, local_path=local_json)
512
+ import json
513
+ try:
514
+ zipped_path = json.loads(local_json.read_text(encoding="utf-8"))
515
+ except (json.JSONDecodeError, FileNotFoundError) as err:
516
+ raise RuntimeError(f"Could not zip {source} - invalid JSON response: {err}") from err
517
+ finally:
518
+ if local_json.exists():
519
+ local_json.unlink()
520
+ assert isinstance(zipped_path, str), f"Could not zip {source}"
521
+ source_obj = Path(zipped_path)
522
+ expanded_source = zipped_path
523
+
330
524
  if target is None:
331
- 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()
332
- if isinstance(tmpx, PathExtended):
333
- target = tmpx
334
- else:
335
- raise RuntimeError(f"Could not resolve target path {target} due to error")
336
- assert target.is_relative_to("~"), f"If target is not specified, source must be relative to home.\n{target=}"
337
- target_obj = PathExtended(target).expanduser().absolute()
525
+ def collapse_to_home(absolute_path: str, json_output_path: str) -> str:
526
+ from pathlib import Path
527
+ import json
528
+ source_absolute_path = Path(absolute_path).expanduser().absolute()
529
+ try:
530
+ relative_to_home = source_absolute_path.relative_to(Path.home())
531
+ collapsed_path_posix = (Path("~") / relative_to_home).as_posix()
532
+ json_result_path = Path(json_output_path)
533
+ json_result_path.parent.mkdir(parents=True, exist_ok=True)
534
+ json_result_path.write_text(json.dumps(collapsed_path_posix, indent=2), encoding="utf-8")
535
+ print(json_result_path.as_posix())
536
+ return collapsed_path_posix
537
+ except ValueError:
538
+ raise RuntimeError(f"Source path must be relative to home directory: {source_absolute_path}")
539
+
540
+ from machineconfig.utils.meta import function_to_script
541
+ from machineconfig.utils.accessories import randstr
542
+ remote_json_output = Path.home().joinpath(f"{DEFAULT_PICKLE_SUBDIR}/return_{randstr()}.json").as_posix()
543
+ command = function_to_script(func=collapse_to_home, call_with_kwargs={"absolute_path": expanded_source, "json_output_path": remote_json_output})
544
+ response = self.run_py(python_code=command, description="Finding default target via relative source path", verbose_output=False, strict_stderr=False, strict_return_code=False)
545
+ remote_json_path = response.op.strip()
546
+ if not remote_json_path:
547
+ raise RuntimeError("Could not resolve target path - no response from remote")
548
+ from machineconfig.utils.accessories import randstr
549
+ local_json = Path.home().joinpath(f"{DEFAULT_PICKLE_SUBDIR}/local_{randstr()}.json")
550
+ self._simple_sftp_get(remote_path=remote_json_path, local_path=local_json)
551
+ import json
552
+ try:
553
+ target_str = json.loads(local_json.read_text(encoding="utf-8"))
554
+ except (json.JSONDecodeError, FileNotFoundError) as err:
555
+ raise RuntimeError(f"Could not resolve target path - invalid JSON response: {err}") from err
556
+ finally:
557
+ if local_json.exists():
558
+ local_json.unlink()
559
+ assert isinstance(target_str, str), "Could not resolve target path"
560
+ target = Path(target_str)
561
+ assert str(target).startswith("~"), f"If target is not specified, source must be relative to home.\n{target=}"
562
+
563
+ target_obj = Path(target).expanduser().absolute()
338
564
  target_obj.parent.mkdir(parents=True, exist_ok=True)
339
- if z and ".zip" not in target_obj.suffix:
340
- target_obj += ".zip"
341
- if "~" in str(source):
342
- 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()
343
- if isinstance(tmp3, PathExtended):
344
- source = tmp3
345
- else:
346
- raise RuntimeError(f"Could not resolve source path {source} due to")
347
- else:
348
- source = PathExtended(source)
349
- print(f"""📥 [DOWNLOAD] Receiving: {source} ==> Local Path: {target_obj}""")
350
- with self.tqdm_wrap(ascii=True, unit="b", unit_scale=True) as pbar: # type: ignore # pylint: disable=E1129
351
- assert self.sftp is not None, f"Could not establish SFTP connection to {self.hostname}."
352
- self.sftp.get(remotepath=source.as_posix(), localpath=str(target_obj), callback=pbar.view_bar) # type: ignore
353
- if z:
354
- target_obj = target_obj.unzip(inplace=True, content=True)
355
- self.run_py(f"from machineconfig.utils.path_extended import PathExtended as P; P(r'{source.as_posix()}').delete(sure=True)", desc="Cleaning temp zip files @ remote.", strict_returncode=True, strict_err=True, verbose=False)
565
+
566
+ if compress_with_zip and target_obj.suffix != ".zip":
567
+ target_obj = target_obj.with_suffix(target_obj.suffix + ".zip")
568
+
569
+ print(f"""📥 [DOWNLOAD] Receiving: {expanded_source} ==> Local Path: {target_obj}""")
570
+ try:
571
+ with self.tqdm_wrap(ascii=True, unit="b", unit_scale=True) as pbar:
572
+ if self.sftp is None: # type: ignore[unreachable]
573
+ raise RuntimeError(f"SFTP connection lost for {self.hostname}")
574
+ self.sftp.get(remotepath=expanded_source, localpath=str(target_obj), callback=pbar.view_bar) # type: ignore
575
+ except Exception:
576
+ if target_obj.exists():
577
+ target_obj.unlink()
578
+ raise
579
+
580
+ if compress_with_zip:
581
+ import zipfile
582
+ extract_to = target_obj.parent / target_obj.stem
583
+ with zipfile.ZipFile(target_obj, "r") as zip_ref:
584
+ zip_ref.extractall(extract_to)
585
+ target_obj.unlink()
586
+ target_obj = extract_to
587
+
588
+ def delete_temp_zip(path_to_delete: str) -> None:
589
+ from pathlib import Path
590
+ import shutil
591
+ file_or_dir_path = Path(path_to_delete)
592
+ if file_or_dir_path.exists():
593
+ if file_or_dir_path.is_dir():
594
+ shutil.rmtree(file_or_dir_path)
595
+ else:
596
+ file_or_dir_path.unlink()
597
+
598
+ from machineconfig.utils.meta import function_to_script
599
+ command = function_to_script(func=delete_temp_zip, call_with_kwargs={"path_to_delete": expanded_source})
600
+ self.run_py(python_code=command, description="Cleaning temp zip files @ remote.", verbose_output=False, strict_stderr=True, strict_return_code=True)
601
+
356
602
  print("\n")
357
603
  return target_obj
358
604
 
359
- # def receieve(self, source: PLike, target: OPLike = None, z: bool = False, r: bool = False) -> PathExtended:
360
- # 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)
361
- # assert isinstance(scout, Scout)
362
- # if not z and scout.is_dir and scout.files is not None:
363
- # if r:
364
- # 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]
365
- # return tmp[0]
366
- # else:
367
- # print("Source is a directory! either set `r=True` for recursive sending or raise `zip_first=True` flag.")
368
- # if target:
369
- # target = PathExtended(target).expanduser().absolute()
370
- # else:
371
- # target = scout.source_rel2home.expanduser().absolute()
372
- # target.parent.mkdir(parents=True, exist_ok=True)
373
- # if z and ".zip" not in target.suffix:
374
- # target += ".zip"
375
- # source = scout.source_full
376
- # with self.tqdm_wrap(ascii=True, unit="b", unit_scale=True) as pbar:
377
- # self.sftp.get(remotepath=source.as_posix(), localpath=target.as_posix(), callback=pbar.view_bar) # type: ignore # pylint: disable=E1129
378
- # if z:
379
- # target = target.unzip(inplace=True, content=True)
380
- # self.run_py(f"""
381
- # from machineconfig.utils.path_extended import PathExtended as P;
382
- # P(r'{source.as_posix()}').delete(sure=True)
383
- # """, desc="Cleaning temp zip files @ remote.", strict_returncode=True, strict_err=True)
384
- # print("\n")
385
- # return target