pandaai-cli 0.1.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.
cli.py ADDED
@@ -0,0 +1,442 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ PandaAI 因子分析 CLI
4
+
5
+ 用法:
6
+ python cli.py login [--phone PHONE] [--password PWD]
7
+ python cli.py factor_create --formula "..." [--name NAME]
8
+ python cli.py factor_info <factor_id>
9
+ python cli.py factor_update <factor_id> [--name NAME] [--adjustment-cycle N]
10
+ python cli.py factor_run <factor_id> [--download]
11
+ python cli.py factor_list [--limit N]
12
+ python cli.py balance
13
+ python cli.py factor_result <run_id>
14
+ python cli.py factor_delete <factor_id> [factor_id...] [--pattern PATTERN]
15
+
16
+ 通用参数: --config PATH, --json
17
+ """
18
+
19
+ import argparse
20
+ import getpass
21
+ import json
22
+ import sys
23
+ import os
24
+ import traceback
25
+ from typing import Optional, Dict, Any, Tuple
26
+
27
+ import requests
28
+ import yaml
29
+
30
+ from template import build_factor_json
31
+ from pandaai import (
32
+ STATUS_SUCCESS,
33
+ load_config, save_token, config_path,
34
+ do_login, login,
35
+ get_balance,
36
+ create_factor, run_factor, poll_factor_run, get_factor_output,
37
+ get_factor, update_factor, extract_factor_info, apply_factor_updates,
38
+ get_factor_run_results, format_factor_analysis_results,
39
+ list_factors, delete_factors, format_factor_list, enrich_factors_with_runs,
40
+ format_success, format_failure,
41
+ download_csv,
42
+ )
43
+
44
+
45
+ def _fail(error_type: str, message: str) -> None:
46
+ """输出错误 JSON 并退出"""
47
+ print(json.dumps(format_failure(status="ERROR", error_type=error_type, message=message), ensure_ascii=False))
48
+ sys.exit(1)
49
+
50
+
51
+ def main():
52
+ parser = argparse.ArgumentParser(description="PandaAI 因子分析 CLI")
53
+ parser.add_argument("--config", type=str, default=None, help="配置文件路径")
54
+ parser.add_argument("--json", action="store_true", help="仅输出 JSON")
55
+
56
+ sub = parser.add_subparsers(dest="command", help="可用命令")
57
+
58
+ # ── login ──
59
+ p = sub.add_parser("login", help="登录并保存 token")
60
+ p.add_argument("--phone", type=str, default=None, help="手机号")
61
+ p.add_argument("--password", type=str, default=None, help="密码")
62
+
63
+ # ── factor_create ──
64
+ p = sub.add_parser("factor_create", help="创建因子分析(不执行),返回 factor_id")
65
+ cg = p.add_mutually_exclusive_group(required=True)
66
+ cg.add_argument("--code", type=str, help="Python 因子代码")
67
+ cg.add_argument("--formula", type=str, help="因子公式")
68
+ cg.add_argument("--file", type=str, help="从文件读取代码/公式")
69
+ p.add_argument("--name", type=str, default="新建因子分析", help="因子分析名称")
70
+ p.add_argument("--start-date", type=str, default=None, help="因子构建开始日期 YYYYMMDD")
71
+ p.add_argument("--end-date", type=str, default=None, help="因子构建结束日期 YYYYMMDD")
72
+ p.add_argument("--adjustment-cycle", type=str, default="1", help="调仓周期 1-10(默认 1)")
73
+ p.add_argument("--factor-direction", type=str, default="1", help="因子方向 0=负向 1=正向(默认 1)")
74
+
75
+ # ── factor_info ──
76
+ p = sub.add_parser("factor_info", help="查看因子详情(代码/公式 + 所有参数)")
77
+ p.add_argument("factor_id", type=str, help="因子分析 ID")
78
+
79
+ # ── factor_update ──
80
+ p = sub.add_parser("factor_update", help="修改因子参数")
81
+ p.add_argument("factor_id", type=str, help="因子分析 ID")
82
+ p.add_argument("--name", type=str, default=None, help="新名称")
83
+ ug = p.add_mutually_exclusive_group()
84
+ ug.add_argument("--code", type=str, default=None, help="新 Python 因子代码")
85
+ ug.add_argument("--formula", type=str, default=None, help="新因子公式")
86
+ ug.add_argument("--file", type=str, default=None, help="从文件读取新代码/公式")
87
+ p.add_argument("--start-date", type=str, default=None, help="因子构建开始日期 YYYYMMDD")
88
+ p.add_argument("--end-date", type=str, default=None, help="因子构建结束日期 YYYYMMDD")
89
+ p.add_argument("--adjustment-cycle", type=str, default=None, help="调仓周期 1-10")
90
+ p.add_argument("--factor-direction", type=str, default=None, help="因子方向 0=负向 1=正向")
91
+
92
+ # ── factor_run ──
93
+ p = sub.add_parser("factor_run", help="执行已有因子分析(启动 + 轮询 + 结果)")
94
+ p.add_argument("factor_id", type=str, help="因子分析 ID")
95
+ p.add_argument("--download", type=str, nargs="?", const="", default=None, help="下载 CSV(默认 ~/Downloads)")
96
+ p.add_argument("--poll-interval", type=int, default=2, help="轮询间隔(秒)")
97
+ p.add_argument("--timeout", type=int, default=600, help="超时时间(秒)")
98
+
99
+ # ── factor_list ──
100
+ p = sub.add_parser("factor_list", help="列出所有因子分析")
101
+ p.add_argument("--limit", type=int, default=100, help="每页条数(最大100)")
102
+ p.add_argument("--offset", type=int, default=0, help="偏移量")
103
+ p.add_argument("--no-detail", action="store_true", help="跳过获取分析摘要(更快)")
104
+
105
+ # ── balance ──
106
+ sub.add_parser("balance", help="查询算力余额")
107
+
108
+ # ── factor_result ──
109
+ p = sub.add_parser("factor_result", help="查询运行结果(14 个分析接口)")
110
+ p.add_argument("run_id", type=str, help="运行 ID")
111
+ p.add_argument("--download", type=str, nargs="?", const="", default=None, help="下载 CSV")
112
+
113
+ # ── factor_delete ──
114
+ p = sub.add_parser("factor_delete", help="删除因子分析")
115
+ p.add_argument("factor_id", type=str, nargs="*", help="因子分析 ID(可多个)")
116
+ p.add_argument("--pattern", type=str, default=None, help="按名称前缀批量删除")
117
+ p.add_argument("--yes", action="store_true", help="跳过确认")
118
+
119
+ args = parser.parse_args()
120
+
121
+ if not args.command:
122
+ parser.print_help()
123
+ sys.exit(1)
124
+
125
+ # ═══════════════════════════════════════════
126
+ # 加载配置
127
+ # ═══════════════════════════════════════════
128
+ config = load_config(args.config)
129
+
130
+ # ═══════════════════════════════════════════
131
+ # login
132
+ # ═══════════════════════════════════════════
133
+ if args.command == "login":
134
+ phone = args.phone or input("手机号: ").strip()
135
+ password = args.password
136
+ if not password:
137
+ password = getpass.getpass("密码: ").strip()
138
+ if not phone or not password:
139
+ _fail("LOGIN_FAILED", "手机号和密码不能为空")
140
+ token, uid, _ = do_login(config, phone, password, config.get("country_code", "86"))
141
+ save_token(token, uid, args.config)
142
+ print(f"✅ 登录成功! userId={uid}")
143
+ return
144
+
145
+ # ═══════════════════════════════════════════
146
+ # 认证
147
+ # ═══════════════════════════════════════════
148
+ token, uid, _ = login(config)
149
+ if not args.json:
150
+ pass # token 有效,静默继续
151
+
152
+ # ═══════════════════════════════════════════
153
+ # create
154
+ # ═══════════════════════════════════════════
155
+ if args.command == "factor_create":
156
+ if args.file:
157
+ try:
158
+ with open(args.file, "r") as f:
159
+ content = f.read()
160
+ except Exception as e:
161
+ _fail("INPUT_ERROR", f"读取文件失败: {e}")
162
+ elif args.code:
163
+ content = args.code
164
+ else:
165
+ content = args.formula
166
+ mode = "formula" if args.formula else "code"
167
+ if not content or not content.strip():
168
+ _fail("INPUT_ERROR", "代码/公式内容为空")
169
+
170
+ # 校验调仓周期
171
+ adj_cycle = args.adjustment_cycle
172
+ try:
173
+ adj_int = int(adj_cycle)
174
+ if adj_int < 1 or adj_int > 10:
175
+ _fail("INPUT_ERROR", "调仓周期必须在 1-10 之间")
176
+ except ValueError:
177
+ _fail("INPUT_ERROR", f"调仓周期必须为整数: {adj_cycle}")
178
+
179
+ factor_json = build_factor_json(content, mode, name=args.name,
180
+ start_date=args.start_date, end_date=args.end_date,
181
+ adjustment_cycle=args.adjustment_cycle,
182
+ factor_direction=args.factor_direction)
183
+ factor_id = create_factor(config, token, uid, factor_json)
184
+ if args.json:
185
+ print(json.dumps({"success": True, "factor_id": factor_id}, ensure_ascii=False))
186
+ else:
187
+ print(f"✅ 因子分析已创建: {factor_id}")
188
+ return
189
+
190
+ # ═══════════════════════════════════════════
191
+ # info
192
+ # ═══════════════════════════════════════════
193
+ if args.command == "factor_info":
194
+ factor_data = get_factor(config, token, uid, args.factor_id)
195
+ if factor_data is None:
196
+ _fail("INFO_FAILED", f"获取因子信息失败: {args.factor_id}")
197
+
198
+ info = extract_factor_info(factor_data)
199
+
200
+ if args.json:
201
+ print(json.dumps({"success": True, **info}, ensure_ascii=False, indent=2))
202
+ else:
203
+ from datetime import datetime as dt
204
+ mode_label = "Python 代码" if info["mode"] == "code" else "因子公式"
205
+ created = ""
206
+ if info.get("create_at"):
207
+ try:
208
+ created = dt.fromtimestamp(info["create_at"] / 1000).strftime("%Y-%m-%d %H:%M:%S")
209
+ except Exception:
210
+ created = str(info["create_at"])
211
+
212
+ print(f"名称: {info['name']}")
213
+ print(f"ID: {info['_id']}")
214
+ print(f"类型: {mode_label}")
215
+ print(f"创建时间: {created}")
216
+ print(f"调仓周期: {info['adjustment_cycle']}")
217
+ print(f"分组数量: {info['group_number']}")
218
+ print(f"因子方向: {info['factor_direction']} ({'正向' if info['factor_direction'] == '1' else '负向'})")
219
+ print(f"股票池: {info['stock_pool']}")
220
+ print(f"开始日期: {info['start_date']}")
221
+ print(f"结束日期: {info['end_date']}")
222
+ if info["last_run_id"]:
223
+ print(f"最后运行: {info['last_run_id']}")
224
+ print(f"\n{'─' * 50}")
225
+ print(f"【{mode_label}】")
226
+ print(info["content"])
227
+ return
228
+
229
+ # ═══════════════════════════════════════════
230
+ # update
231
+ # ═══════════════════════════════════════════
232
+ if args.command == "factor_update":
233
+ factor_data = get_factor(config, token, uid, args.factor_id)
234
+ if factor_data is None:
235
+ _fail("UPDATE_FAILED", f"获取因子信息失败: {args.factor_id}")
236
+
237
+ updates = {}
238
+
239
+ if args.name is not None:
240
+ updates["name"] = args.name
241
+
242
+ if args.file:
243
+ try:
244
+ with open(args.file, "r") as f:
245
+ updates["content"] = f.read()
246
+ except Exception as e:
247
+ _fail("INPUT_ERROR", f"读取文件失败: {e}")
248
+ updates["mode"] = "code"
249
+ elif args.code is not None:
250
+ updates["content"] = args.code
251
+ updates["mode"] = "code"
252
+ elif args.formula is not None:
253
+ updates["content"] = args.formula
254
+ updates["mode"] = "formula"
255
+
256
+ if args.start_date is not None:
257
+ updates["start_date"] = args.start_date
258
+ if args.end_date is not None:
259
+ updates["end_date"] = args.end_date
260
+
261
+ if args.adjustment_cycle is not None:
262
+ try:
263
+ adj_int = int(args.adjustment_cycle)
264
+ if adj_int < 1 or adj_int > 10:
265
+ _fail("INPUT_ERROR", "调仓周期必须在 1-10 之间")
266
+ except ValueError:
267
+ _fail("INPUT_ERROR", f"调仓周期必须为整数: {args.adjustment_cycle}")
268
+ updates["adjustment_cycle"] = args.adjustment_cycle
269
+
270
+ if args.factor_direction is not None:
271
+ if args.factor_direction not in ("0", "1"):
272
+ _fail("INPUT_ERROR", "因子方向必须为 0(负向) 或 1(正向)")
273
+ updates["factor_direction"] = args.factor_direction
274
+
275
+ if not updates:
276
+ print("没有需要更新的参数")
277
+ return
278
+
279
+ factor_json = apply_factor_updates(factor_data, updates)
280
+ factor_id = update_factor(config, token, uid, args.factor_id, factor_json)
281
+ if args.json:
282
+ print(json.dumps({"success": True, "factor_id": factor_id, "updated": list(updates.keys())}, ensure_ascii=False))
283
+ else:
284
+ print(f"✅ 因子分析已更新: {factor_id}")
285
+ print(f" 更新字段: {', '.join(updates.keys())}")
286
+ return
287
+
288
+ # ═══════════════════════════════════════════
289
+ # run
290
+ # ═══════════════════════════════════════════
291
+ if args.command == "factor_run":
292
+ factor_id = args.factor_id
293
+
294
+ balance_start = get_balance(config, token)
295
+ if not args.json:
296
+ power_start = balance_start.get("computingPower") if isinstance(balance_start, dict) else "?"
297
+ print(f"[INFO] 当前算力: {power_start}", file=sys.stderr)
298
+
299
+ run_id, run_error = run_factor(config, token, uid, factor_id)
300
+ if run_id is None:
301
+ balance_end = get_balance(config, token) or balance_start
302
+ power_end = balance_end.get("computingPower") if isinstance(balance_end, dict) else None
303
+ error_msg = run_error or "启动因子分析失败"
304
+ if not args.json:
305
+ print(f"[ERROR] {error_msg}", file=sys.stderr)
306
+ result = {
307
+ "success": False, "status": "RUN_FAILED",
308
+ "factor_id": factor_id, "factor_run_id": None,
309
+ "error": {"type": "RUN_FAILED", "message": error_msg},
310
+ "billing": {"balance": power_end, "status": "ok"}
311
+ }
312
+ print(json.dumps(result, ensure_ascii=False, indent=2))
313
+ sys.exit(1)
314
+
315
+ if not args.json:
316
+ print(f"[INFO] 因子分析已启动: {run_id}", file=sys.stderr)
317
+ print(f"[INFO] 等待执行完成...", file=sys.stderr)
318
+
319
+ run_result = poll_factor_run(config, token, uid, run_id, args.poll_interval, args.timeout)
320
+
321
+ output = None
322
+ full_results = None
323
+ if run_result["status"] == STATUS_SUCCESS:
324
+ output = get_factor_output(config, token, uid, factor_id)
325
+ full_results = get_factor_run_results(config, token, uid, run_id)
326
+
327
+ balance_end = get_balance(config, token) or balance_start
328
+
329
+ s = run_result["status"]
330
+ if s == STATUS_SUCCESS:
331
+ result = format_success(factor_id, run_id, run_result, balance_start, balance_end, output)
332
+ result["results"] = full_results
333
+ elif s == 3: # STATUS_FAILED
334
+ errors = run_result.get("errors", [])
335
+ msg = "; ".join(e.get("error_message", e.get("message", "")) for e in errors) if errors else "因子分析执行失败"
336
+ result = format_failure("FAILED", "WORKFLOW_FAILED", msg, factor_id, run_id, run_result, balance_start, balance_end)
337
+ elif s == 6: # STATUS_BILLING_STOP
338
+ result = format_failure("BILLING_STOP", "INSUFFICIENT_BALANCE", "余额不足", factor_id, run_id, run_result, balance_start, balance_end)
339
+ elif s == "TIMEOUT":
340
+ result = format_failure("TIMEOUT", "TIMEOUT", f"轮询超时 ({args.timeout}s)", factor_id, run_id, run_result, balance_start, balance_end)
341
+ else:
342
+ result = format_failure("UNKNOWN", "UNKNOWN", f"未知状态: {s}", factor_id, run_id, run_result, balance_start, balance_end)
343
+
344
+ print(json.dumps(result, ensure_ascii=False, indent=2))
345
+ if s == STATUS_SUCCESS and full_results and not args.json:
346
+ print("\n" + format_factor_analysis_results(full_results), file=sys.stderr)
347
+ download_csv(full_results, args.download)
348
+ return
349
+
350
+ # ═══════════════════════════════════════════
351
+ # list
352
+ # ═══════════════════════════════════════════
353
+ if args.command == "factor_list":
354
+ factors, total = list_factors(config, token, uid, limit=args.limit, offset=args.offset)
355
+ if factors is None:
356
+ _fail("LIST_FAILED", "获取因子列表失败")
357
+
358
+ if not args.no_detail and factors:
359
+ if not args.json:
360
+ print(f"[INFO] 正在获取分析摘要...", file=sys.stderr)
361
+ factors = enrich_factors_with_runs(config, token, uid, factors)
362
+
363
+ if args.json:
364
+ print(json.dumps({"success": True, "total": total, "count": len(factors),
365
+ "limit": args.limit, "offset": args.offset, "factors": factors},
366
+ ensure_ascii=False, indent=2))
367
+ else:
368
+ print(format_factor_list(factors, total_count=total, limit=args.limit, offset=args.offset))
369
+ return
370
+
371
+ # ═══════════════════════════════════════════
372
+ # balance
373
+ # ═══════════════════════════════════════════
374
+ if args.command == "balance":
375
+ balance = get_balance(config, token)
376
+ if balance is None:
377
+ _fail("BALANCE_FAILED", "查询算力余额失败")
378
+ if args.json:
379
+ print(json.dumps({"success": True, "balance": balance}, ensure_ascii=False))
380
+ else:
381
+ power = balance.get("computingPower", "?")
382
+ print(f"算力: {power}")
383
+ return
384
+
385
+ # ═══════════════════════════════════════════
386
+ # factor_result
387
+ # ═══════════════════════════════════════════
388
+ if args.command == "factor_result":
389
+ results = get_factor_run_results(config, token, uid, args.run_id)
390
+ if args.json:
391
+ print(json.dumps({"success": "error" not in results, **results}, ensure_ascii=False, indent=2))
392
+ else:
393
+ print(format_factor_analysis_results(results))
394
+ download_csv(results, args.download)
395
+ return
396
+
397
+ # ═══════════════════════════════════════════
398
+ # delete
399
+ # ═══════════════════════════════════════════
400
+ if args.command == "factor_delete":
401
+ if args.pattern:
402
+ ids = []
403
+ offset = 0
404
+ while True:
405
+ factors, _ = list_factors(config, token, uid, limit=100, offset=offset)
406
+ if not factors:
407
+ break
408
+ for f in factors:
409
+ if f.get("name", "").startswith(args.pattern):
410
+ ids.append(f["_id"])
411
+ offset += 100
412
+ if not ids:
413
+ print(f"没有匹配 '{args.pattern}' 的因子分析")
414
+ return
415
+ elif args.factor_id:
416
+ ids = args.factor_id
417
+ else:
418
+ print("用法: factor_delete <factor_id> [factor_id...] 或 factor_delete --pattern <前缀>")
419
+ sys.exit(1)
420
+
421
+ if not args.yes and not args.json:
422
+ print(f"将删除 {len(ids)} 个因子分析: {', '.join(ids[:5])}{'...' if len(ids) > 5 else ''}")
423
+ confirm = input("确认? (y/N): ").strip().lower()
424
+ if confirm not in ("y", "yes"):
425
+ print("已取消")
426
+ return
427
+
428
+ deleted, error = delete_factors(config, token, uid, ids)
429
+ if error:
430
+ _fail("DELETE_FAILED", f"删除失败 (已删{deleted}): {error}")
431
+ print(f"✅ 已删除 {deleted} 个因子分析")
432
+ return
433
+
434
+
435
+ if __name__ == "__main__":
436
+ try:
437
+ main()
438
+ except SystemExit:
439
+ pass
440
+ except Exception:
441
+ print(json.dumps(format_failure("ERROR", "UNKNOWN", f"CLI内部错误: {traceback.format_exc()}"), ensure_ascii=False))
442
+ sys.exit(1)
pandaai/__init__.py ADDED
@@ -0,0 +1,12 @@
1
+ """PandaAI CLI 功能模块"""
2
+
3
+ from .constants import *
4
+ from .config import load_config, save_token, config_path
5
+ from .auth import do_login, login, api_url, make_headers
6
+ from .billing import get_balance
7
+ from .factor_core import create_factor, run_factor, poll_factor_run, get_factor_output, get_factor, update_factor, extract_factor_info, apply_factor_updates
8
+ from .output import get_factor_run_detail, get_node_output, extract_task_id, fetch_factor_analysis, get_factor_run_results
9
+ from .factor import FACTOR_ANALYSIS_ENDPOINTS, format_factor_analysis_results
10
+ from .factor_list import list_factors, delete_factors, format_factor_list, enrich_factors_with_runs
11
+ from .formatting import format_success, format_failure, _calc_duration
12
+ from .download import download_csv
pandaai/auth.py ADDED
@@ -0,0 +1,103 @@
1
+ """认证:登录、token 验证"""
2
+
3
+ import hashlib
4
+ import sys
5
+ import json
6
+ from typing import Optional, Tuple
7
+
8
+ import requests
9
+
10
+ from .config import save_token
11
+
12
+
13
+ def _api_url(config: dict, path: str) -> str:
14
+ """拼接完整 API 地址"""
15
+ base = config["gateway_url"].rstrip("/")
16
+ return f"{base}{path}"
17
+
18
+
19
+ def _headers(token: str = None, uid: str = None, extra: dict = None) -> dict:
20
+ """构造请求头"""
21
+ h = {"Content-Type": "application/json"}
22
+ if token:
23
+ h["Authorization"] = token
24
+ if uid:
25
+ h["uid"] = str(uid)
26
+ if extra:
27
+ h.update(extra)
28
+ return h
29
+
30
+
31
+ def _fail(error_type: str, message: str) -> None:
32
+ """输出错误 JSON 并退出(导入自 formatting)"""
33
+ from .formatting import format_failure
34
+ print(json.dumps(format_failure(status="ERROR", error_type=error_type, message=message), ensure_ascii=False))
35
+ sys.exit(1)
36
+
37
+
38
+ def do_login(config: dict, phone: str, password: str, country_code: str = "86") -> Tuple[str, str, dict]:
39
+ """
40
+ 执行密码登录,返回 (token, uid, userinfo)
41
+ """
42
+ url = _api_url(config, "/login/pw")
43
+ body = {
44
+ "phone": phone,
45
+ "password": hashlib.md5(password.encode()).hexdigest(),
46
+ "countryCode": country_code,
47
+ }
48
+ try:
49
+ resp = requests.post(url, json=body, timeout=10)
50
+ data = resp.json()
51
+ except Exception as e:
52
+ return _fail("LOGIN_FAILED", f"登录请求失败: {e}")
53
+
54
+ if data.get("code") != "200":
55
+ return _fail("LOGIN_FAILED", f"登录失败: {data.get('message', '未知错误')}")
56
+
57
+ token = data.get("data")
58
+ if not token:
59
+ return _fail("LOGIN_FAILED", "登录响应中没有 token")
60
+
61
+ user_id, userinfo = _get_user_info(config, token)
62
+ if user_id is None or userinfo is None:
63
+ return _fail("LOGIN_FAILED", "获取用户信息失败")
64
+
65
+ return token, user_id, userinfo
66
+
67
+
68
+ def login(config: dict) -> Tuple[str, str, dict]:
69
+ """
70
+ 认证入口:优先用已保存的 token,失败则提示用户登录
71
+ 返回 (token, uid, userinfo)
72
+ """
73
+ saved_token = config.get("token", "")
74
+ saved_uid = config.get("uid", "")
75
+
76
+ if saved_token and saved_uid:
77
+ uid, userinfo = _get_user_info(config, saved_token)
78
+ if uid is not None and userinfo is not None:
79
+ return saved_token, uid, userinfo
80
+
81
+ return _fail("LOGIN_REQUIRED",
82
+ "未登录或 token 已过期,请先执行: python cli.py login --phone <手机号> --password <密码>")
83
+
84
+
85
+ def _get_user_info(config: dict, token: str) -> Tuple[Optional[str], Optional[dict]]:
86
+ """调 /user/info 获取 userId 和用户信息"""
87
+ url = _api_url(config, "/user/info")
88
+ try:
89
+ resp = requests.get(url, headers=_headers(token=token), timeout=10)
90
+ data = resp.json()
91
+ if data.get("code") == "200":
92
+ user_data = data.get("data", {})
93
+ if isinstance(user_data, dict):
94
+ uid = user_data.get("id")
95
+ return (str(uid) if uid else None), user_data
96
+ except Exception:
97
+ pass
98
+ return None, None
99
+
100
+
101
+ # 导出工具函数供其他模块使用
102
+ api_url = _api_url
103
+ make_headers = _headers
pandaai/billing.py ADDED
@@ -0,0 +1,20 @@
1
+ """算力余额查询"""
2
+
3
+ from typing import Optional
4
+
5
+ import requests
6
+
7
+ from .auth import api_url, make_headers
8
+
9
+
10
+ def get_balance(config: dict, token: str) -> Optional[dict]:
11
+ """查询用户算力余额"""
12
+ url = api_url(config, "/userWallet/myWallet")
13
+ try:
14
+ resp = requests.get(url, headers=make_headers(token=token), timeout=10)
15
+ data = resp.json()
16
+ if data.get("code") == "200":
17
+ return data.get("data", {})
18
+ except Exception:
19
+ pass
20
+ return None
pandaai/config.py ADDED
@@ -0,0 +1,40 @@
1
+ """配置加载与 token 持久化"""
2
+
3
+ import os
4
+ import sys
5
+ import json
6
+ import yaml
7
+
8
+
9
+ def config_path(custom_path: str = None) -> str:
10
+ """获取配置文件路径。默认 ~/.pandaai/config.yaml,可通过 --config 覆盖"""
11
+ if custom_path is None:
12
+ return os.path.join(os.path.expanduser("~"), ".pandaai", "config.yaml")
13
+ return custom_path
14
+
15
+
16
+ def load_config(config_path_arg: str = None) -> dict:
17
+ """加载配置文件"""
18
+ path = config_path(config_path_arg)
19
+ if not os.path.exists(path):
20
+ print(json.dumps({"success": False, "error": {"type": "CONFIG_ERROR", "message": f"配置文件不存在: {path}"}}))
21
+ sys.exit(1)
22
+ with open(path, "r") as f:
23
+ return yaml.safe_load(f)
24
+
25
+
26
+ def save_token(token: str, uid: str, config_path_arg: str = None) -> None:
27
+ """保存 token 和 uid 到配置文件(目录和文件不存在则自动创建)"""
28
+ path = config_path(config_path_arg)
29
+ os.makedirs(os.path.dirname(path), exist_ok=True)
30
+ if os.path.exists(path):
31
+ with open(path, "r") as f:
32
+ config = yaml.safe_load(f) or {}
33
+ else:
34
+ config = {}
35
+ config["token"] = token
36
+ config["uid"] = uid
37
+ # 确保至少有 gateway_url
38
+ config.setdefault("gateway_url", "https://www.pandaaiquant.com/pandaApi")
39
+ with open(path, "w") as f:
40
+ yaml.dump(config, f, allow_unicode=True, default_flow_style=False)
pandaai/constants.py ADDED
@@ -0,0 +1,15 @@
1
+ """状态常量"""
2
+
3
+ STATUS_PENDING = 0
4
+ STATUS_RUNNING = 1
5
+ STATUS_SUCCESS = 2
6
+ STATUS_FAILED = 3
7
+ STATUS_BILLING_STOP = 6 # 余额不足终止
8
+
9
+ STATUS_MAP = {
10
+ STATUS_PENDING: "PENDING",
11
+ STATUS_RUNNING: "RUNNING",
12
+ STATUS_SUCCESS: "SUCCESS",
13
+ STATUS_FAILED: "FAILED",
14
+ STATUS_BILLING_STOP: "BILLING_STOP",
15
+ }
pandaai/download.py ADDED
@@ -0,0 +1,41 @@
1
+ """CSV 下载"""
2
+
3
+ import os
4
+ import sys
5
+
6
+ import requests
7
+
8
+
9
+ def download_csv(results: dict, download_arg: str) -> None:
10
+ """从结果中提取 CSV URL 并下载"""
11
+ csv_url = None
12
+ for _uuid, data in results.get("nodes", {}).items():
13
+ if isinstance(data, dict) and "url" in data:
14
+ csv_url = data["url"]
15
+ break
16
+
17
+ if not csv_url:
18
+ return
19
+
20
+ if download_arg == "":
21
+ save_dir = os.path.join(os.path.expanduser("~"), "Downloads")
22
+ filename = csv_url.split("/")[-1] if "/" in csv_url else "factor_data.csv"
23
+ save_path = os.path.join(save_dir, filename)
24
+ elif download_arg:
25
+ if os.path.isdir(download_arg) or download_arg.endswith("/"):
26
+ filename = csv_url.split("/")[-1] if "/" in csv_url else "factor_data.csv"
27
+ save_path = os.path.join(download_arg, filename)
28
+ else:
29
+ save_path = download_arg
30
+ else:
31
+ return
32
+
33
+ try:
34
+ resp = requests.get(csv_url, timeout=60)
35
+ resp.raise_for_status()
36
+ os.makedirs(os.path.dirname(save_path) or ".", exist_ok=True)
37
+ with open(save_path, "wb") as f:
38
+ f.write(resp.content)
39
+ print(f"📥 CSV已保存: {save_path} ({len(resp.content)} bytes)", file=sys.stderr)
40
+ except Exception as e:
41
+ print(f"⚠ CSV下载失败: {e}", file=sys.stderr)