machineconfig 2.1__py3-none-any.whl → 2.3__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 (127) hide show
  1. machineconfig/cluster/sessions_managers/enhanced_command_runner.py +0 -2
  2. machineconfig/cluster/sessions_managers/layout_types.py +29 -0
  3. machineconfig/cluster/sessions_managers/wt_local.py +68 -62
  4. machineconfig/cluster/sessions_managers/wt_local_manager.py +51 -22
  5. machineconfig/cluster/sessions_managers/wt_remote.py +30 -108
  6. machineconfig/cluster/sessions_managers/wt_remote_manager.py +14 -11
  7. machineconfig/cluster/sessions_managers/wt_utils/layout_generator.py +33 -37
  8. machineconfig/cluster/sessions_managers/wt_utils/process_monitor.py +22 -17
  9. machineconfig/cluster/sessions_managers/wt_utils/session_manager.py +59 -10
  10. machineconfig/cluster/sessions_managers/wt_utils/status_reporter.py +16 -14
  11. machineconfig/cluster/sessions_managers/zellij_local.py +75 -57
  12. machineconfig/cluster/sessions_managers/zellij_local_manager.py +51 -23
  13. machineconfig/cluster/sessions_managers/zellij_remote.py +47 -27
  14. machineconfig/cluster/sessions_managers/zellij_remote_manager.py +13 -12
  15. machineconfig/cluster/sessions_managers/zellij_utils/example_usage.py +14 -10
  16. machineconfig/cluster/sessions_managers/zellij_utils/layout_generator.py +31 -15
  17. machineconfig/cluster/sessions_managers/zellij_utils/process_monitor.py +47 -21
  18. machineconfig/cluster/sessions_managers/zellij_utils/session_manager.py +1 -1
  19. machineconfig/cluster/sessions_managers/zellij_utils/status_reporter.py +8 -7
  20. machineconfig/cluster/templates/utils.py +0 -35
  21. machineconfig/jobs/python/check_installations.py +1 -1
  22. machineconfig/jobs/python_custom_installers/dev/code.py +0 -13
  23. machineconfig/jobs/python_generic_installers/config.json +1 -1
  24. machineconfig/profile/create.py +13 -4
  25. machineconfig/profile/create_hardlinks.py +3 -1
  26. machineconfig/profile/shell.py +8 -7
  27. machineconfig/scripts/__init__.py +0 -2
  28. machineconfig/scripts/linux/devops +6 -4
  29. machineconfig/scripts/python/ai/generate_files.py +14 -15
  30. machineconfig/scripts/python/ai/mcinit.py +8 -5
  31. machineconfig/scripts/python/archive/tmate_conn.py +5 -5
  32. machineconfig/scripts/python/archive/tmate_start.py +7 -7
  33. machineconfig/scripts/python/choose_wezterm_theme.py +35 -32
  34. machineconfig/scripts/python/cloud_copy.py +22 -13
  35. machineconfig/scripts/python/cloud_mount.py +35 -23
  36. machineconfig/scripts/python/cloud_repo_sync.py +38 -25
  37. machineconfig/scripts/python/cloud_sync.py +4 -4
  38. machineconfig/scripts/python/croshell.py +37 -28
  39. machineconfig/scripts/python/devops.py +46 -27
  40. machineconfig/scripts/python/devops_add_identity.py +15 -25
  41. machineconfig/scripts/python/devops_add_ssh_key.py +7 -7
  42. machineconfig/scripts/python/devops_backup_retrieve.py +17 -15
  43. machineconfig/scripts/python/devops_devapps_install.py +26 -20
  44. machineconfig/scripts/python/devops_update_repos.py +142 -57
  45. machineconfig/scripts/python/dotfile.py +16 -14
  46. machineconfig/scripts/python/fire_agents.py +30 -23
  47. machineconfig/scripts/python/fire_jobs.py +86 -98
  48. machineconfig/scripts/python/fire_jobs_args_helper.py +84 -0
  49. machineconfig/scripts/python/fire_jobs_layout_helper.py +66 -0
  50. machineconfig/scripts/python/ftpx.py +24 -14
  51. machineconfig/scripts/python/get_zellij_cmd.py +8 -7
  52. machineconfig/scripts/python/helpers/cloud_helpers.py +33 -28
  53. machineconfig/scripts/python/helpers/helpers2.py +25 -14
  54. machineconfig/scripts/python/helpers/helpers4.py +44 -31
  55. machineconfig/scripts/python/helpers/helpers5.py +1 -1
  56. machineconfig/scripts/python/helpers/repo_sync_helpers.py +31 -9
  57. machineconfig/scripts/python/mount_nfs.py +8 -15
  58. machineconfig/scripts/python/mount_nw_drive.py +10 -5
  59. machineconfig/scripts/python/mount_ssh.py +8 -6
  60. machineconfig/scripts/python/repos.py +215 -57
  61. machineconfig/scripts/python/snapshot.py +0 -1
  62. machineconfig/scripts/python/start_slidev.py +10 -5
  63. machineconfig/scripts/python/start_terminals.py +22 -16
  64. machineconfig/scripts/python/viewer_template.py +0 -1
  65. machineconfig/scripts/python/wifi_conn.py +49 -76
  66. machineconfig/scripts/python/wsl_windows_transfer.py +8 -6
  67. machineconfig/settings/lf/linux/lfrc +1 -0
  68. machineconfig/setup_linux/web_shortcuts/croshell.sh +5 -0
  69. machineconfig/setup_linux/web_shortcuts/interactive.sh +1 -1
  70. machineconfig/setup_linux/web_shortcuts/ssh.sh +0 -4
  71. machineconfig/setup_windows/wt_and_pwsh/set_pwsh_theme.py +3 -12
  72. machineconfig/setup_windows/wt_and_pwsh/set_wt_settings.py +1 -1
  73. machineconfig/utils/code.py +2 -3
  74. machineconfig/utils/installer.py +2 -2
  75. machineconfig/utils/installer_utils/installer_abc.py +2 -4
  76. machineconfig/utils/installer_utils/installer_class.py +6 -4
  77. machineconfig/utils/links.py +103 -33
  78. machineconfig/utils/notifications.py +52 -38
  79. machineconfig/utils/options.py +14 -21
  80. machineconfig/utils/path.py +12 -12
  81. machineconfig/utils/path_reduced.py +239 -200
  82. machineconfig/utils/procs.py +1 -1
  83. machineconfig/utils/source_of_truth.py +27 -0
  84. machineconfig/utils/ssh.py +9 -19
  85. machineconfig/utils/terminal.py +4 -2
  86. machineconfig/utils/upgrade_packages.py +91 -0
  87. machineconfig/utils/utils2.py +1 -2
  88. machineconfig/utils/utils5.py +23 -11
  89. machineconfig/utils/ve.py +4 -1
  90. {machineconfig-2.1.dist-info → machineconfig-2.3.dist-info}/METADATA +13 -13
  91. {machineconfig-2.1.dist-info → machineconfig-2.3.dist-info}/RECORD +105 -121
  92. machineconfig-2.3.dist-info/entry_points.txt +2 -0
  93. machineconfig/cluster/sessions_managers/archive/create_zellij_template.py +0 -59
  94. machineconfig/cluster/sessions_managers/archive/session_managers.py +0 -183
  95. machineconfig/cluster/sessions_managers/demo_rich_zellij.py +0 -0
  96. machineconfig/jobs/__pycache__/__init__.cpython-313.pyc +0 -0
  97. machineconfig/jobs/python_custom_installers/__pycache__/__init__.cpython-313.pyc +0 -0
  98. machineconfig/jobs/python_generic_installers/__pycache__/__init__.cpython-313.pyc +0 -0
  99. machineconfig/jobs/python_linux_installers/__pycache__/__init__.cpython-313.pyc +0 -0
  100. machineconfig/scripts/__pycache__/__init__.cpython-313.pyc +0 -0
  101. machineconfig/scripts/python/__pycache__/__init__.cpython-313.pyc +0 -0
  102. machineconfig/scripts/python/__pycache__/croshell.cpython-313.pyc +0 -0
  103. machineconfig/scripts/python/__pycache__/devops.cpython-313.pyc +0 -0
  104. machineconfig/scripts/python/__pycache__/devops_devapps_install.cpython-313.pyc +0 -0
  105. machineconfig/scripts/python/__pycache__/devops_update_repos.cpython-313.pyc +0 -0
  106. machineconfig/scripts/python/__pycache__/fire_jobs.cpython-313.pyc +0 -0
  107. machineconfig/scripts/python/ai/__pycache__/__init__.cpython-313.pyc +0 -0
  108. machineconfig/scripts/python/ai/__pycache__/generate_files.cpython-313.pyc +0 -0
  109. machineconfig/scripts/python/ai/__pycache__/mcinit.cpython-313.pyc +0 -0
  110. machineconfig/scripts/python/helpers/__pycache__/__init__.cpython-313.pyc +0 -0
  111. machineconfig/scripts/python/helpers/__pycache__/helpers4.cpython-313.pyc +0 -0
  112. machineconfig/setup_linux/web_shortcuts/all.sh +0 -48
  113. machineconfig/setup_linux/web_shortcuts/update_system.sh +0 -48
  114. machineconfig/utils/utils.py +0 -97
  115. /machineconfig/cluster/{cloud_manager.py → remote/cloud_manager.py} +0 -0
  116. /machineconfig/cluster/{data_transfer.py → remote/data_transfer.py} +0 -0
  117. /machineconfig/cluster/{distribute.py → remote/distribute.py} +0 -0
  118. /machineconfig/cluster/{file_manager.py → remote/file_manager.py} +0 -0
  119. /machineconfig/cluster/{job_params.py → remote/job_params.py} +0 -0
  120. /machineconfig/cluster/{loader_runner.py → remote/loader_runner.py} +0 -0
  121. /machineconfig/cluster/{remote_machine.py → remote/remote_machine.py} +0 -0
  122. /machineconfig/cluster/{script_execution.py → remote/script_execution.py} +0 -0
  123. /machineconfig/cluster/{script_notify_upon_completion.py → remote/script_notify_upon_completion.py} +0 -0
  124. /machineconfig/{cluster/sessions_managers/archive/__init__.py → scripts/python/fire_jobs_streamlit_helper.py} +0 -0
  125. /machineconfig/setup_linux/web_shortcuts/{tmp.sh → android.sh} +0 -0
  126. {machineconfig-2.1.dist-info → machineconfig-2.3.dist-info}/WHEEL +0 -0
  127. {machineconfig-2.1.dist-info → machineconfig-2.3.dist-info}/top_level.txt +0 -0
