cli-anything-cliproxyapi 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.
@@ -0,0 +1,309 @@
1
+ """E2E 测试 - 需要运行中的 CLIProxyAPI 服务器。
2
+
3
+ 通过环境变量配置连接:
4
+ CPA_URL=http://127.0.0.1:8317 CPA_KEY=your-management-key pytest test_full_e2e.py -v
5
+
6
+ 设置 CLI_ANYTHING_FORCE_INSTALLED=1 可强制使用已安装的 CLI 命令。
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import os
13
+ import shutil
14
+ import subprocess
15
+ import sys
16
+ import tempfile
17
+
18
+ import pytest
19
+
20
+ # ============================================================
21
+ # 配置
22
+ # ============================================================
23
+
24
+ CPA_URL = os.getenv("CPA_URL", "http://127.0.0.1:8317")
25
+ CPA_KEY = os.getenv("CPA_KEY", "")
26
+ HARNESS_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
27
+ os.environ["PYTHONPATH"] = HARNESS_ROOT + os.pathsep + os.environ.get("PYTHONPATH", "")
28
+
29
+
30
+ def _resolve_cli(name: str = "cli-anything-cliproxyapi") -> list[str]:
31
+ """解析 CLI 命令路径。默认使用当前源码的 python -m 调用,避免依赖外部安装状态。"""
32
+ if os.getenv("CLI_ANYTHING_FORCE_INSTALLED"):
33
+ return [name]
34
+ return [sys.executable, "-m", "cli_anything.cliproxyapi.cliproxyapi_cli"]
35
+
36
+
37
+ def _cli(*args, json_mode: bool = True) -> subprocess.CompletedProcess:
38
+ """执行 CLI 命令。"""
39
+ cmd = _resolve_cli()
40
+ cmd = cmd + ["--url", CPA_URL, "--key", CPA_KEY]
41
+ if json_mode:
42
+ cmd.append("--json")
43
+ cmd.extend(args)
44
+ return subprocess.run(cmd, capture_output=True, text=True, timeout=30)
45
+
46
+
47
+ # ============================================================
48
+ # Skip 条件
49
+ # ============================================================
50
+
51
+ def _server_available() -> bool:
52
+ try:
53
+ result = _cli("server", "status", json_mode=False)
54
+ return result.returncode == 0
55
+ except Exception:
56
+ return False
57
+
58
+
59
+ skip_no_server = pytest.mark.skipif(
60
+ not _server_available(),
61
+ reason="CLIProxyAPI 服务器不可用,设置 CPA_URL 和 CPA_KEY 环境变量",
62
+ )
63
+
64
+
65
+ # ============================================================
66
+ # 服务器状态测试
67
+ # ============================================================
68
+
69
+ class TestServer:
70
+
71
+ @skip_no_server
72
+ def test_health_check(self):
73
+ result = _cli("server", "status")
74
+ assert result.returncode == 0
75
+ data = json.loads(result.stdout)
76
+ assert data.get("status") == "ok"
77
+
78
+ @skip_no_server
79
+ def test_version(self):
80
+ result = _cli("server", "version")
81
+ assert result.returncode == 0
82
+
83
+
84
+ # ============================================================
85
+ # 配置测试
86
+ # ============================================================
87
+
88
+ class TestConfig:
89
+
90
+ @skip_no_server
91
+ def test_get_config(self):
92
+ result = _cli("config", "get")
93
+ assert result.returncode == 0
94
+ data = json.loads(result.stdout)
95
+ assert isinstance(data, dict)
96
+
97
+ @skip_no_server
98
+ def test_get_config_yaml(self):
99
+ result = _cli("config", "get-yaml")
100
+ assert result.returncode == 0
101
+ assert "port" in result.stdout
102
+
103
+ @skip_no_server
104
+ def test_get_debug(self):
105
+ result = _cli("config", "debug")
106
+ assert result.returncode == 0
107
+
108
+ @skip_no_server
109
+ def test_get_routing_strategy(self):
110
+ result = _cli("config", "routing")
111
+ assert result.returncode == 0
112
+
113
+ @skip_no_server
114
+ def test_get_request_retry(self):
115
+ result = _cli("config", "retry")
116
+ assert result.returncode == 0
117
+
118
+ @skip_no_server
119
+ def test_get_proxy_url(self):
120
+ result = _cli("config", "proxy-url")
121
+ assert result.returncode == 0
122
+
123
+
124
+ # ============================================================
125
+ # 认证文件测试
126
+ # ============================================================
127
+
128
+ class TestAuth:
129
+
130
+ @skip_no_server
131
+ def test_list_auth_files(self):
132
+ result = _cli("auth", "list")
133
+ assert result.returncode == 0
134
+
135
+ @skip_no_server
136
+ def test_list_enabled_auth_files(self):
137
+ result = _cli("auth", "list", "--enabled")
138
+ assert result.returncode == 0
139
+ data = json.loads(result.stdout)
140
+ assert "files" in data
141
+
142
+ @skip_no_server
143
+ def test_list_disabled_auth_files(self):
144
+ result = _cli("auth", "list", "--disabled")
145
+ assert result.returncode == 0
146
+ data = json.loads(result.stdout)
147
+ assert "files" in data
148
+
149
+ @skip_no_server
150
+ def test_codex_quota(self):
151
+ result = _cli("auth", "codex-quota")
152
+ assert result.returncode == 0
153
+ data = json.loads(result.stdout)
154
+ assert "quotas" in data
155
+ assert {"total", "success", "failed"}.issubset(data.keys())
156
+
157
+ @skip_no_server
158
+ def test_list_auth_models(self):
159
+ result = _cli("auth", "models")
160
+ assert result.returncode == 0
161
+
162
+
163
+ # ============================================================
164
+ # OAuth 测试
165
+ # ============================================================
166
+
167
+ class TestOAuth:
168
+
169
+ @skip_no_server
170
+ def test_providers_list(self):
171
+ """验证 oauth login 命令接受所有提供商。"""
172
+ for provider in ["anthropic", "codex", "gemini", "antigravity", "qwen", "kimi", "iflow"]:
173
+ # 不实际登录,只验证命令解析
174
+ result = _cli("oauth", "login", "--help")
175
+ assert result.returncode == 0
176
+
177
+
178
+ # ============================================================
179
+ # API 密钥测试
180
+ # ============================================================
181
+
182
+ class TestKeys:
183
+
184
+ @skip_no_server
185
+ def test_list_api_keys(self):
186
+ result = _cli("keys", "list")
187
+ assert result.returncode == 0
188
+
189
+ @skip_no_server
190
+ def test_list_gemini_keys(self):
191
+ result = _cli("keys", "gemini", "list")
192
+ assert result.returncode == 0
193
+
194
+ @skip_no_server
195
+ def test_list_claude_keys(self):
196
+ result = _cli("keys", "claude", "list")
197
+ assert result.returncode == 0
198
+
199
+ @skip_no_server
200
+ def test_list_codex_keys(self):
201
+ result = _cli("keys", "codex", "list")
202
+ assert result.returncode == 0
203
+
204
+
205
+ # ============================================================
206
+ # 模型测试
207
+ # ============================================================
208
+
209
+ class TestModels:
210
+
211
+ @skip_no_server
212
+ def test_get_aliases(self):
213
+ result = _cli("models", "aliases")
214
+ assert result.returncode == 0
215
+
216
+ @skip_no_server
217
+ def test_get_excluded(self):
218
+ result = _cli("models", "excluded")
219
+ assert result.returncode == 0
220
+
221
+
222
+ # ============================================================
223
+ # 使用统计测试
224
+ # ============================================================
225
+
226
+ class TestUsage:
227
+
228
+ @skip_no_server
229
+ def test_get_stats(self):
230
+ result = _cli("usage", "stats")
231
+ assert result.returncode == 0
232
+
233
+
234
+ # ============================================================
235
+ # 日志测试
236
+ # ============================================================
237
+
238
+ class TestLogs:
239
+
240
+ @skip_no_server
241
+ def test_list_logs(self):
242
+ result = _cli("logs", "list")
243
+ assert result.returncode == 0
244
+
245
+
246
+ # ============================================================
247
+ # Amp 测试
248
+ # ============================================================
249
+
250
+ class TestAmp:
251
+
252
+ @skip_no_server
253
+ def test_get_amp_config(self):
254
+ result = _cli("amp", "config")
255
+ assert result.returncode == 0
256
+
257
+
258
+ # ============================================================
259
+ # CLI Subprocess 测试
260
+ # ============================================================
261
+
262
+ class TestCLISubprocess:
263
+ """测试安装后的 CLI 命令行工具。"""
264
+
265
+ def test_help_flag(self):
266
+ cli = _resolve_cli()
267
+ result = subprocess.run(cli + ["--help"], capture_output=True, text=True, timeout=10)
268
+ assert result.returncode == 0
269
+ assert "CLIProxyAPI" in result.stdout
270
+
271
+ def test_version_flag(self):
272
+ cli = _resolve_cli()
273
+ result = subprocess.run(cli + ["--version"], capture_output=True, text=True, timeout=10)
274
+ assert result.returncode == 0
275
+ assert "1.0.0" in result.stdout
276
+
277
+ def test_no_args_shows_help(self):
278
+ cli = _resolve_cli()
279
+ result = subprocess.run(cli, capture_output=True, text=True, timeout=10)
280
+ assert result.returncode == 0
281
+ assert "Usage" in result.stdout
282
+
283
+ def test_json_flag_no_server(self):
284
+ """无服务器时 --json 应输出错误 JSON。"""
285
+ result = _cli("server", "status")
286
+ # 连接失败时返回非零退出码
287
+ assert result.returncode != 0 or "ok" in result.stdout
288
+
289
+ def test_all_command_groups_have_help(self):
290
+ """验证所有命令组的 --help 正常工作。"""
291
+ groups = ["server", "config", "auth", "oauth", "keys", "models", "usage", "logs", "amp"]
292
+ cli = _resolve_cli()
293
+ for group in groups:
294
+ result = subprocess.run(cli + [group, "--help"], capture_output=True, text=True, timeout=10)
295
+ assert result.returncode == 0, f"'{group} --help' 失败: {result.stderr}"
296
+ assert "Usage" in result.stdout, f"'{group} --help' 无 Usage"
297
+
298
+ def test_api_call_help(self):
299
+ """验证 api-call 命令帮助。"""
300
+ cli = _resolve_cli()
301
+ result = subprocess.run(cli + ["api-call", "--help"], capture_output=True, text=True, timeout=10)
302
+ assert result.returncode == 0
303
+
304
+ def test_auth_codex_quota_help(self):
305
+ """验证 auth codex-quota 命令帮助。"""
306
+ cli = _resolve_cli()
307
+ result = subprocess.run(cli + ["auth", "codex-quota", "--help"], capture_output=True, text=True, timeout=10)
308
+ assert result.returncode == 0
309
+ assert "获取已启用 Codex 凭证额度" in result.stdout
@@ -0,0 +1 @@
1
+ """Utility modules for CLIProxyAPI CLI harness."""
@@ -0,0 +1,77 @@
1
+ """输出格式化工具。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any, List, Dict
7
+
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+ from rich.panel import Panel
11
+ from rich.text import Text
12
+
13
+ console = Console()
14
+ error_console = Console(stderr=True)
15
+
16
+
17
+ def output_json(data: Any) -> None:
18
+ """输出 JSON 格式。"""
19
+ console.print(json.dumps(data, indent=2, ensure_ascii=False))
20
+
21
+
22
+ def output_result(data: Any, json_mode: bool = False) -> None:
23
+ """统一输出入口。"""
24
+ if json_mode:
25
+ output_json(data)
26
+ elif isinstance(data, str):
27
+ console.print(data)
28
+ elif isinstance(data, dict):
29
+ _output_dict(data)
30
+ elif isinstance(data, list):
31
+ _output_list(data)
32
+ else:
33
+ console.print(str(data))
34
+
35
+
36
+ def _output_dict(data: dict) -> None:
37
+ if "value" in data and len(data) <= 2:
38
+ console.print(str(data["value"]))
39
+ elif "status" in data and data["status"] == "ok":
40
+ console.print("[green]OK[/green]")
41
+ elif "error" in data:
42
+ console.print(f"[red]错误: {data['error']}[/red]")
43
+ else:
44
+ console.print_json(json.dumps(data, ensure_ascii=False))
45
+
46
+
47
+ def _output_list(data: list) -> None:
48
+ if not data:
49
+ console.print("[dim](空)[/dim]")
50
+ return
51
+ for item in data:
52
+ if isinstance(item, dict):
53
+ _output_dict(item)
54
+ else:
55
+ console.print(str(item))
56
+
57
+
58
+ def output_table(headers: List[str], rows: List[List[str]], title: str = "") -> None:
59
+ """输出 Rich 表格。"""
60
+ table = Table(title=title, show_header=True, header_style="bold cyan")
61
+ for h in headers:
62
+ table.add_column(h)
63
+ for row in rows:
64
+ table.add_row(*[str(c) for c in row])
65
+ console.print(table)
66
+
67
+
68
+ def output_kv(data: dict, title: str = "") -> None:
69
+ """输出键值对。"""
70
+ if title:
71
+ console.print(f"[bold]{title}[/bold]")
72
+ for k, v in data.items():
73
+ console.print(f" [cyan]{k}[/cyan]: {v}")
74
+
75
+
76
+ def output_error(msg: str) -> None:
77
+ error_console.print(f"[red]错误: {msg}[/red]")
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.1
2
+ Name: cli-anything-cliproxyapi
3
+ Version: 1.0.0
4
+ Summary: CLI harness for CLIProxyAPI - proxy server with OpenAI/Gemini/Claude/Codex compatible APIs
5
+ Requires-Python: >=3.9
6
+ Requires-Dist: click >=8.0
7
+ Requires-Dist: requests >=2.28
8
+ Requires-Dist: rich >=13.0
9
+ Requires-Dist: PyYAML >=6.0
10
+
@@ -0,0 +1,21 @@
1
+ cli_anything/cliproxyapi/__init__.py,sha256=SxTEjspfkfc47HH3FFOYRzPgtExokJmtEEW-8M3jIis,117
2
+ cli_anything/cliproxyapi/cliproxyapi_cli.py,sha256=Pc4fAvNJ0ORIfaF7ZnOEAi7q2i1tJRTLmid5ap2Q9As,36681
3
+ cli_anything/cliproxyapi/core/__init__.py,sha256=xJno96prqlLjY_g0sFLUI2csOf3bCJBQeTniyTkywNQ,48
4
+ cli_anything/cliproxyapi/core/api_keys.py,sha256=_XN-egpTa4pM1Hl3qLQfd8tp_5VT0mtSCZyDYywIcQ8,4901
5
+ cli_anything/cliproxyapi/core/auth.py,sha256=H47ihhI5k2TZBak7QaEK9wdbRID49923ZrKKRIVTyDg,6792
6
+ cli_anything/cliproxyapi/core/client.py,sha256=YEtgGzxXyCx9DfgiaUQsgY4e27rjry7Xl1Ovx4aZW5c,3750
7
+ cli_anything/cliproxyapi/core/config.py,sha256=o-WaoNyaYZ5p8Zzri6JuU-IacBgfm0u1FRFJHIWMMGg,6337
8
+ cli_anything/cliproxyapi/core/logs.py,sha256=n2ja9xJMKHzi03Gt5JrlObgeFqX7BdOgx5DP7ESEpdA,1405
9
+ cli_anything/cliproxyapi/core/models.py,sha256=znAE0KJiZaoGm2gl-0ypnNnLZx_oQwUYQwGOYo0kZvk,2313
10
+ cli_anything/cliproxyapi/core/oauth.py,sha256=Z0FUBH-jorKr7UaWhN5Lr2oF0MtsZR0SpAfdG5WOm0c,2058
11
+ cli_anything/cliproxyapi/core/proxy.py,sha256=BVwwp4I8Oz5wfGHKtfX-AntE3bxwsxZd72R2n2wNNTk,5075
12
+ cli_anything/cliproxyapi/core/usage.py,sha256=A3KHhvVxprOb65BO7RcTGgT2luslVzeTArzfGe1EG14,673
13
+ cli_anything/cliproxyapi/tests/test_core.py,sha256=23FUxO8M4UPKhElYVzUR5QdAuYl4il9jdq5YHjJpwWE,24079
14
+ cli_anything/cliproxyapi/tests/test_full_e2e.py,sha256=KP-Re-ebqOMYYIWSaEn-KG119rtD5oIiFqqiUg1cJbM,9618
15
+ cli_anything/cliproxyapi/utils/__init__.py,sha256=7cQdBgWYoBZNtVXRMJ_WI2oidWSNSu-CtjGVDZRrV9A,51
16
+ cli_anything/cliproxyapi/utils/output.py,sha256=DKWZRaR59W689WG6dMf0QDDyUNpAA_QGNMiN5zJbmZk,2081
17
+ cli_anything_cliproxyapi-1.0.0.dist-info/METADATA,sha256=0ranBZbibBZEtFOT7lWKZymAcBe1KWtnSPL-31GgMJ4,305
18
+ cli_anything_cliproxyapi-1.0.0.dist-info/WHEEL,sha256=BNRMDyzLkkcmlv0J8ppDQkk2VED33SesJDynr9ED1gc,91
19
+ cli_anything_cliproxyapi-1.0.0.dist-info/entry_points.txt,sha256=KKK-gkUAuWjcnPPImEUNpoH3E55IuiFa_p1owQ_6DNk,91
20
+ cli_anything_cliproxyapi-1.0.0.dist-info/top_level.txt,sha256=LI1GTe19xehXrxQtg-3ltETALXYkoctC4Y_iuDiCSRo,13
21
+ cli_anything_cliproxyapi-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.3.4)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ cli-anything-cliproxyapi = cli_anything.cliproxyapi.cliproxyapi_cli:main
@@ -0,0 +1 @@
1
+ cli_anything