raytoolsbox 1.0.0__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.
File without changes
@@ -0,0 +1,524 @@
1
+ import os
2
+
3
+ from cryptography.hazmat.primitives import serialization, hashes
4
+ from cryptography.hazmat.primitives.asymmetric import x25519, ed25519
5
+ from cryptography.hazmat.primitives.kdf.hkdf import HKDF
6
+ from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305
7
+ from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PublicKey
8
+
9
+
10
+ from pathlib import Path
11
+
12
+ class CryptoManager:
13
+ """
14
+ ✅ 现代化 ECC 加密管理器(无签名版)
15
+
16
+ - 使用 X25519 进行密钥交换
17
+ - 使用 ChaCha20-Poly1305 进行对称加密
18
+ - 原始数据支持 str / bytes / 文件,加密或签名后返回二进制数据包
19
+ """
20
+ _SIGN_LEN = 64 # Ed25519 固定长度
21
+ # 数据类型信息
22
+ _TYPE_TEXT = b"\x01"
23
+ _TYPE_BYTES = b"\x02"
24
+ # 二进制格式数据包指纹
25
+ _MAGIC = b"ECC1"
26
+ _VERSION = b"\x01"
27
+ _SIGN_MAGIC = b"SIGN"
28
+ _SIGN_VERSION = b"\x01"
29
+
30
+ def __init__(self, key_dir=None, PIN="toolsbox"):
31
+ self._PIN = PIN
32
+ self._key_dir = None
33
+
34
+ if key_dir is not None:
35
+ self.set_key_dir(key_dir)
36
+
37
+ def set_key_dir(self, key_dir):
38
+ """
39
+ 切换 / 设置密钥目录
40
+ """
41
+ self._key_dir = Path(key_dir)
42
+ self._key_dir.mkdir(parents=True, exist_ok=True)
43
+
44
+ self._crypt_key_path = self._key_dir / "crypt_key.pem"
45
+ self._crypt_pub_path = self._key_dir / "crypt_pub.pem"
46
+ self._sign_key_path = self._key_dir / "sign_key.pem"
47
+ self._sign_pub_path = self._key_dir / "sign_pub.pem"
48
+
49
+ # -----------------------------------------
50
+ # 密钥生成 / 保存 / 加载
51
+ # -----------------------------------------
52
+ def generate_keys(self):
53
+
54
+ """ X25519与Ed25519公私钥对生成 """
55
+ crypt_key = x25519.X25519PrivateKey.generate()
56
+ crypt_pub = crypt_key.public_key()
57
+ # Ed25519公私钥对生成
58
+ sign_key = ed25519.Ed25519PrivateKey.generate()
59
+ sign_pub = sign_key.public_key()
60
+ if self._key_dir:
61
+ self._save_keypair(crypt_key, crypt_pub, sign_key, sign_pub)
62
+ return crypt_key, crypt_pub, sign_key, sign_pub
63
+
64
+ # -----------------------------------------
65
+ def _save_keypair(self, crypt_key, crypt_pub, sign_key, sign_pub):
66
+ """保存密钥对为 PEM 文件"""
67
+
68
+ pin = self._PIN
69
+ enc = (
70
+ serialization.BestAvailableEncryption(pin.encode())
71
+ if pin else serialization.NoEncryption()
72
+ )
73
+ with open(self._crypt_key_path, "wb") as f:
74
+ f.write(crypt_key.private_bytes(
75
+ serialization.Encoding.PEM,
76
+ serialization.PrivateFormat.PKCS8,
77
+ enc
78
+ ))
79
+ with open(self._crypt_pub_path, "wb") as f:
80
+ f.write(crypt_pub.public_bytes(
81
+ serialization.Encoding.PEM,
82
+ serialization.PublicFormat.SubjectPublicKeyInfo
83
+ ))
84
+ with open(self._sign_key_path, "wb") as f:
85
+ f.write(sign_key.private_bytes(
86
+ serialization.Encoding.PEM,
87
+ serialization.PrivateFormat.PKCS8,
88
+ enc
89
+ ))
90
+ with open(self._sign_pub_path, "wb") as f:
91
+ f.write(sign_pub.public_bytes(
92
+ serialization.Encoding.PEM,
93
+ serialization.PublicFormat.SubjectPublicKeyInfo
94
+ ))
95
+
96
+ # -----------------------------------------
97
+ def _load_keypair(self):
98
+ """加载私钥和公钥"""
99
+ if not all(p.exists() for p in (
100
+ self._crypt_key_path,
101
+ self._crypt_pub_path,
102
+ self._sign_key_path,
103
+ self._sign_pub_path
104
+ )):
105
+ print("密钥文件不存在")
106
+ return None, None, None, None
107
+
108
+ pwd = self._PIN.encode()
109
+
110
+ try:
111
+ with open(self._crypt_key_path, "rb") as f:
112
+ crypt_key = serialization.load_pem_private_key(f.read(), password=pwd)
113
+ with open(self._crypt_pub_path, "rb") as f:
114
+ crypt_pub = serialization.load_pem_public_key(f.read())
115
+ with open(self._sign_key_path, "rb") as f:
116
+ sign_key = serialization.load_pem_private_key(f.read(), password=pwd)
117
+ with open(self._sign_pub_path, "rb") as f:
118
+ sign_pub = serialization.load_pem_public_key(f.read())
119
+ return crypt_key, crypt_pub, sign_key, sign_pub
120
+ except Exception as e:
121
+ raise RuntimeError(f"加载密钥失败: {e}")
122
+
123
+ # ============================================
124
+ # 混合加密(ECC + AEAD)
125
+ # text → 加密 → dict
126
+ # ============================================
127
+
128
+ def encrypt(self, data=None, pubkey=None,data_path=None,save="off") -> bytes:
129
+ """
130
+ 参数:
131
+ data: str | bytes | data_path
132
+ pubkey: 公钥(X25519)
133
+ data_path: 数据文件路径(可选)
134
+ save: 为空时不保存。否则保存到data_path同名.enc文件或者指定文件路径
135
+
136
+ 返回:
137
+ bytes: 二进制数据包
138
+ """
139
+
140
+ if pubkey is None and self._key_dir is not None:
141
+ _, pubkey, _, _ = self._load_keypair()
142
+ if pubkey is None and self._key_dir is None:
143
+ raise ValueError("未提供公钥,且未指定密钥目录")
144
+ if data is None and data_path is None:
145
+ raise ValueError("必须提供数据或数据路径")
146
+ if not isinstance(pubkey, X25519PublicKey):
147
+ raise TypeError("pubkey 必须是 X25519 公钥")
148
+
149
+ if data_path is not None:
150
+ path = Path(data_path)
151
+ data = path.read_bytes()
152
+
153
+ # ✅ 统一转 bytes
154
+ if isinstance(data, str):
155
+ plaintext = self._TYPE_TEXT+data.encode("utf-8")
156
+ elif isinstance(data, bytes):
157
+ plaintext = self._TYPE_BYTES+data
158
+ else:
159
+ raise TypeError("加密数据必须是 str 或 bytes")
160
+
161
+ # 成一次性密钥(前向安全)
162
+ eph_priv = x25519.X25519PrivateKey.generate()
163
+ eph_pub = eph_priv.public_key()
164
+
165
+ # ECDH 计算共享密钥
166
+ shared_key = eph_priv.exchange(pubkey)
167
+
168
+ key = HKDF(
169
+ algorithm=hashes.SHA256(),
170
+ length=32,
171
+ salt=self._MAGIC + self._VERSION,
172
+ info=b"SIMPLE-ECC",
173
+ ).derive(shared_key)
174
+
175
+ # ChaCha20-Poly1305 加密(带认证)
176
+ nonce = os.urandom(12)
177
+
178
+ eph_pub_bytes = eph_pub.public_bytes(
179
+ encoding=serialization.Encoding.Raw,
180
+ format=serialization.PublicFormat.Raw
181
+ )
182
+ aad = self._MAGIC + self._VERSION + eph_pub_bytes
183
+
184
+ ciphertext = ChaCha20Poly1305(key).encrypt(
185
+ nonce,
186
+ plaintext,
187
+ aad
188
+ )
189
+
190
+ packet = (
191
+ self._MAGIC +
192
+ self._VERSION +
193
+ eph_pub_bytes +
194
+ nonce +
195
+ ciphertext
196
+ )
197
+ # ===== 保存文件 =====
198
+ if save != 'off':
199
+ if data_path is not None:
200
+ if save == 'on':
201
+ out_path = path.with_suffix(path.suffix + ".enc")
202
+ else:
203
+ out_path = save
204
+ else:
205
+ if save == 'on':
206
+ out_path = Path.cwd() / "encrypt_output.enc"
207
+ else:
208
+ out_path = save
209
+ out_path = Path(out_path)
210
+ out_path.parent.mkdir(parents=True, exist_ok=True)
211
+
212
+ with open(out_path, "wb") as f:
213
+ f.write(packet)
214
+
215
+ return packet
216
+
217
+ # ============================================
218
+ # 解密流程:ECC → AEAD → text
219
+ def decrypt(self, packet: bytes = None, privkey=None, enc_data_path=None, save="off"):
220
+ """
221
+ 参数:
222
+ packet (bytes): get_encrypt() 返回的数据
223
+ privkey: 私钥(X25519)
224
+ enc_data_path: .enc 文件路径(可选)
225
+ save: 保存文件名(不含后缀);若 enc_data_path 存在则自动去掉 .enc
226
+ 返回:
227
+ str | bytes
228
+ """
229
+ if privkey is None and self._key_dir is not None:
230
+ privkey, _, _, _ = self._load_keypair()
231
+ if privkey is None and self._key_dir is None:
232
+ raise ValueError("未提供私钥,且未指定密钥目录")
233
+
234
+ if packet is None and enc_data_path is None:
235
+ raise ValueError("必须提供 packet 或 enc_data_path")
236
+
237
+ # ✅ 从文件读取
238
+ if enc_data_path is not None:
239
+ path = Path(enc_data_path)
240
+ packet = path.read_bytes()
241
+
242
+ try:
243
+ # ===== 解析头部 =====
244
+ if len(packet) < 4 + 1 + 32 + 12:
245
+ raise ValueError("数据长度不合法")
246
+
247
+ magic = packet[:4]
248
+ version = packet[4]
249
+
250
+ if magic != self._MAGIC:
251
+ raise ValueError("未知数据格式")
252
+ if version != self._VERSION[0]:
253
+ raise ValueError(f"不支持的版本: {version}")
254
+
255
+ offset = 5
256
+ eph_pub_bytes = packet[offset:offset + 32]
257
+ offset += 32
258
+ nonce = packet[offset:offset + 12]
259
+ offset += 12
260
+ ciphertext = packet[offset:]
261
+
262
+ eph_pub = x25519.X25519PublicKey.from_public_bytes(eph_pub_bytes)
263
+
264
+ # ===== ECDH + HKDF =====
265
+ shared_key = privkey.exchange(eph_pub)
266
+
267
+ key = HKDF(
268
+ algorithm=hashes.SHA256(),
269
+ length=32,
270
+ salt=self._MAGIC + self._VERSION,
271
+ info=b"SIMPLE-ECC",
272
+ ).derive(shared_key)
273
+
274
+ aad = self._MAGIC + self._VERSION + eph_pub_bytes
275
+ plaintext = ChaCha20Poly1305(key).decrypt(nonce, ciphertext, aad)
276
+
277
+ # ===== 解包 =====
278
+ if not plaintext:
279
+ raise ValueError("空明文")
280
+
281
+ t = plaintext[0:1]
282
+ body = plaintext[1:]
283
+ if t == self._TYPE_TEXT:
284
+ result = body.decode("utf-8")
285
+ ext = ".txt"
286
+ out_data = body
287
+ elif t == self._TYPE_BYTES:
288
+ result = body
289
+ ext = ""
290
+ out_data = body
291
+ else:
292
+ raise ValueError("未知明文类型")
293
+
294
+ # ===== 保存文件 =====
295
+
296
+ # ===== 保存文件 =====
297
+ if save != 'off':
298
+ if enc_data_path is not None:
299
+ orig_path = path.with_suffix("") # 去掉 .enc
300
+ if save == 'on':
301
+ # 原文件名 + _decrypt + 原后缀
302
+ out_path = orig_path.with_name(
303
+ f"{orig_path.stem}_decrypt{orig_path.suffix}"
304
+ )
305
+ else:
306
+ out_path = save
307
+ else:
308
+ if save == 'on':
309
+ out_path = Path.cwd() / "decrypt_output.bin"
310
+ else:
311
+ out_path = save
312
+ out_path = Path(out_path)
313
+ out_path.parent.mkdir(parents=True, exist_ok=True)
314
+
315
+ with open(out_path, "wb") as f:
316
+ f.write(out_data)
317
+
318
+ return result
319
+
320
+ except Exception as e:
321
+ print("解密失败:", e)
322
+ return None
323
+
324
+ # ============================================
325
+ # 数字签名与验签功能(Ed25519)
326
+ # ============================================
327
+ def sign(self, data=None, sign_key=None, data_path=None, save="off"):
328
+ """
329
+ 使用 Ed25519 对数据进行数字签名,并生成自描述的签名数据包。
330
+
331
+ 参数:
332
+ data (str | bytes | None): 待签名的数据内容。
333
+
334
+ sign_key (ed25519.Ed25519PrivateKey | None): 可选密钥
335
+
336
+ data_path (str | Path | None): 可选待签名文件的路径(覆盖data)
337
+
338
+ save (str): 控制是否将签名数据包保存为文件。"off"(默认)
339
+
340
+ 返回:
341
+ bytes: 签名数据包(二进制格式),结构如下:
342
+
343
+ """
344
+ if sign_key is None and self._key_dir is not None:
345
+ _, _, sign_key, _ = self._load_keypair()
346
+
347
+ if sign_key is None and self._key_dir is None:
348
+ raise ValueError("未提供签名私钥,且未指定密钥目录")
349
+ if data is None and data_path is None:
350
+ raise ValueError("必须提供 data 或 data_path")
351
+
352
+ # ===== 读取数据 =====
353
+ if data_path is not None:
354
+ path = Path(data_path)
355
+ payload = path.read_bytes() # ✅ 文件永远按 bytes
356
+ t = self._TYPE_BYTES
357
+ else:
358
+ if isinstance(data, str):
359
+ payload = data.encode("utf-8")
360
+ t = self._TYPE_TEXT
361
+ elif isinstance(data, bytes):
362
+ payload = data
363
+ t = self._TYPE_BYTES
364
+ else:
365
+ raise TypeError("签名数据必须是 str 或 bytes")
366
+
367
+ body = t + payload
368
+
369
+ # ===== Ed25519 签名 =====
370
+ sig = sign_key.sign(body)
371
+
372
+ packet = (
373
+ self._SIGN_MAGIC +
374
+ self._SIGN_VERSION +
375
+ body +
376
+ sig
377
+ )
378
+
379
+ # ===== 保存签名文件 =====
380
+ if save != 'off':
381
+ if data_path is not None:
382
+ # 自动生成:原名 + _signed + 原后缀
383
+ if save == 'on':
384
+ out_path = path.with_name(
385
+ f"{path.stem}_signed{path.suffix}"
386
+ )
387
+ else: out_path = save
388
+ else:
389
+ if save == 'on':
390
+ out_path = Path.cwd() / "signed_output.bin"
391
+ else:
392
+ out_path = save
393
+
394
+ out_path = Path(out_path)
395
+ out_path.parent.mkdir(parents=True, exist_ok=True)
396
+
397
+ # ===== 写入签名包 =====
398
+ with open(out_path, "wb") as f:
399
+ f.write(packet)
400
+
401
+ return packet
402
+
403
+ # --------------------------------------------
404
+ def verify(self, signed_data=None, verify_key=None, signed_data_path=None, save="off"):
405
+ """
406
+ 验证 Ed25519 签名
407
+
408
+ 参数:
409
+ signed_data:
410
+ - bytes : 签名数据包
411
+ - str (base64) : 签名数据包
412
+ signed_data_path:
413
+ - None : 从签名包验签
414
+ - 文件路径(str) : 对指定文件验签
415
+
416
+ 返回:
417
+ - str : 验签成功并还原文本数据
418
+ - None : 验签失败
419
+ """
420
+ if verify_key is None and self._key_dir is not None:
421
+ _, _, _, verify_key = self._load_keypair()
422
+ if verify_key is None and self._key_dir is None:
423
+ raise ValueError("未提供验签公钥,且未指定密钥目录")
424
+
425
+ try:
426
+ # ===== 文件验签模式 =====
427
+ if signed_data_path is not None:
428
+ path = Path(signed_data_path)
429
+ packet = path.read_bytes()
430
+
431
+ # ===== 传输层解码 =====
432
+ else:
433
+ if isinstance(signed_data, str):
434
+ import base64
435
+ packet = base64.b64decode(signed_data.encode("ascii"))
436
+ elif isinstance(signed_data, bytes):
437
+ packet = signed_data
438
+ else:
439
+ raise TypeError("验签数据必须是 str 或 bytes")
440
+
441
+ # ===== 基本校验 =====
442
+ if len(packet) < 4 + 1 + 1 + self._SIGN_LEN:
443
+ raise ValueError("签名数据长度不合法")
444
+
445
+ magic = packet[:4]
446
+ version = packet[4]
447
+
448
+ if magic != self._SIGN_MAGIC:
449
+ raise ValueError("未知签名格式")
450
+ if version != self._SIGN_VERSION[0]:
451
+ raise ValueError("不支持的签名版本")
452
+
453
+ offset = 5
454
+ t = packet[offset:offset + 1]
455
+ offset += 1
456
+
457
+ payload = packet[offset:-self._SIGN_LEN]
458
+ sig = packet[-self._SIGN_LEN:]
459
+
460
+ # ===== 数据验签模式 =====
461
+ verify_key.verify(sig, t + payload)
462
+
463
+ # ===== 保存文件 =====
464
+ if save != 'off':
465
+ if signed_data_path is not None:
466
+ # 自动生成:原名 + _verify + 原后缀
467
+ if save == 'on':
468
+ out_path = path.with_name(
469
+ f"{path.stem}_verify{path.suffix}"
470
+ )
471
+ else: out_path = save
472
+ else:
473
+ if save == 'on':
474
+ out_path = Path.cwd() / "verify_output.bin"
475
+ else:
476
+ out_path = save
477
+
478
+ out_path = Path(out_path)
479
+ out_path.parent.mkdir(parents=True, exist_ok=True)
480
+
481
+ # ===== 写入签名包 =====
482
+ with open(out_path, "wb") as f:
483
+ f.write(payload)
484
+
485
+
486
+ # 还原数据,根据类型返回str或bytes
487
+ if t == self._TYPE_TEXT:
488
+ return payload.decode("utf-8")
489
+ elif t == self._TYPE_BYTES:
490
+ return payload
491
+ else:
492
+ raise ValueError("未知数据类型")
493
+
494
+ except Exception as e:
495
+ print("验签失败:", e)
496
+ return None
497
+
498
+
499
+
500
+
501
+ if __name__ == "__main__":
502
+
503
+ from raytoolsbox import useful_func
504
+ tt=useful_func.make_timer()
505
+
506
+ test_data_dir = Path("tests/testData")
507
+ if not test_data_dir.exists():
508
+ test_data_dir.mkdir(parents=True, exist_ok=True)
509
+ with open(test_data_dir / "测试文档.txt", "r", encoding="utf-8") as f:
510
+ plaintext0 = f.read()
511
+ tt()
512
+ cm = CryptoManager(test_data_dir / "ecc_keys")
513
+ priv, pub,_,_ = cm.generate_keypair()
514
+ tt("密钥对生成完成")
515
+ js=cm.get_encrypt(plaintext0, pub)
516
+ tt("加密完成")
517
+ plaintext = cm.get_decrypt(js, priv)
518
+ if plaintext==plaintext0:
519
+ print("解密成功,内容一致")
520
+ tt("解密完成")
521
+ # 写到文件
522
+ with open(test_data_dir / "test_decrypt.txt", "w", encoding="utf-8") as f:
523
+ f.write(plaintext)
524
+ tt()
@@ -0,0 +1,81 @@
1
+ import smtplib
2
+ from email.mime.text import MIMEText
3
+ from email.header import Header
4
+ from email.utils import formataddr
5
+ import yaml
6
+ from pathlib import Path
7
+
8
+ def _load_email_config(provider: str)-> bool:
9
+ # 1. 当前脚本所在目录
10
+ local_config = Path(__file__).parent / "email_config.yaml"
11
+
12
+ # 2. 用户主目录
13
+ home_config = Path.home() / ".email_config.yaml"
14
+
15
+ # 挑选存在的一个
16
+ config_path = local_config if local_config.exists() else home_config
17
+
18
+ if not config_path.exists():
19
+ raise FileNotFoundError(
20
+ f"未找到配置文件: {config_path}\n"
21
+ "你需要手动创建,内容格式如下:\n"
22
+ "qq:\n"
23
+ " smtp_server: smtp.qq.com\n"
24
+ " smtp_port: 465\n"
25
+ " from_addr: your@qq.com\n"
26
+ " password: your_auth_code"
27
+ )
28
+
29
+ with open(config_path, "r", encoding="utf-8") as f:
30
+ all_config = yaml.safe_load(f)
31
+ return all_config[provider]
32
+
33
+
34
+ def send_email(
35
+ subject: str='HelloWorld',
36
+ content: str='这是一个测试邮件',
37
+ to_addr: str='',
38
+ serverName: str='',
39
+ use_smtp:str='qq') -> None:
40
+ """
41
+ :param subject: 邮件主题
42
+ :param content: 邮件内容
43
+ :param to_addr: 接收邮箱地址
44
+ :param serverName: 服务器名称
45
+ :param use_smtp: 使用的SMTP服务提供商
46
+ """
47
+ try:
48
+ config = _load_email_config(use_smtp)
49
+ except Exception as e:
50
+ print(f"加载配置失败: {e}")
51
+ return False
52
+
53
+ smtp_server = config['smtp_server']
54
+ smtp_port = config['smtp_port']
55
+ from_addr = config['from_addr']
56
+ password = config['password']
57
+
58
+ nickname = serverName # 这里自定义你的备注
59
+ if not to_addr:
60
+ to_addr=from_addr # 如果没有指定收件人,则发给自己
61
+
62
+ message = MIMEText(content, 'html', 'utf-8')
63
+ # 用 formataddr,可以自动支持中文昵称
64
+ message['From'] = formataddr((str(Header(nickname, 'utf-8')), from_addr))
65
+ message['To'] = Header(to_addr)
66
+ message['Subject'] = Header(subject, 'utf-8')
67
+
68
+ try:
69
+ server = smtplib.SMTP_SSL(smtp_server, smtp_port)
70
+ server.login(from_addr, password)
71
+ server.sendmail(from_addr, [to_addr], message.as_string())
72
+ server.quit()
73
+ print('消息发送成功!')
74
+ return True
75
+ except Exception as e:
76
+ print('发送失败:', e)
77
+ return False
78
+
79
+ # 下面按你的用法
80
+ if __name__ == '__main__':
81
+ print(send_email())
@@ -0,0 +1,73 @@
1
+ import requests
2
+ # token: str | None = None, 意思是 token 可以是字符串类型,也可以是 None 类型 。默认值是 None 。
3
+ def list_github_repos(username: str, token: str | None = None, print_result: bool = True):
4
+ """
5
+ 获取 GitHub 仓库列表。
6
+
7
+ :param username: GitHub 用户名(可以是自己,也可以是别人)
8
+ :param token: 不传则访问别人公开仓库;传入则可访问自己的公开+私有
9
+ :param print_result: 是否打印
10
+ :return: 仓库列表 [{name, size_mb, private}, ...]
11
+ """
12
+ # 选择 API URL
13
+ if token and username:
14
+ # token 一般只能访问“当前登录用户”
15
+ # 所以如果 username == token中的用户 → /user/repos
16
+ api_url = "https://api.github.com/user/repos"
17
+ use_auth = True
18
+ else:
19
+ # 无 token → 访问公开仓库
20
+ api_url = f"https://api.github.com/users/{username}/repos"
21
+ use_auth = False
22
+
23
+ headers = {"Accept": "application/vnd.github+json"}
24
+ if token:
25
+ headers["Authorization"] = f"token {token}"
26
+
27
+ repos = []
28
+ page = 1
29
+
30
+ while True:
31
+ resp = requests.get(api_url, headers=headers, params={"page": page, "per_page": 100})
32
+
33
+ # ========== 处理错误 Token ==========
34
+ if resp.status_code == 401:
35
+ print("❌ GitHub Token 无效(Bad credentials), 请检查:")
36
+ print(" 1. Token 是否拼写正确")
37
+ print(" 2. Token 是否未过期")
38
+ print(" 3. 如果访问别人公开仓库 → 不要传 token")
39
+ return None # 或者 raise ValueError("无效 Token")
40
+
41
+ # ========== 其它错误 ==========
42
+ if resp.status_code != 200:
43
+ raise RuntimeError(
44
+ f"GitHub API 请求失败:{resp.status_code}\n{resp.text}"
45
+ )
46
+
47
+ batch = resp.json()
48
+ if not batch:
49
+ break
50
+
51
+ for r in batch:
52
+ repos.append({
53
+ "name": r["name"],
54
+ "size_mb": r["size"] / 1024,
55
+ "private": r["private"],
56
+ })
57
+
58
+ page += 1
59
+
60
+ # 如果是 /user/repos,但 username 不是 token 的用户 → 会只返回公开仓库(合理情况)
61
+
62
+ # 打印结果
63
+ if print_result:
64
+ for r in repos:
65
+ print(f"{r['name']:25} {r['size_mb']:7.2f} MB {'private' if r['private'] else 'public'}")
66
+
67
+ return repos
68
+
69
+ if __name__ == "__main__":
70
+
71
+ user = "2dust"
72
+ data = list_github_repos(user)
73
+ # print(data)
@@ -0,0 +1,45 @@
1
+ import os
2
+ import sys
3
+
4
+ def get_resource_path(relpath):
5
+ # PyInstaller 运行环境资源路径修正
6
+ if hasattr(sys, "_MEIPASS"):
7
+ return os.path.join(sys._MEIPASS, relpath)
8
+ return os.path.join(os.path.dirname(__file__), relpath)
9
+
10
+ # 居中窗口
11
+ def center_window(win, width=None, height=None):
12
+ win.update_idletasks()
13
+ w = width or win.winfo_width()
14
+ h = height or win.winfo_height()
15
+ sw = win.winfo_screenwidth()
16
+ sh = win.winfo_screenheight()
17
+ x = (sw - w) // 2
18
+ y = (sh - h) // 3
19
+ win.geometry(f"{w}x{h}+{x}+{y}")
20
+
21
+ import time
22
+
23
+ def make_timer():
24
+ last_time = None
25
+ count = -1
26
+
27
+ def tt(msg=""):
28
+ nonlocal last_time, count
29
+ now = time.time()
30
+ count += 1
31
+ if last_time is None:
32
+ msg_time=f"第{count}次调用({msg}):首次调用,无上次时间参考。"
33
+ print(msg_time)
34
+ else:
35
+ elapsed = (now - last_time) * 1000 # 转为毫秒
36
+ if msg:
37
+ msg_time=f"第{count}次调用({msg}):用时 {elapsed:.2f} ms"
38
+ print(msg_time)
39
+ else:
40
+ msg_time=f"第{count}次调用:用时 {elapsed:.2f} ms"
41
+ print(msg_time)
42
+ last_time = now
43
+ return msg_time
44
+
45
+ return tt
@@ -0,0 +1,133 @@
1
+ Metadata-Version: 2.4
2
+ Name: raytoolsbox
3
+ Version: 1.0.0
4
+ Summary: Add your description here
5
+ Author: TIMLES@https://github.com/TIMLES
6
+ License: MIT
7
+ Requires-Python: >=3.13
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: cryptography>=46.0.4
10
+ Requires-Dist: keyring>=25.7.0
11
+ Requires-Dist: pyyaml>=6.0.3
12
+ Requires-Dist: requests>=2.32.5
13
+
14
+
15
+ # raytoolsbox
16
+
17
+ 一个不断进化的 **个人 Python 工具合集(Toolbox)**。
18
+ 包含常用开发工具、实用脚本、个人算法、桌面应用助手、Web 小工具等。
19
+
20
+ 项目以 **模块化、可扩展、易维护** 为目标,为日常开发与个人项目提供稳定的工具支持。
21
+
22
+ ---
23
+
24
+ ## ✨ 特性(Features)
25
+
26
+ - 📧 **邮件工具**
27
+ 简洁易用的邮件发送函数,可自动读取本地或用户目录的配置文件,自动处理 SSL、中文昵称等问题。
28
+
29
+ - 🔐 **个人加密算法(计划中)**
30
+ 自定义加密/解密模块,适用于本地存储与信息传输。
31
+
32
+ - 🌐 **Web 工具(计划中)**
33
+ 轻量级 FastAPI/Flask 封装,用于快速构建本地服务或 API。
34
+
35
+ - 🖥️ **屏幕工具 & GUI 工具(计划中)**
36
+ 自动 DPI 检测、屏幕尺寸工具、Tkinter/Qt 通用辅助函数。
37
+
38
+ - 🧰 **更多工具持续加入中……**
39
+
40
+ ---
41
+
42
+ ## 📦 安装(Installation)
43
+
44
+ ### 使用 uv(推荐)
45
+ ```
46
+ uv add raytoolsbox
47
+ ```
48
+
49
+ ### 使用 pip
50
+ ```
51
+ pip install raytoolsbox
52
+ ```
53
+
54
+ ---
55
+
56
+ ## 🚀 快速开始(Quick Start)
57
+
58
+
59
+
60
+ ---
61
+
62
+ ## 📂 目录结构(Project Layout)
63
+
64
+ ```
65
+ raytoolsbox/
66
+
67
+ ├── pyproject.toml # 项目信息 + 依赖管理(uv)
68
+ ├── README.md # 项目文档
69
+ ├── src/
70
+ │ └── raytoolsbox/
71
+ │ ├── __init__.py
72
+ │ └── maillToPhone.py
73
+
74
+ └── tests/
75
+ └── test_send_email.py # pytest 单元测试
76
+ ```
77
+
78
+ ---
79
+
80
+ ## 🧪 测试(Testing)
81
+
82
+ 本项目使用 pytest:
83
+
84
+ ```
85
+ uv run pytest
86
+ ```
87
+
88
+ 全部测试应通过:
89
+
90
+ ```
91
+ 2 passed
92
+ ```
93
+
94
+ ---
95
+
96
+ ## 🛠️ 开发路线图(Roadmap)
97
+
98
+ ### 已完成
99
+ - [x] 邮件发送工具封装
100
+ - [x] 可本地覆盖的配置系统
101
+ - [x] pytest 自动化测试(mock 网络发送)
102
+ - [x] 个人加密算法模块(AES + 自定义混淆)
103
+ - [x] 计时工具
104
+
105
+ ### 开发中 / 计划中
106
+ - [ ] Web 工具(FastAPI/Flask 封装)
107
+ - [ ] 屏幕工具(DPI、尺寸测量、窗口工具)
108
+ - [ ] Tkinter / PySide6 GUI 工具类
109
+ - [ ] 文件操作工具(临时文件、下载器)
110
+ - [ ] 日期工具、定时工具、任务提醒系统
111
+ - [ ] 扩展 CLI(命令行使用工具箱)
112
+
113
+ 如果你有想加入的功能欢迎告诉我!
114
+
115
+ ---
116
+
117
+ ## 🤝 贡献(Contributing)
118
+
119
+ 欢迎 Issue 或 PR!
120
+ 可提交:
121
+
122
+ - Bug 报告
123
+ - 新功能建议
124
+ - 工具模块
125
+ - 文档改进
126
+
127
+ ---
128
+
129
+ ## 📄 License
130
+
131
+ 使用 MIT License,自由商用/修改/发布。
132
+
133
+ ---
@@ -0,0 +1,9 @@
1
+ raytoolsbox/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ raytoolsbox/crypto_manager.py,sha256=Oc4Qg_Jw79iWBgu_Wm0jlNgis-eqbApFoHem3nqXbSs,18880
3
+ raytoolsbox/mailToPhone.py,sha256=JhVRMhPlzrQ-B3-Sf2YVNsfa3N1LZSlOSoYJnd4rbNo,2600
4
+ raytoolsbox/to_github.py,sha256=gvOg_ltE2dXQQ2hhpoa0A4Eth-h36NYl0qLRlizxnRM,2602
5
+ raytoolsbox/useful_func.py,sha256=Ks35PHR8IyxdZ1WIIkxL-BNqgKsp7cVYImnHVSRuW_M,1332
6
+ raytoolsbox-1.0.0.dist-info/METADATA,sha256=EqAarP_UBCG3dHydPlgLkY8dlpV2qxY651SHGCOHpV0,2944
7
+ raytoolsbox-1.0.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
8
+ raytoolsbox-1.0.0.dist-info/top_level.txt,sha256=W3bjrGqlV1zMr5MySZQhI4ZT8K-rs2zXQdSNdLI6JDE,12
9
+ raytoolsbox-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ raytoolsbox