hcn 0.0.1__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.
hcn/__init__.py ADDED
@@ -0,0 +1,51 @@
1
+ import os, git, threading, warnings, socket
2
+ from importlib.metadata import version, PackageNotFoundError
3
+
4
+ __all__ = []
5
+
6
+
7
+ try:
8
+ __version__ = version('hcn')
9
+ except PackageNotFoundError:
10
+ __version__ = 'unknown'
11
+
12
+
13
+ def _has_network(host='8.8.8.8', port=53):
14
+ try:
15
+ socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect((host, port))
16
+ return True
17
+ except Exception:
18
+ return False
19
+
20
+
21
+ def _check_update():
22
+ current_dir = os.path.dirname(__file__)
23
+ repo_path = os.path.abspath(os.path.join(current_dir, '..', '..'))
24
+
25
+ try:
26
+ repo = git.Repo(repo_path)
27
+
28
+ if repo.is_dirty():
29
+ warnings.warn('Local changes detected.')
30
+
31
+ if not _has_network():
32
+ warnings.warn('No network connection. Remote update check skipped.')
33
+
34
+ else:
35
+ try:
36
+ repo.remotes.origin.fetch(kill_after_timeout=5)
37
+ active_branch = repo.active_branch
38
+ remote_ref = repo.remotes.origin.refs[active_branch.name]
39
+
40
+ behind = list(repo.iter_commits(f'{active_branch.name}..{remote_ref}'))
41
+ if behind:
42
+ warnings.warn(f'Remote update detected. {len(behind)} commits behind.')
43
+ except Exception as git_err:
44
+ warnings.warn(f'Remote check failed: {git_err}')
45
+
46
+ except Exception as e:
47
+ warnings.warn(f'Git check failed: {e}')
48
+
49
+
50
+ threading.Thread(target=_check_update, daemon=True).start()
51
+
Binary file
Binary file
@@ -0,0 +1,128 @@
1
+ import matplotlib.pyplot as plt
2
+
3
+ from matplotlib_inline import backend_inline
4
+ backend_inline.set_matplotlib_formats('retina')
5
+ del backend_inline
6
+
7
+ import os
8
+ from matplotlib.font_manager import fontManager
9
+ assets_dir = os.path.abspath(
10
+ os.path.join(os.path.dirname(__file__), '../assets/')
11
+ )
12
+ fontManager.addfont(os.path.join(assets_dir, 'fonts/SourceHanSans.otf'))
13
+ del fontManager
14
+
15
+
16
+ plt.rcParams['font.size'] = 7
17
+ plt.rcParams['font.sans-serif'].append('Source Han Sans SC')
18
+
19
+ plt.rcParams['axes.linewidth'] = 0.334
20
+ plt.rcParams['xtick.major.width'] = 0.334
21
+ plt.rcParams['ytick.major.width'] = 0.334
22
+ plt.rcParams['figure.dpi'] = 150
23
+ plt.rcParams['figure.figsize'] = (3.3, 2.5)
24
+ plt.rcParams['lines.linewidth'] = 1
25
+ plt.rcParams['lines.markersize'] = 4
26
+
27
+ plt.rcParams['xtick.top'] = True
28
+ plt.rcParams['ytick.right'] = True
29
+ plt.rcParams['xtick.direction'] = 'in'
30
+ plt.rcParams['ytick.direction'] = 'in'
31
+ plt.rcParams['xtick.major.size'] = 2.3
32
+ plt.rcParams['ytick.major.size'] = 2.3
33
+
34
+
35
+ plt.rcParams['savefig.bbox'] = 'tight'
36
+ plt.rcParams['axes.prop_cycle'] = \
37
+ plt.cycler('color', ['#0072bd', '#d95319', '#edb120', '#7e2f8e', '#77ac30', '#4dbeee', '#a2142f'])
38
+
39
+ plt.rcParams['legend.fancybox'] = False
40
+ plt.rcParams['legend.frameon'] = True
41
+ plt.rcParams['legend.framealpha'] = 1
42
+ plt.rcParams['legend.edgecolor'] = 'k'
43
+ plt.rcParams['legend.handlelength'] = 2.5
44
+ plt.rcParams['legend.borderpad'] = 0.3
45
+ plt.rcParams['legend.handletextpad'] = 0.3
46
+ plt.rcParams['patch.linewidth'] = 0.334
47
+
48
+ plt.rcParams['axes.unicode_minus'] = False
49
+
50
+
51
+ import matplotlib as mpl
52
+
53
+
54
+ cm_data = [
55
+ [0.2081, 0.1663, 0.5292],
56
+ [0.2116238095, 0.1897809524, 0.5776761905],
57
+ [0.212252381, 0.2137714286, 0.6269714286],
58
+ [0.2081, 0.2386, 0.6770857143],
59
+ [0.1959047619, 0.2644571429, 0.7279],
60
+ [0.1707285714, 0.2919380952, 0.779247619],
61
+ [0.1252714286, 0.3242428571, 0.8302714286],
62
+ [0.0591333333, 0.3598333333, 0.8683333333],
63
+ [0.0116952381, 0.3875095238, 0.8819571429],
64
+ [0.0059571429, 0.4086142857, 0.8828428571],
65
+ [0.0165142857, 0.4266, 0.8786333333],
66
+ [0.032852381, 0.4430428571, 0.8719571429],
67
+ [0.0498142857, 0.4585714286, 0.8640571429],
68
+ [0.0629333333, 0.4736904762, 0.8554380952],
69
+ [0.0722666667, 0.4886666667, 0.8467],
70
+ [0.0779428571, 0.5039857143, 0.8383714286],
71
+ [0.079347619, 0.5200238095, 0.8311809524],
72
+ [0.0749428571, 0.5375428571, 0.8262714286],
73
+ [0.0640571429, 0.5569857143, 0.8239571429],
74
+ [0.0487714286, 0.5772238095, 0.8228285714],
75
+ [0.0343428571, 0.5965809524, 0.819852381],
76
+ [0.0265, 0.6137, 0.8135],
77
+ [0.0238904762, 0.6286619048, 0.8037619048],
78
+ [0.0230904762, 0.6417857143, 0.7912666667],
79
+ [0.0227714286, 0.6534857143, 0.7767571429],
80
+ [0.0266619048, 0.6641952381, 0.7607190476],
81
+ [0.0383714286, 0.6742714286, 0.743552381],
82
+ [0.0589714286, 0.6837571429, 0.7253857143],
83
+ [0.0843, 0.6928333333, 0.7061666667],
84
+ [0.1132952381, 0.7015, 0.6858571429],
85
+ [0.1452714286, 0.7097571429, 0.6646285714],
86
+ [0.1801333333, 0.7176571429, 0.6424333333],
87
+ [0.2178285714, 0.7250428571, 0.6192619048],
88
+ [0.2586428571, 0.7317142857, 0.5954285714],
89
+ [0.3021714286, 0.7376047619, 0.5711857143],
90
+ [0.3481666667, 0.7424333333, 0.5472666667],
91
+ [0.3952571429, 0.7459, 0.5244428571],
92
+ [0.4420095238, 0.7480809524, 0.5033142857],
93
+ [0.4871238095, 0.7490619048, 0.4839761905],
94
+ [0.5300285714, 0.7491142857, 0.4661142857],
95
+ [0.5708571429, 0.7485190476, 0.4493904762],
96
+ [0.609852381, 0.7473142857, 0.4336857143],
97
+ [0.6473, 0.7456, 0.4188],
98
+ [0.6834190476, 0.7434761905, 0.4044333333],
99
+ [0.7184095238, 0.7411333333, 0.3904761905],
100
+ [0.7524857143, 0.7384, 0.3768142857],
101
+ [0.7858428571, 0.7355666667, 0.3632714286],
102
+ [0.8185047619, 0.7327333333, 0.3497904762],
103
+ [0.8506571429, 0.7299, 0.3360285714],
104
+ [0.8824333333, 0.7274333333, 0.3217],
105
+ [0.9139333333, 0.7257857143, 0.3062761905],
106
+ [0.9449571429, 0.7261142857, 0.2886428571],
107
+ [0.9738952381, 0.7313952381, 0.266647619],
108
+ [0.9937714286, 0.7454571429, 0.240347619],
109
+ [0.9990428571, 0.7653142857, 0.2164142857],
110
+ [0.9955333333, 0.7860571429, 0.196652381],
111
+ [0.988, 0.8066, 0.1793666667],
112
+ [0.9788571429, 0.8271428571, 0.1633142857],
113
+ [0.9697, 0.8481380952, 0.147452381],
114
+ [0.9625857143, 0.8705142857, 0.1309],
115
+ [0.9588714286, 0.8949, 0.1132428571],
116
+ [0.9598238095, 0.9218333333, 0.0948380952],
117
+ [0.9661, 0.9514428571, 0.0755333333],
118
+ [0.9763, 0.9831, 0.0538],
119
+ ]
120
+
121
+
122
+ parula = mpl.colors.LinearSegmentedColormap.from_list('parula', cm_data)
123
+ mpl.colormaps.register(parula)
124
+ plt.rcParams['image.cmap'] = 'parula'
125
+ del mpl, parula, cm_data
126
+
127
+
128
+ __all__ = []
hcn/io/__init__.py ADDED
@@ -0,0 +1 @@
1
+ from .core import *
hcn/io/core.py ADDED
@@ -0,0 +1,238 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+ from PIL import Image
4
+ import imageio.v3 as iio
5
+ import json
6
+
7
+ import os
8
+ import inspect
9
+ import platform
10
+ import subprocess
11
+
12
+
13
+ __all__ = [
14
+ "code",
15
+ "open",
16
+ "load",
17
+ "save"
18
+ ]
19
+
20
+
21
+ ZLIB_MAP = { "xs": 1, "sm": 3, "md": 6, "lg": 8, "xl": 9 }
22
+ JPG_MAP = { "xs": 98, "sm": 95, "md": 90, "lg": 80, "xl": 70 }
23
+
24
+ COMPRESS_CONFIG = {
25
+ "png": {
26
+ "key": "compression",
27
+ "map": ZLIB_MAP
28
+ },
29
+
30
+ "tif": {
31
+ "key": "compressionargs",
32
+ "map": ZLIB_MAP,
33
+ "flags": { "plugin": "tifffile", "compression": "zlib" }
34
+ },
35
+
36
+ "jpg": {
37
+ "key": "quality",
38
+ "map": JPG_MAP
39
+ },
40
+ }
41
+
42
+ COMPRESS_CONFIG["tiff"] = COMPRESS_CONFIG["tif"]
43
+ COMPRESS_CONFIG["jpeg"] = COMPRESS_CONFIG["jpg"]
44
+
45
+
46
+ def code(input_):
47
+ if inspect.isfunction(input_) or inspect.ismodule(input_) or inspect.isclass(input_):
48
+ file_path = inspect.getfile(input_)
49
+ else:
50
+ file_path = os.path.expanduser(input_)
51
+
52
+ if platform.system() == "Darwin":
53
+ subprocess.run(["code", file_path], check=True)
54
+ elif platform.system() == "Windows":
55
+ subprocess.run(["powershell.exe", "-Command", rf"code '{file_path}'"], check=True)
56
+ else:
57
+ raise NotImplementedError("This function is supported on Windows and macOS only.")
58
+
59
+ return os.path.abspath(file_path)
60
+
61
+
62
+
63
+ def open(input_):
64
+ if inspect.isfunction(input_) or inspect.ismodule(input_) or inspect.isclass(input_):
65
+ file_dir = os.path.dirname(inspect.getfile(input_))
66
+ else:
67
+ file_dir = os.path.expanduser(input_)
68
+
69
+ if platform.system() == "Darwin":
70
+ subprocess.run(["open", file_dir], check=True)
71
+ elif platform.system() == "Windows":
72
+ subprocess.run(["start", "", file_dir], shell=True, check=True)
73
+ else:
74
+ raise NotImplementedError("This function is supported on Windows and macOS only.")
75
+
76
+ return os.path.abspath(file_dir)
77
+
78
+
79
+
80
+ class _FileReader:
81
+ def __init__(self, path):
82
+ self.path = os.path.expanduser(path)
83
+
84
+ if not os.path.exists(self.path):
85
+ raise FileNotFoundError(f"{path} does not exists")
86
+
87
+ self.path = path
88
+ self.ext = os.path.splitext(path)[-1].lower()[1:]
89
+ self.name = os.path.basename(path)
90
+ self.stem = os.path.splitext(self.name)[0]
91
+
92
+
93
+ def _iio(self):
94
+ return np.squeeze(iio.imread(self.path, extension=f".{self.ext}"))
95
+
96
+
97
+ def _json(self):
98
+ with open(self.path, "r") as f:
99
+ dic = json.load(f)
100
+ return dic
101
+
102
+
103
+ def _csv(self):
104
+ return np.array(pd.read_csv(self.path, header=None))
105
+
106
+
107
+ def _npy(self):
108
+ return np.load(self.path)
109
+
110
+
111
+ def _npz(self):
112
+ dic = dict(np.load(self.path))
113
+ for key in dic.keys():
114
+ dic[key] = dic[key]
115
+ return dic
116
+
117
+
118
+ def _other(self):
119
+ msg = f"No implemented method for '.{self.ext}', nothing to return."
120
+ if os.path.exists(self.path):
121
+ msg += f"The file '{self.name}' exists, you may use finder() to open it."
122
+ print(msg)
123
+ return None
124
+
125
+
126
+ def load(self):
127
+ if self.ext == "npz":
128
+ data = self._npz()
129
+
130
+ elif self.ext == "npy":
131
+ data = self._npy()
132
+
133
+ elif self.ext in ["jpg", "jpeg", "png", "bmp", "tif", "tiff", "avi"]:
134
+ data = self._iio()
135
+
136
+ elif self.ext == "csv":
137
+ data = self._csv()
138
+
139
+ elif self.ext == "json":
140
+ data = self._json()
141
+
142
+ else:
143
+ data = self._other()
144
+
145
+ return data
146
+
147
+
148
+
149
+ class _FileWriter:
150
+ def __init__(self, path, data, compress):
151
+ self.path = os.path.abspath(os.path.expanduser(path))
152
+ self.data = data
153
+ self.compress = compress
154
+
155
+ self.ext = os.path.splitext(self.path)[-1].lower()[1:]
156
+ self.name = os.path.basename(self.path)
157
+ self.stem = os.path.splitext(self.name)[0]
158
+
159
+
160
+ def _json(self):
161
+ if isinstance(self.data, dict):
162
+ with open(self.path, "w") as f:
163
+ json.dump(self.data, f, indent=2)
164
+ else:
165
+ raise TypeError("'.json' format requires a dict of arrays")
166
+
167
+
168
+ def _csv(self):
169
+ arr = np.array(self.data)
170
+ pd.DataFrame(arr).to_csv(self.path, index=False, header=False)
171
+
172
+
173
+ def _npy(self):
174
+ np.save(self.path, np.array(self.data))
175
+
176
+ def _npz(self):
177
+ if isinstance(self.data, dict):
178
+ np.savez_compressed(self.path, **{k: np.array(v) for k, v in self.data.items()})
179
+ return self.path
180
+ else:
181
+ raise TypeError("'.npz' format requires a dict of arrays")
182
+
183
+ def _iio(self):
184
+ if self.ext in ["jpg", "jpeg"] and self.compress is False:
185
+ raise ValueError(f"'.{self.ext}' format does not support lossless storage (compress=False).")
186
+
187
+ if self.ext == "bmp" and self.compress is not False:
188
+ print(f"warning: '.bmp' format does not support compression, ignoring 'compress={self.compress}'")
189
+
190
+ lvl = "md" if self.compress is True else self.compress
191
+ cfg = COMPRESS_CONFIG.get(self.ext)
192
+ if cfg is not None and self.compress:
193
+ params = {cfg.get("key"): {"level": cfg["map"].get(lvl, lvl)} if self.ext in ["tiff", "tif"] else cfg["map"].get(lvl, lvl)}
194
+ flags = cfg.get("flags", {})
195
+ else:
196
+ params, flags = {}, {}
197
+
198
+ return iio.imwrite(self.path, self.data, extension=f".{self.ext}", **params, **flags)
199
+
200
+
201
+ def _other(self):
202
+ msg = f"No implemented method for '.{self.ext}', nothing saved."
203
+ print(msg)
204
+
205
+
206
+ def save(self):
207
+ if self.ext == "npz":
208
+ self._npz()
209
+
210
+ elif self.ext == "npy":
211
+ self._npy()
212
+
213
+ elif self.ext in ["jpg", "jpeg", "png", "bmp", "tif", "tiff"]:
214
+ self._iio()
215
+
216
+ elif self.ext == "csv":
217
+ self._csv()
218
+
219
+ elif self.ext == "json":
220
+ self._json()
221
+
222
+ else:
223
+ self._other()
224
+
225
+
226
+
227
+ def load(path, dtype=None):
228
+ target = _FileReader(path)
229
+ return target.load() if dtype is None else target.load().astype(dtype)
230
+
231
+
232
+
233
+ def save(path, data, dtype=None, compress=False):
234
+ data = np.asarray(data) if dtype is None else np.asarray(data).astype(dtype)
235
+ target = _FileWriter(path, data, compress)
236
+ target.save()
237
+ return target.path
238
+
hcn/preamble.py ADDED
@@ -0,0 +1,9 @@
1
+ import numpy as np
2
+ import matplotlib.pyplot as plt
3
+
4
+ import os
5
+ from time import sleep
6
+ from tqdm import tqdm
7
+ from math import e, pi
8
+
9
+ from .assets import matlab_style
hcn/utils/__init__.py ADDED
File without changes
hcn/utils/cache.py ADDED
@@ -0,0 +1,143 @@
1
+ """
2
+ WARNING: DO NOT ABUSE !!!
3
+
4
+ Cache is stored on disk at the location specified by `CACHE_DIR`,
5
+ and is by default deleted after 2 days of inactivity.
6
+
7
+ Since the cache is saved on disk, it will persist across kernel or
8
+ computer restarts. However, this may introduce a risk of computation
9
+ errors.
10
+
11
+ Additionally, because the cache is stored on disk, using it might
12
+ be less efficient than recomputing when the input data or function
13
+ output is very large.
14
+
15
+ Thus, caching is generally most effective for time-consuming
16
+ computations where the input and output data sizes are relatively small.
17
+ """
18
+
19
+
20
+ import os
21
+ import shutil
22
+ import inspect
23
+ import pickle
24
+ import time
25
+
26
+ from glob import glob
27
+ from functools import wraps
28
+ from hashlib import md5
29
+
30
+
31
+ # CACHE_DIR = os.path.join(os.path.dirname(__file__), '__cache.py_cache_dir__/')
32
+ CACHE_DIR = os.path.expanduser('~/.hcn.utils.cache.cache_dir')
33
+ EXPIRATION_TIME = 2 * 24 * 60 * 60
34
+
35
+ if not os.path.exists(CACHE_DIR):
36
+ os.mkdir(CACHE_DIR)
37
+
38
+
39
+ __all__ = [
40
+ 'clean_all_cache',
41
+ 'clean_expired_cache',
42
+ 'open_cache_dir',
43
+ 'cache'
44
+ ]
45
+
46
+
47
+ def extract_timestamp(file_path):
48
+ base_name = os.path.basename(file_path)
49
+ timestamp = base_name.split('_')[1].split('.pkl')[0]
50
+ return int(timestamp)
51
+
52
+
53
+ def find_cache(hash_key):
54
+ time_stamp = int(time.time())
55
+ prefix_name = os.path.join(CACHE_DIR, f'{hash_key}_*')
56
+ cache_file = glob(prefix_name)
57
+
58
+ if len(cache_file) == 0:
59
+ cache_file = os.path.join(CACHE_DIR, f'{hash_key}_{time_stamp}.pkl')
60
+ return cache_file, False
61
+
62
+ elif len(cache_file) == 1:
63
+ cache_file = cache_file[0]
64
+ return cache_file, True
65
+
66
+ else:
67
+ raise
68
+
69
+
70
+ def update_timestamp(cache_file):
71
+ time_stamp = int(time.time())
72
+ base_name = os.path.basename(cache_file)
73
+ hash_key = base_name.split('_')[0]
74
+ new_name = f'{hash_key}_{time_stamp}.pkl'
75
+ os.rename(cache_file, os.path.join(CACHE_DIR, new_name))
76
+
77
+
78
+
79
+ def open_cache_dir():
80
+ import platform
81
+ if platform.system() == 'Windows':
82
+ os.system('start ' + CACHE_DIR)
83
+ else:
84
+ os.system('open ' + CACHE_DIR)
85
+
86
+
87
+ def clean_all_cache():
88
+ shutil.rmtree(CACHE_DIR)
89
+ os.mkdir(CACHE_DIR)
90
+ print('done')
91
+
92
+
93
+ def clean_expired_cache():
94
+ now = int(time.time())
95
+ for file_name in os.listdir(CACHE_DIR):
96
+ file_path = os.path.join(CACHE_DIR, file_name)
97
+
98
+ if file_name.endswith('.pkl'):
99
+ try:
100
+ timestamp = extract_timestamp(file_path)
101
+ if now - timestamp > EXPIRATION_TIME:
102
+ os.remove(file_path)
103
+ except Exception as e:
104
+ print(f"Error processing file {file_path}: {e}")
105
+
106
+
107
+ def cache(func):
108
+ @wraps(func)
109
+ def wrapper(*args, **kwargs):
110
+ abspath = lambda p: os.path.abspath(os.path.normpath(p))
111
+
112
+ args = tuple(
113
+ abspath(arg) if isinstance(arg, str) and os.path.isfile(arg) else arg
114
+ for arg in args
115
+ )
116
+
117
+ kwargs = {
118
+ k: abspath(v) if isinstance(v, str) and os.path.isfile(v) else v
119
+ for k, v in kwargs.items()
120
+ }
121
+
122
+ cache_key = str(inspect.getsource(func)) + str(func.__name__) + str(args) + str(kwargs)
123
+ cache_key = cache_key.encode('utf-8')
124
+ hash_key = md5(cache_key).hexdigest()
125
+
126
+ cache_file, success = find_cache(hash_key)
127
+
128
+ if success and os.path.isfile(cache_file):
129
+ try:
130
+ with open(cache_file, 'rb') as f:
131
+ result = pickle.load(f)
132
+ update_timestamp(cache_file)
133
+ print('cache hit: reading result from cache file')
134
+ return result
135
+ except (EOFError, pickle.UnpicklingError, ValueError):
136
+ os.remove(cache_file)
137
+
138
+ result = func(*args, **kwargs)
139
+ with open(cache_file, 'wb') as f:
140
+ pickle.dump(result, f)
141
+ return result
142
+
143
+ return wrapper
hcn/utils/cpuid.py ADDED
@@ -0,0 +1,29 @@
1
+ import subprocess
2
+ import platform
3
+
4
+ __all__ = ['cpuid']
5
+
6
+ def cpuid():
7
+ system = platform.system().lower()
8
+ if system == "windows":
9
+ return _cpuid_windows()
10
+ elif system == "darwin":
11
+ return _cpuid_macos()
12
+ return None
13
+
14
+ def _cpuid_windows():
15
+ try:
16
+ cpu_id = subprocess.check_output("wmic cpu get processorid", shell=True).decode().strip().split("\n")[1]
17
+ return cpu_id
18
+ except Exception as e:
19
+ print(f"Failed to get CPU ID (Windows): {e}")
20
+ return None
21
+
22
+ def _cpuid_macos():
23
+ try:
24
+ serial_number = subprocess.check_output("system_profiler SPHardwareDataType | grep 'Serial Number (system)'", shell=True).decode().strip().split(":")[1].strip()
25
+ return serial_number
26
+ except Exception as e:
27
+ print(f"Failed to get hardware serial number (macOS): {e}")
28
+ return None
29
+
hcn/utils/crypt.py ADDED
@@ -0,0 +1,146 @@
1
+ import os
2
+ import base64
3
+ import getpass
4
+ from cryptography.hazmat.primitives import hashes
5
+ from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
6
+ from cryptography.fernet import Fernet
7
+
8
+ import hashlib
9
+ import subprocess
10
+ import platform
11
+
12
+
13
+ def _fingerprint_windows():
14
+ try:
15
+ disk_sn = subprocess.check_output('wmic diskdrive get serialnumber', shell=True).decode().strip().split('\n')[1]
16
+ machine_id = subprocess.check_output('reg query "HKLM\\SOFTWARE\\Microsoft\\Cryptography" /v MachineGuid', shell=True).decode().strip().split(' ')[-1]
17
+
18
+ hardware_info = f'{machine_id}|{disk_sn}'
19
+ return hashlib.sha256(hardware_info.encode()).hexdigest()
20
+ except Exception as e:
21
+ print(f'Failed to get hardware info (Windows): {e}')
22
+ return None
23
+
24
+
25
+ def _fingerprint_macos():
26
+ try:
27
+ hw_uuid = subprocess.check_output("system_profiler SPHardwareDataType | grep 'Hardware UUID'", shell=True).decode().strip().split(':')[1].strip()
28
+ disk_uuid = subprocess.check_output("system_profiler SPStorageDataType | grep 'Volume UUID' | head -n 1", shell=True).decode().strip().split(':')[1].strip()
29
+
30
+ hardware_info = f'{hw_uuid}|{disk_uuid}'
31
+ return hashlib.sha256(hardware_info.encode()).hexdigest()
32
+ except Exception as e:
33
+ print(f'Failed to get hardware info (macOS): {e}')
34
+ return None
35
+
36
+
37
+ def _fingerprint_linux():
38
+ try:
39
+ machine_id = subprocess.check_output('cat /etc/machine-id', shell=True).decode().strip()
40
+ disk_uuid = subprocess.check_output('lsblk -o NAME,UUID | grep \'sda\' | awk \'{print $2}\'', shell=True).decode().strip()
41
+
42
+ hardware_info = f'{machine_id}|{disk_uuid}'
43
+ return hashlib.sha256(hardware_info.encode()).hexdigest()
44
+ except Exception as e:
45
+ print(f'Failed to get hardware info (Linux): {e}')
46
+ return None
47
+
48
+
49
+ def device_fingerprint():
50
+ system = platform.system().lower()
51
+
52
+ if system == 'windows':
53
+ return _fingerprint_windows()
54
+ elif system == 'darwin':
55
+ return _fingerprint_macos()
56
+ elif system == 'linux':
57
+ return _fingerprint_linux()
58
+ else:
59
+ return None
60
+
61
+
62
+ def device_fingerprint():
63
+ system = platform.system().lower()
64
+
65
+ if system == 'windows':
66
+ return _fingerprint_windows()
67
+ elif system == 'darwin':
68
+ return _fingerprint_macos()
69
+ elif system == 'linux':
70
+ return _fingerprint_linux()
71
+ else:
72
+ return None
73
+
74
+
75
+ # Internal function to derive the encryption key from the password
76
+ def _derive_key(password: str, salt: bytes) -> bytes:
77
+ """
78
+ Derives a key from the password using PBKDF2-HMAC-SHA256.
79
+
80
+ Args:
81
+ password (str): The password used to derive the key.
82
+ salt (bytes): The salt value added to the password to prevent rainbow table attacks.
83
+
84
+ Returns:
85
+ bytes: The derived encryption key (32 bytes).
86
+ """
87
+ kdf = PBKDF2HMAC(
88
+ algorithm=hashes.SHA256(), # Using SHA-256 as the hash algorithm
89
+ length=32, # The length of the derived key (32 bytes)
90
+ salt=salt,
91
+ iterations=2_000_000, # Number of iterations (to slow down brute force)
92
+ )
93
+ key = kdf.derive(password.encode()) # Derive the key from the password
94
+ return key
95
+
96
+
97
+ def _isNone(password):
98
+ if password is None:
99
+ return getpass.getpass('input password: ')
100
+ else:
101
+ return password
102
+
103
+
104
+ def encrypt(message: str | bytes, password: str = None) -> bytes:
105
+ """
106
+ Encrypts a message using a password and a random salt.
107
+
108
+ Args:
109
+ message (str | bytes): The message to be encrypted (can be a string or bytes).
110
+ password (str): The password used to derive the encryption key.
111
+
112
+ Returns:
113
+ bytes: The encrypted message concatenated with the salt (used for decryption).
114
+ """
115
+ if isinstance(message, str):
116
+ message = message.encode() # Convert string to bytes
117
+
118
+ salt = os.urandom(16) # Generate a 16-byte random salt
119
+ key = _derive_key(_isNone(password), salt) # Derive the encryption key from the password and salt
120
+ f = Fernet(base64.urlsafe_b64encode(key)) # Create a Fernet object using the derived key
121
+ encrypted_message = f.encrypt(message) # Encrypt the message
122
+ return salt + encrypted_message # Return the salt and the encrypted message
123
+
124
+
125
+ def decrypt(encrypted_message: bytes, password: str = None) -> str | bytes:
126
+ """
127
+ Decrypts an encrypted message using a password and the provided salt.
128
+
129
+ Args:
130
+ encrypted_message (bytes): The encrypted message concatenated with the salt.
131
+ password (str): The password used to derive the encryption key.
132
+
133
+ Returns:
134
+ str | bytes: The decrypted plaintext message (either a string or binary data).
135
+ """
136
+ salt = encrypted_message[:16] # Extract the salt from the encrypted data
137
+ encrypted_message = encrypted_message[16:] # Extract the actual encrypted message
138
+ key = _derive_key(_isNone(password), salt) # Derive the encryption key from the password and salt
139
+ f = Fernet(base64.urlsafe_b64encode(key)) # Create a Fernet object using the derived key
140
+
141
+ try:
142
+ # Try to decode as a string (for text files)
143
+ return f.decrypt(encrypted_message).decode() # Try to return as string
144
+ except UnicodeDecodeError:
145
+ # If decoding fails, return the original binary data
146
+ return f.decrypt(encrypted_message) # If it's binary data, return the raw binary data
hcn/utils/ezmail.py ADDED
@@ -0,0 +1,191 @@
1
+ import os
2
+ import json
3
+ import base64
4
+
5
+ import smtplib
6
+ from email.mime.text import MIMEText
7
+ from email.utils import formataddr
8
+
9
+ import io
10
+ import traceback
11
+ import socket
12
+ import getpass
13
+ from datetime import datetime
14
+ from contextlib import contextmanager, redirect_stdout, redirect_stderr
15
+ from functools import wraps
16
+
17
+ from .crypt import encrypt, decrypt, device_fingerprint
18
+
19
+
20
+ CONFIG_PATH = os.path.expanduser('~/.hcn.utils.ezmail.config')
21
+ _config_cache = None
22
+
23
+
24
+ __all__ = ['auth', 'smtp', 'forget', 'send', 'context', 'decorator']
25
+
26
+
27
+ def _save_auth_config():
28
+ config = _load_config()
29
+ keep = {}
30
+ if 'email' in config:
31
+ keep['email'] = config['email']
32
+ if 'password' in config:
33
+ keep['password'] = config['password']
34
+
35
+ if os.path.exists(CONFIG_PATH):
36
+ with open(CONFIG_PATH, 'r') as f:
37
+ raw = json.load(f)
38
+ else:
39
+ raw = {}
40
+ raw.update(keep)
41
+
42
+ with open(CONFIG_PATH, 'w') as f:
43
+ json.dump(raw, f, indent=4)
44
+
45
+
46
+
47
+ def _save_smtp_config():
48
+ config = _load_config()
49
+ keep = {}
50
+ for k in ['smtp_host', 'smtp_port']:
51
+ if k in config:
52
+ keep[k] = config[k]
53
+
54
+ if os.path.exists(CONFIG_PATH):
55
+ with open(CONFIG_PATH, 'r') as f:
56
+ raw = json.load(f)
57
+ else:
58
+ raw = {}
59
+ raw.update(keep)
60
+
61
+ with open(CONFIG_PATH, 'w') as f:
62
+ json.dump(raw, f, indent=4)
63
+
64
+
65
+
66
+ def _load_config():
67
+ global _config_cache
68
+ if _config_cache is None:
69
+ if os.path.exists(CONFIG_PATH):
70
+ with open(CONFIG_PATH, 'r') as f:
71
+ _config_cache = json.load(f)
72
+ else:
73
+ _config_cache = {}
74
+ return _config_cache
75
+
76
+
77
+
78
+ def auth(save=False):
79
+ config = _load_config()
80
+
81
+ email_enc = encrypt(input('Email address: ').strip(), device_fingerprint())
82
+ passwd_enc = encrypt(getpass.getpass('Application password: ').strip(), device_fingerprint())
83
+
84
+ config['email'] = base64.b64encode(email_enc).decode()
85
+ config['password'] = base64.b64encode(passwd_enc).decode()
86
+
87
+ if save:
88
+ _save_auth_config()
89
+
90
+
91
+
92
+ def smtp(save=False):
93
+ config = _load_config()
94
+
95
+ smtp_host = input('SMTP Host: ').strip()
96
+ smtp_port_str = input('SMTP Port: ').strip()
97
+
98
+ if not smtp_port_str.isdigit():
99
+ raise ValueError(f"Invalid SMTP Port: '{smtp_port_str}'")
100
+
101
+ smtp_port = int(smtp_port_str)
102
+
103
+ config.update({
104
+ 'smtp_host': smtp_host,
105
+ 'smtp_port': smtp_port,
106
+ })
107
+
108
+ if save:
109
+ _save_smtp_config()
110
+
111
+
112
+
113
+ def forget():
114
+ if os.path.exists(CONFIG_PATH):
115
+ confirm = input(f"Are you sure you want to delete config at {os.path.expanduser('~')}? (y/N): ").lower().strip()
116
+ if confirm in ('y', 'yes', 'true'):
117
+ os.remove(CONFIG_PATH)
118
+ print('Configuration removed.')
119
+ else:
120
+ print('Abort.')
121
+ else:
122
+ print(f"No config found at '{os.path.expanduser('~')}'.")
123
+
124
+
125
+
126
+ def send(subject, message, recipient):
127
+ config = _load_config()
128
+
129
+ for key in ['email', 'password', 'smtp_host', 'smtp_port']:
130
+ if key not in config:
131
+ raise ValueError(f"Missing '{key}' in config. Run `email_notify.auth()` or `email_notify.smtp()` to configure.")
132
+
133
+ sender = decrypt(base64.b64decode(config['email']), device_fingerprint())
134
+ password = decrypt(base64.b64decode(config['password']), device_fingerprint())
135
+ smtp_host = config['smtp_host']
136
+ smtp_port = config['smtp_port']
137
+
138
+ msg = MIMEText(message, 'plain', 'utf-8')
139
+ msg['From'] = formataddr([f'email_notify', sender])
140
+ msg['To'] = recipient
141
+ msg['Subject'] = subject
142
+
143
+ with smtplib.SMTP_SSL(smtp_host, smtp_port) as server:
144
+ server.login(sender, password)
145
+ server.sendmail(sender, [recipient], msg.as_string())
146
+ server.quit()
147
+
148
+
149
+ class notify:
150
+ @staticmethod
151
+ @contextmanager
152
+ def context(recipient, task_name='Task'):
153
+ buf = io.StringIO()
154
+ start_time = datetime.now()
155
+ user_host = f'{getpass.getuser()}@{socket.gethostname()}'
156
+
157
+ def _body(status: str, output: str):
158
+ end_time = datetime.now()
159
+ elapsed = str(end_time - start_time).split('.')[0]
160
+ return (
161
+ f'Status: {status}\n'
162
+ f'Host: {user_host}\n'
163
+ f'Started: {start_time.strftime("%Y-%m-%d %H:%M:%S")}\n'
164
+ f'Ended: {end_time.strftime("%Y-%m-%d %H:%M:%S")}\n'
165
+ f'Elapsed: {elapsed}\n\n'
166
+
167
+ f'Console output:\n\n{output}\n'
168
+ )
169
+
170
+ try:
171
+ with redirect_stdout(buf), redirect_stderr(buf):
172
+ yield
173
+ send(f'{task_name} Completed', _body('Completed', buf.getvalue()), recipient)
174
+ except Exception as e:
175
+ buf.write('\n')
176
+ buf.write(traceback.format_exc())
177
+ send(f'{task_name} Failed', _body(type(e).__name__, buf.getvalue()), recipient)
178
+ raise
179
+
180
+
181
+ @classmethod
182
+ def decorator(cls, recipient):
183
+ def wrapper(func):
184
+ @wraps(func)
185
+ def inner(*args, **kwargs):
186
+ with cls.context(recipient, task_name=func.__name__):
187
+ return func(*args, **kwargs)
188
+ return inner
189
+ return wrapper
190
+
191
+
@@ -0,0 +1,25 @@
1
+ from functools import wraps
2
+ from dask import delayed, compute
3
+ from dask.distributed import Client
4
+ import psutil
5
+
6
+ # A useful decorator to enable multiprocessing, based on dask.
7
+ def parallelize(cores: int = 0):
8
+ if cores <= 0:
9
+ cores = psutil.cpu_count(logical=False)
10
+
11
+ def decorator(func):
12
+ @wraps(func)
13
+ def wrapper(tasks, *args, **kwargs):
14
+
15
+ @delayed
16
+ def _delayed_func(*args, **kwargs):
17
+ return func(*args, **kwargs)
18
+
19
+ all_tasks = [_delayed_func(task, *args, **kwargs) for task in tasks]
20
+ with Client(n_workers=cores, threads_per_worker=1):
21
+ results = compute(*all_tasks)
22
+
23
+ return results
24
+ return wrapper
25
+ return decorator
@@ -0,0 +1,39 @@
1
+ Metadata-Version: 2.4
2
+ Name: hcn
3
+ Version: 0.0.1
4
+ Summary: HCN's Collections of Nonpareils (or Nonsense)
5
+ License: MIT License
6
+
7
+ Copyright (c) 2023 Chao-Ning Hu
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ of this software and associated documentation files (the "Software"), to deal
11
+ in the Software without restriction, including without limitation the rights
12
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ copies of the Software, and to permit persons to whom the Software is
14
+ furnished to do so, subject to the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in all
17
+ copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ SOFTWARE.
26
+
27
+ Classifier: License :: OSI Approved :: MIT License
28
+ Classifier: Programming Language :: Python :: 3
29
+ Classifier: Operating System :: OS Independent
30
+ Description-Content-Type: text/markdown
31
+ License-File: LICENSE
32
+ Requires-Dist: numpy
33
+ Requires-Dist: scipy
34
+ Requires-Dist: matplotlib
35
+ Requires-Dist: gitpython
36
+ Requires-Dist: imageio[ffmpeg,tifffile]
37
+ Dynamic: license-file
38
+
39
+ Refactoring...
@@ -0,0 +1,18 @@
1
+ hcn/__init__.py,sha256=ozLvYoGAUoQjwkDWiNN79DQVQ6QJDmtdo81oL5G66BI,1497
2
+ hcn/preamble.py,sha256=0QKXhTJyyeBRdyl3OzYvQu0MQNig0bf_UrRXjemXghg,163
3
+ hcn/assets/matlab_style.py,sha256=8RIKNtdMeKq0VaUjn8YAVf1N7bgu5cBbhfqLhYtSKqM,4631
4
+ hcn/assets/fonts/SourceHanSans.otf,sha256=hLvUrOkdMns60aWBxogZYnik5BMIUgF29BkYAGTkrys,16437608
5
+ hcn/assets/fonts/lmroman10.otf,sha256=GqGM_vpYEyxSzl3nDbH9EVQgHBnNKyza_7pJBqM-aFI,111536
6
+ hcn/io/__init__.py,sha256=79Ih1151rfcqZdr7F8HSZSTs_iT2SKd1xCkehMsXeXs,19
7
+ hcn/io/core.py,sha256=OX-lMzz-DABBJCXsHNR-V_CZfapaZ5sXU-BKNHCc6xo,6340
8
+ hcn/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ hcn/utils/cache.py,sha256=z01pE4wAavNhCsPk8GT2x86jnmkSGDiih_cnU4h8bD8,3948
10
+ hcn/utils/cpuid.py,sha256=etJiwzDioYNRp6ufJgoDMxXMYGCWQm2CHjlX8q1xC9Y,855
11
+ hcn/utils/crypt.py,sha256=ZDdGi2BpefxhJvPr696YKpGoI2kkfLxEj8xl984Qsjo,5449
12
+ hcn/utils/ezmail.py,sha256=ytRNbk74bMofaphBmjYAU0nCEP4VtSWEXAgaWQpVTqI,5185
13
+ hcn/utils/parallelize.py,sha256=cdbPnuDWQX19R5x77e2S6dXGHrCBmoAxpGP4XCxxXgQ,759
14
+ hcn-0.0.1.dist-info/licenses/LICENSE,sha256=UnqMTa01uNhGrBkbOqjHSp8ThkITiVhLqdehtq8MC9o,1069
15
+ hcn-0.0.1.dist-info/METADATA,sha256=VVUWkjMzCuSm5OdAXyDI7wqGUwDnHNC9fh7yvx_CKfQ,1728
16
+ hcn-0.0.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
17
+ hcn-0.0.1.dist-info/top_level.txt,sha256=ppbrdjujDMeufmiJrUOY9LoOaHHg65vl1zSp5rTlwLA,4
18
+ hcn-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Chao-Ning Hu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ hcn