machineconfig 1.5__py3-none-any.whl → 1.8__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 (155) hide show
  1. machineconfig/__init__.py +8 -5
  2. machineconfig/jobs/python/check_installations.py +173 -163
  3. machineconfig/jobs/python/checkout_version.py +117 -0
  4. machineconfig/jobs/python/create_bootable_media.py +14 -14
  5. machineconfig/jobs/python/create_zellij_template.py +59 -56
  6. machineconfig/jobs/python/python_cargo_build_share.py +50 -45
  7. machineconfig/jobs/python/python_ve_symlink.py +20 -18
  8. machineconfig/jobs/python/tasks.py +4 -0
  9. machineconfig/jobs/script_installer/azure_data_studio.py +22 -0
  10. machineconfig/jobs/script_installer/bypass_paywall.py +23 -0
  11. machineconfig/jobs/script_installer/code.py +34 -0
  12. machineconfig/jobs/script_installer/docker_desktop.py +41 -0
  13. machineconfig/jobs/script_installer/ngrok.py +29 -0
  14. machineconfig/jobs/{python_linux_installers → script_installer}/skim.py +21 -19
  15. machineconfig/jobs/script_installer/wezterm.py +34 -0
  16. machineconfig/profile/create.py +107 -200
  17. machineconfig/profile/shell.py +127 -0
  18. machineconfig/scripts/__init__.py +6 -6
  19. machineconfig/scripts/python/cloud_copy.py +93 -0
  20. machineconfig/scripts/python/cloud_manager.py +38 -0
  21. machineconfig/scripts/python/cloud_mount.py +115 -52
  22. machineconfig/scripts/python/cloud_repo_sync.py +154 -114
  23. machineconfig/scripts/python/cloud_sync.py +261 -79
  24. machineconfig/scripts/python/croshell.py +151 -0
  25. machineconfig/scripts/python/devops.py +119 -87
  26. machineconfig/scripts/python/devops_add_identity.py +27 -23
  27. machineconfig/scripts/python/devops_add_ssh_key.py +70 -55
  28. machineconfig/scripts/python/devops_backup_retrieve.py +52 -46
  29. machineconfig/scripts/python/devops_devapps_install.py +120 -91
  30. machineconfig/scripts/python/devops_update_repos.py +82 -68
  31. machineconfig/scripts/python/dotfile.py +42 -38
  32. machineconfig/scripts/python/fire_jobs.py +351 -98
  33. machineconfig/scripts/python/ftpx.py +82 -0
  34. machineconfig/scripts/python/mount_nfs.py +54 -3
  35. machineconfig/scripts/python/mount_nw_drive.py +31 -0
  36. machineconfig/scripts/python/mount_ssh.py +44 -20
  37. machineconfig/scripts/python/onetimeshare.py +60 -51
  38. machineconfig/scripts/python/pomodoro.py +41 -37
  39. machineconfig/scripts/python/repos.py +195 -128
  40. machineconfig/scripts/python/scheduler.py +52 -0
  41. machineconfig/scripts/python/snapshot.py +21 -21
  42. machineconfig/scripts/python/start_slidev.py +104 -0
  43. machineconfig/scripts/python/start_terminals.py +97 -0
  44. machineconfig/scripts/python/wifi_conn.py +90 -71
  45. machineconfig/scripts/python/{transfer_wsl_win.py → wsl_windows_transfer.py} +47 -39
  46. machineconfig/setup_windows/wt_and_pwsh/set_pwsh_theme.py +44 -48
  47. machineconfig/setup_windows/wt_and_pwsh/set_wt_settings.py +136 -130
  48. machineconfig/utils/installer.py +251 -0
  49. machineconfig/utils/procs.py +114 -64
  50. machineconfig/utils/scheduling.py +188 -0
  51. machineconfig/utils/utils.py +353 -249
  52. machineconfig/utils/ve.py +222 -0
  53. {machineconfig-1.5.dist-info → machineconfig-1.8.dist-info}/METADATA +140 -110
  54. machineconfig-1.8.dist-info/RECORD +70 -0
  55. {machineconfig-1.5.dist-info → machineconfig-1.8.dist-info}/WHEEL +1 -1
  56. machineconfig/jobs/python/python_linux_installers_all.py +0 -73
  57. machineconfig/jobs/python/python_ve_installer.py +0 -73
  58. machineconfig/jobs/python/python_windows_installers_all.py +0 -23
  59. machineconfig/jobs/python_generic_installers/archive/nvim.py +0 -15
  60. machineconfig/jobs/python_generic_installers/archive/strongbox.py +0 -32
  61. machineconfig/jobs/python_generic_installers/archive/vtm.py +0 -25
  62. machineconfig/jobs/python_generic_installers/broot.py +0 -39
  63. machineconfig/jobs/python_generic_installers/browsh.py +0 -25
  64. machineconfig/jobs/python_generic_installers/bw.py +0 -26
  65. machineconfig/jobs/python_generic_installers/chatgpt.py +0 -24
  66. machineconfig/jobs/python_generic_installers/cpufetch.py +0 -23
  67. machineconfig/jobs/python_generic_installers/delta.py +0 -59
  68. machineconfig/jobs/python_generic_installers/dev/__init__.py +0 -0
  69. machineconfig/jobs/python_generic_installers/dev/autogpt.py +0 -28
  70. machineconfig/jobs/python_generic_installers/dev/bw.py +0 -29
  71. machineconfig/jobs/python_generic_installers/dev/evcxr.py +0 -22
  72. machineconfig/jobs/python_generic_installers/dev/kondo.py +0 -21
  73. machineconfig/jobs/python_generic_installers/dev/lvim.py +0 -60
  74. machineconfig/jobs/python_generic_installers/dev/ngrok.py +0 -35
  75. machineconfig/jobs/python_generic_installers/dev/opencommit.py +0 -21
  76. machineconfig/jobs/python_generic_installers/dev/qrcp.py +0 -25
  77. machineconfig/jobs/python_generic_installers/dev/qrscan.py +0 -16
  78. machineconfig/jobs/python_generic_installers/dev/rust-analyzer.py +0 -24
  79. machineconfig/jobs/python_generic_installers/dev/termscp.py +0 -23
  80. machineconfig/jobs/python_generic_installers/dev/tldr.py +0 -25
  81. machineconfig/jobs/python_generic_installers/dev/tokei.py +0 -24
  82. machineconfig/jobs/python_generic_installers/diskonaut.py +0 -26
  83. machineconfig/jobs/python_generic_installers/dua.py +0 -21
  84. machineconfig/jobs/python_generic_installers/evcxr.py +0 -21
  85. machineconfig/jobs/python_generic_installers/gitui.py +0 -23
  86. machineconfig/jobs/python_generic_installers/gopass.py +0 -19
  87. machineconfig/jobs/python_generic_installers/helix.py +0 -45
  88. machineconfig/jobs/python_generic_installers/kondo.py +0 -20
  89. machineconfig/jobs/python_generic_installers/lf.py +0 -25
  90. machineconfig/jobs/python_generic_installers/lvim.py +0 -34
  91. machineconfig/jobs/python_generic_installers/mprocs.py +0 -20
  92. machineconfig/jobs/python_generic_installers/navi.py +0 -20
  93. machineconfig/jobs/python_generic_installers/ots.py +0 -26
  94. machineconfig/jobs/python_generic_installers/ouch.py +0 -25
  95. machineconfig/jobs/python_generic_installers/pomodoro.py +0 -19
  96. machineconfig/jobs/python_generic_installers/procs.py +0 -20
  97. machineconfig/jobs/python_generic_installers/qrcp.py +0 -22
  98. machineconfig/jobs/python_generic_installers/qrscan.py +0 -14
  99. machineconfig/jobs/python_generic_installers/rclone.py +0 -21
  100. machineconfig/jobs/python_generic_installers/rust-analyzer.py +0 -24
  101. machineconfig/jobs/python_generic_installers/tere.py +0 -26
  102. machineconfig/jobs/python_generic_installers/termscp.py +0 -23
  103. machineconfig/jobs/python_generic_installers/tldr.py +0 -24
  104. machineconfig/jobs/python_generic_installers/tokei.py +0 -21
  105. machineconfig/jobs/python_generic_installers/vtm.py +0 -26
  106. machineconfig/jobs/python_generic_installers/watchexec.py +0 -21
  107. machineconfig/jobs/python_linux_installers/archive/__init__.py +0 -0
  108. machineconfig/jobs/python_linux_installers/archive/ranger.py +0 -18
  109. machineconfig/jobs/python_linux_installers/bandwhich.py +0 -11
  110. machineconfig/jobs/python_linux_installers/bottom.py +0 -17
  111. machineconfig/jobs/python_linux_installers/btop.py +0 -17
  112. machineconfig/jobs/python_linux_installers/dev/bandwhich.py +0 -13
  113. machineconfig/jobs/python_linux_installers/dev/bytehound.py +0 -20
  114. machineconfig/jobs/python_linux_installers/dev/nnn.py +0 -21
  115. machineconfig/jobs/python_linux_installers/gotty.py +0 -16
  116. machineconfig/jobs/python_linux_installers/joshuto.py +0 -28
  117. machineconfig/jobs/python_linux_installers/mcfly.py +0 -12
  118. machineconfig/jobs/python_linux_installers/nnn.py +0 -18
  119. machineconfig/jobs/python_linux_installers/topgrade.py +0 -15
  120. machineconfig/jobs/python_linux_installers/viu.py +0 -19
  121. machineconfig/jobs/python_linux_installers/xplr.py +0 -22
  122. machineconfig/jobs/python_linux_installers/zellij.py +0 -31
  123. machineconfig/jobs/python_windows_installers/archive/ntop.py +0 -20
  124. machineconfig/jobs/python_windows_installers/bat.py +0 -16
  125. machineconfig/jobs/python_windows_installers/boxes.py +0 -19
  126. machineconfig/jobs/python_windows_installers/bypass_paywall.py +0 -18
  127. machineconfig/jobs/python_windows_installers/dev/bypass_paywall.py +0 -21
  128. machineconfig/jobs/python_windows_installers/dev/obs_background_removal_plugin.py +0 -20
  129. machineconfig/jobs/python_windows_installers/fd.py +0 -17
  130. machineconfig/jobs/python_windows_installers/fzf.py +0 -19
  131. machineconfig/jobs/python_windows_installers/obs_background_removal_plugin.py +0 -19
  132. machineconfig/jobs/python_windows_installers/rg.py +0 -15
  133. machineconfig/jobs/python_windows_installers/ugrep.py +0 -14
  134. machineconfig/jobs/python_windows_installers/zoomit.py +0 -20
  135. machineconfig/jobs/python_windows_installers/zoxide.py +0 -20
  136. machineconfig/profile/fix_shell_profiles.py +0 -8
  137. machineconfig/scripts/python/archive/__init__.py +0 -0
  138. machineconfig/scripts/python/archive/bu_gdrive_rx.py +0 -41
  139. machineconfig/scripts/python/archive/bu_gdrive_sx.py +0 -40
  140. machineconfig/scripts/python/archive/bu_onedrive_rx.py +0 -59
  141. machineconfig/scripts/python/archive/bu_onedrive_sx.py +0 -45
  142. machineconfig/scripts/python/chatgpt.py +0 -28
  143. machineconfig/scripts/python/choose_ohmybash_theme.py +0 -25
  144. machineconfig/scripts/python/choose_ohmyposh_theme.py +0 -40
  145. machineconfig/scripts/python/cloud_rx.py +0 -42
  146. machineconfig/scripts/python/cloud_sx.py +0 -40
  147. machineconfig/scripts/python/ftprx.py +0 -37
  148. machineconfig/scripts/python/ftpsx.py +0 -36
  149. machineconfig/scripts/python/im2text.py +0 -15
  150. machineconfig/scripts/python/tmate_conn.py +0 -28
  151. machineconfig/scripts/python/tmate_start.py +0 -31
  152. machineconfig/utils/to_exe.py +0 -7
  153. machineconfig-1.5.dist-info/RECORD +0 -147
  154. /machineconfig/jobs/{python_generic_installers/archive → script_installer}/__init__.py +0 -0
  155. {machineconfig-1.5.dist-info → machineconfig-1.8.dist-info}/top_level.txt +0 -0
