machineconfig 5.58__py3-none-any.whl → 5.60__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.
- machineconfig/scripts/python/devops_helpers/cli_config.py +8 -0
- machineconfig/scripts/python/devops_helpers/cli_self.py +1 -1
- machineconfig/scripts/python/env_manager/__init__.py +1 -0
- machineconfig/scripts/python/env_manager/path_manager_backend.py +47 -0
- machineconfig/scripts/python/env_manager/path_manager_tui.py +219 -0
- machineconfig/scripts/python/helpers_repos/cloud_repo_sync.py +13 -14
- machineconfig/setup_windows/web_shortcuts/interactive.ps1 +7 -7
- machineconfig/utils/code.py +10 -19
- machineconfig/utils/meta.py +166 -0
- machineconfig/utils/ssh.py +58 -47
- {machineconfig-5.58.dist-info → machineconfig-5.60.dist-info}/METADATA +2 -1
- {machineconfig-5.58.dist-info → machineconfig-5.60.dist-info}/RECORD +15 -12
- machineconfig/scripts/python/devops_navigator.py.backup +0 -899
- {machineconfig-5.58.dist-info → machineconfig-5.60.dist-info}/WHEEL +0 -0
- {machineconfig-5.58.dist-info → machineconfig-5.60.dist-info}/entry_points.txt +0 -0
- {machineconfig-5.58.dist-info → machineconfig-5.60.dist-info}/top_level.txt +0 -0
|
@@ -41,6 +41,14 @@ def shell():
|
|
|
41
41
|
from machineconfig.profile.create_shell_profile import create_default_shell_profile
|
|
42
42
|
create_default_shell_profile()
|
|
43
43
|
|
|
44
|
+
@config_apps.command(no_args_is_help=False)
|
|
45
|
+
def path():
|
|
46
|
+
"""📚 NAVIGATE PATH variable with TUI"""
|
|
47
|
+
from machineconfig.scripts.python import env_manager as navigator
|
|
48
|
+
from pathlib import Path
|
|
49
|
+
path = Path(navigator.__file__).resolve().parent.joinpath("path_manager_tui.py")
|
|
50
|
+
from machineconfig.utils.code import run_shell_script
|
|
51
|
+
run_shell_script(f"uv run --no-dev --with machineconfig,textual {path}")
|
|
44
52
|
|
|
45
53
|
@config_apps.command(no_args_is_help=False)
|
|
46
54
|
def pwsh_theme():
|
|
@@ -42,7 +42,7 @@ def navigate():
|
|
|
42
42
|
from pathlib import Path
|
|
43
43
|
path = Path(navigator.__file__).resolve().parent.joinpath("devops_navigator.py")
|
|
44
44
|
from machineconfig.utils.code import run_shell_script
|
|
45
|
-
run_shell_script(f"uv run --with machineconfig {path}")
|
|
45
|
+
run_shell_script(f"uv run --no-dev --with machineconfig,textual {path}")
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
@cli_app.command(no_args_is_help=True)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
a = 2
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Cross-platform PATH explorer backend."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import platform
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
PlatformType = Literal["Windows", "Linux", "Darwin"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_platform() -> PlatformType:
|
|
13
|
+
"""Get the current platform."""
|
|
14
|
+
return platform.system() # type: ignore[return-value]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_path_entries() -> list[str]:
|
|
18
|
+
"""Get all PATH entries for the current platform."""
|
|
19
|
+
path_str = os.environ.get("PATH", "")
|
|
20
|
+
separator = ";" if get_platform() == "Windows" else ":"
|
|
21
|
+
return [entry for entry in path_str.split(separator) if entry.strip()]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_directory_contents(directory: str, max_items: int = 50) -> list[str]:
|
|
25
|
+
"""Get contents of a directory, limited to max_items."""
|
|
26
|
+
try:
|
|
27
|
+
path = Path(directory)
|
|
28
|
+
if not path.exists():
|
|
29
|
+
return ["⚠️ Directory does not exist"]
|
|
30
|
+
if not path.is_dir():
|
|
31
|
+
return ["⚠️ Not a directory"]
|
|
32
|
+
|
|
33
|
+
items = []
|
|
34
|
+
for item in sorted(path.iterdir(), key=lambda p: (not p.is_dir(), p.name.lower())):
|
|
35
|
+
if len(items) >= max_items:
|
|
36
|
+
items.append(f"... and {sum(1 for _ in path.iterdir()) - max_items} more items")
|
|
37
|
+
break
|
|
38
|
+
prefix = "📁 " if item.is_dir() else "📄 "
|
|
39
|
+
items.append(f"{prefix}{item.name}")
|
|
40
|
+
|
|
41
|
+
if not items:
|
|
42
|
+
return ["📭 Empty directory"]
|
|
43
|
+
return items
|
|
44
|
+
except PermissionError:
|
|
45
|
+
return ["⚠️ Permission denied"]
|
|
46
|
+
except Exception as e:
|
|
47
|
+
return [f"⚠️ Error: {e!s}"]
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""Cross-platform PATH explorer with Textual TUI."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from textual import on
|
|
6
|
+
from textual.app import App, ComposeResult
|
|
7
|
+
from textual.binding import Binding
|
|
8
|
+
from textual.containers import Horizontal, Vertical
|
|
9
|
+
from textual.widgets import Footer, Header, Label, ListItem, ListView, Static
|
|
10
|
+
|
|
11
|
+
from machineconfig.scripts.python.env_manager.path_manager_backend import get_directory_contents, get_path_entries, get_platform
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DirectoryPreview(Static):
|
|
15
|
+
"""Widget to display directory contents."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def]
|
|
18
|
+
super().__init__(*args, **kwargs)
|
|
19
|
+
self.border_title = "📂 Directory Preview"
|
|
20
|
+
|
|
21
|
+
def update_preview(self, directory: str) -> None:
|
|
22
|
+
"""Update the preview with directory contents."""
|
|
23
|
+
if not directory:
|
|
24
|
+
self.update("Select a PATH entry to preview its contents")
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
contents = get_directory_contents(directory, max_items=30)
|
|
28
|
+
preview_text = f"[bold cyan]{directory}[/bold cyan]\n\n"
|
|
29
|
+
preview_text += "\n".join(contents)
|
|
30
|
+
self.update(preview_text)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class StatusBar(Static):
|
|
34
|
+
"""Status bar to show messages."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def]
|
|
37
|
+
super().__init__(*args, **kwargs)
|
|
38
|
+
self.border_title = "ℹ️ Status"
|
|
39
|
+
|
|
40
|
+
def show_message(self, message: str, message_type: str = "info") -> None:
|
|
41
|
+
"""Display a status message."""
|
|
42
|
+
color = {"info": "cyan", "success": "green", "error": "red", "warning": "yellow"}.get(message_type, "white")
|
|
43
|
+
self.update(f"[{color}]{message}[/{color}]")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class PathExplorerApp(App[None]):
|
|
47
|
+
"""A Textual app to explore PATH entries."""
|
|
48
|
+
|
|
49
|
+
CSS = """
|
|
50
|
+
Screen {
|
|
51
|
+
background: $surface;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
Header {
|
|
55
|
+
background: $primary;
|
|
56
|
+
color: $text;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
Footer {
|
|
60
|
+
background: $panel;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#main-container {
|
|
64
|
+
height: 100%;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
#left-panel {
|
|
68
|
+
width: 50%;
|
|
69
|
+
height: 100%;
|
|
70
|
+
border: solid $primary;
|
|
71
|
+
padding: 1;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
#right-panel {
|
|
75
|
+
width: 50%;
|
|
76
|
+
height: 100%;
|
|
77
|
+
border: solid $accent;
|
|
78
|
+
padding: 1;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
ListView {
|
|
82
|
+
height: 1fr;
|
|
83
|
+
border: solid $accent;
|
|
84
|
+
background: $surface;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
ListView > ListItem {
|
|
88
|
+
padding: 0 1;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
ListView > ListItem.--highlight {
|
|
92
|
+
background: $accent 20%;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
DirectoryPreview {
|
|
96
|
+
height: 1fr;
|
|
97
|
+
border: solid $primary;
|
|
98
|
+
background: $surface;
|
|
99
|
+
padding: 1;
|
|
100
|
+
overflow-y: auto;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
StatusBar {
|
|
104
|
+
height: 3;
|
|
105
|
+
border: solid $success;
|
|
106
|
+
background: $surface;
|
|
107
|
+
padding: 1;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
Label {
|
|
111
|
+
padding: 0 1;
|
|
112
|
+
height: auto;
|
|
113
|
+
}
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
BINDINGS = [
|
|
117
|
+
Binding("q", "quit", "Quit", show=True),
|
|
118
|
+
Binding("r", "refresh", "Refresh", show=True),
|
|
119
|
+
Binding("c", "copy_path", "Copy Path", show=True),
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
def __init__(self) -> None:
|
|
123
|
+
super().__init__()
|
|
124
|
+
self.selected_path: str = ""
|
|
125
|
+
|
|
126
|
+
def compose(self) -> ComposeResult:
|
|
127
|
+
"""Create child widgets for the app."""
|
|
128
|
+
platform_name = get_platform()
|
|
129
|
+
yield Header(show_clock=True)
|
|
130
|
+
|
|
131
|
+
with Horizontal(id="main-container"):
|
|
132
|
+
with Vertical(id="left-panel"):
|
|
133
|
+
yield Label(f"🔧 PATH Entries ({platform_name})")
|
|
134
|
+
yield ListView(id="path-list")
|
|
135
|
+
|
|
136
|
+
with Vertical(id="right-panel"):
|
|
137
|
+
yield DirectoryPreview(id="preview")
|
|
138
|
+
yield StatusBar(id="status")
|
|
139
|
+
|
|
140
|
+
yield Footer()
|
|
141
|
+
|
|
142
|
+
def on_mount(self) -> None:
|
|
143
|
+
"""Initialize the app when mounted."""
|
|
144
|
+
self.title = "PATH Explorer"
|
|
145
|
+
self.sub_title = f"Platform: {get_platform()}"
|
|
146
|
+
self.refresh_path_list()
|
|
147
|
+
self.query_one("#status", StatusBar).show_message("Ready. Select a PATH entry to preview its contents.", "info")
|
|
148
|
+
|
|
149
|
+
def refresh_path_list(self) -> None:
|
|
150
|
+
"""Refresh the list of PATH entries."""
|
|
151
|
+
list_view = self.query_one("#path-list", ListView)
|
|
152
|
+
list_view.clear()
|
|
153
|
+
|
|
154
|
+
entries = get_path_entries()
|
|
155
|
+
for entry in entries:
|
|
156
|
+
path = Path(entry)
|
|
157
|
+
exists = path.exists()
|
|
158
|
+
icon = "✅" if exists else "❌"
|
|
159
|
+
item = ListItem(Label(f"{icon} {entry}"))
|
|
160
|
+
item.set_class(exists, "--valid")
|
|
161
|
+
list_view.append(item)
|
|
162
|
+
|
|
163
|
+
self.query_one("#status", StatusBar).show_message(f"Loaded {len(entries)} PATH entries", "success")
|
|
164
|
+
|
|
165
|
+
@on(ListView.Highlighted)
|
|
166
|
+
def handle_highlight(self, event: ListView.Highlighted) -> None:
|
|
167
|
+
"""Handle highlighting of a PATH entry (scroll preview)."""
|
|
168
|
+
if event.item is None:
|
|
169
|
+
return
|
|
170
|
+
label = event.item.query_one(Label)
|
|
171
|
+
text = label.render()
|
|
172
|
+
# Remove the icon prefix
|
|
173
|
+
highlighted_path = str(text).split(" ", 1)[1] if " " in str(text) else str(text)
|
|
174
|
+
|
|
175
|
+
preview = self.query_one("#preview", DirectoryPreview)
|
|
176
|
+
preview.update_preview(highlighted_path)
|
|
177
|
+
|
|
178
|
+
self.query_one("#status", StatusBar).show_message(f"Previewing: {highlighted_path}", "info")
|
|
179
|
+
|
|
180
|
+
@on(ListView.Selected)
|
|
181
|
+
def handle_selection(self, event: ListView.Selected) -> None:
|
|
182
|
+
"""Handle selection of a PATH entry (Enter key)."""
|
|
183
|
+
label = event.item.query_one(Label)
|
|
184
|
+
text = label.render()
|
|
185
|
+
# Remove the icon prefix
|
|
186
|
+
self.selected_path = str(text).split(" ", 1)[1] if " " in str(text) else str(text)
|
|
187
|
+
|
|
188
|
+
preview = self.query_one("#preview", DirectoryPreview)
|
|
189
|
+
preview.update_preview(self.selected_path)
|
|
190
|
+
|
|
191
|
+
self.query_one("#status", StatusBar).show_message(f"Selected: {self.selected_path}", "success")
|
|
192
|
+
|
|
193
|
+
def action_refresh(self) -> None:
|
|
194
|
+
"""Refresh the PATH list."""
|
|
195
|
+
self.refresh_path_list()
|
|
196
|
+
|
|
197
|
+
def action_copy_path(self) -> None:
|
|
198
|
+
"""Copy selected path to clipboard."""
|
|
199
|
+
if not self.selected_path:
|
|
200
|
+
self.query_one("#status", StatusBar).show_message("No PATH entry selected", "warning")
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
# Try to copy to clipboard
|
|
204
|
+
try:
|
|
205
|
+
import pyperclip
|
|
206
|
+
pyperclip.copy(self.selected_path)
|
|
207
|
+
self.query_one("#status", StatusBar).show_message(f"Copied to clipboard: {self.selected_path}", "success")
|
|
208
|
+
except ImportError:
|
|
209
|
+
self.query_one("#status", StatusBar).show_message("pyperclip not available. Install it to enable clipboard support.", "warning")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def main() -> None:
|
|
213
|
+
"""Run the PATH Explorer TUI."""
|
|
214
|
+
app = PathExplorerApp()
|
|
215
|
+
app.run()
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
if __name__ == "__main__":
|
|
219
|
+
main()
|
|
@@ -11,7 +11,6 @@ from machineconfig.utils.code import get_shell_file_executing_python_script, wri
|
|
|
11
11
|
import platform
|
|
12
12
|
import subprocess
|
|
13
13
|
from typing import Optional, Literal
|
|
14
|
-
from pathlib import Path
|
|
15
14
|
|
|
16
15
|
|
|
17
16
|
console = Console()
|
|
@@ -96,13 +95,14 @@ git pull originEnc master
|
|
|
96
95
|
|
|
97
96
|
# ================================================================================
|
|
98
97
|
option1 = "Delete remote copy and push local:"
|
|
99
|
-
|
|
100
|
-
from machineconfig.scripts.python.
|
|
101
|
-
|
|
102
|
-
""
|
|
103
|
-
|
|
98
|
+
def func2(remote_repo: str, local_repo: str, cloud: str):
|
|
99
|
+
from machineconfig.scripts.python.repos_helpers.sync import delete_remote_repo_copy_and_push_local
|
|
100
|
+
delete_remote_repo_copy_and_push_local(remote_repo=remote_repo, local_repo=local_repo, cloud=cloud)
|
|
101
|
+
return "done"
|
|
102
|
+
from machineconfig.utils.meta import function_to_script
|
|
103
|
+
program_1_py = function_to_script(func=func2, call_with_args=None, call_with_kwargs={"remote_repo": str(repo_remote_root), "local_repo": str(repo_local_root), "cloud": cloud_resolved})
|
|
104
|
+
shell_file_1 = get_shell_file_executing_python_script(python_script=program_1_py, ve_path=None, executable="uv run --with machineconfig")
|
|
104
105
|
# ================================================================================
|
|
105
|
-
|
|
106
106
|
option2 = "Delete local repo and replace it with remote copy:"
|
|
107
107
|
program_2 = f"""
|
|
108
108
|
rm -rfd {repo_local_root}
|
|
@@ -114,16 +114,15 @@ sudo chmod 600 $HOME/.ssh/*
|
|
|
114
114
|
sudo chmod 700 $HOME/.ssh
|
|
115
115
|
sudo chmod +x $HOME/dotfiles/scripts/linux -R
|
|
116
116
|
"""
|
|
117
|
-
|
|
118
117
|
shell_file_2 = write_shell_script_to_file(shell_script=program_2)
|
|
119
|
-
|
|
120
118
|
# ================================================================================
|
|
121
119
|
option3 = "Inspect repos:"
|
|
122
|
-
|
|
123
|
-
from machineconfig.scripts.python.
|
|
124
|
-
|
|
125
|
-
""
|
|
126
|
-
|
|
120
|
+
def func(repo_local_root: str, repo_remote_root: str):
|
|
121
|
+
from machineconfig.scripts.python.repos_helpers.sync import inspect_repos
|
|
122
|
+
inspect_repos(repo_local_root=repo_local_root, repo_remote_root=repo_remote_root)
|
|
123
|
+
return "done"
|
|
124
|
+
program_3_py = function_to_script(func=func, call_with_args=None, call_with_kwargs={"repo_local_root": str(repo_local_root), "repo_remote_root": str(repo_remote_root)})
|
|
125
|
+
shell_file_3 = get_shell_file_executing_python_script(python_script=program_3_py, ve_path=None, executable="uv run --with machineconfig")
|
|
127
126
|
# ================================================================================
|
|
128
127
|
|
|
129
128
|
option4 = "Remove problematic rclone file from repo and replace with remote:"
|
|
@@ -2,30 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
iex (iwr "https://raw.githubusercontent.com/thisismygitrepo/machineconfig/main/src/machineconfig/setup_windows/uv.ps1").Content
|
|
4
4
|
function devops {
|
|
5
|
-
& "$HOME\.local\bin\uv.exe" run --python 3.
|
|
5
|
+
& "$HOME\.local\bin\uv.exe" run --python 3.14 --with machineconfig devops $args
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
function cloud {
|
|
9
|
-
& "$HOME\.local\bin\uv.exe" run --python 3.
|
|
9
|
+
& "$HOME\.local\bin\uv.exe" run --python 3.14 --with machineconfig cloud $args
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
function croshell {
|
|
13
|
-
& "$HOME\.local\bin\uv.exe" run --python 3.
|
|
13
|
+
& "$HOME\.local\bin\uv.exe" run --python 3.14 --with machineconfig croshell $args
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
function agents {
|
|
17
|
-
& "$HOME\.local\bin\uv.exe" run --python 3.
|
|
17
|
+
& "$HOME\.local\bin\uv.exe" run --python 3.14 --with machineconfig agents $args
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
function fire {
|
|
21
|
-
& "$HOME\.local\bin\uv.exe" run --python 3.
|
|
21
|
+
& "$HOME\.local\bin\uv.exe" run --python 3.14 --with machineconfig fire $args
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
function ftpx {
|
|
25
|
-
& "$HOME\.local\bin\uv.exe" run --python 3.
|
|
25
|
+
& "$HOME\.local\bin\uv.exe" run --python 3.14 --with machineconfig ftpx $args
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
function sessions {
|
|
29
|
-
& "$HOME\.local\bin\uv.exe" run --python 3.
|
|
29
|
+
& "$HOME\.local\bin\uv.exe" run --python 3.14 --with machineconfig sessions $args
|
|
30
30
|
}
|
|
31
31
|
|
machineconfig/utils/code.py
CHANGED
|
@@ -10,14 +10,16 @@ from machineconfig.utils.ve import get_ve_activate_line
|
|
|
10
10
|
from machineconfig.utils.path_extended import PathExtended
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
def get_shell_script_executing_python_file(python_file: str, func: Optional[str], ve_path: str, strict_execution: bool = True):
|
|
14
|
-
if
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
def get_shell_script_executing_python_file(python_file: str, func: Optional[str], ve_path: Optional[str], executable: Optional[str], strict_execution: bool = True):
|
|
14
|
+
if executable is None: exe_resolved = "python"
|
|
15
|
+
else: exe_resolved = executable
|
|
16
|
+
if func is None: exec_line = f"""{exe_resolved} {python_file}"""
|
|
17
|
+
else: exec_line = f"""{exe_resolved} -m fire {python_file} {func}"""
|
|
18
|
+
if ve_path is None: ve_activate_line = ""
|
|
19
|
+
else: ve_activate_line = get_ve_activate_line(ve_path)
|
|
18
20
|
shell_script = f"""
|
|
19
21
|
echo "Executing `{exec_line}`"
|
|
20
|
-
{
|
|
22
|
+
{ve_activate_line}
|
|
21
23
|
{exec_line}
|
|
22
24
|
deactivate || true
|
|
23
25
|
"""
|
|
@@ -31,7 +33,7 @@ deactivate || true
|
|
|
31
33
|
return shell_script
|
|
32
34
|
|
|
33
35
|
|
|
34
|
-
def get_shell_file_executing_python_script(python_script: str, ve_path: str, verbose: bool = True):
|
|
36
|
+
def get_shell_file_executing_python_script(python_script: str, ve_path: Optional[str], executable: Optional[str], verbose: bool = True):
|
|
35
37
|
if verbose:
|
|
36
38
|
python_script = f"""
|
|
37
39
|
code = r'''{python_script}'''
|
|
@@ -47,7 +49,7 @@ except ImportError:
|
|
|
47
49
|
python_file = PathExtended.tmp().joinpath("tmp_scripts", "python", randstr() + ".py")
|
|
48
50
|
python_file.parent.mkdir(parents=True, exist_ok=True)
|
|
49
51
|
python_file.write_text(python_script, encoding="utf-8")
|
|
50
|
-
shell_script = get_shell_script_executing_python_file(python_file=str(python_file), func=None, ve_path=ve_path)
|
|
52
|
+
shell_script = get_shell_script_executing_python_file(python_file=str(python_file), func=None, ve_path=ve_path, executable=executable)
|
|
51
53
|
shell_file = write_shell_script_to_file(shell_script)
|
|
52
54
|
return shell_file
|
|
53
55
|
|
|
@@ -132,14 +134,3 @@ def run_shell_script(script: str, display_script: bool = True, clean_env: bool =
|
|
|
132
134
|
temp_script_path.unlink(missing_ok=True)
|
|
133
135
|
console.print(f"🗑️ [blue]Temporary script deleted:[/blue] [green]{temp_script_path}[/green]")
|
|
134
136
|
return proc
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
# def run_command(command: str, description: str) -> bool:
|
|
138
|
-
# """Execute a shell command and return success status."""
|
|
139
|
-
# console.print(f"\n🔧 {description}", style="bold cyan")
|
|
140
|
-
# try:
|
|
141
|
-
# result = subprocess.run(command, shell=True, check=True, capture_output=False)
|
|
142
|
-
# return result.returncode == 0
|
|
143
|
-
# except subprocess.CalledProcessError as e:
|
|
144
|
-
# console.print(f"❌ Error executing command: {e}", style="bold red")
|
|
145
|
-
# return False
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Metaprogramming utilities for analyzing and serializing Python functions."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import inspect
|
|
5
|
+
import textwrap
|
|
6
|
+
from types import FunctionType, ModuleType
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def function_to_script(func: FunctionType, call_with_args: tuple[Any, ...] | None = None, call_with_kwargs: dict[str, Any] | None = None) -> str:
|
|
11
|
+
"""Convert a function to a standalone executable Python script.
|
|
12
|
+
|
|
13
|
+
This function analyzes a given function and generates a complete Python script
|
|
14
|
+
that includes all necessary imports, global variables, the function definition,
|
|
15
|
+
and optionally a function call.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
func: The function to convert to a script
|
|
19
|
+
call_with_args: Optional tuple of positional arguments to call the function with
|
|
20
|
+
call_with_kwargs: Optional dict of keyword arguments to call the function with
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
A complete Python script as a string that can be executed independently
|
|
24
|
+
|
|
25
|
+
Raises:
|
|
26
|
+
ValueError: If the function cannot be inspected or analyzed
|
|
27
|
+
"""
|
|
28
|
+
if not callable(func) or not hasattr(func, '__code__'):
|
|
29
|
+
raise ValueError(f"Expected a function, got {type(func)}")
|
|
30
|
+
|
|
31
|
+
call_with_args = call_with_args or ()
|
|
32
|
+
call_with_kwargs = call_with_kwargs or {}
|
|
33
|
+
|
|
34
|
+
imports = _extract_imports(func)
|
|
35
|
+
globals_needed = _extract_globals(func)
|
|
36
|
+
source_code = _get_function_source(func)
|
|
37
|
+
call_statement = _generate_call_statement(func, call_with_args, call_with_kwargs)
|
|
38
|
+
|
|
39
|
+
script_parts: list[str] = []
|
|
40
|
+
|
|
41
|
+
if imports:
|
|
42
|
+
script_parts.append(imports)
|
|
43
|
+
script_parts.append("")
|
|
44
|
+
|
|
45
|
+
if globals_needed:
|
|
46
|
+
script_parts.append(globals_needed)
|
|
47
|
+
script_parts.append("")
|
|
48
|
+
|
|
49
|
+
script_parts.append(source_code)
|
|
50
|
+
script_parts.append("")
|
|
51
|
+
|
|
52
|
+
if call_statement:
|
|
53
|
+
script_parts.append("")
|
|
54
|
+
script_parts.append("if __name__ == '__main__':")
|
|
55
|
+
script_parts.append(f" {call_statement}")
|
|
56
|
+
|
|
57
|
+
return "\n".join(script_parts)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _get_function_source(func: FunctionType) -> str:
|
|
61
|
+
"""Extract the source code of a function."""
|
|
62
|
+
try:
|
|
63
|
+
source = inspect.getsource(func)
|
|
64
|
+
return textwrap.dedent(source)
|
|
65
|
+
except (OSError, TypeError) as e:
|
|
66
|
+
raise ValueError(f"Cannot get source code for function {func.__name__}: {e}") from e
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _extract_imports(func: FunctionType) -> str:
|
|
70
|
+
"""Extract all import statements needed by the function."""
|
|
71
|
+
import_statements: set[str] = set()
|
|
72
|
+
|
|
73
|
+
source = _get_function_source(func)
|
|
74
|
+
func_globals = func.__globals__
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
tree = ast.parse(source)
|
|
78
|
+
except SyntaxError as e:
|
|
79
|
+
raise ValueError(f"Failed to parse function source: {e}") from e
|
|
80
|
+
|
|
81
|
+
used_names: set[str] = set()
|
|
82
|
+
for node in ast.walk(tree):
|
|
83
|
+
if isinstance(node, ast.Name):
|
|
84
|
+
used_names.add(node.id)
|
|
85
|
+
elif isinstance(node, ast.Attribute):
|
|
86
|
+
if isinstance(node.value, ast.Name):
|
|
87
|
+
used_names.add(node.value.id)
|
|
88
|
+
|
|
89
|
+
for name in used_names:
|
|
90
|
+
if name in func_globals:
|
|
91
|
+
obj = func_globals[name]
|
|
92
|
+
|
|
93
|
+
if isinstance(obj, ModuleType):
|
|
94
|
+
module_name = obj.__name__
|
|
95
|
+
if name == module_name.split('.')[-1]:
|
|
96
|
+
import_statements.add(f"import {module_name}")
|
|
97
|
+
else:
|
|
98
|
+
import_statements.add(f"import {module_name} as {name}")
|
|
99
|
+
|
|
100
|
+
elif hasattr(obj, '__module__') and obj.__module__ != '__main__':
|
|
101
|
+
try:
|
|
102
|
+
module_name = obj.__module__
|
|
103
|
+
obj_name = obj.__name__ if hasattr(obj, '__name__') else name
|
|
104
|
+
|
|
105
|
+
if module_name and module_name != 'builtins':
|
|
106
|
+
if obj_name == name:
|
|
107
|
+
import_statements.add(f"from {module_name} import {obj_name}")
|
|
108
|
+
else:
|
|
109
|
+
import_statements.add(f"from {module_name} import {obj_name} as {name}")
|
|
110
|
+
except AttributeError:
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
return "\n".join(sorted(import_statements))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _extract_globals(func: FunctionType) -> str:
|
|
117
|
+
"""Extract global variables needed by the function."""
|
|
118
|
+
global_assignments: list[str] = []
|
|
119
|
+
|
|
120
|
+
source = _get_function_source(func)
|
|
121
|
+
func_globals = func.__globals__
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
tree = ast.parse(source)
|
|
125
|
+
except SyntaxError as e:
|
|
126
|
+
raise ValueError(f"Failed to parse function source: {e}") from e
|
|
127
|
+
|
|
128
|
+
used_names: set[str] = set()
|
|
129
|
+
for node in ast.walk(tree):
|
|
130
|
+
if isinstance(node, ast.Name):
|
|
131
|
+
used_names.add(node.id)
|
|
132
|
+
elif isinstance(node, ast.Attribute):
|
|
133
|
+
if isinstance(node.value, ast.Name):
|
|
134
|
+
used_names.add(node.value.id)
|
|
135
|
+
|
|
136
|
+
for name in used_names:
|
|
137
|
+
if name in func_globals:
|
|
138
|
+
obj = func_globals[name]
|
|
139
|
+
|
|
140
|
+
if not isinstance(obj, (ModuleType, FunctionType, type)):
|
|
141
|
+
if not (hasattr(obj, '__module__') and hasattr(obj, '__name__')):
|
|
142
|
+
try:
|
|
143
|
+
repr_str = repr(obj)
|
|
144
|
+
if len(repr_str) < 1000 and '\n' not in repr_str:
|
|
145
|
+
global_assignments.append(f"{name} = {repr_str}")
|
|
146
|
+
except Exception:
|
|
147
|
+
global_assignments.append(f"# Warning: Could not serialize global variable '{name}'")
|
|
148
|
+
|
|
149
|
+
return "\n".join(global_assignments)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _generate_call_statement(func: FunctionType, args: tuple[Any, ...], kwargs: dict[str, Any]) -> str:
|
|
153
|
+
"""Generate a function call statement with the given arguments."""
|
|
154
|
+
if not args and not kwargs:
|
|
155
|
+
return f"{func.__name__}()"
|
|
156
|
+
|
|
157
|
+
arg_parts: list[str] = []
|
|
158
|
+
|
|
159
|
+
for arg in args:
|
|
160
|
+
arg_parts.append(repr(arg))
|
|
161
|
+
|
|
162
|
+
for key, value in kwargs.items():
|
|
163
|
+
arg_parts.append(f"{key}={repr(value)}")
|
|
164
|
+
|
|
165
|
+
args_str = ", ".join(arg_parts)
|
|
166
|
+
return f"{func.__name__}({args_str})"
|