UtilityLibAPI 1.2511.291__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.
@@ -0,0 +1,335 @@
1
+ import os
2
+ import sys
3
+ import shutil
4
+ import subprocess
5
+ import re
6
+ import glob
7
+
8
+ class CLASS_Archive7z:
9
+ def __init__(self):
10
+ self.on_progress = None
11
+ self.seven_zip_path = self._find_7z_exe()
12
+
13
+ # -------------------------------------------------------------
14
+ # 自動搜尋 7z.exe
15
+ # -------------------------------------------------------------
16
+ def _find_7z_exe(self):
17
+ candidate_paths = [
18
+ r"C:\Program Files\7-Zip\7z.exe",
19
+ r"C:\Program Files (x86)\7-Zip\7z.exe"
20
+ ]
21
+ # 1. 先找常用路徑
22
+ for p in candidate_paths:
23
+ if os.path.exists(p):
24
+ return p
25
+ # 2. 從 PATH 搜尋
26
+ p = shutil.which("7z")
27
+ if p:
28
+ return p
29
+
30
+ # 3. 找不到 → 拋錯
31
+ raise FileNotFoundError("找不到 7z.exe,請確認是否已安裝 7-Zip")
32
+
33
+
34
+ # -------------------------------------------------------------
35
+ # 設定進度事件
36
+ # -------------------------------------------------------------
37
+ def CUF_SET_ProgressEvent(self, func_progress):
38
+ self.on_progress = func_progress
39
+
40
+
41
+ def _find_7z_exe(self):
42
+ """
43
+ 依照優先順序尋找 7za.exe → 7z.exe
44
+ 並支援掃描所有磁碟(C, D, E, F...)
45
+ """
46
+ # ---------------------------
47
+ # 1. PATH 中找 7za / 7z
48
+ # ---------------------------
49
+ p = shutil.which("7za")
50
+ if p:
51
+ return p
52
+ p = shutil.which("7z")
53
+ if p:
54
+ return p
55
+ # ---------------------------
56
+ # 2. 常見安裝路徑
57
+ # ---------------------------
58
+ common_paths = [
59
+ r"C:\Program Files\7-Zip\7za.exe",
60
+ r"C:\Program Files (x86)\7-Zip\7za.exe",
61
+ r"C:\Program Files\7-Zip\7z.exe",
62
+ r"C:\Program Files (x86)\7-Zip\7z.exe",
63
+ ]
64
+ for p in common_paths:
65
+ if os.path.exists(p):
66
+ return p
67
+ # ---------------------------
68
+ # 3. 掃描所有磁碟(C, D, E, F...)
69
+ # 搜尋特定資料夾名稱避免太慢
70
+ # ---------------------------
71
+ search_dirs = [
72
+ "7-Zip",
73
+ "7Zip",
74
+ "7zip",
75
+ "7zip\\bin",
76
+ "Program Files\\7-Zip",
77
+ "Program Files (x86)\\7-Zip",
78
+ ]
79
+ drives = []
80
+ for letter in "CDEFGHIJKLMNOPQRSTUVWXYZ":
81
+ d = f"{letter}:\\"
82
+ if os.path.exists(d):
83
+ drives.append(d)
84
+
85
+ for drive in drives:
86
+ for folder in search_dirs:
87
+ base = os.path.join(drive, folder)
88
+
89
+ # 若資料夾不存在跳過
90
+ if not os.path.isdir(base):
91
+ continue
92
+
93
+ # 檢查底下是否有 7za.exe 或 7z.exe
94
+ exe_7za = os.path.join(base, "7za.exe")
95
+ exe_7z = os.path.join(base, "7z.exe")
96
+
97
+ if os.path.exists(exe_7za):
98
+ return exe_7za
99
+ if os.path.exists(exe_7z):
100
+ return exe_7z
101
+
102
+ # 若還沒找到 → 試著遞迴搜尋
103
+ try:
104
+ for root, dirs, files in os.walk(base):
105
+ if "7za.exe" in files:
106
+ return os.path.join(root, "7za.exe")
107
+ if "7z.exe" in files:
108
+ return os.path.join(root, "7z.exe")
109
+ except PermissionError:
110
+ pass # 跳過無權限的資料夾
111
+
112
+ # ---------------------------
113
+ # 全部找不到 → 拋錯
114
+ # ---------------------------
115
+ raise FileNotFoundError("找不到 7za.exe 或 7z.exe,請確認是否已安裝 7-Zip")
116
+
117
+ # -------------------------------------------------------------
118
+ # 封裝執行 7z.exe,並解析 stdout 進度 (%)
119
+ # -------------------------------------------------------------
120
+ def _run_7z(self, args_list):
121
+ """
122
+ 同時支援:
123
+ ✔ 有 % → 觸發 int 進度
124
+ ✔ 沒 % → 逐行觸發文字行
125
+ """
126
+ cmd = [self.seven_zip_path] + args_list
127
+
128
+ process = subprocess.Popen(
129
+ cmd,
130
+ stdout=subprocess.PIPE,
131
+ stderr=subprocess.STDOUT,
132
+ universal_newlines=True,
133
+ encoding="utf-8",
134
+ errors="ignore"
135
+ )
136
+
137
+ progress_pattern = re.compile(r"(\d+)%")
138
+
139
+ for line in process.stdout:
140
+ line = line.strip()
141
+
142
+ # 嘗試找百分比
143
+ match = progress_pattern.search(line)
144
+ if match:
145
+ pct = int(match.group(1))
146
+ if self.on_progress:
147
+ try:
148
+ self.on_progress(pct)
149
+ except:
150
+ pass
151
+ else:
152
+ # 沒有百分比 → 仍然要回報文字行(使用者可自訂怎麼處理)
153
+ if self.on_progress and line:
154
+ try:
155
+ self.on_progress(line) # 回傳文字行
156
+ except:
157
+ pass
158
+ process.wait()
159
+
160
+
161
+ # -------------------------------------------------------------
162
+ # 壓縮資料夾
163
+ # -------------------------------------------------------------
164
+ def CUF_CompressFolder(self, Pms_FolderName:str, Pms_File7z:str,
165
+ Pms_Password:str=None,
166
+ Pmi_SplitSize_K:int=-1):
167
+
168
+ if not os.path.exists(Pms_FolderName):
169
+ raise ValueError("Pms_FolderName 必須是資料夾")
170
+
171
+ # -------------------------------------------------------
172
+ # 取出 Pms_File7z 的路徑並建立
173
+ # -------------------------------------------------------
174
+ out_dir = os.path.dirname(Pms_File7z)
175
+ if out_dir.strip() == "":
176
+ out_dir = os.getcwd()
177
+
178
+ os.makedirs(out_dir, exist_ok=True)
179
+
180
+ file_name_only = os.path.basename(Pms_File7z) # Orange.7z
181
+
182
+ # -------------------------------------------------------
183
+ # 組合 7z 參數
184
+ # -------------------------------------------------------
185
+ args = ["a"]
186
+
187
+ if Pms_Password:
188
+ args.append(f"-p{Pms_Password}")
189
+
190
+ if Pmi_SplitSize_K != -1:
191
+ args.append(f"-v{Pmi_SplitSize_K}k")
192
+
193
+ # 7z 輸出檔名(不能帶路徑)
194
+ args.append(file_name_only)
195
+
196
+ # 加入來源資料夾
197
+ args.append(Pms_FolderName)
198
+
199
+ # -------------------------------------------------------
200
+ # 執行壓縮 (7z 產生在目前工作目錄)
201
+ # -------------------------------------------------------
202
+ self._run_7z(args)
203
+
204
+ # -------------------------------------------------------
205
+ # ▒▒▒ 壓縮後 → 將檔案移到 Pms_File7z 指定路徑 ▒▒▒
206
+ # -------------------------------------------------------
207
+ self._move_output_files(file_name_only, out_dir)
208
+
209
+
210
+ # -------------------------------------------------------------
211
+ # 壓縮檔案(支援萬用字元 *.txt, apple*.*)
212
+ # -------------------------------------------------------------
213
+ def CUF_CompressFile(self, Pms_FileSource:str, Pms_File7z:str,
214
+ Pms_Password:str=None,
215
+ Pmi_SplitSize_K:int=-1):
216
+ file_list = glob.glob(Pms_FileSource)
217
+ if not file_list:
218
+ raise FileNotFoundError(f"找不到符合的檔案: {Pms_FileSource}")
219
+
220
+ # -------------------------------------------------------
221
+ # 取出 Pms_File7z 的路徑並建立
222
+ # -------------------------------------------------------
223
+ out_dir = os.path.dirname(Pms_File7z)
224
+ if out_dir.strip() == "":
225
+ out_dir = os.getcwd()
226
+
227
+ os.makedirs(out_dir, exist_ok=True)
228
+
229
+ file_name_only = os.path.basename(Pms_File7z)
230
+
231
+ # -------------------------------------------------------
232
+ # 組合 7z 參數
233
+ # -------------------------------------------------------
234
+ args = ["a"]
235
+
236
+ if Pms_Password:
237
+ args.append(f"-p{Pms_Password}")
238
+
239
+ if Pmi_SplitSize_K not in (None, -1):
240
+ args.append(f"-v{Pmi_SplitSize_K}k")
241
+
242
+ args.append(file_name_only)
243
+
244
+ for f in file_list:
245
+ args.append(f)
246
+
247
+ # -------------------------------------------------------
248
+ # 執行壓縮
249
+ # -------------------------------------------------------
250
+ self._run_7z(args)
251
+
252
+ # -------------------------------------------------------
253
+ # ▒▒▒ 壓縮後 → 移動所有分割檔 / 單一檔 ▒▒▒
254
+ # -------------------------------------------------------
255
+ self._move_output_files(file_name_only, out_dir)
256
+
257
+
258
+ # -------------------------------------------------------------
259
+ # 共用:移動壓縮結果到指定目錄
260
+ # -------------------------------------------------------------
261
+ def _move_output_files(self, base_file_name:str, out_dir:str):
262
+ """
263
+ base_file_name = "Orange.7z"
264
+ 最後尋找:
265
+ Orange.7z
266
+ Orange.7z.001
267
+ Orange.7z.002
268
+ ...
269
+ """
270
+ for f in os.listdir("."):
271
+ if f == base_file_name or f.startswith(base_file_name + "."):
272
+ shutil.move(f, os.path.join(out_dir, f))
273
+
274
+
275
+ def CUF_Decompress(self, Pms_ArchiveFile, Pms_OutputDir, Pms_Password=None):
276
+ """
277
+ 解壓縮通用函式(最終穩定版本)
278
+ -------------------------------------
279
+ Pms_ArchiveFile : 壓縮檔完整路徑
280
+ Pms_OutputDir : 要解壓到哪裡
281
+ Pms_Password : 解壓密碼(可省略)
282
+ -------------------------------------
283
+ 支援格式:
284
+ 7z, zip, rar, tar, tar.gz, tar.bz2, tar.xz
285
+ """
286
+ # ---------- 路徑檢查 ----------
287
+ if not os.path.exists(Pms_ArchiveFile):
288
+ return False, f"Compress file not found: {Pms_ArchiveFile}"
289
+
290
+ os.makedirs(Pms_OutputDir, exist_ok=True)
291
+
292
+ Pms_7zExe = self.seven_zip_path
293
+
294
+ args = ["x"]
295
+
296
+ if Pms_Password:
297
+ args.append(f"-p{Pms_Password}")
298
+
299
+ args.append(Pms_ArchiveFile)
300
+ args.append(f"-o{Pms_OutputDir}")
301
+ args.append("-y")
302
+
303
+ # ★★★ 使用會有進度事件的版本 ★★★
304
+ try:
305
+ self._run_7z(args)
306
+ return True, "Decompress successful."
307
+ except Exception as e:
308
+ return False, f"Decompress failure:\n{str(e)}"
309
+
310
+
311
+ # ================================================================================
312
+ # [使用範例]
313
+ def GE_ProgressEvent(Pmf_Percent):
314
+ print(str(Pmf_Percent));
315
+
316
+ if(__name__=="__main__"):
317
+ ms_Path_7z: str;
318
+
319
+ obj_7z = CLASS_Archive7z();
320
+
321
+ obj_7z.CUF_SET_ProgressEvent(GE_ProgressEvent);
322
+
323
+ obj_7z.CUF_CompressFolder(Pms_FolderName="X:\\TEMP\\BOOK\\MUSIC",
324
+ Pms_File7z="X:\\temp\\book\\PPPK\\GOOD_FILE",
325
+ Pms_Password="Gotech123",
326
+ Pmi_SplitSize_K=640);
327
+
328
+ obj_7z.CUF_CompressFile(Pms_FileSource="X:\\temp\\book\\*.png",
329
+ Pms_File7z="X:\\temp\\book\\abcde\\pqrst\\PPPK-2\\我的PNG圖檔",
330
+ Pms_Password="OKOKGOOD",
331
+ Pmi_SplitSize_K=100);
332
+
333
+ obj_7z.CUF_Decompress(Pms_ArchiveFile="X:\\TEMP\\BOOK\\abcde\\pqrst\\PPPK-2\\我的PNG圖檔.7z.001",
334
+ Pms_OutputDir="X:\\temp\\OK了",
335
+ Pms_Password="OKOKGOOD")
@@ -0,0 +1,198 @@
1
+ # ============================================================================
2
+ # 通用 加密/解密 (跨平台)
3
+ # ============================================================================
4
+ import base64
5
+
6
+ # ================================================================================
7
+ class CLASS_EnDeCodeLib:
8
+ def __init__(self):
9
+ # 設定預設的 CROSS 加密金鑰
10
+ self.CVmba_CROSS_KEY = [];
11
+ self.CVmba_CROSS_KEY.append(0x3f);
12
+ self.CVmba_CROSS_KEY.append(0x6b);
13
+ self.CVmba_CROSS_KEY.append(0x59);
14
+ self.CVmba_CROSS_KEY.append(0x48);
15
+ # ------------------------------------------------------------------
16
+ self.CVms_CROSS_ASC = \
17
+ "AaBbFfGg 125~!@#$)HhIiLlMm_+`-=CcD34dEe:[]JjKk\\\'"\
18
+ "NnOoSsTt670%UuXxYyZz^&*Pp89QqRr(<>?VvWw,./;{}|\"";
19
+ self.CVms_CROSS_MAP = \
20
+ "~!@#$%^& ()_+QWERbnmTOP{}|ASghjkDFL:\"ZXiopCV<>?`" \
21
+ "125-=*qwert67890yuBNM[]\\asd34fGHJKl;\'zxYUIcv,./";
22
+ # ------------------------------------------------------------------
23
+
24
+
25
+ # ================================================================================
26
+ # PRIVATE
27
+ # ================================================================================
28
+ # ----------------------------------------------------------------------------
29
+ # 功能: CROSS 金鑰加密 (針對不可視字元: 使用金鑰加密)
30
+ # 傳入:
31
+ # 傳回:
32
+ # ----------------------------------------------------------------------------
33
+ def CVF_CROSS_XOR(self, Pmc_SRC_CH):
34
+ mia_SRCCH = [];
35
+ ms_ENCODE = "";
36
+ mi_ENCODE = 0;
37
+ mi_c = 0;
38
+
39
+ mia_SRCCH = Pmc_SRC_CH.encode('big5');
40
+ # ------------------------------------------------------------
41
+ for mi_c in range(0, len(mia_SRCCH)):
42
+ mi_ENCODE = mia_SRCCH[mi_c];
43
+ # ------------------------------------------------------------
44
+ for mi_d in range(0, len(self.CVmba_CROSS_KEY)):
45
+ mi_ENCODE ^= self.CVmba_CROSS_KEY[mi_d];
46
+ # ------------------------------------------------------------
47
+ ms_ENCODE += chr(mi_ENCODE);
48
+ # ------------------------------------------------------------
49
+ return(ms_ENCODE);
50
+
51
+
52
+ # ================================================================================
53
+ # PUBLIC
54
+ # ================================================================================
55
+ # ----------------------------------------------------------------------------
56
+ # 功能: 設定 CROSS 加密金鑰
57
+ # 傳入:
58
+ # 傳回:
59
+ # ----------------------------------------------------------------------------
60
+ def CUF_CROSS_SetKey(self, Rmca_KeyCode):
61
+ if(len(Rmca_KeyCode) <= 0):
62
+ return;
63
+ # ------------------------------------------------------------
64
+ self.CVmba_CROSS_KEY.clear();
65
+ # ------------------------------------------------------------
66
+ for mi_c in range(0, len(Rmca_KeyCode)):
67
+ self.CVmba_CROSS_KEY = Rmca_KeyCode[mi_c];
68
+
69
+ # ----------------------------------------------------------------------------
70
+ # 功能: 回傳 CROSS 加密金鑰
71
+ # 傳入:
72
+ # 傳回:
73
+ # ----------------------------------------------------------------------------
74
+ def CUF_CROSS_GetKey(self):
75
+ return(self.CVmba_CROSS_KEY);
76
+
77
+
78
+ # ----------------------------------------------------------------------------
79
+ # 功能: CROSS 加密程序
80
+ # 傳入:
81
+ # 傳回:
82
+ # ----------------------------------------------------------------------------
83
+ def CUF_CROSS_EnCode(self, Pms_SRC_Text):
84
+ ms_RET_STR = "";
85
+ mB_Found = False;
86
+ mi_c = 0;
87
+ mi_d = 0;
88
+ # ------------------------------------------------------------
89
+ if(type(Pms_SRC_Text) is bytes):
90
+ Pms_SRC_Text = str(Pms_SRC_Text);
91
+ # ------------------------------------------------------------
92
+ # (1) 判斷是否為 ASCII 可視字元,若是則搜尋對應的 CVms_CROSS_MAP[?] 字元
93
+ for mi_c in range(0, len(Pms_SRC_Text)):
94
+ mB_Found = False;
95
+ # --------------------------------------------
96
+ for mi_d in range(0, len(self.CVms_CROSS_ASC)):
97
+ if(Pms_SRC_Text[mi_c] == self.CVms_CROSS_ASC[mi_d]):
98
+ mB_Found = True;
99
+ break;
100
+ # --------------------------------------------
101
+ if(mB_Found):
102
+ # (1-1) 可視字元 : 找到可視字元對應的加密表字元
103
+ ms_RET_STR += self.CVms_CROSS_MAP[mi_d];
104
+ else:
105
+ # (1-2) 不可視字元: 使用金鑰加密
106
+ ms_RET_STR += self.CVF_CROSS_XOR(Pms_SRC_Text[mi_c]);
107
+ # ------------------------------------------------------------
108
+ return(ms_RET_STR);
109
+
110
+
111
+ # ----------------------------------------------------------------------------
112
+ # 功能: CROSS 解密程序
113
+ # 傳入:
114
+ # 傳回:
115
+ # ----------------------------------------------------------------------------
116
+ def CUF_CROSS_DeCode(self, Pms_ENC_Text):
117
+ ms_RET_STR = "";
118
+ mB_Found = False;
119
+ mi_c = 0;
120
+ mi_d = 0;
121
+ # ------------------------------------------------------------
122
+ # (1) 判斷是否為 ASCII 可視字元,若是則搜尋對應的 CVms_CROSS_MAP[?] 字元
123
+ for mi_c in range(0, len(Pms_ENC_Text)):
124
+ mB_Found = False;
125
+ # --------------------------------------------
126
+ for mi_d in range(0, len(self.CVms_CROSS_MAP)):
127
+ if(Pms_ENC_Text[mi_c] == self.CVms_CROSS_MAP[mi_d]):
128
+ mB_Found = True;
129
+ break;
130
+ # --------------------------------------------
131
+ if(mB_Found):
132
+ # (1-1) 可視字元 : 找到可視字元對應的解密表字元
133
+ ms_RET_STR += self.CVms_CROSS_ASC[mi_d];
134
+ else:
135
+ # (1-2) 不可視字元: 使用金鑰加密
136
+ ms_RET_STR += self.CVF_CROSS_XOR(Pms_ENC_Text[mi_c]);
137
+ # ------------------------------------------------------------
138
+ return(ms_RET_STR);
139
+
140
+
141
+ # ----------------------------------------------------------------------------
142
+ # 功能: CROSS 加密程序 (Base64 版本)
143
+ # 傳入:
144
+ # 傳回:
145
+ # ----------------------------------------------------------------------------
146
+ def CUF_CROSS_EnCodeBase64(self, Pms_SRC_Text):
147
+ ms_EnCode = "";
148
+ ms_BuffSTR = "";
149
+
150
+ ms_BuffSTR = Pms_SRC_Text.encode('big5');
151
+ # ------------------------------------------------------------
152
+ for mi_c in range(0, len(ms_BuffSTR)):
153
+ ms_EnCode += chr(ms_BuffSTR[mi_c]);
154
+ # ------------------------------------------------------------
155
+ # (1) Base64 轉碼
156
+ ms_EnCode = base64.b64encode(ms_EnCode.encode('utf-8'));
157
+ # ------------------------------------------------------------
158
+ # (2) 明文加密
159
+ ms_EnCode = self.CUF_CROSS_EnCode(ms_EnCode);
160
+ # ------------------------------------------------------------
161
+ ms_EnCode = ms_EnCode.encode("utf-8");
162
+ # ---------e---------------------------------------------------
163
+ return(ms_EnCode);
164
+
165
+
166
+ # ----------------------------------------------------------------------------
167
+ # 功能: CROSS 解密程序 (Base64 版本)
168
+ # 傳入:
169
+ # 傳回:
170
+ # ----------------------------------------------------------------------------
171
+ def CUF_CROSS_DeCodeBase64(self, Pms_ENC_Text):
172
+ ms_DeCode = "";
173
+
174
+ ms_EnCode = Pms_ENC_Text.encode('big5');
175
+ # ------------------------------------------------------------
176
+ # (1) Base64 解碼
177
+ ms_DeCode = base64.b64decode(ms_EnCode);
178
+ # ------------------------------------------------------------
179
+ # (2) 解密
180
+ ms_DeCode = self.CUF_CROSS_DeCode(ms_DeCode);
181
+ # ------------------------------------------------------------
182
+ ms_DeCode = ms_DeCode.encode("utf-8");
183
+ # ------------------------------------------------------------
184
+ return(ms_DeCode);
185
+
186
+
187
+ # ================================================================================
188
+ # EXAMPLE
189
+ # ================================================================================
190
+ if(__name__=="__main__"):
191
+ Gobj_EnDeCode = CLASS_EnDeCodeLib();
192
+
193
+ ms_TEXT1 = "abcd,123 4567";
194
+ ms_TEXT2 = "abc真正好123,Good 太棒了 ";
195
+ # ------------------------------------------------------------
196
+ print(Gobj_EnDeCode.CUF_CROSS_EnCode(ms_TEXT1));
197
+ print(Gobj_EnDeCode.CUF_CROSS_EnCode(ms_TEXT2.encode('big5')));
198
+ print(Gobj_EnDeCode.CUF_CROSS_EnCodeBase64(ms_TEXT2));
@@ -0,0 +1,166 @@
1
+ import os
2
+ import threading
3
+ import subprocess
4
+ import time
5
+ from typing import Callable, Dict, Optional
6
+
7
+ class CLASS_ExtWrapper7z:
8
+ def __init__(self, Pms_7z_path: Optional[str] = None):
9
+ self.Pms_7z_path = Pms_7z_path or self.CUF_Find7zPath()
10
+ self.tasks: Dict[int, Dict] = {}
11
+ self.task_counter = 0
12
+ self.lock = threading.Lock()
13
+
14
+ # 🔍 自動搜尋 7z.exe
15
+ def CUF_Find7zPath(self) -> Optional[str]:
16
+ common_paths = [
17
+ r"C:\\Program Files\\7-Zip\\7z.exe",
18
+ r"C:\\Program Files (x86)\\7-Zip\\7z.exe",
19
+ ]
20
+ for path in common_paths:
21
+ if os.path.exists(path):
22
+ return path
23
+
24
+ for root, dirs, files in os.walk("C:\\"):
25
+ if "7z.exe" in files:
26
+ return os.path.join(root, "7z.exe")
27
+ return None
28
+
29
+ # 📦 建立壓縮或解壓縮任務
30
+ def CUF_AddTask(
31
+ self,
32
+ Pms_Mode: str, # "compress" 或 "extract"
33
+ Pms_InputPath: str,
34
+ Pms_OutputPath: str,
35
+ Pmi_VolumeSizeMB: int = 0,
36
+ Pms_Password: str = "",
37
+ Pmf_Callback: Optional[Callable[[int, float, str], None]] = None,
38
+ ) -> int:
39
+ with self.lock:
40
+ self.task_counter += 1
41
+ mi_TaskID = self.task_counter
42
+ self.tasks[mi_TaskID] = {
43
+ "thread": None,
44
+ "progress": 0.0,
45
+ "status": "queued",
46
+ "cancel": False,
47
+ "mode": Pms_Mode,
48
+ "input": Pms_InputPath,
49
+ "output": Pms_OutputPath,
50
+ "callback": Pmf_Callback,
51
+ }
52
+
53
+ thread = threading.Thread(
54
+ target=self._CUF_RunTask,
55
+ args=(mi_TaskID, Pms_Mode, Pms_InputPath, Pms_OutputPath, Pmi_VolumeSizeMB, Pms_Password),
56
+ daemon=True,
57
+ )
58
+ self.tasks[mi_TaskID]["thread"] = thread
59
+ thread.start()
60
+ return mi_TaskID
61
+
62
+ # 🧩 執行壓縮/解壓縮
63
+ def _CUF_RunTask(self, TaskID, Mode, InputPath, OutputPath, VolumeSizeMB, Password):
64
+ task = self.tasks[TaskID]
65
+ task["status"] = "running"
66
+ Pms_7z_path = self.Pms_7z_path
67
+ if not Pms_7z_path or not os.path.exists(Pms_7z_path):
68
+ task["status"] = "error"
69
+ if task["callback"]:
70
+ task["callback"](TaskID, 0, "7z.exe not found")
71
+ return
72
+
73
+ if Mode == "compress":
74
+ cmd = [Pms_7z_path, "a", OutputPath, "-y"]
75
+ # 🔹 支援多個檔案/資料夾;以 ; 分隔
76
+ paths = [p.strip() for p in InputPath.split(";") if p.strip()]
77
+ cmd.extend(paths)
78
+ if Password:
79
+ cmd.append(f"-p{Password}")
80
+ if VolumeSizeMB > 0:
81
+ cmd.append(f"-v{VolumeSizeMB}m")
82
+
83
+ elif Mode == "extract":
84
+ cmd = [Pms_7z_path, "x", InputPath, f"-o{OutputPath}", "-y"]
85
+ if Password:
86
+ cmd.append(f"-p{Password}")
87
+ else:
88
+ task["status"] = "error"
89
+ if task["callback"]:
90
+ task["callback"](TaskID, 0, "Invalid mode")
91
+ return
92
+
93
+ proc = subprocess.Popen(
94
+ cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
95
+ )
96
+
97
+ for line in proc.stdout:
98
+ if task["cancel"]:
99
+ proc.terminate()
100
+ task["status"] = "cancelled"
101
+ if task["callback"]:
102
+ task["callback"](TaskID, task["progress"], "Cancelled")
103
+ return
104
+
105
+ if "%" in line:
106
+ try:
107
+ percent = float(line.strip().split("%")[0].split()[-1])
108
+ task["progress"] = percent
109
+ if task["callback"]:
110
+ task["callback"](TaskID, percent, "running")
111
+ except:
112
+ pass
113
+
114
+ proc.wait()
115
+ if proc.returncode == 0 and not task["cancel"]:
116
+ task["status"] = "done"
117
+ task["progress"] = 100.0
118
+ if task["callback"]:
119
+ task["callback"](TaskID, 100.0, "done")
120
+ elif not task["cancel"]:
121
+ task["status"] = "error"
122
+ if task["callback"]:
123
+ task["callback"](TaskID, task["progress"], "error")
124
+
125
+ # ❌ 取消任務
126
+ def CUF_CancelTask(self, Pmi_TaskID: int):
127
+ if Pmi_TaskID in self.tasks:
128
+ self.tasks[Pmi_TaskID]["cancel"] = True
129
+
130
+ # 📊 查詢所有任務總進度
131
+ def CUF_GetTotalProgress(self) -> float:
132
+ with self.lock:
133
+ if not self.tasks:
134
+ return 0.0
135
+ total = sum(task["progress"] for task in self.tasks.values())
136
+ return total / len(self.tasks)
137
+
138
+ # 🔍 查詢任務狀態
139
+ def CUF_GetTaskStatus(self, Pmi_TaskID: int) -> str:
140
+ return self.tasks.get(Pmi_TaskID, {}).get("status", "unknown")
141
+
142
+
143
+
144
+
145
+ # ================================================================================
146
+ # === 測試範例 ===
147
+ if(__name__ == "__main__"):
148
+ def my_callback(TaskID, progress, status):
149
+ print(f"[Task {TaskID}] {status} - {progress:.1f}%")
150
+
151
+ wrapper = CLASS_ExtWrapper7z()
152
+ print("7z 路徑:", wrapper.Pms_7z_path)
153
+
154
+ # 壓縮多個檔案 (同時支援萬用字元與多個資料夾)
155
+ task1 = wrapper.CUF_AddTask(
156
+ "compress",
157
+ Pms_InputPath=r"C:\\Temp\\Rubber\b1*.bmp;C:\\boost\\more",
158
+ Pms_OutputPath=r"C:\\TEMP\\OKOKO_GOOD.7z",
159
+ Pmi_VolumeSizeMB=50,
160
+ Pmf_Callback=my_callback,
161
+ )
162
+
163
+ while True:
164
+ total_progress = wrapper.CUF_GetTotalProgress()
165
+ print(f"總進度: {total_progress:.2f}%")
166
+ time.sleep(2)
@@ -0,0 +1,206 @@
1
+ import smtplib
2
+ from email.mime.text import MIMEText
3
+ from email.mime.multipart import MIMEMultipart
4
+ from email.mime.application import MIMEApplication
5
+ from email.mime.base import MIMEBase
6
+ from email.utils import formataddr
7
+ from email.header import Header
8
+ from email import encoders
9
+ from typing import List, Optional
10
+ import os
11
+ import mimetypes
12
+
13
+ class CLASS_MailSender:
14
+ def __init__(self,
15
+ Pms_SMTP_Server: str,
16
+ Pmi_Port: int,
17
+ Pms_Account: str,
18
+ Pms_Password: str,
19
+ Pmb_UseSSL: bool = True):
20
+ """
21
+ 初始化郵件寄送物件
22
+ :param Pms_SMTP_Server: 郵件伺服器主機 (ex: smtp.gmail.com)
23
+ :param Pmi_Port: 通訊埠 (SSL=465, TLS=587)
24
+ :param Pms_Account: 登入帳號
25
+ :param Pms_Password: 登入密碼或應用程式密碼
26
+ :param Pmb_UseSSL: 是否使用 SSL 連線 (預設 True)
27
+ """
28
+ self.Pms_SMTP_Server = Pms_SMTP_Server
29
+ self.Pmi_Port = Pmi_Port
30
+ self.Pms_Account = Pms_Account
31
+ self.Pms_Password = Pms_Password
32
+ self.Pmb_UseSSL = Pmb_UseSSL
33
+
34
+ #------------------------------------------------------------
35
+ def CUF_CreateMessage(self,
36
+ Pms_Subject: str,
37
+ Pms_BodyText: str,
38
+ Pms_BodyHTML: Optional[str] = None,
39
+ Pms_From: Optional[str] = None,
40
+ Pobj_To: Optional[List[str]] = None,
41
+ Pobj_Cc: Optional[List[str]] = None,
42
+ Pobj_Bcc: Optional[List[str]] = None,
43
+ Pobj_Attachments: Optional[List[str]] = None) -> MIMEMultipart:
44
+ """
45
+ 建立郵件內容(支援 HTML / 附件 / 顯示別名)
46
+ """
47
+ msg = MIMEMultipart("alternative")
48
+
49
+ # 設定寄件者別名
50
+ from_addr = formataddr((Pms_From or self.Pms_Account, self.Pms_Account))
51
+ msg["From"] = from_addr
52
+
53
+ msg["To"] = ", ".join(Pobj_To or [])
54
+ if Pobj_Cc:
55
+ msg["Cc"] = ", ".join(Pobj_Cc)
56
+ msg["Subject"] = Pms_Subject
57
+
58
+ # 純文字版本
59
+ msg.attach(MIMEText(Pms_BodyText or "", "plain", "utf-8"))
60
+
61
+ # HTML 版本
62
+ if Pms_BodyHTML:
63
+ msg.attach(MIMEText(Pms_BodyHTML, "html", "utf-8"))
64
+
65
+ # 附件(支援中文檔名 & Outlook)
66
+ if Pobj_Attachments:
67
+ for ms_path in Pobj_Attachments:
68
+ try:
69
+ if not os.path.isfile(ms_path):
70
+ print(f"⚠️ Attachements not exists: {ms_path}")
71
+ continue
72
+
73
+ ctype, encoding = mimetypes.guess_type(ms_path)
74
+ if ctype is None or encoding is not None:
75
+ ctype = "application/octet-stream"
76
+ maintype, subtype = ctype.split("/", 1)
77
+
78
+ with open(ms_path, "rb") as f:
79
+ part = MIMEApplication(f.read(), _subtype=subtype)
80
+ filename = os.path.basename(ms_path)
81
+ #part.add_header(
82
+ # "Content-Disposition",
83
+ # "attachment",
84
+ # filename=("utf-8", "", filename)
85
+ #)
86
+ filename_header = Header(filename, "utf-8").encode()
87
+ part.add_header(
88
+ "Content-Disposition",
89
+ "attachement",
90
+ filename=filename_header
91
+ )
92
+ msg.attach(part)
93
+ except Exception as e:
94
+ print(f"⚠️ Can not add attachment: {ms_path} ({e})")
95
+ return msg
96
+
97
+ #------------------------------------------------------------
98
+ def CUF_SendMail(self,
99
+ Pms_Subject: str,
100
+ Pms_BodyText: str,
101
+ Pms_BodyHTML: Optional[str],
102
+ Pobj_To: List[str],
103
+ Pms_From: Optional[str] = None,
104
+ Pobj_Cc: Optional[List[str]] = None,
105
+ Pobj_Bcc: Optional[List[str]] = None,
106
+ Pobj_Attachments: Optional[List[str]] = None) -> bool:
107
+ """
108
+ 實際寄送郵件,支援寄件者別名
109
+ """
110
+ msg = self.CUF_CreateMessage(
111
+ Pms_Subject=Pms_Subject,
112
+ Pms_BodyText=Pms_BodyText,
113
+ Pms_BodyHTML=Pms_BodyHTML,
114
+ Pms_From=Pms_From,
115
+ Pobj_To=Pobj_To,
116
+ Pobj_Cc=Pobj_Cc,
117
+ Pobj_Bcc=Pobj_Bcc,
118
+ Pobj_Attachments=Pobj_Attachments,
119
+ )
120
+
121
+ all_recipients = (Pobj_To or []) + (Pobj_Cc or []) + (Pobj_Bcc or [])
122
+ from_addr = formataddr((Pms_From or self.Pms_Account, self.Pms_Account))
123
+
124
+ try:
125
+ if self.Pmb_UseSSL:
126
+ with smtplib.SMTP_SSL(self.Pms_SMTP_Server, self.Pmi_Port) as server:
127
+ server.login(self.Pms_Account, self.Pms_Password)
128
+ server.sendmail(from_addr, all_recipients, msg.as_string())
129
+ else:
130
+ with smtplib.SMTP(self.Pms_SMTP_Server, self.Pmi_Port) as server:
131
+ server.ehlo()
132
+ server.starttls()
133
+ server.login(self.Pms_Account, self.Pms_Password)
134
+ server.sendmail(from_addr, all_recipients, msg.as_string())
135
+
136
+ print("✅ Send Success!")
137
+ return True
138
+ except Exception as e:
139
+ print("❌ Failure:", e)
140
+ return False
141
+
142
+ # ================================================================================
143
+ # [範例]
144
+ # 匯入剛才寫好的 CLASS_MailSender
145
+ #from MailSender import CLASS_MailSender # 假設你把上面的 class 儲存為 MailSender.py
146
+ if __name__ == "__main__":
147
+ # === 初始化郵件傳送物件 ===
148
+ mailer = CLASS_MailSender(
149
+ Pms_SMTP_Server="msr.hinet.net",
150
+ Pmi_Port=465,
151
+ Pms_Account="gxxxx.xz@msa.hinet.net",
152
+ Pms_Password="xxxxx",
153
+ Pmb_UseSSL=True
154
+ )
155
+
156
+ # === 郵件收件者設定 ===
157
+ ml_To = ["xxxx4@xxxxxh.xxz", "xxxxxx23@xxx7.hxxxx.net"]
158
+ ml_Cc = ["exxxxx03@hotmail.com.tw"]
159
+ ml_Bcc = ["xxxxxxy.lin@msa.hxxxx.net"] # 密件副本,不會顯示在郵件中
160
+
161
+ # === 郵件主題與內容 ===
162
+ ms_Subject = "團隊週報通知 wahaha 123*()F"
163
+
164
+ ms_BodyText = """
165
+ 這是一封自動寄出的團隊週報通知。
166
+ 請參閱附加的 PDF 檔或 HTML 內文。
167
+ """
168
+
169
+ ms_BodyHTML = """
170
+ <html>
171
+ <body style="font-family: Microsoft JhengHei;">
172
+ <h2 style="color: green;">🌟 團隊週報</h2>
173
+ <p>各位同仁您好:</p>
174
+ <p>以下為本週重點摘要:</p>
175
+ <ul>
176
+ <li>完成模組 A 測試</li>
177
+ <li>修正 ERP 匯出問題</li>
178
+ <li>新增客戶報表功能</li>
179
+ </ul>
180
+ <p>詳細報表請參閱附件。</p>
181
+ <p style="color: gray;">系統自動寄出,請勿回覆。</p>
182
+ </body>
183
+ </html>
184
+ """
185
+
186
+ # === 附件路徑 ===
187
+ ml_Attachments = [
188
+ "X:\\TEMP\\112年縣檸檬幼申報收入.jpg"
189
+ ]
190
+
191
+ # === 寄送 ===
192
+ mb_OK = mailer.CUF_SendMail(
193
+ Pms_Subject=ms_Subject,
194
+ Pms_BodyText=ms_BodyText,
195
+ Pms_BodyHTML=ms_BodyHTML,
196
+ Pms_From = "太棒了!!abcdefg123",
197
+ Pobj_To=ml_To,
198
+ Pobj_Cc=ml_Cc,
199
+ Pobj_Bcc=ml_Bcc,
200
+ Pobj_Attachments=ml_Attachments
201
+ )
202
+
203
+ if mb_OK:
204
+ print("✅ 多人郵件已成功寄出!")
205
+ else:
206
+ print("❌ 寄送失敗,請檢查設定。")
@@ -0,0 +1,143 @@
1
+ import pymysql
2
+ import pickle
3
+ import time
4
+ import uuid
5
+
6
+ class CLASS_SessionVAR:
7
+ def __init__(self):
8
+ self.conn = None
9
+ self.cursor = None
10
+ self.table_name = "_session_table"
11
+
12
+ # ----------------------------
13
+ # 建立 SessionID
14
+ # ----------------------------
15
+ def CUF_New_SessionID(self):
16
+ return(str(uuid.uuid4()))
17
+
18
+ # ----------------------------
19
+ # 建立連線與表
20
+ # ----------------------------
21
+ def CUF_Create(self, Pms_host:str, Pmi_port:int, Pms_user:str, Pms_password:str, Pms_db:str,
22
+ Pms_SessionID:str=None,
23
+ Pms_table_name:str="_session_table"):
24
+ self.conn = pymysql.connect(
25
+ host=Pms_host,
26
+ port=Pmi_port,
27
+ user=Pms_user,
28
+ password=Pms_password,
29
+ database=Pms_db,
30
+ charset='utf8mb4',
31
+ autocommit=True
32
+ )
33
+ self.cursor = self.conn.cursor()
34
+ self.table_name = Pms_table_name
35
+
36
+ # 建立表格,如果不存在
37
+ create_sql = f"""
38
+ CREATE TABLE IF NOT EXISTS `{self.table_name}` (
39
+ `SessionID` VARCHAR(36) NOT NULL,
40
+ `VarName` VARCHAR(255) NOT NULL,
41
+ `VarValue` LONGBLOB,
42
+ `UpdateTime` BIGINT NOT NULL,
43
+ PRIMARY KEY (`SessionID`, `VarName`)
44
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
45
+ """
46
+ self.cursor.execute(create_sql)
47
+
48
+ # 回傳 SessionID
49
+ if(Pms_SessionID == ""):
50
+ session_id = self.CUF_New_SessionID();
51
+ else:
52
+ session_id = Pms_SessionID;
53
+ return(session_id);
54
+
55
+ # ----------------------------
56
+ # 儲存變數
57
+ # ----------------------------
58
+ def CUF_SET(self, Pms_SessionID, Pms_VarName, Pms_VarValue):
59
+ blob_data = pickle.dumps(Pms_VarValue)
60
+ update_time = int(time.time())
61
+ sql = f"""
62
+ INSERT INTO `{self.table_name}` (SessionID, VarName, VarValue, UpdateTime)
63
+ VALUES (%s, %s, %s, %s)
64
+ ON DUPLICATE KEY UPDATE
65
+ VarValue = VALUES(VarValue),
66
+ UpdateTime = VALUES(UpdateTime)
67
+ """
68
+ self.cursor.execute(sql, (Pms_SessionID, Pms_VarName, blob_data, update_time))
69
+
70
+ # ----------------------------
71
+ # 取得變數
72
+ # ----------------------------
73
+ def CUF_GET(self, Pms_SessionID, Pms_VarName):
74
+ sql = f"SELECT VarValue FROM `{self.table_name}` WHERE SessionID=%s AND VarName=%s"
75
+ self.cursor.execute(sql, (Pms_SessionID, Pms_VarName))
76
+ result = self.cursor.fetchone()
77
+ if result:
78
+ return pickle.loads(result[0])
79
+ return None
80
+
81
+ # ----------------------------
82
+ # 釋放 Session
83
+ # ----------------------------
84
+ # 依 SessionID 刪除,或依秒數刪除舊資料
85
+ def CUF_Release(self, Pms_param):
86
+ if isinstance(Pms_param, str):
87
+ sql = f"DELETE FROM `{self.table_name}` WHERE SessionID=%s"
88
+ self.cursor.execute(sql, (Pms_param,))
89
+ elif isinstance(Pms_param, (int, float)):
90
+ threshold = int(time.time()) - int(Pms_param)
91
+ sql = f"DELETE FROM `{self.table_name}` WHERE UpdateTime < %s"
92
+ self.cursor.execute(sql, (threshold,))
93
+
94
+ # ----------------------------
95
+ # 關閉資源
96
+ # ----------------------------
97
+ def CUF_Close(self):
98
+ if self.cursor:
99
+ self.cursor.close()
100
+ self.cursor = None
101
+ if self.conn:
102
+ self.conn.close()
103
+ self.conn = None
104
+
105
+ # =============================
106
+ # 範例用法
107
+ # =============================
108
+ if(__name__ == "__main__"):
109
+ objSession = CLASS_SessionVAR()
110
+ session_id = objSession.CUF_Create(Pms_host="mis.gotech.biz",
111
+ Pmi_port=3300, Pms_db="df8000",
112
+ Pms_user="root",
113
+ Pms_password="gotechdf8000sys",
114
+ Pms_SessionID="xx-202510131127");
115
+
116
+ # 儲存資料
117
+ objSession.CUF_SET(session_id, "mylist", [1,2,3])
118
+ objSession.CUF_SET(session_id, "mydict", {"a":1, "b":2})
119
+
120
+ # 取得資料
121
+ mylist = objSession.CUF_GET(session_id, "mylist")
122
+ mydict = objSession.CUF_GET(session_id, "mydict")
123
+ print(mylist, mydict)
124
+
125
+ # 釋放單一 session
126
+ # objSession.CUF_Release(session_id)
127
+
128
+ # 釋放超過 600 秒的資料
129
+ # objSession.CUF_Release(600)
130
+
131
+
132
+ mylist = [5,6,7,8,9];
133
+ objSession.CUF_SET(session_id, "mylist", mylist);
134
+
135
+ mylist = [0];
136
+ print(mylist);
137
+
138
+ mylist = objSession.CUF_GET(session_id, "mylist");
139
+ print(mylist);
140
+
141
+ objSession.CUF_SET(session_id, "")
142
+
143
+ objSession.CUF_Close()
@@ -0,0 +1,12 @@
1
+ from .EnDeCodeLib import CLASS_EnDeCodeLib
2
+ from .SessionVARLib import CLASS_SessionVAR
3
+ from .MailSenderLib import CLASS_MailSender
4
+ from .ExtWrapper7zLib import CLASS_ExtWrapper7z
5
+ from .Archive7zLib import CLASS_Archive7z
6
+
7
+ __all__ = ["CLASS_EnDeCodeLib",
8
+ "CLASS_SessionVAR",
9
+ "CLASS_MailSender",
10
+ "CLASS_ExtWrapper7z",
11
+ "CLASS_Archive7z"
12
+ ]
@@ -0,0 +1,62 @@
1
+ Metadata-Version: 2.4
2
+ Name: UtilityLibAPI
3
+ Version: 1.2511.291
4
+ Summary: UtilityLibAPI Python package
5
+ Author-email: James Lin <tylin123@ms27.hinet.net>
6
+ License: Copyright (c) 2025 James Lin **UtilityLibAPI**
7
+
8
+ All rights reserved.
9
+
10
+ Permission is granted for **personal, educational, or non-commercial use only**.
11
+ You **may NOT**:
12
+
13
+ 1. Sell or otherwise commercially exploit this software or any part of it.
14
+ 2. Reproduce, distribute, or sublicense this software for commercial purposes.
15
+ 3. Modify or create derivative works for commercial purposes without explicit written permission from the copyright holder.
16
+
17
+ You **may**:
18
+
19
+ - Use the software for personal learning, research, or internal company evaluation (non-commercial).
20
+ - Share the software with attribution to the original author, as long as it is not for commercial gain.
21
+
22
+ **Disclaimer:**
23
+ This software is provided "AS IS", without any warranty of any kind, either expressed or implied,
24
+ including but not limited to the warranties of merchantability, fitness for a particular purpose, or non-infringement.
25
+ In no event shall the author be liable for any claim, damages, or other liability arising from, out of, or in connection with the software or its use.
26
+
27
+ Project-URL: Homepage, https://mis.gotech.biz
28
+ Description-Content-Type: text/markdown
29
+ License-File: LICENSE
30
+ Requires-Dist: pymysql
31
+ Dynamic: license-file
32
+
33
+ #UtilityLibAPI Classes
34
+
35
+ ## History of version
36
+ Version 1.2511.291: 2025/11/29<BR>
37
+ Fixed MailSenderLib: Attachement file name encode problem.
38
+ Add function for user can modify mail caption of from field.
39
+
40
+ Version 1.2511.282: 2025/11/27<BR>
41
+ Fixed MailSenderLib: Attachement file name encode problem.
42
+
43
+ Version 1.2511.281: 2025/11/27<BR>
44
+ Modified 7zArchiveLib to Archive7zLib --> 7z Archive tools library
45
+
46
+ Version 1.2511.28: 2025/11/27<BR>
47
+ Fixed 7zArchiveLib --> 7z Archive tools library
48
+
49
+ Version 1.2511.27: 2025/11/27<BR>
50
+ Add 7zArchiveLib --> 7z Archive tools library
51
+
52
+ Version 1.2510.22: 2025/10/21<BR>
53
+ Add ExtWrapper7zLib --> 7z Archive tools library
54
+
55
+ Version 1.2510.22: 2025/10/21<BR>
56
+ Add MailSenderLib --> CLASS_MailSender library
57
+
58
+ Version 0.1.1: 2025/10/13<BR>
59
+ Fixed export class path<BR>
60
+
61
+ Version 0.1.0: 2025/10/13<BR>
62
+ Create UtilityLibAPI -- EnDeCodeLib + SessionVARLib<BR>
@@ -0,0 +1,11 @@
1
+ UtilityLibAPI/Archive7zLib.py,sha256=aZWS3cLA_YdWaoBzBYS7jWIzWxJa8Eg6rFCVOX_u0t4,11949
2
+ UtilityLibAPI/EnDeCodeLib.py,sha256=YPaqzZJXl5f0xwPv4V4zMRWaH6-6K5MNlKMsyUcQ95U,9189
3
+ UtilityLibAPI/ExtWrapper7zLib.py,sha256=ZD1jEzCNh6F_wsZ70O-Tw2KgUNZ7b58siHZNIlZ3n7o,5815
4
+ UtilityLibAPI/MailSenderLib.py,sha256=OZ1ANXJpwPM6Y7Ez1fAW7pSxfL44tzKgrCqaBnLeR1Q,7888
5
+ UtilityLibAPI/SessionVARLib.py,sha256=WGzozRfENQrjNuI5vuRC8CXbz9jSKvE5d612e5WB2q8,4821
6
+ UtilityLibAPI/__init__.py,sha256=FMcTZBeOoejbxU-pIzkpNgUZVMNXtSWNmFXX5USUyTg,413
7
+ utilitylibapi-1.2511.291.dist-info/licenses/LICENSE,sha256=fpKsUxampChuMsbZd_ww_SEJqK88RFiYAvqzAsklgFI,1072
8
+ utilitylibapi-1.2511.291.dist-info/METADATA,sha256=Bw077sxaEkK9rn3ynw2rARlaRf1Xe7z14Fdh1ag5HZQ,2463
9
+ utilitylibapi-1.2511.291.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
10
+ utilitylibapi-1.2511.291.dist-info/top_level.txt,sha256=keGrZAwheJUvsOa8IUlwa3fYR7OzUbjFMQUczJC99Tg,14
11
+ utilitylibapi-1.2511.291.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2025 James Lin **UtilityLibAPI**
2
+
3
+ All rights reserved.
4
+
5
+ Permission is granted for **personal, educational, or non-commercial use only**.
6
+ You **may NOT**:
7
+
8
+ 1. Sell or otherwise commercially exploit this software or any part of it.
9
+ 2. Reproduce, distribute, or sublicense this software for commercial purposes.
10
+ 3. Modify or create derivative works for commercial purposes without explicit written permission from the copyright holder.
11
+
12
+ You **may**:
13
+
14
+ - Use the software for personal learning, research, or internal company evaluation (non-commercial).
15
+ - Share the software with attribution to the original author, as long as it is not for commercial gain.
16
+
17
+ **Disclaimer:**
18
+ This software is provided "AS IS", without any warranty of any kind, either expressed or implied,
19
+ including but not limited to the warranties of merchantability, fitness for a particular purpose, or non-infringement.
20
+ In no event shall the author be liable for any claim, damages, or other liability arising from, out of, or in connection with the software or its use.
@@ -0,0 +1 @@
1
+ UtilityLibAPI