machineconfig 2.1__py3-none-any.whl → 2.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of machineconfig might be problematic. Click here for more details.
- machineconfig/cluster/sessions_managers/enhanced_command_runner.py +0 -2
- machineconfig/cluster/sessions_managers/layout_types.py +29 -0
- machineconfig/cluster/sessions_managers/wt_local.py +68 -62
- machineconfig/cluster/sessions_managers/wt_local_manager.py +51 -22
- machineconfig/cluster/sessions_managers/wt_remote.py +30 -108
- machineconfig/cluster/sessions_managers/wt_remote_manager.py +14 -11
- machineconfig/cluster/sessions_managers/wt_utils/layout_generator.py +33 -37
- machineconfig/cluster/sessions_managers/wt_utils/process_monitor.py +22 -17
- machineconfig/cluster/sessions_managers/wt_utils/session_manager.py +59 -10
- machineconfig/cluster/sessions_managers/wt_utils/status_reporter.py +16 -14
- machineconfig/cluster/sessions_managers/zellij_local.py +75 -57
- machineconfig/cluster/sessions_managers/zellij_local_manager.py +51 -23
- machineconfig/cluster/sessions_managers/zellij_remote.py +47 -27
- machineconfig/cluster/sessions_managers/zellij_remote_manager.py +13 -12
- machineconfig/cluster/sessions_managers/zellij_utils/example_usage.py +14 -10
- machineconfig/cluster/sessions_managers/zellij_utils/layout_generator.py +31 -15
- machineconfig/cluster/sessions_managers/zellij_utils/process_monitor.py +47 -21
- machineconfig/cluster/sessions_managers/zellij_utils/session_manager.py +1 -1
- machineconfig/cluster/sessions_managers/zellij_utils/status_reporter.py +8 -7
- machineconfig/cluster/templates/utils.py +0 -35
- machineconfig/jobs/python/check_installations.py +1 -1
- machineconfig/jobs/python_custom_installers/dev/code.py +0 -13
- machineconfig/jobs/python_generic_installers/config.json +1 -1
- machineconfig/profile/create.py +13 -4
- machineconfig/profile/create_hardlinks.py +3 -1
- machineconfig/profile/shell.py +8 -7
- machineconfig/scripts/__init__.py +0 -2
- machineconfig/scripts/linux/devops +6 -4
- machineconfig/scripts/python/ai/generate_files.py +14 -15
- machineconfig/scripts/python/ai/mcinit.py +8 -5
- machineconfig/scripts/python/archive/tmate_conn.py +5 -5
- machineconfig/scripts/python/archive/tmate_start.py +7 -7
- machineconfig/scripts/python/choose_wezterm_theme.py +35 -32
- machineconfig/scripts/python/cloud_copy.py +22 -13
- machineconfig/scripts/python/cloud_mount.py +35 -23
- machineconfig/scripts/python/cloud_repo_sync.py +38 -25
- machineconfig/scripts/python/cloud_sync.py +4 -4
- machineconfig/scripts/python/croshell.py +37 -28
- machineconfig/scripts/python/devops.py +46 -27
- machineconfig/scripts/python/devops_add_identity.py +15 -25
- machineconfig/scripts/python/devops_add_ssh_key.py +7 -7
- machineconfig/scripts/python/devops_backup_retrieve.py +17 -15
- machineconfig/scripts/python/devops_devapps_install.py +26 -20
- machineconfig/scripts/python/devops_update_repos.py +142 -57
- machineconfig/scripts/python/dotfile.py +16 -14
- machineconfig/scripts/python/fire_agents.py +30 -23
- machineconfig/scripts/python/fire_jobs.py +86 -98
- machineconfig/scripts/python/fire_jobs_args_helper.py +84 -0
- machineconfig/scripts/python/fire_jobs_layout_helper.py +66 -0
- machineconfig/scripts/python/ftpx.py +24 -14
- machineconfig/scripts/python/get_zellij_cmd.py +8 -7
- machineconfig/scripts/python/helpers/cloud_helpers.py +33 -28
- machineconfig/scripts/python/helpers/helpers2.py +25 -14
- machineconfig/scripts/python/helpers/helpers4.py +44 -31
- machineconfig/scripts/python/helpers/helpers5.py +1 -1
- machineconfig/scripts/python/helpers/repo_sync_helpers.py +31 -9
- machineconfig/scripts/python/mount_nfs.py +8 -15
- machineconfig/scripts/python/mount_nw_drive.py +10 -5
- machineconfig/scripts/python/mount_ssh.py +8 -6
- machineconfig/scripts/python/repos.py +215 -57
- machineconfig/scripts/python/snapshot.py +0 -1
- machineconfig/scripts/python/start_slidev.py +10 -5
- machineconfig/scripts/python/start_terminals.py +22 -16
- machineconfig/scripts/python/viewer_template.py +0 -1
- machineconfig/scripts/python/wifi_conn.py +49 -76
- machineconfig/scripts/python/wsl_windows_transfer.py +8 -6
- machineconfig/settings/lf/linux/lfrc +1 -0
- machineconfig/setup_linux/web_shortcuts/croshell.sh +5 -0
- machineconfig/setup_linux/web_shortcuts/interactive.sh +1 -1
- machineconfig/setup_linux/web_shortcuts/ssh.sh +0 -4
- machineconfig/setup_windows/wt_and_pwsh/set_pwsh_theme.py +3 -12
- machineconfig/setup_windows/wt_and_pwsh/set_wt_settings.py +1 -1
- machineconfig/utils/code.py +2 -3
- machineconfig/utils/installer.py +2 -2
- machineconfig/utils/installer_utils/installer_abc.py +2 -4
- machineconfig/utils/installer_utils/installer_class.py +6 -4
- machineconfig/utils/links.py +103 -33
- machineconfig/utils/notifications.py +52 -38
- machineconfig/utils/options.py +14 -21
- machineconfig/utils/path.py +12 -12
- machineconfig/utils/path_reduced.py +239 -200
- machineconfig/utils/procs.py +1 -1
- machineconfig/utils/source_of_truth.py +27 -0
- machineconfig/utils/ssh.py +9 -19
- machineconfig/utils/terminal.py +4 -2
- machineconfig/utils/upgrade_packages.py +91 -0
- machineconfig/utils/utils2.py +1 -2
- machineconfig/utils/utils5.py +23 -11
- machineconfig/utils/ve.py +4 -1
- {machineconfig-2.1.dist-info → machineconfig-2.3.dist-info}/METADATA +13 -13
- {machineconfig-2.1.dist-info → machineconfig-2.3.dist-info}/RECORD +105 -121
- machineconfig-2.3.dist-info/entry_points.txt +2 -0
- machineconfig/cluster/sessions_managers/archive/create_zellij_template.py +0 -59
- machineconfig/cluster/sessions_managers/archive/session_managers.py +0 -183
- machineconfig/cluster/sessions_managers/demo_rich_zellij.py +0 -0
- machineconfig/jobs/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/jobs/python_custom_installers/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/jobs/python_generic_installers/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/jobs/python_linux_installers/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/scripts/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/croshell.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/devops.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/devops_devapps_install.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/devops_update_repos.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/fire_jobs.cpython-313.pyc +0 -0
- machineconfig/scripts/python/ai/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/scripts/python/ai/__pycache__/generate_files.cpython-313.pyc +0 -0
- machineconfig/scripts/python/ai/__pycache__/mcinit.cpython-313.pyc +0 -0
- machineconfig/scripts/python/helpers/__pycache__/__init__.cpython-313.pyc +0 -0
- machineconfig/scripts/python/helpers/__pycache__/helpers4.cpython-313.pyc +0 -0
- machineconfig/setup_linux/web_shortcuts/all.sh +0 -48
- machineconfig/setup_linux/web_shortcuts/update_system.sh +0 -48
- machineconfig/utils/utils.py +0 -97
- /machineconfig/cluster/{cloud_manager.py → remote/cloud_manager.py} +0 -0
- /machineconfig/cluster/{data_transfer.py → remote/data_transfer.py} +0 -0
- /machineconfig/cluster/{distribute.py → remote/distribute.py} +0 -0
- /machineconfig/cluster/{file_manager.py → remote/file_manager.py} +0 -0
- /machineconfig/cluster/{job_params.py → remote/job_params.py} +0 -0
- /machineconfig/cluster/{loader_runner.py → remote/loader_runner.py} +0 -0
- /machineconfig/cluster/{remote_machine.py → remote/remote_machine.py} +0 -0
- /machineconfig/cluster/{script_execution.py → remote/script_execution.py} +0 -0
- /machineconfig/cluster/{script_notify_upon_completion.py → remote/script_notify_upon_completion.py} +0 -0
- /machineconfig/{cluster/sessions_managers/archive/__init__.py → scripts/python/fire_jobs_streamlit_helper.py} +0 -0
- /machineconfig/setup_linux/web_shortcuts/{tmp.sh → android.sh} +0 -0
- {machineconfig-2.1.dist-info → machineconfig-2.3.dist-info}/WHEEL +0 -0
- {machineconfig-2.1.dist-info → machineconfig-2.3.dist-info}/top_level.txt +0 -0
machineconfig/utils/links.py
CHANGED
|
@@ -33,31 +33,78 @@ def build_links(target_paths: list[tuple[PLike, str]], repo_root: PLike):
|
|
|
33
33
|
links_path.symlink_to(target=a_target_path)
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
def symlink_func(this: PathExtended, to_this: PathExtended, prioritize_to_this: bool
|
|
36
|
+
def symlink_func(this: PathExtended, to_this: PathExtended, prioritize_to_this: bool):
|
|
37
37
|
"""helper function. creates a symlink from `this` to `to_this`.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
+
"""
|
|
42
51
|
this = PathExtended(this).expanduser().absolute()
|
|
43
52
|
to_this = PathExtended(to_this).expanduser().absolute()
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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)
|
|
50
71
|
else:
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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()
|
|
55
93
|
else:
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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))
|
|
59
105
|
to_this.parent.mkdir(parents=True, exist_ok=True)
|
|
60
|
-
to_this.touch()
|
|
106
|
+
to_this.touch()
|
|
107
|
+
# Create the symlink
|
|
61
108
|
try:
|
|
62
109
|
console.print(Panel(f"🔗 LINKING | Creating symlink from {this} ➡️ {to_this}", title="Linking", expand=False))
|
|
63
110
|
PathExtended(this).symlink_to(target=to_this, verbose=True, overwrite=True)
|
|
@@ -68,23 +115,46 @@ def symlink_func(this: PathExtended, to_this: PathExtended, prioritize_to_this:
|
|
|
68
115
|
def symlink_copy(this: PathExtended, to_this: PathExtended, prioritize_to_this: bool = True):
|
|
69
116
|
this = PathExtended(this).expanduser().absolute()
|
|
70
117
|
to_this = PathExtended(to_this).expanduser().absolute()
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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)
|
|
77
132
|
else:
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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()
|
|
82
148
|
else:
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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))
|
|
86
156
|
to_this.parent.mkdir(parents=True, exist_ok=True)
|
|
87
|
-
to_this.touch()
|
|
157
|
+
to_this.touch()
|
|
88
158
|
try:
|
|
89
159
|
console.print(Panel(f"📋 COPYING | Copying {to_this} to {this}", title="Copying", expand=False))
|
|
90
160
|
to_this.copy(path=this, overwrite=True, verbose=True)
|
|
@@ -1,30 +1,32 @@
|
|
|
1
|
-
|
|
2
|
-
"""Notifications Module
|
|
3
|
-
"""
|
|
1
|
+
"""Notifications Module"""
|
|
4
2
|
|
|
5
3
|
# from crocodile.core import install_n_import
|
|
6
4
|
# from crocodile.file_management import P, Read
|
|
7
5
|
from pathlib import Path
|
|
6
|
+
|
|
8
7
|
# from crocodile.meta import RepeatUntilNoException
|
|
9
8
|
import smtplib
|
|
10
9
|
import imaplib
|
|
10
|
+
|
|
11
11
|
# from email import message
|
|
12
12
|
# from email import encoders
|
|
13
13
|
# from email.mime.base import MIMEBase
|
|
14
14
|
from email.mime.text import MIMEText
|
|
15
15
|
from email.mime.multipart import MIMEMultipart
|
|
16
|
-
from typing import Optional, Any, Union
|
|
16
|
+
from typing import Optional, Any, Union
|
|
17
17
|
from markdown import markdown
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
def download_to_memory(path: Path, allow_redirects: bool = True, timeout: Optional[float] = None, params: Any = None) -> 'Any':
|
|
20
|
+
def download_to_memory(path: Path, allow_redirects: bool = True, timeout: Optional[float] = None, params: Any = None) -> "Any":
|
|
22
21
|
import requests
|
|
23
|
-
|
|
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').
|
|
24
26
|
|
|
25
27
|
|
|
26
28
|
def get_github_markdown_css() -> str:
|
|
27
|
-
pp = r
|
|
29
|
+
pp = r"https://raw.githubusercontent.com/sindresorhus/github-markdown-css/main/github-markdown-dark.css"
|
|
28
30
|
return download_to_memory(Path(pp)).text
|
|
29
31
|
|
|
30
32
|
|
|
@@ -75,8 +77,10 @@ encryption = ssl
|
|
|
75
77
|
|
|
76
78
|
""")
|
|
77
79
|
|
|
78
|
-
if not Path(path).exists() or Path(path).is_dir():
|
|
80
|
+
if not Path(path).exists() or Path(path).is_dir():
|
|
81
|
+
raise FileNotFoundError(f"File not found or is a directory: {path}")
|
|
79
82
|
import configparser
|
|
83
|
+
|
|
80
84
|
res = configparser.ConfigParser()
|
|
81
85
|
res.read(filenames=[str(path)], encoding=None)
|
|
82
86
|
return res
|
|
@@ -84,10 +88,13 @@ encryption = ssl
|
|
|
84
88
|
def __init__(self, config: dict[str, Any]):
|
|
85
89
|
self.config = config
|
|
86
90
|
from smtplib import SMTP_SSL, SMTP
|
|
91
|
+
|
|
87
92
|
self.server: Union[SMTP_SSL, SMTP]
|
|
88
|
-
if config[
|
|
89
|
-
|
|
90
|
-
|
|
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"])
|
|
91
98
|
|
|
92
99
|
def send_message(self, to: str, subject: str, body: str, txt_to_html: bool = True, attachments: Optional[list[Any]] = None):
|
|
93
100
|
_ = attachments
|
|
@@ -95,7 +102,7 @@ encryption = ssl
|
|
|
95
102
|
# msg = message.EmailMessage()
|
|
96
103
|
msg = MIMEMultipart("alternative")
|
|
97
104
|
msg["subject"] = subject
|
|
98
|
-
msg["From"] = self.config[
|
|
105
|
+
msg["From"] = self.config["email_add"]
|
|
99
106
|
msg["To"] = to
|
|
100
107
|
# msg['Content-Type'] = "text/html"
|
|
101
108
|
# msg.set_content(body)
|
|
@@ -103,7 +110,8 @@ encryption = ssl
|
|
|
103
110
|
# <link rel="stylesheet" href="github-markdown.css">
|
|
104
111
|
# <link type="text/css" rel="stylesheet" href="https://raw.githubusercontent.com/sindresorhus/github-markdown-css/main/github-markdown-dark.css" />
|
|
105
112
|
|
|
106
|
-
if txt_to_html:
|
|
113
|
+
if txt_to_html:
|
|
114
|
+
body = md2html(body=body)
|
|
107
115
|
msg.attach(MIMEText(body, "html"))
|
|
108
116
|
# if attachments is None: attachments = [] # see: https://fedingo.com/how-to-send-html-mail-with-attachment-using-python/
|
|
109
117
|
# for attachment in attachmenthrs: msg.attach(attachment.read_bytes(), filename=attachment.stem, maintype="image", subtype=attachment.suffix)
|
|
@@ -117,14 +125,19 @@ encryption = ssl
|
|
|
117
125
|
server.starttls()
|
|
118
126
|
server.login(email_add, password=pwd)
|
|
119
127
|
|
|
120
|
-
def send_email(self, to_addrs: str, msg: str):
|
|
121
|
-
|
|
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.
|
|
122
133
|
|
|
123
134
|
@staticmethod
|
|
124
135
|
def send_and_close(config_name: Optional[str], to: str, subject: str, body: str) -> Any:
|
|
125
136
|
"""If config_name is None, it sends from a generic email address."""
|
|
126
137
|
if config_name is None:
|
|
127
|
-
raise NotImplementedError(
|
|
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
|
+
)
|
|
128
141
|
# config = Email.get_source_of_truth()
|
|
129
142
|
# try:
|
|
130
143
|
# api_key = config['resend']['api_key']
|
|
@@ -150,26 +163,27 @@ encryption = ssl
|
|
|
150
163
|
tmp.send_message(to=to, subject=subject, body=body)
|
|
151
164
|
tmp.close()
|
|
152
165
|
|
|
153
|
-
@staticmethod
|
|
154
|
-
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:
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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")
|
|
173
187
|
|
|
174
188
|
|
|
175
189
|
# class PhoneNotification: # security concerns: avoid using this.
|
|
@@ -193,5 +207,5 @@ encryption = ssl
|
|
|
193
207
|
# n.send_notification()
|
|
194
208
|
|
|
195
209
|
|
|
196
|
-
if __name__ ==
|
|
210
|
+
if __name__ == "__main__":
|
|
197
211
|
pass
|
machineconfig/utils/options.py
CHANGED
|
@@ -5,38 +5,31 @@ from rich.console import Console
|
|
|
5
5
|
import platform
|
|
6
6
|
import subprocess
|
|
7
7
|
from typing import Optional, Union, TypeVar, Iterable
|
|
8
|
+
from machineconfig.utils.source_of_truth import WINDOWS_INSTALL_PATH, LINUX_INSTALL_PATH
|
|
9
|
+
|
|
8
10
|
|
|
9
11
|
T = TypeVar("T")
|
|
10
12
|
|
|
11
13
|
|
|
12
|
-
def check_tool_exists(tool_name: str
|
|
14
|
+
def check_tool_exists(tool_name: str) -> bool:
|
|
13
15
|
if platform.system() == "Windows":
|
|
14
16
|
tool_name = tool_name.replace(".exe", "") + ".exe"
|
|
15
|
-
|
|
16
|
-
if platform.system() == "Windows":
|
|
17
17
|
cmd = "where.exe"
|
|
18
|
+
root_path = Path(WINDOWS_INSTALL_PATH)
|
|
18
19
|
elif platform.system() in ["Linux", "Darwin"]:
|
|
19
20
|
cmd = "which"
|
|
21
|
+
root_path = Path(LINUX_INSTALL_PATH)
|
|
22
|
+
return any([Path("/usr/local/bin").joinpath(tool_name).is_file(), Path("/usr/bin").joinpath(tool_name).is_file(), root_path.joinpath(tool_name).is_file()])
|
|
20
23
|
else:
|
|
21
24
|
raise NotImplementedError(f"platform {platform.system()} not implemented")
|
|
22
|
-
|
|
23
|
-
try:
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
console.print(Panel(f"📥 INSTALLING TOOL | Installing {tool_name}...", border_style="bold blue", expand=False))
|
|
31
|
-
result = subprocess.run(install_script, shell=True, capture_output=True, text=True)
|
|
32
|
-
print(f"Command: {install_script}")
|
|
33
|
-
if result.stdout:
|
|
34
|
-
print(f"STDOUT: {result.stdout}")
|
|
35
|
-
if result.stderr:
|
|
36
|
-
print(f"STDERR: {result.stderr}")
|
|
37
|
-
print(f"Return code: {result.returncode}")
|
|
38
|
-
return check_tool_exists(tool_name=tool_name, install_script=None)
|
|
39
|
-
return res
|
|
25
|
+
_ = cmd
|
|
26
|
+
# try: # talking to terminal is too slow.
|
|
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()
|
|
40
33
|
|
|
41
34
|
|
|
42
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:
|
machineconfig/utils/path.py
CHANGED
|
@@ -12,7 +12,7 @@ T = TypeVar("T")
|
|
|
12
12
|
console = Console()
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
def sanitize_path(a_path:
|
|
15
|
+
def sanitize_path(a_path: str) -> PathExtended:
|
|
16
16
|
path = PathExtended(a_path)
|
|
17
17
|
if Path.cwd() == Path.home() and not path.exists():
|
|
18
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"
|
|
@@ -26,14 +26,14 @@ def sanitize_path(a_path: PathExtended) -> PathExtended:
|
|
|
26
26
|
skip_parts = 3 if path.as_posix().startswith("/home") else 3 # Both have 3 parts to skip
|
|
27
27
|
path = PathExtended.home().joinpath(*path.parts[skip_parts:])
|
|
28
28
|
assert path.exists(), f"File not found: {path}"
|
|
29
|
-
source_os = "Linux" if
|
|
29
|
+
source_os = "Linux" if path.as_posix().startswith("/home") else "macOS"
|
|
30
30
|
console.print(Panel(f"🔗 PATH MAPPING | {source_os} → Windows: `{a_path}` ➡️ `{path}`", title="Path Mapping", expand=False))
|
|
31
31
|
elif platform.system() in ["Linux", "Darwin"] and PathExtended.home().as_posix() not in path.as_posix(): # copied between Unix-like systems with different username
|
|
32
32
|
skip_parts = 3 # Both /home/username and /Users/username have 3 parts to skip
|
|
33
33
|
path = PathExtended.home().joinpath(*path.parts[skip_parts:])
|
|
34
34
|
assert path.exists(), f"File not found: {path}"
|
|
35
35
|
current_os = "Linux" if platform.system() == "Linux" else "macOS"
|
|
36
|
-
source_os = "Linux" if
|
|
36
|
+
source_os = "Linux" if path.as_posix().startswith("/home") else "macOS"
|
|
37
37
|
console.print(Panel(f"🔗 PATH MAPPING | {source_os} → {current_os}: `{a_path}` ➡️ `{path}`", title="Path Mapping", expand=False))
|
|
38
38
|
elif path.as_posix().startswith("C:"):
|
|
39
39
|
if platform.system() in ["Linux", "Darwin"]: # path copied from Windows to Linux/Mac
|
|
@@ -49,18 +49,18 @@ def sanitize_path(a_path: PathExtended) -> PathExtended:
|
|
|
49
49
|
return path.expanduser().absolute()
|
|
50
50
|
|
|
51
51
|
|
|
52
|
-
def find_scripts(root: Path, name_substring: str) -> tuple[list[Path], list[Path]]:
|
|
52
|
+
def find_scripts(root: Path, name_substring: str, suffixes: set[str]) -> tuple[list[Path], list[Path]]:
|
|
53
53
|
filename_matches = []
|
|
54
54
|
partial_path_matches = []
|
|
55
55
|
for entry in root.iterdir():
|
|
56
56
|
if entry.is_dir():
|
|
57
|
-
if entry.name in {".links", ".venv", ".git", ".idea", ".vscode", "node_modules", "__pycache__"}:
|
|
57
|
+
if entry.name in {".links", ".venv", ".git", ".idea", ".vscode", "node_modules", "__pycache__", ".mypy_cache"}:
|
|
58
58
|
# prune this entire subtree
|
|
59
59
|
continue
|
|
60
|
-
tmp1, tmp2 = find_scripts(entry, name_substring)
|
|
60
|
+
tmp1, tmp2 = find_scripts(entry, name_substring, suffixes)
|
|
61
61
|
filename_matches.extend(tmp1)
|
|
62
62
|
partial_path_matches.extend(tmp2)
|
|
63
|
-
elif entry.is_file() and entry.suffix in
|
|
63
|
+
elif entry.is_file() and entry.suffix in suffixes:
|
|
64
64
|
if name_substring.lower() in entry.name.lower():
|
|
65
65
|
filename_matches.append(entry)
|
|
66
66
|
elif name_substring.lower() in entry.as_posix().lower():
|
|
@@ -68,14 +68,14 @@ def find_scripts(root: Path, name_substring: str) -> tuple[list[Path], list[Path
|
|
|
68
68
|
return filename_matches, partial_path_matches
|
|
69
69
|
|
|
70
70
|
|
|
71
|
-
def match_file_name(sub_string: str, search_root: PathExtended) -> PathExtended:
|
|
71
|
+
def match_file_name(sub_string: str, search_root: PathExtended, suffixes: set[str]) -> PathExtended:
|
|
72
72
|
search_root_obj = search_root.absolute()
|
|
73
73
|
# assume subscript is filename only, not a sub_path. There is no need to fzf over the paths.
|
|
74
|
-
filename_matches, partial_path_matches = find_scripts(search_root_obj, sub_string)
|
|
74
|
+
filename_matches, partial_path_matches = find_scripts(search_root_obj, sub_string, suffixes)
|
|
75
75
|
if len(filename_matches) == 1:
|
|
76
76
|
return PathExtended(filename_matches[0])
|
|
77
|
-
console.print(Panel(f"Partial filename match with case-insensitivity failed. This generated #{len(filename_matches)} results.", title="Search", expand=False))
|
|
78
|
-
if len(filename_matches) <
|
|
77
|
+
console.print(Panel(f"Partial filename {search_root_obj} match with case-insensitivity failed. This generated #{len(filename_matches)} results.", title="Search", expand=False))
|
|
78
|
+
if len(filename_matches) < 20:
|
|
79
79
|
print("\n".join([a_potential_match.as_posix() for a_potential_match in filename_matches]))
|
|
80
80
|
if len(filename_matches) > 1:
|
|
81
81
|
print("Try to narrow down filename_matches search by case-sensitivity.")
|
|
@@ -112,7 +112,7 @@ def match_file_name(sub_string: str, search_root: PathExtended) -> PathExtended:
|
|
|
112
112
|
if len(search_res) == 1:
|
|
113
113
|
return search_root_obj.joinpath(search_res_raw)
|
|
114
114
|
|
|
115
|
-
print(f"⚠️ WARNING | Multiple search results found for `{sub_string}
|
|
115
|
+
print(f"⚠️ WARNING | Multiple search results found for `{sub_string}`:\n'{search_res}'")
|
|
116
116
|
cmd = f"cd '{search_root_obj}'; fd --type file | fzf --select-1 --query={sub_string}"
|
|
117
117
|
console.print(Panel(f"🔍 SEARCH STRATEGY | Trying with raw fzf search ...\n{cmd}", title="Search Strategy", expand=False))
|
|
118
118
|
try:
|