cyberquant 1.0.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,53 @@
1
+ # Dependencies
2
+ node_modules/
3
+ .pnpm-store/
4
+
5
+ # Python
6
+ __pycache__/
7
+ *.py[cod]
8
+ *$py.class
9
+ *.so
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ *.egg-info/
24
+ .installed.cfg
25
+ *.egg
26
+ MANIFEST
27
+ .venv/
28
+ venv/
29
+ ENV/
30
+ env/
31
+
32
+ # Node
33
+ dist/
34
+ *.log
35
+ npm-debug.log*
36
+ yarn-debug.log*
37
+ yarn-error.log*
38
+
39
+ # IDE
40
+ .vscode/
41
+ .idea/
42
+ *.swp
43
+ *.swo
44
+ *~
45
+ .DS_Store
46
+
47
+ # Environment
48
+ .env
49
+ .env.local
50
+ .env.*.local
51
+
52
+ # Config
53
+ ~/.cyberquant/
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: cyberquant
3
+ Version: 1.0.0
4
+ Summary: 数据共享API服务平台命令行工具
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: httpx>=0.28.0
7
+ Requires-Dist: rich>=13.9.0
8
+ Requires-Dist: typer>=0.15.0
@@ -0,0 +1,202 @@
1
+ # cyberquant — Python CLI
2
+
3
+ 数据共享API服务平台命令行工具(Python 版),通过 pipx 或 pip 安装使用。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ # 一次性运行
9
+ pipx run cyberquant query stock_daily --code 000001.SZ
10
+
11
+ # 安装后使用
12
+ pipx install cyberquant
13
+ cyberquant query stock_daily --code 000001.SZ
14
+
15
+ # 或 pip 安装
16
+ pip install cyberquant
17
+ ```
18
+
19
+ ## 首次配置
20
+
21
+ ```bash
22
+ cyberquant config set
23
+ ```
24
+
25
+ 交互式输入 API 端点地址和 API Key,保存后自动验证。
26
+
27
+ 配置文件存储在 `~/.cyberquant/config.json`(与 Node.js 版共用):
28
+
29
+ ```json
30
+ {
31
+ "endpoint": "https://api.example.com",
32
+ "apiKey": "sk_live_xxxxx",
33
+ "pageSize": 50
34
+ }
35
+ ```
36
+
37
+ ## 命令
38
+
39
+ ### config — 配置管理
40
+
41
+ ```bash
42
+ # 交互式设置端点地址和 API Key
43
+ cyberquant config set
44
+
45
+ # 查看当前配置
46
+ cyberquant config list
47
+
48
+ # 查询当前 API Key 对应的用户状态
49
+ cyberquant config status
50
+ ```
51
+
52
+ `config status` 调用 API Gateway 的 `/api/v1/me` 端点,显示:
53
+ - 邮箱、Tier 等级、可用市场、过期时间、限流配置
54
+
55
+ ### query — 数据查询
56
+
57
+ ```bash
58
+ cyberquant query <routeSlug> [动态参数...] [选项]
59
+ ```
60
+
61
+ **选项**:
62
+
63
+ | 选项 | 说明 | 默认值 |
64
+ |------|------|--------|
65
+ | `--format <format>` | 输出格式:`json` 或 `csv` | `json` |
66
+ | `--output <file>` | 输出到文件(不指定则输出到终端) | - |
67
+ | `--pageSize <number>` | 每页条数 | config.json > 50 |
68
+ | `--cursor <cursor>` | 游标翻页 | - |
69
+
70
+ **动态参数**:除上述固定选项外,所有 `--key value` 直接作为查询参数透传给 API Gateway,CLI 不做校验。
71
+
72
+ **示例**:
73
+
74
+ ```bash
75
+ # 基本查询(JSON 输出)
76
+ cyberquant query stock_daily --code 000001.SZ
77
+
78
+ # CSV 格式输出到文件
79
+ cyberquant query stock_daily --code 000001.SZ --format csv --output daily.csv
80
+
81
+ # 带日期范围和自定义每页条数
82
+ cyberquant query stock_daily --code 000001.SZ --startDate 2026-01-01 --endDate 2026-05-01 --pageSize 100
83
+
84
+ # 游标翻页(返回结果中会提示 nextCursor)
85
+ cyberquant query stock_daily --code 000001.SZ --cursor eyJ0cmFkZV9kYXRlIjoiMjAyNC0wMS0wMiJ9
86
+ ```
87
+
88
+ **pageSize 优先级**:`--pageSize` 参数 > `config.json` 中的 `pageSize` > 默认 50
89
+
90
+ ### stream — 实时流
91
+
92
+ ```bash
93
+ cyberquant stream <routeSlug> [动态参数...] [选项]
94
+ ```
95
+
96
+ **选项**:
97
+
98
+ | 选项 | 说明 | 默认值 |
99
+ |------|------|--------|
100
+ | `--pageSize <number>` | 每批条数 | config.json > 50 |
101
+ | `--format <format>` | 输出格式:`json` 或 `csv` | `json` |
102
+ | `--output <file>` | 输出到文件(不指定则输出到终端) | - |
103
+
104
+ **示例**:
105
+
106
+ ```bash
107
+ # 实时行情(JSON 输出到终端)
108
+ cyberquant stream stock_realtime --code 000001.SZ
109
+
110
+ # 输出到 CSV 文件
111
+ cyberquant stream stock_realtime --code 000001.SZ --format csv --output realtime.csv
112
+
113
+ # Ctrl+C 优雅退出
114
+ ```
115
+
116
+ ## 技术栈
117
+
118
+ | 依赖 | 用途 |
119
+ |------|------|
120
+ | Typer | CLI 框架(Click 现代封装) |
121
+ | Rich | 终端美化(表格、面板、颜色、进度条) |
122
+ | httpx | HTTP 客户端 |
123
+ | hatchling | 构建后端 |
124
+
125
+ **Python 版本**:>= 3.10
126
+
127
+ ## 项目结构
128
+
129
+ ```
130
+ src/cyberquant/
131
+ ├── __init__.py # 版本信息
132
+ ├── __main__.py # Typer 入口
133
+ ├── commands/
134
+ │ ├── config.py # config set/list/status
135
+ │ ├── query.py # query 命令 + 动态参数解析
136
+ │ ├── stream.py # stream 命令(SSE)
137
+ │ └── export.py # export 命令(ThreadPoolExecutor 并发)
138
+ └── lib/
139
+ ├── api_client.py # httpx 客户端(自动重试、错误映射)
140
+ ├── sse_client.py # SSE 客户端(httpx stream 解析)
141
+ ├── config.py # 读写 ~/.cyberquant/config.json
142
+ ├── formatter.py # JSON/CSV 格式化输出
143
+ ├── display.py # Rich 面板展示用户信息
144
+ └── errors.py # 异常类 + 中文消息映射
145
+ ```
146
+
147
+ ## 开发
148
+
149
+ ### 本地开发模式(推荐)
150
+
151
+ 使用 `pip install -e .` 进行**可编辑模式安装**,修改代码后无需重新安装即可直接运行:
152
+
153
+ ```bash
154
+ cd packages/api-cli-py
155
+
156
+ # 可编辑模式安装(仅需执行一次)
157
+ pip install -e .
158
+
159
+ # 运行
160
+ cyberquant --help
161
+ cyberquant query stock_daily --code 000001.SZ
162
+ ```
163
+
164
+ ### 直接运行模块模式(不安装)
165
+
166
+ 如果不希望安装到系统,也可以直接运行模块:
167
+
168
+ ```bash
169
+ cd packages/api-cli-py
170
+
171
+ # 不安装直接运行
172
+ PYTHONPATH=src python3 -m cyberquant --help
173
+ PYTHONPATH=src python3 -m cyberquant query stock_daily --code 000001.SZ
174
+ ```
175
+
176
+ **注意**:直接运行模块模式需要每次都设置 `PYTHONPATH=src`,不如可编辑模式方便。
177
+
178
+ ### 卸载开发模式
179
+
180
+ ```bash
181
+ pip uninstall cyberquant
182
+ ```
183
+
184
+ ## 构建
185
+
186
+ ```bash
187
+ pip install build twine
188
+ python3 -m build
189
+ twine upload dist/*
190
+ ```
191
+
192
+ ## 错误码
193
+
194
+ | HTTP 状态码 | 中文消息 |
195
+ |-------------|---------|
196
+ | 400 | 参数校验失败 |
197
+ | 401 | API Key 无效或已过期 |
198
+ | 403 | 权限不足 |
199
+ | 404 | 未找到 API 路由 |
200
+ | 429 | 请求频率超限(自动重试) |
201
+ | 500 | 服务器内部错误 |
202
+ | 503 | 服务暂不可用(自动重试) |
@@ -0,0 +1,20 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "cyberquant"
7
+ version = "1.0.0"
8
+ description = "数据共享API服务平台命令行工具"
9
+ requires-python = ">=3.10"
10
+ dependencies = [
11
+ "typer>=0.15.0",
12
+ "rich>=13.9.0",
13
+ "httpx>=0.28.0",
14
+ ]
15
+
16
+ [project.scripts]
17
+ cyberquant = "cyberquant.__main__:cli"
18
+
19
+ [tool.hatch.build.targets.wheel]
20
+ packages = ["src/cyberquant"]
@@ -0,0 +1 @@
1
+ __version__ = "1.0.0"
@@ -0,0 +1,35 @@
1
+ import typer
2
+ from typing import Optional
3
+ from cyberquant.commands import config, query, stream
4
+
5
+ cli = typer.Typer(help="数据共享API服务平台命令行工具", no_args_is_help=True)
6
+
7
+ cli.add_typer(config.app, name="config")
8
+
9
+ # 直接注册 query 和 stream 为命令
10
+ # 设置 allow_extra_args 和 allow_interspersed_args 以支持任意动态参数
11
+ @cli.command(name="query", context_settings={"ignore_unknown_options": True, "allow_extra_args": True, "allow_interspersed_args": True})
12
+ def query_cmd(
13
+ ctx: typer.Context,
14
+ route_slug: str = typer.Argument(help="路由名称"),
15
+ format: str = typer.Option("json", "--format", help="输出格式: json | csv"),
16
+ output: Optional[str] = typer.Option(None, "--output", help="输出到文件"),
17
+ page_size: Optional[int] = typer.Option(None, "--pageSize", help="每页条数"),
18
+ cursor: Optional[str] = typer.Option(None, "--cursor", help="游标翻页"),
19
+ fetch_all: bool = typer.Option(False, "--all", help="自动翻页查询全部数据(流式写入)"),
20
+ ) -> None:
21
+ """查询数据"""
22
+ query.query(ctx, route_slug, format, output, page_size, cursor, fetch_all)
23
+
24
+ @cli.command(name="stream", context_settings={"ignore_unknown_options": True, "allow_extra_args": True, "allow_interspersed_args": True})
25
+ def stream_cmd(
26
+ ctx: typer.Context,
27
+ route_slug: str = typer.Argument(help="路由名称"),
28
+ format: str = typer.Option("json", "--format", help="输出格式: json | csv"),
29
+ output: Optional[str] = typer.Option(None, "--output", help="输出到文件"),
30
+ ) -> None:
31
+ """流式查询"""
32
+ stream.stream_cmd(ctx, route_slug, format, output)
33
+
34
+ if __name__ == "__main__":
35
+ cli()
File without changes
@@ -0,0 +1,62 @@
1
+ from typing import Optional
2
+
3
+ import typer
4
+ from rich.console import Console
5
+ from rich.prompt import Prompt
6
+
7
+ from cyberquant.lib.config import load_config, require_config, save_config, mask_api_key
8
+ from cyberquant.lib.api_client import ApiClient
9
+ from cyberquant.lib.display import show_user_info
10
+ from cyberquant.lib.errors import ConfigError
11
+
12
+ app = typer.Typer(help="配置管理")
13
+ console = Console()
14
+
15
+
16
+ @app.command("set")
17
+ def config_set() -> None:
18
+ """设置 API 端点和密钥"""
19
+ existing = load_config()
20
+
21
+ endpoint = Prompt.ask("API 端点地址", default=existing.get("endpoint", "http://localhost:4000") if existing else "http://localhost:4000")
22
+ api_key = Prompt.ask("API Key", password=True)
23
+
24
+ with console.status("验证中..."):
25
+ with ApiClient({"endpoint": endpoint, "apiKey": api_key}) as client:
26
+ try:
27
+ res = client.get_profile()
28
+ if not res.get("success") or not res.get("data"):
29
+ console.print(f"[red]验证失败: {res.get('error', '未知错误')}[/red]")
30
+ return
31
+ save_config({"endpoint": endpoint, "apiKey": api_key})
32
+ console.print("[green]配置已保存并验证通过[/green]")
33
+ show_user_info(res["data"])
34
+ except Exception as e:
35
+ console.print(f"[red]验证失败: {e}[/red]")
36
+
37
+
38
+ @app.command("list")
39
+ def config_list() -> None:
40
+ """查看当前配置"""
41
+ cfg = require_config()
42
+ lines = [
43
+ f"端点: {cfg['endpoint']}",
44
+ f"API Key: {mask_api_key(cfg['apiKey'])}",
45
+ ]
46
+ if cfg.get("pageSize"):
47
+ lines.append(f"默认条数: {cfg['pageSize']}")
48
+ console.print("\n".join(lines))
49
+
50
+
51
+ @app.command("status")
52
+ def config_status() -> None:
53
+ """查询当前 API Key 用户状态"""
54
+ cfg = require_config()
55
+ with console.status("查询中..."):
56
+ with ApiClient(cfg) as client:
57
+ res = client.get_profile()
58
+ if not res.get("success") or not res.get("data"):
59
+ console.print(f"[red]查询失败: {res.get('error', '未知错误')}[/red]")
60
+ return
61
+ console.print("[green]查询成功[/green]")
62
+ show_user_info(res["data"])
@@ -0,0 +1,158 @@
1
+ import sys
2
+ from typing import Optional
3
+
4
+ import typer
5
+
6
+ from cyberquant.lib.config import require_config
7
+ from cyberquant.lib.api_client import ApiClient
8
+ from cyberquant.lib.formatter import format_json, format_csv, create_stream_writer
9
+ from cyberquant.lib.stream_writer import StreamWriter
10
+
11
+
12
+ def _parse_extra_args(args: list[str], known_options: list[str] | None = None) -> dict[str, str | list[str]]:
13
+ """解析动态参数,支持重复参数转为数组
14
+
15
+ Args:
16
+ args: 原始参数列表
17
+ known_options: 已知选项列表,这些选项不会被解析为动态参数
18
+
19
+ Returns:
20
+ 动态参数字典,重复的 key 会转为 list
21
+ """
22
+ params: dict[str, str | list[str]] = {}
23
+ known_set = set(known_options or [])
24
+
25
+ i = 0
26
+ while i < len(args):
27
+ if args[i].startswith("--"):
28
+ key = args[i][2:]
29
+
30
+ # 跳过已知选项及其值
31
+ if key in known_set:
32
+ i += 2
33
+ continue
34
+
35
+ # 处理动态参数
36
+ if i + 1 < len(args) and not args[i + 1].startswith("--"):
37
+ _append_param(params, key, args[i + 1])
38
+ i += 2
39
+ else:
40
+ _append_param(params, key, "true")
41
+ i += 1
42
+ else:
43
+ i += 1
44
+
45
+ return params
46
+
47
+
48
+ def _append_param(params: dict[str, str | list[str]], key: str, value: str) -> None:
49
+ """添加参数到字典,如果 key 已存在则转为数组"""
50
+ if key in params:
51
+ existing = params[key]
52
+ if isinstance(existing, list):
53
+ existing.append(value)
54
+ else:
55
+ params[key] = [existing, value]
56
+ else:
57
+ params[key] = value
58
+
59
+
60
+ def query(
61
+ ctx: typer.Context,
62
+ route_slug: str = typer.Argument(help="路由名称"),
63
+ format: str = typer.Option("json", "--format", help="输出格式: json | csv"),
64
+ output: Optional[str] = typer.Option(None, "--output", help="输出到文件"),
65
+ page_size: Optional[int] = typer.Option(None, "--pageSize", help="每页条数"),
66
+ cursor: Optional[str] = typer.Option(None, "--cursor", help="游标翻页"),
67
+ fetch_all: bool = typer.Option(False, "--all", help="自动翻页查询全部数据(流式写入)"),
68
+ ) -> None:
69
+ """查询数据"""
70
+ cfg = require_config()
71
+ ps = page_size or cfg.get("pageSize", 50)
72
+
73
+ # 从原始命令行参数解析动态参数
74
+ known_options = ["format", "output", "pageSize", "cursor", "all"]
75
+ dynamic = _parse_extra_args(ctx.args, known_options)
76
+ params: dict[str, str | list[str] | int] = {**dynamic, "pageSize": ps}
77
+
78
+ # 如果指定了 --all,忽略手动传入的 cursor
79
+ if not fetch_all and cursor:
80
+ params["cursor"] = cursor
81
+
82
+ try:
83
+ if fetch_all:
84
+ # 自动翻页查询全部数据,使用流式写入
85
+ # 确保 output 是字符串或 None,防止 Typer 的 OptionInfo 类型泄漏
86
+ output_path: str | None = output if isinstance(output, (str, type(None))) else None
87
+ writer: StreamWriter = create_stream_writer(format, output_path, cfg)
88
+ writer.initialize()
89
+
90
+ query_cursor: str | None = None
91
+ count = 0
92
+
93
+ try:
94
+ # 使用连接池复用 HTTP 连接
95
+ with ApiClient(cfg) as client:
96
+ while True:
97
+ query_params = {**params}
98
+ if query_cursor:
99
+ query_params["cursor"] = query_cursor
100
+
101
+ res = client.query(route_slug, query_params)
102
+ if not res.get("success"):
103
+ print(res.get("error", "查询失败"), file=sys.stderr)
104
+ return
105
+
106
+ data = res.get("data", [])
107
+ if isinstance(data, list):
108
+ for record in data:
109
+ writer.write_record(record)
110
+ count += 1
111
+
112
+ pagination = res.get("meta", {}).get("pagination", {})
113
+ if not pagination.get("hasMore"):
114
+ break
115
+
116
+ query_cursor = pagination.get("nextCursor")
117
+
118
+ finally:
119
+ # 确保 Ctrl+C 时也能正确闭合文件
120
+ writer.finalize()
121
+
122
+ # 输出警告(如果有)
123
+ warning = writer.get_warning() if hasattr(writer, "get_warning") else None
124
+ if warning:
125
+ print(warning, file=sys.stderr)
126
+ else:
127
+ # 单页查询
128
+ with ApiClient(cfg) as client:
129
+ res = client.query(route_slug, params)
130
+
131
+ if not res.get("success"):
132
+ print(res.get("error", "查询失败"), file=sys.stderr)
133
+ return
134
+
135
+ data = res.get("data", [])
136
+
137
+ if format == "csv":
138
+ content = format_csv(data)
139
+ else:
140
+ content = format_json(data, pretty=False)
141
+
142
+ if output:
143
+ from pathlib import Path
144
+ expanded = Path(output).expanduser()
145
+ with open(expanded, "w", encoding="utf-8") as f:
146
+ f.write(content)
147
+ print(f"数据已保存 → {output}", file=sys.stderr)
148
+ else:
149
+ print(content)
150
+
151
+ # 提示翻页
152
+ pagination = res.get("meta", {}).get("pagination", {})
153
+ if pagination.get("hasMore"):
154
+ nc = pagination.get("nextCursor", "")
155
+ print(f"还有更多数据,使用 --cursor {nc} 翻页", file=sys.stderr)
156
+
157
+ except Exception as e:
158
+ print(str(e), file=sys.stderr)
@@ -0,0 +1,67 @@
1
+ from typing import Optional
2
+
3
+ import typer
4
+ from rich.console import Console
5
+
6
+ from cyberquant.lib.config import require_config
7
+ from cyberquant.lib.api_client import ApiClient
8
+ from cyberquant.lib.formatter import format_json, format_csv, write_output
9
+
10
+ console = Console()
11
+
12
+
13
+ def _parse_extra_args(args: list[str]) -> dict[str, str]:
14
+ params: dict[str, str] = {}
15
+ i = 0
16
+ while i < len(args):
17
+ if args[i].startswith("--"):
18
+ key = args[i][2:]
19
+ if i + 1 < len(args) and not args[i + 1].startswith("--"):
20
+ params[key] = args[i + 1]
21
+ i += 2
22
+ else:
23
+ params[key] = "true"
24
+ i += 1
25
+ else:
26
+ i += 1
27
+ return params
28
+
29
+
30
+ def query(
31
+ ctx: typer.Context,
32
+ route_slug: str = typer.Argument(help="路由名称"),
33
+ format: str = typer.Option("json", "--format", help="输出格式: json | csv"),
34
+ output: Optional[str] = typer.Option(None, "--output", help="输出到文件"),
35
+ page_size: Optional[int] = typer.Option(None, "--pageSize", help="每页条数"),
36
+ cursor: Optional[str] = typer.Option(None, "--cursor", help="游标翻页"),
37
+ ) -> None:
38
+ """查询数据"""
39
+ cfg = require_config()
40
+ client = ApiClient(cfg)
41
+
42
+ ps = page_size or cfg.get("pageSize", 50)
43
+ dynamic = _parse_extra_args(ctx.args)
44
+ params: dict = {**dynamic, "pageSize": ps}
45
+ if cursor:
46
+ params["cursor"] = cursor
47
+
48
+ with console.status("查询中..."):
49
+ res = client.query(route_slug, params)
50
+
51
+ if not res.get("success"):
52
+ console.print(f"[red]{res.get('error', '查询失败')}[/red]")
53
+ return
54
+
55
+ data = res.get("data", [])
56
+ console.print(f"[green]查询成功,共 {len(data)} 条[/green]")
57
+
58
+ if format == "csv":
59
+ content = format_csv(data)
60
+ else:
61
+ content = format_json(data)
62
+ write_output(content, output)
63
+
64
+ pagination = res.get("meta", {}).get("pagination", {})
65
+ if pagination.get("hasMore"):
66
+ nc = pagination.get("nextCursor", "")
67
+ console.print(f"[dim]还有更多数据,使用 --cursor {nc} 翻页[/dim]")
@@ -0,0 +1,60 @@
1
+ import sys
2
+ from typing import Optional
3
+
4
+ import typer
5
+
6
+ from cyberquant.lib.config import require_config
7
+ from cyberquant.lib.sse_client import connect_sse
8
+ from cyberquant.lib.formatter import create_stream_writer
9
+ from cyberquant.commands.query import _parse_extra_args
10
+
11
+
12
+ def stream_cmd(
13
+ ctx: typer.Context,
14
+ route_slug: str = typer.Argument(help="路由名称"),
15
+ page_size: Optional[int] = typer.Option(None, "--pageSize", help="每批条数"),
16
+ format: str = typer.Option("json", "--format", help="输出格式: json | csv"),
17
+ output: Optional[str] = typer.Option(None, "--output", help="输出到文件"),
18
+ ) -> None:
19
+ """实时流式数据"""
20
+ cfg = require_config()
21
+ ps = page_size or cfg.get("pageSize", 50)
22
+
23
+ known_options = ["pageSize", "format", "output"]
24
+ dynamic = _parse_extra_args(ctx.args, known_options)
25
+ params: dict[str, str | list[str] | int] = {**dynamic, "pageSize": ps}
26
+
27
+ # 文件输出或 JSON 模式:使用流式写入
28
+ from cyberquant.lib.stream_writer import StreamWriter
29
+
30
+ # 确保 output 是字符串或 None,防止 Typer 的 OptionInfo 类型泄漏
31
+ output_path: str | None = output if isinstance(output, (str, type(None))) else None
32
+ writer: StreamWriter = create_stream_writer(format, output_path, cfg)
33
+ writer.initialize()
34
+
35
+ count = 0
36
+
37
+ def on_data(data: dict) -> None:
38
+ nonlocal count
39
+ writer.write_record(data)
40
+ count += 1
41
+
42
+ def on_complete(info=None) -> None:
43
+ writer.finalize()
44
+
45
+ # 输出警告(如果有)
46
+ warning = writer.get_warning() if hasattr(writer, "get_warning") else None
47
+ if warning:
48
+ print(warning, file=sys.stderr)
49
+
50
+ if output_path:
51
+ print(f"数据已保存 → {output_path}", file=sys.stderr)
52
+
53
+ def on_error(error: str) -> None:
54
+ print(f"流式错误: {error}", file=sys.stderr)
55
+
56
+ try:
57
+ connect_sse(cfg, route_slug, params, on_data, on_complete, on_error)
58
+ except KeyboardInterrupt:
59
+ writer.finalize()
60
+ print("已断开连接", file=sys.stderr)