myastro 0.3.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.
Files changed (58) hide show
  1. myastro/__init__.py +102 -0
  2. myastro/__main__.py +6 -0
  3. myastro/cli.py +404 -0
  4. myastro/descriptors.py +245 -0
  5. myastro/errors.py +30 -0
  6. myastro/facade.py +265 -0
  7. myastro/imaging/__init__.py +0 -0
  8. myastro/io/__init__.py +1 -0
  9. myastro/io/aria2.py +84 -0
  10. myastro/io/backup.py +164 -0
  11. myastro/io/config.py +34 -0
  12. myastro/io/coords.py +133 -0
  13. myastro/io/core.py +132 -0
  14. myastro/io/db.py +126 -0
  15. myastro/io/docsync.py +58 -0
  16. myastro/io/doctor.py +280 -0
  17. myastro/io/health.py +95 -0
  18. myastro/io/index.py +191 -0
  19. myastro/io/integrity.py +83 -0
  20. myastro/io/jobs/__init__.py +71 -0
  21. myastro/io/jobs/aria2_exec.py +114 -0
  22. myastro/io/jobs/lifecycle.py +171 -0
  23. myastro/io/jobs/query.py +82 -0
  24. myastro/io/jobs/rebuild.py +50 -0
  25. myastro/io/jobs/runner.py +160 -0
  26. myastro/io/jobs/spec.py +89 -0
  27. myastro/io/jobs/store.py +95 -0
  28. myastro/io/manifest.py +47 -0
  29. myastro/io/schedule.py +101 -0
  30. myastro/io/search.py +139 -0
  31. myastro/io/sources/__init__.py +128 -0
  32. myastro/io/sources/asassn.py +64 -0
  33. myastro/io/sources/exoplanet.py +59 -0
  34. myastro/io/sources/gaia.py +46 -0
  35. myastro/io/sources/mpc.py +38 -0
  36. myastro/io/sources/simbad.py +44 -0
  37. myastro/io/sources/tess.py +76 -0
  38. myastro/io/sources/unwise.py +97 -0
  39. myastro/io/sources/vizier.py +49 -0
  40. myastro/io/sources/ztf.py +120 -0
  41. myastro/io/sources/ztf_lc.py +57 -0
  42. myastro/io/testmap.py +189 -0
  43. myastro/io/top/__init__.py +13 -0
  44. myastro/io/top/app.py +412 -0
  45. myastro/io/top/batchscreen.py +146 -0
  46. myastro/io/top/panes.py +388 -0
  47. myastro/io/top/screens.py +270 -0
  48. myastro/io/top/suggest.py +46 -0
  49. myastro/io/types.py +45 -0
  50. myastro/io/urls.py +47 -0
  51. myastro/lc/__init__.py +250 -0
  52. myastro/phot/__init__.py +0 -0
  53. myastro/tasks.py +64 -0
  54. myastro-0.3.0.dist-info/METADATA +211 -0
  55. myastro-0.3.0.dist-info/RECORD +58 -0
  56. myastro-0.3.0.dist-info/WHEEL +4 -0
  57. myastro-0.3.0.dist-info/entry_points.txt +2 -0
  58. myastro-0.3.0.dist-info/licenses/LICENSE +21 -0
