machineconfig 7.49__py3-none-any.whl → 7.64__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 (93) hide show
  1. machineconfig/cluster/sessions_managers/utils/maker.py +21 -11
  2. machineconfig/jobs/installer/custom/boxes.py +2 -2
  3. machineconfig/jobs/installer/custom/hx.py +16 -12
  4. machineconfig/jobs/installer/custom_dev/brave.py +1 -1
  5. machineconfig/jobs/installer/custom_dev/cloudflare_warp_cli.py +23 -0
  6. machineconfig/jobs/installer/custom_dev/code.py +4 -1
  7. machineconfig/jobs/installer/custom_dev/dubdb_adbc.py +1 -1
  8. machineconfig/jobs/installer/custom_dev/nerfont_windows_helper.py +1 -10
  9. machineconfig/jobs/installer/custom_dev/sysabc.py +119 -0
  10. machineconfig/jobs/installer/custom_dev/wezterm.py +2 -19
  11. machineconfig/jobs/installer/installer_data.json +739 -25
  12. machineconfig/jobs/installer/linux_scripts/redis.sh +1 -0
  13. machineconfig/jobs/installer/package_groups.py +49 -83
  14. machineconfig/logger.py +0 -1
  15. machineconfig/profile/create_links_export.py +21 -7
  16. machineconfig/profile/mapper.toml +1 -4
  17. machineconfig/scripts/linux/wrap_mcfg +1 -1
  18. machineconfig/scripts/python/croshell.py +20 -43
  19. machineconfig/scripts/python/devops.py +3 -4
  20. machineconfig/scripts/python/env_manager/path_manager_tui.py +1 -1
  21. machineconfig/scripts/python/fire_jobs.py +53 -39
  22. machineconfig/scripts/python/ftpx.py +4 -2
  23. machineconfig/scripts/python/helpers_agents/agentic_frameworks/fire_qwen.py +0 -12
  24. machineconfig/scripts/python/helpers_croshell/crosh.py +3 -3
  25. machineconfig/scripts/python/helpers_devops/cli_config.py +3 -19
  26. machineconfig/scripts/python/helpers_devops/cli_config_dotfile.py +22 -13
  27. machineconfig/scripts/python/helpers_devops/cli_self.py +12 -6
  28. machineconfig/scripts/python/helpers_devops/cli_share_file.py +2 -2
  29. machineconfig/scripts/python/helpers_devops/cli_share_server.py +1 -1
  30. machineconfig/scripts/python/helpers_devops/cli_terminal.py +1 -1
  31. machineconfig/scripts/python/helpers_devops/cli_utils.py +1 -152
  32. machineconfig/scripts/python/helpers_devops/devops_backup_retrieve.py +4 -4
  33. machineconfig/scripts/python/helpers_fire_command/file_wrangler.py +2 -20
  34. machineconfig/scripts/python/helpers_fire_command/fire_jobs_route_helper.py +3 -4
  35. machineconfig/scripts/python/helpers_msearch/scripts_linux/fzfg +1 -1
  36. machineconfig/scripts/python/helpers_repos/clone.py +0 -1
  37. machineconfig/scripts/python/helpers_repos/cloud_repo_sync.py +9 -3
  38. machineconfig/scripts/python/helpers_repos/count_lines_frontend.py +1 -1
  39. machineconfig/scripts/python/helpers_repos/entrypoint.py +2 -1
  40. machineconfig/scripts/python/helpers_repos/record.py +2 -1
  41. machineconfig/scripts/python/helpers_sessions/sessions_multiprocess.py +7 -7
  42. machineconfig/scripts/python/helpers_utils/download.py +151 -0
  43. machineconfig/scripts/python/helpers_utils/path.py +106 -0
  44. machineconfig/scripts/python/interactive.py +17 -26
  45. machineconfig/scripts/python/nw/devops_add_ssh_key.py +21 -5
  46. machineconfig/scripts/python/nw/ssh_debug_linux.py +7 -7
  47. machineconfig/scripts/python/nw/ssh_debug_windows.py +4 -4
  48. machineconfig/scripts/python/nw/wsl_windows_transfer.py +3 -2
  49. machineconfig/scripts/python/sessions.py +37 -22
  50. machineconfig/scripts/python/utils.py +8 -3
  51. machineconfig/scripts/windows/mounts/mount_ssh.ps1 +1 -1
  52. machineconfig/settings/shells/nushell/init.nu +2 -2
  53. machineconfig/settings/shells/wezterm/wezterm.lua +2 -0
  54. machineconfig/settings/shells/zsh/init.sh +1 -8
  55. machineconfig/settings/yazi/init.lua +45 -24
  56. machineconfig/settings/yazi/keymap_windows.toml +1 -2
  57. machineconfig/settings/yazi/shell/yazi_cd.ps1 +29 -5
  58. machineconfig/setup_linux/__init__.py +0 -1
  59. machineconfig/setup_linux/web_shortcuts/interactive.sh +12 -10
  60. machineconfig/setup_mac/__init__.py +2 -3
  61. machineconfig/setup_windows/__init__.py +0 -3
  62. machineconfig/setup_windows/web_shortcuts/interactive.ps1 +12 -10
  63. machineconfig/setup_windows/web_shortcuts/quick_init.ps1 +16 -0
  64. machineconfig/utils/code.py +2 -2
  65. machineconfig/utils/files/headers.py +2 -2
  66. machineconfig/utils/installer_utils/installer_class.py +42 -40
  67. machineconfig/utils/installer_utils/{installer.py → installer_cli.py} +61 -101
  68. machineconfig/utils/installer_utils/{installer_abc.py → installer_locator_utils.py} +0 -68
  69. machineconfig/utils/{installer.py → installer_utils/installer_runner.py} +11 -51
  70. machineconfig/utils/io.py +0 -1
  71. machineconfig/utils/meta.py +29 -15
  72. machineconfig/utils/options.py +1 -1
  73. machineconfig/utils/path_extended.py +40 -19
  74. machineconfig/utils/path_helper.py +75 -21
  75. machineconfig/utils/schemas/layouts/layout_types.py +1 -1
  76. machineconfig/utils/ssh.py +3 -3
  77. machineconfig-7.64.dist-info/METADATA +124 -0
  78. {machineconfig-7.49.dist-info → machineconfig-7.64.dist-info}/RECORD +84 -87
  79. machineconfig/jobs/installer/linux_scripts/pgsql.sh +0 -41
  80. machineconfig/jobs/installer/linux_scripts/timescaledb.sh +0 -71
  81. machineconfig/jobs/installer/powershell_scripts/archive_pygraphviz.ps1 +0 -12
  82. machineconfig/scripts/python/nw/add_ssh_key.py +0 -148
  83. machineconfig/settings/lf/linux/exe/fzf_nano.sh +0 -16
  84. machineconfig/setup_linux/apps.sh +0 -66
  85. machineconfig/setup_mac/apps.sh +0 -73
  86. machineconfig/setup_windows/apps.ps1 +0 -62
  87. machineconfig-7.49.dist-info/METADATA +0 -92
  88. /machineconfig/jobs/installer/linux_scripts/{warp-cli.sh → cloudflare_warp_cli.sh} +0 -0
  89. /machineconfig/{jobs/installer/powershell_scripts → setup_windows/ssh}/openssh-server_add_key.ps1 +0 -0
  90. /machineconfig/{jobs/installer/powershell_scripts → setup_windows/ssh}/openssh-server_copy-ssh-id.ps1 +0 -0
  91. {machineconfig-7.49.dist-info → machineconfig-7.64.dist-info}/WHEEL +0 -0
  92. {machineconfig-7.49.dist-info → machineconfig-7.64.dist-info}/entry_points.txt +0 -0
  93. {machineconfig-7.49.dist-info → machineconfig-7.64.dist-info}/top_level.txt +0 -0
