machineconfig 6.83__py3-none-any.whl → 6.85__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 (42) hide show
  1. machineconfig/cluster/sessions_managers/wt_local.py +16 -221
  2. machineconfig/cluster/sessions_managers/wt_local_manager.py +33 -174
  3. machineconfig/cluster/sessions_managers/wt_remote_manager.py +39 -197
  4. machineconfig/cluster/sessions_managers/wt_utils/manager_persistence.py +52 -0
  5. machineconfig/cluster/sessions_managers/wt_utils/monitoring_helpers.py +50 -0
  6. machineconfig/cluster/sessions_managers/wt_utils/status_reporting.py +76 -0
  7. machineconfig/cluster/sessions_managers/wt_utils/wt_helpers.py +199 -0
  8. machineconfig/scripts/linux/mcfgs +1 -1
  9. machineconfig/scripts/linux/term +39 -0
  10. machineconfig/scripts/python/ai/vscode_tasks.py +7 -2
  11. machineconfig/scripts/python/env_manager/path_manager_tui.py +1 -1
  12. machineconfig/scripts/python/fire_jobs.py +30 -65
  13. machineconfig/scripts/python/helpers_devops/cli_config.py +1 -1
  14. machineconfig/scripts/python/helpers_devops/cli_nw.py +50 -0
  15. machineconfig/scripts/python/helpers_devops/cli_self.py +3 -3
  16. machineconfig/scripts/python/helpers_devops/cli_utils.py +1 -1
  17. machineconfig/scripts/python/helpers_fire/helpers4.py +15 -0
  18. machineconfig/scripts/python/helpers_repos/cloud_repo_sync.py +1 -1
  19. machineconfig/scripts/python/nw/mount_nfs +1 -1
  20. machineconfig/scripts/python/nw/wifi_conn.py +1 -53
  21. machineconfig/scripts/python/terminal.py +110 -0
  22. machineconfig/scripts/python/utils.py +2 -0
  23. machineconfig/scripts/windows/mounts/mount_ssh.ps1 +1 -1
  24. machineconfig/scripts/windows/term.ps1 +48 -0
  25. machineconfig/settings/shells/bash/init.sh +2 -0
  26. machineconfig/settings/shells/pwsh/init.ps1 +1 -0
  27. machineconfig/setup_linux/web_shortcuts/interactive.sh +2 -1
  28. machineconfig/setup_windows/web_shortcuts/interactive.ps1 +2 -1
  29. machineconfig/utils/code.py +21 -14
  30. machineconfig/utils/installer.py +0 -1
  31. machineconfig/utils/options.py +12 -2
  32. machineconfig/utils/path_helper.py +1 -1
  33. machineconfig/utils/scheduling.py +0 -2
  34. machineconfig/utils/ssh.py +2 -2
  35. {machineconfig-6.83.dist-info → machineconfig-6.85.dist-info}/METADATA +1 -1
  36. {machineconfig-6.83.dist-info → machineconfig-6.85.dist-info}/RECORD +39 -35
  37. {machineconfig-6.83.dist-info → machineconfig-6.85.dist-info}/entry_points.txt +1 -0
  38. machineconfig/scripts/linux/other/share_smb +0 -1
  39. machineconfig/scripts/linux/warp-cli.sh +0 -122
  40. machineconfig/scripts/linux/z_ls +0 -104
  41. {machineconfig-6.83.dist-info → machineconfig-6.85.dist-info}/WHEEL +0 -0
  42. {machineconfig-6.83.dist-info → machineconfig-6.85.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,199 @@
1
+ import subprocess
2
+ import random
3
+ import string
4
+ import json
5
+ import shlex
6
+ import logging
7
+ from typing import Any
8
+ from pathlib import Path
9
+
10
+ from machineconfig.utils.schemas.layouts.layout_types import LayoutConfig
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ POWERSHELL_CMD = "powershell" if __import__("platform").system().lower() == "windows" else "pwsh"
15
+
16
+
17
+ def generate_random_suffix(length: int) -> str:
18
+ """Generate a random string suffix for unique PowerShell script names."""
19
+ return "".join(random.choices(string.ascii_lowercase + string.digits, k=length))
20
+
21
+
22
+ def parse_command(command: str) -> tuple[str, list[str]]:
23
+ try:
24
+ parts = shlex.split(command)
25
+ if not parts:
26
+ raise ValueError("Empty command provided")
27
+ return parts[0], parts[1:] if len(parts) > 1 else []
28
+ except ValueError as e:
29
+ logger.error(f"Error parsing command '{command}': {e}")
30
+ parts = command.split()
31
+ return parts[0] if parts else "", parts[1:] if len(parts) > 1 else []
32
+
33
+
34
+ def escape_for_wt(text: str) -> str:
35
+ """Escape text for use in Windows Terminal commands."""
36
+ text = text.replace('"', '""')
37
+ if " " in text or ";" in text or "&" in text or "|" in text:
38
+ return f'"{text}"'
39
+ return text
40
+
41
+
42
+ def validate_layout_config(layout_config: LayoutConfig) -> None:
43
+ """Validate layout configuration format and content."""
44
+ if not layout_config["layoutTabs"]:
45
+ raise ValueError("Layout must contain at least one tab")
46
+ for tab in layout_config["layoutTabs"]:
47
+ if not tab["tabName"].strip():
48
+ raise ValueError(f"Invalid tab name: {tab['tabName']}")
49
+ if not tab["command"].strip():
50
+ raise ValueError(f"Invalid command for tab '{tab['tabName']}': {tab['command']}")
51
+ if not tab["startDir"].strip():
52
+ raise ValueError(f"Invalid startDir for tab '{tab['tabName']}': {tab['startDir']}")
53
+
54
+
55
+ def generate_wt_command_string(layout_config: LayoutConfig, window_name: str) -> str:
56
+ """Generate complete Windows Terminal command string."""
57
+ command_parts = []
58
+
59
+ for i, tab in enumerate(layout_config["layoutTabs"]):
60
+ is_first = i == 0
61
+
62
+ if is_first:
63
+ tab_parts = ["wt", "-w", escape_for_wt(window_name)]
64
+ else:
65
+ tab_parts = ["new-tab"]
66
+
67
+ tab_name = tab["tabName"]
68
+ cwd = tab["startDir"]
69
+ command = tab["command"]
70
+
71
+ if cwd.startswith("~/"):
72
+ cwd = cwd.replace("~/", f"{Path.home()}/")
73
+ elif cwd == "~":
74
+ cwd = str(Path.home())
75
+
76
+ tab_parts.extend(["-d", escape_for_wt(cwd)])
77
+ tab_parts.extend(["--title", escape_for_wt(tab_name)])
78
+ tab_parts.append("--")
79
+
80
+ # Split the command into arguments
81
+ command_args = shlex.split(command)
82
+ tab_parts.extend(command_args)
83
+
84
+ command_parts.append(" ".join(tab_parts))
85
+
86
+ return " `; ".join(command_parts)
87
+
88
+
89
+ def check_wt_session_status(session_name: str) -> dict[str, Any]:
90
+ try:
91
+ ps_script = """
92
+ try {
93
+ $wtProcesses = Get-Process -Name 'WindowsTerminal' -ErrorAction SilentlyContinue
94
+ if ($wtProcesses) {
95
+ $processInfo = @()
96
+ $wtProcesses | ForEach-Object {
97
+ $info = @{
98
+ "Id" = $_.Id
99
+ "ProcessName" = $_.ProcessName
100
+ "StartTime" = $_.StartTime.ToString()
101
+ }
102
+ $processInfo += $info
103
+ }
104
+ $processInfo | ConvertTo-Json -Depth 2
105
+ }
106
+ } catch {
107
+ # No Windows Terminal processes found
108
+ }
109
+ """
110
+
111
+ result = subprocess.run([POWERSHELL_CMD, "-Command", ps_script], capture_output=True, text=True, timeout=5)
112
+
113
+ if result.returncode == 0:
114
+ output = result.stdout.strip()
115
+ if output and output != "":
116
+ try:
117
+ processes = json.loads(output)
118
+ if not isinstance(processes, list):
119
+ processes = [processes]
120
+
121
+ return {"wt_running": True, "session_exists": len(processes) > 0, "session_name": session_name, "all_windows": processes, "session_windows": processes}
122
+ except Exception as e:
123
+ return {"wt_running": True, "session_exists": False, "error": f"Failed to parse process info: {e}", "session_name": session_name}
124
+ else:
125
+ return {"wt_running": False, "session_exists": False, "session_name": session_name, "all_windows": []}
126
+ else:
127
+ return {"wt_running": False, "error": result.stderr, "session_name": session_name}
128
+
129
+ except subprocess.TimeoutExpired:
130
+ return {"wt_running": False, "error": "Timeout while checking Windows Terminal processes", "session_name": session_name}
131
+ except FileNotFoundError:
132
+ return {"wt_running": False, "error": f"PowerShell ({POWERSHELL_CMD}) not found in PATH", "session_name": session_name}
133
+ except Exception as e:
134
+ return {"wt_running": False, "error": str(e), "session_name": session_name}
135
+
136
+
137
+ def check_command_status(tab_name: str, layout_config: LayoutConfig) -> dict[str, Any]:
138
+ """Check if a command is running by looking for processes."""
139
+ tab_config = None
140
+ for tab in layout_config["layoutTabs"]:
141
+ if tab["tabName"] == tab_name:
142
+ tab_config = tab
143
+ break
144
+
145
+ if tab_config is None:
146
+ return {"status": "unknown", "error": f"Tab '{tab_name}' not found in layout config", "running": False, "pid": None, "command": None}
147
+
148
+ command = tab_config["command"]
149
+
150
+ try:
151
+ primary_cmd = command.split()[0] if command.strip() else ""
152
+ if not primary_cmd:
153
+ return {"status": "error", "error": "Empty command", "running": False, "command": command, "tab_name": tab_name}
154
+
155
+ ps_script = f"""
156
+ try {{
157
+ $processes = Get-Process -Name '{primary_cmd}' -ErrorAction SilentlyContinue
158
+ if ($processes) {{
159
+ $processes | ForEach-Object {{
160
+ $procInfo = @{{
161
+ "pid" = $_.Id
162
+ "name" = $_.ProcessName
163
+ "start_time" = $_.StartTime.ToString()
164
+ }}
165
+ Write-Output ($procInfo | ConvertTo-Json -Compress)
166
+ }}
167
+ }}
168
+ }} catch {{
169
+ # No processes found or other error
170
+ }}
171
+ """
172
+
173
+ result = subprocess.run([POWERSHELL_CMD, "-Command", ps_script], capture_output=True, text=True, timeout=5)
174
+
175
+ if result.returncode == 0:
176
+ output_lines = [line.strip() for line in result.stdout.strip().split("\n") if line.strip()]
177
+ matching_processes = []
178
+
179
+ for line in output_lines:
180
+ if line.startswith("{") and line.endswith("}"):
181
+ try:
182
+ proc_info = json.loads(line)
183
+ matching_processes.append(proc_info)
184
+ except json.JSONDecodeError:
185
+ continue
186
+
187
+ if matching_processes:
188
+ return {"status": "running", "running": True, "processes": matching_processes, "command": command, "tab_name": tab_name}
189
+ else:
190
+ return {"status": "not_running", "running": False, "processes": [], "command": command, "tab_name": tab_name}
191
+ else:
192
+ return {"status": "error", "error": f"Command failed: {result.stderr}", "running": False, "command": command, "tab_name": tab_name}
193
+
194
+ except subprocess.TimeoutExpired:
195
+ logger.error(f"Timeout checking command status for tab '{tab_name}'")
196
+ return {"status": "timeout", "error": "Timeout checking process status", "running": False, "command": command, "tab_name": tab_name}
197
+ except Exception as e:
198
+ logger.error(f"Error checking command status for tab '{tab_name}': {e}")
199
+ return {"status": "error", "error": str(e), "running": False, "command": command, "tab_name": tab_name}
@@ -25,7 +25,7 @@ mcfg "$@"
25
25
 
26
26
  if [[ -f "$OP_PROGRAM_PATH" ]]; then
27
27
  printf "%b\n" "${GREEN}✅ Found op program:${RESET} ${OP_PROGRAM_PATH}"
28
- bat "$OP_PROGRAM_PATH"
28
+ bat --style=plain --paging=never "$OP_PROGRAM_PATH"
29
29
  printf "%b\n" "${GREEN}▶ Running...${RESET}"
30
30
  . "$OP_PROGRAM_PATH"
31
31
  status=$?
@@ -0,0 +1,39 @@
1
+
2
+ #!/usr/bin/env bash
3
+ set -euo pipefail
4
+
5
+ RANDOM_NAME=$(date +%s%N | sha256sum | head -c 16)
6
+ OP_DIR="$HOME/tmp_results/tmp_scripts/machineconfig"
7
+ OP_PROGRAM_PATH="$OP_DIR/${RANDOM_NAME}.sh"
8
+ export OP_PROGRAM_PATH
9
+
10
+ # ANSI color/style codes
11
+ BOLD="\033[1m"
12
+ RESET="\033[0m"
13
+ GREEN="\033[32m"
14
+ YELLOW="\033[33m"
15
+ BLUE="\033[34m"
16
+ RED="\033[31m"
17
+
18
+ timestamp=$(date -u +"%Y-%m-%d %H:%M:%SZ")
19
+
20
+ printf "%b\n" "${BOLD}${BLUE}🛠️ terminal — running term${RESET}"
21
+ printf "%b\n" "${BLUE}Timestamp:${RESET} ${timestamp}"
22
+ printf "%b\n" "${BLUE}Op program path:${RESET} ${OP_PROGRAM_PATH}"
23
+
24
+ terminal "$@"
25
+
26
+ if [[ -f "$OP_PROGRAM_PATH" ]]; then
27
+ printf "%b\n" "${GREEN}✅ Found op program:${RESET} ${OP_PROGRAM_PATH}"
28
+ bat --style=plain --paging=never "$OP_PROGRAM_PATH"
29
+ printf "%b\n" "${GREEN}▶ Running...${RESET}"
30
+ . "$OP_PROGRAM_PATH"
31
+ status=$?
32
+ if [[ $status -eq 0 ]]; then
33
+ printf "%b\n" "${GREEN}✅ Completed successfully (exit ${status})${RESET}"
34
+ else
35
+ printf "%b\n" "${YELLOW}⚠️ Program exited with status ${status}${RESET}"
36
+ fi
37
+ else
38
+ printf "%b\n" "${YELLOW}⚠️ No op program found at: ${OP_PROGRAM_PATH}${RESET}"
39
+ fi
@@ -1,5 +1,6 @@
1
1
  import json
2
2
  from pathlib import Path
3
+ from typing import Any
3
4
 
4
5
 
5
6
  def add_lint_and_type_check_task(repo_root: Path) -> None:
@@ -18,8 +19,12 @@ def add_lint_and_type_check_task(repo_root: Path) -> None:
18
19
  }