myastro/__init__.py ADDED
@@ -0,0 +1,102 @@
1
+ """myastro — 天文数据获取与管理库(公开门面)。
2
+
3
+ 第三方唯一应导入的模块。io/ 以下为内核私有,重构不另行通知。
4
+
5
+ 用法:
6
+ import myastro as ma
7
+ r = ma.fetch("tess", target="WASP-12", sector=20) # 首次下载,此后缓存命中
8
+ df = pd.read_parquet(r.paths[0])
9
+ ma.status(); ma.wait(id); ma.retry(id); ma.delete([id])
10
+
11
+ 稳定性承诺:
12
+ - __all__ 中的符号按语义化版本演进(破坏性改动走 deprecation)
13
+ - 产物 schema(如光变 parquet 的 time/flux/flux_err/quality 列)是契约,
14
+ 变更走 schema_version 字段(当前 SCHEMA_VERSION = 1)
15
+ - 异常体系见 errors.py:ParamError / NetworkError / NotFoundError / StateError
16
+ """
17
+ from __future__ import annotations
18
+
19
+
20
+ def _detect_version() -> str:
21
+ """版本单一来源:以 pyproject.toml 为唯一手写处。
22
+
23
+ 已安装时读包元数据(importlib.metadata);直接从源码树运行时元数据可能缺失,
24
+ 回退到 "0.3.0+source",避免出现第二个手写版本号(此前 pyproject 0.1.0 与
25
+ 这里 0.2.0 已经漂移过)。
26
+ """
27
+ from importlib.metadata import PackageNotFoundError, version
28
+
29
+ try:
30
+ return version("myastro")
31
+ except PackageNotFoundError: # 源码直跑(未安装)
32
+ return "0.3.0+source"
33
+
34
+
35
+ __version__ = _detect_version()
36
+ SCHEMA_VERSION = 1
37
+
38
+ from .descriptors import FieldSpec, SourceDescriptor
39
+ from .errors import NetworkError, NotFoundError, ParamError, StateError
40
+ from .facade import (
41
+ audit,
42
+ backup,
43
+ batch,
44
+ coverage,
45
+ delete,
46
+ describe,
47
+ doctor,
48
+ fetch,
49
+ health,
50
+ index_list,
51
+ index_search,
52
+ index_update,
53
+ init_root,
54
+ prefill,
55
+ query,
56
+ register_source,
57
+ restore,
58
+ retry,
59
+ retry_batch,
60
+ schedule_install,
61
+ schedule_list,
62
+ search,
63
+ sources,
64
+ status,
65
+ wait,
66
+ )
67
+
68
+ __all__ = [
69
+ "SCHEMA_VERSION",
70
+ "FieldSpec",
71
+ "NetworkError",
72
+ "NotFoundError",
73
+ "ParamError",
74
+ "SourceDescriptor",
75
+ "StateError",
76
+ "__version__",
77
+ "audit",
78
+ "backup",
79
+ "batch",
80
+ "coverage",
81
+ "delete",
82
+ "describe",
83
+ "doctor",
84
+ "fetch",
85
+ "health",
86
+ "index_list",
87
+ "index_search",
88
+ "index_update",
89
+ "init_root",
90
+ "prefill",
91
+ "query",
92
+ "register_source",
93
+ "restore",
94
+ "retry",
95
+ "retry_batch",
96
+ "schedule_install",
97
+ "schedule_list",
98
+ "search",
99
+ "sources",
100
+ "status",
101
+ "wait",
102
+ ]
myastro/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """python -m myastro 入口(launchd/脚本调用用)。"""
2
+ import sys
3
+
4
+ from .cli import main
5
+
6
+ sys.exit(main())
myastro/cli.py ADDED
@@ -0,0 +1,404 @@
1
+ """CLI 入口:myastro <子命令>。每命令一个处理函数,main 负责分发与错误→退出码。"""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import sys
6
+
7
+ from .io import jobs
8
+ from .io.db import DB
9
+
10
+
11
+ def _cmd_jobs(args, db) -> int:
12
+ import myastro as ma
13
+
14
+ rows = ma.status()
15
+ if not rows:
16
+ print("(无作业)")
17
+ for r in rows:
18
+ err = f" err={r['error'][:60]}" if r.get("error") else ""
19
+ print(f"#{r['id']:<4} {r['executor']:<7} {r['status']:<10}"
20
+ f" {r['updated_at']}{err}")
21
+ return 0
22
+
23
+
24
+ def _cmd_wait(args, db) -> int:
25
+ import myastro as ma
26
+
27
+ result = ma.wait(args.job_id)
28
+ for p in result.paths:
29
+ print(p)
30
+ return 0
31
+
32
+
33
+ def _cmd_retry(args, db) -> int:
34
+ import myastro as ma
35
+
36
+ ma.retry(args.job_id)
37
+ print(f"job {args.job_id} 已重新提交")
38
+ return 0
39
+
40
+
41
+ def _cmd_delete(args, db) -> int:
42
+ import myastro as ma
43
+
44
+ out = ma.delete(args.job_ids)
45
+ if out["deleted"]:
46
+ print(f"已删除 {len(out['deleted'])}: {out['deleted']}"
47
+ f"(dataset/files 资产保留)")
48
+ for job_id, reason in out["refused"]:
49
+ print(f"拒绝 job {job_id}: {reason}")
50
+ return 0 if not out["refused"] else 1
51
+
52
+
53
+ def _cmd_orphans(args, db) -> int:
54
+ n = jobs.find_orphans(db=db)
55
+ print(f"标记孤儿作业 {n} 个")
56
+ return 0
57
+
58
+
59
+ def _cmd_index(args, db) -> int:
60
+ import myastro as ma
61
+
62
+ if args.index_cmd == "list":
63
+ for r in ma.index_list():
64
+ print(f"{r['name']:<16} "
65
+ f"{'已构建' if r['built'] else '未构建'} {r['desc']}")
66
+ else:
67
+ print(f"索引已更新: {ma.index_update(args.name)}")
68
+ return 0
69
+
70
+
71
+ def _cmd_fetch(args, db) -> int:
72
+ import myastro as ma
73
+ params = {k: v for k, v in vars(args).items()
74
+ if k not in ("fn", "cmd", "source") and v is not None}
75
+ r = ma.fetch(args.source, **params)
76
+ for p in r.paths:
77
+ print(p, f"(cache_hit={r.cache_hit})")
78
+ return 0
79
+
80
+
81
+ def _cmd_audit(args, db) -> int:
82
+ import myastro as ma
83
+ r = ma.audit()
84
+ print(f"核对 {r['total']}:ok {r['ok']} | 基线新建 {r['baselined']} | "
85
+ f"缺失 {r['missing']} | 尺寸不符 {r['size_mismatch']} | "
86
+ f"损坏 {r['corrupt']}")
87
+ for kind in ("missing", "size_mismatch", "corrupt"):
88
+ for item in (__import__("myastro.io.integrity", fromlist=["x"])
89
+ .audit_detail(kind, limit=10)):
90
+ print(f" [{kind}] {item['path']} ({item['why']})")
91
+ return 0 if (r["missing"] + r["size_mismatch"] + r["corrupt"]) == 0 else 1
92
+
93
+
94
+ def _cmd_search(args, db) -> int:
95
+ import myastro as ma
96
+ near = tuple(args.near) if args.near else None
97
+ rows = ma.search(source=args.source, target=args.target,
98
+ name=args.name, since=args.since, until=args.until,
99
+ near=near, radius_deg=args.radius)
100
+ for r in rows:
101
+ sep = f" sep={r['sep_deg']:.4f}°" if "sep_deg" in r else ""
102
+ print(f"#{r['id']:<3} {r['source']:<10} {r['target'] or '-':<14}"
103
+ f"{sep} {r['created']} {r['path'] or ''}")
104
+ print(f"共 {len(rows)} 条")
105
+ return 0
106
+
107
+
108
+ def _cmd_coverage(args, db) -> int:
109
+ import myastro as ma
110
+ r = ma.coverage(args.index)
111
+ print(f"{r['index']}: 本地 {r['local']} / 全量 {r['total']}"
112
+ f"({r['pct']}%)")
113
+ return 0
114
+
115
+
116
+ def _cmd_retry_batch(args, db) -> int:
117
+ import myastro as ma
118
+ out = ma.retry_batch(args.batch_id)
119
+ print(f"重试 {len(out['retried'])}: {out['retried']}")
120
+ for jid, why in out["skipped"]:
121
+ print(f" 跳过 {jid}: {why}")
122
+ return 0
123
+
124
+
125
+ def _cmd_test_map(args, db) -> int:
126
+ from .errors import ParamError
127
+
128
+ try:
129
+ return _test_map(args)
130
+ except ValueError as e: # 不在源码仓库(如已安装的 wheel)
131
+ raise ParamError(f"该命令仅用于源码仓库:{e}") from e
132
+
133
+
134
+ def _test_map(args) -> int:
135
+ from .io.testmap import problems, sync
136
+ if args.check:
137
+ issues = problems()
138
+ for i in issues:
139
+ print("✗", i)
140
+ print("测试能力地图:", "一致" if not issues else f"{len(issues)} 处不一致")
141
+ return 1 if issues else 0
142
+ path = sync()
143
+ print(f"已生成 {path}")
144
+ for i in problems():
145
+ print(" ⚠", i)
146
+ return 0
147
+
148
+
149
+ def _cmd_health(args, db) -> int:
150
+ import myastro as ma
151
+ for r in ma.health(args.source):
152
+ mark = {"ok": "✅", "degraded": "⚠", "unreachable": "❌"}.get(
153
+ r["status"], "?")
154
+ print(f"{mark} {r['source']:<10} {r['status']:<12} {r['detail']}")
155
+ return 0
156
+
157
+
158
+ def _cmd_schedule(args, db) -> int:
159
+ import myastro as ma
160
+
161
+ from .io.schedule import remove
162
+ if args.schedule_cmd == "list":
163
+ for r in ma.schedule_list():
164
+ mark = "✅已装" if r["installed"] else "—未装"
165
+ print(f"{mark} {r['task']:<13} {r['when']} {r['desc']}")
166
+ elif args.schedule_cmd == "install":
167
+ p = ma.schedule_install(args.task)
168
+ print(f"已安装 launchd 任务: {p}")
169
+ else:
170
+ print("已移除" if remove(args.task) else "本就未安装")
171
+ return 0
172
+
173
+
174
+ def _cmd_docsync(args, db) -> int:
175
+ from .errors import ParamError
176
+ from .io.docsync import sync
177
+
178
+ try:
179
+ print("文档已更新" if sync() else "文档已是最新(无变化)")
180
+ except ValueError as e: # 不在源码仓库(如已安装的 wheel)
181
+ raise ParamError(f"该命令仅用于源码仓库:{e}") from e
182
+ return 0
183
+
184
+
185
+ def _cmd_init(args, db) -> int:
186
+ import myastro as ma
187
+
188
+ paths = ma.init_root()
189
+ print("数据根:", paths["data"])
190
+ print("缓存: ", paths["cache"])
191
+ print("账本: ", paths["db"])
192
+ return 0
193
+
194
+
195
+ def _cmd_doctor(args, db) -> int:
196
+ import myastro as ma
197
+
198
+ checks = ma.doctor()
199
+ symbol = {"ok": "✓", "warn": "⚠", "fail": "✗"}
200
+ for c in checks:
201
+ line = f"{symbol.get(c['status'], '?')} {c['name']}"
202
+ if c["detail"]:
203
+ line += f":{c['detail']}"
204
+ print(line)
205
+ if c["fix"] and c["status"] != "ok":
206
+ print(f" → {c['fix']}")
207
+ fails = sum(1 for c in checks if c["status"] == "fail")
208
+ warns = sum(1 for c in checks if c["status"] == "warn")
209
+ print(f"自检完成:{len(checks)} 项,{warns} 个警告,{fails} 个问题")
210
+ return 1 if fails else 0
211
+
212
+
213
+ def _cmd_backup(args, db) -> int:
214
+ import myastro as ma
215
+
216
+ out = ma.backup(args.out, with_index=args.with_index)
217
+ print(f"备份目录: {out['dir']}")
218
+ print(f"账本: {out['db']}({out['bytes'] / 1024:.0f} KB)")
219
+ print(f"清单: {out['manifest']}")
220
+ if out["with_index"]:
221
+ print("(含索引副本)")
222
+ return 0
223
+
224
+
225
+ def _cmd_restore(args, db) -> int:
226
+ import myastro as ma
227
+
228
+ out = ma.restore(args.source, force=args.force)
229
+ print(f"已从 {out['restored_from']} 恢复 → {out['db']}")
230
+ if out["safety_copy"]:
231
+ print(f"覆盖前的账本已留存: {out['safety_copy']}")
232
+ print(f"计数: {out['counts']}")
233
+ return 0
234
+
235
+
236
+ def _cmd_top(args, db) -> int:
237
+ from .io.top import AstroTop
238
+ AstroTop().run()
239
+ return 0
240
+
241
+
242
+ def _add_fetch(parser: argparse.ArgumentParser) -> None:
243
+ """fetch 子命令:参数由源描述符的 FieldSpec 声明生成。"""
244
+ from .descriptors import REGISTRY
245
+ sub = parser.add_subparsers(dest="source", required=True,
246
+ metavar="SOURCE")
247
+ for name, d in REGISTRY.items():
248
+ p = sub.add_parser(name, help=d.desc)
249
+ for f in d.fields:
250
+ extra = {}
251
+ if f.choices:
252
+ extra["choices"] = f.choices
253
+ p.add_argument(f"--{f.name.replace('_', '-')}",
254
+ dest=f.name,
255
+ default=f.default,
256
+ type=str if f.type is str else f.type,
257
+ help=f.label or None, **extra)
258
+ p.set_defaults(fn=_cmd_fetch)
259
+
260
+
261
+ def _build_parser() -> argparse.ArgumentParser:
262
+ from . import __version__
263
+
264
+ parser = argparse.ArgumentParser(prog="myastro",
265
+ description="天文数据获取与管理")
266
+ # --version(版本单一来源见 src/myastro/__init__.py::_detect_version)
267
+ parser.add_argument("--version", action="version",
268
+ version=f"myastro {__version__}")
269
+ sub = parser.add_subparsers(dest="cmd", required=True)
270
+
271
+ sub.add_parser("jobs", help="列出作业(最近 100 条)").set_defaults(
272
+ fn=_cmd_jobs)
273
+ p_wait = sub.add_parser("wait", help="阻塞等待作业完成")
274
+ p_wait.add_argument("job_id", type=int)
275
+ p_wait.set_defaults(fn=_cmd_wait)
276
+ p_retry = sub.add_parser("retry", help="重试失败/僵尸/done(force)作业")
277
+ p_retry.add_argument("job_id", type=int)
278
+ p_retry.set_defaults(fn=_cmd_retry)
279
+ p_fetch = sub.add_parser("fetch", help="获取数据(参数由描述符生成)")
280
+ _add_fetch(p_fetch)
281
+ p_sched = sub.add_parser("schedule", help="定时任务(launchd)管理")
282
+ sc = p_sched.add_subparsers(dest="schedule_cmd", required=True)
283
+ sc.add_parser("list", help="列出任务与安装状态")
284
+ p_si = sc.add_parser("install", help="安装 launchd 任务")
285
+ p_si.add_argument("task", choices=list(
286
+ __import__("myastro.io.schedule", fromlist=["TASKS"]).TASKS))
287
+ p_sr = sc.add_parser("remove", help="移除任务")
288
+ p_sr.add_argument("task", choices=list(
289
+ __import__("myastro.io.schedule", fromlist=["TASKS"]).TASKS))
290
+ p_sched.set_defaults(fn=_cmd_schedule)
291
+ sub.add_parser("init", help="创建数据根与账本(幂等)").set_defaults(
292
+ fn=_cmd_init)
293
+ sub.add_parser("doctor", help="环境自检(依赖/路径/账本/索引/平台)").set_defaults(
294
+ fn=_cmd_doctor)
295
+ p_bk = sub.add_parser("backup", help="备份账本(SQLite 在线备份)")
296
+ p_bk.add_argument("--out", help="备份输出目录(默认 账本目录/backups/时间戳)")
297
+ p_bk.add_argument("--with-index", action="store_true", dest="with_index",
298
+ help="连同索引 parquet 一起复制(可上百 MB)")
299
+ p_bk.set_defaults(fn=_cmd_backup)
300
+ p_rs = sub.add_parser("restore", help="从备份恢复账本(默认拒绝覆盖)")
301
+ p_rs.add_argument("source", help="备份目录或 astro.db 路径")
302
+ p_rs.add_argument("--force", action="store_true",
303
+ help="覆盖现有账本(会先留存 .pre-restore-*.bak)")
304
+ p_rs.set_defaults(fn=_cmd_restore)
305
+ p_tmap = sub.add_parser("test-map", help="生成/检查测试能力地图")
306
+ p_tmap.add_argument("--check", action="store_true",
307
+ help="只检查不写文件(守卫用)")
308
+ p_tmap.set_defaults(fn=_cmd_test_map)
309
+ p_health = sub.add_parser("health", help="源健康探测")
310
+ p_health.add_argument("source", nargs="?", default=None)
311
+ p_health.set_defaults(fn=_cmd_health)
312
+ p_audit = sub.add_parser("audit", help="完整性巡检(sha 基线+核对)")
313
+ p_audit.set_defaults(fn=_cmd_audit)
314
+ p_search = sub.add_parser("search", help="本地检索已有数据")
315
+ p_search.add_argument("--source"); p_search.add_argument("--target")
316
+ p_search.add_argument("--name"); p_search.add_argument("--since")
317
+ p_search.add_argument("--until")
318
+ p_search.add_argument("--near", nargs=2, type=float, metavar=("RA", "DEC"),
319
+ help="精确圆锥检索中心(度)")
320
+ p_search.add_argument("--radius", type=float, default=0.1,
321
+ help="圆锥半径(度,默认 0.1)")
322
+ p_search.set_defaults(fn=_cmd_search)
323
+ p_cov = sub.add_parser("coverage", help="索引覆盖率")
324
+ p_cov.add_argument("index")
325
+ p_cov.set_defaults(fn=_cmd_coverage)
326
+ p_rb = sub.add_parser("retry-batch", help="整批重试失败作业")
327
+ p_rb.add_argument("batch_id")
328
+ p_rb.set_defaults(fn=_cmd_retry_batch)
329
+ p_del = sub.add_parser("delete", help="删除作业记录(资产保留)")
330
+ p_del.add_argument("job_ids", nargs="+", type=int)
331
+ p_del.set_defaults(fn=_cmd_delete)
332
+ sub.add_parser("orphans", help="扫描并标记心跳超时的孤儿作业"
333
+ ).set_defaults(fn=_cmd_orphans)
334
+
335
+ p_idx = sub.add_parser("index", help="本地索引管理(TUI 候选列表)")
336
+ p_idx_sub = p_idx.add_subparsers(dest="index_cmd", required=True)
337
+ p_idx_sub.add_parser("list", help="列出索引").set_defaults(fn=_cmd_index)
338
+ p_upd = p_idx_sub.add_parser("update", help="强制更新(下载→解析→删源)")
339
+ p_upd.add_argument("name")
340
+ p_upd.set_defaults(fn=_cmd_index)
341
+
342
+ sub.add_parser("doc-sync",
343
+ help="再生 design-io.md 数据源矩阵(文档=渲染产物)"
344
+ ).set_defaults(fn=_cmd_docsync)
345
+ sub.add_parser("top", help="数据管理 TUI").set_defaults(fn=_cmd_top)
346
+ return parser
347
+
348
+
349
+ # 退出码契约(脚本可据此分流;错误消息走 stderr,不打印栈):
350
+ # 0 成功 | 1 部分失败(如删除时部分作业被拒绝)| 2 参数错 | 3 未找到
351
+ # | 4 网络 | 5 状态不允许 | 130 键盘中断
352
+ EXIT_CODES = {
353
+ "ParamError": 2, # 参数/未知源:调用方传错了
354
+ "NotFoundError": 3, # 目标不存在(作业/索引/批次)
355
+ "NetworkError": 4, # 网络侧失败(重试后)
356
+ "StateError": 5, # 状态不允许(如 done 重试、健康作业删除)
357
+ }
358
+
359
+
360
+ def main(argv: list[str] | None = None) -> int:
361
+ """CLI 入口:异常转成清晰的 stderr 消息 + 退出码。
362
+
363
+ 早期版本没有这一层——`myastro retry 999999` 直接抛栈崩溃。
364
+ 对"第三个客户端"(脚本/第三方)而言,退出码是可编程契约的一部分。
365
+ """
366
+ from . import errors
367
+
368
+ args = _build_parser().parse_args(argv)
369
+ if args.cmd == "top":
370
+ try:
371
+ return args.fn(args, None)
372
+ except KeyboardInterrupt:
373
+ print("已中断", file=sys.stderr)
374
+ return 130
375
+ db = DB().init() # 单连接:用完即关(不再每命令各开一个)
376
+ try:
377
+ return args.fn(args, db)
378
+ except errors.ParamError as e:
379
+ print(f"参数错误:{e}", file=sys.stderr)
380
+ return EXIT_CODES["ParamError"]
381
+ except errors.NotFoundError as e:
382
+ print(f"未找到:{e}", file=sys.stderr)
383
+ return EXIT_CODES["NotFoundError"]
384
+ except errors.NetworkError as e:
385
+ print(f"网络错误:{e}", file=sys.stderr)
386
+ return EXIT_CODES["NetworkError"]
387
+ except errors.StateError as e:
388
+ print(f"状态不允许:{e}", file=sys.stderr)
389
+ return EXIT_CODES["StateError"]
390
+ except LookupError as e: # 内核侧"不存在"(如未知任务/索引)
391
+ print(f"未找到:{e}", file=sys.stderr)
392
+ return EXIT_CODES["NotFoundError"]
393
+ except ValueError as e: # 内核侧参数问题
394
+ print(f"参数错误:{e}", file=sys.stderr)
395
+ return EXIT_CODES["ParamError"]
396
+ except KeyboardInterrupt:
397
+ print("已中断", file=sys.stderr)
398
+ return 130
399
+ finally:
400
+ db.close()
401
+
402
+
403
+ if __name__ == "__main__":
404
+ sys.exit(main())