@@ -5,6 +5,7 @@ import rich.console
5
5
  from machineconfig.utils.terminal import Terminal, Response, MACHINE
6
6
  from machineconfig.utils.path_reduced import PathExtended, PLike, OPLike
7
7
  from machineconfig.utils.utils2 import pprint
8
+ # from machineconfig.utils.ve import get_ve_activate_line
8
9
 
9
10
 
10
11
  @dataclass
@@ -95,7 +96,6 @@ class SSH: # inferior alternative: https://github.com/fabric/fabric
95
96
  self.ssh.load_system_host_keys()
96
97
  self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
97
98
  pprint(dict(host=self.host, hostname=self.hostname, username=self.username, password="***", port=self.port, key_filename=self.sshkey, ve=self.ve), title="SSHing To")
98
-
99
99
  sock = paramiko.ProxyCommand(self.proxycommand) if self.proxycommand is not None else None
100
100
  try:
101
101
  if pwd is None:
@@ -109,7 +109,6 @@ class SSH: # inferior alternative: https://github.com/fabric/fabric
109
109
  rich.console.Console().print_exception()
110
110
  self.pwd = getpass.getpass(f"Enter password for {self.username}@{self.hostname}: ")
111
111
  self.ssh.connect(hostname=self.hostname, username=self.username, password=self.pwd, port=self.port, key_filename=self.sshkey, compress=self.compress, sock=sock, allow_agent=False, look_for_keys=False) # type: ignore