19
20
 
20
21
  if tasks_json_path.exists():
21
- with tasks_json_path.open("r") as f:
22
- tasks_config = json.load(f)
22
+ json_data = tasks_json_path.read_text(encoding="utf-8")
23
+ if not json_data.strip():
24
+ tasks_config: dict[str, Any] = {"version": "2.0.0", "tasks": []}
25
+ else:
26
+ tasks_config = json.loads(json_data)
27
+ assert isinstance(tasks_config, dict)
23
28
  if "tasks" not in tasks_config:
24
29
  tasks_config["tasks"] = []
25
30
  existing_labels = {task.get("label") for task in tasks_config.get("tasks", [])}
@@ -2,7 +2,7 @@
2
2
  # /// script
3
3
  # requires-python = ">=3.13"
4
4
  # dependencies = [
5
- # "machineconfig>=6.83",
5
+ # "machineconfig>=6.85",
6
6
  # "textual",
7
7
  # "pyperclip",
8
8
  # ]
@@ -7,7 +7,7 @@ fire
7
7
 
8
8
  """
9
9
 
10
- from machineconfig.utils.ve import get_ve_activate_line, get_ve_path_and_ipython_profile
10
+ from machineconfig.utils.ve import get_ve_path_and_ipython_profile
11
11
  from machineconfig.utils.options import choose_from_options
12
12
  from machineconfig.utils.path_helper import match_file_name, sanitize_path
13
13
  from machineconfig.utils.path_extended import PathExtended
@@ -38,10 +38,6 @@ def route(args: FireJobArgs, fire_args: str = "") -> None:
38
38
 
39
39
  repo_root = get_repo_root(Path(choice_file))
40
40
  print(f"💾 Selected file: {choice_file}.\nRepo root: {repo_root}")
41
- ve_root_from_file, ipy_profile = get_ve_path_and_ipython_profile(choice_file)
42
- if ipy_profile is None:
43
- ipy_profile = "default"
44
-
45
41
  if args.marimo:
46
42
  tmp_dir = PathExtended.tmp().joinpath(f"tmp_scripts/marimo/{choice_file.stem}_{randstr()}")
47
43
  tmp_dir.mkdir(parents=True, exist_ok=True)
@@ -58,15 +54,9 @@ uv run --project {repo_root} --with marimo marimo edit --host 0.0.0.0 marimo_nb.
58
54
  # ========================= preparing kwargs_dict
59
55
  if choice_file.suffix == ".py":
60
56
  kwargs_dict = extract_kwargs(args) # This now returns empty dict, but kept for compatibility
61
- ve_root = args.ve or ve_root_from_file
62
- if ve_root is None:
63
- raise ValueError(f"Could not determine virtual environment for file {choice_file}. Please ensure it is within a recognized project structure or specify the `--ve` option.")
64
- activate_ve_line = get_ve_activate_line(ve_root=ve_root)
65
57
  else:
66
- activate_ve_line = ""
67
58
  kwargs_dict = {}
68
59
 
69
-
70
60
  # ========================= choosing function to run
71
61
  choice_function: Optional[str] = None # Initialize to avoid unbound variable
72
62
  if args.choose_function:
@@ -77,58 +67,35 @@ uv run --project {repo_root} --with marimo marimo edit --host 0.0.0.0 marimo_nb.
77
67
 
78
68
  if choice_file.suffix == ".py":
79
69
  from machineconfig.scripts.python.helpers_fire_command.fire_jobs_route_helper import get_command_streamlit
80
- if args.streamlit: exe = get_command_streamlit(choice_file, args.environment, repo_root)
81
- elif args.interactive is False: exe = "python"
82
- elif args.jupyter: exe = "jupyter-lab"
83
- else: exe = f"ipython -i --no-banner --profile {ipy_profile} "
70
+ if args.streamlit:
71
+ exe = get_command_streamlit(choice_file=choice_file, environment=args.environment, repo_root=repo_root)
72
+ exe = f"uv run {exe} "
73
+ elif args.jupyter: exe = "uv run jupyter-lab"
74
+ else:
75
+ if args.interactive:
76
+ _ve_root_from_file, ipy_profile = get_ve_path_and_ipython_profile(choice_file)
77
+ if ipy_profile is None:
78
+ ipy_profile = "default"
79
+ exe = f"uv run ipython -i --no-banner --profile {ipy_profile} "
80
+ else:
81
+ exe = "uv run python "
84
82
  elif choice_file.suffix == ".ps1" or choice_file.suffix == ".sh": exe = "."
85
83
  elif choice_file.suffix == "": exe = ""
86
84
  else: raise NotImplementedError(f"File type {choice_file.suffix} not supported, in the sense that I don't know how to fire it.")
87
85
 
88
86
  if args.module or (args.debug and args.choose_function): # because debugging tools do not support choosing functions and don't interplay with fire module. So the only way to have debugging and choose function options is to import the file as a module into a new script and run the function of interest there and debug the new script.
89
87
  assert choice_file.suffix == ".py", f"File must be a python file to be imported as a module. Got {choice_file}"
90
- from machineconfig.scripts.python.helpers_fire.helpers4 import get_import_module_code
91
- import_line = get_import_module_code(str(choice_file))
92
- if repo_root is not None:
93
- repo_root_add = f"""sys.path.append(r'{repo_root}')"""
94
- else:
95
- repo_root_add = ""
96
- txt: str = f"""
97
- try:
98
- {import_line}
99
- except (ImportError, ModuleNotFoundError) as ex:
100
- print(fr"❌ Failed to import `{choice_file}` as a module: {{ex}} ")
101
- print(fr"⚠️ Attempting import with ad-hoc `$PATH` manipulation. DO NOT pickle any objects in this session as correct deserialization cannot be guaranteed.")
102
- import sys
103
- sys.path.append(r'{PathExtended(choice_file).parent}')
104
- {repo_root_add}
105
- from {PathExtended(choice_file).stem} import *
106
- print(fr"✅ Successfully imported `{choice_file}`")
107
- """
108
- if choice_function is not None:
109
- txt = (
110
- txt
111
- + f"""
112
- res = {choice_function}({("**" + str(kwargs_dict)) if kwargs_dict else ""})
113
- """
114
- )
115
-
116
- txt = (
117
- f"""
118
- try:
119
- from rich.panel import Panel
120
- from rich.console import Console
121
- from rich.syntax import Syntax
122
- console = Console()
123
- console.print(Panel(Syntax(code=r'''{txt}''', lexer='python'), title='Import Script'), style="bold red")
124
- except ImportError as _ex:
125
- print(r'''{txt}''')
126
- """
127
- + txt
128
- )
129
- choice_file = PathExtended.tmp().joinpath(f"tmp_scripts/python/{PathExtended(choice_file).parent.name}_{PathExtended(choice_file).stem}_{randstr()}.py")
88
+ from machineconfig.scripts.python.helpers_fire.helpers4 import get_import_module_code, wrap_import_in_try_except
89
+ from machineconfig.utils.meta import lambda_to_python_script
90
+ from machineconfig.utils.code import print_code
91
+ import_code = get_import_module_code(str(choice_file))
92
+ import_code_robust = lambda_to_python_script(lambda: wrap_import_in_try_except(import_line=import_code, pyfile=str(choice_file), repo_root=str(repo_root) if repo_root is not None else None), in_global=True, import_module=False)
93
+ code_printing = lambda_to_python_script(lambda: print_code(code=import_code_robust, lexer="python", desc="import code"), in_global=True, import_module=False)
94
+ if choice_function is not None: calling = f"""res = {choice_function}({("**" + str(kwargs_dict)) if kwargs_dict else ""})"""
95
+ else: calling = """# No function selected to call. You can add your code here."""
96
+ choice_file = Path.home().joinpath(f"tmp_results/tmp_scripts/python/{Path(choice_file).parent.name}_{Path(choice_file).stem}_{randstr()}.py")
130
97
  choice_file.parent.mkdir(parents=True, exist_ok=True)
