machineconfig 1.9__py3-none-any.whl → 1.92__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 (55) hide show
  1. machineconfig/__init__.py +2 -4
  2. machineconfig/jobs/python/check_installations.py +14 -6
  3. machineconfig/jobs/python/checkout_version.py +27 -32
  4. machineconfig/jobs/python/create_bootable_media.py +1 -1
  5. machineconfig/jobs/python/python_cargo_build_share.py +2 -2
  6. machineconfig/jobs/python/tasks.py +2 -2
  7. machineconfig/jobs/python_custom_installers/{helix.py → hx.py} +21 -6
  8. machineconfig/profile/create.py +23 -21
  9. machineconfig/profile/create_hardlinks.py +101 -0
  10. machineconfig/profile/shell.py +10 -7
  11. machineconfig/scripts/python/cloud_copy.py +19 -16
  12. machineconfig/scripts/python/cloud_repo_sync.py +94 -47
  13. machineconfig/scripts/python/cloud_sync.py +78 -61
  14. machineconfig/scripts/python/croshell.py +6 -6
  15. machineconfig/scripts/python/devops.py +22 -22
  16. machineconfig/scripts/python/devops_add_ssh_key.py +1 -1
  17. machineconfig/scripts/python/devops_backup_retrieve.py +19 -10
  18. machineconfig/scripts/python/devops_devapps_install.py +80 -62
  19. machineconfig/scripts/python/devops_update_repos.py +5 -5
  20. machineconfig/scripts/python/dotfile.py +4 -4
  21. machineconfig/scripts/python/fire_jobs.py +115 -63
  22. machineconfig/scripts/python/gh_models.py +55 -0
  23. machineconfig/scripts/python/mount_nfs.py +1 -1
  24. machineconfig/scripts/python/mount_nw_drive.py +3 -3
  25. machineconfig/scripts/python/mount_ssh.py +2 -3
  26. machineconfig/scripts/python/pomodoro.py +1 -1
  27. machineconfig/scripts/python/repos.py +22 -23
  28. machineconfig/scripts/python/scheduler.py +1 -1
  29. machineconfig/scripts/python/start_slidev.py +12 -6
  30. machineconfig/scripts/python/start_terminals.py +5 -4
  31. machineconfig/scripts/python/wifi_conn.py +34 -42
  32. machineconfig/scripts/python/wsl_windows_transfer.py +1 -1
  33. machineconfig/settings/__init__.py +0 -0
  34. machineconfig/setup_windows/wt_and_pwsh/set_pwsh_theme.py +1 -1
  35. machineconfig/setup_windows/wt_and_pwsh/set_wt_settings.py +3 -2
  36. machineconfig/utils/installer.py +86 -41
  37. machineconfig/utils/procs.py +2 -2
  38. machineconfig/utils/scheduling.py +3 -3
  39. machineconfig/utils/utils.py +136 -56
  40. machineconfig/utils/ve.py +145 -95
  41. {machineconfig-1.9.dist-info → machineconfig-1.92.dist-info}/METADATA +160 -155
  42. machineconfig-1.92.dist-info/RECORD +70 -0
  43. machineconfig/jobs/python_custom_installers/azuredatastudio.py +0 -36
  44. machineconfig/jobs/python_custom_installers/bypass_paywall.py +0 -30
  45. machineconfig/jobs/python_custom_installers/docker_desktop.py +0 -52
  46. machineconfig/jobs/python_custom_installers/goes.py +0 -35
  47. machineconfig/jobs/python_custom_installers/lvim.py +0 -48
  48. machineconfig/jobs/python_custom_installers/ngrok.py +0 -39
  49. machineconfig/jobs/python_custom_installers/nvim.py +0 -48
  50. machineconfig/jobs/python_custom_installers/vscode.py +0 -45
  51. machineconfig/jobs/python_custom_installers/wezterm.py +0 -41
  52. machineconfig-1.9.dist-info/RECORD +0 -76
  53. {machineconfig-1.9.dist-info → machineconfig-1.92.dist-info}/LICENSE +0 -0
  54. {machineconfig-1.9.dist-info → machineconfig-1.92.dist-info}/WHEEL +0 -0
  55. {machineconfig-1.9.dist-info → machineconfig-1.92.dist-info}/top_level.txt +0 -0