@@ -1,249 +1,353 @@
1
-
2
- from crocodile.file_management import P, randstr
3
- from crocodile.meta import Terminal
4
- from crocodile.core import install_n_import
5
- # import crocodile.environment as env
6
- import machineconfig
7
- from rich.text import Text
8
- from rich.panel import Panel
9
- from rich.console import Console
10
- from rich.syntax import Syntax
11
- import platform
12
-
13
-
14
- LIBRARY_ROOT = P(machineconfig.__file__).resolve().parent # .replace(P.home().str.lower(), P.home().str)
15
- REPO_ROOT = LIBRARY_ROOT.parent.parent
16
- PROGRAM_PATH = P.tmp().joinpath("shells/python_return_command") + (".ps1" if platform.system() == "Windows" else ".sh")
17
- CONFIG_PATH = P.home().joinpath(".config/machineconfig")
18
- tmp_install_dir = P.tmp(folder="tmp_installers")
19
-
20
-
21
- def display_options(msg, options: list, header="", tail="", prompt="", default=None, fzf=False, multi=False) -> str or list:
22
- tool_name = "fzf"
23
- if fzf and check_tool_exists(tool_name):
24
- install_n_import("pyfzf")
25
- from pyfzf.pyfzf import FzfPrompt
26
- fzf = FzfPrompt()
27
- nl = "\n"
28
- choice_idx = fzf.prompt(options, fzf_options=("--multi" if multi else "") + f" --prompt={prompt.replace(nl, ' ')} --border=rounded") # --border-label={msg.replace(nl, ' ')}")
29
- else:
30
- console = Console()
31
- if default is not None:
32
- assert default in options, f"Default `{default}` option not in options `{list(options)}`"
33
- default_msg = Text(f" <<<<-------- DEFAULT", style="bold red")
34
- else: default_msg = ""
35
- txt = Text("\n" + msg + "\n")
36
- for idx, key in enumerate(options):
37
- txt = txt + Text(f"{idx:2d} ", style="bold blue") + str(key) + (default_msg if default is not None and default == key else "") + "\n"
38
- txt = Panel(txt, title=header, subtitle=tail, border_style="bold red")
39
- console.print(txt)
40
- choice_idx = input(f"{prompt}\nEnter option *number* (or option name starting with space): ")
41
-
42
- if not fzf:
43
- if choice_idx.startswith(" "):
44
- choice_key = choice_idx.strip()
45
- assert choice_key in options, f"Choice `{choice_key}` not in options `{options}`"
46
- choice_idx = options.index(choice_key)
47
- else:
48
- if choice_idx == "":
49
- assert default is not None, f"Default option not available!"
50
- choice_idx = options.index(default)
51
- try:
52
- try: choice_idx = int(choice_idx)
53
- except ValueError: # parsing error
54
- raise IndexError
55
- choice_key = options[choice_idx]
56
- except IndexError:
57
- # many be user forgotten to start with a dash
58
- if choice_idx in options:
59
- choice_key = choice_idx
60
- choice_idx = options.index(choice_idx)
61
- else: raise ValueError(f"Unknown choice. {choice_idx}")
62
- except TypeError:
63
- raise TypeError(f"Unknown choice. {choice_idx}")
64
- # pass
65
- print(f"{choice_idx}: {choice_key}", f"<<<<-------- CHOICE MADE")
66
- else:
67
- if not multi and type(choice_idx) is list and len(choice_idx) == 1: choice_idx = choice_idx[0]
68
- choice_key = choice_idx
69
- return choice_key
70
-
71
-
72
- def symlink(this: P, to_this: P, prioritize_to_this=True):
73
- """helper function. creates a symlink from `this` to `to_this`.
74
- What can go wrong?
75
- depending on this and to_this existence, one will be prioretized depending on overwrite value.
76
- True means this will potentially be overwritten (depending on whether to_this exists or not)
77
- False means to_this will potentially be overwittten."""
78
- this = P(this).expanduser().absolute()
79
- to_this = P(to_this).expanduser().absolute()
80
- if this.is_symlink(): this.delete(sure=True) # delete if it exists as symblic link, not a concrete path.
81
- if this.exists(): # this is a problem. It will be resolved via `overwrite`
82
- if prioritize_to_this is True: # it *can* be deleted, but let's look at target first.
83
- if to_this.exists(): # this exists, to_this as well. to_this is prioritized.
84
- this.append(f".orig_{randstr()}", inplace=True) # rename is better than deletion
85
- else: this.move(path=to_this) # this exists, to_this doesn't. to_this is prioritized.
86
- elif prioritize_to_this is False: # don't sacrefice this, sacrefice to_this.
87
- 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
88
- else: this.move(path=to_this) # this exists, to_this doesn't, this is prioritized.
89
- else: # this doesn't exist.
90
- if not to_this.exists(): to_this.touch() # we have to touch it (file) or create it (folder)
91
- 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.
92
- try:
93
- P(this).symlink_to(to_this, verbose=True, overwrite=True)
94
- except Exception as ex: print(f"Failed at linking {this} ==> {to_this}.\nReason: {ex}")
95
-
96
-
97
- def find_move_delete_windows(downloaded, tool_name=None, delete=True):
98
- if tool_name is not None and ".exe" in tool_name: tool_name = tool_name.replace(".exe", "")
99
- if downloaded.is_file():
100
- exe = downloaded
101
- else:
102
- if tool_name is None: exe = downloaded.search("*.exe", r=True)[0]
103
- else:
104
- tmp = downloaded.search(f"{tool_name}.exe", r=True)
105
- if len(tmp) == 1: exe = tmp[0]
106
- else: exe = downloaded.search("*.exe", r=True)[0]
107
- exe.move(folder=P.get_env().WindowsApps, overwrite=True) # latest version overwrites older installation.
108
- if delete: downloaded.delete(sure=True)
109
- return exe
110
-
111
-
112
- def find_move_delete_linux(downloaded, tool_name, delete=True):
113
- if downloaded.is_file():
114
- exe = downloaded
115
- else:
116
- res = downloaded.search(f"*{tool_name}*", folders=False, r=True)
117
- if len(res) == 1: exe = res[0]
118
- else: exe = downloaded.search(tool_name, folders=False, r=True)[0]
119
- print(f"MOVING file `{repr(exe)}` to '/usr/local/bin'")
120
- exe.chmod(0o777)
121
- # exe.move(folder=r"/usr/local/bin", overwrite=False)
122
- Terminal().run(f"sudo mv {exe} /usr/local/bin/").print_if_unsuccessful(desc="MOVING executable to /usr/local/bin", strict_err=True, strict_returncode=True)
123
- if delete: downloaded.delete(sure=True)
124
- return None
125
-
126
-
127
- def get_latest_release(repo_url, download_n_extract=False, suffix="x86_64-pc-windows-msvc", file_name=None, tool_name=None, exe_name=None, delete=True, strip_v=False, linux=False, compression=None, sep="-", version=None):
128
- console = Console()
129
- print("\n\n\n")
130
- print(f"Inspecting latest release @ {repo_url} ...")
131
- # with console.status("Installing..."): # makes troubles on linux when prompt asks for password to move file to /usr/bin
132
-
133
- if version is None:
134
- import requests # https://docs.github.com/en/repositories/releasing-projects-on-github/linking-to-releases
135
- latest_version = requests.get(str(repo_url) + "/releases/latest").url.split("/")[-1] # this is to resolve the redirection that occures: https://stackoverflow.com/questions/36070821/how-to-get-redirect-url-using-python-requests
136
- else: latest_version = version
137
-
138
- download_link = P(repo_url + "/releases/download/" + latest_version)
139
- compression = compression or ("zip" if not linux else "tar.gz")
140
- version = download_link[-1]
141
- version = str(version).replace("v", "") if strip_v else str(version)
142
- tool_name = tool_name or P(repo_url)[-1]
143
- P.home().joinpath(f"tmp_results/cli_tools_installers/versions/{tool_name}").create(parents_only=True).write_text(version)
144
-
145
- if not download_n_extract: return download_link
146
- console.rule(f"Installing {tool_name} version {version}")
147
- if file_name is None: # it is not constant, so we compile it from parts as follows:
148
- file_name = f'{tool_name}{sep}{version}{sep}{suffix}.{compression}'
149
- download_link = download_link.joinpath(file_name)
150
- print("Downloading", download_link.as_url_str())
151
- downloaded = download_link.download(folder=tmp_install_dir)
152
-
153
- if "tar.gz" in download_link: downloaded = downloaded.ungz_untar(inplace=True)
154
- elif "zip" in download_link: downloaded = downloaded.unzip(inplace=True, overwrite=True)
155
- elif "tar.xz" in download_link: downloaded = downloaded.unxz_untar(inplace=True)
156
- else: pass # no compression.
157
-
158
- if download_n_extract and not linux: return find_move_delete_windows(downloaded, exe_name or tool_name, delete)
159
- elif download_n_extract and linux: return find_move_delete_linux(downloaded, exe_name or tool_name, delete)
160
-
161
- # console.rule(f"Completed Installation")
162
- # return res
163
-
164
-
165
- def get_shell_script_executing_pyscript(python_file, func=None, system=None, ve_name="ve"):
166
- if func is None: exec_line = f"""python {python_file}"""
167
- else: exec_line = f"""python -m fire {python_file} {func}"""
168
- return f"""
169
- . ~/scripts/activate_ve {ve_name}
170
- {exec_line}
171
- deactivate
172
- """
173
-
174
-
175
- def write_shell_script(program, desc="", preserve_cwd=True, display=True, execute=False):
176
- if preserve_cwd:
177
- if platform.system() == "Windows":
178
- program = "$orig_path = $pwd\n" + program + "\ncd $orig_path"
179
- else:
180
- program = 'orig_path=$(cd -- "." && pwd)\n' + program + '\ncd "$orig_path" || exit'
181
- if display:
182
- print(f"Executing {PROGRAM_PATH}")
183
- print_programming_script(program=program, lexer="shell", desc=desc)
184
- if platform.system() == 'Windows': PROGRAM_PATH.create(parents_only=True).write_text(program)
185
- else: PROGRAM_PATH.create(parents_only=True).write_text(f"{program}")
186
-
187
- if execute: Terminal().run(f". {PROGRAM_PATH}", shell="powershell").print_if_unsuccessful(desc="Executing shell script", strict_err=True, strict_returncode=True)
188
- return None
189
-
190
-
191
- def print_programming_script(program: str, lexer: str, desc=""):
192
- if lexer == "shell":
193
- if platform.system() == "Windows": lexer = "powershell"
194
- elif platform.system() == "Linux": lexer = "sh"
195
- else: raise NotImplementedError(f"lexer {lexer} not implemented for system {platform.system()}")
196
- console = Console()
197
- console.print(Panel(Syntax(program, lexer=lexer), title=desc), style="bold red")
198
-
199
-
200
- def get_latest_version(url):
201
- # not yet used, consider, using it.
202
- import requests
203
- import json
204
- url = f"https://api.github.com/repos/{url.split('github.com/')[1]}/releases/latest"
205
- # Replace {owner} and {repo} with the actual owner and repository name
206
- response = requests.get(url)
207
- if response.status_code == 200:
208
- data = json.loads(response.text)
209
- latest_version = data["tag_name"]
210
- print("Latest release version:", latest_version)
211
- else: print("Error:", response.status_code)
212
-
213
-
214
- def check_tool_exists(tool_name):
215
- if platform.system() == "Windows": tool_name = tool_name + ".exe"
216
- if platform.system() == "Windows": cmd = "where.exe"
217
- elif platform.system() == "Linux": cmd = "which"
218
- else: raise NotImplementedError(f"platform {platform.system()} not implemented")
219
- import subprocess
220
- try:
221
- subprocess.check_output([cmd, tool_name])
222
- return True
223
- except (subprocess.CalledProcessError, FileNotFoundError):
224
- return False
225
-
226
-
227
- def get_current_ve():
228
- import sys
229
- path = P(sys.executable) # something like ~\\venvs\\ve\\Scripts\\python.exe'
230
- if P.home().joinpath("ve") in path:
231
- return path.parent.parent.stem
232
- else:
233
- raise NotImplementedError("Not a kind of virtual enviroment that I expected.")
234
- # return path.parent.parent.stem
235
-
236
-
237
- def choose_ssh_host(multi=True):
238
- from paramiko import SSHConfig
239
- c = SSHConfig()
240
- c.parse(open(P.home().joinpath(".ssh/config").str))
241
- choices = list(c.get_hostnames())
242
- hosts = display_options(msg="", options=choices, multi=multi, fzf=True)
243
- return hosts
244
-
245
-
246
- if __name__ == '__main__':
247
- # import typer
248
- # typer.run(check_tool_exists)
249
- pass
1
+
2
+ """
3
+ Utils
4
+ """
5
+
6
+ from crocodile.file_management import P, randstr
7
+ from crocodile.meta import Terminal
8
+ from crocodile.core import install_n_import
9
+ # import crocodile.environment as env
10
+ import machineconfig
11
+ from rich.text import Text
12
+ from rich.panel import Panel
13
+ from rich.console import Console
14
+ from rich.syntax import Syntax
15
+ import platform
16
+ import subprocess
17
+ from typing import Optional, Union, TypeVar, Iterable
18
+
19
+
20
+ LIBRARY_ROOT = P(machineconfig.__file__).resolve().parent # .replace(P.home().str.lower(), P.home().str)
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)
23
+ CONFIG_PATH = P.home().joinpath(".config/machineconfig")
24
+ INSTALL_VERSION_ROOT = CONFIG_PATH.joinpath("cli_tools_installers/versions")
25
+ INSTALL_TMP_DIR = P.tmp(folder="tmp_installers")
26
+
27
+ DEFAULTS_PATH = P.home().joinpath("dotfiles/machineconfig/defaults.ini")
28
+
29
+
30
+ T = TypeVar("T")
31
+
32
+
33
+ def choose_cloud_interactively() -> str:
34
+ from crocodile.core import List as L
35
+ print(f"Listing Remotes ... ")
36
+ tmp = Terminal().run("rclone listremotes").op_if_successfull_or_default(strict_returcode=False)
37
+ # consider this: remotes = Read.ini(P.home().joinpath(".config/rclone/rclone.conf")).sections()
38
+ if isinstance(tmp, str):
39
+ remotes: list[str] = L(tmp.splitlines()).apply(lambda x: x.replace(":", "")).list
40
+
41
+ else: raise ValueError(f"Got {tmp} from rclone listremotes")
42
+ 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)
45
+ return cloud
46
+
47
+
48
+ def sanitize_path(a_path: P) -> P:
49
+ path = P(a_path)
50
+ if path.as_posix().startswith("/home"):
51
+ if platform.system() == "Windows": # path copied from Linux to Windows
52
+ path = P.home().joinpath(*path.parts[2:]) # exlcude /home/username
53
+ assert path.exists(), f"File not found: {path}"
54
+ print(f"\n{'--' * 50}\n🔗 Mapped `{a_path}` ➡️ `{path}`\n{'--' * 50}\n")
55
+ elif platform.system() == "Linux" and P.home().as_posix() not in path.as_posix(): # copied from Linux to Linux with different username
56
+ path = P.home().joinpath(*path.parts[3:]) # exlcude /home/username (three parts: /, home, username)
57
+ assert path.exists(), f"File not found: {path}"
58
+ print(f"\n{'--' * 50}\n🔗 Mapped `{a_path}` ➡️ `{path}`\n{'--' * 50}\n")
59
+ elif path.as_posix().startswith("C:"):
60
+ if platform.system() == "Linux": # path copied from Windows to Linux
61
+ xx = str(a_path).replace("\\", "/")
62
+ path = P.home().joinpath(*P(xx).parts[3:]) # exlcude C:\Users\username
63
+ assert path.exists(), f"File not found: {path}"
64
+ print(f"\n{'--' * 50}\n🔗 Mapped `{a_path}` ➡️ `{path}`\n{'--' * 50}\n")
65
+ elif platform.system() == "Windows" and P.home().as_posix() not in path.as_posix(): # copied from Windows to Windows with different username
66
+ path = P.home().joinpath(*path.parts[2:])
67
+ assert path.exists(), f"File not found: {path}"
68
+ print(f"\n{'--' * 50}\n🔗 Mapped `{a_path}` ➡️ `{path}`\n{'--' * 50}\n")
69
+ return path.expanduser().absolute()
70
+
71
+
72
+ def match_file_name(sub_string: str, search_root: Optional[P] = None) -> P:
73
+ """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)
77
+ if len(search_results) == 1:
78
+ path_obj = search_results.list[0]
79
+ 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)
81
+ path_obj = P(choice)
82
+ else:
83
+ # let's do a final retry with sub_string.small()
84
+ sub_string_small = sub_string.lower()
85
+ if sub_string_small != sub_string:
86
+ return match_file_name(sub_string=sub_string_small)
87
+ from git.repo import Repo
88
+ from git.exc import InvalidGitRepositoryError
89
+ try:
90
+ repo = Repo(root, search_parent_directories=True)
91
+ 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.
93
+ return match_file_name(sub_string=sub_string, search_root=repo_root_dir)
94
+ else:
95
+ root = repo_root_dir
96
+ except InvalidGitRepositoryError:
97
+ pass
98
+
99
+ if check_tool_exists("fzf"):
100
+ 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]
102
+ except subprocess.CalledProcessError as cpe:
103
+ print(f"Failed at fzf search with {sub_string} in {root}.\n{cpe}")
104
+ msg = f"\n{'--' * 50}\n💥 Path {sub_string} does not exist. No search results\n{'--' * 50}\n"
105
+ raise FileNotFoundError(msg) from cpe
106
+ if len(search_res) == 1: return root.joinpath(search_res[0])
107
+ else:
108
+ try:
109
+ res = subprocess.run(f"cd '{root}'; fzf --query={sub_string}", check=True, stdout=subprocess.PIPE, text=True, shell=True).stdout.strip()
110
+ except subprocess.CalledProcessError as cpe:
111
+ print(f"Failed at fzf search with {sub_string} in {root}. {cpe}")
112
+ msg = f"\n{'--' * 50}\n💥 Path {sub_string} does not exist. No search results\n{'--' * 50}\n"
113
+ raise FileNotFoundError(msg) from cpe
114
+ return root.joinpath(res)
115
+ msg = f"\n{'--' * 50}\n💥 Path {sub_string} does not exist. No search results\n{'--' * 50}\n"
116
+ raise FileNotFoundError(msg)
117
+ print(f"\n{'--' * 50}\n🔗 Matched `{sub_string}` ➡️ `{path_obj}`\n{'--' * 50}\n")
118
+ return path_obj
119
+
120
+
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:
123
+ choice_key = display_options(msg=msg, options=options, header=header, tail=tail, prompt=prompt,
124
+ default=default, fzf=fzf, multi=False, custom_input=custom_input)
125
+ assert not isinstance(choice_key, list)
126
+ return choice_key
127
+
128
+
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]:
131
+ choice_key = display_options(msg=msg, options=options, header=header, tail=tail, prompt=prompt,
132
+ default=default, fzf=True, multi=True,
133
+ custom_input=custom_input)
134
+ if isinstance(choice_key, list): return choice_key
135
+ return [choice_key]
136
+
137
+
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
141
+ tool_name = "fzf"
142
+ options_strings: list[str] = [str(x) for x in options]
143
+ default_string = str(default) if default is not None else None
144
+ if fzf and check_tool_exists(tool_name):
145
+ install_n_import("pyfzf")
146
+ from pyfzf.pyfzf import FzfPrompt
147
+ fzf_prompt = FzfPrompt()
148
+ nl = "\n"
149
+ choice_string_multi: list[str] = fzf_prompt.prompt(choices=options_strings, fzf_options=("--multi" if multi else "") + f' --prompt "{prompt.replace(nl, " ")}" ') # --border-label={msg.replace(nl, ' ')}")
150
+ # --border=rounded doens't work on older versions of fzf installed at Ubuntu 20.04
151
+ if not multi:
152
+ try:
153
+ choice_one_string = choice_string_multi[0]
154
+ choice_idx = options_strings.index(choice_one_string)
155
+ return list(options)[choice_idx]
156
+ except IndexError as ie:
157
+ print(f"{options=}, {choice_string_multi=}")
158
+ print(choice_string_multi)
159
+ raise ie
160
+ choice_idx_s = [options_strings.index(x) for x in choice_string_multi]
161
+ return [list(options)[x] for x in choice_idx_s]
162
+ else:
163
+ console = Console()
164
+ if default is not None:
165
+ assert default in options, f"Default `{default}` option not in options `{list(options)}`"
166
+ default_msg = Text(f" <<<<-------- DEFAULT", style="bold red")
167
+ else: default_msg = Text("")
168
+ txt = Text("\n" + msg + "\n")
169
+ for idx, key in enumerate(options):
170
+ txt = txt + Text(f"{idx:2d} ", style="bold blue") + str(key) + (default_msg if default is not None and default == key else "") + "\n"
171
+ txt_panel = Panel(txt, title=header, subtitle=tail, border_style="bold red")
172
+ console.print(txt_panel)
173
+ if default is not None:
174
+ choice_string = input(f"{prompt}\nEnter option number or hit enter for default choice: ")
175
+ else: choice_string = input(f"{prompt}\nEnter option number: ")
176
+
177
+ if choice_string == "":
178
+ if default_string is None:
179
+ print(f"Default option not available!")
180
+ return display_options(msg=msg, options=options, header=header, tail=tail, prompt=prompt, default=default, fzf=fzf, multi=multi, custom_input=custom_input)
181
+ choice_idx = options_strings.index(default_string)
182
+ assert default is not None, f"🧨 Default option not available!"
183
+ choice_one: T = default
184
+ else:
185
+ try:
186
+ choice_idx = int(choice_string, base=10)
187
+ choice_one = list(options)[choice_idx]
188
+ except IndexError as ie: # i.e. converting to integer was successful but indexing failed.
189
+ if choice_string in options_strings: # string input
190
+ choice_idx = options_strings.index(choice_one) # type: ignore #TODO: fix this
191
+ choice_one = list(options)[choice_idx]
192
+ elif custom_input: return str(choice_string) # type: ignore #TODO: fix this
193
+ else: raise ValueError(f"Unknown choice. {choice_string}") from ie
194
+ except TypeError as te: # int(choice_string) failed due to # either the number is invalid, or the input is custom.
195
+ if choice_string in options_strings: # string input
196
+ choice_idx = options_strings.index(choice_one) # type: ignore #TODO: fix this
197
+ choice_one = list(options)[choice_idx]
198
+ elif custom_input: return str(choice_string) # type: ignore #TODO: fix this
199
+ else: raise ValueError(f"Unknown choice. {choice_string}") from te
200
+ print(f"{choice_idx}: {choice_one}", f"<<<<-------- CHOICE MADE")
201
+ if multi: return [choice_one]
202
+ return choice_one
203
+
204
+
205
+ def symlink(this: P, to_this: P, prioritize_to_this: bool = True):
206
+ """helper function. creates a symlink from `this` to `to_this`.
207
+ What can go wrong?
208
+ depending on this and to_this existence, one will be prioretized depending on overwrite value.
209
+ True means this will potentially be overwritten (depending on whether to_this exists or not)
210
+ False means to_this will potentially be overwittten."""
211
+ this = P(this).expanduser().absolute()
212
+ to_this = P(to_this).expanduser().absolute()
213
+ if this.is_symlink(): this.delete(sure=True) # delete if it exists as symblic link, not a concrete path.
214
+ if this.exists(): # this is a problem. It will be resolved via `overwrite`
215
+ if prioritize_to_this is True: # it *can* be deleted, but let's look at target first.
216
+ if to_this.exists(): # this exists, to_this as well. to_this is prioritized.
217
+ this.append(f".orig_{randstr()}", inplace=True) # rename is better than deletion
218
+ else: this.move(path=to_this) # this exists, to_this doesn't. to_this is prioritized.
219
+ elif prioritize_to_this is False: # don't sacrefice this, sacrefice to_this.
220
+ 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
221
+ else: this.move(path=to_this) # this exists, to_this doesn't, this is prioritized.
222
+ else: # this doesn't exist.
223
+ if not to_this.exists(): to_this.touch() # we have to touch it (file) or create it (folder)
224
+ 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.
225
+ try:
226
+ P(this).symlink_to(to_this, verbose=True, overwrite=True)
227
+ except Exception as ex: print(f"Failed at linking {this} ➡️ {to_this}.\nReason: {ex}")
228
+
229
+
230
+ def get_shell_script_executing_python_file(python_file: str, func: Optional[str] = None, ve_name: str = "ve", strict_execution: bool = True):
231
+ if func is None: exec_line = f"""python {python_file}"""
232
+ else: exec_line = f"""python -m fire {python_file} {func}"""
233
+ shell_script = f"""
234
+ . $HOME/scripts/activate_ve {ve_name}
235
+ echo "Executing {exec_line}"
236
+ {exec_line}
237
+ deactivate || true
238
+ """
239
+
240
+ if strict_execution:
241
+ if platform.system() == "Windows": shell_script = """$ErrorActionPreference = "Stop" """ + "\n" + shell_script
242
+ if platform.system() == "Linux": shell_script = "set -e" + "\n" + shell_script
243
+
244
+ if platform.system() == "Linux": shell_script = "#!/bin/bash" + "\n" + shell_script # vs #!/usr/bin/env bash
245
+ return shell_script
246
+
247
+
248
+ def get_shell_file_executing_python_script(python_script: str, ve_name: str = "ve", verbose: bool = True):
249
+ if verbose:
250
+ python_script = f"""
251
+ code = r'''{python_script}'''
252
+ try:
253
+ from machineconfig.utils.utils import print_code
254
+ print_code(code=code, lexer="python", desc="Python Script")
255
+ except ImportError: print(code)
256
+ """ + python_script
257
+ python_file = P.tmp().joinpath("tmp_scripts", "python", randstr() + ".py").create(parents_only=True).write_text(python_script)
258
+ shell_script = get_shell_script_executing_python_file(python_file=python_file.str, ve_name=ve_name)
259
+ if platform.system() == "Linux": suffix = ".sh"
260
+ elif platform.system() == "Windows": suffix = ".ps1"
261
+ else: raise NotImplementedError(f"Platform {platform.system()} not implemented.")
262
+ shell_file = P.tmp().joinpath("tmp_scripts", "shell", randstr() + suffix).create(parents_only=True).write_text(shell_script)
263
+ return shell_file
264
+
265
+
266
+ def write_shell_script(program: str, desc: str = "", preserve_cwd: bool = True, display: bool = True, execute: bool = False):
267
+ if preserve_cwd:
268
+ if platform.system() == "Windows":
269
+ program = "$orig_path = $pwd\n" + program + "\ncd $orig_path"
270
+ else:
271
+ program = 'orig_path=$(cd -- "." && pwd)\n' + program + '\ncd "$orig_path" || exit'
272
+ if display:
273
+ print(f"Executing {PROGRAM_PATH}")
274
+ print_code(code=program, lexer="shell", desc=desc)
275
+ if platform.system() == 'Windows': PROGRAM_PATH.create(parents_only=True).write_text(program)
276
+ else: PROGRAM_PATH.create(parents_only=True).write_text(f"{program}")
277
+ if execute: Terminal().run(f". {PROGRAM_PATH}", shell="powershell").print_if_unsuccessful(desc="Executing shell script", strict_err=True, strict_returncode=True)
278
+ return None
279
+
280
+
281
+ def print_code(code: str, lexer: str, desc: str = ""):
282
+ if lexer == "shell":
283
+ if platform.system() == "Windows": lexer = "powershell"
284
+ elif platform.system() == "Linux": lexer = "sh"
285
+ else: raise NotImplementedError(f"lexer {lexer} not implemented for system {platform.system()}")
286
+ console = Console()
287
+ console.print(Panel(Syntax(code=code, lexer=lexer), title=desc), style="bold red")
288
+
289
+
290
+ # def get_latest_version(url: str) -> None:
291
+ # # not yet used, consider, using it.
292
+ # import requests
293
+ # import json
294
+ # url = f"https://api.github.com/repos/{url.split('github.com/')[1]}/releases/latest"
295
+ # # Replace {owner} and {repo} with the actual owner and repository name
296
+ # response = requests.get(url, timeout=10)
297
+ # if response.status_code == 200:
298
+ # data = json.loads(response.text)
299
+ # latest_version = data["tag_name"]
300
+ # print("Latest release version:", latest_version)
301
+ # else: print("Error:", response.status_code)
302
+
303
+
304
+ def check_tool_exists(tool_name: str, install_script: Optional[str] = None) -> bool:
305
+ """This is the CLI equivalent of `install_n_import` function of crocodile. """
306
+ if platform.system() == "Windows":
307
+ tool_name = tool_name.replace(".exe", "") + ".exe"
308
+
309
+ if platform.system() == "Windows": cmd = "where.exe"
310
+ elif platform.system() == "Linux": cmd = "which"
311
+ else: raise NotImplementedError(f"platform {platform.system()} not implemented")
312
+
313
+ try:
314
+ _tmp = subprocess.check_output([cmd, tool_name])
315
+ res: bool = True
316
+ except (subprocess.CalledProcessError, FileNotFoundError):
317
+ res = False
318
+ if res is False and install_script is not None:
319
+ print(f"Installing {tool_name} ...")
320
+ Terminal().run(install_script, shell="powershell").print()
321
+ return check_tool_exists(tool_name=tool_name, install_script=None)
322
+ return res
323
+
324
+
325
+ def get_ssh_hosts() -> list[str]:
326
+ from paramiko import SSHConfig
327
+ c = SSHConfig()
328
+ c.parse(open(P.home().joinpath(".ssh/config").str, encoding="utf-8"))
329
+ return list(c.get_hostnames())
330
+ def choose_ssh_host(multi: bool = True): return display_options(msg="", options=get_ssh_hosts(), multi=multi, fzf=True)
331
+
332
+
333
+
334
+ def check_dotfiles_version_is_beyond(commit_dtm: str, update: bool = False):
335
+ dotfiles_path = str(P.home().joinpath("dotfiles"))
336
+ from git import Repo
337
+ repo = Repo(path=dotfiles_path)
338
+ last_commit = repo.head.commit
339
+ dtm = last_commit.committed_datetime
340
+ from datetime import datetime # make it tz unaware
341
+ dtm = datetime(dtm.year, dtm.month, dtm.day, dtm.hour, dtm.minute, dtm.second)
342
+ res = dtm > datetime.fromisoformat(commit_dtm)
343
+ if res is False and update is True:
344
+ print(f"Updating dotfiles because {dtm} < {datetime.fromisoformat(commit_dtm)}")
345
+ from machineconfig.scripts.python.cloud_repo_sync import main
346
+ main(cloud=None, path=dotfiles_path, push=False)
347
+ return res
348
+
349
+
350
+ if __name__ == '__main__':
351
+ # import typer
352
+ # typer.run(check_tool_exists)
353
+ pass