131
- choice_file.write_text(txt, encoding="utf-8")
98
+ choice_file.write_text(import_code_robust + "\n" + code_printing + "\n" + calling, encoding="utf-8")
132
99
 
133
100
  # ========================= determining basic command structure: putting together exe & choice_file & choice_function & pdb
134
101
  if args.debug:
@@ -149,26 +116,24 @@ except ImportError as _ex:
149
116
  command = f"{exe} {choice_file}"
150
117
  else:
151
118
  command = f"cd {choice_file.parent}\n{exe} {choice_file.name}\ncd {PathExtended.cwd()}"
152
-
153
119
  elif args.cmd:
154
120
  command = rf""" cd /d {choice_file.parent} & {exe} {choice_file.name} """
155
121
  else:
156
122
  if choice_file.suffix == "": command = f"{exe} {choice_file} {fire_args}"
157
123
  else: command = f"{exe} {choice_file} "
158
124
 
159
- if not args.cmd: command = f"{activate_ve_line}\n{command}"
125
+
126
+ if not args.cmd: pass
160
127
  else:
161
128
  new_line = "\n"
162
- command = rf"""start cmd -Argument "/k {activate_ve_line.replace(".ps1", ".bat").replace(". ", "")} & {command.replace(new_line, " & ")} " """ # this works from powershell
129
+ command = rf"""start cmd -Argument "/k {command.replace(new_line, " & ")} " """ # this works from powershell
163
130
  if args.submit_to_cloud:
164
- command = f"""
165
- {activate_ve_line}
166
- python -m machineconfig.cluster.templates.cli_click --file {choice_file} """
131
+ command = f"""uv run python -m machineconfig.cluster.templates.cli_click --file {choice_file} """
167
132
  if choice_function is not None:
168
133
  command += f"--function {choice_function} "
169
134
 
170
- if args.optimized:
171
- command = command.replace("python ", "python -OO ")
135
+ if args.optimized: command = command.replace("python ", "python -OO ")
136
+
172
137
  from rich.panel import Panel
173
138
  from rich.console import Console
174
139
  from rich.syntax import Syntax
@@ -207,7 +172,7 @@ python -m machineconfig.cluster.templates.cli_click --file {choice_file} """
207
172
  import os
208
173
  op_program_path = os.environ.get("OP_PROGRAM_PATH", None)
209
174
  if op_program_path is not None:
210
- op_program_path = PathExtended(op_program_path)
175
+ op_program_path = Path(op_program_path)
211
176
  op_program_path.parent.mkdir(parents=True, exist_ok=True)
212
177
  op_program_path.write_text(command, encoding="utf-8")
213
178
  else:
@@ -46,7 +46,7 @@ def path():
46
46
  uv_with = ["textual"]
47
47
  uv_project_dir = None
48
48
  if not Path.home().joinpath("code/machineconfig").exists():
49
- uv_with.append("machineconfig>=6.83")
49
+ uv_with.append("machineconfig>=6.85")
50
50
  else:
51
51
  uv_project_dir = str(Path.home().joinpath("code/machineconfig"))
52
52
  run_shell_script(get_uv_command_executing_python_script(python_script=path.read_text(encoding="utf-8"), uv_with=uv_with, uv_project_dir=uv_project_dir)[0])
@@ -54,6 +54,52 @@ def debug_ssh():
54
54
  else:
55
55
  raise NotImplementedError(f"Platform {system()} is not supported.")
56
56
 
57
+ def wifi_select(
58
+ ssid: Annotated[str, typer.Option("-n", "--ssid", help="🔗 SSID of WiFi (from config)")] = "MyPhoneHotSpot",
59
+ manual: Annotated[bool, typer.Option("-m", "--manual", help="🔍 Manual network selection mode")] = False,
60
+ list_: Annotated[bool, typer.Option("-l", "--list", help="📡 List available networks only")] = False,
61
+ ) -> None:
62
+ """Main function with fallback network selection"""
63
+ from rich.panel import Panel
64
+ from rich.prompt import Confirm
65
+ from rich.console import Console
66
+ from machineconfig.scripts.python.nw.wifi_conn import try_config_connection, manual_network_selection, display_available_networks
67
+ console = Console()
68
+ console.print(Panel("📶 Welcome to the WiFi Connector Tool", title="[bold blue]WiFi Connection[/bold blue]", border_style="blue"))
69
+
70
+ # If user just wants to list networks
71
+ if list_:
72
+ display_available_networks()
73
+ return
74
+
75
+ # If user wants manual mode, skip config and go straight to selection
76
+ if manual:
77
+ console.print("[blue]🔍 Manual network selection mode[/blue]")
78
+ if manual_network_selection():
79
+ console.print("[green]🎉 Successfully connected![/green]")
80
+ else:
81
+ console.print("[red]❌ Failed to connect[/red]")
82
+ return
83
+
84
+ # Try to connect using configuration first
85
+ console.print(f"[blue]🔍 Attempting to connect to configured network: {ssid}[/blue]")
86
+
87
+ if try_config_connection(ssid):
88
+ console.print("[green]🎉 Successfully connected using configuration![/green]")
89
+ return
90
+
91
+ # Configuration failed, offer fallback options
92
+ console.print("\n[yellow]⚠️ Configuration connection failed or not available[/yellow]")
93
+
94
+ if Confirm.ask("[blue]Would you like to manually select a network?[/blue]", default=True):
95
+ if manual_network_selection():
96
+ console.print("[green]🎉 Successfully connected![/green]")
97
+ else:
98
+ console.print("[red]❌ Failed to connect[/red]")
99
+ else:
100
+ console.print("[blue]👋 Goodbye![/blue]")
101
+
102
+
57
103
  def get_app():
58
104
  nw_apps = typer.Typer(help="🔐 [n] Network subcommands", no_args_is_help=True, add_help_option=False, add_completion=False)
59
105
  nw_apps.command(name="share-terminal", help="📡 [t] Share terminal via web browser")(cli_terminal.main)
@@ -70,4 +116,8 @@ def get_app():
70
116
  nw_apps.command(name="a", help="Show this computer addresses on network", hidden=True)(show_address)
71
117
  nw_apps.command(name="debug-ssh", help="🐛 [d] Debug SSH connection")(debug_ssh)
72
118
  nw_apps.command(name="d", help="Debug SSH connection", hidden=True)(debug_ssh)
119
+
120
+ nw_apps.command(name="wifi-select", no_args_is_help=True, help="[w] WiFi connection utility.")(wifi_select)
121
+ nw_apps.command(name="w", no_args_is_help=True, hidden=True)(wifi_select)
122
+
73
123
  return nw_apps
@@ -41,9 +41,9 @@ def install(no_copy_assets: Annotated[bool, typer.Option("--no-assets-copy", "-n
41
41
  else:
42
42
  import platform
43
43
  if platform.system() == "Windows":
44
- run_shell_script(r"""& "$HOME\.local\bin\uv.exe" tool install --upgrade "machineconfig>=6.83" """)
44
+ run_shell_script(r"""& "$HOME\.local\bin\uv.exe" tool install --upgrade "machineconfig>=6.85" """)
45
45
  else:
46
- run_shell_script("""$HOME/.local/bin/uv tool install --upgrade "machineconfig>=6.83" """)
46
+ run_shell_script("""$HOME/.local/bin/uv tool install --upgrade "machineconfig>=6.85" """)
47
47
  from machineconfig.profile.create_shell_profile import create_default_shell_profile
48
48
  if not no_copy_assets:
49
49
  create_default_shell_profile() # involves copying assets too
@@ -68,7 +68,7 @@ def navigate():
68
68
  path = Path(navigator.__file__).resolve().parent.joinpath("devops_navigator.py")
69
69
  from machineconfig.utils.code import run_shell_script
70
70
  if Path.home().joinpath("code/machineconfig").exists(): executable = f"""--project "{str(Path.home().joinpath("code/machineconfig"))}" --with textual"""
71
- else: executable = """--with "machineconfig>=6.83,textual" """
71
+ else: executable = """--with "machineconfig>=6.85,textual" """
72
72
  run_shell_script(f"""uv run {executable} {path}""")
73
73
 
74
74
 
@@ -198,7 +198,7 @@ def init_project(python: Annotated[Literal["3.13", "3.14"], typer.Option("--pyth
198
198
  if not (repo_root / "pyproject.toml").exists():
199
199
  typer.echo("❌ Error: pyproject.toml not found.", err=True)
200
200
  raise typer.Exit(code=1)
201
- print(f"Adding group `plot` with common data science and plotting packages...")
201
+ print("Adding group `plot` with common data science and plotting packages...")
202
202
  script = """
