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 +1 -0
- cyberquant/__main__.py +35 -0
- cyberquant/commands/__init__.py +0 -0
- cyberquant/commands/config.py +62 -0
- cyberquant/commands/query.py +158 -0
- cyberquant/commands/query.py.bak2 +67 -0
- cyberquant/commands/stream.py +60 -0
- cyberquant-1.0.0.dist-info/METADATA +8 -0
- cyberquant-1.0.0.dist-info/RECORD +11 -0
- cyberquant-1.0.0.dist-info/WHEEL +4 -0
- cyberquant-1.0.0.dist-info/entry_points.txt +2 -0
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,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,,
|