sub2api-usage 0.1.0__tar.gz → 0.2.0__tar.gz
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.
- {sub2api_usage-0.1.0 → sub2api_usage-0.2.0}/PKG-INFO +1 -19
- {sub2api_usage-0.1.0 → sub2api_usage-0.2.0}/README.md +0 -18
- {sub2api_usage-0.1.0 → sub2api_usage-0.2.0}/_version.py +2 -2
- {sub2api_usage-0.1.0 → sub2api_usage-0.2.0}/sub2api_usage.py +144 -27
- {sub2api_usage-0.1.0 → sub2api_usage-0.2.0}/.gitignore +0 -0
- {sub2api_usage-0.1.0 → sub2api_usage-0.2.0}/LICENSE +0 -0
- {sub2api_usage-0.1.0 → sub2api_usage-0.2.0}/pyproject.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sub2api-usage
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Interactive TUI for querying sub2api backend usage stats (Claude / OpenAI / Gemini)
|
|
5
5
|
Project-URL: Repository, https://github.com/kadaliao/sub2api-usage
|
|
6
6
|
Project-URL: Issues, https://github.com/kadaliao/sub2api-usage/issues
|
|
@@ -75,24 +75,6 @@ sub2api-usage print --period month --json
|
|
|
75
75
|
|
|
76
76
|
数量统一用计算机领域的 K/M/G/T,耗时按 ms/s/min/h/d 自动选择最适合的尺度。
|
|
77
77
|
|
|
78
|
-
## 开发
|
|
79
|
-
|
|
80
|
-
```bash
|
|
81
|
-
git clone https://github.com/kadaliao/sub2api-usage.git
|
|
82
|
-
cd sub2api-usage
|
|
83
|
-
uv sync
|
|
84
|
-
uv run sub2api-usage --help
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
## 发布流程
|
|
88
|
-
|
|
89
|
-
CI 在每次 push 都会构建包;推 `v*.*.*` tag 时通过 Trusted Publishing 自动发布到 PyPI。
|
|
90
|
-
|
|
91
|
-
```bash
|
|
92
|
-
git tag v0.1.0
|
|
93
|
-
git push --tags
|
|
94
|
-
```
|
|
95
|
-
|
|
96
78
|
## License
|
|
97
79
|
|
|
98
80
|
[MIT](LICENSE)
|
|
@@ -50,24 +50,6 @@ sub2api-usage print --period month --json
|
|
|
50
50
|
|
|
51
51
|
数量统一用计算机领域的 K/M/G/T,耗时按 ms/s/min/h/d 自动选择最适合的尺度。
|
|
52
52
|
|
|
53
|
-
## 开发
|
|
54
|
-
|
|
55
|
-
```bash
|
|
56
|
-
git clone https://github.com/kadaliao/sub2api-usage.git
|
|
57
|
-
cd sub2api-usage
|
|
58
|
-
uv sync
|
|
59
|
-
uv run sub2api-usage --help
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
## 发布流程
|
|
63
|
-
|
|
64
|
-
CI 在每次 push 都会构建包;推 `v*.*.*` tag 时通过 Trusted Publishing 自动发布到 PyPI。
|
|
65
|
-
|
|
66
|
-
```bash
|
|
67
|
-
git tag v0.1.0
|
|
68
|
-
git push --tags
|
|
69
|
-
```
|
|
70
|
-
|
|
71
53
|
## License
|
|
72
54
|
|
|
73
55
|
[MIT](LICENSE)
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.
|
|
22
|
-
__version_tuple__ = version_tuple = (0,
|
|
21
|
+
__version__ = version = '0.2.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -34,7 +34,7 @@ CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
|
34
34
|
|
|
35
35
|
# ===== Config =================================================================
|
|
36
36
|
|
|
37
|
-
def load_config() -> Optional[dict[str,
|
|
37
|
+
def load_config() -> Optional[dict[str, Any]]:
|
|
38
38
|
if not CONFIG_FILE.exists():
|
|
39
39
|
return None
|
|
40
40
|
try:
|
|
@@ -43,15 +43,30 @@ def load_config() -> Optional[dict[str, str]]:
|
|
|
43
43
|
return None
|
|
44
44
|
if not isinstance(data, dict):
|
|
45
45
|
return None
|
|
46
|
-
|
|
46
|
+
if "profiles" not in data and "email" in data:
|
|
47
|
+
return {"default": "default", "profiles": {"default": data}}
|
|
48
|
+
if isinstance(data.get("profiles"), dict):
|
|
49
|
+
return data
|
|
50
|
+
return None
|
|
47
51
|
|
|
48
52
|
|
|
49
|
-
def save_config(cfg: dict[str,
|
|
53
|
+
def save_config(cfg: dict[str, Any]) -> None:
|
|
50
54
|
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
51
55
|
CONFIG_FILE.write_text(json.dumps(cfg, indent=2, ensure_ascii=False))
|
|
52
56
|
CONFIG_FILE.chmod(stat.S_IRUSR | stat.S_IWUSR)
|
|
53
57
|
|
|
54
58
|
|
|
59
|
+
def resolve_profile(cfg: dict[str, Any], name: Optional[str] = None) -> tuple[str, dict[str, str]]:
|
|
60
|
+
profiles = cfg.get("profiles") or {}
|
|
61
|
+
target = name or cfg.get("default")
|
|
62
|
+
if not target:
|
|
63
|
+
raise APIError("配置中没有可用 profile,请先运行 'sub2api-usage setup'")
|
|
64
|
+
if target not in profiles:
|
|
65
|
+
avail = ", ".join(profiles) or "(空)"
|
|
66
|
+
raise APIError(f"profile '{target}' 不存在;现有: {avail}")
|
|
67
|
+
return target, profiles[target]
|
|
68
|
+
|
|
69
|
+
|
|
55
70
|
# ===== API client =============================================================
|
|
56
71
|
|
|
57
72
|
class APIError(RuntimeError):
|
|
@@ -210,41 +225,60 @@ def _prompt(label: str, default: Optional[str] = None, secret: bool = False) ->
|
|
|
210
225
|
print(" 请输入非空值")
|
|
211
226
|
|
|
212
227
|
|
|
213
|
-
async def run_setup(
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
228
|
+
async def run_setup(cfg: Optional[dict[str, Any]] = None, name: Optional[str] = None) -> dict[str, Any]:
|
|
229
|
+
cfg = cfg or {"default": "", "profiles": {}}
|
|
230
|
+
profiles: dict[str, dict[str, str]] = dict(cfg.get("profiles") or {})
|
|
231
|
+
profile_name = name or cfg.get("default") or "default"
|
|
232
|
+
|
|
233
|
+
if not profiles:
|
|
234
|
+
print("\n== sub2api-usage 首次配置 ==")
|
|
217
235
|
print("(密码以明文保存到 ~/.config/sub2api-usage/config.json,文件权限 600)")
|
|
236
|
+
elif profile_name in profiles:
|
|
237
|
+
print(f"\n== 修改 profile: {profile_name} ==")
|
|
218
238
|
else:
|
|
219
|
-
print("==
|
|
220
|
-
print(f"
|
|
239
|
+
print(f"\n== 新建 profile: {profile_name} ==")
|
|
240
|
+
print(f" 现有 profile: {', '.join(profiles)}")
|
|
221
241
|
print()
|
|
222
242
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
243
|
+
existing = profiles.get(profile_name) or {}
|
|
244
|
+
base_url = _prompt("后台地址", default=existing.get("base_url") or DEFAULT_BASE_URL)
|
|
245
|
+
email = _prompt("邮箱", default=existing.get("email"))
|
|
246
|
+
if existing.get("password"):
|
|
226
247
|
pwd = getpass.getpass("密码 (回车保留原值): ").strip() or existing["password"]
|
|
227
248
|
else:
|
|
228
249
|
pwd = _prompt("密码", secret=True)
|
|
229
|
-
tz = _prompt("时区", default=
|
|
250
|
+
tz = _prompt("时区", default=existing.get("timezone") or DEFAULT_TIMEZONE)
|
|
230
251
|
|
|
231
|
-
|
|
252
|
+
entry = {"base_url": base_url, "email": email, "password": pwd, "timezone": tz}
|
|
232
253
|
|
|
233
254
|
print("\n登录验证中...")
|
|
234
255
|
client = Client(base_url, email, pwd, tz)
|
|
256
|
+
login_err: Optional[APIError] = None
|
|
235
257
|
try:
|
|
236
258
|
await client.login()
|
|
237
259
|
except APIError as e:
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
await client.aclose()
|
|
241
|
-
return await run_setup(cfg)
|
|
260
|
+
login_err = e
|
|
261
|
+
finally:
|
|
242
262
|
await client.aclose()
|
|
263
|
+
|
|
264
|
+
if login_err is not None:
|
|
265
|
+
print(f"[失败] {login_err}", file=sys.stderr)
|
|
266
|
+
if input("是否重新输入?[Y/n] ").strip().lower() in ("", "y", "yes"):
|
|
267
|
+
profiles[profile_name] = entry
|
|
268
|
+
return await run_setup({"default": cfg.get("default") or "", "profiles": profiles}, profile_name)
|
|
243
269
|
raise SystemExit(1)
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
270
|
+
|
|
271
|
+
profiles[profile_name] = entry
|
|
272
|
+
new_cfg = {
|
|
273
|
+
"default": cfg.get("default") or profile_name,
|
|
274
|
+
"profiles": profiles,
|
|
275
|
+
}
|
|
276
|
+
save_config(new_cfg)
|
|
277
|
+
print(f"[OK] profile '{profile_name}' 已保存到 {CONFIG_FILE}")
|
|
278
|
+
if len(profiles) > 1 and new_cfg["default"] != profile_name:
|
|
279
|
+
print(f" 当前 default 仍是 '{new_cfg['default']}' (用 'sub2api-usage profiles use {profile_name}' 切换)")
|
|
280
|
+
print()
|
|
281
|
+
return new_cfg
|
|
248
282
|
|
|
249
283
|
|
|
250
284
|
# ===== Non-interactive print mode ============================================
|
|
@@ -451,13 +485,62 @@ def run_tui(cfg: dict[str, str]) -> None:
|
|
|
451
485
|
UsageApp(cfg).run()
|
|
452
486
|
|
|
453
487
|
|
|
488
|
+
# ===== Profile management =====================================================
|
|
489
|
+
|
|
490
|
+
def cmd_profiles_list(cfg: dict[str, Any]) -> int:
|
|
491
|
+
profiles = cfg.get("profiles") or {}
|
|
492
|
+
if not profiles:
|
|
493
|
+
print("(无 profile,先运行 'sub2api-usage setup')")
|
|
494
|
+
return 0
|
|
495
|
+
default = cfg.get("default")
|
|
496
|
+
name_w = max(len(n) for n in profiles)
|
|
497
|
+
email_w = max(len(p.get("email", "")) for p in profiles.values())
|
|
498
|
+
for n, p in profiles.items():
|
|
499
|
+
marker = "*" if n == default else " "
|
|
500
|
+
print(f" {marker} {n:<{name_w}} {p.get('email', ''):<{email_w}} {p.get('base_url', '')}")
|
|
501
|
+
print(f"\n* = default ({default})")
|
|
502
|
+
return 0
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def cmd_profiles_use(cfg: dict[str, Any], name: str) -> int:
|
|
506
|
+
profiles = cfg.get("profiles") or {}
|
|
507
|
+
if name not in profiles:
|
|
508
|
+
print(f"[错误] profile '{name}' 不存在;现有: {', '.join(profiles) or '(空)'}", file=sys.stderr)
|
|
509
|
+
return 1
|
|
510
|
+
cfg["default"] = name
|
|
511
|
+
save_config(cfg)
|
|
512
|
+
print(f"default 已切换到 '{name}'")
|
|
513
|
+
return 0
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def cmd_profiles_remove(cfg: dict[str, Any], name: str) -> int:
|
|
517
|
+
profiles = dict(cfg.get("profiles") or {})
|
|
518
|
+
if name not in profiles:
|
|
519
|
+
print(f"[错误] profile '{name}' 不存在", file=sys.stderr)
|
|
520
|
+
return 1
|
|
521
|
+
if input(f"确认删除 profile '{name}' ? [y/N] ").strip().lower() not in ("y", "yes"):
|
|
522
|
+
print("取消")
|
|
523
|
+
return 0
|
|
524
|
+
del profiles[name]
|
|
525
|
+
cfg["profiles"] = profiles
|
|
526
|
+
if cfg.get("default") == name:
|
|
527
|
+
cfg["default"] = next(iter(profiles), "")
|
|
528
|
+
if cfg["default"]:
|
|
529
|
+
print(f" 顺便把 default 切到了 '{cfg['default']}'")
|
|
530
|
+
save_config(cfg)
|
|
531
|
+
print(f"已删除 profile '{name}'")
|
|
532
|
+
return 0
|
|
533
|
+
|
|
534
|
+
|
|
454
535
|
# ===== CLI ====================================================================
|
|
455
536
|
|
|
456
537
|
def build_parser() -> argparse.ArgumentParser:
|
|
457
538
|
p = argparse.ArgumentParser(description="sub2api 用量查询")
|
|
539
|
+
p.add_argument("-P", "--profile", help="使用指定的 profile (默认: 配置里的 default)")
|
|
458
540
|
sub = p.add_subparsers(dest="cmd")
|
|
459
541
|
|
|
460
|
-
sub.add_parser("setup", help="(重新) 配置账号信息")
|
|
542
|
+
sp = sub.add_parser("setup", help="(重新) 配置账号信息")
|
|
543
|
+
sp.add_argument("name", nargs="?", help="profile 名称,省略则更新当前 default")
|
|
461
544
|
|
|
462
545
|
pp = sub.add_parser("print", help="非交互打印 (脚本/管道用)")
|
|
463
546
|
pp.add_argument("--period", default="today", choices=[k for k, _ in PERIODS])
|
|
@@ -465,31 +548,65 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
465
548
|
pp.add_argument("--page", type=int, default=1)
|
|
466
549
|
pp.add_argument("--page-size", type=int, default=20)
|
|
467
550
|
pp.add_argument("--json", action="store_true")
|
|
551
|
+
|
|
552
|
+
pf = sub.add_parser("profiles", help="管理 profile (多账号/多后台)")
|
|
553
|
+
pf_sub = pf.add_subparsers(dest="action")
|
|
554
|
+
pf_sub.add_parser("list", help="列出所有 profile")
|
|
555
|
+
pu = pf_sub.add_parser("use", help="切换 default profile")
|
|
556
|
+
pu.add_argument("name")
|
|
557
|
+
prm = pf_sub.add_parser("remove", help="删除 profile")
|
|
558
|
+
prm.add_argument("name")
|
|
468
559
|
return p
|
|
469
560
|
|
|
470
561
|
|
|
471
562
|
def main() -> int:
|
|
563
|
+
try:
|
|
564
|
+
return _main()
|
|
565
|
+
except KeyboardInterrupt:
|
|
566
|
+
print("\n已中断", file=sys.stderr)
|
|
567
|
+
return 130
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def _main() -> int:
|
|
472
571
|
args = build_parser().parse_args()
|
|
473
572
|
cfg = load_config()
|
|
474
573
|
|
|
475
574
|
if args.cmd == "setup":
|
|
476
|
-
asyncio.run(run_setup(cfg))
|
|
575
|
+
asyncio.run(run_setup(cfg, args.name))
|
|
576
|
+
return 0
|
|
577
|
+
|
|
578
|
+
if args.cmd == "profiles":
|
|
579
|
+
if cfg is None:
|
|
580
|
+
print("还没有任何 profile,先运行 'sub2api-usage setup'", file=sys.stderr)
|
|
581
|
+
return 1
|
|
582
|
+
action = args.action or "list"
|
|
583
|
+
if action == "list":
|
|
584
|
+
return cmd_profiles_list(cfg)
|
|
585
|
+
if action == "use":
|
|
586
|
+
return cmd_profiles_use(cfg, args.name)
|
|
587
|
+
if action == "remove":
|
|
588
|
+
return cmd_profiles_remove(cfg, args.name)
|
|
477
589
|
return 0
|
|
478
590
|
|
|
479
591
|
if cfg is None:
|
|
480
592
|
print("未检测到配置,进入引导...")
|
|
481
593
|
cfg = asyncio.run(run_setup(None))
|
|
482
594
|
|
|
595
|
+
try:
|
|
596
|
+
_, profile = resolve_profile(cfg, args.profile)
|
|
597
|
+
except APIError as e:
|
|
598
|
+
print(f"[错误] {e}", file=sys.stderr)
|
|
599
|
+
return 2
|
|
600
|
+
|
|
483
601
|
if args.cmd == "print":
|
|
484
602
|
try:
|
|
485
|
-
asyncio.run(cmd_print(
|
|
603
|
+
asyncio.run(cmd_print(profile, args.period, args.list, args.page, args.page_size, args.json))
|
|
486
604
|
except APIError as e:
|
|
487
605
|
print(f"[错误] {e}", file=sys.stderr)
|
|
488
606
|
return 1
|
|
489
607
|
return 0
|
|
490
608
|
|
|
491
|
-
|
|
492
|
-
run_tui(cfg)
|
|
609
|
+
run_tui(profile)
|
|
493
610
|
return 0
|
|
494
611
|
|
|
495
612
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|