112
-
113
112
  try:
114
113
  self.sftp: Optional[paramiko.SFTPClient] = self.ssh.open_sftp()
115
114
  except Exception as err:
@@ -129,14 +128,6 @@ class SSH: # inferior alternative: https://github.com/fabric/fabric
129
128
  self._remote_machine: Optional[MACHINE] = None
130
129
  self.terminal_responses: list[Response] = []
131
130
  self.platform = platform
132
- self.remote_env_cmd = rf"""~/code/machineconfig/{self.ve}/Scripts/Activate.ps1""" if self.get_remote_machine() == "Windows" else rf"""source ~/code/machineconfig/{self.ve}/bin/activate"""
133
- self.local_env_cmd = rf"""~/code/machineconfig/{self.ve}/Scripts/Activate.ps1""" if self.platform.system() == "Windows" else rf"""source ~/code/machineconfig/{self.ve}/bin/activate""" # works for both cmd and pwsh
134
-
135
- def __getstate__(self):
136
- return {attr: self.__getattribute__(attr) for attr in ["username", "hostname", "host", "port", "sshkey", "compress", "pwd", "ve"]}
137
-
138
- def __setstate__(self, state: dict[str, Any]):
139
- SSH(**state)
140
131
 
141
132
  def get_remote_machine(self) -> MACHINE:
