dragon-quant 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.
@@ -0,0 +1,17 @@
1
+ """
2
+ dragon_quant — 龙头战法四维量化筛选系统
3
+
4
+ 公共 API:
5
+ from dragon_quant import scan # 编排器:完整扫描
6
+ from dragon_quant.data import ( # 原子数据查询
7
+ get_sector_ranking, get_sector_components, get_sector_5min_kline,
8
+ get_kline, get_minute_kline, get_quote, batch_get_quotes,
9
+ )
10
+ from dragon_quant.logging.query import ( # 日志查询
11
+ tail_logs, query_logs, clear_logs, list_logs, log_summary,
12
+ )
13
+ """
14
+
15
+ from dragon_quant.orchestrator import scan
16
+
17
+ __all__ = ["scan"]
@@ -0,0 +1,5 @@
1
+ """python -m dragon_quant → 默认运行批量扫描"""
2
+ from dragon_quant.cli import main
3
+
4
+ if __name__ == "__main__":
5
+ main()
@@ -0,0 +1,113 @@
1
+ """
2
+ analyze — 子进程入口:加载共享缓存,对全部候选股四维打分
3
+
4
+ 用法:
5
+ python -m dragon_quant.analyze --shared-cache <path> [--json]
6
+ """
7
+
8
+ import json
9
+ import sys
10
+ import argparse
11
+ from dragon_quant.cache.data_cache import DataCache
12
+ from dragon_quant.models.types import Candidate
13
+ from dragon_quant.scorers.drive import score as score_drive
14
+ from dragon_quant.scorers.anti_drop import score as score_anti_drop
15
+ from dragon_quant.scorers.leadership import score as score_leadership
16
+ from dragon_quant.scorers.absorption import score as score_absorption
17
+
18
+
19
+ SCORERS = [
20
+ ("drive", score_drive, 0.35),
21
+ ("anti_drop", score_anti_drop, 0.15),
22
+ ("leadership", score_leadership, 0.25),
23
+ ("absorption", score_absorption, 0.25),
24
+ ]
25
+
26
+
27
+ def main():
28
+ parser = argparse.ArgumentParser(description="龙头战法四维评分(子进程入口)")
29
+ parser.add_argument("--shared-cache", required=True, help="共享缓存文件路径")
30
+ parser.add_argument("--json", action="store_true", help="JSON 输出(默认开启)")
31
+ args = parser.parse_args()
32
+
33
+ # 加载共享缓存
34
+ cache = DataCache()
35
+ with open(args.shared_cache) as f:
36
+ data = json.load(f)
37
+ cache.load_snapshot(data)
38
+
39
+ # 读取元数据
40
+ candidates_raw = cache.get("__meta__:candidates") or []
41
+ all_sector_codes = cache.get("__meta__:sector_codes") or []
42
+ sector_name_map_raw = cache.get("__meta__:sector_name_map") or {}
43
+
44
+ if not candidates_raw:
45
+ print("[]")
46
+ return
47
+
48
+ # 重建候选股对象(用于 drive 的 peer_pool 构建)
49
+ candidate_pool = [
50
+ Candidate(
51
+ code=c["code"], name=c["name"],
52
+ concepts=c.get("concepts", []),
53
+ primary_sector=c.get("primary_sector", ""),
54
+ board_count=c.get("board_count", 0),
55
+ )
56
+ for c in candidates_raw
57
+ ]
58
+
59
+ # ─── 逐只打分 ───
60
+ results = []
61
+ for cand in candidates_raw:
62
+ code = cand["code"]
63
+ name = cand["name"]
64
+ primary_sector = cand.get("primary_sector", "")
65
+
66
+ dims = {}
67
+ composite = 0.0
68
+
69
+ for dim_name, score_fn, weight in SCORERS:
70
+ try:
71
+ kwargs = {"code": code, "cache": cache}
72
+ if dim_name == "drive":
73
+ kwargs["candidate_pool"] = candidate_pool
74
+ if dim_name in ("drive", "leadership", "absorption"):
75
+ kwargs["primary_sector"] = primary_sector
76
+ if dim_name == "absorption":
77
+ kwargs["all_sector_codes"] = all_sector_codes
78
+ kwargs["sector_name_map"] = sector_name_map_raw
79
+
80
+ sr = score_fn(**kwargs)
81
+ dims[dim_name] = {
82
+ "score": sr.score,
83
+ "weight": sr.weight,
84
+ "details": sr.details,
85
+ }
86
+ composite += sr.score * sr.weight
87
+ except Exception as e:
88
+ print(f" ⚠️ {dim_name} 打分异常 {code}: {e}", file=sys.stderr)
89
+ dims[dim_name] = {
90
+ "score": 50.0,
91
+ "weight": weight,
92
+ "details": {"error": str(e)},
93
+ }
94
+ composite += 50.0 * weight
95
+
96
+ results.append({
97
+ "code": code,
98
+ "name": name,
99
+ "concepts": cand.get("concepts", []),
100
+ "board_count": cand.get("board_count", 0),
101
+ "primary_sector": primary_sector,
102
+ "primary_sector_name": sector_name_map_raw.get(primary_sector, ""),
103
+ "composite_score": round(composite, 2),
104
+ "dimensions": dims,
105
+ })
106
+
107
+ # 输出
108
+ output = json.dumps(results, ensure_ascii=False, indent=2)
109
+ print(output)
110
+
111
+
112
+ if __name__ == "__main__":
113
+ main()
File without changes
@@ -0,0 +1,171 @@
1
+ """
2
+ DataCache — 内存 + 本地双重缓存
3
+
4
+ 流程:调用方 → DataCache(先查缓存) → RateLimiter(并发控制) → Provider(HTTP)
5
+
6
+ 缓存策略:
7
+ - 板块行情/个股K线 → 当天有效(过期=次日00:00)
8
+ - 涨停榜 → 当天有效
9
+ - 写入本地 JSON 做持久化(subprocess 共享)
10
+ """
11
+
12
+ import json
13
+ import os
14
+ import sys
15
+ import threading
16
+ import time
17
+ from pathlib import Path
18
+ from typing import Any, Optional
19
+
20
+
21
+ def _today_end() -> int:
22
+ """当日 23:59:59 的时间戳(秒)"""
23
+ t = time.localtime()
24
+ return int(time.mktime((t.tm_year, t.tm_mon, t.tm_mday, 23, 59, 59, 0, 0, 0)))
25
+
26
+
27
+ def _to_json_safe(obj):
28
+ """递归转换 dataclass / bytes 为 JSON 安全类型"""
29
+ if hasattr(obj, '__dataclass_fields__'):
30
+ return {
31
+ f.name: _to_json_safe(getattr(obj, f.name))
32
+ for f in obj.__dataclass_fields__.values()
33
+ }
34
+ if isinstance(obj, list):
35
+ return [_to_json_safe(i) for i in obj]
36
+ if isinstance(obj, dict):
37
+ return {k: _to_json_safe(v) for k, v in obj.items()}
38
+ if isinstance(obj, bytes):
39
+ return obj.decode('utf-8', errors='replace')
40
+ return obj
41
+
42
+
43
+ class CacheEntry:
44
+ __slots__ = ("data", "ttl")
45
+
46
+ def __init__(self, data: Any, ttl: int):
47
+ self.data = data
48
+ self.ttl = ttl
49
+
50
+ @property
51
+ def expired(self) -> bool:
52
+ return time.time() > self.ttl
53
+
54
+
55
+ class DataCache:
56
+ """内存缓存 + 可选的本地持久化"""
57
+
58
+ def __init__(self, cache_dir: Optional[Path] = None):
59
+ self._mem: dict[str, CacheEntry] = {}
60
+ self._lock = threading.Lock()
61
+
62
+ if cache_dir is None:
63
+ from dragon_quant.storage.paths import CACHE_DIR
64
+ cache_dir = CACHE_DIR
65
+
66
+ self._cache_dir = cache_dir
67
+ self._hit = 0
68
+ self._miss = 0
69
+
70
+ # ─── 内存操作 ───
71
+
72
+ def get(self, key: str) -> Optional[Any]:
73
+ with self._lock:
74
+ entry = self._mem.get(key)
75
+ if entry is None:
76
+ self._miss += 1
77
+ return None
78
+ if entry.expired:
79
+ with self._lock:
80
+ del self._mem[key]
81
+ self._miss += 1
82
+ return None
83
+ self._hit += 1
84
+ return entry.data
85
+
86
+ def set(self, key: str, data: Any, ttl: Optional[int] = None):
87
+ if ttl is None:
88
+ ttl = _today_end()
89
+ with self._lock:
90
+ self._mem[key] = CacheEntry(data, ttl)
91
+ # 持久化
92
+ if self._cache_dir:
93
+ self._persist(key, data)
94
+
95
+ def invalidate(self, key: str):
96
+ with self._lock:
97
+ self._mem.pop(key, None)
98
+ if self._cache_dir:
99
+ p = self._cache_dir / f"{key}.json"
100
+ if p.exists():
101
+ p.unlink()
102
+
103
+ def clear_expired(self):
104
+ now = time.time()
105
+ with self._lock:
106
+ expired = [k for k, v in self._mem.items() if v.expired]
107
+ for k in expired:
108
+ del self._mem[k]
109
+
110
+ def snapshot(self) -> dict[str, Any]:
111
+ """导出全部未过期数据快照(用于共享缓存)"""
112
+ now = time.time()
113
+ result = {}
114
+ with self._lock:
115
+ for k, v in self._mem.items():
116
+ if v.ttl > now:
117
+ result[k] = v.data
118
+ return result
119
+
120
+ def load_snapshot(self, data: dict[str, Any], ttl: Optional[int] = None):
121
+ """从外部加载数据到缓存"""
122
+ if ttl is None:
123
+ ttl = _today_end()
124
+ with self._lock:
125
+ for k, v in data.items():
126
+ self._mem[k] = CacheEntry(v, ttl)
127
+
128
+ # ─── 本地持久化 ───
129
+
130
+ def _persist(self, key: str, data: Any):
131
+ if not self._cache_dir:
132
+ return
133
+ self._cache_dir.mkdir(parents=True, exist_ok=True)
134
+ try:
135
+ safe = _to_json_safe(data)
136
+ with open(self._cache_dir / f"{key}.json", "w") as f:
137
+ json.dump({"data": safe, "ts": time.time()}, f, ensure_ascii=False)
138
+ except Exception as e:
139
+ print(f" ⚠️ 缓存持久化失败 {key}: {e}", file=sys.stderr)
140
+
141
+ def load_persisted(self, key: str, max_age: int = 86400) -> Optional[Any]:
142
+ """从本地文件恢复缓存"""
143
+ if not self._cache_dir:
144
+ return None
145
+ p = self._cache_dir / f"{key}.json"
146
+ if not p.exists():
147
+ return None
148
+ try:
149
+ with open(p) as f:
150
+ blob = json.load(f)
151
+ age = time.time() - blob.get("ts", 0)
152
+ if age > max_age:
153
+ p.unlink()
154
+ return None
155
+ data = blob["data"]
156
+ self.set(key, data, ttl=int(time.time()) + max_age - int(age))
157
+ return data
158
+ except Exception:
159
+ return None
160
+
161
+ # ─── 统计 ───
162
+
163
+ def stats(self) -> dict:
164
+ return {"hit": self._hit, "miss": self._miss, "size": len(self._mem)}
165
+
166
+ # ─── 助手 ───
167
+
168
+ def cache_key(self, provider: str, endpoint: str, *args) -> str:
169
+ parts = [provider, endpoint]
170
+ parts.extend(str(a) for a in args)
171
+ return ":".join(parts)
dragon_quant/cli.py ADDED
@@ -0,0 +1,301 @@
1
+ """
2
+ CLI 入口 — dragon-quant 命令行工具
3
+
4
+ 命令:
5
+ dragon-quant scan [--top 5] [--candidates 5] [--workers 2]
6
+ dragon-quant logs {tail,query,clear,list} [options]
7
+ dragon-quant data {sector,components,kline,minute,quote,batch-quote} [options]
8
+ dragon-quant storage {status,size,clear} [options]
9
+ """
10
+
11
+ import argparse
12
+ import json
13
+ import sys
14
+
15
+ from dragon_quant.orchestrator import scan as orchestrate_scan
16
+ from dragon_quant.storage.manager import StorageManager
17
+
18
+
19
+ def _cmd_scan(args):
20
+ """扫描命令"""
21
+ orchestrate_scan(
22
+ top_n=args.top,
23
+ candidates_n=args.candidates,
24
+ workers=args.workers,
25
+ verbose=True,
26
+ )
27
+
28
+
29
+ def _cmd_logs(args):
30
+ """日志命令"""
31
+ from dragon_quant.logging.query import (
32
+ tail_logs, query_logs, clear_logs, list_logs, log_summary,
33
+ )
34
+
35
+ if args.logs_action == "tail":
36
+ entries = tail_logs(lines=args.lines)
37
+ for e in entries:
38
+ print(json.dumps(e, ensure_ascii=False))
39
+
40
+ elif args.logs_action == "query":
41
+ entries = query_logs(
42
+ date=args.date,
43
+ category=args.category,
44
+ level=args.level,
45
+ code=args.code,
46
+ tail=args.tail,
47
+ )
48
+ for e in entries:
49
+ print(json.dumps(e, ensure_ascii=False))
50
+
51
+ elif args.logs_action == "clear":
52
+ result = clear_logs(days=args.days)
53
+ print(f"清除日志: {result['cleared']} 个文件")
54
+ for f in result.get("files_removed", []):
55
+ print(f" ✓ {f}")
56
+ print(f"保留: {result['kept']} 个文件")
57
+
58
+ elif args.logs_action == "list":
59
+ files = list_logs()
60
+ if not files:
61
+ print("(无日志文件)")
62
+ else:
63
+ print(f"{'文件名':30s} {'大小':>8s} {'时间':20s} {'行数':>6s}")
64
+ print("-" * 74)
65
+ for f in files:
66
+ print(f"{f['name']:30s} {f['size']:>8s} {f['mtime']:20s} {f['lines']:6d}")
67
+
68
+ elif args.logs_action == "summary":
69
+ summary = log_summary(date=args.date)
70
+ print(json.dumps(summary, ensure_ascii=False, indent=2))
71
+
72
+
73
+ def _cmd_data(args):
74
+ """数据查询命令"""
75
+ from dragon_quant.data import (
76
+ get_sector_ranking, get_sector_components, get_sector_5min_kline,
77
+ get_kline, get_minute_kline, get_quote, batch_get_quotes,
78
+ )
79
+
80
+ if args.data_action == "sector":
81
+ sectors = get_sector_ranking(asc=args.asc)
82
+ for s in sectors:
83
+ print(f"{s.code:10s} {s.name:12s} {s.pct:>+8.2f}%")
84
+
85
+ elif args.data_action == "components":
86
+ if not args.sector:
87
+ print("错误: 需要 --sector <板块代码>", file=sys.stderr)
88
+ return
89
+ stocks = get_sector_components(args.sector)
90
+ for s in stocks:
91
+ print(f"{s.code:8s} {s.name:8s} {s.pct:>+8.2f}%")
92
+
93
+ elif args.data_action == "kline":
94
+ if not args.code:
95
+ print("错误: 需要 --code <股票代码>", file=sys.stderr)
96
+ return
97
+ klines = get_kline(args.code, source=args.source, days=args.days)
98
+ for k in klines:
99
+ print(json.dumps(_kbar_to_dict(k), ensure_ascii=False))
100
+
101
+ elif args.data_action == "minute":
102
+ if not args.code:
103
+ print("错误: 需要 --code <股票代码>", file=sys.stderr)
104
+ return
105
+ klines = get_minute_kline(args.code, source=args.source)
106
+ for k in klines:
107
+ print(json.dumps(_kbar_to_dict(k), ensure_ascii=False))
108
+
109
+ elif args.data_action == "quote":
110
+ if not args.code:
111
+ print("错误: 需要 --code <股票代码>", file=sys.stderr)
112
+ return
113
+ q = get_quote(args.code, source=args.source)
114
+ if q:
115
+ print(json.dumps(_to_dict(q), ensure_ascii=False, indent=2))
116
+ else:
117
+ print(f"获取行情失败: {args.code}")
118
+
119
+ elif args.data_action == "batch_quote":
120
+ if not args.codes:
121
+ print("错误: 需要 --codes <代码列表,逗号分隔>", file=sys.stderr)
122
+ return
123
+ codes = [c.strip() for c in args.codes.split(",")]
124
+ quotes = batch_get_quotes(codes, source=args.source)
125
+ for q in quotes:
126
+ if q:
127
+ print(json.dumps(_to_dict(q), ensure_ascii=False))
128
+
129
+ elif args.data_action == "cookie_status":
130
+ from dragon_quant.data import cookie_status
131
+ print(json.dumps(cookie_status(), ensure_ascii=False, indent=2))
132
+
133
+ elif args.data_action == "cookie_fetch":
134
+ from dragon_quant.data import fetch_cookies
135
+ result = fetch_cookies(source=args.source)
136
+ print(json.dumps(result, ensure_ascii=False, indent=2))
137
+
138
+
139
+ def _cmd_storage(args):
140
+ """存储管理命令"""
141
+ mgr = StorageManager()
142
+
143
+ if args.storage_action == "status":
144
+ s = mgr.status()
145
+ print(f"数据根目录: {s['data_dir']}")
146
+ print(f"{'目录':10s} {'文件数':>6s} {'大小':>8s}")
147
+ print("-" * 28)
148
+ for key in ("cookies", "cache", "logs", "results", "shared"):
149
+ d = s[key]
150
+ if d["exists"]:
151
+ print(f"{key:10s} {d['files']:6d} {d['size']:>8s}")
152
+ else:
153
+ print(f"{key:10s} (不存在)")
154
+
155
+ elif args.storage_action == "clear":
156
+ if args.all:
157
+ r = mgr.clear_all()
158
+ for k, v in r.items():
159
+ print(f" 清理 {k}: {v} 个文件")
160
+ else:
161
+ if args.cache:
162
+ n = mgr.clear_cache()
163
+ print(f" 清理 cache: {n} 个文件")
164
+ if args.results:
165
+ n = mgr.clear_results(days=args.days)
166
+ print(f" 清理 results: {n} 个文件")
167
+ if args.days:
168
+ print(f" (保留最近 {args.days} 天)")
169
+ if args.logs:
170
+ n = mgr.clear_logs(days=args.days)
171
+ print(f" 清理 logs: {n} 个文件")
172
+ if args.days:
173
+ print(f" (保留最近 {args.days} 天)")
174
+
175
+ elif args.storage_action == "size":
176
+ s = mgr.size()
177
+ print(f"总占用: {s['total']}")
178
+ from dragon_quant.storage.manager import _fmt_size
179
+ for k, b in s["by_dir"].items():
180
+ print(f" {k}: {_fmt_size(b)}")
181
+
182
+
183
+ def _kbar_to_dict(kbar) -> dict:
184
+ """KBar → dict"""
185
+ d = {}
186
+ for f in ("time", "open", "close", "high", "low", "volume", "pct"):
187
+ v = getattr(kbar, f, None)
188
+ if isinstance(v, float):
189
+ v = round(v, 4)
190
+ d[f] = v
191
+ return d
192
+
193
+
194
+ def _to_dict(obj) -> dict:
195
+ """dataclass → dict"""
196
+ if hasattr(obj, '__dataclass_fields__'):
197
+ return {f.name: _to_dict(getattr(obj, f.name)) for f in obj.__dataclass_fields__.values()}
198
+ if isinstance(obj, list):
199
+ return [_to_dict(i) for i in obj]
200
+ if isinstance(obj, dict):
201
+ return {k: _to_dict(v) for k, v in obj.items()}
202
+ return obj
203
+
204
+
205
+ def main():
206
+ shared = argparse.ArgumentParser(add_help=False)
207
+ shared.add_argument("--top", type=int, default=25, help="最终候选股数量 (默认25)")
208
+ shared.add_argument("--candidates", type=int, default=5, help="每板块取前N只 (默认5)")
209
+ shared.add_argument("--workers", type=int, default=2, help="并发线程数 (默认2)")
210
+
211
+ parser = argparse.ArgumentParser(
212
+ description="龙头战法四维量化筛选系统",
213
+ )
214
+ parser.set_defaults(command="scan")
215
+ sub = parser.add_subparsers(dest="command")
216
+
217
+ # scan 子命令
218
+ scan_p = sub.add_parser("scan", help="批量扫描龙头股", parents=[shared])
219
+
220
+ # logs 子命令
221
+ logs_p = sub.add_parser("logs", help="日志查询与管理")
222
+ logs_subs = logs_p.add_subparsers(dest="logs_action")
223
+
224
+ tail_p = logs_subs.add_parser("tail", help="查看最新日志")
225
+ tail_p.add_argument("-n", "--lines", type=int, default=20, help="返回行数 (默认20)")
226
+
227
+ query_p = logs_subs.add_parser("query", help="按条件查询日志")
228
+ query_p.add_argument("--date", help="日期 (YYYYMMDD)")
229
+ query_p.add_argument("--category", help="类别过滤,如 phase、api、scorer:drive")
230
+ query_p.add_argument("--level", help="级别过滤,如 info、warn、error")
231
+ query_p.add_argument("--code", help="股票代码过滤")
232
+ query_p.add_argument("--tail", type=int, default=200, help="最多返回条数 (默认200)")
233
+
234
+ clear_logs_p = logs_subs.add_parser("clear", help="清除旧日志")
235
+ clear_logs_p.add_argument("--days", type=int, default=7, help="保留最近N天 (默认7)")
236
+
237
+ logs_subs.add_parser("list", help="列出所有日志文件")
238
+ sum_p = logs_subs.add_parser("summary", help="最新扫描摘要")
239
+ sum_p.add_argument("--date", help="日期过滤")
240
+
241
+ # data 子命令
242
+ data_p = sub.add_parser("data", help="原子数据查询")
243
+ data_subs = data_p.add_subparsers(dest="data_action")
244
+
245
+ sector_p = data_subs.add_parser("sector", help="板块排行榜")
246
+ sector_p.add_argument("--asc", action="store_true", help="跌幅榜(默认涨幅榜)")
247
+
248
+ comp_p = data_subs.add_parser("components", help="板块成分股")
249
+ comp_p.add_argument("--sector", required=True, help="板块代码,如 BK0487")
250
+
251
+ kline_p = data_subs.add_parser("kline", help="个股日K线")
252
+ kline_p.add_argument("--code", required=True, help="股票代码")
253
+ kline_p.add_argument("--source", default="xueqiu", choices=["xueqiu", "tencent"])
254
+ kline_p.add_argument("--days", type=int, default=20)
255
+
256
+ min_p = data_subs.add_parser("minute", help="个股1分钟K线(分时)")
257
+ min_p.add_argument("--code", required=True, help="股票代码")
258
+ min_p.add_argument("--source", default="xueqiu", choices=["xueqiu", "tencent"])
259
+
260
+ quote_p = data_subs.add_parser("quote", help="个股实时行情")
261
+ quote_p.add_argument("--code", required=True, help="股票代码")
262
+ quote_p.add_argument("--source", default="tencent", choices=["tencent", "xueqiu"])
263
+
264
+ bq_p = data_subs.add_parser("batch-quote", help="批量实时行情")
265
+ bq_p.add_argument("--codes", required=True, help="股票代码,逗号分隔")
266
+ bq_p.add_argument("--source", default="tencent", choices=["tencent", "xueqiu"])
267
+
268
+ data_subs.add_parser("cookie-status", help="查看 Cookie 状态")
269
+ cf_p = data_subs.add_parser("cookie-fetch", help="刷新 Cookie")
270
+ cf_p.add_argument("--source", default="all", choices=["all", "eastmoney", "xueqiu"])
271
+
272
+ # storage 子命令
273
+ st_p = sub.add_parser("storage", help="持久化数据管理")
274
+ st_subs = st_p.add_subparsers(dest="storage_action")
275
+
276
+ st_subs.add_parser("status", help="查看存储状态")
277
+ st_subs.add_parser("size", help="查看磁盘占用")
278
+
279
+ clear_p = st_subs.add_parser("clear", help="清理数据")
280
+ clear_p.add_argument("--all", action="store_true", help="清理全部(cache+results+logs)")
281
+ clear_p.add_argument("--cache", action="store_true", help="清理缓存")
282
+ clear_p.add_argument("--results", action="store_true", help="清理结果")
283
+ clear_p.add_argument("--logs", action="store_true", help="清理日志")
284
+ clear_p.add_argument("--days", type=int, default=None, help="保留最近N天")
285
+
286
+ args = parser.parse_args()
287
+
288
+ if args.command == "scan":
289
+ _cmd_scan(args)
290
+ elif args.command == "logs":
291
+ _cmd_logs(args)
292
+ elif args.command == "data":
293
+ _cmd_data(args)
294
+ elif args.command == "storage":
295
+ _cmd_storage(args)
296
+ else:
297
+ parser.print_help()
298
+
299
+
300
+ if __name__ == "__main__":
301
+ main()