machineconfig 7.49__py3-none-any.whl → 7.64__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of machineconfig might be problematic. Click here for more details.
- machineconfig/cluster/sessions_managers/utils/maker.py +21 -11
- machineconfig/jobs/installer/custom/boxes.py +2 -2
- machineconfig/jobs/installer/custom/hx.py +16 -12
- machineconfig/jobs/installer/custom_dev/brave.py +1 -1
- machineconfig/jobs/installer/custom_dev/cloudflare_warp_cli.py +23 -0
- machineconfig/jobs/installer/custom_dev/code.py +4 -1
- machineconfig/jobs/installer/custom_dev/dubdb_adbc.py +1 -1
- machineconfig/jobs/installer/custom_dev/nerfont_windows_helper.py +1 -10
- machineconfig/jobs/installer/custom_dev/sysabc.py +119 -0
- machineconfig/jobs/installer/custom_dev/wezterm.py +2 -19
- machineconfig/jobs/installer/installer_data.json +739 -25
- machineconfig/jobs/installer/linux_scripts/redis.sh +1 -0
- machineconfig/jobs/installer/package_groups.py +49 -83
- machineconfig/logger.py +0 -1
- machineconfig/profile/create_links_export.py +21 -7
- machineconfig/profile/mapper.toml +1 -4
- machineconfig/scripts/linux/wrap_mcfg +1 -1
- machineconfig/scripts/python/croshell.py +20 -43
- machineconfig/scripts/python/devops.py +3 -4
- machineconfig/scripts/python/env_manager/path_manager_tui.py +1 -1
- machineconfig/scripts/python/fire_jobs.py +53 -39
- machineconfig/scripts/python/ftpx.py +4 -2
- machineconfig/scripts/python/helpers_agents/agentic_frameworks/fire_qwen.py +0 -12
- machineconfig/scripts/python/helpers_croshell/crosh.py +3 -3
- machineconfig/scripts/python/helpers_devops/cli_config.py +3 -19
- machineconfig/scripts/python/helpers_devops/cli_config_dotfile.py +22 -13
- machineconfig/scripts/python/helpers_devops/cli_self.py +12 -6
- machineconfig/scripts/python/helpers_devops/cli_share_file.py +2 -2
- machineconfig/scripts/python/helpers_devops/cli_share_server.py +1 -1
- machineconfig/scripts/python/helpers_devops/cli_terminal.py +1 -1
- machineconfig/scripts/python/helpers_devops/cli_utils.py +1 -152
- machineconfig/scripts/python/helpers_devops/devops_backup_retrieve.py +4 -4
- machineconfig/scripts/python/helpers_fire_command/file_wrangler.py +2 -20
- machineconfig/scripts/python/helpers_fire_command/fire_jobs_route_helper.py +3 -4
- machineconfig/scripts/python/helpers_msearch/scripts_linux/fzfg +1 -1
- machineconfig/scripts/python/helpers_repos/clone.py +0 -1
- machineconfig/scripts/python/helpers_repos/cloud_repo_sync.py +9 -3
- machineconfig/scripts/python/helpers_repos/count_lines_frontend.py +1 -1
- machineconfig/scripts/python/helpers_repos/entrypoint.py +2 -1
- machineconfig/scripts/python/helpers_repos/record.py +2 -1
- machineconfig/scripts/python/helpers_sessions/sessions_multiprocess.py +7 -7
- machineconfig/scripts/python/helpers_utils/download.py +151 -0
- machineconfig/scripts/python/helpers_utils/path.py +106 -0
- machineconfig/scripts/python/interactive.py +17 -26
- machineconfig/scripts/python/nw/devops_add_ssh_key.py +21 -5
- machineconfig/scripts/python/nw/ssh_debug_linux.py +7 -7
- machineconfig/scripts/python/nw/ssh_debug_windows.py +4 -4
- machineconfig/scripts/python/nw/wsl_windows_transfer.py +3 -2
- machineconfig/scripts/python/sessions.py +37 -22
- machineconfig/scripts/python/utils.py +8 -3
- machineconfig/scripts/windows/mounts/mount_ssh.ps1 +1 -1
- machineconfig/settings/shells/nushell/init.nu +2 -2
- machineconfig/settings/shells/wezterm/wezterm.lua +2 -0
- machineconfig/settings/shells/zsh/init.sh +1 -8
- machineconfig/settings/yazi/init.lua +45 -24
- machineconfig/settings/yazi/keymap_windows.toml +1 -2
- machineconfig/settings/yazi/shell/yazi_cd.ps1 +29 -5
- machineconfig/setup_linux/__init__.py +0 -1
- machineconfig/setup_linux/web_shortcuts/interactive.sh +12 -10
- machineconfig/setup_mac/__init__.py +2 -3
- machineconfig/setup_windows/__init__.py +0 -3
- machineconfig/setup_windows/web_shortcuts/interactive.ps1 +12 -10
- machineconfig/setup_windows/web_shortcuts/quick_init.ps1 +16 -0
- machineconfig/utils/code.py +2 -2
- machineconfig/utils/files/headers.py +2 -2
- machineconfig/utils/installer_utils/installer_class.py +42 -40
- machineconfig/utils/installer_utils/{installer.py → installer_cli.py} +61 -101
- machineconfig/utils/installer_utils/{installer_abc.py → installer_locator_utils.py} +0 -68
- machineconfig/utils/{installer.py → installer_utils/installer_runner.py} +11 -51
- machineconfig/utils/io.py +0 -1
- machineconfig/utils/meta.py +29 -15
- machineconfig/utils/options.py +1 -1
- machineconfig/utils/path_extended.py +40 -19
- machineconfig/utils/path_helper.py +75 -21
- machineconfig/utils/schemas/layouts/layout_types.py +1 -1
- machineconfig/utils/ssh.py +3 -3
- machineconfig-7.64.dist-info/METADATA +124 -0
- {machineconfig-7.49.dist-info → machineconfig-7.64.dist-info}/RECORD +84 -87
- machineconfig/jobs/installer/linux_scripts/pgsql.sh +0 -41
- machineconfig/jobs/installer/linux_scripts/timescaledb.sh +0 -71
- machineconfig/jobs/installer/powershell_scripts/archive_pygraphviz.ps1 +0 -12
- machineconfig/scripts/python/nw/add_ssh_key.py +0 -148
- machineconfig/settings/lf/linux/exe/fzf_nano.sh +0 -16
- machineconfig/setup_linux/apps.sh +0 -66
- machineconfig/setup_mac/apps.sh +0 -73
- machineconfig/setup_windows/apps.ps1 +0 -62
- machineconfig-7.49.dist-info/METADATA +0 -92
- /machineconfig/jobs/installer/linux_scripts/{warp-cli.sh → cloudflare_warp_cli.sh} +0 -0
- /machineconfig/{jobs/installer/powershell_scripts → setup_windows/ssh}/openssh-server_add_key.ps1 +0 -0
- /machineconfig/{jobs/installer/powershell_scripts → setup_windows/ssh}/openssh-server_copy-ssh-id.ps1 +0 -0
- {machineconfig-7.49.dist-info → machineconfig-7.64.dist-info}/WHEEL +0 -0
- {machineconfig-7.49.dist-info → machineconfig-7.64.dist-info}/entry_points.txt +0 -0
- {machineconfig-7.49.dist-info → machineconfig-7.64.dist-info}/top_level.txt +0 -0
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
# import subprocess
|
|
4
4
|
from machineconfig.utils.io import read_ini
|
|
5
|
-
from machineconfig.utils.path_extended import PathExtended
|
|
6
5
|
from machineconfig.utils.source_of_truth import LIBRARY_ROOT, DEFAULTS_PATH
|
|
7
6
|
from machineconfig.utils.code import print_code
|
|
8
7
|
from machineconfig.utils.options import choose_cloud_interactively, choose_from_options
|
|
@@ -11,6 +10,7 @@ from platform import system
|
|
|
11
10
|
from typing import Any, Literal, Optional
|
|
12
11
|
from rich.console import Console
|
|
13
12
|
from rich.panel import Panel
|
|
13
|
+
from pathlib import Path
|
|
14
14
|
import tomllib
|
|
15
15
|
|
|
16
16
|
|
|
@@ -56,13 +56,13 @@ def main_backup_retrieve(direction: OPTIONS, which: Optional[str], cloud: Option
|
|
|
56
56
|
flags += "e" if item["encrypt"] == "True" else ""
|
|
57
57
|
flags += "r" if item["rel2home"] == "True" else ""
|
|
58
58
|
flags += "o" if system().lower() in item_name else ""
|
|
59
|
-
console.print(Panel(f"📦 PROCESSING: {item_name}\n📂 Path: {
|
|
59
|
+
console.print(Panel(f"📦 PROCESSING: {item_name}\n📂 Path: {Path(item['path']).as_posix()}\n🏳️ Flags: {flags or 'None'}", title=f"[bold blue]Processing Item: {item_name}[/bold blue]", border_style="blue"))
|
|
60
60
|
if flags:
|
|
61
61
|
flags = "-" + flags
|
|
62
62
|
if direction == "BACKUP":
|
|
63
|
-
program += f"""\ncloud_copy "{
|
|
63
|
+
program += f"""\ncloud_copy "{Path(item["path"]).as_posix()}" $cloud {flags}\n"""
|
|
64
64
|
elif direction == "RETRIEVE":
|
|
65
|
-
program += f"""\ncloud_copy $cloud "{
|
|
65
|
+
program += f"""\ncloud_copy $cloud "{Path(item["path"]).as_posix()}" {flags}\n"""
|
|
66
66
|
else:
|
|
67
67
|
console.print(Panel('❌ ERROR: INVALID DIRECTION\n⚠️ Direction must be either "BACKUP" or "RETRIEVE"', title="[bold red]Error: Invalid Direction[/bold red]", border_style="red"))
|
|
68
68
|
raise RuntimeError(f"Unknown direction: {direction}")
|
|
@@ -1,26 +1,9 @@
|
|
|
1
1
|
from typing import Optional
|
|
2
2
|
import os
|
|
3
|
-
from
|
|
3
|
+
from pathlib import Path
|
|
4
4
|
import platform
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
def search_for_files_of_interest(path_obj: PathExtended):
|
|
8
|
-
if path_obj.joinpath(".venv").exists():
|
|
9
|
-
path_objects = path_obj.search("*", not_in=[".venv"])
|
|
10
|
-
files: list[PathExtended] = []
|
|
11
|
-
for a_path_obj in path_objects:
|
|
12
|
-
files += search_for_files_of_interest(path_obj=a_path_obj)
|
|
13
|
-
return files
|
|
14
|
-
if path_obj.is_file():
|
|
15
|
-
return [path_obj]
|
|
16
|
-
py_files = path_obj.search(pattern="*.py", not_in=["__init__.py"], r=True)
|
|
17
|
-
ps_files = path_obj.search(pattern="*.ps1", r=True)
|
|
18
|
-
sh_files = path_obj.search(pattern="*.sh", r=True)
|
|
19
|
-
files = py_files + ps_files + sh_files
|
|
20
|
-
return files
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
7
|
def parse_pyfile(file_path: str):
|
|
25
8
|
print(f"🔍 Loading {file_path} ...")
|
|
26
9
|
from typing import NamedTuple
|
|
@@ -29,7 +12,7 @@ def parse_pyfile(file_path: str):
|
|
|
29
12
|
func_args: list[list[args_spec]] = [[]] # this firt prepopulated dict is for the option 'RUN AS MAIN' which has no args
|
|
30
13
|
import ast
|
|
31
14
|
|
|
32
|
-
parsed_ast = ast.parse(
|
|
15
|
+
parsed_ast = ast.parse(Path(file_path).read_text(encoding="utf-8"))
|
|
33
16
|
functions = [node for node in ast.walk(parsed_ast) if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))]
|
|
34
17
|
module__doc__ = ast.get_docstring(parsed_ast)
|
|
35
18
|
main_option = f"RUN AS MAIN -- {module__doc__ if module__doc__ is not None else 'NoDocs'}"
|
|
@@ -119,7 +102,6 @@ def wrap_import_in_try_except(import_line: str, pyfile: str, repo_root: Optional
|
|
|
119
102
|
print(fr"❌ Failed to import `{pyfile}` as a module: {ex} ")
|
|
120
103
|
print("⚠️ Attempting import with ad-hoc `$PATH` manipulation. DO NOT pickle any objects in this session as correct deserialization cannot be guaranteed.")
|
|
121
104
|
import sys
|
|
122
|
-
from pathlib import Path
|
|
123
105
|
sys.path.append(str(Path(pyfile).parent))
|
|
124
106
|
if repo_root is not None:
|
|
125
107
|
sys.path.append(repo_root)
|
|
@@ -5,11 +5,10 @@ from typing import Optional
|
|
|
5
5
|
import tomllib
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from machineconfig.utils.accessories import randstr
|
|
8
|
-
from machineconfig.utils.path_extended import PathExtended
|
|
9
8
|
from machineconfig.utils.options import choose_from_options
|
|
10
9
|
|
|
11
10
|
|
|
12
|
-
def choose_function_or_lines(choice_file:
|
|
11
|
+
def choose_function_or_lines(choice_file: Path, kwargs_dict: dict[str, object]) -> tuple[Optional[str], Path, dict[str, object]]:
|
|
13
12
|
"""
|
|
14
13
|
Choose a function to run from a Python file or lines from a shell script.
|
|
15
14
|
|
|
@@ -46,7 +45,7 @@ def choose_function_or_lines(choice_file: PathExtended, kwargs_dict: dict[str, o
|
|
|
46
45
|
continue
|
|
47
46
|
options.append(line)
|
|
48
47
|
chosen_lines = choose_from_options(msg="Choose a line to run", options=options, fzf=True, multi=True)
|
|
49
|
-
choice_file =
|
|
48
|
+
choice_file = Path.home().joinpath(f"tmp_results/tmp_scripts/shell/{randstr(10)}.sh")
|
|
50
49
|
choice_file.parent.mkdir(parents=True, exist_ok=True)
|
|
51
50
|
choice_file.write_text("\n".join(chosen_lines), encoding="utf-8")
|
|
52
51
|
choice_function = None
|
|
@@ -82,7 +81,7 @@ def get_command_streamlit(choice_file: Path, environment: str, repo_root: Option
|
|
|
82
81
|
port = config["server"]["port"]
|
|
83
82
|
secrets_path = toml_path.with_name("secrets.toml")
|
|
84
83
|
if repo_root is not None:
|
|
85
|
-
secrets_template_path = Path.home().joinpath(f"dotfiles/creds/streamlit/{
|
|
84
|
+
secrets_template_path = Path.home().joinpath(f"dotfiles/creds/streamlit/{Path(repo_root).name}/{choice_file.name}/secrets.toml")
|
|
86
85
|
if environment != "" and not secrets_path.exists() and secrets_template_path.exists():
|
|
87
86
|
secrets_template = tomllib.loads(secrets_template_path.read_text(encoding="utf-8"))
|
|
88
87
|
if environment == "ip":
|
|
@@ -19,5 +19,5 @@ IFS=: read -ra selected < <(
|
|
|
19
19
|
--preview 'bat --color=always {1} --highlight-line {2}' \
|
|
20
20
|
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3'
|
|
21
21
|
)
|
|
22
|
-
[ -n "${selected[0]}" ] && hx "${selected[0]}
|
|
22
|
+
[ -n "${selected[0]}" ] && hx "${selected[0]}:${selected[1]}:${selected[2]}"
|
|
23
23
|
"
|
|
@@ -54,10 +54,16 @@ def main(
|
|
|
54
54
|
else:
|
|
55
55
|
cloud_resolved = cloud
|
|
56
56
|
repo_local_root = PathExtended.cwd() if repo is None else PathExtended(repo).expanduser().absolute()
|
|
57
|
-
|
|
57
|
+
try:
|
|
58
|
+
repo_local_obj = git.Repo(repo_local_root, search_parent_directories=True)
|
|
59
|
+
except git.InvalidGitRepositoryError:
|
|
60
|
+
typer.echo(f"[red]Error:[/] The specified path '{repo_local_root}' is not a valid git repository.")
|
|
61
|
+
typer.Exit(code=1)
|
|
62
|
+
return ""
|
|
58
63
|
repo_local_root = PathExtended(repo_local_obj.working_dir) # cwd might have been in a sub directory of repo_root, so its better to redefine it.
|
|
64
|
+
local_relative_home = PathExtended(repo_local_root.expanduser().absolute().relative_to(Path.home()))
|
|
59
65
|
PathExtended(CONFIG_ROOT).joinpath("remote").mkdir(parents=True, exist_ok=True)
|
|
60
|
-
repo_remote_root = PathExtended(CONFIG_ROOT).joinpath("remote",
|
|
66
|
+
repo_remote_root = PathExtended(CONFIG_ROOT).joinpath("remote", local_relative_home)
|
|
61
67
|
repo_remote_root.delete(sure=True)
|
|
62
68
|
try:
|
|
63
69
|
console.print(Panel("📥 DOWNLOADING REMOTE REPOSITORY", title_align="left", border_style="blue"))
|
|
@@ -99,7 +105,7 @@ git pull originEnc master
|
|
|
99
105
|
uv_project_dir = f"""{str(Path.home().joinpath("code/machineconfig"))}"""
|
|
100
106
|
uv_with = None
|
|
101
107
|
else:
|
|
102
|
-
uv_with = ["machineconfig>=7.
|
|
108
|
+
uv_with = ["machineconfig>=7.64"]
|
|
103
109
|
uv_project_dir = None
|
|
104
110
|
|
|
105
111
|
import tempfile
|
|
@@ -8,7 +8,7 @@ def analyze_repo_development(repo_path: Annotated[str, typer.Argument(..., help=
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
count_lines_path = Path(count_lines.__file__)
|
|
10
10
|
# --project $HOME/code/ machineconfig --group plot
|
|
11
|
-
cmd = f"""uv run --python 3.14 --with "machineconfig[plot]>=7.
|
|
11
|
+
cmd = f"""uv run --python 3.14 --with "machineconfig[plot]>=7.64" {count_lines_path} analyze-over-time {repo_path}"""
|
|
12
12
|
from machineconfig.utils.code import run_shell_script
|
|
13
13
|
run_shell_script(cmd)
|
|
14
14
|
|
|
@@ -38,7 +38,8 @@ def resolve_spec_path(directory: Optional[str], cloud: Optional[str]) -> Path:
|
|
|
38
38
|
repos_root = resolve_directory(directory)
|
|
39
39
|
from machineconfig.utils.path_extended import PathExtended
|
|
40
40
|
if not repos_root.exists() or repos_root.name != "repos.json":
|
|
41
|
-
|
|
41
|
+
relative_repos_root = PathExtended(repos_root).expanduser().absolute().relative_to(Path.home())
|
|
42
|
+
candidate = Path(CONFIG_ROOT).joinpath("repos").joinpath(relative_repos_root).joinpath("repos.json")
|
|
42
43
|
repos_root = candidate
|
|
43
44
|
if not repos_root.exists():
|
|
44
45
|
cloud_name: Optional[str]
|
|
@@ -242,7 +242,8 @@ def main_record(repos_root: Path):
|
|
|
242
242
|
tree_structure = build_tree_structure(repo_records, repos_root)
|
|
243
243
|
print(tree_structure)
|
|
244
244
|
|
|
245
|
-
|
|
245
|
+
relative_repos_root = PathExtended(repos_root).expanduser().absolute().relative_to(Path.home())
|
|
246
|
+
save_path = CONFIG_ROOT.joinpath("repos").joinpath(relative_repos_root).joinpath("repos.json")
|
|
246
247
|
save_json(obj=res, path=save_path, indent=4)
|
|
247
248
|
pprint(f"📁 Result saved at {PathExtended(save_path)}")
|
|
248
249
|
print(">>>>>>>>> Finished Recording")
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
3
|
from typing import Optional, Annotated
|
|
4
|
-
from pathlib import Path
|
|
5
4
|
import typer
|
|
6
5
|
|
|
7
6
|
|
|
@@ -13,20 +12,21 @@ def create_from_function(
|
|
|
13
12
|
from machineconfig.utils.ve import get_ve_activate_line, get_ve_path_and_ipython_profile
|
|
14
13
|
from machineconfig.utils.options import choose_from_options
|
|
15
14
|
from machineconfig.utils.path_helper import match_file_name, sanitize_path
|
|
16
|
-
from machineconfig.utils.path_extended import PathExtended
|
|
17
15
|
from machineconfig.utils.accessories import get_repo_root
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
18
|
|
|
19
19
|
path_obj = sanitize_path(path)
|
|
20
20
|
if not path_obj.exists():
|
|
21
21
|
suffixes = {".py"}
|
|
22
|
-
choice_file = match_file_name(sub_string=path, search_root=
|
|
22
|
+
choice_file = match_file_name(sub_string=path, search_root=Path.cwd(), suffixes=suffixes)
|
|
23
23
|
elif path_obj.is_dir():
|
|
24
|
-
from machineconfig.
|
|
24
|
+
from machineconfig.utils.path_helper import search_for_files_of_interest
|
|
25
25
|
print(f"🔍 Searching recursively for Python, PowerShell and Shell scripts in directory `{path_obj}`")
|
|
26
|
-
files = search_for_files_of_interest(path_obj)
|
|
26
|
+
files = search_for_files_of_interest(path_obj, suffixes={".py", ".sh", ".ps1"})
|
|
27
27
|
print(f"🔍 Got #{len(files)} results.")
|
|
28
28
|
choice_file = choose_from_options(multi=False, options=files, fzf=True, msg="Choose one option")
|
|
29
|
-
choice_file =
|
|
29
|
+
choice_file = Path(choice_file)
|
|
30
30
|
else:
|
|
31
31
|
choice_file = path_obj
|
|
32
32
|
|
|
@@ -52,7 +52,7 @@ def create_from_function(
|
|
|
52
52
|
from machineconfig.utils.schemas.layouts.layout_types import LayoutConfig
|
|
53
53
|
layout: LayoutConfig = {"layoutName": "fireNprocess", "layoutTabs": []}
|
|
54
54
|
for an_arg in range(num_process):
|
|
55
|
-
layout["layoutTabs"].append({"tabName": f"tab{an_arg}", "startDir": str(
|
|
55
|
+
layout["layoutTabs"].append({"tabName": f"tab{an_arg}", "startDir": str(Path.cwd()), "command": f"uv run -m fire {choice_file} {choice_function} --idx={an_arg} --idx_max={num_process}"})
|
|
56
56
|
print(layout)
|
|
57
57
|
run_zellij_layout(layout_config=layout)
|
|
58
58
|
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
from typing import Annotated, Optional
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def download(
|
|
8
|
+
url: Annotated[Optional[str], typer.Argument(..., help="The URL to download the file from.")] = None,
|
|
9
|
+
decompress: Annotated[bool, typer.Option(..., "--decompress", "-d", help="Decompress the file if it's an archive.")] = False,
|
|
10
|
+
output: Annotated[Optional[str], typer.Option("--output", "-o", help="The output file path.")] = None,
|
|
11
|
+
output_dir: Annotated[Optional[str], typer.Option("--output-dir", help="Directory to place the downloaded file in.")] = None,
|
|
12
|
+
) -> Optional["Path"]:
|
|
13
|
+
|
|
14
|
+
import subprocess
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from urllib.parse import parse_qs, unquote, urlparse
|
|
17
|
+
from requests import Response
|
|
18
|
+
import requests
|
|
19
|
+
|
|
20
|
+
if url is None:
|
|
21
|
+
typer.echo("❌ Error: URL is required.", err=True)
|
|
22
|
+
return None
|
|
23
|
+
if output is not None and output_dir is not None:
|
|
24
|
+
typer.echo("❌ Error: --output and --output-dir cannot be used together.", err=True)
|
|
25
|
+
return None
|
|
26
|
+
typer.echo(f"📥 Downloading from: {url}")
|
|
27
|
+
|
|
28
|
+
def _sanitize_candidate_filename(name: str) -> Optional[str]:
|
|
29
|
+
candidate = Path(name).name.strip()
|
|
30
|
+
if not candidate or candidate in {".", ".."}:
|
|
31
|
+
return None
|
|
32
|
+
return candidate
|
|
33
|
+
|
|
34
|
+
def _filename_from_content_disposition(header_value: Optional[str]) -> Optional[str]:
|
|
35
|
+
if header_value is None:
|
|
36
|
+
return None
|
|
37
|
+
parts = [segment.strip() for segment in header_value.split(";")]
|
|
38
|
+
for part in parts:
|
|
39
|
+
lower = part.lower()
|
|
40
|
+
if lower.startswith("filename*="):
|
|
41
|
+
value = part.split("=", 1)[1]
|
|
42
|
+
value = value.strip().strip('"')
|
|
43
|
+
if "''" in value:
|
|
44
|
+
value = value.split("''", 1)[1]
|
|
45
|
+
decoded = unquote(value)
|
|
46
|
+
sanitized = _sanitize_candidate_filename(decoded)
|
|
47
|
+
if sanitized is not None:
|
|
48
|
+
return sanitized
|
|
49
|
+
if lower.startswith("filename="):
|
|
50
|
+
value = part.split("=", 1)[1].strip().strip('"')
|
|
51
|
+
decoded = unquote(value)
|
|
52
|
+
sanitized = _sanitize_candidate_filename(decoded)
|
|
53
|
+
if sanitized is not None:
|
|
54
|
+
return sanitized
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
def _filename_from_url(source_url: str) -> Optional[str]:
|
|
58
|
+
parsed = urlparse(source_url)
|
|
59
|
+
url_candidate = _sanitize_candidate_filename(unquote(Path(parsed.path).name))
|
|
60
|
+
if url_candidate is not None:
|
|
61
|
+
return url_candidate
|
|
62
|
+
query_params = parse_qs(parsed.query, keep_blank_values=True)
|
|
63
|
+
for key, values in query_params.items():
|
|
64
|
+
lower_key = key.lower()
|
|
65
|
+
if "name" in lower_key or "file" in lower_key:
|
|
66
|
+
for value in values:
|
|
67
|
+
sanitized = _sanitize_candidate_filename(unquote(value))
|
|
68
|
+
if sanitized is not None:
|
|
69
|
+
return sanitized
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
def _resolve_download_path(request_url: str, response: Response, requested_output: Optional[str], requested_output_dir: Optional[str]) -> Path:
|
|
73
|
+
if requested_output is not None:
|
|
74
|
+
return Path(requested_output)
|
|
75
|
+
header_candidate = _filename_from_content_disposition(response.headers.get("content-disposition"))
|
|
76
|
+
if header_candidate is None:
|
|
77
|
+
header_candidate = _filename_from_url(response.url)
|
|
78
|
+
if header_candidate is None:
|
|
79
|
+
header_candidate = _filename_from_url(request_url)
|
|
80
|
+
if header_candidate is None:
|
|
81
|
+
header_candidate = "downloaded_file"
|
|
82
|
+
if requested_output_dir is not None:
|
|
83
|
+
return Path(requested_output_dir) / header_candidate
|
|
84
|
+
return Path(header_candidate)
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
with requests.get(url, allow_redirects=True, stream=True, timeout=60) as response:
|
|
88
|
+
response.raise_for_status()
|
|
89
|
+
download_path = _resolve_download_path(url, response, output, output_dir)
|
|
90
|
+
download_path.parent.mkdir(parents=True, exist_ok=True)
|
|
91
|
+
total_size_header = response.headers.get("content-length", "0")
|
|
92
|
+
try:
|
|
93
|
+
total_size = int(total_size_header)
|
|
94
|
+
except (TypeError, ValueError):
|
|
95
|
+
total_size = 0
|
|
96
|
+
if total_size <= 0:
|
|
97
|
+
with open(download_path, "wb") as file_handle:
|
|
98
|
+
file_handle.write(response.content)
|
|
99
|
+
else:
|
|
100
|
+
downloaded = 0
|
|
101
|
+
chunk_size = 8192 * 4
|
|
102
|
+
with open(download_path, "wb") as file_handle:
|
|
103
|
+
for chunk in response.iter_content(chunk_size=chunk_size):
|
|
104
|
+
if not chunk:
|
|
105
|
+
continue
|
|
106
|
+
file_handle.write(chunk)
|
|
107
|
+
downloaded += len(chunk)
|
|
108
|
+
progress = (downloaded / total_size) * 100
|
|
109
|
+
typer.echo(f"\r⏬ Progress: {progress:.1f}% ({downloaded}/{total_size} bytes)", nl=False)
|
|
110
|
+
typer.echo()
|
|
111
|
+
except requests.exceptions.RequestException as exception:
|
|
112
|
+
typer.echo(f"❌ Download failed: {exception}", err=True)
|
|
113
|
+
return None
|
|
114
|
+
except OSError as exception:
|
|
115
|
+
typer.echo(f"❌ File write error: {exception}", err=True)
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
typer.echo(f"✅ Downloaded to: {download_path}")
|
|
119
|
+
result_path: Path = download_path
|
|
120
|
+
|
|
121
|
+
if decompress:
|
|
122
|
+
typer.echo(f"📦 Decompressing: {download_path}")
|
|
123
|
+
base_name = download_path.stem
|
|
124
|
+
if base_name in {"", ".", ".."}:
|
|
125
|
+
base_name = "extracted"
|
|
126
|
+
extract_dir = download_path.parent / base_name
|
|
127
|
+
extract_dir.mkdir(parents=True, exist_ok=True)
|
|
128
|
+
try:
|
|
129
|
+
subprocess.run(
|
|
130
|
+
["ouch", "decompress", str(download_path), "--dir", str(extract_dir)],
|
|
131
|
+
check=True,
|
|
132
|
+
capture_output=True,
|
|
133
|
+
text=True,
|
|
134
|
+
)
|
|
135
|
+
typer.echo(f"✅ Decompressed to: {extract_dir}")
|
|
136
|
+
if download_path.exists():
|
|
137
|
+
download_path.unlink()
|
|
138
|
+
typer.echo(f"🗑️ Removed archive: {download_path}")
|
|
139
|
+
result_path = extract_dir
|
|
140
|
+
except subprocess.CalledProcessError as exception:
|
|
141
|
+
typer.echo(f"❌ Decompression failed: {exception.stderr}", err=True)
|
|
142
|
+
return None
|
|
143
|
+
except FileNotFoundError:
|
|
144
|
+
typer.echo("❌ Error: ouch command not found. Please install ouch.", err=True)
|
|
145
|
+
typer.echo("💡 Install with: cargo install ouch", err=True)
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
return result_path.resolve()
|
|
149
|
+
|
|
150
|
+
if __name__ == "__main__":
|
|
151
|
+
from pathlib import Path
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
from typing import Optional
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Annotated, Literal, TypedDict
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def path():
|
|
12
|
+
"""📚 NAVIGATE PATH variable with TUI"""
|
|
13
|
+
from machineconfig.scripts.python import env_manager as navigator
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
path = Path(navigator.__file__).resolve().parent.joinpath("path_manager_tui.py")
|
|
16
|
+
from machineconfig.utils.code import run_shell_script, get_uv_command_executing_python_script
|
|
17
|
+
uv_with = ["textual"]
|
|
18
|
+
uv_project_dir = None
|
|
19
|
+
if not Path.home().joinpath("code/machineconfig").exists():
|
|
20
|
+
uv_with.append("machineconfig>=7.64")
|
|
21
|
+
else:
|
|
22
|
+
uv_project_dir = str(Path.home().joinpath("code/machineconfig"))
|
|
23
|
+
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])
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def init_project(python: Annotated[Literal["3.13", "3.14"], typer.Option("--python", "-p", help="Python version for the uv virtual environment.")]= "3.13") -> None:
|
|
27
|
+
_ = python
|
|
28
|
+
repo_root = Path.cwd()
|
|
29
|
+
if not (repo_root / "pyproject.toml").exists():
|
|
30
|
+
typer.echo("❌ Error: pyproject.toml not found.", err=True)
|
|
31
|
+
raise typer.Exit(code=1)
|
|
32
|
+
print("Adding group `plot` with common data science and plotting packages...")
|
|
33
|
+
script = """
|
|
34
|
+
uv add --group plot \
|
|
35
|
+
# Data & computation
|
|
36
|
+
numpy pandas polars duckdb-engine python-magic \
|
|
37
|
+
# Plotting / visualization
|
|
38
|
+
matplotlib plotly kaleido \
|
|
39
|
+
# Notebooks / interactive
|
|
40
|
+
ipython ipykernel jupyterlab nbformat marimo \
|
|
41
|
+
# Code analysis / type checking / linting
|
|
42
|
+
mypy pyright ruff pylint pyrefly \
|
|
43
|
+
# Packaging / build / dev
|
|
44
|
+
cleanpy \
|
|
45
|
+
# CLI / debugging / utilities
|
|
46
|
+
ipdb pudb \
|
|
47
|
+
# Type hints for packages
|
|
48
|
+
types-python-dateutil types-pyyaml types-requests types-tqdm \
|
|
49
|
+
types-mysqlclient types-paramiko types-pytz types-sqlalchemy types-toml types-urllib3 \
|
|
50
|
+
|
|
51
|
+
"""
|
|
52
|
+
from machineconfig.utils.code import run_shell_script
|
|
53
|
+
run_shell_script(script)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def edit_file_with_hx(path: Annotated[Optional[str], typer.Argument(..., help="The root directory of the project to edit, or a file path.")] = None) -> None:
|
|
57
|
+
if path is None:
|
|
58
|
+
root_path = Path.cwd()
|
|
59
|
+
print(f"No path provided. Using current working directory: {root_path}")
|
|
60
|
+
else:
|
|
61
|
+
root_path = Path(path).expanduser().resolve()
|
|
62
|
+
print(f"Using provided path: {root_path}")
|
|
63
|
+
from machineconfig.utils.accessories import get_repo_root
|
|
64
|
+
repo_root = get_repo_root(root_path)
|
|
65
|
+
if repo_root is not None and repo_root.joinpath("pyproject.toml").exists():
|
|
66
|
+
code = f"""
|
|
67
|
+
cd {repo_root}
|
|
68
|
+
uv add --dev pylsp-mypy python-lsp-server[all] pyright ruff-lsp # for helix editor.
|
|
69
|
+
source ./.venv/bin/activate
|
|
70
|
+
"""
|
|
71
|
+
else:
|
|
72
|
+
code = ""
|
|
73
|
+
if root_path.is_file():
|
|
74
|
+
code += f"hx {root_path}"
|
|
75
|
+
else:
|
|
76
|
+
code += "hx"
|
|
77
|
+
from machineconfig.utils.code import exit_then_run_shell_script
|
|
78
|
+
exit_then_run_shell_script(code)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class MachineSpecs(TypedDict):
|
|
82
|
+
system: str
|
|
83
|
+
distro: str
|
|
84
|
+
home_dir: str
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def get_machine_specs() -> MachineSpecs:
|
|
88
|
+
"""Write print and return the local machine specs."""
|
|
89
|
+
import platform
|
|
90
|
+
UV_RUN_CMD = "$HOME/.local/bin/uv run" if platform.system() != "Windows" else """& "$env:USERPROFILE/.local/bin/uv" run"""
|
|
91
|
+
command = f"""{UV_RUN_CMD} --with distro python -c "import distro; print(distro.name(pretty=True))" """
|
|
92
|
+
import subprocess
|
|
93
|
+
distro = subprocess.run(command, shell=True, capture_output=True, text=True).stdout.strip()
|
|
94
|
+
specs: MachineSpecs = {
|
|
95
|
+
"system": platform.system(),
|
|
96
|
+
"distro": distro,
|
|
97
|
+
"home_dir": str(Path.home()),
|
|
98
|
+
}
|
|
99
|
+
print(specs)
|
|
100
|
+
from machineconfig.utils.source_of_truth import CONFIG_ROOT
|
|
101
|
+
path = CONFIG_ROOT.joinpath("machine_specs.json")
|
|
102
|
+
CONFIG_ROOT.mkdir(parents=True, exist_ok=True)
|
|
103
|
+
import json
|
|
104
|
+
path.write_text(json.dumps(specs, indent=4), encoding="utf-8")
|
|
105
|
+
return specs
|
|
106
|
+
|
|
@@ -19,9 +19,7 @@ for better user experience with checkbox selections.
|
|
|
19
19
|
|
|
20
20
|
import sys
|
|
21
21
|
from pathlib import Path
|
|
22
|
-
# from typing import cast
|
|
23
22
|
import platform
|
|
24
|
-
|
|
25
23
|
import questionary
|
|
26
24
|
from questionary import Choice
|
|
27
25
|
from rich.console import Console
|
|
@@ -29,12 +27,11 @@ from rich.panel import Panel
|
|
|
29
27
|
from rich.text import Text
|
|
30
28
|
from machineconfig.utils.code import run_shell_script
|
|
31
29
|
|
|
32
|
-
# _ = cast
|
|
33
30
|
console = Console()
|
|
34
31
|
|
|
35
32
|
|
|
36
33
|
def display_header() -> None:
|
|
37
|
-
from machineconfig.utils.
|
|
34
|
+
from machineconfig.utils.installer_utils.installer_runner import get_machineconfig_version
|
|
38
35
|
from rich.align import Align
|
|
39
36
|
|
|
40
37
|
# Fancy ASCII art header
|
|
@@ -96,35 +93,29 @@ def display_dotfiles_instructions() -> None:
|
|
|
96
93
|
def get_installation_choices() -> list[str]:
|
|
97
94
|
"""Get user choices for installation options."""
|
|
98
95
|
choices = [
|
|
99
|
-
Choice(value="install_machineconfig", title="🐍 Install machineconfig.", checked=False),
|
|
100
|
-
Choice(value="
|
|
101
|
-
Choice(value="
|
|
102
|
-
Choice(value="
|
|
103
|
-
Choice(value="
|
|
104
|
-
Choice(value="
|
|
105
|
-
Choice(value="
|
|
106
|
-
Choice(value="retrieve_repositories", title="📚 Retrieve Repositories", checked=False),
|
|
107
|
-
Choice(value="retrieve_data", title="💾 Retrieve Data.", checked=False),
|
|
96
|
+
Choice(value="install_machineconfig", title="🐍 Install machineconfig cli.", checked=False),
|
|
97
|
+
Choice(value="sysabc", title="📥 Install System Package Manager (Needed for other apps to be installed).", checked=False),
|
|
98
|
+
Choice(value="termabc", title="⚡ Install Terminal CLI apps essentials (group `termabc`)", checked=False),
|
|
99
|
+
Choice(value="install_shell_profile", title="🐚 Configure Shell Profile And Map Other Configs.", checked=False),
|
|
100
|
+
Choice(value="install_ssh_server", title="🔒 [ADVANCED] Configure SSH Server", checked=False),
|
|
101
|
+
Choice(value="retrieve_repositories", title="📚 [ADVANCED] Retrieve Repositories", checked=False),
|
|
102
|
+
Choice(value="retrieve_data", title="💾 [ADVANCED] Retrieve Data.", checked=False),
|
|
108
103
|
]
|
|
109
|
-
# Add Windows-specific options
|
|
110
|
-
if platform.system() == "Windows":
|
|
111
|
-
choices.append(Choice(value="install_windows_desktop", title="💻 Install Windows Desktop Apps - Install nerd fonts and set WT config.", checked=False))
|
|
112
104
|
selected = questionary.checkbox("Select the installation options you want to execute:", choices=choices, show_description=True).ask()
|
|
113
105
|
return selected or []
|
|
114
106
|
|
|
115
107
|
|
|
116
108
|
def execute_installations(selected_options: list[str]) -> None:
|
|
117
109
|
for maybe_a_group in selected_options:
|
|
118
|
-
if maybe_a_group in ("
|
|
110
|
+
if maybe_a_group in ("termabc", "sysabc"):
|
|
119
111
|
console.print(Panel("⚡ [bold bright_yellow]CLI APPLICATIONS[/bold bright_yellow]\n[italic]Command-line tools installation[/italic]", border_style="bright_yellow"))
|
|
120
112
|
console.print("🔧 Installing CLI applications", style="bold cyan")
|
|
121
113
|
try:
|
|
122
|
-
from machineconfig.utils.installer_utils.
|
|
114
|
+
from machineconfig.utils.installer_utils.installer_cli import main as devops_devapps_install_main
|
|
123
115
|
devops_devapps_install_main(group=True, which=maybe_a_group, interactive=False)
|
|
124
116
|
console.print("✅ CLI applications installed successfully", style="bold green")
|
|
125
117
|
except Exception as e:
|
|
126
118
|
console.print(f"❌ Error installing CLI applications: {e}", style="bold red")
|
|
127
|
-
import platform
|
|
128
119
|
if platform.system() != "Windows":
|
|
129
120
|
run_shell_script(". $HOME/.bashrc")
|
|
130
121
|
|
|
@@ -135,7 +126,6 @@ def execute_installations(selected_options: list[str]) -> None:
|
|
|
135
126
|
|
|
136
127
|
if "install_ssh_server" in selected_options:
|
|
137
128
|
console.print(Panel("🔒 [bold red]SSH SERVER[/bold red]\n[italic]Remote access setup[/italic]", border_style="red"))
|
|
138
|
-
import platform
|
|
139
129
|
if platform.system() == "Windows":
|
|
140
130
|
powershell_script = """Write-Host "🔧 Installing and configuring SSH server..."
|
|
141
131
|
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
|
|
@@ -152,6 +142,13 @@ Set-Service -Name sshd -StartupType 'Automatic'"""
|
|
|
152
142
|
from machineconfig.profile.create_shell_profile import create_default_shell_profile
|
|
153
143
|
create_default_shell_profile()
|
|
154
144
|
console.print("✅ Shell profile configured successfully", style="bold green")
|
|
145
|
+
from machineconfig.profile.create_links_export import main_public_from_parser
|
|
146
|
+
main_public_from_parser(method="copy", on_conflict="overwrite-default-path", which="all", interactive=False)
|
|
147
|
+
if platform.system() == "Windows":
|
|
148
|
+
from machineconfig.jobs.installer.custom_dev.nerfont_windows_helper import install_nerd_fonts
|
|
149
|
+
install_nerd_fonts()
|
|
150
|
+
from machineconfig.setup_windows.wt_and_pwsh.set_wt_settings import main as set_wt_settings_main
|
|
151
|
+
set_wt_settings_main()
|
|
155
152
|
except Exception as e:
|
|
156
153
|
console.print(f"❌ Error configuring shell profile: {e}", style="bold red")
|
|
157
154
|
|
|
@@ -170,12 +167,6 @@ Set-Service -Name sshd -StartupType 'Automatic'"""
|
|
|
170
167
|
except Exception as e:
|
|
171
168
|
console.print(f"❌ Error retrieving backup data: {e}", style="bold red")
|
|
172
169
|
|
|
173
|
-
if "install_windows_desktop" in selected_options:
|
|
174
|
-
from machineconfig.jobs.installer.custom_dev.nerfont_windows_helper import install_nerd_fonts
|
|
175
|
-
install_nerd_fonts()
|
|
176
|
-
from machineconfig.setup_windows.wt_and_pwsh.set_wt_settings import main as set_wt_settings_main
|
|
177
|
-
set_wt_settings_main()
|
|
178
|
-
|
|
179
170
|
|
|
180
171
|
def main() -> None:
|
|
181
172
|
display_header()
|