machineconfig 7.53__py3-none-any.whl → 7.69__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 (90) hide show
  1. machineconfig/cluster/sessions_managers/utils/maker.py +21 -9
  2. machineconfig/jobs/installer/custom/boxes.py +2 -2
  3. machineconfig/jobs/installer/custom/hx.py +15 -12
  4. machineconfig/jobs/installer/custom_dev/cloudflare_warp_cli.py +23 -0
  5. machineconfig/jobs/installer/custom_dev/dubdb_adbc.py +1 -1
  6. machineconfig/jobs/installer/custom_dev/nerfont_windows_helper.py +1 -1
  7. machineconfig/jobs/installer/custom_dev/sysabc.py +39 -34
  8. machineconfig/jobs/installer/custom_dev/wezterm.py +0 -4
  9. machineconfig/jobs/installer/installer_data.json +103 -35
  10. machineconfig/jobs/installer/package_groups.py +28 -13
  11. machineconfig/scripts/__init__.py +0 -4
  12. machineconfig/scripts/linux/wrap_mcfg +1 -1
  13. machineconfig/scripts/python/ai/solutions/copilot/instructions/python/dev.instructions.md +3 -0
  14. machineconfig/scripts/python/croshell.py +22 -17
  15. machineconfig/scripts/python/devops.py +3 -4
  16. machineconfig/scripts/python/devops_navigator.py +0 -4
  17. machineconfig/scripts/python/env_manager/path_manager_tui.py +1 -1
  18. machineconfig/scripts/python/fire_jobs.py +17 -15
  19. machineconfig/scripts/python/ftpx.py +13 -11
  20. machineconfig/scripts/python/helpers/ast_search.py +74 -0
  21. machineconfig/scripts/python/helpers/repo_rag.py +325 -0
  22. machineconfig/scripts/python/helpers/symantic_search.py +25 -0
  23. machineconfig/scripts/python/helpers_cloud/cloud_copy.py +28 -21
  24. machineconfig/scripts/python/helpers_cloud/cloud_helpers.py +1 -1
  25. machineconfig/scripts/python/helpers_cloud/cloud_sync.py +8 -7
  26. machineconfig/scripts/python/helpers_croshell/crosh.py +2 -2
  27. machineconfig/scripts/python/helpers_devops/cli_config_dotfile.py +22 -13
  28. machineconfig/scripts/python/helpers_devops/cli_self.py +7 -6
  29. machineconfig/scripts/python/helpers_devops/cli_share_file.py +2 -2
  30. machineconfig/scripts/python/helpers_devops/cli_share_server.py +1 -1
  31. machineconfig/scripts/python/helpers_devops/cli_terminal.py +1 -1
  32. machineconfig/scripts/python/helpers_devops/cli_utils.py +2 -73
  33. machineconfig/scripts/python/helpers_devops/devops_backup_retrieve.py +4 -4
  34. machineconfig/scripts/python/helpers_fire_command/file_wrangler.py +2 -3
  35. machineconfig/scripts/python/helpers_fire_command/fire_jobs_route_helper.py +3 -4
  36. machineconfig/scripts/python/helpers_navigator/command_tree.py +50 -18
  37. machineconfig/scripts/python/helpers_repos/cloud_repo_sync.py +13 -5
  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 +5 -5
  42. machineconfig/scripts/python/helpers_utils/download.py +152 -0
  43. machineconfig/scripts/python/helpers_utils/path.py +4 -2
  44. machineconfig/scripts/python/interactive.py +11 -14
  45. machineconfig/scripts/python/{machineconfig.py → mcfg_entry.py} +4 -0
  46. machineconfig/scripts/python/msearch.py +21 -2
  47. machineconfig/scripts/python/nw/devops_add_ssh_key.py +21 -5
  48. machineconfig/scripts/python/nw/ssh_debug_linux.py +7 -7
  49. machineconfig/scripts/python/nw/ssh_debug_windows.py +4 -4
  50. machineconfig/scripts/python/nw/wsl_windows_transfer.py +3 -2
  51. machineconfig/scripts/python/sessions.py +35 -20
  52. machineconfig/scripts/python/terminal.py +2 -2
  53. machineconfig/scripts/python/utils.py +12 -10
  54. machineconfig/scripts/windows/mounts/mount_ssh.ps1 +1 -1
  55. machineconfig/settings/lf/windows/lfcd.ps1 +1 -1
  56. machineconfig/settings/shells/pwsh/init.ps1 +1 -0
  57. machineconfig/settings/shells/wezterm/wezterm.lua +2 -0
  58. machineconfig/settings/shells/zsh/init.sh +0 -7
  59. machineconfig/settings/yazi/shell/yazi_cd.ps1 +29 -5
  60. machineconfig/setup_linux/web_shortcuts/interactive.sh +12 -11
  61. machineconfig/setup_windows/uv.ps1 +8 -1
  62. machineconfig/setup_windows/web_shortcuts/interactive.ps1 +12 -11
  63. machineconfig/setup_windows/web_shortcuts/quick_init.ps1 +4 -2
  64. machineconfig/utils/accessories.py +7 -4
  65. machineconfig/utils/code.py +6 -4
  66. machineconfig/utils/files/headers.py +2 -2
  67. machineconfig/utils/installer_utils/install_from_url.py +180 -0
  68. machineconfig/utils/installer_utils/installer_class.py +56 -46
  69. machineconfig/utils/installer_utils/{installer.py → installer_cli.py} +71 -65
  70. machineconfig/utils/{installer.py → installer_utils/installer_runner.py} +1 -25
  71. machineconfig/utils/meta.py +28 -15
  72. machineconfig/utils/options.py +4 -4
  73. machineconfig/utils/path_extended.py +40 -19
  74. machineconfig/utils/path_helper.py +33 -31
  75. machineconfig/utils/schemas/layouts/layout_types.py +1 -1
  76. machineconfig/utils/ssh.py +330 -99
  77. machineconfig/utils/ve.py +11 -4
  78. machineconfig-7.69.dist-info/METADATA +124 -0
  79. {machineconfig-7.53.dist-info → machineconfig-7.69.dist-info}/RECORD +85 -83
  80. {machineconfig-7.53.dist-info → machineconfig-7.69.dist-info}/entry_points.txt +2 -2
  81. machineconfig/jobs/installer/linux_scripts/pgsql.sh +0 -41
  82. machineconfig/scripts/python/explore.py +0 -49
  83. machineconfig/scripts/python/nw/add_ssh_key.py +0 -148
  84. machineconfig/settings/lf/linux/exe/fzf_nano.sh +0 -16
  85. machineconfig-7.53.dist-info/METADATA +0 -94
  86. /machineconfig/jobs/installer/linux_scripts/{warp-cli.sh → cloudflare_warp_cli.sh} +0 -0
  87. /machineconfig/scripts/{Restore-ThunderbirdProfile.ps1 → windows/mounts/Restore-ThunderbirdProfile.ps1} +0 -0
  88. /machineconfig/utils/installer_utils/{installer_abc.py → installer_locator_utils.py} +0 -0
  89. {machineconfig-7.53.dist-info → machineconfig-7.69.dist-info}/WHEEL +0 -0
  90. {machineconfig-7.53.dist-info → machineconfig-7.69.dist-info}/top_level.txt +0 -0