203
203
  uv add --group plot \
204
204
  # Data & computation
@@ -110,3 +110,18 @@ def get_import_module_code(module_path: str):
110
110
  module_name = "IncorrectModuleName"
111
111
  # TODO: use py_compile to check if the statement is valid code to avoid syntax errors that can't be caught.
112
112
  return f"from {module_name} import *"
113
+
114
+
115
+ def wrap_import_in_try_except(import_line: str, pyfile: str, repo_root: Optional[str] = None) -> None:
116
+ try:
117
+ exec(import_line) # type: ignore
118
+ except (ImportError, ModuleNotFoundError) as ex:
119
+ print(fr"❌ Failed to import `{pyfile}` as a module: {ex} ")
120
+ print("⚠️ Attempting import with ad-hoc `$PATH` manipulation. DO NOT pickle any objects in this session as correct deserialization cannot be guaranteed.")
121
+ import sys
122
+ from pathlib import Path
123
+ sys.path.append(str(Path(pyfile).parent))
124
+ if repo_root is not None:
125
+ sys.path.append(repo_root)
126
+ exec(f"from {Path(pyfile).stem} import *")
127
+ print(fr"✅ Successfully imported `{pyfile}`")
@@ -80,7 +80,7 @@ git pull originEnc master
80
80
  uv_project_dir = f"""{str(Path.home().joinpath("code/machineconfig"))}"""