@@ -1,9 +1,9 @@
1
1
  """package manager"""
2
2
 
3
- from machineconfig.utils.installer_utils.installer_abc import check_if_installed_already, parse_apps_installer_linux, parse_apps_installer_windows
3
+ from machineconfig.utils.installer_utils.installer_locator_utils import check_if_installed_already
4
4
  from machineconfig.utils.installer_utils.installer_class import Installer
5
5
  from machineconfig.utils.schemas.installer.installer_types import InstallerData, InstallerDataFiles, get_normalized_arch, get_os_name, OPERATING_SYSTEMS, CPU_ARCHITECTURES
6
- from machineconfig.jobs.installer.package_groups import PACKAGE_GROUPS, PACKAGE_GROUP2NAMES
6
+ from machineconfig.jobs.installer.package_groups import PACKAGE_GROUP2NAMES
7
7
  from machineconfig.utils.path_extended import PathExtended
8
8
  from machineconfig.utils.source_of_truth import INSTALL_VERSION_ROOT, LINUX_INSTALL_PATH
9
9
  from machineconfig.utils.io import read_json
@@ -18,7 +18,7 @@ from joblib import Parallel, delayed
18
18
  def check_latest():
19
19
  console = Console() # Added console initialization
20
20
  console.print(Panel("🔍 CHECKING FOR LATEST VERSIONS", title="Status", expand=False)) # Replaced print with Panel
21
- installers = get_installers(os=get_os_name(), arch=get_normalized_arch(), which_cats=["ESSENTIAL"])
21
+ installers = get_installers(os=get_os_name(), arch=get_normalized_arch(), which_cats=["termabc"])
22
22
  installers_github = []
23
23
  for inst__ in installers:
24
24
  app_name = inst__["appName"]
@@ -91,7 +91,7 @@ def get_installed_cli_apps():
91
91
  return apps
92
92
 
93
93
 
94
- def get_installers(os: OPERATING_SYSTEMS, arch: CPU_ARCHITECTURES, which_cats: Optional[list[PACKAGE_GROUPS]]) -> list[InstallerData]:
94
+ def get_installers(os: OPERATING_SYSTEMS, arch: CPU_ARCHITECTURES, which_cats: Optional[list[str]]) -> list[InstallerData]:
95
95
  res_all = get_all_installer_data_files()
96
96
  acceptable_apps_names: list[str] | None = None
97
97
  if which_cats is not None:
@@ -105,8 +105,13 @@ def get_installers(os: OPERATING_SYSTEMS, arch: CPU_ARCHITECTURES, which_cats: O
105
105
  if acceptable_apps_names is not None:
106
106
  if installer_data["appName"] not in acceptable_apps_names:
107
107
  continue
108
- if installer_data["fileNamePattern"][arch][os] is None:
109
- continue
108
+ try:
109
+ if installer_data["fileNamePattern"][arch][os] is None:
110
+ continue
111
+ except KeyError as ke:
112
+ print(f"❌ ERROR: Missing key in installer data: {ke}")
113
+ print(f"Installer data: {installer_data}")
114
+ raise KeyError(f"Missing key in installer data: {ke}")
110
115
  all_installers.append(installer_data)
111
116
  return all_installers
112
117
 
@@ -119,27 +124,6 @@ def get_all_installer_data_files() -> list[InstallerData]:
119
124
  return res_final
120
125
 
121
126
 
122
- def dynamically_extract_installers_system_groups_from_scripts():
123
- res_final: list[InstallerData] = []
124
- from platform import system
125
- if system() == "Windows":
126
- from machineconfig.setup_windows import APPS
127
- options_system = parse_apps_installer_windows(APPS.read_text(encoding="utf-8"))
128
- elif system() == "Linux":
129
- from machineconfig.setup_linux import APPS
130
- options_system = parse_apps_installer_linux(APPS.read_text(encoding="utf-8"))
131
- elif system() == "Darwin":
132
- from machineconfig.setup_mac import APPS
133
- options_system = parse_apps_installer_linux(APPS.read_text(encoding="utf-8"))
134
- else:
135
- raise NotImplementedError(f"❌ System {system()} not supported")
136
- os_name = get_os_name()
137
- for group_name, (docs, script) in options_system.items():
138
- item: InstallerData = {"appName": group_name, "doc": docs, "repoURL": "CMD", "fileNamePattern": {"amd64": {os_name: script}, "arm64": {os_name: script}}}
139
- res_final.append(item)
140
- return res_final
141
-
142
-
143
127
  def install_bulk(installers_data: list[InstallerData], safe: bool = False, jobs: int = 10, fresh: bool = False):
144
128
  print("🚀 BULK INSTALLATION PROCESS 🚀")
145
129
  if fresh:
@@ -148,30 +132,6 @@ def install_bulk(installers_data: list[InstallerData], safe: bool = False, jobs:
148
132
  print("✅ Version cache cleared")
149
133
  if safe:
150
134
  pass
151
- # print("⚠️ Safe installation mode activated...")
152
- # from machineconfig.jobs.python.check_installations import APP_SUMMARY_PATH
153
- # if platform.system().lower() == "windows":
154
- # print("🪟 Moving applications to Windows Apps folder...")
155
- # # PathExtended.get_env().WindowsPaths().WindowsApps)
156
- # folder = PathExtended.home().joinpath("AppData/Local/Microsoft/WindowsApps")
157
- # apps_dir.search("*").apply(lambda app: app.move(folder=folder))
158
- # elif platform.system().lower() in ["linux", "darwin"]:
159
- # system_name = "Linux" if platform.system().lower() == "linux" else "macOS"
160
- # print(f"🐧 Moving applications to {system_name} bin folder...")
161
- # if platform.system().lower() == "linux":
162
- # install_path = LINUX_INSTALL_PATH
163
- # else: # Darwin/macOS
164
- # install_path = "/usr/local/bin"
165
- # Terminal().run(f"sudo mv {apps_dir.as_posix()}/* {install_path}/").capture().print_if_unsuccessful(desc=f"MOVING executable to {install_path}", strict_err=True, strict_returncode=True)
166
- # else:
167
- # error_msg = f"❌ ERROR: System {platform.system()} not supported"
168
- # print(error_msg)
169
- # raise NotImplementedError(error_msg)
170
-
171
- # apps_dir.delete(sure=True)
172
- # print(f"✅ Safe installation completed\n{'='*80}")
173
- # return None
174
-
175
135
  print(f"🚀 Starting installation of {len(installers_data)} packages...")
176
136
  print("📦 INSTALLING FIRST PACKAGE 📦")
177
137
  Installer(installers_data[0]).install(version=None)
machineconfig/utils/io.py CHANGED
@@ -1,4 +1,3 @@
1
- from __future__ import annotations
2
1
 
3
2
  from typing import Any, Union, Optional, Mapping
4
3
  from pathlib import Path
@@ -55,6 +55,16 @@ def lambda_to_python_script(lmb: Callable[[], Any], in_global: bool, import_modu
55
55
  import types as _types
56
56
  from pathlib import Path as _Path
57
57
 
58
+ def _stringify_annotation(annotation: Any) -> Any:
59
+ if annotation is _inspect.Signature.empty or annotation is _inspect.Parameter.empty:
60
+ return annotation
61
+ if isinstance(annotation, str):
62
+ return annotation
63
+ try:
64
+ return _inspect.formatannotation(annotation)
65
+ except Exception:
66
+ return str(annotation)
67
+
58
68
  # sanity checks
59
69
  if not (callable(lmb) and isinstance(lmb, _types.LambdaType)):
60
70
  raise TypeError("Expected a lambda function object")
@@ -174,16 +184,18 @@ def lambda_to_python_script(lmb: Callable[[], Any], in_global: bool, import_modu
174
184
  else:
175
185
  new_default = param.default
176
186
 
177
- # Recreate the Parameter (keeping annotation and kind)
187
+ normalized_annotation = _stringify_annotation(param.annotation)
188
+
178
189
  if new_default is _inspect.Parameter.empty:
179
- new_param = _inspect.Parameter(name, param.kind, annotation=param.annotation)
190
+ new_param = _inspect.Parameter(name, param.kind, annotation=normalized_annotation)
180
191
  else:
181
192
  new_param = _inspect.Parameter(
182
- name, param.kind, default=new_default, annotation=param.annotation
193
+ name, param.kind, default=new_default, annotation=normalized_annotation
183
194
  )
184
195
  new_params.append(new_param)
185
196
 
186
- new_sig = _inspect.Signature(parameters=new_params, return_annotation=sig.return_annotation)
197
+ return_annotation = _stringify_annotation(sig.return_annotation)
198
+ new_sig = _inspect.Signature(parameters=new_params, return_annotation=return_annotation)
187
199
 
188
200
  # If in_global mode, return kwargs as global assignments + dedented body
189
201
  if in_global:
@@ -200,15 +212,11 @@ def lambda_to_python_script(lmb: Callable[[], Any], in_global: bool, import_modu
200
212
 
201
213
  # Build type annotation string if available
202
214
  if param.annotation is not _inspect.Parameter.empty:
203
- # Try to get a nice string representation of the annotation
204
- try:
205
- if hasattr(param.annotation, "__name__"):
206
- type_str = param.annotation.__name__
207
- else:
208
- type_str = str(param.annotation)
209
- except Exception:
210
- type_str = str(param.annotation)
211
- global_assignments.append(f"{name}: {type_str} = {repr(value)}")
215
+ annotation_literal = _stringify_annotation(param.annotation)
216
+ if isinstance(annotation_literal, str):
217
+ global_assignments.append(f"{name}: {repr(annotation_literal)} = {repr(value)}")
218
+ else:
219
+ global_assignments.append(f"{name} = {repr(value)}")
212
220
  else:
213
221
  global_assignments.append(f"{name} = {repr(value)}")
214
222
 
@@ -233,10 +241,16 @@ def lambda_to_python_script(lmb: Callable[[], Any], in_global: bool, import_modu
233
241
 
234
242
  if "Optional" in result_text or "Any" in result_text or "Union" in result_text or "Literal" in result_text:
235
243
  result_text = "from typing import Optional, Any, Union, Literal\n\n" + result_text
236
-
237
244
  if import_prefix:
238
245
  result_text = f"{import_prefix}{result_text}"
239
246
  return result_text
240
247
 
241
248
  if __name__ == "__main__":
242
- pass
249
+ from machineconfig.utils.code import print_code
250
+ import_code_robust = "<import_code_robust>"
251
+ res = lambda_to_python_script(
252
+ lambda: print_code(code=import_code_robust, lexer="python", desc="import as module code"),
253
+ # in_global=True, import_module=False
254
+ in_global=True, import_module=False
255
+ )
256
+ print(res)
@@ -1,5 +1,5 @@
1
1
  from pathlib import Path
2
- from machineconfig.utils.installer_utils.installer_abc import check_tool_exists
2
+ from machineconfig.utils.installer_utils.installer_locator_utils import check_tool_exists
3
3
  from rich.text import Text
4
4
  from rich.panel import Panel
5
5
  from rich.console import Console
@@ -16,6 +16,7 @@ OPLike: TypeAlias = Union[str, "PathExtended", Path, None]
16
16
  PLike: TypeAlias = Union[str, "PathExtended", Path]
17
17
  FILE_MODE: TypeAlias = Literal["r", "w", "x", "a"]
18
18
  SHUTIL_FORMATS: TypeAlias = Literal["zip", "tar", "gztar", "bztar", "xztar"]
19
+ DECOMPRESS_SUPPORTED_FORMATS = [".tar.gz", ".tgz", ".tar", ".gz", ".tar.bz", ".tbz", ".tar.xz", ".zip", ".7z"]
19
20
 
20
21
 
21
22
  def _is_user_admin() -> bool:
@@ -152,7 +153,6 @@ class PathExtended(type(Path()), Path): # type: ignore # pylint: disable=E0241
152
153
  # ======================================= File Editing / Reading ===================================
153
154
  def download(self, folder: OPLike = None, name: Optional[str] = None, allow_redirects: bool = True, timeout: Optional[int] = None, params: Any = None) -> "PathExtended":
154
155
  import requests
155
-
156
156
  response = requests.get(self.as_url_str(), allow_redirects=allow_redirects, timeout=timeout, params=params) # Alternative: from urllib import request; request.urlopen(url).read().decode('utf-8').
157
157
  assert response.status_code == 200, f"Download failed with status code {response.status_code}\n{response.text}"
158
158
  if name is not None:
@@ -480,9 +480,6 @@ class PathExtended(type(Path()), Path): # type: ignore # pylint: disable=E0241
480
480
  **kwargs: Any,
481
481
  ) -> "PathExtended":
482
482
  path_resolved, slf = self._resolve_path(folder, name, path, self.name).expanduser().resolve(), self.expanduser().resolve()
483
- # if use_7z: # benefits over regular zip and encrypt: can handle very large files with low memory footprint
484
- # path_resolved = path_resolved + '.7z' if not path_resolved.suffix == '.7z' else path_resolved
485
- # with install_n_import("py7zr").SevenZipFile(file=path_resolved, mode=mode, password=pwd) as archive: archive.writeall(path=str(slf), arcname=None)
486
483
  arcname_obj = PathExtended(arcname or slf.name)
487
484
  if arcname_obj.name != slf.name:
488
485
  arcname_obj /= slf.name # arcname has to start from somewhere and end with filename
@@ -555,15 +552,6 @@ class PathExtended(type(Path()), Path): # type: ignore # pylint: disable=E0241
555
552
  folder = folder if not content else folder.parent
556
553
  if slf.suffix == ".7z":
557
554
  raise NotImplementedError("I have not implemented this yet")
558
- # if overwrite: P(folder).delete(sure=True)
559
- # result = folder
560
- # import py7zr
561
- # with py7zr.SevenZipFile(file=slf, mode='r', password=pwd) as archive:
562
- # if pattern is not None:
563
- # import re
564
- # pat = re.compile(pattern)
565
- # archive.extract(path=folder, targets=[f for f in archive.getnames() if pat.match(f)])
566
- # else: archive.extractall(path=folder)
567
555
  else:
568
556
  if overwrite:
569
557
  if not content:
@@ -698,19 +686,52 @@ class PathExtended(type(Path()), Path): # type: ignore # pylint: disable=E0241
698
686
  return ret
699
687
 
700
688
  def decompress(self, folder: OPLike = None, name: Optional[str] = None, path: OPLike = None, inplace: bool = False, orig: bool = False, verbose: bool = True) -> "PathExtended":
701
- if ".tar.gz" in str(self) or ".tgz" in str(self):
689
+ if str(self).endswith(".tar.gz") or str(self).endswith(".tgz"):
702
690
  # res = self.ungz_untar(folder=folder, path=path, name=name, inplace=inplace, verbose=verbose, orig=orig)
703
691
  return self.ungz(name=f"tmp_{randstr()}.tar", inplace=inplace).untar(folder=folder, name=name, path=path, inplace=True, orig=orig, verbose=verbose) # this works for .tgz suffix as well as .tar.gz
704
- elif ".gz" in str(self):
692
+ elif str(self).endswith(".tar"):
693
+ res = self.untar(folder=folder, name=name, path=path, inplace=inplace, orig=orig, verbose=verbose)
694
+ elif str(self).endswith(".gz"):
705
695
  res = self.ungz(folder=folder, path=path, name=name, inplace=inplace, verbose=verbose, orig=orig)
706
- elif ".tar.bz" in str(self) or "tbz" in str(self):
696
+ elif str(self).endswith(".tar.bz") or str(self).endswith(".tbz"):
707
697
  res = self.unbz(name=f"tmp_{randstr()}.tar", inplace=inplace)
708
698
  return res.untar(folder=folder, name=name, path=path, inplace=True, orig=orig, verbose=verbose)
709
- elif ".tar.xz" in str(self):
699
+ elif str(self).endswith(".tar.xz"):
710
700
  # res = self.unxz_untar(folder=folder, path=path, name=name, inplace=inplace, verbose=verbose, orig=orig)
711
701
  res = self.unxz(inplace=inplace).untar(folder=folder, name=name, path=path, inplace=True, orig=orig, verbose=verbose)
712
- elif ".zip" in str(self):
702
+ elif str(self).endswith(".zip"):
713
703
  res = self.unzip(folder=folder, path=path, name=name, inplace=inplace, verbose=verbose, orig=orig)
704
+ elif str(self).endswith(".7z"):
705
+ def unzip_7z(archive_path: str, dest_dir: Optional[str] = None) -> Path:
706
+ """
707
+ Uncompresses a .7z archive to a directory and returns the Path to the extraction directory.
708
+
709
+ :param archive_path: path to the .7z archive file
710
+ :param dest_dir: optional path to directory to extract into; if None a temporary dir will be created
711
+ :return: pathlib.Path pointing to the destination directory where contents were extracted
712
+ :raises: FileNotFoundError if archive does not exist; py7zr.Bad7zFile or other error if extraction fails
713
+ """
714
+ import py7zr
715
+ import tempfile
716
+ from pathlib import Path
717
+ archive_path_obj = Path(archive_path)
718
+ if not archive_path_obj.is_file():
719
+ raise FileNotFoundError(f"Archive file not found: {archive_path_obj!r}")
720
+ if dest_dir is None:
721
+ # create a temporary directory
722
+ dest = Path(tempfile.mkdtemp(prefix=f"unzip7z_{archive_path_obj.stem}_"))
723
+ else:
724
+ dest = Path(dest_dir)
725
+ dest.mkdir(parents=True, exist_ok=True)
726
+ # Perform extraction
727
+ with py7zr.SevenZipFile(str(archive_path_obj), mode='r') as archive:
728
+ archive.extractall(path=str(dest))
729
+ # Return the extraction directory path
730
+ return dest
731
+ from machineconfig.utils.code import run_lambda_function
732
+ destination_dir = str(self.expanduser().resolve()).replace(".7z", "")
733
+ run_lambda_function(lambda: unzip_7z(archive_path=str(self), dest_dir=destination_dir), uv_project_dir=None, uv_with=["py7zr"])
734
+ res = PathExtended(destination_dir)
714
735
  else:
715
736
  res = self
716
737
  return res
@@ -788,7 +809,7 @@ class PathExtended(type(Path()), Path): # type: ignore # pylint: disable=E0241
788
809
  path = self
789
810
  else:
790
811
  try:
791
- path = self.rel2home()
812
+ path = PathExtended(self.expanduser().absolute().relative_to(Path.home()))
792
813
  except ValueError as ve:
793
814
  if strict:
794
815
  raise ve
@@ -1,18 +1,17 @@
1
- from machineconfig.utils.path_extended import PathExtended
2
- from machineconfig.utils.options import choose_from_options
1
+ # from machineconfig.utils.path_extended import Path
3
2
  from machineconfig.utils.source_of_truth import EXCLUDE_DIRS
4
3
  from rich.console import Console
5
4
  from rich.panel import Panel
6
5
  import platform
7
6
  import subprocess
8
7
  from pathlib import Path
9
-
8
+ from typing import Optional
10
9
 
11
10
  console = Console()
12
11
 
13
12
 
14
- def sanitize_path(a_path: str) -> PathExtended:
15
- path = PathExtended(a_path)
13
+ def sanitize_path(a_path: str) -> Path:
14
+ path = Path(a_path)
16
15
  if Path.cwd() == Path.home() and not path.exists():
17
16
  result = input("Current working directory is home, and passed path is not full path, are you sure you want to continue, [y]/n? ") or "y"
18
17
  if result == "y":
@@ -23,13 +22,13 @@ def sanitize_path(a_path: str) -> PathExtended:
23
22
  if platform.system() == "Windows": # path copied from Linux/Mac to Windows
24
23
  # For Linux: /home/username, for Mac: /Users/username
25
24
  skip_parts = 3 if path.as_posix().startswith("/home") else 3 # Both have 3 parts to skip
26
- path = PathExtended.home().joinpath(*path.parts[skip_parts:])
25
+ path = Path.home().joinpath(*path.parts[skip_parts:])
27
26
  assert path.exists(), f"File not found: {path}"
28
27
  source_os = "Linux" if path.as_posix().startswith("/home") else "macOS"
29
28
  console.print(Panel(f"🔗 PATH MAPPING | {source_os} → Windows: `{a_path}` ➡️ `{path}`", title="Path Mapping", expand=False))
30
- elif platform.system() in ["Linux", "Darwin"] and PathExtended.home().as_posix() not in path.as_posix(): # copied between Unix-like systems with different username
29
+ elif platform.system() in ["Linux", "Darwin"] and Path.home().as_posix() not in path.as_posix(): # copied between Unix-like systems with different username
31
30
  skip_parts = 3 # Both /home/username and /Users/username have 3 parts to skip
32
- path = PathExtended.home().joinpath(*path.parts[skip_parts:])
31
+ path = Path.home().joinpath(*path.parts[skip_parts:])
33
32
  assert path.exists(), f"File not found: {path}"
34
33
  current_os = "Linux" if platform.system() == "Linux" else "macOS"
35
34
  source_os = "Linux" if path.as_posix().startswith("/home") else "macOS"
@@ -37,12 +36,12 @@ def sanitize_path(a_path: str) -> PathExtended:
37
36
  elif path.as_posix().startswith("C:"):
38
37
  if platform.system() in ["Linux", "Darwin"]: # path copied from Windows to Linux/Mac
39
38
  xx = str(a_path).replace("\\\\", "/")
40
- path = PathExtended.home().joinpath(*PathExtended(xx).parts[3:]) # exclude C:\\Users\\username
39
+ path = Path.home().joinpath(*Path(xx).parts[3:]) # exclude C:\\Users\\username
41
40
  assert path.exists(), f"File not found: {path}"
42
41
  target_os = "Linux" if platform.system() == "Linux" else "macOS"
43
42
  console.print(Panel(f"🔗 PATH MAPPING | Windows → {target_os}: `{a_path}` ➡️ `{path}`", title="Path Mapping", expand=False))
44
- elif platform.system() == "Windows" and PathExtended.home().as_posix() not in path.as_posix(): # copied from Windows to Windows with different username
45
- path = PathExtended.home().joinpath(*path.parts[2:])
43
+ elif platform.system() == "Windows" and Path.home().as_posix() not in path.as_posix(): # copied from Windows to Windows with different username
44
+ path = Path.home().joinpath(*path.parts[2:])
46
45
  assert path.exists(), f"File not found: {path}"
47
46
  console.print(Panel(f"🔗 PATH MAPPING | Windows → Windows: `{a_path}` ➡️ `{path}`", title="Path Mapping", expand=False))
48
47
  return path.expanduser().absolute()
@@ -67,12 +66,12 @@ def find_scripts(root: Path, name_substring: str, suffixes: set[str]) -> tuple[l
67
66
  return filename_matches, partial_path_matches
68
67
 
69
68
 
70
- def match_file_name(sub_string: str, search_root: PathExtended, suffixes: set[str]) -> PathExtended:
69
+ def match_file_name(sub_string: str, search_root: Path, suffixes: set[str]) -> Path:
71
70
  search_root_obj = search_root.absolute()
72
71
  # assume subscript is filename only, not a sub_path. There is no need to fzf over the paths.
73
72
  filename_matches, partial_path_matches = find_scripts(search_root_obj, sub_string, suffixes)
74
73
  if len(filename_matches) == 1:
75
- return PathExtended(filename_matches[0])
74
+ return Path(filename_matches[0])
76
75
  console.print(Panel(f"Partial filename {search_root_obj} match with case-insensitivity failed. This generated #{len(filename_matches)} results.", title="Search", expand=False))
77
76
  if len(filename_matches) < 20:
78
77
  print("\n".join([a_potential_match.as_posix() for a_potential_match in filename_matches]))
@@ -81,35 +80,44 @@ def match_file_name(sub_string: str, search_root: PathExtended, suffixes: set[st
81
80
  # let's see if avoiding .lower() helps narrowing down to one result
82
81
  reduced_scripts = [a_potential_match for a_potential_match in filename_matches if sub_string in a_potential_match.name]
83
82
  if len(reduced_scripts) == 1:
84
- return PathExtended(reduced_scripts[0])
83
+ return Path(reduced_scripts[0])
85
84
  elif len(reduced_scripts) > 1:
85
+ from machineconfig.utils.options import choose_from_options
86
86
  choice = choose_from_options(multi=False, msg="Multiple matches found", options=reduced_scripts, fzf=True)
87
- return PathExtended(choice)
87
+ return Path(choice)
88
88
  print(f"Result: This still generated {len(reduced_scripts)} results.")
89
89
  if len(reduced_scripts) < 10:
90
90
  print("\n".join([a_potential_match.as_posix() for a_potential_match in reduced_scripts]))
91
91
 
92
92
  console.print(Panel(f"Partial path match with case-insensitivity failed. This generated #{len(partial_path_matches)} results.", title="Search", expand=False))
93
93
  if len(partial_path_matches) == 1:
94
- return PathExtended(partial_path_matches[0])
94
+ return Path(partial_path_matches[0])
95
95
  elif len(partial_path_matches) > 1:
96
96
  print("Try to narrow down partial_path_matches search by case-sensitivity.")
97
97
  reduced_scripts = [a_potential_match for a_potential_match in partial_path_matches if sub_string in a_potential_match.as_posix()]
98
98
  if len(reduced_scripts) == 1:
99
- return PathExtended(reduced_scripts[0])
100
- print(f"Result: This still generated {len(reduced_scripts)} results.")
99
+ return Path(reduced_scripts[0])
100
+ print(f"Result: This still generated {len(reduced_scripts)} results.")
101
+
101
102
  try:
102
- fzf_cmd = f"cd '{search_root_obj}'; fd --type file --strip-cwd-prefix | fzf --ignore-case --exact --query={sub_string}"
103
+
104
+ if len(partial_path_matches) == 0:
105
+ print("No partial path matches found, trying to do fd with --no-ignore ...")
106
+ fzf_cmd = f"cd '{search_root_obj}'; fd --no-ignore --type file --strip-cwd-prefix | fzf --ignore-case --exact --query={sub_string}"
107
+ else:
108
+ fzf_cmd = f"cd '{search_root_obj}'; fd --type file --strip-cwd-prefix | fzf --ignore-case --exact --query={sub_string}"
103
109
  console.print(Panel(f"🔍 Second attempt: SEARCH STRATEGY | Using fd to search for '{sub_string}' in '{search_root_obj}' ...\n{fzf_cmd}", title="Search Strategy", expand=False))
104
110
  search_res_raw = subprocess.run(fzf_cmd, stdout=subprocess.PIPE, text=True, check=True, shell=True).stdout
105
- search_res = search_res_raw.strip().split("\\n")[:-1]
111
+ search_res = search_res_raw.strip().split("\n")
106
112
  except subprocess.CalledProcessError as cpe:
107
113
  console.print(Panel(f"❌ ERROR | FZF search failed with '{sub_string}' in '{search_root_obj}'.\n{cpe}", title="Error", expand=False))
108
114
  import sys
109
-
110
115
  sys.exit(f"💥 FILE NOT FOUND | Path {sub_string} does not exist @ root {search_root_obj}. No search results.")
111
116
  if len(search_res) == 1:
112
117
  return search_root_obj.joinpath(search_res_raw)
118
+ elif len(search_res) == 0:
119
+ msg = Panel(f"💥 FILE NOT FOUND | Path {sub_string} does not exist @ root {search_root_obj}. No search results", title="File Not Found", expand=False)
120
+ raise FileNotFoundError(msg)
113
121
 
114
122
  print(f"⚠️ WARNING | Multiple search results found for `{sub_string}`:\n'{search_res}'")
115
123
  cmd = f"cd '{search_root_obj}'; fd --type file | fzf --select-1 --query={sub_string}"
@@ -121,3 +129,49 @@ def match_file_name(sub_string: str, search_root: PathExtended, suffixes: set[st
121
129
  msg = Panel(f"💥 FILE NOT FOUND | Path {sub_string} does not exist @ root {search_root_obj}. No search results", title="File Not Found", expand=False)
122
130
  raise FileNotFoundError(msg) from cpe
123
131
  return search_root_obj.joinpath(res)
132
+
133
+
134
+ def search_for_files_of_interest(path_obj: Path, suffixes: set[str]) -> list[Path]:
135
+ if path_obj.is_file():
136
+ return [path_obj]
137
+ files: list[Path] = []
138
+ directories_to_visit: list[Path] = [path_obj]
139
+ while directories_to_visit:
140
+ current_dir = directories_to_visit.pop()
141
+ for entry in current_dir.iterdir():
142
+ if entry.is_dir():
143
+ if entry.name == ".venv":
144
+ continue
145
+ directories_to_visit.append(entry)
146
+ continue
147
+ if entry.suffix not in suffixes:
148
+ continue
149
+ if entry.suffix == ".py" and entry.name == "__init__.py":
150
+ continue
151
+ files.append(entry)
152
+ return files
153
+
154
+
155
+ def get_choice_file(path: str, suffixes: Optional[set[str]]):
156
+ path_obj = sanitize_path(path)
157
+ if suffixes is None:
158
+ import platform
159
+ if platform.system() == "Windows":
160
+ suffixes = {".py", ".ps1", ".sh"}
161
+ elif platform.system() in ["Linux", "Darwin"]:
162
+ suffixes = {".py", ".sh"}
163
+ else:
164
+ suffixes = {".py"}
165
+ if not path_obj.exists():
166
+ print(f"🔍 Searching for file matching `{path}` under `{Path.cwd()}`, but only if suffix matches {suffixes}")
167
+ choice_file = match_file_name(sub_string=path, search_root=Path.cwd(), suffixes=suffixes)
168
+ elif path_obj.is_dir():
169
+ print(f"🔍 Searching recursively for Python, PowerShell and Shell scripts in directory `{path_obj}`")
170
+ files = search_for_files_of_interest(path_obj, suffixes=suffixes)
171
+ print(f"🔍 Got #{len(files)} results.")
172
+ from machineconfig.utils.options import choose_from_options
173
+ choice_file = choose_from_options(multi=False, options=files, fzf=True, msg="Choose one option")
174
+ choice_file = Path(choice_file)
175
+ else:
176
+ choice_file = path_obj
177
+ return choice_file
@@ -13,7 +13,7 @@ class TabConfig(TypedDict):
13
13
  tabName: str
14
14
  startDir: str
15
15
  command: str
16
- tabWeight: NotRequired[int] # Optional, defaults to 1 if not provided
16
+ tabWeight: NotRequired[int]
17
17
 
18
18
 
19
19
  class LayoutConfig(TypedDict):
@@ -2,13 +2,13 @@ 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
+ from machineconfig.scripts.python.helpers_utils.path import MachineSpecs
6
6
  import rich.console
7
7
  from machineconfig.utils.terminal import Response
8
8
  from machineconfig.utils.accessories import pprint, randstr
9
9
  from machineconfig.utils.meta import lambda_to_python_script
10
10
  UV_RUN_CMD = "$HOME/.local/bin/uv run" if platform.system() != "Windows" else """& "$env:USERPROFILE/.local/bin/uv" run"""
11
- MACHINECONFIG_VERSION = "machineconfig>=7.49"
11
+ MACHINECONFIG_VERSION = "machineconfig>=7.64"
12
12
  DEFAULT_PICKLE_SUBDIR = "tmp_results/tmp_scripts/ssh"
13
13
 
14
14
  class SSH:
@@ -113,7 +113,7 @@ class SSH:
113
113
  if self.progress and self.task is not None:
114
114
  self.progress.update(self.task, completed=transferred, total=total)
115
115
  self.tqdm_wrap = RichProgressWrapper
116
- from machineconfig.scripts.python.helpers_devops.cli_utils import get_machine_specs
116
+ from machineconfig.scripts.python.helpers_utils.path import get_machine_specs
117
117
  self.local_specs: MachineSpecs = get_machine_specs()
118
118
  resp = self.run_shell(command="""~/.local/bin/utils get-machine-specs """, verbose_output=False, description="Getting remote machine specs", strict_stderr=False, strict_return_code=False)
119
119
  json_str = resp.op
@@ -0,0 +1,124 @@
1
+ Metadata-Version: 2.4
2
+ Name: machineconfig
3
+ Version: 7.64
4
+ Summary: Dotfiles management package
5
+ Author-email: Alex Al-Saffar <programmer@usa.com>
6
+ License: Apache 2.0
7
+ Project-URL: Homepage, https://github.com/thisismygitrepo/machineconfig
8
+ Project-URL: Bug Tracker, https://github.com/thisismygitrepo/machineconfig/issues
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: Apache Software License
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.13
13
+ Description-Content-Type: text/markdown
14
+ Requires-Dist: cryptography>=44.0.2
15
+ Requires-Dist: fire>=0.7.0
16
+ Requires-Dist: joblib>=1.5.2
17
+ Requires-Dist: paramiko>=3.5.1
18
+ Requires-Dist: randomname>=0.2.1
19
+ Requires-Dist: requests>=2.32.5
20
+ Requires-Dist: rich>=14.0.0
21
+ Requires-Dist: tenacity>=9.1.2
22
+ Requires-Dist: psutil>=7.0.0
23
+ Requires-Dist: gitpython>=3.1.44
24
+ Requires-Dist: pyfzf>=0.3.1
25
+ Requires-Dist: rclone-python>=0.1.23
26
+ Requires-Dist: questionary>=2.1.1
27
+ Requires-Dist: typer-slim>=0.19.2
28
+ Requires-Dist: typer>=0.19.2
29
+ Provides-Extra: windows
30
+ Requires-Dist: pywin32; extra == "windows"
31
+ Provides-Extra: plot
32
+ Requires-Dist: sqlalchemy>=2.0.43; extra == "plot"
33
+ Requires-Dist: ipykernel>=6.30.1; extra == "plot"
34
+ Requires-Dist: ipython>=9.5.0; extra == "plot"
35
+ Requires-Dist: jupyterlab>=4.4.9; extra == "plot"
36
+ Requires-Dist: kaleido>=1.1.0; extra == "plot"
37
+ Requires-Dist: matplotlib>=3.10.6; extra == "plot"
38
+ Requires-Dist: nbformat>=5.10.4; extra == "plot"
39
+ Requires-Dist: numpy>=2.3.3; extra == "plot"
40
+ Requires-Dist: plotly>=6.3.0; extra == "plot"
41
+ Requires-Dist: polars>=1.33.1; extra == "plot"
42
+ Requires-Dist: python-magic>=0.4.27; extra == "plot"
43
+
44
+
45
+ <p align="center">
46
+
47
+ <a href="https://github.com/thisismygitrepo/machineconfig/commits">
48
+ <img src="https://img.shields.io/github/commit-activity/m/thisismygitrepo/machineconfig" />
49
+ </a>
50
+
51
+ </p>
52
+
53
+
54
+ # 🧠 Welcome to **Machineconfig**
55
+
56
+ **Machineconfig** is a cli-based **Digital Life Manager** — It's called so because no existing category of software fully captures its scope. At the same time, it is a *Package Manager*, *Configuration Manager*, *Automation Tool*, *Dotfiles Manager*, *Data Solution*, and *Code Manager*, among other functionalities covered, all rolled into one seamless experience.
57
+
58
+
59
+ ## 💡 Motivation
60
+
61
+ But why do we need such a tool to combine all those functionalities?? Because you need one tool to manager your stack and dev-environment, put it together and maintain it.
62
+ Consider this concrete scenario: When setting up a new machine, VM, or Docker container, you often face dependency chains like this:
63
+
64
+ ```mermaid
65
+ flowchart TD
66
+ A["Need to setup my [dev] environment"] --> B["need my tool x, e.g.: yadm"]
67
+ B --> C["Requires git"]
68
+ C --> D["Requires package manager, e.g. brew"]
69
+ D --> E["Requires curl"]
70
+ E --> F["Requires network setup / system update"]
71
+ F --> G["Requires system configuration access"]
72
+ G --> H["Finally ready to start setup the tool x."]
73
+ ```
74
+
75
+ Machineconfig builds on shoulder of giants. A suite of best-in-class stack of projects on github are used, the most starred, active and written in Rust tools are used when possible. The goal is to provide a seamless experience that abstracts away the complexity of setting up and maintaining your digital environment. The goal of machineconfig is to replicate your setup, config, code, data and secrets on any machine, any os, in 5 minutes, using minimal user input. Then, from that point, machineconfig will help you maintain, update, backup and sync your digital life across all your devices, automatically.
76
+
77
+
78
+ ## ⚙️ Functional Overview
79
+
80
+ | Category | Comparable Tools | Description |
81
+ |------------------------|----------------------------------------------|-----------------------------------------------------------|
82
+ | **Package Manager** | `winget`, `apt`, `brew`, `nix` | Installs and manages software packages across systems. |
83
+ | **Configuration Manager** | `Ansible`, `Chef`, `Puppet` | Configures and maintains system‐level preferences. |
84
+ | **Automation Tool** | `Airflow`, `Prefect`, `Dagster`, `Celery` | Automates repetitive tasks, pipelines, orchestration. |
85
+ | **Dotfiles Manager** | `chezmoi`, `yadm`, `rcm`, `GNU Stow` | Synchronises dotfiles & personal configs across systems. |
86
+ | **Data Solution** | `rclone`, `rsync` | Handles backups, mirroring and secure file sync. |
87
+ | **Code Manager** | `strong‐box`, `Vault` | Manages and protects code snippets, secrets and creds. |
88
+
89
+ ---
90
+
91
+
92
+ # Install On Windows:
93
+
94
+ ```powershell
95
+ # install tool the tool only:
96
+ powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" # Skip if UV is already installed
97
+ uv tool install --upgrade --python 3.14 machineconfig
98
+ # interactive install of machineconfig and following on to run it and make basic machine configuration (RECOMMENDED):
99
+ iex (iwr bit.ly/cfgwindows).Content # Or, if UV is installed: iex (uvx machineconfig define)
100
+ # Quick install and configure (optionals are accepted by default):
101
+ iex (iwr bit.ly/cfgwq).Content
102
+ ```
103
+
104
+
105
+ # Install On Linux and MacOS
106
+
107
+ ```bash
108
+ # install the tool only:
109
+ curl -LsSf https://astral.sh/uv/install.sh | sh # Skip if UV is already installed
110
+ uv tool install --upgrade --python 3.14 machineconfig
111
+ # interactive install of machineconfig and following on to run it and make basic machine configuration (RECOMMENDED):
112
+ . <(curl -L bit.ly/cfglinux) # Or, if UV is installed: . <(uvx machineconfig define)
113
+ ```
114
+
115
+
116
+ # Author
117
+ Alex Al-Saffar. [email](mailto:programmer@usa.com)
118
+
119
+ # Contributor
120
+ Ruby Chan. [email](mailto:ruby.chan@sa.gov.au)
121
+
122
+
123
+ [![Alex's github activity graph](https://github-readme-activity-graph.vercel.app/graph?username=thisismygitrepo)](https://github.com/ashutosh00710/github-readme-activity-graph)
124
+