raytoolsbox 1.0.0__py3-none-any.whl → 1.1.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.
- raytoolsbox/security/__init__.py +5 -0
- raytoolsbox/security/authenticator.py +241 -0
- raytoolsbox/{crypto_manager.py → security/crypto_manager.py} +16 -22
- raytoolsbox/security/pin_manager.py +79 -0
- raytoolsbox/utils/__init__.py +2 -0
- raytoolsbox/utils/useful_func.py +185 -0
- {raytoolsbox-1.0.0.dist-info → raytoolsbox-1.1.0.dist-info}/METADATA +6 -4
- raytoolsbox-1.1.0.dist-info/RECORD +13 -0
- raytoolsbox/useful_func.py +0 -45
- raytoolsbox-1.0.0.dist-info/RECORD +0 -9
- /raytoolsbox/{mailToPhone.py → utils/mailToPhone.py} +0 -0
- {raytoolsbox-1.0.0.dist-info → raytoolsbox-1.1.0.dist-info}/WHEEL +0 -0
- {raytoolsbox-1.0.0.dist-info → raytoolsbox-1.1.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import time,json,os,stat,keyring,pyotp,qrcode
|
|
2
|
+
class Authenticator:
|
|
3
|
+
"""
|
|
4
|
+
基于 RFC 6238 的 TOTP 验证器。
|
|
5
|
+
功能:
|
|
6
|
+
- 生成及验证 TOTP(基于时间的一次性验证码)
|
|
7
|
+
- 使用系统安全存储(keyring)保存密钥,文件存储作为兜底
|
|
8
|
+
- 支持首次绑定流程(先生成 secret,验证成功后再保存)
|
|
9
|
+
- 支持终端展示绑定二维码(ASCII)
|
|
10
|
+
|
|
11
|
+
说明:
|
|
12
|
+
TOTP 本质上需要一个“secret 秘钥”来生成验证码。
|
|
13
|
+
此类负责安全地保存 secret,并提供验证与绑定机制。
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, service_name: str="", account: str = ""):
|
|
17
|
+
"""
|
|
18
|
+
初始化 Authenticator 对象。
|
|
19
|
+
- service_name:你的应用名称,如 "testApp"
|
|
20
|
+
- account:区分不同账户的标识(多个账号时很重要)
|
|
21
|
+
"""
|
|
22
|
+
self.service_name = service_name
|
|
23
|
+
self.account = account
|
|
24
|
+
self._key = f"totp_secret:{account}"
|
|
25
|
+
|
|
26
|
+
# 文件存储路径,例如 ~/.raypassapp_user1_totp
|
|
27
|
+
self._file_path = os.path.expanduser(
|
|
28
|
+
f"~/.{service_name.lower()}_{account}_totp"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# 临时 secret(仅在 get_secret → verify_and_bind 期间使用)
|
|
32
|
+
self._pending_secret = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
self._load_secret()
|
|
37
|
+
self._if_bind = True
|
|
38
|
+
print("✅ 已存在绑定的 Authenticator。")
|
|
39
|
+
|
|
40
|
+
except RuntimeError:
|
|
41
|
+
self._if_bind = False
|
|
42
|
+
print("🔐 未绑定,请使用 .get_secret()初始化...")
|
|
43
|
+
|
|
44
|
+
def exist(self):
|
|
45
|
+
return self._if_bind
|
|
46
|
+
# ============================================================
|
|
47
|
+
# 绑定 / 初始化(不立即保存)
|
|
48
|
+
# ============================================================
|
|
49
|
+
def get_secret(self):
|
|
50
|
+
"""
|
|
51
|
+
生成新的 TOTP secret,但不立即永久保存。
|
|
52
|
+
|
|
53
|
+
功能:
|
|
54
|
+
- 账户尚未绑定时,调用此方法会生成一个新的临时密钥(secret)。
|
|
55
|
+
需配合 verify() 输入首次验证码验证成功后永久保存。
|
|
56
|
+
|
|
57
|
+
返回:
|
|
58
|
+
- dict: 包含以下键值:
|
|
59
|
+
- "uri":TOTP 绑定用的 otpauth URI,可用于生成二维码。
|
|
60
|
+
- "secret":此次生成的临时密钥。
|
|
61
|
+
如果当前账户已绑定,则返回错误说明字符串。
|
|
62
|
+
"""
|
|
63
|
+
if not self._if_bind:
|
|
64
|
+
# 随机生成 TOTP 的 Base32 秘钥
|
|
65
|
+
self._pending_secret = pyotp.random_base32()
|
|
66
|
+
|
|
67
|
+
print("🔐 正在生成 Authenticator 绑定信息...")
|
|
68
|
+
|
|
69
|
+
# 根据 secret 生成 provisioning URI(用于二维码扫描)
|
|
70
|
+
totp = pyotp.TOTP(self._pending_secret)
|
|
71
|
+
uri = totp.provisioning_uri(
|
|
72
|
+
name=self.account, # 在 Authenticator 中显示的账户名
|
|
73
|
+
issuer_name=self.service_name, # 在 Authenticator 中显示的服务名
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# 生成二维码(控制二维码大小)
|
|
77
|
+
qr = qrcode.QRCode(
|
|
78
|
+
version=2, # 固定中等大小,避免过大
|
|
79
|
+
error_correction=qrcode.constants.ERROR_CORRECT_Q,
|
|
80
|
+
box_size=1,
|
|
81
|
+
border=0
|
|
82
|
+
)
|
|
83
|
+
qr.add_data(uri)
|
|
84
|
+
qr.make(fit=True)
|
|
85
|
+
|
|
86
|
+
print("\n📱 请使用 Authenticator 扫描下面二维码:\n")
|
|
87
|
+
qr.print_ascii(invert=True) # 终端 ASCII QR 显示
|
|
88
|
+
print()
|
|
89
|
+
print("📌 请扫描二维码后,输入首次验证码完成绑定。")
|
|
90
|
+
|
|
91
|
+
return {"uri": uri, "secret": self._pending_secret}
|
|
92
|
+
else:
|
|
93
|
+
msg="已存在对应账户和应用的Authenticator,请先删除"
|
|
94
|
+
print(msg)
|
|
95
|
+
return msg
|
|
96
|
+
|
|
97
|
+
# ============================================================
|
|
98
|
+
# 删除现有绑定(解绑)
|
|
99
|
+
# ============================================================
|
|
100
|
+
|
|
101
|
+
def delete(self) -> bool:
|
|
102
|
+
"""
|
|
103
|
+
删除当前账户的 TOTP 绑定。
|
|
104
|
+
|
|
105
|
+
返回:
|
|
106
|
+
- True:至少有一个存储被删除
|
|
107
|
+
- False:两个地方都没有数据(或删除失败)
|
|
108
|
+
"""
|
|
109
|
+
removed = False
|
|
110
|
+
try:
|
|
111
|
+
stored = keyring.get_password(self.service_name, self._key)
|
|
112
|
+
if stored is not None:
|
|
113
|
+
keyring.delete_password(self.service_name, self._key)
|
|
114
|
+
removed = True
|
|
115
|
+
except Exception:
|
|
116
|
+
pass
|
|
117
|
+
# 尝试删除文件存储
|
|
118
|
+
if os.path.exists(self._file_path):
|
|
119
|
+
try:
|
|
120
|
+
os.remove(self._file_path)
|
|
121
|
+
removed = True
|
|
122
|
+
except Exception:
|
|
123
|
+
pass
|
|
124
|
+
self._if_bind = False
|
|
125
|
+
return removed
|
|
126
|
+
|
|
127
|
+
# ============================================================
|
|
128
|
+
# 验证验证码
|
|
129
|
+
# ============================================================
|
|
130
|
+
|
|
131
|
+
def verify(self, code: str, window: int = 1) -> bool:
|
|
132
|
+
"""
|
|
133
|
+
验证已绑定账户的 TOTP 验证码。
|
|
134
|
+
|
|
135
|
+
参数:
|
|
136
|
+
- code: 用户输入的 6 位数字字符串
|
|
137
|
+
- window: 时间容差(默认 1)
|
|
138
|
+
window=0:只接受当前30秒(最严格)
|
|
139
|
+
window=1:接受前/当前/后 共 90 秒(推荐,防手机时间误差)
|
|
140
|
+
|
|
141
|
+
返回:
|
|
142
|
+
- True 验证成功
|
|
143
|
+
- False 验证失败
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
if self._if_bind:
|
|
147
|
+
totp = pyotp.TOTP(self._load_secret()) # 根据存储密钥,创建 TOTP 验证器
|
|
148
|
+
return totp.verify(code, valid_window=window) # 验证输入的验证码
|
|
149
|
+
else:
|
|
150
|
+
if not self._pending_secret:
|
|
151
|
+
raise RuntimeError("当前没有待绑定的 secret,请先执行 get_secret()")
|
|
152
|
+
|
|
153
|
+
totp = pyotp.TOTP(self._pending_secret)
|
|
154
|
+
|
|
155
|
+
# 验证用户输入的验证码
|
|
156
|
+
if totp.verify(code, valid_window=window):
|
|
157
|
+
# 构建保存的数据
|
|
158
|
+
data = {
|
|
159
|
+
"secret": self._pending_secret,
|
|
160
|
+
"created": int(time.time()),
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
# 优先写入系统安全存储
|
|
164
|
+
if not self._save_keyring(data):
|
|
165
|
+
# keyring 不可用则回退到文件存储
|
|
166
|
+
self._save_file(data)
|
|
167
|
+
self._if_bind = True
|
|
168
|
+
print("🎉 首次验证成功,Authenticator 绑定完成。")
|
|
169
|
+
|
|
170
|
+
# 清空临时 secret
|
|
171
|
+
self._pending_secret = None
|
|
172
|
+
return True
|
|
173
|
+
else:
|
|
174
|
+
print("❌ 验证失败请重试。")
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# ============================================================
|
|
179
|
+
# 存储层
|
|
180
|
+
# ============================================================
|
|
181
|
+
|
|
182
|
+
def _save_keyring(self, data: dict) -> bool:
|
|
183
|
+
"""
|
|
184
|
+
尝试将 secret 保存到系统安全存储(keyring)
|
|
185
|
+
写入失败返回 False。
|
|
186
|
+
"""
|
|
187
|
+
try:
|
|
188
|
+
keyring.set_password(self.service_name, self._key, json.dumps(data))
|
|
189
|
+
return True
|
|
190
|
+
except Exception:
|
|
191
|
+
return False
|
|
192
|
+
|
|
193
|
+
def _load_secret(self) -> str:
|
|
194
|
+
"""
|
|
195
|
+
加载已绑定的 secret。
|
|
196
|
+
优先从 keyring 获取。
|
|
197
|
+
如果 keyring 不可用,则回退读取文件。
|
|
198
|
+
如果都不存在 → 表示尚未绑定。
|
|
199
|
+
"""
|
|
200
|
+
# 尝试从 keyring 获取
|
|
201
|
+
try:
|
|
202
|
+
stored = keyring.get_password(self.service_name, self._key)
|
|
203
|
+
if stored:
|
|
204
|
+
return json.loads(stored)["secret"]
|
|
205
|
+
except Exception:
|
|
206
|
+
pass
|
|
207
|
+
|
|
208
|
+
# 回退:文件存储
|
|
209
|
+
if os.path.exists(self._file_path):
|
|
210
|
+
with open(self._file_path, "r") as f:
|
|
211
|
+
return json.load(f)["secret"]
|
|
212
|
+
|
|
213
|
+
raise RuntimeError("Authenticator 尚未绑定")
|
|
214
|
+
|
|
215
|
+
def _save_file(self, data: dict):
|
|
216
|
+
"""
|
|
217
|
+
将 secret 写入文件(兜底方案)。
|
|
218
|
+
写入后将文件权限设置为 600(仅当前用户可读写)。
|
|
219
|
+
"""
|
|
220
|
+
with open(self._file_path, "w") as f:
|
|
221
|
+
json.dump(data, f)
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
os.chmod(self._file_path, stat.S_IRUSR | stat.S_IWUSR)
|
|
225
|
+
except Exception:
|
|
226
|
+
pass
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
if __name__ == "__main__":
|
|
230
|
+
|
|
231
|
+
SERVICE = "RayPassApp"
|
|
232
|
+
ACCOUNT = "user1"
|
|
233
|
+
|
|
234
|
+
auth = Authenticator(SERVICE, ACCOUNT)
|
|
235
|
+
# auth.delete()
|
|
236
|
+
if not auth.exist():
|
|
237
|
+
auth.get_secret()
|
|
238
|
+
print("=== Authenticator Demo ===")
|
|
239
|
+
# 测试绑定成功
|
|
240
|
+
user_input = input("请输入验证码:").strip()
|
|
241
|
+
print("✅ 验证成功" if auth.verify(user_input) else "❌ 验证失败")
|
|
@@ -494,31 +494,25 @@ class CryptoManager:
|
|
|
494
494
|
except Exception as e:
|
|
495
495
|
print("验签失败:", e)
|
|
496
496
|
return None
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
497
|
+
|
|
500
498
|
|
|
501
499
|
if __name__ == "__main__":
|
|
502
500
|
|
|
503
|
-
from raytoolsbox import useful_func
|
|
501
|
+
from raytoolsbox.utils import useful_func
|
|
504
502
|
tt=useful_func.make_timer()
|
|
505
503
|
|
|
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
504
|
tt()
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
tt(
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
505
|
+
test_data_dir = Path("tests/testData")
|
|
506
|
+
cm = CryptoManager()
|
|
507
|
+
priv, pub,sign_priv,sign_pub = cm.generate_keys()
|
|
508
|
+
plaintext0="这是一个测试案例"
|
|
509
|
+
tt('开始')
|
|
510
|
+
data_signed=cm.sign(plaintext0,sign_priv)
|
|
511
|
+
x=cm.encrypt(data_signed, pub)
|
|
512
|
+
|
|
513
|
+
a_signed=cm.decrypt(x,priv)
|
|
514
|
+
a=cm.verify(a_signed,sign_pub)
|
|
515
|
+
|
|
516
|
+
tt('签名加密完成')
|
|
517
|
+
if a==plaintext0:
|
|
518
|
+
print(f"OK")
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import os,base64,hashlib,keyring # 操作系统安全存储
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
class PinManager:
|
|
5
|
+
"""
|
|
6
|
+
只负责 PIN 的生成、验证、修改。
|
|
7
|
+
使用操作系统安全存储 (keyring) 保存哈希,提高安全性。
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
def __init__(self, service_name):
|
|
11
|
+
# service_name 用来区分你的程序名称
|
|
12
|
+
self.service_name = service_name
|
|
13
|
+
|
|
14
|
+
# ============================================================
|
|
15
|
+
# PIN 生成 / 保存
|
|
16
|
+
# ============================================================
|
|
17
|
+
def set_pin(self, pin: str, iterations: int = 200_000):
|
|
18
|
+
"""生成新的 PIN 哈希并保存到系统安全存储"""
|
|
19
|
+
salt = os.urandom(16)
|
|
20
|
+
hash_bytes = hashlib.pbkdf2_hmac(
|
|
21
|
+
"sha256", pin.encode("utf-8"), salt, iterations
|
|
22
|
+
)
|
|
23
|
+
data = {
|
|
24
|
+
"salt": base64.b64encode(salt).decode(),
|
|
25
|
+
"hash": base64.b64encode(hash_bytes).decode(),
|
|
26
|
+
"iterations": iterations,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
# 以字符串方式保存到系统安全存储
|
|
30
|
+
keyring.set_password(self.service_name, "pin_data", json.dumps(data))
|
|
31
|
+
print("PIN 已保存到系统安全存储。")
|
|
32
|
+
return data
|
|
33
|
+
|
|
34
|
+
# ============================================================
|
|
35
|
+
# PIN 验证
|
|
36
|
+
# ============================================================
|
|
37
|
+
def verify_pin(self, pin: str)-> str:
|
|
38
|
+
"""从系统安全存储读取哈希验证输入的 PIN"""
|
|
39
|
+
stored_json = keyring.get_password(self.service_name, "pin_data")
|
|
40
|
+
if not stored_json:
|
|
41
|
+
print("没有检测到保存的 PIN,已经使用默认 PIN。")
|
|
42
|
+
self.set_pin("TIMLES")
|
|
43
|
+
return "not_set"
|
|
44
|
+
|
|
45
|
+
stored = json.loads(stored_json)
|
|
46
|
+
salt = base64.b64decode(stored["salt"])
|
|
47
|
+
iterations = stored["iterations"]
|
|
48
|
+
stored_hash = base64.b64decode(stored["hash"])
|
|
49
|
+
|
|
50
|
+
test_hash = hashlib.pbkdf2_hmac(
|
|
51
|
+
"sha256", pin.encode("utf-8"), salt, iterations
|
|
52
|
+
)
|
|
53
|
+
return "OK" if test_hash == stored_hash else "FAIL"
|
|
54
|
+
|
|
55
|
+
# ============================================================
|
|
56
|
+
# 修改 PIN
|
|
57
|
+
# ============================================================
|
|
58
|
+
def change_pin(self, old_pin: str, new_pin: str):
|
|
59
|
+
"""验证旧 PIN 并更新新的 PIN"""
|
|
60
|
+
if self.verify_pin(old_pin) == "OK":
|
|
61
|
+
return self.set_pin(new_pin)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
if __name__ == "__main__":
|
|
65
|
+
|
|
66
|
+
from raytoolsbox.utils import useful_func
|
|
67
|
+
tt=useful_func.make_timer()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
tt('测试PIN')
|
|
72
|
+
pm = PinManager("RayPassApp")
|
|
73
|
+
# 设置 PIN
|
|
74
|
+
pm.set_pin("TIMLES")
|
|
75
|
+
# 验证 PIN
|
|
76
|
+
tt('设置')
|
|
77
|
+
print("验证正确 PIN:", pm.verify_pin("TIMLES"))
|
|
78
|
+
print("验证错误 PIN:", pm.verify_pin("TIMLEs"))
|
|
79
|
+
tt('验证')
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""
|
|
2
|
+
===============================================
|
|
3
|
+
📦 raytoolsbox.utils.common_tools
|
|
4
|
+
常用辅助函数模块
|
|
5
|
+
-----------------------------------------------
|
|
6
|
+
功能包含:
|
|
7
|
+
- 获取资源路径(兼容 PyInstaller)
|
|
8
|
+
- Tk 窗口居中显示
|
|
9
|
+
- 简易性能计时器 make_timer()
|
|
10
|
+
- 获取指定 GitHub 用户的仓库列表
|
|
11
|
+
===============================================
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import os,sys,time,requests
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
# ============================================================
|
|
17
|
+
# 计时器工具
|
|
18
|
+
# ============================================================
|
|
19
|
+
def make_timer():
|
|
20
|
+
"""
|
|
21
|
+
创建一个简单的性能计时函数。
|
|
22
|
+
|
|
23
|
+
功能:
|
|
24
|
+
- 返回一个内部函数,可多次调用自动计算时间间隔。
|
|
25
|
+
- 输出调用次数与耗时(毫秒)。
|
|
26
|
+
|
|
27
|
+
返回:
|
|
28
|
+
- callable: 内部计时函数 tt(msg)。
|
|
29
|
+
调用格式:
|
|
30
|
+
tt("说明文字") → 打印上次调用到当前的耗时。
|
|
31
|
+
"""
|
|
32
|
+
last_time = None
|
|
33
|
+
count = -1
|
|
34
|
+
|
|
35
|
+
def tt(msg: str = "") -> str:
|
|
36
|
+
nonlocal last_time, count
|
|
37
|
+
now = time.time()
|
|
38
|
+
count += 1
|
|
39
|
+
if last_time is None:
|
|
40
|
+
msg_time = f"第{count}次调用({msg}):首次调用,无上次时间参考。"
|
|
41
|
+
print(msg_time)
|
|
42
|
+
else:
|
|
43
|
+
elapsed = (now - last_time) * 1000 # 毫秒
|
|
44
|
+
msg_time = (
|
|
45
|
+
f"第{count}次调用({msg}):用时 {elapsed:.2f} ms"
|
|
46
|
+
if msg
|
|
47
|
+
else f"第{count}次调用:用时 {elapsed:.2f} ms"
|
|
48
|
+
)
|
|
49
|
+
print(msg_time)
|
|
50
|
+
last_time = now
|
|
51
|
+
return msg_time
|
|
52
|
+
|
|
53
|
+
return tt
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ============================================================
|
|
57
|
+
# 路径工具函数
|
|
58
|
+
# ============================================================
|
|
59
|
+
def get_resource_path(relpath: str) -> str:
|
|
60
|
+
"""
|
|
61
|
+
修正资源路径(兼容 PyInstaller 打包运行环境)。
|
|
62
|
+
|
|
63
|
+
功能:
|
|
64
|
+
- 判断程序是否由 PyInstaller 打包。
|
|
65
|
+
- 自动切换资源访问路径:打包环境使用 sys._MEIPASS。
|
|
66
|
+
|
|
67
|
+
参数:
|
|
68
|
+
- relpath (str): 相对路径字符串。
|
|
69
|
+
|
|
70
|
+
返回:
|
|
71
|
+
- str: 绝对资源路径。
|
|
72
|
+
"""
|
|
73
|
+
if hasattr(sys, "_MEIPASS"):
|
|
74
|
+
return os.path.join(sys._MEIPASS, relpath)
|
|
75
|
+
return os.path.join(os.path.dirname(__file__), relpath)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# ============================================================
|
|
79
|
+
# Tk 窗口居中
|
|
80
|
+
# ============================================================
|
|
81
|
+
def center_window(win, width: int | None = None, height: int | None = None):
|
|
82
|
+
"""
|
|
83
|
+
将 Tkinter 窗口居中显示。
|
|
84
|
+
|
|
85
|
+
参数:
|
|
86
|
+
- win: Tkinter 窗口或 Toplevel 对象。
|
|
87
|
+
- width: 目标宽度(可选,默认当前窗口宽度)。
|
|
88
|
+
- height: 目标高度(可选,默认当前窗口高度)。
|
|
89
|
+
|
|
90
|
+
返回:
|
|
91
|
+
- None(直接更新窗口位置)
|
|
92
|
+
"""
|
|
93
|
+
win.update_idletasks()
|
|
94
|
+
w = width or win.winfo_width()
|
|
95
|
+
h = height or win.winfo_height()
|
|
96
|
+
sw = win.winfo_screenwidth()
|
|
97
|
+
sh = win.winfo_screenheight()
|
|
98
|
+
x = (sw - w) // 2
|
|
99
|
+
y = (sh - h) // 3
|
|
100
|
+
win.geometry(f"{w}x{h}+{x}+{y}")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ============================================================
|
|
105
|
+
# GitHub API 工具
|
|
106
|
+
# ============================================================
|
|
107
|
+
def list_github_repos(username: str, token: str | None = None, print_result: bool = True):
|
|
108
|
+
"""
|
|
109
|
+
获取指定 GitHub 用户的仓库列表。
|
|
110
|
+
|
|
111
|
+
功能:
|
|
112
|
+
- 支持传入个人访问 Token(可访问私有仓库)。
|
|
113
|
+
- 支持分页抓取所有仓库(最大 100 每页)。
|
|
114
|
+
|
|
115
|
+
参数:
|
|
116
|
+
- username: GitHub 用户名。
|
|
117
|
+
- token: 个人访问令牌(str 或 None)。
|
|
118
|
+
如果 token 不为空且用户名为自己,则访问 /user/repos。
|
|
119
|
+
- print_result: 是否打印获取结果(默认 True)。
|
|
120
|
+
|
|
121
|
+
返回:
|
|
122
|
+
- list[dict]: 仓库列表,每个元素结构:
|
|
123
|
+
{
|
|
124
|
+
"name": 仓库名称,
|
|
125
|
+
"size_mb": 仓库大小(MB),
|
|
126
|
+
"private": 是否私有仓库 (bool)
|
|
127
|
+
}
|
|
128
|
+
如果 Token 无效返回 None。
|
|
129
|
+
"""
|
|
130
|
+
# 选择 API URL
|
|
131
|
+
if token and username:
|
|
132
|
+
api_url = "https://api.github.com/user/repos"
|
|
133
|
+
else:
|
|
134
|
+
api_url = f"https://api.github.com/users/{username}/repos"
|
|
135
|
+
|
|
136
|
+
headers = {"Accept": "application/vnd.github+json"}
|
|
137
|
+
if token:
|
|
138
|
+
headers["Authorization"] = f"token {token}"
|
|
139
|
+
|
|
140
|
+
repos = []
|
|
141
|
+
page = 1
|
|
142
|
+
|
|
143
|
+
while True:
|
|
144
|
+
resp = requests.get(api_url, headers=headers, params={"page": page, "per_page": 100})
|
|
145
|
+
|
|
146
|
+
# Token 错误处理
|
|
147
|
+
if resp.status_code == 401:
|
|
148
|
+
print("❌ GitHub Token 无效(Bad credentials),请检查:")
|
|
149
|
+
print(" 1. Token 是否拼写正确")
|
|
150
|
+
print(" 2. Token 是否过期")
|
|
151
|
+
print(" 3. 若只访问公开仓库,请不要传入 Token")
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
if resp.status_code != 200:
|
|
155
|
+
raise RuntimeError(f"GitHub API 请求失败:{resp.status_code}\n{resp.text}")
|
|
156
|
+
|
|
157
|
+
batch = resp.json()
|
|
158
|
+
if not batch:
|
|
159
|
+
break
|
|
160
|
+
|
|
161
|
+
for r in batch:
|
|
162
|
+
repos.append({
|
|
163
|
+
"name": r["name"],
|
|
164
|
+
"size_mb": r["size"] / 1024,
|
|
165
|
+
"private": r["private"],
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
page += 1
|
|
169
|
+
|
|
170
|
+
if print_result:
|
|
171
|
+
for r in repos:
|
|
172
|
+
print(f"{r['name']:25} {r['size_mb']:7.2f} MB {'private' if r['private'] else 'public'}")
|
|
173
|
+
|
|
174
|
+
return repos
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# ========================== 测试入口 ===========================
|
|
178
|
+
if __name__ == "__main__":
|
|
179
|
+
tt = make_timer()
|
|
180
|
+
tt("计时器初始化")
|
|
181
|
+
user = "2dust"
|
|
182
|
+
tt("开始抓取 GitHub 仓库")
|
|
183
|
+
data = list_github_repos(user)
|
|
184
|
+
tt("任务完成")
|
|
185
|
+
print(f"共获取到 {len(data)} 个仓库。")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: raytoolsbox
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: Add your description here
|
|
5
5
|
Author: TIMLES@https://github.com/TIMLES
|
|
6
6
|
License: MIT
|
|
@@ -8,7 +8,9 @@ Requires-Python: >=3.13
|
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
9
|
Requires-Dist: cryptography>=46.0.4
|
|
10
10
|
Requires-Dist: keyring>=25.7.0
|
|
11
|
+
Requires-Dist: pyotp>=2.9.0
|
|
11
12
|
Requires-Dist: pyyaml>=6.0.3
|
|
13
|
+
Requires-Dist: qrcode>=8.2
|
|
12
14
|
Requires-Dist: requests>=2.32.5
|
|
13
15
|
|
|
14
16
|
|
|
@@ -26,13 +28,13 @@ Requires-Dist: requests>=2.32.5
|
|
|
26
28
|
- 📧 **邮件工具**
|
|
27
29
|
简洁易用的邮件发送函数,可自动读取本地或用户目录的配置文件,自动处理 SSL、中文昵称等问题。
|
|
28
30
|
|
|
29
|
-
- 🔐
|
|
31
|
+
- 🔐 **个人加密算法**
|
|
30
32
|
自定义加密/解密模块,适用于本地存储与信息传输。
|
|
31
33
|
|
|
32
|
-
- 🌐 **Web
|
|
34
|
+
- 🌐 **Web 工具**
|
|
33
35
|
轻量级 FastAPI/Flask 封装,用于快速构建本地服务或 API。
|
|
34
36
|
|
|
35
|
-
- 🖥️ **屏幕工具 & GUI
|
|
37
|
+
- 🖥️ **屏幕工具 & GUI 工具**
|
|
36
38
|
自动 DPI 检测、屏幕尺寸工具、Tkinter/Qt 通用辅助函数。
|
|
37
39
|
|
|
38
40
|
- 🧰 **更多工具持续加入中……**
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
raytoolsbox/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
raytoolsbox/to_github.py,sha256=gvOg_ltE2dXQQ2hhpoa0A4Eth-h36NYl0qLRlizxnRM,2602
|
|
3
|
+
raytoolsbox/security/__init__.py,sha256=2S4Eld0loDwKle0gW83byxu6KH8Zk9SfpwH9TiAsecs,182
|
|
4
|
+
raytoolsbox/security/authenticator.py,sha256=POk11M4MR5nhs9ungVyGsIC1laP225IUF3sCQv14Tss,8944
|
|
5
|
+
raytoolsbox/security/crypto_manager.py,sha256=6LMlfd9PrtvFMaW3x3hQ3ddGZU4v4myLvNbCJK7pqOE,18585
|
|
6
|
+
raytoolsbox/security/pin_manager.py,sha256=B0Iuu8oY9a_9Cr5QhE9jr9h92f0VnMe8mciXPfQb5kU,2905
|
|
7
|
+
raytoolsbox/utils/__init__.py,sha256=sncYTfqEkIk_cUDRyrle0rTOt_h7LOaJQJI38rmlaQ4,177
|
|
8
|
+
raytoolsbox/utils/mailToPhone.py,sha256=JhVRMhPlzrQ-B3-Sf2YVNsfa3N1LZSlOSoYJnd4rbNo,2600
|
|
9
|
+
raytoolsbox/utils/useful_func.py,sha256=-KzFo5_nXTzeh-KbCBGCzva8EeerK0nuyTAP-yflW4E,5958
|
|
10
|
+
raytoolsbox-1.1.0.dist-info/METADATA,sha256=vg2AHFtN9GXDKHR0_hecTo9fqtKZvn696Og3Q6BRnZ4,2956
|
|
11
|
+
raytoolsbox-1.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
12
|
+
raytoolsbox-1.1.0.dist-info/top_level.txt,sha256=W3bjrGqlV1zMr5MySZQhI4ZT8K-rs2zXQdSNdLI6JDE,12
|
|
13
|
+
raytoolsbox-1.1.0.dist-info/RECORD,,
|
raytoolsbox/useful_func.py
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
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
|
|
@@ -1,9 +0,0 @@
|
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|