@@ -1,55 +1,16 @@
1
1
  """Devops Devapps Install"""
2
2
 
3
3
  import typer
4
- from rich.console import Console
5
- from rich.panel import Panel
6
- from rich.table import Table
7
- from typing import Optional, Annotated
4
+ from typing import Annotated, Optional
8
5
  from machineconfig.jobs.installer.package_groups import PACKAGE_GROUP2NAMES
6
+ from machineconfig.utils.installer_utils.installer_class import Installer
9
7
 
10
- console = Console()
11
8
 
12
9
 
13
- def _handle_installer_not_found(search_term: str, all_names: list[str]) -> None: # type: ignore
14
- """Handle installer not found with friendly suggestions using fuzzy matching."""
15
- from difflib import get_close_matches
16
- close_matches = get_close_matches(search_term, all_names, n=5, cutoff=0.4)
17
- console.print(f"\n❌ '[red]{search_term}[/red]' was not found.", style="bold")
18
- if close_matches:
19
- console.print("🤔 Did you mean one of these?", style="yellow")
20
- table = Table(show_header=False, box=None, pad_edge=False)
21
- for i, match in enumerate(close_matches, 1):
22
- table.add_row(f"[cyan]{i}.[/cyan]", f"[green]{match}[/green]")
23
- console.print(table)
24
- else:
25
- console.print("📋 Here are some available options:", style="blue")
26
- # Show first 10 installers as examples
27
- if len(all_names) > 10:
28
- sample_names = all_names[:10]
29
- else:
30
- sample_names = all_names
31
- table = Table(show_header=False, box=None, pad_edge=False)
32
- for i, name in enumerate(sample_names, 1):
33
- table.add_row(f"[cyan]{i}.[/cyan]", f"[green]{name}[/green]")
34
- console.print(table)
35
- if len(all_names) > 10:
36
- console.print(f" [dim]... and {len(all_names) - 10} more[/dim]")
37
-
38
- panel = Panel(f"[bold blue]💡 Use 'ia' to interactively browse all available installers.[/bold blue]\n[bold blue]💡 Use one of the categories: {list(PACKAGE_GROUP2NAMES.keys())}[/bold blue]", title="[yellow]Helpful Tips[/yellow]", border_style="yellow")
39
- console.print(panel)
40
-
41
-
42
- def main_with_parser():
43
- import typer
44
- app = typer.Typer()
45
- app.command()(main)
46
- app()
47
-
48
-
49
- def main(
10
+ def main_installer_cli(
50
11
  which: Annotated[Optional[str], typer.Argument(..., help="Comma-separated list of program/groups names to install (if --group flag is set).")] = None,
51
12
  group: Annotated[bool, typer.Option(..., "--group", "-g", help="Treat 'which' as a group name. A group is bundle of apps.")] = False,
52
- interactive: Annotated[bool, typer.Option(..., "--interactive", "-ia", help="Interactive selection of programs to install.")] = False,
13
+ interactive: Annotated[bool, typer.Option(..., "--interactive", "-i", help="Interactive selection of programs to install.")] = False,
53
14
  ) -> None:
54
15
  if interactive:
55
16
  return install_interactively()
@@ -61,6 +22,10 @@ def main(
61
22
  return install_clis(clis_names=[x.strip() for x in which.split(",") if x.strip() != ""])
62
23
  else:
63
24
  if group:
25
+ from rich.console import Console
26
+ from rich.table import Table
27
+ console = Console()
28
+
64
29
  typer.echo("❌ You must provide a group name when using the --group/-g option.")
65
30
  res = get_group_name_to_repr()
66
31
  console.print("[bold blue]Here are the available groups:[/bold blue]")
@@ -98,20 +63,18 @@ def get_group_name_to_repr() -> dict[str, str]:
98
63
  def install_interactively():
99
64
  from machineconfig.utils.options import choose_from_options
100
65
  from machineconfig.utils.schemas.installer.installer_types import get_normalized_arch, get_os_name
101
- from machineconfig.utils.installer import get_installers
66
+ from machineconfig.utils.installer_utils.installer_runner import get_installers
102
67
  from machineconfig.utils.installer_utils.installer_class import Installer
68
+ from rich.console import Console
69
+ from rich.panel import Panel
70
+ # from rich.table import Table
103
71
  installers = get_installers(os=get_os_name(), arch=get_normalized_arch(), which_cats=None)
104
- installer_options = []
105
- for x in installers:
106
- installer_options.append(Installer(installer_data=x).get_description())
107
-
72
+ installer_options = [Installer(installer_data=x).get_description() for x in installers]
108
73
  category_display_to_name = get_group_name_to_repr()
109
- options = list(category_display_to_name.keys()) + ["─" * 50] + installer_options
110
- program_names = choose_from_options(multi=True, msg="Categories are prefixed with 📦", options=options, header="🚀 CHOOSE DEV APP OR CATEGORY", default="📦 termabc", fzf=True)
74
+ options = list(category_display_to_name.keys()) + installer_options
75
+ program_names = choose_from_options(multi=True, msg="Categories are prefixed with 📦", options=options, header="🚀 CHOOSE DEV APP OR CATEGORY", fzf=True)
111
76
  installation_messages: list[str] = []
112
77
  for _an_idx, a_program_name in enumerate(program_names):
113
- if a_program_name.startswith("─"): # 50 dashes separator
114
- continue
115
78
  if a_program_name.startswith("📦 "):
116
79
  category_name = category_display_to_name.get(a_program_name)
117
80
  if category_name:
@@ -122,59 +85,102 @@ def install_interactively():
122
85
  status_message = Installer(an_installer_data).install_robust(version=None) # finish the task - this returns a status message, not a command
123
86
  installation_messages.append(status_message)
124
87
  if installation_messages:
88
+ console = Console()
89
+
125
90
  panel = Panel("\n".join([f"[blue]• {message}[/blue]" for message in installation_messages]), title="[bold green]📊 Installation Summary[/bold green]", border_style="green", padding=(1, 2))
126
91
  console.print(panel)
127
92
 
128
93
 
129
94
  def install_group(package_group: str):
130
- from machineconfig.utils.installer import get_installers, install_bulk
95
+ from machineconfig.utils.installer_utils.installer_runner import get_installers, install_bulk
131
96
  from machineconfig.utils.schemas.installer.installer_types import get_normalized_arch, get_os_name
97
+ from rich.console import Console
98
+ from rich.panel import Panel
99
+ # from rich.table import Table
132
100
  if package_group in PACKAGE_GROUP2NAMES:
133
101
  panel = Panel(f"[bold yellow]Installing programs from category: [green]{package_group}[/green][/bold yellow]", title="[bold blue]📦 Category Installation[/bold blue]", border_style="blue", padding=(1, 2))
102
+ console = Console()
134
103
  console.print(panel)
135
104
  installers_ = get_installers(os=get_os_name(), arch=get_normalized_arch(), which_cats=[package_group])
136
105
  install_bulk(installers_data=installers_)
137
106
  return
138
- print(f"❌ ERROR: Unknown package group: {package_group}. Available groups are: {list(PACKAGE_GROUP2NAMES.keys())}")
107
+ console = Console()
108
+ console.print(f"❌ ERROR: Unknown package group: {package_group}. Available groups are: {list(PACKAGE_GROUP2NAMES.keys())}")
109
+ def _handle_installer_not_found(search_term: str, all_names: list[str]) -> None: # type: ignore
110
+ """Handle installer not found with friendly suggestions using fuzzy matching."""
111
+ from difflib import get_close_matches
112
+ from rich.console import Console
113
+ from rich.panel import Panel
114
+ from rich.table import Table
115
+ close_matches = get_close_matches(search_term, all_names, n=5, cutoff=0.4)
116
+ console = Console()
117
+
118
+ console.print(f"\n❌ '[red]{search_term}[/red]' was not found.", style="bold")
119
+ if close_matches:
120
+ console.print("🤔 Did you mean one of these?", style="yellow")
121
+ table = Table(show_header=False, box=None, pad_edge=False)
122
+ for i, match in enumerate(close_matches, 1):
123
+ table.add_row(f"[cyan]{i}.[/cyan]", f"[green]{match}[/green]")
124
+ console.print(table)
125
+ else:
126
+ console.print("📋 Here are some available options:", style="blue")
127
+ # Show first 10 installers as examples
128
+ if len(all_names) > 10:
129
+ sample_names = all_names[:10]
130
+ else:
131
+ sample_names = all_names
132
+ table = Table(show_header=False, box=None, pad_edge=False)
133
+ for i, name in enumerate(sample_names, 1):
134
+ table.add_row(f"[cyan]{i}.[/cyan]", f"[green]{name}[/green]")
135
+ console.print(table)
136
+ if len(all_names) > 10:
137
+ console.print(f" [dim]... and {len(all_names) - 10} more[/dim]")
138
+
139
+ panel = Panel(f"[bold blue]💡 Use 'ia' to interactively browse all available installers.[/bold blue]\n[bold blue]💡 Use one of the categories: {list(PACKAGE_GROUP2NAMES.keys())}[/bold blue]", title="[yellow]Helpful Tips[/yellow]", border_style="yellow")
140
+ console.print(panel)
141
+
139
142
  def install_clis(clis_names: list[str]):
140
143
  from machineconfig.utils.schemas.installer.installer_types import get_normalized_arch, get_os_name
141
- from machineconfig.utils.installer import get_installers
144
+ from machineconfig.utils.installer_utils.installer_runner import get_installers
142
145
  from machineconfig.utils.installer_utils.installer_class import Installer
146
+ from rich.console import Console
147
+ all_installers = get_installers(os=get_os_name(), arch=get_normalized_arch(), which_cats=None)
143
148
  total_messages: list[str] = []
144
- for a_which in clis_names:
145
- all_installers = get_installers(os=get_os_name(), arch=get_normalized_arch(), which_cats=None)
149
+ for a_cli_name in clis_names:
150
+ if "github.com" in a_cli_name.lower():
151
+ from machineconfig.utils.installer_utils.install_from_url import install_from_github_url
152
+ install_from_github_url(github_url=a_cli_name)
153
+ continue
146
154
  selected_installer = None
147
155
  for installer in all_installers:
148
156
  app_name = installer["appName"]
149
- if app_name.lower() == a_which.lower():
157
+ if app_name.lower() == a_cli_name.lower():
150
158
  selected_installer = installer
151
159
  break
152
160
  if selected_installer is None:
153
- _handle_installer_not_found(a_which, all_names=[inst["appName"] for inst in all_installers])
161
+ _handle_installer_not_found(a_cli_name, all_names=[inst["appName"] for inst in all_installers])
154
162
  return None
155
163
  message = Installer(selected_installer).install_robust(version=None) # finish the task
156
164
  total_messages.append(message)
157
165
  if total_messages:
166
+ console = Console()
158
167
  console.print("\n[bold green]📊 Installation Results:[/bold green]")
159
168
  for a_message in total_messages:
160
169
  console.print(f"[blue]• {a_message}[/blue]")
161
170
  return None
162
-
163
-
164
171
  def install_if_missing(which: str):
165
- from machineconfig.utils.installer_utils.installer_abc import check_tool_exists
172
+ from machineconfig.utils.installer_utils.installer_locator_utils import check_tool_exists
166
173
  exists = check_tool_exists(which)
167
174
  if exists:
168
175
  print(f"✅ {which} is already installed.")
169
176
  return
170
177
  print(f"⏳ {which} not found. Installing...")
171
- from machineconfig.utils.installer_utils.installer import main
172
- main(which=which, interactive=False)
178
+ from machineconfig.utils.installer_utils.installer_cli import main_installer_cli
179
+ main_installer_cli(which=which, interactive=False)
173
180
 
174
181
 
175
182
  if __name__ == "__main__":
176
183
  from machineconfig.utils.schemas.installer.installer_types import InstallerData
177
184
  from machineconfig.utils.installer_utils.installer_class import Installer
178
-
179
185
  _ = InstallerData, Installer
180
186
  pass
@@ -1,6 +1,6 @@
1
1
  """package manager"""
2
2
 
3
- from machineconfig.utils.installer_utils.installer_abc import check_if_installed_already
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
6
  from machineconfig.jobs.installer.package_groups import PACKAGE_GROUP2NAMES
@@ -132,30 +132,6 @@ def install_bulk(installers_data: list[InstallerData], safe: bool = False, jobs:
132
132
  print("✅ Version cache cleared")
133
133
  if safe:
134
134
  pass
135
- # print("⚠️ Safe installation mode activated...")
136
- # from machineconfig.jobs.python.check_installations import APP_SUMMARY_PATH
137
- # if platform.system().lower() == "windows":
138
- # print("🪟 Moving applications to Windows Apps folder...")
139
- # # PathExtended.get_env().WindowsPaths().WindowsApps)
140
- # folder = PathExtended.home().joinpath("AppData/Local/Microsoft/WindowsApps")
141
- # apps_dir.search("*").apply(lambda app: app.move(folder=folder))
142
- # elif platform.system().lower() in ["linux", "darwin"]:
143
- # system_name = "Linux" if platform.system().lower() == "linux" else "macOS"
144
- # print(f"🐧 Moving applications to {system_name} bin folder...")
145
- # if platform.system().lower() == "linux":
146
- # install_path = LINUX_INSTALL_PATH
147
- # else: # Darwin/macOS
148
- # install_path = "/usr/local/bin"
149
- # 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)
150
- # else:
151
- # error_msg = f"❌ ERROR: System {platform.system()} not supported"
152
- # print(error_msg)
153
- # raise NotImplementedError(error_msg)
154
-
155
- # apps_dir.delete(sure=True)
156
- # print(f"✅ Safe installation completed\n{'='*80}")
157
- # return None
158
-
159
135
  print(f"🚀 Starting installation of {len(installers_data)} packages...")
160
136
  print("📦 INSTALLING FIRST PACKAGE 📦")
161
137
  Installer(installers_data[0]).install(version=None)
@@ -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,15 @@ 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
+ lmb=lambda: print_code(code=import_code_robust, lexer="python", desc="import as module code"),
253
+ in_global=True, import_module=False
254
+ )
255
+ 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
@@ -14,10 +14,10 @@ from typing import Optional, Union, Iterable, overload, Literal, cast
14
14
 
15
15
 
16
16
  @overload
17
- def choose_from_options[T](msg: str, options: Iterable[T], multi: Literal[False], custom_input: bool = False, header: str = "", tail: str = "", prompt: str = "", default: Optional[T] = None, fzf: bool = False) -> T: ...
17
+ def choose_from_options[T](options: Iterable[T], msg: str, multi: Literal[False], custom_input: bool = False, header: str = "", tail: str = "", prompt: str = "", default: Optional[T] = None, fzf: bool = False) -> T: ...
18
18
  @overload
19
- def choose_from_options[T](msg: str, options: Iterable[T], multi: Literal[True], custom_input: bool = True, header: str = "", tail: str = "", prompt: str = "", default: Optional[T] = None, fzf: bool = False, ) -> list[T]: ...
20
- def choose_from_options[T](msg: str, options: Iterable[T], multi: bool, custom_input: bool = True, header: str = "", tail: str = "", prompt: str = "", default: Optional[T] = None, fzf: bool = False, ) -> Union[T, list[T]]:
19
+ def choose_from_options[T](options: Iterable[T], msg: str, multi: Literal[True], custom_input: bool = True, header: str = "", tail: str = "", prompt: str = "", default: Optional[T] = None, fzf: bool = False, ) -> list[T]: ...
20
+ def choose_from_options[T](options: Iterable[T], msg: str, multi: bool, custom_input: bool = True, header: str = "", tail: str = "", prompt: str = "", default: Optional[T] = None, fzf: bool = False, ) -> Union[T, list[T]]:
21
21
  # TODO: replace with https://github.com/tmbo/questionary
22
22
  # # also see https://github.com/charmbracelet/gum
23
23
  options_strings: list[str] = [str(x) for x in options]
@@ -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 # type: ignore
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,4 +1,3 @@
1
- from machineconfig.utils.path_extended import PathExtended
2
1
  from machineconfig.utils.source_of_truth import EXCLUDE_DIRS
3
2
  from rich.console import Console
4
3
  from rich.panel import Panel
@@ -10,8 +9,8 @@ from typing import Optional
10
9
  console = Console()
11
10
 
12
11
 
13
- def sanitize_path(a_path: str) -> PathExtended:
14
- path = PathExtended(a_path)
12
+ def sanitize_path(a_path: str) -> Path:
13
+ path = Path(a_path)
15
14
  if Path.cwd() == Path.home() and not path.exists():
16
15
  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"
17
16
  if result == "y":
@@ -22,13 +21,13 @@ def sanitize_path(a_path: str) -> PathExtended:
22
21
  if platform.system() == "Windows": # path copied from Linux/Mac to Windows
23
22
  # For Linux: /home/username, for Mac: /Users/username
24
23
  skip_parts = 3 if path.as_posix().startswith("/home") else 3 # Both have 3 parts to skip
25
- path = PathExtended.home().joinpath(*path.parts[skip_parts:])
24
+ path = Path.home().joinpath(*path.parts[skip_parts:])
26
25
  assert path.exists(), f"File not found: {path}"
27
26
  source_os = "Linux" if path.as_posix().startswith("/home") else "macOS"
28
27
  console.print(Panel(f"🔗 PATH MAPPING | {source_os} → Windows: `{a_path}` ➡️ `{path}`", title="Path Mapping", expand=False))
29
- elif platform.system() in ["Linux", "Darwin"] and PathExtended.home().as_posix() not in path.as_posix(): # copied between Unix-like systems with different username
28
+ elif platform.system() in ["Linux", "Darwin"] and Path.home().as_posix() not in path.as_posix(): # copied between Unix-like systems with different username
30
29
  skip_parts = 3 # Both /home/username and /Users/username have 3 parts to skip
31
- path = PathExtended.home().joinpath(*path.parts[skip_parts:])
30
+ path = Path.home().joinpath(*path.parts[skip_parts:])
32
31
  assert path.exists(), f"File not found: {path}"
33
32
  current_os = "Linux" if platform.system() == "Linux" else "macOS"
34
33
  source_os = "Linux" if path.as_posix().startswith("/home") else "macOS"
@@ -36,12 +35,12 @@ def sanitize_path(a_path: str) -> PathExtended:
36
35
  elif path.as_posix().startswith("C:"):
37
36
  if platform.system() in ["Linux", "Darwin"]: # path copied from Windows to Linux/Mac
38
37
  xx = str(a_path).replace("\\\\", "/")
39
- path = PathExtended.home().joinpath(*PathExtended(xx).parts[3:]) # exclude C:\\Users\\username
38
+ path = Path.home().joinpath(*Path(xx).parts[3:]) # exclude C:\\Users\\username
40
39
  assert path.exists(), f"File not found: {path}"
41
40
  target_os = "Linux" if platform.system() == "Linux" else "macOS"
42
41
  console.print(Panel(f"🔗 PATH MAPPING | Windows → {target_os}: `{a_path}` ➡️ `{path}`", title="Path Mapping", expand=False))
43
- elif platform.system() == "Windows" and PathExtended.home().as_posix() not in path.as_posix(): # copied from Windows to Windows with different username
44
- path = PathExtended.home().joinpath(*path.parts[2:])
42
+ elif platform.system() == "Windows" and Path.home().as_posix() not in path.as_posix(): # copied from Windows to Windows with different username
43
+ path = Path.home().joinpath(*path.parts[2:])
45
44
  assert path.exists(), f"File not found: {path}"
46
45
  console.print(Panel(f"🔗 PATH MAPPING | Windows → Windows: `{a_path}` ➡️ `{path}`", title="Path Mapping", expand=False))
47
46
  return path.expanduser().absolute()
@@ -66,12 +65,12 @@ def find_scripts(root: Path, name_substring: str, suffixes: set[str]) -> tuple[l
66
65
  return filename_matches, partial_path_matches
67
66
 
68
67
 
69
- def match_file_name(sub_string: str, search_root: PathExtended, suffixes: set[str]) -> PathExtended:
68
+ def match_file_name(sub_string: str, search_root: Path, suffixes: set[str]) -> Path:
70
69
  search_root_obj = search_root.absolute()
71
70
  # assume subscript is filename only, not a sub_path. There is no need to fzf over the paths.
72
71
  filename_matches, partial_path_matches = find_scripts(search_root_obj, sub_string, suffixes)
73
72
  if len(filename_matches) == 1:
74
- return PathExtended(filename_matches[0])
73
+ return Path(filename_matches[0])
75
74
  console.print(Panel(f"Partial filename {search_root_obj} match with case-insensitivity failed. This generated #{len(filename_matches)} results.", title="Search", expand=False))
76
75
  if len(filename_matches) < 20:
77
76
  print("\n".join([a_potential_match.as_posix() for a_potential_match in filename_matches]))
@@ -80,23 +79,23 @@ def match_file_name(sub_string: str, search_root: PathExtended, suffixes: set[st
80
79
  # let's see if avoiding .lower() helps narrowing down to one result
81
80
  reduced_scripts = [a_potential_match for a_potential_match in filename_matches if sub_string in a_potential_match.name]
82
81
  if len(reduced_scripts) == 1:
83
- return PathExtended(reduced_scripts[0])
82
+ return Path(reduced_scripts[0])
84
83
  elif len(reduced_scripts) > 1:
85
84
  from machineconfig.utils.options import choose_from_options
86
85
  choice = choose_from_options(multi=False, msg="Multiple matches found", options=reduced_scripts, fzf=True)
87
- return PathExtended(choice)
86
+ return Path(choice)
88
87
  print(f"Result: This still generated {len(reduced_scripts)} results.")
89
88
  if len(reduced_scripts) < 10:
90
89
  print("\n".join([a_potential_match.as_posix() for a_potential_match in reduced_scripts]))
91
90
 
92
91
  console.print(Panel(f"Partial path match with case-insensitivity failed. This generated #{len(partial_path_matches)} results.", title="Search", expand=False))
93
92
  if len(partial_path_matches) == 1:
94
- return PathExtended(partial_path_matches[0])
93
+ return Path(partial_path_matches[0])
95
94
  elif len(partial_path_matches) > 1:
96
95
  print("Try to narrow down partial_path_matches search by case-sensitivity.")
97
96
  reduced_scripts = [a_potential_match for a_potential_match in partial_path_matches if sub_string in a_potential_match.as_posix()]
98
97
  if len(reduced_scripts) == 1:
99
- return PathExtended(reduced_scripts[0])
98
+ return Path(reduced_scripts[0])
100
99
  print(f"Result: This still generated {len(reduced_scripts)} results.")
101
100
 
102
101
  try:
@@ -131,21 +130,24 @@ def match_file_name(sub_string: str, search_root: PathExtended, suffixes: set[st
131
130
  return search_root_obj.joinpath(res)
132
131
 
133
132
 
134
- def search_for_files_of_interest(path_obj: PathExtended, suffixes: set[str]):
135
- if path_obj.joinpath(".venv").exists():
136
- path_objects = path_obj.search("*", not_in=[".venv"])
137
- files: list[PathExtended] = []
138
- for a_path_obj in path_objects:
139
- files += search_for_files_of_interest(path_obj=a_path_obj, suffixes=suffixes)
140
- return files
133
+ def search_for_files_of_interest(path_obj: Path, suffixes: set[str]) -> list[Path]:
141
134
  if path_obj.is_file():
142
135
  return [path_obj]
143
- files: list[PathExtended] = []
144
- for a_suffix in suffixes:
145
- if a_suffix == ".py":
146
- files += path_obj.search(pattern="*.py", r=True, not_in=["__init__.py"])
147
- else:
148
- files += path_obj.search(pattern=f"*{a_suffix}", r=True)
136
+ files: list[Path] = []
137
+ directories_to_visit: list[Path] = [path_obj]
138
+ while directories_to_visit:
139
+ current_dir = directories_to_visit.pop()
140
+ for entry in current_dir.iterdir():
141
+ if entry.is_dir():
142
+ if entry.name == ".venv":
143
+ continue
144
+ directories_to_visit.append(entry)
145
+ continue
146
+ if entry.suffix not in suffixes:
147
+ continue
148
+ if entry.suffix == ".py" and entry.name == "__init__.py":
149
+ continue
150
+ files.append(entry)
149
151
  return files
150
152
 
151
153
 
@@ -160,15 +162,15 @@ def get_choice_file(path: str, suffixes: Optional[set[str]]):
160
162
  else:
161
163
  suffixes = {".py"}
162
164
  if not path_obj.exists():
163
- print(f"🔍 Searching for file matching `{path}` under `{PathExtended.cwd()}`, but only if suffix matches {suffixes}")
164
- choice_file = match_file_name(sub_string=path, search_root=PathExtended.cwd(), suffixes=suffixes)
165
+ print(f"🔍 Searching for file matching `{path}` under `{Path.cwd()}`, but only if suffix matches {suffixes}")
166
+ choice_file = match_file_name(sub_string=path, search_root=Path.cwd(), suffixes=suffixes)
165
167
  elif path_obj.is_dir():
166
168
  print(f"🔍 Searching recursively for Python, PowerShell and Shell scripts in directory `{path_obj}`")
167
169
  files = search_for_files_of_interest(path_obj, suffixes=suffixes)
168
170
  print(f"🔍 Got #{len(files)} results.")
169
171
  from machineconfig.utils.options import choose_from_options
170
172
  choice_file = choose_from_options(multi=False, options=files, fzf=True, msg="Choose one option")
171
- choice_file = PathExtended(choice_file)
173
+ choice_file = Path(choice_file)
172
174
  else:
173
175
  choice_file = path_obj
174
176
  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):