tytcli 0.2.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.
- tytcli/__init__.py +7 -0
- tytcli/cli.py +174 -0
- tytcli/client.py +832 -0
- tytcli/commands/__init__.py +52 -0
- tytcli/commands/auth.py +147 -0
- tytcli/commands/config.py +61 -0
- tytcli/commands/env.py +103 -0
- tytcli/commands/help.py +105 -0
- tytcli/commands/roadshow.py +898 -0
- tytcli/commands/sentiment.py +200 -0
- tytcli/commands/shared.py +210 -0
- tytcli/commands/upgrade.py +95 -0
- tytcli/commands/wechat.py +253 -0
- tytcli/commands/whoami.py +76 -0
- tytcli/config.py +578 -0
- tytcli/errors.py +71 -0
- tytcli/output.py +82 -0
- tytcli/selfupdate.py +639 -0
- tytcli/skill/SKILL.md +122 -0
- tytcli/skill/references/output-contract.md +93 -0
- tytcli/skill/references/roadshow.md +176 -0
- tytcli/skill/references/selfupdate.md +62 -0
- tytcli/skill/references/sentiment.md +68 -0
- tytcli/skill/references/setup.md +105 -0
- tytcli/skill/references/wechat.md +87 -0
- tytcli/skill/references/whoami.md +42 -0
- tytcli/skill.py +118 -0
- tytcli-0.2.0.dist-info/METADATA +437 -0
- tytcli-0.2.0.dist-info/RECORD +32 -0
- tytcli-0.2.0.dist-info/WHEEL +4 -0
- tytcli-0.2.0.dist-info/entry_points.txt +2 -0
- tytcli-0.2.0.dist-info/licenses/LICENSE.txt +16 -0
tytcli/__init__.py
ADDED
tytcli/cli.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""tytcli 的命令行入口。
|
|
2
|
+
|
|
3
|
+
输出契约(稳定 —— agent 要解析它):
|
|
4
|
+
|
|
5
|
+
* 结果是 **stdout** 上恰好一个 JSON 文档;**命令一律不写 stderr**(唯一两个例外
|
|
6
|
+
见 AGENTS.md:隐式自更新钩子的警告,以及裸调 ``tytcli`` 时被路由到 stderr 的
|
|
7
|
+
help 文本)。同一个事实只允许一个通道,否则两份副本会逐渐不一致,而自动化会
|
|
8
|
+
分不清哪份是结果。
|
|
9
|
+
* 成功:``{"ok": true, "env": ..., "base_url": ..., "data": {...}}``
|
|
10
|
+
* 失败:``{"ok": false, "error": {"code": ..., "message": ..., "details": {...}}}``
|
|
11
|
+
* 退出码::
|
|
12
|
+
|
|
13
|
+
0 成功
|
|
14
|
+
1 API / 业务错误(服务端有响应,但请求失败)
|
|
15
|
+
2 用法或配置错误
|
|
16
|
+
3 认证错误(TYT_Token 缺失、无效或已过期)
|
|
17
|
+
4 网络错误或超时
|
|
18
|
+
|
|
19
|
+
命令绝不交互式提问;缺值就是一个错误。
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
命令实现住在 ``tytcli/commands/`` 里——一个命令组一个模块。本模块只保留入口该拥有
|
|
23
|
+
的东西:根 group、全局的"异常 -> JSON"翻译,以及 ``main()`` / ``entrypoint()``。
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import sys
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
import click
|
|
32
|
+
|
|
33
|
+
# 以模块方式 import(而不是 `from .selfupdate import plan_upgrade`),这样测试
|
|
34
|
+
# monkeypatch `selfupdate.plan_upgrade` 才会生效。把名字绑定到本地命名空间会
|
|
35
|
+
# **静默**让 patch 失效,于是测试真的去跑一次安装器。
|
|
36
|
+
from . import __version__, selfupdate, skill
|
|
37
|
+
from .commands import ALL_COMMANDS
|
|
38
|
+
from .config import ENV_VAR_CONFIG, ENV_VAR_ENV, ENV_VAR_TOKEN
|
|
39
|
+
from .errors import TytCliError, UsageError
|
|
40
|
+
from .output import (
|
|
41
|
+
EXIT_API,
|
|
42
|
+
EXIT_AUTH,
|
|
43
|
+
EXIT_NETWORK,
|
|
44
|
+
EXIT_OK,
|
|
45
|
+
EXIT_USAGE,
|
|
46
|
+
emit_error,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@click.group(
|
|
51
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
52
|
+
epilog=(
|
|
53
|
+
"\b\n环境变量覆盖:\n"
|
|
54
|
+
f" {ENV_VAR_CONFIG} 配置文件路径\n"
|
|
55
|
+
f" {ENV_VAR_ENV} 当前环境\n"
|
|
56
|
+
f" {ENV_VAR_TOKEN} 当前环境的 TYT_Token\n"
|
|
57
|
+
"\b\n退出码:0 成功 / 1 API 错误 / 2 用法或配置错误 / 3 认证失败 / 4 网络错误"
|
|
58
|
+
),
|
|
59
|
+
)
|
|
60
|
+
@click.version_option(__version__, prog_name="tytcli", message="%(prog)s %(version)s")
|
|
61
|
+
@click.option("--pretty", is_flag=True, help="缩进输出 JSON(默认紧凑单行)。")
|
|
62
|
+
@click.pass_context
|
|
63
|
+
def cli(ctx: click.Context, pretty: bool) -> None:
|
|
64
|
+
"""tytcli — 投研通 API 命令行工具。"""
|
|
65
|
+
ctx.ensure_object(dict)
|
|
66
|
+
ctx.obj["pretty"] = pretty
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# 注册是显式的,而不是靠装饰器:import 一个命令模块不应该要求 import 本模块,
|
|
70
|
+
# 否则就是循环依赖。
|
|
71
|
+
for _command in ALL_COMMANDS:
|
|
72
|
+
cli.add_command(_command)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _append_doc_pointer(command: click.Command, topic: str) -> None:
|
|
76
|
+
"""递归地往一个命令的 ``--help`` epilog 末尾加一行指针。
|
|
77
|
+
|
|
78
|
+
全文由 ``tytcli help`` 承载(见 commands/help.py);``--help`` 只负责指路。
|
|
79
|
+
把指针压到一行,既保住了 ``--help`` 的本分——快而可扫的用法摘要——又让随包
|
|
80
|
+
的 skill 文档能从大家最先看的地方被发现。
|
|
81
|
+
"""
|
|
82
|
+
line = skill.pointer_line(topic)
|
|
83
|
+
command.epilog = (command.epilog or "").rstrip() + "\n\n" + line
|
|
84
|
+
if isinstance(command, click.Group):
|
|
85
|
+
for sub in command.commands.values():
|
|
86
|
+
# 子命令继承所属 group 的主题:参考文档覆盖的是整个模块,
|
|
87
|
+
# 不是每个子命令各一份。
|
|
88
|
+
_append_doc_pointer(sub, topic)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
for _command in ALL_COMMANDS:
|
|
92
|
+
_append_doc_pointer(_command, _command.name or "")
|
|
93
|
+
# 根 group 单独设置指针:从它往下递归会给每个顶层命令**再**追加一次,而它们
|
|
94
|
+
# 已经各自带了一行(变成双指针)。
|
|
95
|
+
cli.epilog = (cli.epilog or "").rstrip() + "\n\n" + skill.pointer_line("")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def main(argv: list[str] | None = None) -> int:
|
|
99
|
+
"""运行 CLI,并且总是把失败翻译成一个 JSON 错误文档。"""
|
|
100
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
101
|
+
pretty = "--pretty" in args
|
|
102
|
+
|
|
103
|
+
# 隐式自更新:查一次包索引(每 10 分钟最多一次),发现新版就升级,然后
|
|
104
|
+
# re-exec,让用户的命令跑在新代码上。**刻意 fail open**——索引不可达绝不能
|
|
105
|
+
# 成为业务命令失败的原因。
|
|
106
|
+
selfupdate.maybe_self_update(__version__, args)
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
cli.main(args=args, prog_name="tytcli", standalone_mode=False)
|
|
110
|
+
except TytCliError as exc:
|
|
111
|
+
emit_error(exc, pretty=pretty)
|
|
112
|
+
return exc.exit_code
|
|
113
|
+
except click.exceptions.UsageError as exc:
|
|
114
|
+
details: dict[str, Any] = {}
|
|
115
|
+
ctx = getattr(exc, "ctx", None)
|
|
116
|
+
command = getattr(ctx, "command", None)
|
|
117
|
+
message = exc.format_message()
|
|
118
|
+
|
|
119
|
+
if ctx is not None:
|
|
120
|
+
details["command"] = ctx.command_path
|
|
121
|
+
|
|
122
|
+
# 裸调 `tytcli` 时,click 会把整份 help 文本塞进 UsageError 的 message。
|
|
123
|
+
# 为了保住 stdout 的机器可读性,把面向人的 help 送到 stderr——那才是
|
|
124
|
+
# 诊断信息该待的地方。
|
|
125
|
+
if message.startswith("Usage:"):
|
|
126
|
+
click.echo(message, err=True)
|
|
127
|
+
message = "缺少子命令。"
|
|
128
|
+
|
|
129
|
+
if isinstance(command, click.Group):
|
|
130
|
+
names = sorted(command.commands)
|
|
131
|
+
details["available_commands"] = names
|
|
132
|
+
details["hint"] = f"可用子命令:{', '.join(names)}"
|
|
133
|
+
else:
|
|
134
|
+
details["hint"] = f"运行 {(ctx.command_path if ctx else 'tytcli')} --help 查看用法。"
|
|
135
|
+
|
|
136
|
+
emit_error(UsageError(message, details=details), pretty=pretty)
|
|
137
|
+
return EXIT_USAGE
|
|
138
|
+
except click.exceptions.Abort:
|
|
139
|
+
emit_error(TytCliError("操作已取消。", code="ABORTED"), pretty=pretty)
|
|
140
|
+
return 130
|
|
141
|
+
except click.exceptions.Exit as exc:
|
|
142
|
+
return int(exc.exit_code or 0)
|
|
143
|
+
except Exception as exc: # 最后一道防线:保证输出仍然是机器可读的
|
|
144
|
+
emit_error(
|
|
145
|
+
TytCliError(
|
|
146
|
+
f"未预期的内部错误:{type(exc).__name__}: {exc}",
|
|
147
|
+
code="INTERNAL_ERROR",
|
|
148
|
+
details={"error_type": type(exc).__name__},
|
|
149
|
+
),
|
|
150
|
+
pretty=pretty,
|
|
151
|
+
)
|
|
152
|
+
return EXIT_API
|
|
153
|
+
return EXIT_OK
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def entrypoint() -> None:
|
|
157
|
+
"""console script 的包装。"""
|
|
158
|
+
sys.exit(main())
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
if __name__ == "__main__":
|
|
162
|
+
entrypoint()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
__all__ = [
|
|
166
|
+
"EXIT_API",
|
|
167
|
+
"EXIT_AUTH",
|
|
168
|
+
"EXIT_NETWORK",
|
|
169
|
+
"EXIT_OK",
|
|
170
|
+
"EXIT_USAGE",
|
|
171
|
+
"cli",
|
|
172
|
+
"entrypoint",
|
|
173
|
+
"main",
|
|
174
|
+
]
|