81
81
  uv_with = None
82
82
  else:
83
- uv_with = ["machineconfig>=6.83"]
83
+ uv_with = ["machineconfig>=6.85"]
84
84
  uv_project_dir = None
85
85
 
86
86
  import tempfile
@@ -5,7 +5,7 @@
5
5
  # mkdir ~/data/local
6
6
  # sudo mount -o nolock,noatime,nodiratime,proto=tcp,timeo=600,retrans=2,noac alex-p51s-5:/home/alex/data/local ./data/local
7
7
 
8
- uv run --python 3.14 --with "machineconfig>=6.83" python -m machineconfig.scripts.python.mount_nfs
8
+ uv run --python 3.14 --with "machineconfig>=6.85" python -m machineconfig.scripts.python.mount_nfs
9
9
  # Check if remote server is reachable and share folder exists
10
10
  if ! ping -c 1 "$remote_server" &> /dev/null; then
11
11
  echo "💥 Error: Remote server $remote_server is not reachable."
@@ -28,8 +28,6 @@ Usage examples:
28
28
 
29
29
  """
30
30
 
31
- from typing import Annotated
32
- import typer
33
31
  import configparser
34
32
  from pathlib import Path
35
33
  import os
@@ -38,8 +36,7 @@ import subprocess
38
36
  import getpass
39
37
  from typing import List, Dict, Optional
40
38
  from rich.console import Console
41
- from rich.panel import Panel
42
- from rich.prompt import Prompt, Confirm
39
+ from rich.prompt import Prompt
43
40
  from rich.table import Table
44
41
 
45
42
  console = Console()
@@ -263,51 +260,6 @@ def manual_network_selection() -> bool:
263
260
  return False
264
261
 
265
262
 
266
- def main(
267
- ssid: Annotated[str, typer.Option("-n", "--ssid", help="🔗 SSID of WiFi (from config)")] = "MyPhoneHotSpot",
268
- manual: Annotated[bool, typer.Option("-m", "--manual", help="🔍 Manual network selection mode")] = False,
269
- list_: Annotated[bool, typer.Option("-l", "--list", help="📡 List available networks only")] = False,
270
- ) -> None:
271
- """Main function with fallback network selection"""
272
- console.print(Panel("📶 Welcome to the WiFi Connector Tool", title="[bold blue]WiFi Connection[/bold blue]", border_style="blue"))
273
-
274
- # If user just wants to list networks
275
- if list_:
276
- display_available_networks()
277
- return
278
-
279
- # If user wants manual mode, skip config and go straight to selection
280
- if manual:
281
- console.print("[blue]🔍 Manual network selection mode[/blue]")
282
- if manual_network_selection():
283
- console.print("[green]🎉 Successfully connected![/green]")
284
- else:
285
- console.print("[red]❌ Failed to connect[/red]")
286
- return
287
-
288
- # Try to connect using configuration first
289
- console.print(f"[blue]🔍 Attempting to connect to configured network: {ssid}[/blue]")
290
-
291
- if try_config_connection(ssid):
292
- console.print("[green]🎉 Successfully connected using configuration![/green]")
293
- return
294
-
295
- # Configuration failed, offer fallback options
296
- console.print("\n[yellow]⚠️ Configuration connection failed or not available[/yellow]")
297
-
298
- if Confirm.ask("[blue]Would you like to manually select a network?[/blue]", default=True):
299
- if manual_network_selection():
300
- console.print("[green]🎉 Successfully connected![/green]")
301
- else:
302
- console.print("[red]❌ Failed to connect[/red]")
303
- else:
304
- console.print("[blue]👋 Goodbye![/blue]")
305
-
306
-
307
- def arg_parser() -> None:
308
- typer.run(main)
309
-
310
-
311
263
  def get_current_wifi_name() -> str:
312
264
  """Get the name of the currently connected WiFi network"""
313
265
  console.print("\n[blue]🔍 Checking current WiFi connection...[/blue]")
@@ -412,7 +364,3 @@ def create_new_connection(name: str, ssid: str, password: str):
412
364
  except Exception as e:
413
365
  console.print(f"[red]❌ Unexpected error: {e}[/red]")
414
366
  raise
415
-
416
-
417
- if __name__ == "__main__":
418
- arg_parser()