cyberquant 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.
cyberquant/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "1.0.0"
cyberquant/__main__.py ADDED
@@ -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)
@@ -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,11 @@
1
+ cyberquant/__init__.py,sha256=J-j-u0itpEFT6irdmWmixQqYMadNl1X91TxUmoiLHMI,22
2
+ cyberquant/__main__.py,sha256=W5AeNBJrE2JJQdRawz06K9kue7IBO_8hZpgCfl8Bsrs,1681
3
+ cyberquant/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ cyberquant/commands/config.py,sha256=RFJbSd2Ej3B_dnPkq_7-JgQ0hQPYv-Y046ZB-qyK9d0,2235
5
+ cyberquant/commands/query.py,sha256=k95fAv9c-f00b5PiK8K5Lj8Z6zdzdoDXfTanq19RYOk,5702
6
+ cyberquant/commands/query.py.bak2,sha256=nuQSgIw7_DKoH3hcqFfRPkAf4n1JycwsWNt0rFkrXl4,2112
7
+ cyberquant/commands/stream.py,sha256=l-TXXUvRhdKZ0V7U1KcyWM-6ZEwS9-1Tc3Nq_ahJGyc,2076
8
+ cyberquant-1.0.0.dist-info/METADATA,sha256=C4qxbrTfRPMSUbviftMppH8CNkNbgYNoxmnll27RCbE,216
9
+ cyberquant-1.0.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
10
+ cyberquant-1.0.0.dist-info/entry_points.txt,sha256=x5GEj4HRuMplRFgVXMCx6cID8tYO7jfrpRg3d31zsB8,55
11
+ cyberquant-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ cyberquant = cyberquant.__main__:cli