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.

Files changed (30) hide show
  1. machineconfig/jobs/python/check_installations.py +0 -2
  2. machineconfig/jobs/python/python_ve_symlink.py +1 -1
  3. machineconfig/scripts/python/__pycache__/devops.cpython-313.pyc +0 -0
  4. machineconfig/scripts/python/__pycache__/devops_update_repos.cpython-313.pyc +0 -0
  5. machineconfig/scripts/python/__pycache__/fire_agents.cpython-313.pyc +0 -0
  6. machineconfig/scripts/python/__pycache__/repos_helper_record.cpython-313.pyc +0 -0
  7. machineconfig/scripts/python/devops.py +18 -64
  8. machineconfig/scripts/python/devops_add_identity.py +6 -2
  9. machineconfig/scripts/python/devops_add_ssh_key.py +5 -2
  10. machineconfig/scripts/python/devops_backup_retrieve.py +3 -15
  11. machineconfig/scripts/python/devops_devapps_install.py +22 -8
  12. machineconfig/scripts/python/devops_update_repos.py +125 -226
  13. machineconfig/scripts/python/fire_agents.py +108 -151
  14. machineconfig/scripts/python/fire_agents_help_launch.py +97 -0
  15. machineconfig/scripts/python/fire_agents_help_search.py +83 -0
  16. machineconfig/scripts/python/helpers/cloud_helpers.py +2 -5
  17. machineconfig/scripts/python/repos.py +1 -1
  18. machineconfig/scripts/python/repos_helper_record.py +82 -5
  19. machineconfig/scripts/python/repos_helper_update.py +288 -0
  20. machineconfig/utils/installer_utils/installer_class.py +3 -3
  21. machineconfig/utils/notifications.py +24 -4
  22. machineconfig/utils/path.py +2 -1
  23. machineconfig/utils/procs.py +50 -35
  24. machineconfig/utils/source_of_truth.py +2 -0
  25. machineconfig/utils/ssh.py +29 -7
  26. {machineconfig-2.7.dist-info → machineconfig-2.9.dist-info}/METADATA +7 -11
  27. {machineconfig-2.7.dist-info → machineconfig-2.9.dist-info}/RECORD +30 -26
  28. {machineconfig-2.7.dist-info → machineconfig-2.9.dist-info}/WHEEL +0 -0
  29. {machineconfig-2.7.dist-info → machineconfig-2.9.dist-info}/entry_points.txt +0 -0
  30. {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"""echo "📦️ 😑 {self.exe_name}, same version: {old_version_cli}" """
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"""echo "📦️ 🤩 {self.exe_name} updated from {old_version_cli} ➡️ TO ➡️ {new_version_cli}" """
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"""echo "📦️ ❌ Failed to install `{self.name}` with error: {ex}" """
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 markdown import markdown
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
- gh_style = Path(__file__).parent.joinpath("gh_style.css").read_text()
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
- {markdown(body)}
74
+ {html_output}
55
75
  </div>
56
76
  </body>
57
77
  </html>"""
@@ -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 {".links", ".venv", ".git", ".idea", ".vscode", "node_modules", "__pycache__", ".mypy_cache"}:
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)
@@ -1,13 +1,13 @@
1
1
  """Procs"""
2
2
 
3
3
  import psutil
4
- from tqdm import tqdm
5
- from pytz import timezone
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
- for proc in tqdm(psutil.process_iter(), desc="🔎 Scanning processes"):
24
- try:
25
- files = proc.open_files()
26
- except psutil.AccessDenied:
27
- continue
28
- tmp = [file.path for file in files if path in file.path]
29
- if len(tmp) > 0:
30
- res[proc.pid] = tmp
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
- for proc in tqdm(psutil.process_iter(), desc="🔍 Reading system processes"):
57
- try:
58
- mem_usage_mb = proc.memory_info().rss / (1024 * 1024)
59
- # Convert create_time to local timezone
60
- create_time_utc = datetime.fromtimestamp(proc.create_time(), tz=timezone("UTC"))
61
- create_time_local = create_time_utc.astimezone(timezone("Australia/Adelaide"))
62
-
63
- process_info.append(
64
- {
65
- "pid": proc.pid,
66
- "name": proc.name(),
67
- "username": proc.username(),
68
- "cpu_percent": proc.cpu_percent(),
69
- "memory_usage_mb": mem_usage_mb,
70
- "status": proc.status(),
71
- "create_time": create_time_local,
72
- "command": " ".join(proc.cmdline()),
73
- }
74
- )
75
- except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
76
- pass
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("UTC"))
211
- create_time_local = create_time_utc.astimezone(timezone("Australia/Adelaide"))
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=timezone("Australia/Adelaide"))
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
 
@@ -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
- def view_bar(slf: Any, a: Any, b: Any):
120
- slf.total = int(b)
121
- slf.update(int(a - slf.n)) # update pbar with increment
122
-
123
- from tqdm import tqdm
124
-
125
- self.tqdm_wrap = type("TqdmWrap", (tqdm,), {"view_bar": view_bar})
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.7
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