142
133
  if self._remote_machine is None:
@@ -158,8 +149,8 @@ class SSH: # inferior alternative: https://github.com/fabric/fabric
158
149
 
159
150
  def get_remote_distro(self):
160
151
  if self._remote_distro is None:
161
- self._remote_distro = self.run_py("print(install_n_import('distro').name(pretty=True))", verbose=False).op_if_successfull_or_default() or ""
162
- # q.run("""~/.local/bin/uv run --with distro python -c "import distro; print(distro.name(pretty=True))" """)
152
+ res = self.run("""~/.local/bin/uv run --with distro python -c "import distro; print(distro.name(pretty=True))" """)
153
+ self._remote_distro = res.op_if_successfull_or_default() or ""
163
154
  return self._remote_distro
164
155
 
165
156
  def restart_computer(self):
@@ -196,9 +187,7 @@ class SSH: # inferior alternative: https://github.com/fabric/fabric
196
187
  def get_ssh_conn_str(self, cmd: str = ""):
197
188
  return "ssh " + (f" -i {self.sshkey}" if self.sshkey else "") + self.get_remote_repr().replace(":", " -p ") + (f" -t {cmd} " if cmd != "" else " ")
198
189
 
199
- # def open_console(self, cmd: str = '', new_window: bool = True, terminal: Optional[str] = None, shell: str = "pwsh"): Terminal().run_async(*(self.get_ssh_conn_str(cmd=cmd).split(" ")), new_window=new_window, terminal=terminal, shell=shell)
200
- def run(self, cmd: str, verbose: bool = True, desc: str = "", strict_err: bool = False, strict_returncode: bool = False, env_prefix: bool = False) -> Response: # most central method.
201
- cmd = (self.remote_env_cmd + "; " + cmd) if env_prefix else cmd
190
+ def run(self, cmd: str, verbose: bool = True, desc: str = "", strict_err: bool = False, strict_returncode: bool = False) -> Response:
202
191
  raw = self.ssh.exec_command(cmd)
203
192
  res = Response(stdin=raw[0], stdout=raw[1], stderr=raw[2], cmd=cmd, desc=desc) # type: ignore
204
193
  if not verbose:
@@ -212,15 +201,14 @@ class SSH: # inferior alternative: https://github.com/fabric/fabric
212
201
  assert '"' not in cmd, 'Avoid using `"` in your command. I dont know how to handle this when passing is as command to python in pwsh command.'
213
202
  if not return_obj:
214
203
  return self.run(
215
- cmd=f"""{self.remote_env_cmd}; python -c "{Terminal.get_header(wdir=None, toolbox=True)}{cmd}\n""" + '"', desc=desc or f"run_py on {self.get_remote_repr()}", verbose=verbose, strict_err=strict_err, strict_returncode=strict_returncode
204
+ cmd=f"""uv run --with machineconfig -c "{Terminal.get_header(wdir=None, toolbox=True)}{cmd}\n""" + '"', desc=desc or f"run_py on {self.get_remote_repr()}", verbose=verbose, strict_err=strict_err, strict_returncode=strict_returncode
216
205
  )
217
206
  assert "obj=" in cmd, "The command sent to run_py must have `obj=` statement if return_obj is set to True"
