tars-cli 0.1.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.
@@ -0,0 +1,18 @@
1
+ Metadata-Version: 2.4
2
+ Name: tars-cli
3
+ Version: 0.1.0
4
+ Summary: tars 平台命令行工具
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: typer>=0.12
7
+ Requires-Dist: httpx>=0.27
8
+ Requires-Dist: rich>=13.0
9
+ Requires-Dist: pyyaml>=6.0
10
+ Requires-Dist: platformdirs>=4.0
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest>=8.0; extra == "dev"
13
+ Requires-Dist: pytest-mock>=3.12; extra == "dev"
14
+ Requires-Dist: pytest-cov>=5.0; extra == "dev"
15
+ Requires-Dist: respx>=0.21; extra == "dev"
16
+ Requires-Dist: mypy>=1.8; extra == "dev"
17
+ Requires-Dist: ruff>=0.3; extra == "dev"
18
+ Requires-Dist: build>=1.0; extra == "dev"
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "tars-cli"
7
+ version = "0.1.0"
8
+ description = "tars 平台命令行工具"
9
+ requires-python = ">=3.11"
10
+ dependencies = [
11
+ "typer>=0.12",
12
+ "httpx>=0.27",
13
+ "rich>=13.0",
14
+ "pyyaml>=6.0",
15
+ "platformdirs>=4.0",
16
+ ]
17
+
18
+ [project.optional-dependencies]
19
+ dev = [
20
+ "pytest>=8.0",
21
+ "pytest-mock>=3.12",
22
+ "pytest-cov>=5.0",
23
+ "respx>=0.21",
24
+ "mypy>=1.8",
25
+ "ruff>=0.3",
26
+ "build>=1.0",
27
+ ]
28
+
29
+ [project.scripts]
30
+ tars = "tars_cli.app:app"
31
+
32
+ [tool.pytest.ini_options]
33
+ testpaths = ["tests"]
34
+
35
+ [tool.mypy]
36
+ python_version = "3.11"
37
+ strict = false
38
+ warn_return_any = false
39
+ disallow_untyped_defs = true
40
+
41
+ [tool.ruff]
42
+ target-version = "py311"
43
+ line-length = 120
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,4 @@
1
+ from tars_cli.app import app
2
+
3
+ if __name__ == "__main__":
4
+ app()
@@ -0,0 +1,164 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+
5
+ import typer
6
+ from rich.console import Console
7
+
8
+ from tars_cli import __version__
9
+
10
+ console = Console()
11
+
12
+ app = typer.Typer(
13
+ name="tars",
14
+ help=(
15
+ "tars CLI - 评估平台命令行工具\n\n"
16
+ "🚀 快速上手:\n"
17
+ " 路线 A(智能生成): tars auth login → tars plugin import → tars dataset generate → tars eval run\n"
18
+ " 路线 B(手动上传): tars auth login → tars plugin import → tars template download → tars dataset upload → tars eval run\n\n"
19
+ "运行 tars quickstart 查看完整示例"
20
+ ),
21
+ )
22
+
23
+ auth_app = typer.Typer(
24
+ name="auth",
25
+ help="认证管理\n\n典型流程: tars auth login --server <url>",
26
+ no_args_is_help=True,
27
+ )
28
+ plugin_app = typer.Typer(
29
+ name="plugin",
30
+ help="Plugin 管理\n\n典型流程: tars plugin import <git_url> → tars plugin export-report <plugin_id>",
31
+ no_args_is_help=True,
32
+ )
33
+ dataset_app = typer.Typer(
34
+ name="dataset",
35
+ help="数据集管理\n\n典型流程: tars dataset generate <plugin_id> → tars dataset publish <version_id>",
36
+ no_args_is_help=True,
37
+ )
38
+ eval_app = typer.Typer(
39
+ name="eval",
40
+ help="评估管理\n\n典型流程: tars eval run <plugin_id> → tars eval status <task_id>",
41
+ no_args_is_help=True,
42
+ )
43
+ template_app = typer.Typer(
44
+ name="template",
45
+ help="模板管理\n\n典型流程: tars template download plan → tars template download testcase-single",
46
+ no_args_is_help=True,
47
+ )
48
+
49
+ app.add_typer(auth_app, name="auth")
50
+ app.add_typer(plugin_app, name="plugin")
51
+ app.add_typer(dataset_app, name="dataset")
52
+ app.add_typer(eval_app, name="eval")
53
+ app.add_typer(template_app, name="template")
54
+
55
+ # Global options stored in context
56
+ _output_format: str = "table"
57
+ _quiet: bool = False
58
+ _verbose: bool = False
59
+ _server: Optional[str] = None
60
+
61
+
62
+ @app.callback(invoke_without_command=True)
63
+ def main(
64
+ ctx: typer.Context,
65
+ output: str = typer.Option("table", "--output", "-o", help="输出格式: table, json, yaml"),
66
+ quiet: bool = typer.Option(False, "--quiet", "-q", help="静默模式,仅输出核心结果"),
67
+ verbose: bool = typer.Option(False, "--verbose", help="详细模式,输出 HTTP 请求日志"),
68
+ server: Optional[str] = typer.Option(None, "--server", help="服务端地址"),
69
+ version: bool = typer.Option(False, "--version", "-V", help="显示版本号"),
70
+ ) -> None:
71
+ global _output_format, _quiet, _verbose, _server
72
+ if version:
73
+ typer.echo(f"tars-cli {__version__}")
74
+ raise typer.Exit()
75
+ _output_format = output
76
+ _quiet = quiet
77
+ _verbose = verbose
78
+ _server = server
79
+ if ctx.invoked_subcommand is None:
80
+ typer.echo(ctx.get_help())
81
+ raise typer.Exit()
82
+
83
+
84
+ def get_global_options() -> tuple[str, bool, bool, Optional[str]]:
85
+ return _output_format, _quiet, _verbose, _server
86
+
87
+
88
+ QUICKSTART_TEXT = """\
89
+ 🚀 tars CLI 快速上手
90
+
91
+ ━━━ 路线 A: 智能生成 ━━━
92
+
93
+ # 1. 登录
94
+ tars auth login --server https://tars.example.com
95
+
96
+ # 2. 导入 Plugin
97
+ tars plugin import https://git.example.com/my-plugin.git
98
+
99
+ # 3. 智能生成数据集(Agent / Skill)
100
+ tars dataset generate <plugin_id> --agent-name <name> --publish
101
+ tars dataset generate <plugin_id> --skill-name <name> --publish
102
+
103
+ # 多轮场景
104
+ tars dataset generate <plugin_id> --agent-name <name> -t multi_turn --publish
105
+
106
+ # 4. 运行评估
107
+ tars eval run <plugin_id> --agent-name <name> --dataset-version <version_id>
108
+ tars eval run <plugin_id> --skill-name <name> --dataset-version <version_id>
109
+
110
+ # 5. 导出报告(可选)
111
+ tars plugin export-report <plugin_id>
112
+
113
+ ━━━ 路线 B: 手动上传 ━━━
114
+
115
+ # 1. 登录
116
+ tars auth login --server https://tars.example.com
117
+
118
+ # 2. 导入 Plugin
119
+ tars plugin import https://git.example.com/my-plugin.git
120
+
121
+ # 3. 下载模板
122
+ tars template download plan
123
+ tars template download testcase-single
124
+ tars template download testcase-multi
125
+
126
+ # 4. 填写模板后上传(Agent / Skill)
127
+ tars dataset upload <plugin_id> --agent-name <name> --plan plan.yaml --cases cases.yaml
128
+ tars dataset upload <plugin_id> --skill-name <name> --plan plan.yaml --cases cases.yaml
129
+
130
+ # 5. 运行评估
131
+ tars eval run <plugin_id> --agent-name <name> --dataset-version <version_id>
132
+
133
+ ━━━ 其他常用命令 ━━━
134
+
135
+ # 查看数据集版本列表
136
+ tars dataset list <plugin_id> --agent-name <name>
137
+ tars dataset list <plugin_id> --skill-name <name> -t multi_turn
138
+
139
+ # 发布草稿数据集
140
+ tars dataset publish <version_id>
141
+
142
+ # 查看评估任务状态
143
+ tars eval status <task_id>
144
+
145
+ ━━━ CI/CD 集成 ━━━
146
+
147
+ export TARS_ACCESS_TOKEN=eyJhbGc...
148
+ export TARS_SERVER_URL=https://tars.example.com
149
+ tars eval run <plugin_id> --agent-name <name> --dataset-version <id> -o json --threshold 0.8
150
+ # 退出码: 0=通过, 1=未通过, 2=错误
151
+ """
152
+
153
+
154
+ @app.command()
155
+ def quickstart() -> None:
156
+ """输出完整的端到端使用示例"""
157
+ typer.echo(QUICKSTART_TEXT)
158
+
159
+
160
+ import tars_cli.commands.auth_cmd # noqa: E402, F401
161
+ import tars_cli.commands.plugin_cmd # noqa: E402, F401
162
+ import tars_cli.commands.dataset_cmd # noqa: E402, F401
163
+ import tars_cli.commands.eval_cmd # noqa: E402, F401
164
+ import tars_cli.commands.template_cmd # noqa: E402, F401
@@ -0,0 +1,58 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import stat
6
+ from dataclasses import dataclass
7
+
8
+ from tars_cli.config import get_credentials_path, get_env_token, resolve_server_url
9
+
10
+
11
+ @dataclass
12
+ class Credentials:
13
+ server_url: str
14
+ access_token: str
15
+ refresh_token: str
16
+ expires_at: str = ""
17
+
18
+
19
+ def save_credentials(creds: Credentials) -> None:
20
+ path = get_credentials_path()
21
+ path.parent.mkdir(parents=True, exist_ok=True)
22
+ path.write_text(json.dumps({
23
+ "server_url": creds.server_url,
24
+ "access_token": creds.access_token,
25
+ "refresh_token": creds.refresh_token,
26
+ "expires_at": creds.expires_at,
27
+ }, indent=2))
28
+ os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)
29
+
30
+
31
+ def load_credentials(cli_server: str | None = None) -> Credentials | None:
32
+ env_token = get_env_token()
33
+ if env_token:
34
+ return Credentials(
35
+ server_url=resolve_server_url(cli_server),
36
+ access_token=env_token,
37
+ refresh_token="",
38
+ )
39
+
40
+ path = get_credentials_path()
41
+ if not path.exists():
42
+ return None
43
+ try:
44
+ data = json.loads(path.read_text())
45
+ return Credentials(
46
+ server_url=resolve_server_url(cli_server) if cli_server else data.get("server_url", ""),
47
+ access_token=data["access_token"],
48
+ refresh_token=data.get("refresh_token", ""),
49
+ expires_at=data.get("expires_at", ""),
50
+ )
51
+ except (json.JSONDecodeError, KeyError):
52
+ return None
53
+
54
+
55
+ def clear_credentials() -> None:
56
+ path = get_credentials_path()
57
+ if path.exists():
58
+ path.unlink()
@@ -0,0 +1,117 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import httpx
6
+ import typer
7
+
8
+ from httpx import HTTPTransport
9
+
10
+ from tars_cli.auth import Credentials, load_credentials, save_credentials
11
+
12
+
13
+ class AuthExpiredError(Exception):
14
+ pass
15
+
16
+
17
+ class TarsAPIClient:
18
+ def __init__(self, server_url: str, credentials: Credentials, verbose: bool = False) -> None:
19
+ self._server_url = server_url.rstrip("/")
20
+ self._credentials = credentials
21
+ self._verbose = verbose
22
+ self._client = httpx.Client(base_url=f"{self._server_url}/api/v1", timeout=30.0, transport=HTTPTransport(proxy=None))
23
+
24
+ @classmethod
25
+ def from_context(cls, cli_server: str | None = None, verbose: bool = False) -> TarsAPIClient:
26
+ creds = load_credentials(cli_server)
27
+ if not creds:
28
+ typer.echo("未登录,请先执行 tars auth login", err=True)
29
+ raise typer.Exit(code=2)
30
+ server_url = creds.server_url
31
+ return cls(server_url=server_url, credentials=creds, verbose=verbose)
32
+
33
+ @property
34
+ def server_url(self) -> str:
35
+ return self._server_url
36
+
37
+ def _headers(self) -> dict[str, str]:
38
+ return {"Authorization": f"Bearer {self._credentials.access_token}"}
39
+
40
+ def _log_request(self, method: str, url: str, **kwargs: Any) -> None:
41
+ if self._verbose:
42
+ typer.echo(f" → {method} {url}", err=True)
43
+ if "json" in kwargs:
44
+ typer.echo(f" Body: {kwargs['json']}", err=True)
45
+
46
+ def _log_response(self, resp: httpx.Response) -> None:
47
+ if self._verbose:
48
+ typer.echo(f" ← {resp.status_code} ({len(resp.content)} bytes)", err=True)
49
+
50
+ def _refresh_token(self) -> bool:
51
+ if not self._credentials.refresh_token:
52
+ return False
53
+ try:
54
+ resp = self._client.post("/auth/refresh", json={"refresh_token": self._credentials.refresh_token})
55
+ if resp.status_code == 200:
56
+ data = resp.json()
57
+ self._credentials.access_token = data["access_token"]
58
+ self._credentials.refresh_token = data.get("refresh_token", self._credentials.refresh_token)
59
+ self._credentials.expires_at = data.get("expires_at", "")
60
+ save_credentials(self._credentials)
61
+ return True
62
+ except httpx.HTTPError:
63
+ pass
64
+ return False
65
+
66
+ def _request(self, method: str, path: str, **kwargs: Any) -> httpx.Response:
67
+ url = path
68
+ headers = kwargs.pop("headers", None) or {}
69
+ headers.update(self._headers())
70
+ kwargs["headers"] = headers
71
+ self._log_request(method, url, **kwargs)
72
+
73
+ try:
74
+ resp = self._client.request(method, url, **kwargs)
75
+ except httpx.ConnectError:
76
+ typer.echo(f"无法连接到服务端: {self._server_url}", err=True)
77
+ raise typer.Exit(code=2)
78
+ except httpx.TimeoutException:
79
+ typer.echo(f"请求超时: {method} {url}", err=True)
80
+ raise typer.Exit(code=2)
81
+
82
+ self._log_response(resp)
83
+
84
+ if resp.status_code == 401:
85
+ if self._refresh_token():
86
+ kwargs["headers"].update(self._headers())
87
+ self._log_request(method, url, **kwargs)
88
+ resp = self._client.request(method, url, **kwargs)
89
+ self._log_response(resp)
90
+ else:
91
+ raise AuthExpiredError("认证已过期,请重新执行 tars auth login")
92
+
93
+ return resp
94
+
95
+ def get(self, path: str, **kwargs: Any) -> httpx.Response:
96
+ try:
97
+ return self._request("GET", path, **kwargs)
98
+ except AuthExpiredError as e:
99
+ typer.echo(str(e), err=True)
100
+ raise typer.Exit(code=2)
101
+
102
+ def post(self, path: str, **kwargs: Any) -> httpx.Response:
103
+ try:
104
+ return self._request("POST", path, **kwargs)
105
+ except AuthExpiredError as e:
106
+ typer.echo(str(e), err=True)
107
+ raise typer.Exit(code=2)
108
+
109
+ def put(self, path: str, **kwargs: Any) -> httpx.Response:
110
+ try:
111
+ return self._request("PUT", path, **kwargs)
112
+ except AuthExpiredError as e:
113
+ typer.echo(str(e), err=True)
114
+ raise typer.Exit(code=2)
115
+
116
+ def close(self) -> None:
117
+ self._client.close()
File without changes
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+ import httpx
4
+ import typer
5
+
6
+ from httpx import HTTPTransport
7
+
8
+ from tars_cli.app import auth_app, get_global_options
9
+ from tars_cli.auth import Credentials, clear_credentials, load_credentials, save_credentials
10
+ from tars_cli.client import TarsAPIClient
11
+ from tars_cli.config import resolve_server_url
12
+ from tars_cli.hints import print_hints
13
+ from tars_cli.output import render
14
+
15
+
16
+ @auth_app.command()
17
+ def login(
18
+ server: str = typer.Option("", "--server", "-s", help="服务端地址"),
19
+ ) -> None:
20
+ """登录 tars 平台"""
21
+ output_fmt, quiet, verbose, global_server = get_global_options()
22
+ server_url = resolve_server_url(server or global_server)
23
+
24
+ username = typer.prompt("用户名")
25
+ password = typer.prompt("密码", hide_input=True)
26
+
27
+ client = httpx.Client(base_url=f"{server_url}/api/v1", timeout=30.0, transport=HTTPTransport(proxy=None))
28
+ try:
29
+ resp = client.post("/auth/login", json={"username": username, "password": password})
30
+ if resp.status_code != 200:
31
+ typer.echo(f"登录失败: {resp.json().get('detail', resp.text)}", err=True)
32
+ raise typer.Exit(code=2)
33
+
34
+ data = resp.json()
35
+ creds = Credentials(
36
+ server_url=server_url,
37
+ access_token=data["access_token"],
38
+ refresh_token=data.get("refresh_token", ""),
39
+ expires_at=data.get("expires_at", ""),
40
+ )
41
+ save_credentials(creds)
42
+
43
+ me_resp = client.get("/auth/me", headers={"Authorization": f"Bearer {creds.access_token}"})
44
+ user_info = me_resp.json() if me_resp.status_code == 200 else {}
45
+
46
+ result = {
47
+ "message": "登录成功",
48
+ "username": user_info.get("username", username),
49
+ "server": server_url,
50
+ }
51
+ web_url, next_step = print_hints(server_url, "auth_login", quiet=quiet)
52
+ render(result, output_format=output_fmt, quiet=quiet, web_url=web_url, next_step=next_step)
53
+ finally:
54
+ client.close()
55
+
56
+
57
+ @auth_app.command()
58
+ def logout() -> None:
59
+ """登出并清除本地凭证"""
60
+ output_fmt, quiet, verbose, global_server = get_global_options()
61
+ creds = load_credentials(global_server)
62
+ if creds and creds.refresh_token:
63
+ try:
64
+ client = TarsAPIClient(server_url=creds.server_url, credentials=creds, verbose=verbose)
65
+ client.post("/auth/logout", json={"refresh_token": creds.refresh_token})
66
+ client.close()
67
+ except Exception:
68
+ pass
69
+
70
+ clear_credentials()
71
+ typer.echo("已登出")