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 +51 -0
- hcn/assets/fonts/SourceHanSans.otf +0 -0
- hcn/assets/fonts/lmroman10.otf +0 -0
- hcn/assets/matlab_style.py +128 -0
- hcn/io/__init__.py +1 -0
- hcn/io/core.py +238 -0
- hcn/preamble.py +9 -0
- hcn/utils/__init__.py +0 -0
- hcn/utils/cache.py +143 -0
- hcn/utils/cpuid.py +29 -0
- hcn/utils/crypt.py +146 -0
- hcn/utils/ezmail.py +191 -0
- hcn/utils/parallelize.py +25 -0
- hcn-0.0.1.dist-info/METADATA +39 -0
- hcn-0.0.1.dist-info/RECORD +18 -0
- hcn-0.0.1.dist-info/WHEEL +5 -0
- hcn-0.0.1.dist-info/licenses/LICENSE +21 -0
- hcn-0.0.1.dist-info/top_level.txt +1 -0
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
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
|
+
|
hcn/utils/parallelize.py
ADDED
|
@@ -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,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
|