218
207
  source_file = self.run_py(f"""{cmd}\npath = Save.pickle(obj=obj, path=P.tmpfile(suffix='.pkl'))\nprint(path)""", desc=desc, verbose=verbose, strict_err=True, strict_returncode=True).op.split("\n")[-1]
219
208
  res = self.copy_to_here(source=source_file, target=PathExtended.tmpfile(suffix=".pkl"))
220
209
  import pickle
221
210
 
222
- res_bytes = res.read_bytes()
223
- return pickle.loads(res_bytes)
211
+ return pickle.loads(res.read_bytes())
224
212
 
225
213
  def copy_from_here(self, source: PLike, target: OPLike = None, z: bool = False, r: bool = False, overwrite: bool = False, init: bool = True) -> Union[PathExtended, list[PathExtended]]:
226
214
  if init:
@@ -239,7 +227,9 @@ class SSH: # inferior alternative: https://github.com/fabric/fabric
239
227
  source_list: list[PathExtended] = source_obj.search("*", folders=False, files=True, r=True)
240
228
  remote_root = (
241
229
  self.run_py(
242
- f"path=P(r'{PathExtended(target).as_posix()}').expanduser()\n{'path.delete(sure=True)' if overwrite else ''}\nprint(path.create())", desc=f"Creating Target directory `{PathExtended(target).as_posix()}` @ {self.get_remote_repr()}", verbose=False
230
+ f"path=P(r'{PathExtended(target).as_posix()}').expanduser()\n{'path.delete(sure=True)' if overwrite else ''}\nprint(path.create())",
231
+ desc=f"Creating Target directory `{PathExtended(target).as_posix()}` @ {self.get_remote_repr()}",
232
+ verbose=False,
243
233
  ).op
244
234
  or ""
245
235
  )
@@ -143,7 +143,8 @@ class Terminal:
143
143
  def run_script(self, script: str, shell: SHELLS = "default", verbose: bool = False):
144
144
  if self.machine == "Linux":
145
145
  script = "#!/bin/bash" + "\n" + script # `source` is only available in bash.
146
- script_file = PathExtended.tmpfile(name="tmp_shell_script", suffix=".ps1" if self.machine == "Windows" else ".sh", folder="tmp_scripts").write_text(script, newline={"Windows": None, "Linux": "\n"}[self.machine])
146
+ script_file = PathExtended.tmpfile(name="tmp_shell_script", suffix=".ps1" if self.machine == "Windows" else ".sh", folder="tmp_scripts")
147
+ script_file.write_text(script, newline={"Windows": None, "Linux": "\n"}[self.machine])
147
148
  if shell == "default":
148
149
  if self.machine == "Windows":
149
150
  start_cmd = "powershell" # default shell on Windows is cmd which is not very useful. (./source is not available)
@@ -188,7 +189,8 @@ class Terminal:
188
189
  {f"cd {wdir}" if wdir is not None else ""}
189
190
  {"ipython" if ipython else "python"} {"-i" if interactive else ""} {py_script}
