machineconfig 2.7__py3-none-any.whl → 2.9__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/jobs/python/check_installations.py +0 -2
- machineconfig/jobs/python/python_ve_symlink.py +1 -1
- machineconfig/scripts/python/__pycache__/devops.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/devops_update_repos.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/fire_agents.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/repos_helper_record.cpython-313.pyc +0 -0
- machineconfig/scripts/python/devops.py +18 -64
- machineconfig/scripts/python/devops_add_identity.py +6 -2
- machineconfig/scripts/python/devops_add_ssh_key.py +5 -2
- machineconfig/scripts/python/devops_backup_retrieve.py +3 -15
- machineconfig/scripts/python/devops_devapps_install.py +22 -8
- machineconfig/scripts/python/devops_update_repos.py +125 -226
- machineconfig/scripts/python/fire_agents.py +108 -151
- machineconfig/scripts/python/fire_agents_help_launch.py +97 -0
- machineconfig/scripts/python/fire_agents_help_search.py +83 -0
- machineconfig/scripts/python/helpers/cloud_helpers.py +2 -5
- machineconfig/scripts/python/repos.py +1 -1
- machineconfig/scripts/python/repos_helper_record.py +82 -5
- machineconfig/scripts/python/repos_helper_update.py +288 -0
- machineconfig/utils/installer_utils/installer_class.py +3 -3
- machineconfig/utils/notifications.py +24 -4
- machineconfig/utils/path.py +2 -1
- machineconfig/utils/procs.py +50 -35
- machineconfig/utils/source_of_truth.py +2 -0
- machineconfig/utils/ssh.py +29 -7
- {machineconfig-2.7.dist-info → machineconfig-2.9.dist-info}/METADATA +7 -11
- {machineconfig-2.7.dist-info → machineconfig-2.9.dist-info}/RECORD +30 -26
- {machineconfig-2.7.dist-info → machineconfig-2.9.dist-info}/WHEEL +0 -0
- {machineconfig-2.7.dist-info → machineconfig-2.9.dist-info}/entry_points.txt +0 -0
- {machineconfig-2.7.dist-info → machineconfig-2.9.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import TypedDict
|
|
3
|
+
import subprocess
|
|
4
|
+
import git
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RepositoryUpdateResult(TypedDict):
|
|
9
|
+
"""Result of updating a single repository."""
|
|
10
|
+
repo_path: str
|
|
11
|
+
status: str # "success", "error", "skipped", "auth_failed"
|
|
12
|
+
had_uncommitted_changes: bool
|
|
13
|
+
uncommitted_files: list[str]
|
|
14
|
+
commit_before: str
|
|
15
|
+
commit_after: str
|
|
16
|
+
commits_changed: bool
|
|
17
|
+
pyproject_changed: bool
|
|
18
|
+
uv_lock_changed: bool
|
|
19
|
+
dependencies_changed: bool
|
|
20
|
+
uv_sync_ran: bool
|
|
21
|
+
uv_sync_success: bool
|
|
22
|
+
remotes_processed: list[str]
|
|
23
|
+
remotes_skipped: list[str]
|
|
24
|
+
error_message: str | None
|
|
25
|
+
is_machineconfig_repo: bool
|
|
26
|
+
permissions_updated: bool
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def set_permissions_recursive(path: Path, executable: bool = True) -> None:
|
|
30
|
+
"""Set permissions recursively for a directory."""
|
|
31
|
+
if not path.exists():
|
|
32
|
+
return
|
|
33
|
+
if path.is_file():
|
|
34
|
+
if executable:
|
|
35
|
+
path.chmod(0o755)
|
|
36
|
+
else:
|
|
37
|
+
path.chmod(0o644)
|
|
38
|
+
elif path.is_dir():
|
|
39
|
+
path.chmod(0o755)
|
|
40
|
+
for item in path.rglob("*"):
|
|
41
|
+
set_permissions_recursive(item, executable)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def run_uv_sync(repo_path: Path) -> bool:
|
|
45
|
+
"""Run uv sync in the given repository path. Returns True if successful."""
|
|
46
|
+
try:
|
|
47
|
+
print(f"🔄 Running uv sync in {repo_path}")
|
|
48
|
+
# Run uv sync with output directly to terminal (no capture)
|
|
49
|
+
subprocess.run(["uv", "sync"], cwd=repo_path, check=True)
|
|
50
|
+
print("✅ uv sync completed successfully")
|
|
51
|
+
return True
|
|
52
|
+
except subprocess.CalledProcessError as e:
|
|
53
|
+
print(f"❌ uv sync failed with return code {e.returncode}")
|
|
54
|
+
return False
|
|
55
|
+
except FileNotFoundError:
|
|
56
|
+
print("⚠️ uv command not found. Please install uv first.")
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_file_hash(file_path: Path) -> str | None:
|
|
61
|
+
"""Get SHA256 hash of a file, return None if file doesn't exist."""
|
|
62
|
+
if not file_path.exists():
|
|
63
|
+
return None
|
|
64
|
+
import hashlib
|
|
65
|
+
return hashlib.sha256(file_path.read_bytes()).hexdigest()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def update_repository(repo: git.Repo, auto_sync: bool, allow_password_prompt: bool) -> RepositoryUpdateResult:
|
|
69
|
+
"""Update a single repository and return detailed information about what happened."""
|
|
70
|
+
repo_path = Path(repo.working_dir)
|
|
71
|
+
print(f"🔄 {'Updating ' + str(repo_path):.^80}")
|
|
72
|
+
|
|
73
|
+
# Initialize result dict
|
|
74
|
+
result: RepositoryUpdateResult = {
|
|
75
|
+
"repo_path": str(repo_path),
|
|
76
|
+
"status": "success",
|
|
77
|
+
"had_uncommitted_changes": False,
|
|
78
|
+
"uncommitted_files": [],
|
|
79
|
+
"commit_before": "",
|
|
80
|
+
"commit_after": "",
|
|
81
|
+
"commits_changed": False,
|
|
82
|
+
"pyproject_changed": False,
|
|
83
|
+
"uv_lock_changed": False,
|
|
84
|
+
"dependencies_changed": False,
|
|
85
|
+
"uv_sync_ran": False,
|
|
86
|
+
"uv_sync_success": False,
|
|
87
|
+
"remotes_processed": [],
|
|
88
|
+
"remotes_skipped": [],
|
|
89
|
+
"error_message": None,
|
|
90
|
+
"is_machineconfig_repo": "machineconfig" in str(repo_path),
|
|
91
|
+
"permissions_updated": False,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# Check git status first
|
|
95
|
+
print("📊 Checking git status...")
|
|
96
|
+
if repo.is_dirty():
|
|
97
|
+
# Get the list of modified files
|
|
98
|
+
changed_files_raw = [item.a_path for item in repo.index.diff(None)]
|
|
99
|
+
changed_files_raw.extend([item.a_path for item in repo.index.diff("HEAD")])
|
|
100
|
+
# Filter out None values and remove duplicates
|
|
101
|
+
changed_files = list(set(file for file in changed_files_raw if file is not None))
|
|
102
|
+
|
|
103
|
+
result["had_uncommitted_changes"] = True
|
|
104
|
+
result["uncommitted_files"] = changed_files
|
|
105
|
+
print(f"⚠️ Repository has uncommitted changes: {', '.join(changed_files)}")
|
|
106
|
+
|
|
107
|
+
# Check if the only change is uv.lock
|
|
108
|
+
if len(changed_files) == 1 and changed_files[0] == "uv.lock":
|
|
109
|
+
print("🔒 Only uv.lock has changes, resetting it...")
|
|
110
|
+
try:
|
|
111
|
+
# Reset uv.lock file
|
|
112
|
+
subprocess.run(["git", "checkout", "HEAD", "--", "uv.lock"], cwd=repo_path, check=True)
|
|
113
|
+
print("✅ uv.lock has been reset")
|
|
114
|
+
except subprocess.CalledProcessError as e:
|
|
115
|
+
result["status"] = "error"
|
|
116
|
+
result["error_message"] = f"Failed to reset uv.lock: {e}"
|
|
117
|
+
print(f"❌ Failed to reset uv.lock: {e}")
|
|
118
|
+
return result
|
|
119
|
+
else:
|
|
120
|
+
# Multiple files or files other than uv.lock have changes
|
|
121
|
+
result["status"] = "error"
|
|
122
|
+
result["error_message"] = f"Cannot update repository - there are pending changes in: {', '.join(changed_files)}. Please commit or stash your changes first."
|
|
123
|
+
raise RuntimeError(result["error_message"])
|
|
124
|
+
else:
|
|
125
|
+
print("✅ Repository is clean")
|
|
126
|
+
|
|
127
|
+
# Check if this repo has pyproject.toml or uv.lock
|
|
128
|
+
pyproject_path = repo_path / "pyproject.toml"
|
|
129
|
+
uv_lock_path = repo_path / "uv.lock"
|
|
130
|
+
|
|
131
|
+
# Get hashes before pull
|
|
132
|
+
pyproject_hash_before = get_file_hash(pyproject_path)
|
|
133
|
+
uv_lock_hash_before = get_file_hash(uv_lock_path)
|
|
134
|
+
|
|
135
|
+
# Get current commit hash before pull
|
|
136
|
+
result["commit_before"] = repo.head.commit.hexsha
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
# Use subprocess for git pull to get better output control
|
|
140
|
+
|
|
141
|
+
# Get list of remotes
|
|
142
|
+
remotes = list(repo.remotes)
|
|
143
|
+
if not remotes:
|
|
144
|
+
print("⚠️ No remotes configured for this repository")
|
|
145
|
+
result["status"] = "skipped"
|
|
146
|
+
result["error_message"] = "No remotes configured for this repository"
|
|
147
|
+
return result
|
|
148
|
+
|
|
149
|
+
for remote in remotes:
|
|
150
|
+
try:
|
|
151
|
+
print(f"📥 Fetching from {remote.name}...")
|
|
152
|
+
|
|
153
|
+
# Set up environment for git commands
|
|
154
|
+
env = None
|
|
155
|
+
if not allow_password_prompt:
|
|
156
|
+
# Disable interactive prompts
|
|
157
|
+
import os
|
|
158
|
+
|
|
159
|
+
env = os.environ.copy()
|
|
160
|
+
env["GIT_TERMINAL_PROMPT"] = "0"
|
|
161
|
+
env["GIT_ASKPASS"] = "echo" # Returns empty string for any credential request
|
|
162
|
+
|
|
163
|
+
# First fetch to see what's available
|
|
164
|
+
fetch_result = subprocess.run(
|
|
165
|
+
["git", "fetch", remote.name, "--verbose"],
|
|
166
|
+
cwd=repo_path,
|
|
167
|
+
capture_output=True,
|
|
168
|
+
text=True,
|
|
169
|
+
env=env,
|
|
170
|
+
timeout=30, # Add timeout to prevent hanging
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Check if fetch failed due to authentication
|
|
174
|
+
if fetch_result.returncode != 0 and not allow_password_prompt:
|
|
175
|
+
auth_error_indicators = [
|
|
176
|
+
"Authentication failed",
|
|
177
|
+
"Password for",
|
|
178
|
+
"Username for",
|
|
179
|
+
"could not read Username",
|
|
180
|
+
"could not read Password",
|
|
181
|
+
"fatal: Authentication failed",
|
|
182
|
+
"fatal: could not read Username",
|
|
183
|
+
"fatal: could not read Password",
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
error_output = (fetch_result.stderr or "") + (fetch_result.stdout or "")
|
|
187
|
+
if any(indicator in error_output for indicator in auth_error_indicators):
|
|
188
|
+
print(f"⚠️ Skipping {remote.name} - authentication required but password prompts are disabled")
|
|
189
|
+
continue
|
|
190
|
+
|
|
191
|
+
if fetch_result.stdout:
|
|
192
|
+
print(f"📡 Fetch output: {fetch_result.stdout.strip()}")
|
|
193
|
+
if fetch_result.stderr:
|
|
194
|
+
print(f"📡 Fetch info: {fetch_result.stderr.strip()}")
|
|
195
|
+
|
|
196
|
+
# Now pull with verbose output
|
|
197
|
+
print(f"📥 Pulling from {remote.name}/{repo.active_branch.name}...")
|
|
198
|
+
pull_result = subprocess.run(["git", "pull", remote.name, repo.active_branch.name, "--verbose"], cwd=repo_path, capture_output=True, text=True, env=env, timeout=30)
|
|
199
|
+
|
|
200
|
+
# Check if pull failed due to authentication
|
|
201
|
+
if pull_result.returncode != 0 and not allow_password_prompt:
|
|
202
|
+
auth_error_indicators = [
|
|
203
|
+
"Authentication failed",
|
|
204
|
+
"Password for",
|
|
205
|
+
"Username for",
|
|
206
|
+
"could not read Username",
|
|
207
|
+
"could not read Password",
|
|
208
|
+
"fatal: Authentication failed",
|
|
209
|
+
"fatal: could not read Username",
|
|
210
|
+
"fatal: could not read Password",
|
|
211
|
+
]
|
|
212
|
+
|
|
213
|
+
error_output = (pull_result.stderr or "") + (pull_result.stdout or "")
|
|
214
|
+
if any(indicator in error_output for indicator in auth_error_indicators):
|
|
215
|
+
print(f"⚠️ Skipping pull from {remote.name} - authentication required but password prompts are disabled")
|
|
216
|
+
continue
|
|
217
|
+
|
|
218
|
+
if pull_result.stdout:
|
|
219
|
+
print(f"📦 Pull output: {pull_result.stdout.strip()}")
|
|
220
|
+
if pull_result.stderr:
|
|
221
|
+
print(f"📦 Pull info: {pull_result.stderr.strip()}")
|
|
222
|
+
|
|
223
|
+
# Check if pull was successful
|
|
224
|
+
if pull_result.returncode == 0:
|
|
225
|
+
result["remotes_processed"].append(remote.name)
|
|
226
|
+
# Check if commits changed
|
|
227
|
+
result["commit_after"] = repo.head.commit.hexsha
|
|
228
|
+
if result["commit_before"] != result["commit_after"]:
|
|
229
|
+
result["commits_changed"] = True
|
|
230
|
+
print(f"✅ Repository updated: {result['commit_before'][:8]} → {result['commit_after'][:8]}")
|
|
231
|
+
else:
|
|
232
|
+
print("✅ Already up to date")
|
|
233
|
+
else:
|
|
234
|
+
result["remotes_skipped"].append(remote.name)
|
|
235
|
+
print(f"❌ Pull failed with return code {pull_result.returncode}")
|
|
236
|
+
|
|
237
|
+
except Exception as e:
|
|
238
|
+
result["remotes_skipped"].append(remote.name)
|
|
239
|
+
print(f"⚠️ Failed to pull from {remote.name}: {e}")
|
|
240
|
+
continue
|
|
241
|
+
|
|
242
|
+
# Check if pyproject.toml or uv.lock changed after pull
|
|
243
|
+
pyproject_hash_after = get_file_hash(pyproject_path)
|
|
244
|
+
uv_lock_hash_after = get_file_hash(uv_lock_path)
|
|
245
|
+
|
|
246
|
+
if pyproject_hash_before != pyproject_hash_after:
|
|
247
|
+
print("📋 pyproject.toml has changed")
|
|
248
|
+
result["pyproject_changed"] = True
|
|
249
|
+
result["dependencies_changed"] = True
|
|
250
|
+
|
|
251
|
+
if uv_lock_hash_before != uv_lock_hash_after:
|
|
252
|
+
print("🔒 uv.lock has changed")
|
|
253
|
+
result["uv_lock_changed"] = True
|
|
254
|
+
result["dependencies_changed"] = True
|
|
255
|
+
|
|
256
|
+
# Special handling for machineconfig repository
|
|
257
|
+
if result["is_machineconfig_repo"]:
|
|
258
|
+
print("🛠 Special handling for machineconfig repository...")
|
|
259
|
+
scripts_path = Path.home() / "scripts"
|
|
260
|
+
if scripts_path.exists():
|
|
261
|
+
set_permissions_recursive(scripts_path)
|
|
262
|
+
result["permissions_updated"] = True
|
|
263
|
+
print(f"✅ Set permissions for {scripts_path}")
|
|
264
|
+
|
|
265
|
+
linux_jobs_path = repo_path / "src" / "machineconfig" / "jobs" / "linux"
|
|
266
|
+
if linux_jobs_path.exists():
|
|
267
|
+
set_permissions_recursive(linux_jobs_path)
|
|
268
|
+
result["permissions_updated"] = True
|
|
269
|
+
print(f"✅ Set permissions for {linux_jobs_path}")
|
|
270
|
+
|
|
271
|
+
lf_exe_path = repo_path / "src" / "machineconfig" / "settings" / "lf" / "linux" / "exe"
|
|
272
|
+
if lf_exe_path.exists():
|
|
273
|
+
set_permissions_recursive(lf_exe_path)
|
|
274
|
+
result["permissions_updated"] = True
|
|
275
|
+
print(f"✅ Set permissions for {lf_exe_path}")
|
|
276
|
+
|
|
277
|
+
# Run uv sync if dependencies changed and auto_sync is enabled
|
|
278
|
+
if result["dependencies_changed"] and auto_sync:
|
|
279
|
+
result["uv_sync_ran"] = True
|
|
280
|
+
result["uv_sync_success"] = run_uv_sync(repo_path)
|
|
281
|
+
|
|
282
|
+
return result
|
|
283
|
+
|
|
284
|
+
except Exception as e:
|
|
285
|
+
result["status"] = "error"
|
|
286
|
+
result["error_message"] = str(e)
|
|
287
|
+
print(f"❌ Error updating repository {repo_path}: {e}")
|
|
288
|
+
return result
|
|
@@ -94,14 +94,14 @@ class Installer:
|
|
|
94
94
|
|
|
95
95
|
if old_version_cli == new_version_cli:
|
|
96
96
|
print(f"ℹ️ Same version detected: {old_version_cli}")
|
|
97
|
-
return f"""
|
|
97
|
+
return f"""📦️ 😑 {self.exe_name}, same version: {old_version_cli}"""
|
|
98
98
|
else:
|
|
99
99
|
print(f"🚀 Update successful: {old_version_cli} ➡️ {new_version_cli}")
|
|
100
|
-
return f"""
|
|
100
|
+
return f"""📦️ 🤩 {self.exe_name} updated from {old_version_cli} ➡️ TO ➡️ {new_version_cli}"""
|
|
101
101
|
|
|
102
102
|
except Exception as ex:
|
|
103
103
|
print(f"❌ ERROR: Installation failed for {self.exe_name}: {ex}")
|
|
104
|
-
return f"""
|
|
104
|
+
return f"""📦️ ❌ Failed to install `{self.name}` with error: {ex}"""
|
|
105
105
|
|
|
106
106
|
def install(self, version: Optional[str]):
|
|
107
107
|
print(f"\n{'=' * 80}\n🔧 INSTALLATION PROCESS: {self.exe_name} 🔧\n{'=' * 80}")
|
|
@@ -14,7 +14,8 @@ import imaplib
|
|
|
14
14
|
from email.mime.text import MIMEText
|
|
15
15
|
from email.mime.multipart import MIMEMultipart
|
|
16
16
|
from typing import Optional, Any, Union
|
|
17
|
-
from
|
|
17
|
+
from rich.console import Console
|
|
18
|
+
from rich.markdown import Markdown
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
def download_to_memory(path: Path, allow_redirects: bool = True, timeout: Optional[float] = None, params: Any = None) -> "Any":
|
|
@@ -30,8 +31,27 @@ def get_github_markdown_css() -> str:
|
|
|
30
31
|
return download_to_memory(Path(pp)).text
|
|
31
32
|
|
|
32
33
|
|
|
33
|
-
def md2html(body: str):
|
|
34
|
-
|
|
34
|
+
def md2html(body: str) -> str:
|
|
35
|
+
"""Convert markdown to HTML using Rich library."""
|
|
36
|
+
# Use Rich's HTML export functionality to convert markdown to HTML
|
|
37
|
+
console = Console(record=True, width=120)
|
|
38
|
+
markdown_obj = Markdown(body)
|
|
39
|
+
console.print(markdown_obj)
|
|
40
|
+
html_output = console.export_html(inline_styles=True)
|
|
41
|
+
|
|
42
|
+
# Try to load GitHub CSS style, fallback to basic style if not found
|
|
43
|
+
gh_style_path = Path(__file__).parent.joinpath("gh_style.css")
|
|
44
|
+
if gh_style_path.exists():
|
|
45
|
+
gh_style = gh_style_path.read_text()
|
|
46
|
+
else:
|
|
47
|
+
# Fallback basic styling
|
|
48
|
+
gh_style = """
|
|
49
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }
|
|
50
|
+
h1, h2, h3, h4, h5, h6 { color: #0366d6; }
|
|
51
|
+
code { background-color: #f6f8fa; padding: 2px 4px; border-radius: 3px; }
|
|
52
|
+
pre { background-color: #f6f8fa; padding: 16px; border-radius: 6px; overflow: auto; }
|
|
53
|
+
"""
|
|
54
|
+
|
|
35
55
|
return f"""
|
|
36
56
|
<!DOCTYPE html>
|
|
37
57
|
<html>
|
|
@@ -51,7 +71,7 @@ def md2html(body: str):
|
|
|
51
71
|
</style>
|
|
52
72
|
<body>
|
|
53
73
|
<div class="markdown-body">
|
|
54
|
-
{
|
|
74
|
+
{html_output}
|
|
55
75
|
</div>
|
|
56
76
|
</body>
|
|
57
77
|
</html>"""
|
machineconfig/utils/path.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from machineconfig.utils.path_reduced import PathExtended as PathExtended
|
|
2
2
|
from machineconfig.utils.options import choose_one_option
|
|
3
|
+
from machineconfig.utils.source_of_truth import EXCLUDE_DIRS
|
|
3
4
|
from rich.console import Console
|
|
4
5
|
from rich.panel import Panel
|
|
5
6
|
import platform
|
|
@@ -54,7 +55,7 @@ def find_scripts(root: Path, name_substring: str, suffixes: set[str]) -> tuple[l
|
|
|
54
55
|
partial_path_matches = []
|
|
55
56
|
for entry in root.iterdir():
|
|
56
57
|
if entry.is_dir():
|
|
57
|
-
if entry.name in
|
|
58
|
+
if entry.name in set(EXCLUDE_DIRS):
|
|
58
59
|
# prune this entire subtree
|
|
59
60
|
continue
|
|
60
61
|
tmp1, tmp2 = find_scripts(entry, name_substring, suffixes)
|
machineconfig/utils/procs.py
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
"""Procs"""
|
|
2
2
|
|
|
3
3
|
import psutil
|
|
4
|
-
from
|
|
5
|
-
from
|
|
4
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
5
|
+
from zoneinfo import ZoneInfo
|
|
6
6
|
from machineconfig.utils.options import display_options
|
|
7
7
|
from typing import Optional, Any
|
|
8
8
|
from rich.console import Console
|
|
9
9
|
from rich.panel import Panel
|
|
10
|
-
from datetime import datetime
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
11
|
from machineconfig.utils.utils2 import pprint
|
|
12
12
|
|
|
13
13
|
console = Console()
|
|
@@ -20,14 +20,22 @@ def get_processes_accessing_file(path: str):
|
|
|
20
20
|
title = "🔍 SEARCHING FOR PROCESSES ACCESSING FILE"
|
|
21
21
|
console.print(Panel(title, title="[bold blue]Process Info[/bold blue]", border_style="blue"))
|
|
22
22
|
res: dict[int, list[str]] = {}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
23
|
+
|
|
24
|
+
with Progress(
|
|
25
|
+
SpinnerColumn(),
|
|
26
|
+
TextColumn("[progress.description]{task.description}"),
|
|
27
|
+
) as progress:
|
|
28
|
+
progress.add_task("🔎 Scanning processes...", total=None)
|
|
29
|
+
|
|
30
|
+
for proc in psutil.process_iter():
|
|
31
|
+
try:
|
|
32
|
+
files = proc.open_files()
|
|
33
|
+
except psutil.AccessDenied:
|
|
34
|
+
continue
|
|
35
|
+
tmp = [file.path for file in files if path in file.path]
|
|
36
|
+
if len(tmp) > 0:
|
|
37
|
+
res[proc.pid] = tmp
|
|
38
|
+
|
|
31
39
|
# Convert to list of dictionaries for consistent data structure
|
|
32
40
|
result_data = [{"pid": pid, "files": files} for pid, files in res.items()]
|
|
33
41
|
console.print(Panel(f"✅ Found {len(res)} processes accessing the specified file", title="[bold blue]Process Info[/bold blue]", border_style="blue"))
|
|
@@ -53,27 +61,34 @@ class ProcessManager:
|
|
|
53
61
|
title = "📊 INITIALIZING PROCESS MANAGER"
|
|
54
62
|
console.print(Panel(title, title="[bold blue]Process Info[/bold blue]", border_style="blue"))
|
|
55
63
|
process_info = []
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
64
|
+
|
|
65
|
+
with Progress(
|
|
66
|
+
SpinnerColumn(),
|
|
67
|
+
TextColumn("[progress.description]{task.description}"),
|
|
68
|
+
) as progress:
|
|
69
|
+
progress.add_task("🔍 Reading system processes...", total=None)
|
|
70
|
+
|
|
71
|
+
for proc in psutil.process_iter():
|
|
72
|
+
try:
|
|
73
|
+
mem_usage_mb = proc.memory_info().rss / (1024 * 1024)
|
|
74
|
+
# Convert create_time to local timezone
|
|
75
|
+
create_time_utc = datetime.fromtimestamp(proc.create_time(), tz=timezone.utc)
|
|
76
|
+
create_time_local = create_time_utc.astimezone(ZoneInfo("Australia/Adelaide"))
|
|
77
|
+
|
|
78
|
+
process_info.append(
|
|
79
|
+
{
|
|
80
|
+
"pid": proc.pid,
|
|
81
|
+
"name": proc.name(),
|
|
82
|
+
"username": proc.username(),
|
|
83
|
+
"cpu_percent": proc.cpu_percent(),
|
|
84
|
+
"memory_usage_mb": mem_usage_mb,
|
|
85
|
+
"status": proc.status(),
|
|
86
|
+
"create_time": create_time_local,
|
|
87
|
+
"command": " ".join(proc.cmdline()),
|
|
88
|
+
}
|
|
89
|
+
)
|
|
90
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
|
91
|
+
pass
|
|
77
92
|
|
|
78
93
|
# Sort by memory usage (descending)
|
|
79
94
|
process_info.sort(key=lambda x: x["memory_usage_mb"], reverse=True)
|
|
@@ -207,13 +222,13 @@ def get_age(create_time: Any) -> str:
|
|
|
207
222
|
try:
|
|
208
223
|
if isinstance(create_time, (int, float)):
|
|
209
224
|
# Handle timestampz
|
|
210
|
-
create_time_utc = datetime.fromtimestamp(create_time, tz=timezone
|
|
211
|
-
create_time_local = create_time_utc.astimezone(
|
|
225
|
+
create_time_utc = datetime.fromtimestamp(create_time, tz=timezone.utc)
|
|
226
|
+
create_time_local = create_time_utc.astimezone(ZoneInfo("Australia/Adelaide"))
|
|
212
227
|
else:
|
|
213
228
|
# Already a datetime object
|
|
214
229
|
create_time_local = create_time
|
|
215
230
|
|
|
216
|
-
now_local = datetime.now(tz=
|
|
231
|
+
now_local = datetime.now(tz=ZoneInfo("Australia/Adelaide"))
|
|
217
232
|
age = now_local - create_time_local
|
|
218
233
|
return str(age)
|
|
219
234
|
except Exception as e:
|
|
@@ -5,6 +5,8 @@ Utils
|
|
|
5
5
|
import machineconfig
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
|
+
EXCLUDE_DIRS = [".links", ".ai", ".scripts", ".venv", ".git", ".idea", ".vscode", "node_modules", "__pycache__", ".mypy_cache"]
|
|
9
|
+
|
|
8
10
|
LIBRARY_ROOT = Path(machineconfig.__file__).resolve().parent
|
|
9
11
|
REPO_ROOT = LIBRARY_ROOT.parent.parent
|
|
10
12
|
|
machineconfig/utils/ssh.py
CHANGED
|
@@ -116,13 +116,35 @@ class SSH: # inferior alternative: https://github.com/fabric/fabric
|
|
|
116
116
|
print(f"""⚠️ WARNING: Failed to open SFTP connection to {hostname}.
|
|
117
117
|
Error Details: {err}\nData transfer may be affected!""")
|
|
118
118
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
119
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, FileSizeColumn, TransferSpeedColumn
|
|
120
|
+
|
|
121
|
+
class RichProgressWrapper:
|
|
122
|
+
def __init__(self, **kwargs: Any):
|
|
123
|
+
self.kwargs = kwargs
|
|
124
|
+
self.progress: Optional[Progress] = None
|
|
125
|
+
self.task: Optional[Any] = None
|
|
126
|
+
|
|
127
|
+
def __enter__(self) -> "RichProgressWrapper":
|
|
128
|
+
self.progress = Progress(
|
|
129
|
+
SpinnerColumn(),
|
|
130
|
+
TextColumn("[bold blue]{task.description}"),
|
|
131
|
+
BarColumn(),
|
|
132
|
+
FileSizeColumn(),
|
|
133
|
+
TransferSpeedColumn(),
|
|
134
|
+
)
|
|
135
|
+
self.progress.start()
|
|
136
|
+
self.task = self.progress.add_task("Transferring...", total=0)
|
|
137
|
+
return self
|
|
138
|
+
|
|
139
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
140
|
+
if self.progress:
|
|
141
|
+
self.progress.stop()
|
|
142
|
+
|
|
143
|
+
def view_bar(self, transferred: int, total: int) -> None:
|
|
144
|
+
if self.progress and self.task is not None:
|
|
145
|
+
self.progress.update(self.task, completed=transferred, total=total)
|
|
146
|
+
|
|
147
|
+
self.tqdm_wrap = RichProgressWrapper
|
|
126
148
|
self._local_distro: Optional[str] = None
|
|
127
149
|
self._remote_distro: Optional[str] = None
|
|
128
150
|
self._remote_machine: Optional[MACHINE] = None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: machineconfig
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.9
|
|
4
4
|
Summary: Dotfiles management package
|
|
5
5
|
Author-email: Alex Al-Saffar <programmer@usa.com>
|
|
6
6
|
License: Apache 2.0
|
|
@@ -13,24 +13,20 @@ Requires-Python: >=3.13
|
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
14
|
Requires-Dist: cryptography>=44.0.2
|
|
15
15
|
Requires-Dist: fire>=0.7.0
|
|
16
|
-
Requires-Dist: gitpython>=3.1.44
|
|
17
16
|
Requires-Dist: joblib>=1.5.2
|
|
18
|
-
Requires-Dist: markdown>=3.9
|
|
19
17
|
Requires-Dist: paramiko>=3.5.1
|
|
20
|
-
Requires-Dist: psutil>=7.0.0
|
|
21
|
-
Requires-Dist: pydantic>=2.11.3
|
|
22
|
-
Requires-Dist: pyfzf>=0.3.1
|
|
23
|
-
Requires-Dist: pyjson5>=1.6.9
|
|
24
|
-
Requires-Dist: pytz>=2025.2
|
|
25
|
-
Requires-Dist: pyyaml>=6.0.2
|
|
26
18
|
Requires-Dist: randomname>=0.2.1
|
|
27
|
-
Requires-Dist: rclone-python>=0.1.23
|
|
28
19
|
Requires-Dist: requests>=2.32.5
|
|
29
20
|
Requires-Dist: rich>=14.0.0
|
|
30
21
|
Requires-Dist: tenacity>=9.1.2
|
|
22
|
+
Requires-Dist: psutil>=7.0.0
|
|
23
|
+
Requires-Dist: gitpython>=3.1.44
|
|
24
|
+
Requires-Dist: pyfzf>=0.3.1
|
|
25
|
+
Requires-Dist: rclone-python>=0.1.23
|
|
26
|
+
Requires-Dist: pyjson5>=1.6.9
|
|
27
|
+
Requires-Dist: pyyaml>=6.0.2
|
|
31
28
|
Requires-Dist: toml>=0.10.2
|
|
32
29
|
Requires-Dist: tomli>=2.2.1
|
|
33
|
-
Requires-Dist: tqdm>=4.67.1
|
|
34
30
|
Provides-Extra: windows
|
|
35
31
|
Requires-Dist: pywin32; extra == "windows"
|
|
36
32
|
Provides-Extra: docs
|