@@ -62,7 +62,7 @@ class Report:
62
62
  status: str
63
63
 
64
64
  @classmethod
65
- def from_path(cls, path: P, return_default_if_not_found: bool = False):
65
+ def from_path(cls, path: P, return_default_if_not_found: bool=False):
66
66
  if not path.exists():
67
67
  if return_default_if_not_found:
68
68
  return Report(name=path.parent.name, start=datetime(year=2000, month=1, day=1), end=datetime(year=2000, month=1, day=1), status="NA")
@@ -108,7 +108,7 @@ def read_task_from_dir(path: P):
108
108
  return task
109
109
 
110
110
 
111
- def main(root: Optional[str] = None, ignore_conditions: bool = True):
111
+ def main(root: Optional[str] = None, ignore_conditions: bool=True):
112
112
  if root is None: root_resolved = SCHEDULER_DEFAULT_ROOT
113
113
  else: root_resolved = P(root).expanduser().absolute()
114
114
  tasks_dirs = root_resolved.search(files=False, folders=True).filter(lambda x: x.joinpath("task.py").exists())
@@ -168,7 +168,7 @@ def should_task_run(task: Task, tolerance_mins: int = 1440) -> tuple[bool, Optio
168
168
  def run_task(task: Task) -> Report:
169
169
  start_time = datetime.now()
170
170
 
171
- shell_script = get_shell_script_executing_python_file(python_file=task.task_root.joinpath("task.py").str, ve_name=task.venv)
171
+ shell_script = get_shell_script_executing_python_file(python_file=task.task_root.joinpath("task.py").to_str(), ve_name=task.venv)
172
172
  shell_script_root = P.tmp().joinpath(f"tmp_scripts/scheduler/{task.name}").create()
173
173
  try:
174
174
  if platform.system() == 'Windows':
@@ -3,9 +3,9 @@
3
3
  Utils
4
4
  """
5
5
 
6
- from crocodile.file_management import P, randstr
6
+ from crocodile.core import List as L
7
+ from crocodile.file_management import P, randstr, PLike
7
8
  from crocodile.meta import Terminal
8
- from crocodile.core import install_n_import
9
9
  # import crocodile.environment as env
10
10
  import machineconfig
11
11
  from rich.text import Text
@@ -17,13 +17,12 @@ import subprocess
17
17
  from typing import Optional, Union, TypeVar, Iterable
18
18
 
19
19
 
20
- LIBRARY_ROOT = P(machineconfig.__file__).resolve().parent # .replace(P.home().str.lower(), P.home().str)
20
+ LIBRARY_ROOT = P(machineconfig.__file__).resolve().parent # .replace(P.home().to_str().lower(), P.home().str)
21
21
  REPO_ROOT = LIBRARY_ROOT.parent.parent
22
- PROGRAM_PATH = (P.tmp().joinpath("shells/python_return_command") + (".ps1" if platform.system() == "Windows" else ".sh")).create(parents_only=True)
22
+ PROGRAM_PATH = (P.home().joinpath("tmp_results", "shells", "python_return_command") + (".ps1" if platform.system() == "Windows" else ".sh"))
23
23
  CONFIG_PATH = P.home().joinpath(".config/machineconfig")
24
24
  INSTALL_VERSION_ROOT = CONFIG_PATH.joinpath("cli_tools_installers/versions")
25
- INSTALL_TMP_DIR = P.tmp(folder="tmp_installers")
26
-
25
+ INSTALL_TMP_DIR = P.home().joinpath("tmp_results", "tmp_installers")
27
26
  DEFAULTS_PATH = P.home().joinpath("dotfiles/machineconfig/defaults.ini")
28
27
 
29
28
 
@@ -31,8 +30,7 @@ T = TypeVar("T")
31
30
 
32
31
 
33
32
  def choose_cloud_interactively() -> str:
34
- from crocodile.core import List as L
35
- print(f"Listing Remotes ... ")
33
+ print("Listing Remotes ... ")
36
34
  tmp = Terminal().run("rclone listremotes").op_if_successfull_or_default(strict_returcode=False)
37
35
  # consider this: remotes = Read.ini(P.home().joinpath(".config/rclone/rclone.conf")).sections()
38
36
  if isinstance(tmp, str):
@@ -40,8 +38,8 @@ def choose_cloud_interactively() -> str:
40
38
 
41
39
  else: raise ValueError(f"Got {tmp} from rclone listremotes")
42
40
  if len(remotes) == 0:
43
- raise RuntimeError(f"You don't have remotes. Configure your rclone first to get cloud services access.")
44
- cloud: str = choose_one_option(msg="WHICH CLOUD?", options=list(remotes), default=remotes[0], fzf=True)
41
+ raise RuntimeError("You don't have remotes. Configure your rclone first to get cloud services access.")
42
+ cloud: str=choose_one_option(msg="WHICH CLOUD?", options=list(remotes), default=remotes[0], fzf=True)
45
43
  return cloud
46
44
 
47
45
 
@@ -71,63 +69,78 @@ def sanitize_path(a_path: P) -> P:
71
69
 
72
70
  def match_file_name(sub_string: str, search_root: Optional[P] = None) -> P:
73
71
  """Look up current directory for file name that matches the passed substring."""
74
- root = search_root if search_root is not None else P.cwd()
75
- print(f"Searching for {sub_string} in {root}")
76
- search_results = root.absolute().search(f"*{sub_string}*.py", r=True)
72
+ search_root_obj = search_root if search_root is not None else P.cwd()
73
+ search_root_obj = search_root_obj.absolute()
74
+ print(f"Searching for {sub_string} in {search_root_obj}")
75
+
76
+ search_root_objects = search_root_obj.search("*", not_in=["links", ".venv", ".git", ".idea", ".vscode", "node_modules", "__pycache__"])
77
+ search_results: L[P] = L([a_search_root_obj.search(f"*{sub_string}*", r=True) for a_search_root_obj in search_root_objects]).reduce(lambda x, y: x + y) # type: ignore
78
+ search_results = search_results.filter(lambda x: x.suffix in (".py", ".sh", ".ps1"))
79
+
77
80
  if len(search_results) == 1:
78
81
  path_obj = search_results.list[0]
79
82
  elif len(search_results) > 1:
80
- choice = choose_one_option(msg=f"Search results are ambiguous or non-existent", options=search_results.list, fzf=True)
83
+ msg = "Search results are ambiguous or non-existent, choose manually:"
84
+ print(msg)
85
+ choice = choose_one_option(msg=msg, options=search_results.list, fzf=True)
81
86
  path_obj = P(choice)
82
87
  else:
83
88
  # let's do a final retry with sub_string.small()
84
89
  sub_string_small = sub_string.lower()
85
90
  if sub_string_small != sub_string:
91
+ print("Retrying with small letters")
86
92
  return match_file_name(sub_string=sub_string_small)
87
93
  from git.repo import Repo
88
94
  from git.exc import InvalidGitRepositoryError
89
95
  try:
90
- repo = Repo(root, search_parent_directories=True)
96
+ repo = Repo(search_root_obj, search_parent_directories=True)
91
97
  repo_root_dir = P(repo.working_dir)
92
- if repo_root_dir != root: # may be user is in a subdirectory of the repo root, try with root dir.
98
+ if repo_root_dir != search_root_obj: # may be user is in a subdirectory of the repo root, try with root dir.
99
+ print("Retrying with root repo instea of cwd")
93
100
  return match_file_name(sub_string=sub_string, search_root=repo_root_dir)
94
101
  else:
95
- root = repo_root_dir
102
+ search_root_obj = repo_root_dir
96
103
  except InvalidGitRepositoryError:
97
104
  pass
98
105
 
99
- if check_tool_exists("fzf"):
106
+ if check_tool_exists(tool_name="fzf"):
100
107
  try:
101
- search_res = subprocess.run(f"cd '{root}'; fzf --filter={sub_string}", stdout=subprocess.PIPE, text=True, check=True, shell=True).stdout.split("\n")[:-1]
108
+ print(f"Using fd to searching for `{sub_string}` in `{search_root_obj}` ...")
109
+ fzf_cmd = f"cd '{search_root_obj}'; fd --type f --strip-cwd-prefix | fzf --filter={sub_string}"
110
+ search_res = subprocess.run(fzf_cmd, stdout=subprocess.PIPE, text=True, check=True, shell=True).stdout.split("\n")[:-1]
102
111
  except subprocess.CalledProcessError as cpe:
103
- print(f"Failed at fzf search with {sub_string} in {root}.\n{cpe}")
112
+ print(f"Failed at fzf search with {sub_string} in {search_root_obj}.\n{cpe}")
104
113
  msg = f"\n{'--' * 50}\n💥 Path {sub_string} does not exist. No search results\n{'--' * 50}\n"
105
114
  raise FileNotFoundError(msg) from cpe
106
- if len(search_res) == 1: return root.joinpath(search_res[0])
115
+
116
+ if len(search_res) == 1: return search_root_obj.joinpath(search_res[0])
107
117
  else:
118
+ print("Tring with raw fzf search ...")
108
119
  try:
109
- res = subprocess.run(f"cd '{root}'; fzf --query={sub_string}", check=True, stdout=subprocess.PIPE, text=True, shell=True).stdout.strip()
120
+ # res = subprocess.run(f"cd '{search_root_obj}'; fzf --query={sub_string}", check=True, stdout=subprocess.PIPE, text=True, shell=True).stdout.strip()
121
+ res = subprocess.run(f"cd '{search_root_obj}'; fd | fzf --query={sub_string}", check=True, stdout=subprocess.PIPE, text=True, shell=True).stdout.strip()
110
122
  except subprocess.CalledProcessError as cpe:
111
- print(f"Failed at fzf search with {sub_string} in {root}. {cpe}")
123
+ print(f"Failed at fzf search with {sub_string} in {search_root_obj}. {cpe}")
112
124
  msg = f"\n{'--' * 50}\n💥 Path {sub_string} does not exist. No search results\n{'--' * 50}\n"
113
125
  raise FileNotFoundError(msg) from cpe
114
- return root.joinpath(res)
126
+ return search_root_obj.joinpath(res)
127
+
115
128
  msg = f"\n{'--' * 50}\n💥 Path {sub_string} does not exist. No search results\n{'--' * 50}\n"
116
129
  raise FileNotFoundError(msg)
117
130
  print(f"\n{'--' * 50}\n🔗 Matched `{sub_string}` ➡️ `{path_obj}`\n{'--' * 50}\n")
118
131
  return path_obj
119
132
 
120
133
 
121
- def choose_one_option(options: Iterable[T], header: str = "", tail: str = "", prompt: str = "", msg: str = "",
122
- default: Optional[T] = None, fzf: bool = False, custom_input: bool = False) -> T:
134
+ def choose_one_option(options: Iterable[T], header: str="", tail: str="", prompt: str="", msg: str="",
135
+ default: Optional[T] = None, fzf: bool=False, custom_input: bool=False) -> T:
123
136
  choice_key = display_options(msg=msg, options=options, header=header, tail=tail, prompt=prompt,
124
137
  default=default, fzf=fzf, multi=False, custom_input=custom_input)
125
138
  assert not isinstance(choice_key, list)
126
139
  return choice_key
127
140
 
128
141
 
129
- def choose_multiple_options(options: Iterable[T], header: str = "", tail: str = "", prompt: str = "", msg: str = "",
130
- default: Optional[T] = None, custom_input: bool = False) -> list[T]:
142
+ def choose_multiple_options(options: Iterable[T], header: str="", tail: str="", prompt: str="", msg: str="",
143
+ default: Optional[T] = None, custom_input: bool=False) -> list[T]:
131
144
  choice_key = display_options(msg=msg, options=options, header=header, tail=tail, prompt=prompt,
132
145
  default=default, fzf=True, multi=True,
133
146
  custom_input=custom_input)
@@ -135,14 +148,14 @@ def choose_multiple_options(options: Iterable[T], header: str = "", tail: str =
135
148
  return [choice_key]
136
149
 
137
150
 
138
- def display_options(msg: str, options: Iterable[T], header: str = "", tail: str = "", prompt: str = "",
139
- default: Optional[T] = None, fzf: bool = False, multi: bool = False, custom_input: bool = False) -> Union[T, list[T]]:
140
- # TODO: replace with https://github.com/tmbo/questionary # also see https://github.com/charmbracelet/gum
151
+ def display_options(msg: str, options: Iterable[T], header: str="", tail: str="", prompt: str="",
152
+ default: Optional[T] = None, fzf: bool=False, multi: bool=False, custom_input: bool=False) -> Union[T, list[T]]:
153
+ # TODO: replace with https://github.com/tmbo/questionary
154
+ # # also see https://github.com/charmbracelet/gum
141
155
  tool_name = "fzf"
142
156
  options_strings: list[str] = [str(x) for x in options]
143
157
  default_string = str(default) if default is not None else None
144
158
  if fzf and check_tool_exists(tool_name):
145
- install_n_import("pyfzf")
146
159
  from pyfzf.pyfzf import FzfPrompt
147
160
  fzf_prompt = FzfPrompt()
148
161
  nl = "\n"
@@ -163,7 +176,7 @@ def display_options(msg: str, options: Iterable[T], header: str = "", tail: str
163
176
  console = Console()
164
177
  if default is not None:
165
178
  assert default in options, f"Default `{default}` option not in options `{list(options)}`"
166
- default_msg = Text(f" <<<<-------- DEFAULT", style="bold red")
179
+ default_msg = Text(" <<<<-------- DEFAULT", style="bold red")
167
180
  else: default_msg = Text("")
168
181
  txt = Text("\n" + msg + "\n")
169
182
  for idx, key in enumerate(options):
@@ -176,10 +189,10 @@ def display_options(msg: str, options: Iterable[T], header: str = "", tail: str
176
189
 
177
190
  if choice_string == "":
178
191
  if default_string is None:
179
- print(f"Default option not available!")
192
+ print("Default option not available!")
180
193
  return display_options(msg=msg, options=options, header=header, tail=tail, prompt=prompt, default=default, fzf=fzf, multi=multi, custom_input=custom_input)
181
194
  choice_idx = options_strings.index(default_string)
182
- assert default is not None, f"🧨 Default option not available!"
195
+ assert default is not None, "🧨 Default option not available!"
183
196
  choice_one: T = default
184
197
  else:
185
198
  try:
@@ -198,12 +211,12 @@ def display_options(msg: str, options: Iterable[T], header: str = "", tail: str
198
211
  elif custom_input:
199
212
  return choice_string # type: ignore #TODO: fix this
200
213
  else: raise ValueError(f"Unknown choice. {choice_string}") from te
201
- print(f"{choice_idx}: {choice_one}", f"<<<<-------- CHOICE MADE")
214
+ print(f"{choice_idx}: {choice_one}", "<<<<-------- CHOICE MADE")
202
215
  if multi: return [choice_one]
203
216
  return choice_one
204
217
 
205
218
 
206
- def symlink(this: P, to_this: P, prioritize_to_this: bool = True):
219
+ def symlink_func(this: P, to_this: P, prioritize_to_this: bool=True):
207
220
  """helper function. creates a symlink from `this` to `to_this`.
208
221
  What can go wrong?
209
222
  depending on this and to_this existence, one will be prioretized depending on overwrite value.
@@ -222,14 +235,32 @@ def symlink(this: P, to_this: P, prioritize_to_this: bool = True):
222
235
  else: this.move(path=to_this) # this exists, to_this doesn't, this is prioritized.
223
236
  else: # this doesn't exist.
224
237
  if not to_this.exists(): to_this.touch() # we have to touch it (file) or create it (folder)
225
- if platform.system() == "Windows": _ = install_n_import("win32api", "pywin32") # this is crucial for windows to pop up the concent window in case python was not run as admin.
226
238
  try:
227
239
  # print(f"Linking {this} ➡️ {to_this}")
228
240
  P(this).symlink_to(target=to_this, verbose=True, overwrite=True)
229
241
  except Exception as ex: print(f"Failed at linking {this} ➡️ {to_this}.\nReason: {ex}")
230
242
 
231
243
 
232
- def get_shell_script_executing_python_file(python_file: str, func: Optional[str] = None, ve_name: str = "ve", strict_execution: bool = True):
244
+ def symlink_copy(this: P, to_this: P, prioritize_to_this: bool=True):
245
+ this = P(this).expanduser().absolute()
246
+ to_this = P(to_this).expanduser().absolute()
247
+ if this.is_symlink(): this.delete(sure=True) # delete if it exists as symblic link, not a concrete path.
248
+ if this.exists(): # this is a problem. It will be resolved via `overwrite`
249
+ if prioritize_to_this is True: # it *can* be deleted, but let's look at target first.
250
+ if to_this.exists(): # this exists, to_this as well. to_this is prioritized.
251
+ this.append(f".orig_{randstr()}", inplace=True) # rename is better than deletion
252
+ else: this.move(path=to_this) # this exists, to_this doesn't. to_this is prioritized.
253
+ elif prioritize_to_this is False: # don't sacrefice this, sacrefice to_this.
254
+ if to_this.exists(): this.move(path=to_this, overwrite=True) # this exists, to_this as well, this is prioritized. # now we are readly to make the link
255
+ else: this.move(path=to_this) # this exists, to_this doesn't, this is prioritized.
256
+ else: # this doesn't exist.
257
+ if not to_this.exists(): to_this.touch() # we have to touch it (file) or create it (folder)
258
+ try:
259
+ to_this.copy(path=this, overwrite=True, verbose=True)
260
+ except Exception as ex: print(f"Failed at linking {this} ➡️ {to_this}.\nReason: {ex}")
261
+
262
+
263
+ def get_shell_script_executing_python_file(python_file: str, func: Optional[str] = None, ve_name: str="ve", strict_execution: bool=True):
233
264
  if func is None: exec_line = f"""python {python_file}"""
234
265
  else: exec_line = f"""python -m fire {python_file} {func}"""
235
266
  shell_script = f"""
@@ -247,7 +278,15 @@ deactivate || true
247
278
  return shell_script
248
279
 
249
280
 
250
- def get_shell_file_executing_python_script(python_script: str, ve_name: str = "ve", verbose: bool = True):
281
+ def get_shell_script(shell_script: str):
282
+ if platform.system() == "Linux": suffix = ".sh"
283
+ elif platform.system() == "Windows": suffix = ".ps1"
284
+ else: raise NotImplementedError(f"Platform {platform.system()} not implemented.")
285
+ shell_file = P.tmp().joinpath("tmp_scripts", "shell", randstr() + suffix).create(parents_only=True).write_text(shell_script)
286
+ return shell_file
287
+
288
+
289
+ def get_shell_file_executing_python_script(python_script: str, ve_name: str, verbose: bool=True):
251
290
  if verbose:
252
291
  python_script = f"""
253
292
  code = r'''{python_script}'''
@@ -257,15 +296,13 @@ try:
257
296
  except ImportError: print(code)
258
297
  """ + python_script
259
298
  python_file = P.tmp().joinpath("tmp_scripts", "python", randstr() + ".py").create(parents_only=True).write_text(python_script)
260
- shell_script = get_shell_script_executing_python_file(python_file=python_file.str, ve_name=ve_name)
261
- if platform.system() == "Linux": suffix = ".sh"
262
- elif platform.system() == "Windows": suffix = ".ps1"
263
- else: raise NotImplementedError(f"Platform {platform.system()} not implemented.")
264
- shell_file = P.tmp().joinpath("tmp_scripts", "shell", randstr() + suffix).create(parents_only=True).write_text(shell_script)
299
+ shell_script = get_shell_script_executing_python_file(python_file=python_file.to_str(), ve_name=ve_name)
300
+ shell_file = get_shell_script(shell_script)
265
301
  return shell_file
266
302
 
267
303
 
268
- def write_shell_script(program: str, desc: str = "", preserve_cwd: bool = True, display: bool = True, execute: bool = False):
304
+ # def write_shell_script(program: str, desc: str="", preserve_cwd: bool=True, display: bool=True, execute: bool=False):
305
+ def write_shell_script(program: str, desc: str, preserve_cwd: bool, display: bool, execute: bool):
269
306
  if preserve_cwd:
270
307
  if platform.system() == "Windows":
271
308
  program = "$orig_path = $pwd\n" + program + "\ncd $orig_path"
@@ -274,13 +311,13 @@ def write_shell_script(program: str, desc: str = "", preserve_cwd: bool = True,
274
311
  if display:
275
312
  print(f"Executing {PROGRAM_PATH}")
276
313
  print_code(code=program, lexer="shell", desc=desc)
277
- if platform.system() == 'Windows': PROGRAM_PATH.create(parents_only=True).write_text(program)
278
- else: PROGRAM_PATH.create(parents_only=True).write_text(f"{program}")
279
- if execute: Terminal().run(f". {PROGRAM_PATH}", shell="powershell").print_if_unsuccessful(desc="Executing shell script", strict_err=True, strict_returncode=True)
314
+ PROGRAM_PATH.create(parents_only=True).write_text(program)
315
+ if execute:
316
+ Terminal().run(f". {PROGRAM_PATH}", shell="powershell").print_if_unsuccessful(desc="Executing shell script", strict_err=True, strict_returncode=True)
280
317
  return None
281
318
 
282
319
 
283
- def print_code(code: str, lexer: str, desc: str = ""):
320
+ def print_code(code: str, lexer: str, desc: str):
284
321
  if lexer == "shell":
285
322
  if platform.system() == "Windows": lexer = "powershell"
286
323
  elif platform.system() == "Linux": lexer = "sh"
@@ -314,7 +351,7 @@ def check_tool_exists(tool_name: str, install_script: Optional[str] = None) -> b
314
351
 
315
352
  try:
316
353
  _tmp = subprocess.check_output([cmd, tool_name], stderr=subprocess.DEVNULL)
317
- res: bool = True
354
+ res: bool=True
318
355
  except (subprocess.CalledProcessError, FileNotFoundError):
319
356
  res = False
320
357
  if res is False and install_script is not None:
@@ -327,13 +364,12 @@ def check_tool_exists(tool_name: str, install_script: Optional[str] = None) -> b
327
364
  def get_ssh_hosts() -> list[str]:
328
365
  from paramiko import SSHConfig
329
366
  c = SSHConfig()
330
- c.parse(open(P.home().joinpath(".ssh/config").str, encoding="utf-8"))
367
+ c.parse(open(P.home().joinpath(".ssh/config").to_str(), encoding="utf-8"))
331
368
  return list(c.get_hostnames())
332
- def choose_ssh_host(multi: bool = True): return display_options(msg="", options=get_ssh_hosts(), multi=multi, fzf=True)
333
-
369
+ def choose_ssh_host(multi: bool=True): return display_options(msg="", options=get_ssh_hosts(), multi=multi, fzf=True)
334
370
 
335
371
 
336
- def check_dotfiles_version_is_beyond(commit_dtm: str, update: bool = False):
372
+ def check_dotfiles_version_is_beyond(commit_dtm: str, update: bool=False):
337
373
  dotfiles_path = str(P.home().joinpath("dotfiles"))
338
374
  from git import Repo
339
375
  repo = Repo(path=dotfiles_path)
@@ -345,10 +381,54 @@ def check_dotfiles_version_is_beyond(commit_dtm: str, update: bool = False):
345
381
  if res is False and update is True:
346
382
  print(f"Updating dotfiles because {dtm} < {datetime.fromisoformat(commit_dtm)}")
347
383
  from machineconfig.scripts.python.cloud_repo_sync import main
348
- main(cloud=None, path=dotfiles_path, push=False)
384
+ main(cloud=None, path=dotfiles_path)
349
385
  return res
350
386
 
351
387
 
388
+ def build_links(target_paths: list[tuple[PLike, str]], repo_root: PLike):
389
+ """Build symboic links from various relevant paths (e.g. data) to `repo_root/links/<name>` to facilitate easy access from
390
+ tree explorer of the IDE.
391
+ """
392
+ target_dirs_filtered: list[tuple[P, str]] = []
393
+ for a_dir, a_name in target_paths:
394
+ a_dir_obj = P(a_dir).resolve()
395
+ if not a_dir_obj.exists():
396
+ a_dir_obj.mkdir(parents=True, exist_ok=True)
397
+ target_dirs_filtered.append((a_dir_obj, a_name))
398
+
399
+ import git
400
+ repo = git.Repo(repo_root, search_parent_directories=True)
401
+ root_maybe = repo.working_tree_dir
402
+ assert root_maybe is not None
403
+ repo_root_obj = P(root_maybe)
404
+ tmp_results_root = P.home().joinpath("tmp_results", "tmp_data", repo_root_obj.name)
405
+ tmp_results_root.mkdir(parents=True, exist_ok=True)
406
+ target_dirs_filtered.append((tmp_results_root, "tmp_results"))
407
+
408
+ for a_target_path, a_name in target_dirs_filtered:
409
+ links_path = repo_root_obj.joinpath("links", a_name)
410
+ links_path.symlink_to(target=a_target_path)
411
+
412
+
413
+ def wait_for_jobs_to_finish(root: P, pattern: str, wait_for_n_jobs: int, max_wait_minutes: float) -> bool:
414
+ wait_finished: bool=False
415
+ import time
416
+ t0 = time.time()
417
+ while not wait_finished:
418
+ parts = root.search(pattern, folders=False, r=False)
419
+ counter = len(parts)
420
+ if counter == wait_for_n_jobs:
421
+ wait_finished = True
422
+ print(f"{counter} Jobs finished. Exiting.")
423
+ return True
424
+ if (time.time() - t0) > 60 * max_wait_minutes:
425
+ print(f"Waited for {max_wait_minutes} minutes. Exiting.")
426
+ return False
427
+ print(f"{counter} Jobs finished. Waiting for {wait_for_n_jobs - counter} / {wait_for_n_jobs} jobs to finish, sleeping for 60 seconds.")
428
+ time.sleep(60)
429
+ return False
430
+
431
+
352
432
  if __name__ == '__main__':
353
433
  # import typer
354
434
  # typer.run(check_tool_exists)