190
191
  """
191
- shell_script = PathExtended.tmpfile(name="tmp_shell_script", suffix=".sh" if self.machine == "Linux" else ".ps1", folder="tmp_scripts/shell").write_text(shell_script)
192
+ shell_path = PathExtended.tmpfile(name="tmp_shell_script", suffix=".sh" if self.machine == "Linux" else ".ps1", folder="tmp_scripts/shell")
193
+ shell_path.write_text(shell_script)
192
194
  if shell is None and self.machine == "Windows":
193
195
  shell = "pwsh"
194
196
  window = "start" if new_window and self.machine == "Windows" else ""
@@ -0,0 +1,91 @@
1
+ """
2
+ Generate uv add commands from pyproject.toml dependency groups.
3
+ """
4
+
5
+ from pathlib import Path
6
+ import tomllib
7
+ from typing import Any
8
+
9
+
10
+ def generate_uv_add_commands(pyproject_path: Path, output_path: Path) -> None:
11
+ """
12
+ Generate uv add commands for each dependency group in pyproject.toml.
13
+
14
+ Args:
15
+ pyproject_path: Path to the pyproject.toml file
16
+ output_path: Path where to write the uv add commands
17
+ """
18
+ # Read pyproject.toml
19
+ with open(pyproject_path, "rb") as f:
20
+ pyproject_data: dict[str, Any] = tomllib.load(f)
21
+
22
+ commands: list[str] = []
23
+
24
+ # Handle main dependencies (no group)
25
+ if "project" in pyproject_data and "dependencies" in pyproject_data["project"]:
26
+ main_deps = pyproject_data["project"]["dependencies"]
27
+ if main_deps:
28
+ # Extract package names without version constraints
29
+ package_names = [extract_package_name(dep) for dep in main_deps]
30
+ commands.append(f"uv add {' '.join(package_names)}")
31
+
32
+ # Handle optional dependencies as groups
33
+ if "project" in pyproject_data and "optional-dependencies" in pyproject_data["project"]:
34
+ optional_deps = pyproject_data["project"]["optional-dependencies"]
35
+ for group_name, deps in optional_deps.items():
36
+ if deps:
37
+ package_names = [extract_package_name(dep) for dep in deps]
38
+ commands.append(f"uv add {' '.join(package_names)} --group {group_name}")
39
+
40
+ # Handle dependency-groups (like dev)
41
+ if "dependency-groups" in pyproject_data:
42
+ dep_groups = pyproject_data["dependency-groups"]
43
+ for group_name, deps in dep_groups.items():
44
+ if deps:
45
+ package_names = [extract_package_name(dep) for dep in deps]
46
+ if group_name == "dev":
47
+ commands.append(f"uv add {' '.join(package_names)} --dev")
48
+ else:
49
+ commands.append(f"uv add {' '.join(package_names)} --group {group_name}")
50
+
51
+ # Write commands to output file
52
+ with open(output_path, "w") as f:
53
+ for command in commands:
54
+ f.write(command + "\n")
55
+
56
+ print(f"Generated {len(commands)} uv add commands in {output_path}")
57
+
58
+
59
+ def extract_package_name(dependency_spec: str) -> str:
60
+ """
61
+ Extract package name from dependency specification.
62
+
63
+ Examples:
64
+ "rich>=14.0.0" -> "rich"
65
+ "requests>=2.32.5" -> "requests"
66
+ "pywin32" -> "pywin32"
67
+ "package[extra]>=1.0" -> "package"
68
+ """
69
+ # Handle extras like "package[extra]>=1.0" first
70
+ if "[" in dependency_spec:
71
+ dependency_spec = dependency_spec.split("[")[0].strip()
72
+
73
+ # Split on common version operators and take the first part
74
+ for operator in [">=", "<=", "==", "!=", ">", "<", "~=", "===", "@"]:
75
+ if operator in dependency_spec:
76
+ return dependency_spec.split(operator)[0].strip()
77
+
78
+ # Return as-is if no version constraint found
79
+ return dependency_spec.strip()
80
+
81
+
82
+ if __name__ == "__main__":
83
+ # Example usage
84
+ current_dir = Path.cwd()
85
+ pyproject_file = current_dir / "pyproject.toml"
86
+ output_file = current_dir / "uv_add_commands.txt"
87
+
88
+ if pyproject_file.exists():
89
+ generate_uv_add_commands(pyproject_file, output_file)
90
+ else:
91
+ print(f"pyproject.toml not found at {pyproject_file}")
@@ -1,7 +1,5 @@
1
1
  from pathlib import Path
2
2
  from typing import Optional, Any
3
- # import time
4
- # from typing import Callable, Literal, TypeVar, ParamSpec
5
3
 
6
4
 
7
5
  def randstr(length: int = 10, lower: bool = True, upper: bool = True, digits: bool = True, punctuation: bool = False, safe: bool = False, noun: bool = False) -> str:
@@ -51,6 +49,7 @@ def read_toml(path: "Path"):
51
49
 
52
50
  def pprint(obj: dict[Any, Any], title: str) -> None:
53
51
  from rich import inspect
52
+
54
53
  inspect(type("TempStruct", (object,), obj)(), value=False, title=title, docs=False, dunder=False, sort=False)
55
54
 
56
55
 
@@ -149,20 +149,25 @@ T2 = TypeVar("T2")
149
149
  class PrintFunc(Protocol):
150
150
  def __call__(self, msg: str) -> Union[NoReturn, None]: ...
151
151
 
152
+
152
153
  def to_pickle(obj: Any, path: Path) -> None:
153
154
  import pickle
155
+
154
156
  path.parent.mkdir(parents=True, exist_ok=True)
155
157
  path.write_bytes(pickle.dumps(obj))
158
+
159
+
156
160
  def from_pickle(path: Path) -> Any:
157
161
  import pickle
162
+
158
163
  return pickle.loads(path.read_bytes())
159
164
 
160
165
 
161
166
  class Cache(Generic[T]): # This class helps to accelrate access to latest data coming from expensive function. The class has two flavours, memory-based and disk-based variants."""
