machineconfig 2.0__py3-none-any.whl → 2.2__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 (253) hide show
  1. machineconfig/cluster/cloud_manager.py +0 -3
  2. machineconfig/cluster/data_transfer.py +0 -1
  3. machineconfig/cluster/file_manager.py +0 -1
  4. machineconfig/cluster/job_params.py +0 -3
  5. machineconfig/cluster/loader_runner.py +0 -3
  6. machineconfig/cluster/remote_machine.py +0 -1
  7. machineconfig/cluster/script_notify_upon_completion.py +0 -1
  8. machineconfig/cluster/sessions_managers/archive/create_zellij_template.py +5 -6
  9. machineconfig/cluster/sessions_managers/archive/session_managers.py +0 -1
  10. machineconfig/cluster/sessions_managers/enhanced_command_runner.py +17 -57
  11. machineconfig/cluster/sessions_managers/wt_local.py +36 -110
  12. machineconfig/cluster/sessions_managers/wt_local_manager.py +42 -112
  13. machineconfig/cluster/sessions_managers/wt_remote.py +23 -30
  14. machineconfig/cluster/sessions_managers/wt_remote_manager.py +20 -62
  15. machineconfig/cluster/sessions_managers/wt_utils/layout_generator.py +10 -15
  16. machineconfig/cluster/sessions_managers/wt_utils/process_monitor.py +27 -127
  17. machineconfig/cluster/sessions_managers/wt_utils/remote_executor.py +10 -43
  18. machineconfig/cluster/sessions_managers/wt_utils/session_manager.py +22 -101
  19. machineconfig/cluster/sessions_managers/wt_utils/status_reporter.py +11 -39
  20. machineconfig/cluster/sessions_managers/zellij_local.py +49 -102
  21. machineconfig/cluster/sessions_managers/zellij_local_manager.py +34 -78
  22. machineconfig/cluster/sessions_managers/zellij_remote.py +17 -24
  23. machineconfig/cluster/sessions_managers/zellij_remote_manager.py +7 -13
  24. machineconfig/cluster/sessions_managers/zellij_utils/example_usage.py +4 -2
  25. machineconfig/cluster/sessions_managers/zellij_utils/layout_generator.py +6 -6
  26. machineconfig/cluster/sessions_managers/zellij_utils/process_monitor.py +18 -88
  27. machineconfig/cluster/sessions_managers/zellij_utils/remote_executor.py +2 -6
  28. machineconfig/cluster/sessions_managers/zellij_utils/session_manager.py +12 -40
  29. machineconfig/cluster/sessions_managers/zellij_utils/status_reporter.py +3 -2
  30. machineconfig/cluster/templates/cli_click.py +0 -1
  31. machineconfig/cluster/templates/cli_gooey.py +0 -2
  32. machineconfig/cluster/templates/cli_trogon.py +0 -1
  33. machineconfig/cluster/templates/run_cloud.py +0 -1
  34. machineconfig/cluster/templates/run_cluster.py +0 -1
  35. machineconfig/cluster/templates/run_remote.py +0 -1
  36. machineconfig/cluster/templates/utils.py +27 -46
  37. machineconfig/jobs/__pycache__/__init__.cpython-313.pyc +0 -0
  38. machineconfig/jobs/linux/msc/cli_agents.sh +16 -0
  39. machineconfig/jobs/python/check_installations.py +2 -1
  40. machineconfig/jobs/python/create_bootable_media.py +0 -2
  41. machineconfig/jobs/python/python_ve_symlink.py +9 -11
  42. machineconfig/jobs/python/tasks.py +0 -1
  43. machineconfig/jobs/python/vscode/api.py +5 -5
  44. machineconfig/jobs/python/vscode/link_ve.py +13 -14
  45. machineconfig/jobs/python/vscode/select_interpreter.py +21 -22
  46. machineconfig/jobs/python/vscode/sync_code.py +9 -13
  47. machineconfig/jobs/python_custom_installers/archive/ngrok.py +13 -13
  48. machineconfig/jobs/python_custom_installers/dev/aider.py +7 -15
  49. machineconfig/jobs/python_custom_installers/dev/alacritty.py +9 -18
  50. machineconfig/jobs/python_custom_installers/dev/brave.py +10 -19
  51. machineconfig/jobs/python_custom_installers/dev/bypass_paywall.py +8 -15
  52. machineconfig/jobs/python_custom_installers/dev/code.py +12 -32
  53. machineconfig/jobs/python_custom_installers/dev/cursor.py +3 -14
  54. machineconfig/jobs/python_custom_installers/dev/docker_desktop.py +8 -7
  55. machineconfig/jobs/python_custom_installers/dev/espanso.py +15 -19
  56. machineconfig/jobs/python_custom_installers/dev/goes.py +5 -12
  57. machineconfig/jobs/python_custom_installers/dev/lvim.py +9 -17
  58. machineconfig/jobs/python_custom_installers/dev/nerdfont.py +12 -19
  59. machineconfig/jobs/python_custom_installers/dev/redis.py +12 -20
  60. machineconfig/jobs/python_custom_installers/dev/wezterm.py +12 -19
  61. machineconfig/jobs/python_custom_installers/dev/winget.py +5 -23
  62. machineconfig/jobs/python_custom_installers/docker.py +12 -21
  63. machineconfig/jobs/python_custom_installers/gh.py +11 -19
  64. machineconfig/jobs/python_custom_installers/hx.py +32 -16
  65. machineconfig/jobs/python_custom_installers/warp-cli.py +12 -20
  66. machineconfig/jobs/python_generic_installers/__pycache__/__init__.cpython-313.pyc +0 -0
  67. machineconfig/jobs/python_generic_installers/config.json +1 -1
  68. machineconfig/jobs/python_linux_installers/__pycache__/__init__.cpython-313.pyc +0 -0
  69. machineconfig/jobs/windows/archive/archive_pygraphviz.ps1 +1 -1
  70. machineconfig/jobs/windows/msc/cli_agents.bat +0 -0
  71. machineconfig/jobs/windows/msc/cli_agents.ps1 +0 -0
  72. machineconfig/jobs/windows/start_terminal.ps1 +1 -1
  73. machineconfig/profile/create.py +38 -26
  74. machineconfig/profile/create_hardlinks.py +29 -20
  75. machineconfig/profile/shell.py +56 -32
  76. machineconfig/scripts/__init__.py +0 -2
  77. machineconfig/scripts/__pycache__/__init__.cpython-313.pyc +0 -0
  78. machineconfig/scripts/cloud/init.sh +2 -2
  79. machineconfig/scripts/linux/checkout_versions +1 -1
  80. machineconfig/scripts/linux/choose_wezterm_theme +1 -1
  81. machineconfig/scripts/linux/cloud_copy +1 -1
  82. machineconfig/scripts/linux/cloud_manager +1 -1
  83. machineconfig/scripts/linux/cloud_mount +1 -1
  84. machineconfig/scripts/linux/cloud_repo_sync +1 -1
  85. machineconfig/scripts/linux/cloud_sync +1 -1
  86. machineconfig/scripts/linux/croshell +1 -1
  87. machineconfig/scripts/linux/devops +7 -7
  88. machineconfig/scripts/linux/fire +1 -1
  89. machineconfig/scripts/linux/fire_agents +3 -2
  90. machineconfig/scripts/linux/ftpx +1 -1
  91. machineconfig/scripts/linux/gh_models +1 -1
  92. machineconfig/scripts/linux/kill_process +1 -1
  93. machineconfig/scripts/linux/mcinit +1 -1
  94. machineconfig/scripts/linux/repos +1 -1
  95. machineconfig/scripts/linux/scheduler +1 -1
  96. machineconfig/scripts/linux/start_slidev +1 -1
  97. machineconfig/scripts/linux/start_terminals +1 -1
  98. machineconfig/scripts/linux/url2md +1 -1
  99. machineconfig/scripts/linux/warp-cli.sh +122 -0
  100. machineconfig/scripts/linux/wifi_conn +1 -1
  101. machineconfig/scripts/python/__pycache__/__init__.cpython-313.pyc +0 -0
  102. machineconfig/scripts/python/__pycache__/devops.cpython-313.pyc +0 -0
  103. machineconfig/scripts/python/__pycache__/devops_devapps_install.cpython-313.pyc +0 -0
  104. machineconfig/scripts/python/__pycache__/devops_update_repos.cpython-313.pyc +0 -0
  105. machineconfig/scripts/python/__pycache__/fire_agents.cpython-313.pyc +0 -0
  106. machineconfig/scripts/python/ai/__init__.py +0 -0
  107. machineconfig/scripts/python/ai/generate_files.py +83 -0
  108. machineconfig/scripts/python/ai/instructions/python/dev.instructions.md +2 -2
  109. machineconfig/scripts/python/ai/mcinit.py +14 -7
  110. machineconfig/scripts/python/ai/scripts/lint_and_type_check.sh +10 -5
  111. machineconfig/scripts/python/archive/tmate_conn.py +5 -5
  112. machineconfig/scripts/python/archive/tmate_start.py +7 -7
  113. machineconfig/scripts/python/choose_wezterm_theme.py +35 -32
  114. machineconfig/scripts/python/cloud_copy.py +23 -14
  115. machineconfig/scripts/python/cloud_mount.py +36 -24
  116. machineconfig/scripts/python/cloud_repo_sync.py +40 -27
  117. machineconfig/scripts/python/cloud_sync.py +4 -4
  118. machineconfig/scripts/python/croshell.py +40 -29
  119. machineconfig/scripts/python/devops.py +45 -27
  120. machineconfig/scripts/python/devops_add_identity.py +15 -25
  121. machineconfig/scripts/python/devops_add_ssh_key.py +8 -8
  122. machineconfig/scripts/python/devops_backup_retrieve.py +18 -16
  123. machineconfig/scripts/python/devops_devapps_install.py +25 -20
  124. machineconfig/scripts/python/devops_update_repos.py +232 -59
  125. machineconfig/scripts/python/dotfile.py +17 -15
  126. machineconfig/scripts/python/fire_agents.py +48 -22
  127. machineconfig/scripts/python/fire_jobs.py +93 -58
  128. machineconfig/scripts/python/ftpx.py +26 -15
  129. machineconfig/scripts/python/get_zellij_cmd.py +8 -7
  130. machineconfig/scripts/python/helpers/cloud_helpers.py +33 -28
  131. machineconfig/scripts/python/helpers/helpers2.py +27 -16
  132. machineconfig/scripts/python/helpers/helpers4.py +45 -32
  133. machineconfig/scripts/python/helpers/helpers5.py +1 -1
  134. machineconfig/scripts/python/helpers/repo_sync_helpers.py +32 -10
  135. machineconfig/scripts/python/mount_nfs.py +9 -16
  136. machineconfig/scripts/python/mount_nw_drive.py +10 -5
  137. machineconfig/scripts/python/mount_ssh.py +9 -7
  138. machineconfig/scripts/python/repos.py +216 -58
  139. machineconfig/scripts/python/snapshot.py +0 -1
  140. machineconfig/scripts/python/start_slidev.py +11 -6
  141. machineconfig/scripts/python/start_terminals.py +22 -16
  142. machineconfig/scripts/python/viewer_template.py +0 -1
  143. machineconfig/scripts/python/wifi_conn.py +49 -75
  144. machineconfig/scripts/python/wsl_windows_transfer.py +9 -7
  145. machineconfig/scripts/windows/checkout_version.ps1 +1 -3
  146. machineconfig/scripts/windows/choose_wezterm_theme.ps1 +1 -3
  147. machineconfig/scripts/windows/cloud_copy.ps1 +2 -6
  148. machineconfig/scripts/windows/cloud_manager.ps1 +1 -1
  149. machineconfig/scripts/windows/cloud_repo_sync.ps1 +1 -2
  150. machineconfig/scripts/windows/cloud_sync.ps1 +2 -2
  151. machineconfig/scripts/windows/croshell.ps1 +2 -2
  152. machineconfig/scripts/windows/devops.ps1 +1 -4
  153. machineconfig/scripts/windows/dotfile.ps1 +1 -3
  154. machineconfig/scripts/windows/fire.ps1 +1 -1
  155. machineconfig/scripts/windows/ftpx.ps1 +2 -2
  156. machineconfig/scripts/windows/gpt.ps1 +1 -1
  157. machineconfig/scripts/windows/kill_process.ps1 +1 -2
  158. machineconfig/scripts/windows/mcinit.ps1 +1 -1
  159. machineconfig/scripts/windows/mount_nfs.ps1 +1 -1
  160. machineconfig/scripts/windows/mount_ssh.ps1 +1 -1
  161. machineconfig/scripts/windows/pomodoro.ps1 +1 -1
  162. machineconfig/scripts/windows/py2exe.ps1 +1 -3
  163. machineconfig/scripts/windows/repos.ps1 +1 -1
  164. machineconfig/scripts/windows/scheduler.ps1 +1 -1
  165. machineconfig/scripts/windows/snapshot.ps1 +2 -2
  166. machineconfig/scripts/windows/start_slidev.ps1 +1 -1
  167. machineconfig/scripts/windows/start_terminals.ps1 +1 -1
  168. machineconfig/scripts/windows/wifi_conn.ps1 +1 -1
  169. machineconfig/scripts/windows/wsl_windows_transfer.ps1 +1 -3
  170. machineconfig/settings/lf/linux/lfrc +2 -1
  171. machineconfig/settings/linters/.ruff_cache/.gitignore +2 -0
  172. machineconfig/settings/linters/.ruff_cache/CACHEDIR.TAG +1 -0
  173. machineconfig/settings/lvim/windows/archive/config_additional.lua +1 -1
  174. machineconfig/settings/svim/linux/init.toml +1 -1
  175. machineconfig/settings/svim/windows/init.toml +1 -1
  176. machineconfig/setup_linux/web_shortcuts/croshell.sh +3 -52
  177. machineconfig/setup_linux/web_shortcuts/interactive.sh +6 -6
  178. machineconfig/setup_linux/web_shortcuts/ssh.sh +0 -4
  179. machineconfig/setup_windows/web_shortcuts/all.ps1 +2 -2
  180. machineconfig/setup_windows/web_shortcuts/ascii_art.ps1 +1 -1
  181. machineconfig/setup_windows/web_shortcuts/croshell.ps1 +1 -1
  182. machineconfig/setup_windows/web_shortcuts/interactive.ps1 +5 -5
  183. machineconfig/setup_windows/wt_and_pwsh/install_fonts.ps1 +51 -15
  184. machineconfig/setup_windows/wt_and_pwsh/set_pwsh_theme.py +58 -13
  185. machineconfig/setup_windows/wt_and_pwsh/set_wt_settings.py +45 -37
  186. machineconfig/utils/ai/generate_file_checklist.py +8 -10
  187. machineconfig/utils/ai/url2md.py +4 -2
  188. machineconfig/utils/cloud/onedrive/setup_oauth.py +1 -0
  189. machineconfig/utils/cloud/onedrive/transaction.py +63 -98
  190. machineconfig/utils/code.py +62 -41
  191. machineconfig/utils/installer.py +29 -35
  192. machineconfig/utils/installer_utils/installer_abc.py +11 -11
  193. machineconfig/utils/installer_utils/installer_class.py +155 -74
  194. machineconfig/utils/links.py +112 -31
  195. machineconfig/utils/notifications.py +211 -0
  196. machineconfig/utils/options.py +41 -42
  197. machineconfig/utils/path.py +13 -6
  198. machineconfig/utils/path_reduced.py +614 -311
  199. machineconfig/utils/procs.py +48 -42
  200. machineconfig/utils/scheduling.py +0 -1
  201. machineconfig/utils/source_of_truth.py +27 -0
  202. machineconfig/utils/ssh.py +146 -85
  203. machineconfig/utils/terminal.py +84 -37
  204. machineconfig/utils/upgrade_packages.py +91 -0
  205. machineconfig/utils/utils2.py +39 -50
  206. machineconfig/utils/utils5.py +195 -116
  207. machineconfig/utils/ve.py +13 -5
  208. {machineconfig-2.0.dist-info → machineconfig-2.2.dist-info}/METADATA +14 -13
  209. {machineconfig-2.0.dist-info → machineconfig-2.2.dist-info}/RECORD +212 -237
  210. machineconfig/jobs/__pycache__/__init__.cpython-311.pyc +0 -0
  211. machineconfig/jobs/python/__pycache__/__init__.cpython-311.pyc +0 -0
  212. machineconfig/jobs/python/__pycache__/python_ve_symlink.cpython-311.pyc +0 -0
  213. machineconfig/jobs/python/archive/python_tools.txt +0 -12
  214. machineconfig/jobs/python/vscode/__pycache__/select_interpreter.cpython-311.pyc +0 -0
  215. machineconfig/jobs/python_custom_installers/__pycache__/__init__.cpython-311.pyc +0 -0
  216. machineconfig/jobs/python_generic_installers/__pycache__/__init__.cpython-311.pyc +0 -0
  217. machineconfig/jobs/python_generic_installers/update.py +0 -3
  218. machineconfig/jobs/python_linux_installers/__pycache__/__init__.cpython-311.pyc +0 -0
  219. machineconfig/profile/__pycache__/__init__.cpython-311.pyc +0 -0
  220. machineconfig/profile/__pycache__/create.cpython-311.pyc +0 -0
  221. machineconfig/profile/__pycache__/shell.cpython-311.pyc +0 -0
  222. machineconfig/scripts/__pycache__/__init__.cpython-311.pyc +0 -0
  223. machineconfig/scripts/linux/activate_ve +0 -87
  224. machineconfig/scripts/python/__pycache__/__init__.cpython-311.pyc +0 -0
  225. machineconfig/scripts/python/__pycache__/cloud_copy.cpython-311.pyc +0 -0
  226. machineconfig/scripts/python/__pycache__/cloud_mount.cpython-311.pyc +0 -0
  227. machineconfig/scripts/python/__pycache__/cloud_sync.cpython-311.pyc +0 -0
  228. machineconfig/scripts/python/__pycache__/croshell.cpython-311.pyc +0 -0
  229. machineconfig/scripts/python/__pycache__/devops.cpython-311.pyc +0 -0
  230. machineconfig/scripts/python/__pycache__/devops_backup_retrieve.cpython-311.pyc +0 -0
  231. machineconfig/scripts/python/__pycache__/devops_devapps_install.cpython-311.pyc +0 -0
  232. machineconfig/scripts/python/__pycache__/devops_update_repos.cpython-311.pyc +0 -0
  233. machineconfig/scripts/python/__pycache__/fire_agents.cpython-311.pyc +0 -0
  234. machineconfig/scripts/python/__pycache__/fire_jobs.cpython-311.pyc +0 -0
  235. machineconfig/scripts/python/__pycache__/fire_jobs.cpython-313.pyc +0 -0
  236. machineconfig/scripts/python/__pycache__/get_zellij_cmd.cpython-311.pyc +0 -0
  237. machineconfig/scripts/python/__pycache__/repos.cpython-311.pyc +0 -0
  238. machineconfig/scripts/python/ai/__pycache__/init.cpython-311.pyc +0 -0
  239. machineconfig/scripts/python/ai/__pycache__/mcinit.cpython-311.pyc +0 -0
  240. machineconfig/scripts/python/helpers/__pycache__/__init__.cpython-311.pyc +0 -0
  241. machineconfig/scripts/python/helpers/__pycache__/__init__.cpython-313.pyc +0 -0
  242. machineconfig/scripts/python/helpers/__pycache__/cloud_helpers.cpython-311.pyc +0 -0
  243. machineconfig/scripts/python/helpers/__pycache__/helpers2.cpython-311.pyc +0 -0
  244. machineconfig/scripts/python/helpers/__pycache__/helpers4.cpython-311.pyc +0 -0
  245. machineconfig/scripts/python/helpers/__pycache__/helpers4.cpython-313.pyc +0 -0
  246. machineconfig/scripts/python/helpers/__pycache__/repo_sync_helpers.cpython-311.pyc +0 -0
  247. machineconfig/scripts/windows/activate_ve.ps1 +0 -54
  248. machineconfig/setup_linux/web_shortcuts/all.sh +0 -48
  249. machineconfig/setup_linux/web_shortcuts/update_system.sh +0 -48
  250. machineconfig/utils/utils.py +0 -95
  251. /machineconfig/setup_linux/web_shortcuts/{tmp.sh → android.sh} +0 -0
  252. {machineconfig-2.0.dist-info → machineconfig-2.2.dist-info}/WHEEL +0 -0
  253. {machineconfig-2.0.dist-info → machineconfig-2.2.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,4 @@
1
- from machineconfig.utils.path_reduced import P as PathExtended, PLike
1
+ from machineconfig.utils.path_reduced import PathExtended as PathExtended, PLike
2
2
  from machineconfig.utils.utils2 import randstr
3
3
  from rich.console import Console
4
4
  from rich.panel import Panel
@@ -19,6 +19,7 @@ def build_links(target_paths: list[tuple[PLike, str]], repo_root: PLike):
19
19
  target_dirs_filtered.append((a_dir_obj, a_name))
20
20
 
21
21
  import git
22
+
22
23
  repo = git.Repo(repo_root, search_parent_directories=True)
23
24
  root_maybe = repo.working_tree_dir
24
25
  assert root_maybe is not None
@@ -31,49 +32,129 @@ def build_links(target_paths: list[tuple[PLike, str]], repo_root: PLike):
31
32
  links_path = repo_root_obj.joinpath("links", a_name)
32
33
  links_path.symlink_to(target=a_target_path)
33
34
 
34
- def symlink_func(this: PathExtended, to_this: PathExtended, prioritize_to_this: bool=True):
35
+
36
+ def symlink_func(this: PathExtended, to_this: PathExtended, prioritize_to_this: bool):
35
37
  """helper function. creates a symlink from `this` to `to_this`.
36
- What can go wrong?
37
- depending on this and to_this existence, one will be prioretized depending on overwrite value.
38
- True means this will potentially be overwritten (depending on whether to_this exists or not)
39
- False means to_this will potentially be overwittten."""
38
+
39
+ this: exists AND to_this exists AND this is a symlink pointing to to_this ===> Resolution: AUTO: do nothing, already linked correctly.
40
+ this: exists AND to_this exists AND this is a symlink pointing to somewhere else ===> Resolution: AUTO: delete this symlink, create symlink to to_this
41
+ this: exists AND to_this exists AND this is a concrete path ===> Resolution: DANGER: require user input to decide (param prioritize_to_this). Give two options: 1) prioritize `this`: to_this is backed up as to_this.orig_<randstr()>, to_this is deleted, and symlink is created from this to to_this as normal; 2) prioritize `to_this`: `this` is backed up as this.orig_<randstr()>, `this` is deleted, and symlink is created from this to to_this as normal.
42
+
43
+ this: exists AND to_this doesn't exist AND this is a symlink pointing to somewhere else ===> Resolution: AUTO: delete this symlink, create symlink to to_this (touch to_this)
44
+ this: exists AND to_this doesn't exist AND this is a symlink pointing to to_this ===> Resolution: AUTO: delete this symlink, create symlink to to_this (touch to_this)
45
+ this: exists AND to_this doesn't exist AND this is a concrete path ===> Resolution: AUTO: move this to to_this, then create symlink from this to to_this.
46
+
47
+ this: doesn't exist AND to_this exists ===> Resolution: AUTO: create link from this to to_this
48
+ this: doesn't exist AND to_this doesn't exist ===> Resolution: AUTO: create link from this to to_this (touch to_this)
49
+
50
+ """
40
51
  this = PathExtended(this).expanduser().absolute()
41
52
  to_this = PathExtended(to_this).expanduser().absolute()
42
- if this.is_symlink(): this.delete(sure=True) # delete if it exists as symblic link, not a concrete path.
43
- if this.exists(): # this is a problem. It will be resolved via `overwrite`
44
- if prioritize_to_this is True: # it *can* be deleted, but let's look at target first.
45
- if to_this.exists(): # this exists, to_this as well. to_this is prioritized.
46
- this.append(f".orig_{randstr()}", inplace=True) # rename is better than deletion
47
- else: this.move(path=to_this) # this exists, to_this doesn't. to_this is prioritized.
48
- elif prioritize_to_this is False: # don't sacrefice this, sacrefice to_this.
49
- 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
50
- else: this.move(path=to_this) # this exists, to_this doesn't, this is prioritized.
51
- else: # this doesn't exist.
52
- if not to_this.exists():
53
+ # Case analysis based on docstring
54
+ if this.exists():
55
+ if to_this.exists():
56
+ if this.is_symlink():
57
+ # Check if symlink already points to correct target
58
+ try:
59
+ if this.readlink().resolve() == to_this.resolve():
60
+ # Case: this exists AND to_this exists AND this is a symlink pointing to to_this
61
+ console.print(Panel(f"✅ ALREADY LINKED | {this} ➡️ {to_this}", title="Already Linked", expand=False))
62
+ return
63
+ else:
64
+ # Case: this exists AND to_this exists AND this is a symlink pointing to somewhere else
65
+ console.print(Panel(f"🔄 RELINKING | Updating symlink from {this} ➡️ {to_this}", title="Relinking", expand=False))
66
+ this.delete(sure=True)
67
+ except OSError:
68
+ # Broken symlink case
69
+ console.print(Panel(f"🔄 FIXING BROKEN LINK | Fixing broken symlink from {this} ➡️ {to_this}", title="Fixing Broken Link", expand=False))
70
+ this.delete(sure=True)
71
+ else:
72
+ # Case: this exists AND to_this exists AND this is a concrete path
73
+ if prioritize_to_this:
74
+ # prioritize `to_this`: `this` is backed up, `this` is deleted, symlink created
75
+ backup_name = f"{this}.orig_{randstr()}"
76
+ console.print(Panel(f"📦 BACKING UP | Moving {this} to {backup_name}, prioritizing {to_this}", title="Backing Up", expand=False))
77
+ this.move(path=backup_name)
78
+ else:
79
+ # prioritize `this`: to_this is backed up, to_this is deleted, this content moved to to_this location
80
+ backup_name = f"{to_this}.orig_{randstr()}"
81
+ console.print(Panel(f"📦 BACKING UP | Moving {to_this} to {backup_name}, prioritizing {this}", title="Backing Up", expand=False))
82
+ to_this.move(path=backup_name)
83
+ this.move(path=to_this)
84
+ else:
85
+ # to_this doesn't exist
86
+ if this.is_symlink():
87
+ # Case: this exists AND to_this doesn't exist AND this is a symlink (pointing anywhere)
88
+ console.print(Panel(f"🔄 RELINKING | Updating symlink from {this} ➡️ {to_this}", title="Relinking", expand=False))
89
+ this.delete(sure=True)
90
+ # Create to_this
91
+ to_this.parent.mkdir(parents=True, exist_ok=True)
92
+ to_this.touch()
93
+ else:
94
+ # Case: this exists AND to_this doesn't exist AND this is a concrete path
95
+ console.print(Panel(f"📁 MOVING | Moving {this} to {to_this}, then creating symlink", title="Moving", expand=False))
96
+ this.move(path=to_this)
97
+ else:
98
+ # this doesn't exist
99
+ if to_this.exists():
100
+ # Case: this doesn't exist AND to_this exists
101
+ console.print(Panel(f"🆕 NEW LINK | Creating new symlink from {this} ➡️ {to_this}", title="New Link", expand=False))
102
+ else:
103
+ # Case: this doesn't exist AND to_this doesn't exist
104
+ console.print(Panel(f"🆕 NEW LINK & TARGET | Creating {to_this} and symlink from {this} ➡️ {to_this}", title="New Link & Target", expand=False))
53
105
  to_this.parent.mkdir(parents=True, exist_ok=True)
54
- to_this.touch() # we have to touch it (file) or create it (folder)
106
+ to_this.touch()
107
+ # Create the symlink
55
108
  try:
56
109
  console.print(Panel(f"🔗 LINKING | Creating symlink from {this} ➡️ {to_this}", title="Linking", expand=False))
57
110
  PathExtended(this).symlink_to(target=to_this, verbose=True, overwrite=True)
58
111
  except Exception as ex:
59
112
  console.print(Panel(f"❌ ERROR | Failed at linking {this} ➡️ {to_this}. Reason: {ex}", title="Error", expand=False))
60
113
 
61
- def symlink_copy(this: PathExtended, to_this: PathExtended, prioritize_to_this: bool=True):
114
+
115
+ def symlink_copy(this: PathExtended, to_this: PathExtended, prioritize_to_this: bool = True):
62
116
  this = PathExtended(this).expanduser().absolute()
63
117
  to_this = PathExtended(to_this).expanduser().absolute()
64
- if this.is_symlink(): this.delete(sure=True) # delete if it exists as symblic link, not a concrete path.
65
- if this.exists(): # this is a problem. It will be resolved via `overwrite`
66
- if prioritize_to_this is True: # it *can* be deleted, but let's look at target first.
67
- if to_this.exists(): # this exists, to_this as well. to_this is prioritized.
68
- this.append(f".orig_{randstr()}", inplace=True) # rename is better than deletion
69
- else: this.move(path=to_this) # this exists, to_this doesn't. to_this is prioritized.
70
- elif prioritize_to_this is False: # don't sacrefice this, sacrefice to_this.
71
- 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
72
- else: this.move(path=to_this) # this exists, to_this doesn't, this is prioritized.
73
- else: # this doesn't exist.
74
- if not to_this.exists():
118
+ # Case analysis based on docstring of symlink_func
119
+ if this.exists():
120
+ if to_this.exists():
121
+ if this.is_symlink():
122
+ try:
123
+ if this.readlink().resolve() == to_this.resolve():
124
+ console.print(Panel(f"✅ ALREADY LINKED | {this} ➡️ {to_this}", title="Already Linked", expand=False))
125
+ return
126
+ else:
127
+ console.print(Panel(f"🔄 RELINKING | Updating symlink from {this} ➡️ {to_this}", title="Relinking", expand=False))
128
+ this.delete(sure=True)
129
+ except OSError:
130
+ console.print(Panel(f"🔄 FIXING BROKEN LINK | Fixing broken symlink from {this} ➡️ {to_this}", title="Fixing Broken Link", expand=False))
131
+ this.delete(sure=True)
132
+ else:
133
+ if prioritize_to_this:
134
+ backup_name = f"{this}.orig_{randstr()}"
135
+ console.print(Panel(f"📦 BACKING UP | Moving {this} to {backup_name}, prioritizing {to_this}", title="Backing Up", expand=False))
136
+ this.move(path=backup_name)
137
+ else:
138
+ backup_name = f"{to_this}.orig_{randstr()}"
139
+ console.print(Panel(f"📦 BACKING UP | Moving {to_this} to {backup_name}, prioritizing {this}", title="Backing Up", expand=False))
140
+ to_this.move(path=backup_name)
141
+ this.move(path=to_this)
142
+ else:
143
+ if this.is_symlink():
144
+ console.print(Panel(f"🔄 RELINKING | Updating symlink from {this} ➡️ {to_this}", title="Relinking", expand=False))
145
+ this.delete(sure=True)
146
+ to_this.parent.mkdir(parents=True, exist_ok=True)
147
+ to_this.touch()
148
+ else:
149
+ console.print(Panel(f"📁 MOVING | Moving {this} to {to_this}, then copying", title="Moving", expand=False))
150
+ this.move(path=to_this)
151
+ else:
152
+ if to_this.exists():
153
+ console.print(Panel(f"🆕 NEW LINK | Copying {to_this} to {this}", title="New Link", expand=False))
154
+ else:
155
+ console.print(Panel(f"🆕 NEW LINK & TARGET | Creating {to_this} and copying to {this}", title="New Link & Target", expand=False))
75
156
  to_this.parent.mkdir(parents=True, exist_ok=True)
76
- to_this.touch() # we have to touch it (file) or create it (folder)
157
+ to_this.touch()
77
158
  try:
78
159
  console.print(Panel(f"📋 COPYING | Copying {to_this} to {this}", title="Copying", expand=False))
79
160
  to_this.copy(path=this, overwrite=True, verbose=True)
@@ -0,0 +1,211 @@
1
+ """Notifications Module"""
2
+
3
+ # from crocodile.core import install_n_import
4
+ # from crocodile.file_management import P, Read
5
+ from pathlib import Path
6
+
7
+ # from crocodile.meta import RepeatUntilNoException
8
+ import smtplib
9
+ import imaplib
10
+
11
+ # from email import message
12
+ # from email import encoders
13
+ # from email.mime.base import MIMEBase
14
+ from email.mime.text import MIMEText
15
+ from email.mime.multipart import MIMEMultipart
16
+ from typing import Optional, Any, Union
17
+ from markdown import markdown
18
+
19
+
20
+ def download_to_memory(path: Path, allow_redirects: bool = True, timeout: Optional[float] = None, params: Any = None) -> "Any":
21
+ import requests
22
+
23
+ return requests.get(
24
+ path.as_posix().replace("https:/", "https://").replace("http:/", "http://"), allow_redirects=allow_redirects, timeout=timeout, params=params
25
+ ) # Alternative: from urllib import request; request.urlopen(url).read().decode('utf-8').
26
+
27
+
28
+ def get_github_markdown_css() -> str:
29
+ pp = r"https://raw.githubusercontent.com/sindresorhus/github-markdown-css/main/github-markdown-dark.css"
30
+ return download_to_memory(Path(pp)).text
31
+
32
+
33
+ def md2html(body: str):
34
+ gh_style = Path(__file__).parent.joinpath("gh_style.css").read_text()
35
+ return f"""
36
+ <!DOCTYPE html>
37
+ <html>
38
+ <meta name="viewport" content="width=device-width, initial-scale=1">
39
+ <style>
40
+ {gh_style}
41
+ .markdown-body {{
42
+ box-sizing: border-box;
43
+ min-width: 200px;
44
+ max-width: 1350px;
45
+ margin: 0 auto;
46
+ padding: 45px;
47
+ line-height: 1.8;
48
+ }}
49
+ @media (max-width: 767px) {{.markdown-body {{padding: 15px;}}
50
+ }}
51
+ </style>
52
+ <body>
53
+ <div class="markdown-body">
54
+ {markdown(body)}
55
+ </div>
56
+ </body>
57
+ </html>"""
58
+
59
+
60
+ class Email:
61
+ @staticmethod
62
+ def get_source_of_truth():
63
+ path = Path.home().joinpath("dotfiles/machineconfig/emails.ini")
64
+ if not path.exists():
65
+ raise FileNotFoundError(f"""File not found: {path}. It should be an ini file with this structure
66
+ [resend]
67
+ api_key = xxx
68
+
69
+ [config1]
70
+ email_add = a@b.com
71
+ password = 123
72
+ smtp_host = a@b.com
73
+ smtp_port = 465
74
+ imap_host = b@c.com
75
+ imap_port = 465
76
+ encryption = ssl
77
+
78
+ """)
79
+
80
+ if not Path(path).exists() or Path(path).is_dir():
81
+ raise FileNotFoundError(f"File not found or is a directory: {path}")
82
+ import configparser
83
+
84
+ res = configparser.ConfigParser()
85
+ res.read(filenames=[str(path)], encoding=None)
86
+ return res
87
+
88
+ def __init__(self, config: dict[str, Any]):
89
+ self.config = config
90
+ from smtplib import SMTP_SSL, SMTP
91
+
92
+ self.server: Union[SMTP_SSL, SMTP]
93
+ if config["encryption"].lower() == "ssl":
94
+ self.server = smtplib.SMTP_SSL(host=self.config["smtp_host"], port=self.config["smtp_port"])
95
+ elif config["encryption"].lower() == "tls":
96
+ self.server = smtplib.SMTP(host=self.config["smtp_host"], port=self.config["smtp_port"])
97
+ self.server.login(self.config["email_add"], password=self.config["password"])
98
+
99
+ def send_message(self, to: str, subject: str, body: str, txt_to_html: bool = True, attachments: Optional[list[Any]] = None):
100
+ _ = attachments
101
+ body += "\n\nThis is an automated email sent via crocodile.comms script."
102
+ # msg = message.EmailMessage()
103
+ msg = MIMEMultipart("alternative")
104
+ msg["subject"] = subject
105
+ msg["From"] = self.config["email_add"]
106
+ msg["To"] = to
107
+ # msg['Content-Type'] = "text/html"
108
+ # msg.set_content(body)
109
+
110
+ # <link rel="stylesheet" href="github-markdown.css">
111
+ # <link type="text/css" rel="stylesheet" href="https://raw.githubusercontent.com/sindresorhus/github-markdown-css/main/github-markdown-dark.css" />
112
+
113
+ if txt_to_html:
114
+ body = md2html(body=body)
115
+ msg.attach(MIMEText(body, "html"))
116
+ # if attachments is None: attachments = [] # see: https://fedingo.com/how-to-send-html-mail-with-attachment-using-python/
117
+ # for attachment in attachmenthrs: msg.attach(attachment.read_bytes(), filename=attachment.stem, maintype="image", subtype=attachment.suffix)
118
+ # for attachment in attachments: msg.attach(attachment.read_bytes(), filename=attachment.stem, maintype="application", subtype="octet-stream")
119
+
120
+ self.server.send_message(msg)
121
+
122
+ @staticmethod
123
+ def manage_folders(email_add: str, pwd: str):
124
+ server = imaplib.IMAP4()
125
+ server.starttls()
126
+ server.login(email_add, password=pwd)
127
+
128
+ def send_email(self, to_addrs: str, msg: str):
129
+ return self.server.sendmail(from_addr=self.config["email_add"], to_addrs=to_addrs, msg=msg)
130
+
131
+ def close(self):
132
+ self.server.quit() # Closing is vital as many servers do not allow mutiple connections.
133
+
134
+ @staticmethod
135
+ def send_and_close(config_name: Optional[str], to: str, subject: str, body: str) -> Any:
136
+ """If config_name is None, it sends from a generic email address."""
137
+ if config_name is None:
138
+ raise NotImplementedError(
139
+ "Sending email without a config_name is not implemented. You need to create an emails.ini file in ~/dotfiles/machineconfig/ with your email configuration. See the docstring of the get_source_of_truth method for more information."
140
+ )
141
+ # config = Email.get_source_of_truth()
142
+ # try:
143
+ # api_key = config['resend']['api_key']
144
+ # to = config["resend"]["signup_email"]
145
+ # except KeyError as ke:
146
+ # msggg = "You did not pass a config_name, therefore, the default is to use resend, however, you need to add your resend api key to the emails.ini file."
147
+ # raise KeyError(msggg) from ke
148
+
149
+ # _resend = install_n_import("resend")
150
+ # import resend # type: ignore
151
+ # resend.api_key = api_key
152
+ # r = resend.Emails.send({
153
+ # "from": "onboarding@resend.dev",
154
+ # "to": to,
155
+ # "subject": subject,
156
+ # "html": md2html(body=body)
157
+ # })
158
+ # return r
159
+
160
+ else:
161
+ config = dict(Email.get_source_of_truth()[config_name])
162
+ tmp = Email(config=config)
163
+ tmp.send_message(to=to, subject=subject, body=body)
164
+ tmp.close()
165
+
166
+ # @staticmethod
167
+ # def send_m365(to: list[str], subject: str, body: Optional[str], body_file: Optional[str], body_content_type: Literal["HTML", "Text"], attachments: Optional[list[Path]] = None) -> None:
168
+ # if body_file is not None:
169
+ # assert body is None, "You cannot pass both body and body_file."
170
+ # body_file_path = Path(body_file)
171
+ # assert body_file_path.exists(), f"File not found: {body_file_path}"
172
+ # else:
173
+ # body_file_path = None
174
+ # assert body is not None, "You must pass either body or body_file."
175
+ # from crocodile.meta import Terminal
176
+
177
+ # to_str = ",".join(to)
178
+ # attachments_str = " ".join([f"--attachment {str(p)}" for p in attachments]) if attachments is not None else ""
179
+
180
+ # if body_file is not None:
181
+ # body_arg = f"--bodyContents @{body_file_path}"
182
+ # else:
183
+ # body_arg = f'"{body}"'
184
+ # cmd = f"""m365 outlook mail send --verbose --saveToSentItems --importance normal --bodyContentType {body_content_type} --bodyContents {body_arg} --subject "{subject}" --to {to_str} {attachments_str}"""
185
+ # response = Terminal().run(cmd, shell="powershell")
186
+ # response.print(desc="Email sending response")
187
+
188
+
189
+ # class PhoneNotification: # security concerns: avoid using this.
190
+ # def __init__(self, token: Optional[str]):
191
+ # if token is None:
192
+ # path = P.home().joinpath("dotfiles/machineconfig/phone_notification.ini")
193
+ # ini = Read.ini(path)
194
+ # token_ = ini["default"]["token"]
195
+ # else:
196
+ # token_ = token
197
+ # pushbullet = install_n_import("pushbullet")
198
+ # self.api = pushbullet.Pushbullet(token_)
199
+ # def send_notification(self, title: str = "Note From Python", body: str = "A notfication"):
200
+ # self.api.push_note(title=title, body=body)
201
+ # @staticmethod
202
+ # def open_website():
203
+ # P(r"https://www.pushbullet.com/")()
204
+ # @staticmethod # https://www.youtube.com/watch?v=tbzPcKRZlHg
205
+ # def try_me(bulletpoint_token: str):
206
+ # n = PhoneNotification(bulletpoint_token)
207
+ # n.send_notification()
208
+
209
+
210
+ if __name__ == "__main__":
211
+ pass
@@ -1,4 +1,3 @@
1
-
2
1
  from pathlib import Path
3
2
  from rich.text import Text
4
3
  from rich.panel import Panel
@@ -10,53 +9,43 @@ from typing import Optional, Union, TypeVar, Iterable
10
9
  T = TypeVar("T")
11
10
 
12
11
 
12
+ def check_tool_exists(tool_name: str) -> bool:
13
+ if platform.system() == "Windows": tool_name = tool_name.replace(".exe", "") + ".exe"
13
14
 
14
- def check_tool_exists(tool_name: str, install_script: Optional[str] = None) -> bool:
15
- if platform.system() == "Windows":
16
- tool_name = tool_name.replace(".exe", "") + ".exe"
15
+ from machineconfig.utils.source_of_truth import WINDOWS_INSTALL_PATH, LINUX_INSTALL_PATH
17
16
 
18
- if platform.system() == "Windows": cmd = "where.exe"
19
- elif platform.system() in ["Linux", "Darwin"]: cmd = "which"
17
+ if platform.system() == "Windows":
18
+ cmd = "where.exe"
19
+ root_path = Path(WINDOWS_INSTALL_PATH)
20
+ elif platform.system() in ["Linux", "Darwin"]:
21
+ cmd = "which"
22
+ root_path = Path(LINUX_INSTALL_PATH)
20
23
  else: raise NotImplementedError(f"platform {platform.system()} not implemented")
21
24
 
22
- try:
23
- _tmp = subprocess.check_output([cmd, tool_name], stderr=subprocess.DEVNULL)
24
- res: bool=True
25
- except (subprocess.CalledProcessError, FileNotFoundError):
26
- res = False
27
- if res is False and install_script is not None:
28
- console = Console()
29
- console.print(Panel(f"📥 INSTALLING TOOL | Installing {tool_name}...", border_style="bold blue", expand=False))
30
- result = subprocess.run(install_script, shell=True, capture_output=True, text=True)
31
- print(f"Command: {install_script}")
32
- if result.stdout:
33
- print(f"STDOUT: {result.stdout}")
34
- if result.stderr:
35
- print(f"STDERR: {result.stderr}")
36
- print(f"Return code: {result.returncode}")
37
- return check_tool_exists(tool_name=tool_name, install_script=None)
38
- return res
39
-
40
-
41
- def choose_one_option(options: Iterable[T], header: str="", tail: str="", prompt: str="", msg: str="",
42
- default: Optional[T] = None, fzf: bool=False, custom_input: bool=False) -> T:
43
- choice_key = display_options(msg=msg, options=options, header=header, tail=tail, prompt=prompt,
44
- default=default, fzf=fzf, multi=False, custom_input=custom_input)
25
+ _ = cmd
26
+ # try:
27
+ # _tmp = subprocess.check_output([cmd, tool_name], stderr=subprocess.DEVNULL)
28
+ # res: bool = True
29
+ # except (subprocess.CalledProcessError, FileNotFoundError):
30
+ # res = False
31
+ # return res
32
+ return root_path.joinpath(tool_name).is_file()
33
+
34
+
35
+ def choose_one_option(options: Iterable[T], header: str = "", tail: str = "", prompt: str = "", msg: str = "", default: Optional[T] = None, fzf: bool = False, custom_input: bool = False) -> T:
36
+ choice_key = display_options(msg=msg, options=options, header=header, tail=tail, prompt=prompt, default=default, fzf=fzf, multi=False, custom_input=custom_input)
45
37
  assert not isinstance(choice_key, list)
46
38
  return choice_key
47
39
 
48
40
 
49
- def choose_multiple_options(options: Iterable[T], header: str="", tail: str="", prompt: str="", msg: str="",
50
- default: Optional[T] = None, custom_input: bool=False) -> list[T]:
51
- choice_key = display_options(msg=msg, options=options, header=header, tail=tail, prompt=prompt,
52
- default=default, fzf=True, multi=True,
53
- custom_input=custom_input)
54
- if isinstance(choice_key, list): return choice_key
41
+ def choose_multiple_options(options: Iterable[T], header: str = "", tail: str = "", prompt: str = "", msg: str = "", default: Optional[T] = None, custom_input: bool = False) -> list[T]:
42
+ choice_key = display_options(msg=msg, options=options, header=header, tail=tail, prompt=prompt, default=default, fzf=True, multi=True, custom_input=custom_input)
43
+ if isinstance(choice_key, list):
44
+ return choice_key
55
45
  return [choice_key]
56
46
 
57
47
 
58
- def display_options(msg: str, options: Iterable[T], header: str="", tail: str="", prompt: str="",
59
- default: Optional[T] = None, fzf: bool=False, multi: bool=False, custom_input: bool=False) -> Union[T, list[T]]:
48
+ def display_options(msg: str, options: Iterable[T], header: str = "", tail: str = "", prompt: str = "", default: Optional[T] = None, fzf: bool = False, multi: bool = False, custom_input: bool = False) -> Union[T, list[T]]:
60
49
  # TODO: replace with https://github.com/tmbo/questionary
61
50
  # # also see https://github.com/charmbracelet/gum
62
51
  tool_name = "fzf"
@@ -65,6 +54,7 @@ def display_options(msg: str, options: Iterable[T], header: str="", tail: str=""
65
54
  console = Console()
66
55
  if fzf and check_tool_exists(tool_name):
67
56
  from pyfzf.pyfzf import FzfPrompt
57
+
68
58
  fzf_prompt = FzfPrompt()
69
59
  nl = "\n"
70
60
  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, ' ')}")
@@ -84,7 +74,8 @@ def display_options(msg: str, options: Iterable[T], header: str="", tail: str=""
84
74
  if default is not None:
85
75
  assert default in options, f"Default `{default}` option not in options `{list(options)}`"
86
76
  default_msg = Text(" <<<<-------- DEFAULT", style="bold red")
87
- else: default_msg = Text("")
77
+ else:
78
+ default_msg = Text("")
88
79
  txt = Text("\n" + msg + "\n")
89
80
  for idx, key in enumerate(options):
90
81
  txt = txt + Text(f"{idx:2d} ", style="bold blue") + str(key) + (default_msg if default is not None and default == key else "") + "\n"
@@ -111,7 +102,8 @@ def display_options(msg: str, options: Iterable[T], header: str="", tail: str=""
111
102
  if choice_string in options_strings: # string input
112
103
  choice_idx = options_strings.index(choice_one) # type: ignore
113
104
  choice_one = list(options)[choice_idx]
114
- elif custom_input: return str(choice_string) # type: ignore
105
+ elif custom_input:
106
+ return str(choice_string) # type: ignore
115
107
  else:
116
108
  _ = ie
117
109
  # raise ValueError(f"Unknown choice. {choice_string}") from ie
@@ -129,7 +121,8 @@ def display_options(msg: str, options: Iterable[T], header: str="", tail: str=""
129
121
  console.print(Panel(f"❓ Unknown choice: '{choice_string}'", title="Error", expand=False))
130
122
  return display_options(msg=msg, options=options, header=header, tail=tail, prompt=prompt, default=default, fzf=fzf, multi=multi, custom_input=custom_input)
131
123
  console.print(Panel(f"✅ Selected option {choice_idx}: {choice_one}", title="Selected", expand=False))
132
- if multi: return [choice_one]
124
+ if multi:
125
+ return [choice_one]
133
126
  return choice_one
134
127
 
135
128
 
@@ -141,16 +134,22 @@ def choose_cloud_interactively() -> str:
141
134
  if isinstance(tmp, str):
142
135
  remotes: list[str] = [x.replace(":", "") for x in tmp.splitlines()]
143
136
 
144
- else: raise ValueError(f"Got {tmp} from rclone listremotes")
137
+ else:
138
+ raise ValueError(f"Got {tmp} from rclone listremotes")
145
139
  if len(remotes) == 0:
146
140
  raise RuntimeError("You don't have remotes. Configure your rclone first to get cloud services access.")
147
141
  cloud: str = choose_one_option(msg="WHICH CLOUD?", options=list(remotes), default=remotes[0], fzf=True)
148
142
  console.print(Panel(f"✅ SELECTED CLOUD | {cloud}", border_style="bold blue", expand=False))
149
143
  return cloud
150
144
 
145
+
151
146
  def get_ssh_hosts() -> list[str]:
152
147
  from paramiko import SSHConfig
148
+
153
149
  c = SSHConfig()
154
150
  c.parse(open(Path.home().joinpath(".ssh/config"), encoding="utf-8"))
155
151
  return list(c.get_hostnames())
156
- def choose_ssh_host(multi: bool=True): return display_options(msg="", options=get_ssh_hosts(), multi=multi, fzf=True)
152
+
153
+
154
+ def choose_ssh_host(multi: bool = True):
155
+ return display_options(msg="", options=get_ssh_hosts(), multi=multi, fzf=True)
@@ -1,4 +1,4 @@
1
- from machineconfig.utils.path_reduced import P as PathExtended
1
+ from machineconfig.utils.path_reduced import PathExtended as PathExtended
2
2
  from machineconfig.utils.options import choose_one_option
3
3
  from rich.console import Console
4
4
  from rich.panel import Panel
@@ -11,12 +11,14 @@ from pathlib import Path
11
11
  T = TypeVar("T")
12
12
  console = Console()
13
13
 
14
+
14
15
  def sanitize_path(a_path: PathExtended) -> PathExtended:
15
16
  path = PathExtended(a_path)
16
17
  if Path.cwd() == Path.home() and not path.exists():
17
18
  result = input("Current working directory is home, and passed path is not full path, are you sure you want to continue, [y]/n? ") or "y"
18
19
  if result == "y":
19
20
  import sys
21
+
20
22
  sys.exit()
21
23
  if path.as_posix().startswith("/home") or path.as_posix().startswith("/Users"):
22
24
  if platform.system() == "Windows": # path copied from Linux/Mac to Windows
@@ -70,7 +72,8 @@ def match_file_name(sub_string: str, search_root: PathExtended) -> PathExtended:
70
72
  search_root_obj = search_root.absolute()
71
73
  # assume subscript is filename only, not a sub_path. There is no need to fzf over the paths.
72
74
  filename_matches, partial_path_matches = find_scripts(search_root_obj, sub_string)
73
- if len(filename_matches) == 1: return PathExtended(filename_matches[0])
75
+ if len(filename_matches) == 1:
76
+ return PathExtended(filename_matches[0])
74
77
  console.print(Panel(f"Partial filename match with case-insensitivity failed. This generated #{len(filename_matches)} results.", title="Search", expand=False))
75
78
  if len(filename_matches) < 10:
76
79
  print("\n".join([a_potential_match.as_posix() for a_potential_match in filename_matches]))
@@ -78,7 +81,8 @@ def match_file_name(sub_string: str, search_root: PathExtended) -> PathExtended:
78
81
  print("Try to narrow down filename_matches search by case-sensitivity.")
79
82
  # let's see if avoiding .lower() helps narrowing down to one result
80
83
  reduced_scripts = [a_potential_match for a_potential_match in filename_matches if sub_string in a_potential_match.name]
81
- if len(reduced_scripts) == 1: return PathExtended(reduced_scripts[0])
84
+ if len(reduced_scripts) == 1:
85
+ return PathExtended(reduced_scripts[0])
82
86
  elif len(reduced_scripts) > 1:
83
87
  choice = choose_one_option(msg="Multiple matches found", options=reduced_scripts, fzf=True)
84
88
  return PathExtended(choice)
@@ -92,18 +96,21 @@ def match_file_name(sub_string: str, search_root: PathExtended) -> PathExtended:
92
96
  elif len(partial_path_matches) > 1:
93
97
  print("Try to narrow down partial_path_matches search by case-sensitivity.")
94
98
  reduced_scripts = [a_potential_match for a_potential_match in partial_path_matches if sub_string in a_potential_match.as_posix()]
95
- if len(reduced_scripts) == 1: return PathExtended(reduced_scripts[0])
99
+ if len(reduced_scripts) == 1:
100
+ return PathExtended(reduced_scripts[0])
96
101
  print(f"Result: This still generated {len(reduced_scripts)} results.")
97
102
  try:
98
103
  fzf_cmd = f"cd '{search_root_obj}'; fd --type file --strip-cwd-prefix | fzf --ignore-case --exact --query={sub_string}"
99
104
  console.print(Panel(f"🔍 Second attempt: SEARCH STRATEGY | Using fd to search for '{sub_string}' in '{search_root_obj}' ...\n{fzf_cmd}", title="Search Strategy", expand=False))
100
- search_res_raw = subprocess.run(fzf_cmd, stdout=subprocess.PIPE, text=True, check=True, shell=True,).stdout
105
+ search_res_raw = subprocess.run(fzf_cmd, stdout=subprocess.PIPE, text=True, check=True, shell=True).stdout
101
106
  search_res = search_res_raw.strip().split("\\n")[:-1]
102
107
  except subprocess.CalledProcessError as cpe:
103
108
  console.print(Panel(f"❌ ERROR | FZF search failed with '{sub_string}' in '{search_root_obj}'.\n{cpe}", title="Error", expand=False))
104
109
  import sys
110
+
105
111
  sys.exit(f"💥 FILE NOT FOUND | Path {sub_string} does not exist @ root {search_root_obj}. No search results.")
106
- if len(search_res) == 1: return search_root_obj.joinpath(search_res_raw)
112
+ if len(search_res) == 1:
113
+ return search_root_obj.joinpath(search_res_raw)
107
114
 
108
115
  print(f"⚠️ WARNING | Multiple search results found for `{sub_string}`\n'{search_res_raw}'")
109
116
  cmd = f"cd '{search_root_obj}'; fd --type file | fzf --select-1 --query={sub_string}"