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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sub2api-usage
3
- Version: 0.1.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.1.0'
22
- __version_tuple__ = version_tuple = (0, 1, 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, 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
- return data
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, str]) -> None:
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(existing: Optional[dict[str, str]] = None) -> dict[str, str]:
214
- print()
215
- if existing is None:
216
- print("== sub2api-usage 首次配置 ==")
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("== sub2api-usage 修改配置 ==")
220
- print(f"当前账号: {existing.get('email')} 地址: {existing.get('base_url')}")
239
+ print(f"\n== 新建 profile: {profile_name} ==")
240
+ print(f" 现有 profile: {', '.join(profiles)}")
221
241
  print()
222
242
 
223
- base_url = _prompt("后台地址", default=(existing or {}).get("base_url") or DEFAULT_BASE_URL)
224
- email = _prompt("邮箱", default=(existing or {}).get("email"))
225
- if existing and existing.get("password"):
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=(existing or {}).get("timezone") or DEFAULT_TIMEZONE)
250
+ tz = _prompt("时区", default=existing.get("timezone") or DEFAULT_TIMEZONE)
230
251
 
231
- cfg = {"base_url": base_url, "email": email, "password": pwd, "timezone": tz}
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
- print(f"[失败] {e}", file=sys.stderr)
239
- if input("是否重新输入?[Y/n] ").strip().lower() in ("", "y", "yes"):
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
- await client.aclose()
245
- save_config(cfg)
246
- print(f"[OK] 已保存到 {CONFIG_FILE}\n")
247
- return cfg
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(cfg, args.period, args.list, args.page, args.page_size, args.json))
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
- # default: TUI
492
- run_tui(cfg)
609
+ run_tui(profile)
493
610
  return 0
494
611
 
495
612
 
File without changes
File without changes