162
167
  # source_func: Callable[[], T]
163
- def __init__(self, source_func: Callable[[], T],
164
- expire: timedelta, logger: Optional[PrintFunc] = None, path: Optional[Path] = None,
165
- saver: Callable[[T, Path], Any] = to_pickle, reader: Callable[[Path], T] = from_pickle, name: Optional[str] = None) -> None:
168
+ def __init__(
169
+ self, source_func: Callable[[], T], expire: timedelta, logger: Optional[PrintFunc] = None, path: Optional[Path] = None, saver: Callable[[T, Path], Any] = to_pickle, reader: Callable[[Path], T] = from_pickle, name: Optional[str] = None
170
+ ) -> None:
166
171
  self.cache: T
167
172
  self.source_func = source_func # function which when called returns a fresh object to be frozen.
168
173
  self.path: Optional[PathExtended] = PathExtended(path) if path is not None else None # if path is passed, it will function as disk-based flavour.
@@ -173,12 +178,14 @@ class Cache(Generic[T]): # This class helps to accelrate access to latest data
173
178
  self.expire = expire
174
179
  self.name = name if isinstance(name, str) else str(self.source_func)
175
180
  self.last_call_is_fresh = False
181
+
176
182
  @property
177
183
  def age(self):
178
184
  """Throws AttributeError if called before cache is populated and path doesn't exists"""
179
185
  if self.path is None: # memory-based cache.
180
186
  return datetime.now() - self.time_produced
181
187
  return datetime.now() - datetime.fromtimestamp(self.path.stat().st_mtime)
188
+
182
189
  # def __setstate__(self, state: dict[str, Any]) -> None:
183
190
  # self.__dict__.update(state)
184
191
  # self.path = P.home() / self.path if self.path is not None else self.path
@@ -228,9 +235,11 @@ class Cache(Generic[T]): # This class helps to accelrate access to latest data
228
235
  self.cache = self.source_func() # fresh data.
229
236
  self.last_call_is_fresh = True
230
237
  self.time_produced = datetime.now()
231
- if self.path is not None: self.save(self.cache, self.path)
238
+ if self.path is not None:
239
+ self.save(self.cache, self.path)
232
240
  else: # cache exists
233
- try: age = self.age
241
+ try:
242
+ age = self.age
234
243
  except AttributeError: # path doesn't exist (may be deleted) ==> need to repopulate cache form source_func.
235
244
  return self(fresh=True)
236
245
  if age > self.expire:
@@ -243,7 +252,8 @@ class Cache(Generic[T]): # This class helps to accelrate access to latest data
243
252
  self.cache = self.source_func()
244
253
  self.last_call_is_fresh = True
245
254
  self.time_produced = datetime.now()
246
- if self.path is not None: self.save(self.cache, self.path)
255
+ if self.path is not None:
256
+ self.save(self.cache, self.path)
247
257
  else:
248
258
  if self.logger:
249
259
  self.logger(f"""
@@ -252,15 +262,17 @@ class Cache(Generic[T]): # This class helps to accelrate access to latest data
252
262
  ⏱️ Lag = {age}
253
263
  ════════════════════════════════════════════════════════""")
254
264
  return self.cache
265
+
255
266
  @staticmethod
256
- def as_decorator(expire: timedelta, logger: Optional[PrintFunc] = None, path: Optional[Path] = None,
257
- saver: Callable[[T2, Path], Any] = to_pickle,
258
- reader: Callable[[Path], T2] = from_pickle,
259
- name: Optional[str] = None): # -> Callable[..., 'Cache[T2]']:
260
- def decorator(source_func: Callable[[], T2]) -> Cache['T2']:
267
+ def as_decorator(
268
+ expire: timedelta, logger: Optional[PrintFunc] = None, path: Optional[Path] = None, saver: Callable[[T2, Path], Any] = to_pickle, reader: Callable[[Path], T2] = from_pickle, name: Optional[str] = None
269
+ ): # -> Callable[..., 'Cache[T2]']:
270
+ def decorator(source_func: Callable[[], T2]) -> Cache["T2"]:
261
271
  res = Cache(source_func=source_func, expire=expire, logger=logger, path=path, name=name, reader=reader, saver=saver)
262
272
  return res
273
+
263
274
  return decorator
275
+
264
276
  def from_cloud(self, cloud: str, rel2home: bool = True, root: Optional[str] = None):
265
277
  assert self.path is not None
266
278
  exists = self.path.exists()
machineconfig/utils/ve.py CHANGED
@@ -13,7 +13,10 @@ def get_ve_path_and_ipython_profile(init_path: PathExtended) -> tuple[Optional[s
13
13
  if tmp.joinpath(".ve.ini").exists():
14
14
  ini = read_ini(tmp.joinpath(".ve.ini"))
15
15
  if ve_path is None:
16
- ve_path = ini["specs"]["ve_path"]
16
+ try:
17
+ ve_path = ini["specs"]["ve_path"]
18
+ except KeyError:
19
+ raise KeyError(f".ve.ini file at {tmp.joinpath('.ve.ini')} is missing the 've_path' key in the 'specs' section.")
17
20
  print(f"🐍 Using Virtual Environment: {ve_path}. This is based on this file {tmp.joinpath('.ve.ini')}")
18
21
  if ipy_profile is None:
19
22
  ipy_profile = ini["specs"]["ipy_profile"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: machineconfig
3
- Version: 2.1
3
+ Version: 2.3
4
4
  Summary: Dotfiles management package
5
5
  Author-email: Alex Al-Saffar <programmer@usa.com>
6
6
  License: Apache 2.0
@@ -11,26 +11,26 @@ Classifier: License :: OSI Approved :: Apache Software License
11
11
  Classifier: Operating System :: OS Independent
12
12
  Requires-Python: >=3.13
13
13
  Description-Content-Type: text/markdown
14
- Requires-Dist: rich>=14.0.0
14
+ Requires-Dist: cryptography>=44.0.2
15
+ Requires-Dist: fire>=0.7.0
16
+ Requires-Dist: gitpython>=3.1.44
17
+ Requires-Dist: joblib>=1.5.2
18
+ Requires-Dist: markdown>=3.9
15
19
  Requires-Dist: paramiko>=3.5.1
16
20
  Requires-Dist: psutil>=7.0.0
17
- Requires-Dist: fire>=0.7.0
18
21
  Requires-Dist: pydantic>=2.11.3
19
- Requires-Dist: gitpython>=3.1.44
20
22
  Requires-Dist: pyfzf>=0.3.1
21
- Requires-Dist: rclone-python>=0.1.23
23
+ Requires-Dist: pyjson5>=1.6.9
22
24
  Requires-Dist: pytz>=2025.2
23
- Requires-Dist: tomli>=2.2.1
24
- Requires-Dist: toml>=0.10.2
25
25
  Requires-Dist: pyyaml>=6.0.2
26
- Requires-Dist: pyjson5>=1.6.9
27
- Requires-Dist: requests>=2.32.5
28
- Requires-Dist: tqdm>=4.67.1
29
- Requires-Dist: joblib>=1.5.2
30
26
  Requires-Dist: randomname>=0.2.1
31
- Requires-Dist: cryptography>=44.0.2
27
+ Requires-Dist: rclone-python>=0.1.23
28
+ Requires-Dist: requests>=2.32.5
29
+ Requires-Dist: rich>=14.0.0
32
30
  Requires-Dist: tenacity>=9.1.2
33
- Requires-Dist: markdown>=3.9
31
+ Requires-Dist: toml>=0.10.2
32
+ Requires-Dist: tomli>=2.2.1
33
+ Requires-Dist: tqdm>=4.67.1
34
34
  Provides-Extra: windows
35
35
  Requires-Dist: pywin32; extra == "windows"
36
36
  Provides-Extra: docs