dtflow 0.4.2__py3-none-any.whl → 0.4.3__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.
- dtflow/__init__.py +1 -1
- dtflow/__main__.py +6 -3
- dtflow/cli/clean.py +486 -0
- dtflow/cli/commands.py +53 -2637
- dtflow/cli/common.py +384 -0
- dtflow/cli/io_ops.py +385 -0
- dtflow/cli/lineage.py +49 -0
- dtflow/cli/pipeline.py +54 -0
- dtflow/cli/sample.py +294 -0
- dtflow/cli/stats.py +589 -0
- dtflow/cli/transform.py +486 -0
- dtflow/core.py +35 -0
- dtflow/storage/io.py +49 -6
- dtflow/streaming.py +25 -4
- {dtflow-0.4.2.dist-info → dtflow-0.4.3.dist-info}/METADATA +12 -1
- dtflow-0.4.3.dist-info/RECORD +33 -0
- dtflow-0.4.2.dist-info/RECORD +0 -25
- {dtflow-0.4.2.dist-info → dtflow-0.4.3.dist-info}/WHEEL +0 -0
- {dtflow-0.4.2.dist-info → dtflow-0.4.3.dist-info}/entry_points.txt +0 -0
dtflow/cli/io_ops.py
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI IO 操作相关命令 (concat, diff)
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import tempfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
import orjson
|
|
12
|
+
|
|
13
|
+
from ..storage.io import load_data, save_data
|
|
14
|
+
from ..streaming import load_stream
|
|
15
|
+
from ..utils.field_path import get_field_with_spec
|
|
16
|
+
from .common import _check_file_format, _is_streaming_supported
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def concat(
|
|
20
|
+
*files: str,
|
|
21
|
+
output: Optional[str] = None,
|
|
22
|
+
strict: bool = False,
|
|
23
|
+
) -> None:
|
|
24
|
+
"""
|
|
25
|
+
拼接多个数据文件(流式处理,内存占用 O(1))。
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
*files: 输入文件路径列表,支持 csv/excel/jsonl/json/parquet/arrow/feather 格式
|
|
29
|
+
output: 输出文件路径,必须指定
|
|
30
|
+
strict: 严格模式,字段必须完全一致,否则报错
|
|
31
|
+
|
|
32
|
+
Examples:
|
|
33
|
+
dt concat a.jsonl b.jsonl -o merged.jsonl
|
|
34
|
+
dt concat data1.csv data2.csv data3.csv -o all.jsonl
|
|
35
|
+
dt concat a.jsonl b.jsonl --strict -o merged.jsonl
|
|
36
|
+
"""
|
|
37
|
+
if len(files) < 2:
|
|
38
|
+
print("错误: 至少需要两个文件")
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
if not output:
|
|
42
|
+
print("错误: 必须指定输出文件 (-o/--output)")
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
# 验证所有文件
|
|
46
|
+
file_paths = []
|
|
47
|
+
for f in files:
|
|
48
|
+
filepath = Path(f).resolve() # 使用绝对路径进行比较
|
|
49
|
+
if not filepath.exists():
|
|
50
|
+
print(f"错误: 文件不存在 - {f}")
|
|
51
|
+
return
|
|
52
|
+
if not _check_file_format(filepath):
|
|
53
|
+
return
|
|
54
|
+
file_paths.append(filepath)
|
|
55
|
+
|
|
56
|
+
# 检查输出文件是否与输入文件冲突
|
|
57
|
+
output_path = Path(output).resolve()
|
|
58
|
+
use_temp_file = output_path in file_paths
|
|
59
|
+
if use_temp_file:
|
|
60
|
+
print("⚠ 检测到输出文件与输入文件相同,将使用临时文件")
|
|
61
|
+
|
|
62
|
+
# 流式分析字段(只读取每个文件的第一行)
|
|
63
|
+
print("📊 文件字段分析:")
|
|
64
|
+
file_fields = [] # [(filepath, fields)]
|
|
65
|
+
|
|
66
|
+
for filepath in file_paths:
|
|
67
|
+
try:
|
|
68
|
+
# 只读取第一行来获取字段(根据格式选择加载方式)
|
|
69
|
+
if _is_streaming_supported(filepath):
|
|
70
|
+
first_row = load_stream(str(filepath)).head(1).collect()
|
|
71
|
+
else:
|
|
72
|
+
# 非流式格式(如 .json, .xlsx)使用全量加载
|
|
73
|
+
data = load_data(str(filepath))
|
|
74
|
+
first_row = data[:1] if data else []
|
|
75
|
+
if not first_row:
|
|
76
|
+
print(f"警告: 文件为空 - {filepath}")
|
|
77
|
+
fields = set()
|
|
78
|
+
else:
|
|
79
|
+
fields = set(first_row[0].keys())
|
|
80
|
+
except Exception as e:
|
|
81
|
+
print(f"错误: 无法读取文件 {filepath} - {e}")
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
file_fields.append((filepath, fields))
|
|
85
|
+
fields_str = ", ".join(sorted(fields)) if fields else "(空)"
|
|
86
|
+
print(f" {filepath.name}: {fields_str}")
|
|
87
|
+
|
|
88
|
+
# 分析字段差异
|
|
89
|
+
all_fields = set()
|
|
90
|
+
common_fields = None
|
|
91
|
+
for _, fields in file_fields:
|
|
92
|
+
all_fields.update(fields)
|
|
93
|
+
if common_fields is None:
|
|
94
|
+
common_fields = fields.copy()
|
|
95
|
+
else:
|
|
96
|
+
common_fields &= fields
|
|
97
|
+
|
|
98
|
+
common_fields = common_fields or set()
|
|
99
|
+
diff_fields = all_fields - common_fields
|
|
100
|
+
|
|
101
|
+
if diff_fields:
|
|
102
|
+
if strict:
|
|
103
|
+
print(f"\n❌ 严格模式: 字段不一致")
|
|
104
|
+
print(f" 共同字段: {', '.join(sorted(common_fields)) or '(无)'}")
|
|
105
|
+
print(f" 差异字段: {', '.join(sorted(diff_fields))}")
|
|
106
|
+
return
|
|
107
|
+
else:
|
|
108
|
+
print(f"\n⚠ 字段差异: {', '.join(sorted(diff_fields))} 仅在部分文件中存在")
|
|
109
|
+
|
|
110
|
+
# 流式拼接
|
|
111
|
+
print("\n🔄 流式拼接...")
|
|
112
|
+
|
|
113
|
+
# 如果输出文件与输入文件冲突,使用临时文件(在输出文件同一目录下)
|
|
114
|
+
if use_temp_file:
|
|
115
|
+
output_dir = output_path.parent
|
|
116
|
+
temp_fd, temp_path = tempfile.mkstemp(
|
|
117
|
+
suffix=output_path.suffix,
|
|
118
|
+
prefix=".tmp_",
|
|
119
|
+
dir=output_dir,
|
|
120
|
+
)
|
|
121
|
+
os.close(temp_fd)
|
|
122
|
+
actual_output = temp_path
|
|
123
|
+
print(f"💾 写入临时文件: {temp_path}")
|
|
124
|
+
else:
|
|
125
|
+
actual_output = output
|
|
126
|
+
print(f"💾 保存结果: {output}")
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
total_count = _concat_streaming(file_paths, actual_output)
|
|
130
|
+
|
|
131
|
+
# 如果使用了临时文件,重命名为目标文件
|
|
132
|
+
if use_temp_file:
|
|
133
|
+
shutil.move(temp_path, output)
|
|
134
|
+
print(f"💾 移动到目标文件: {output}")
|
|
135
|
+
except Exception as e:
|
|
136
|
+
# 清理临时文件
|
|
137
|
+
if use_temp_file and os.path.exists(temp_path):
|
|
138
|
+
os.unlink(temp_path)
|
|
139
|
+
print(f"错误: 拼接失败 - {e}")
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
file_count = len(files)
|
|
143
|
+
print(f"\n✅ 完成! 已合并 {file_count} 个文件,共 {total_count} 条数据到 {output}")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _concat_streaming(file_paths: List[Path], output: str) -> int:
|
|
147
|
+
"""流式拼接多个文件"""
|
|
148
|
+
from ..streaming import (
|
|
149
|
+
StreamingTransformer,
|
|
150
|
+
_stream_arrow,
|
|
151
|
+
_stream_csv,
|
|
152
|
+
_stream_jsonl,
|
|
153
|
+
_stream_parquet,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def generator():
|
|
157
|
+
for filepath in file_paths:
|
|
158
|
+
ext = filepath.suffix.lower()
|
|
159
|
+
if ext == ".jsonl":
|
|
160
|
+
yield from _stream_jsonl(str(filepath))
|
|
161
|
+
elif ext == ".csv":
|
|
162
|
+
yield from _stream_csv(str(filepath))
|
|
163
|
+
elif ext == ".parquet":
|
|
164
|
+
yield from _stream_parquet(str(filepath))
|
|
165
|
+
elif ext in (".arrow", ".feather"):
|
|
166
|
+
yield from _stream_arrow(str(filepath))
|
|
167
|
+
elif ext in (".json",):
|
|
168
|
+
# JSON 需要全量加载
|
|
169
|
+
data = load_data(str(filepath))
|
|
170
|
+
yield from data
|
|
171
|
+
elif ext in (".xlsx", ".xls"):
|
|
172
|
+
# Excel 需要全量加载
|
|
173
|
+
data = load_data(str(filepath))
|
|
174
|
+
yield from data
|
|
175
|
+
else:
|
|
176
|
+
yield from _stream_jsonl(str(filepath))
|
|
177
|
+
|
|
178
|
+
st = StreamingTransformer(generator())
|
|
179
|
+
return st.save(output, show_progress=True)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def diff(
|
|
183
|
+
file1: str,
|
|
184
|
+
file2: str,
|
|
185
|
+
key: Optional[str] = None,
|
|
186
|
+
output: Optional[str] = None,
|
|
187
|
+
) -> None:
|
|
188
|
+
"""
|
|
189
|
+
对比两个数据集的差异。
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
file1: 第一个文件路径
|
|
193
|
+
file2: 第二个文件路径
|
|
194
|
+
key: 用于匹配的键字段,支持嵌套路径语法(可选)
|
|
195
|
+
output: 差异报告输出路径(可选)
|
|
196
|
+
|
|
197
|
+
Examples:
|
|
198
|
+
dt diff v1/train.jsonl v2/train.jsonl
|
|
199
|
+
dt diff a.jsonl b.jsonl --key=id
|
|
200
|
+
dt diff a.jsonl b.jsonl --key=meta.uuid # 按嵌套字段匹配
|
|
201
|
+
dt diff a.jsonl b.jsonl --output=diff_report.json
|
|
202
|
+
"""
|
|
203
|
+
path1 = Path(file1)
|
|
204
|
+
path2 = Path(file2)
|
|
205
|
+
|
|
206
|
+
# 验证文件
|
|
207
|
+
for p, name in [(path1, "file1"), (path2, "file2")]:
|
|
208
|
+
if not p.exists():
|
|
209
|
+
print(f"错误: 文件不存在 - {p}")
|
|
210
|
+
return
|
|
211
|
+
if not _check_file_format(p):
|
|
212
|
+
return
|
|
213
|
+
|
|
214
|
+
# 加载数据
|
|
215
|
+
print(f"📊 加载数据...")
|
|
216
|
+
try:
|
|
217
|
+
data1 = load_data(str(path1))
|
|
218
|
+
data2 = load_data(str(path2))
|
|
219
|
+
except Exception as e:
|
|
220
|
+
print(f"错误: 无法读取文件 - {e}")
|
|
221
|
+
return
|
|
222
|
+
|
|
223
|
+
print(f" 文件1: {path1.name} ({len(data1)} 条)")
|
|
224
|
+
print(f" 文件2: {path2.name} ({len(data2)} 条)")
|
|
225
|
+
|
|
226
|
+
# 计算差异
|
|
227
|
+
print("🔍 计算差异...")
|
|
228
|
+
diff_result = _compute_diff(data1, data2, key)
|
|
229
|
+
|
|
230
|
+
# 打印差异报告
|
|
231
|
+
_print_diff_report(diff_result, path1.name, path2.name)
|
|
232
|
+
|
|
233
|
+
# 保存报告
|
|
234
|
+
if output:
|
|
235
|
+
print(f"\n💾 保存报告: {output}")
|
|
236
|
+
save_data([diff_result], output)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _compute_diff(
|
|
240
|
+
data1: List[Dict],
|
|
241
|
+
data2: List[Dict],
|
|
242
|
+
key: Optional[str] = None,
|
|
243
|
+
) -> Dict[str, Any]:
|
|
244
|
+
"""计算两个数据集的差异"""
|
|
245
|
+
result = {
|
|
246
|
+
"summary": {
|
|
247
|
+
"file1_count": len(data1),
|
|
248
|
+
"file2_count": len(data2),
|
|
249
|
+
"added": 0,
|
|
250
|
+
"removed": 0,
|
|
251
|
+
"modified": 0,
|
|
252
|
+
"unchanged": 0,
|
|
253
|
+
},
|
|
254
|
+
"field_changes": {},
|
|
255
|
+
"details": {
|
|
256
|
+
"added": [],
|
|
257
|
+
"removed": [],
|
|
258
|
+
"modified": [],
|
|
259
|
+
},
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if key:
|
|
263
|
+
# 基于 key 的精确匹配(支持嵌套路径)
|
|
264
|
+
dict1 = {get_field_with_spec(item, key): item for item in data1 if get_field_with_spec(item, key) is not None}
|
|
265
|
+
dict2 = {get_field_with_spec(item, key): item for item in data2 if get_field_with_spec(item, key) is not None}
|
|
266
|
+
|
|
267
|
+
keys1 = set(dict1.keys())
|
|
268
|
+
keys2 = set(dict2.keys())
|
|
269
|
+
|
|
270
|
+
# 新增
|
|
271
|
+
added_keys = keys2 - keys1
|
|
272
|
+
result["summary"]["added"] = len(added_keys)
|
|
273
|
+
result["details"]["added"] = [dict2[k] for k in list(added_keys)[:10]] # 最多显示 10 条
|
|
274
|
+
|
|
275
|
+
# 删除
|
|
276
|
+
removed_keys = keys1 - keys2
|
|
277
|
+
result["summary"]["removed"] = len(removed_keys)
|
|
278
|
+
result["details"]["removed"] = [dict1[k] for k in list(removed_keys)[:10]]
|
|
279
|
+
|
|
280
|
+
# 修改/未变
|
|
281
|
+
common_keys = keys1 & keys2
|
|
282
|
+
for k in common_keys:
|
|
283
|
+
if dict1[k] == dict2[k]:
|
|
284
|
+
result["summary"]["unchanged"] += 1
|
|
285
|
+
else:
|
|
286
|
+
result["summary"]["modified"] += 1
|
|
287
|
+
if len(result["details"]["modified"]) < 10:
|
|
288
|
+
result["details"]["modified"].append(
|
|
289
|
+
{
|
|
290
|
+
"key": k,
|
|
291
|
+
"before": dict1[k],
|
|
292
|
+
"after": dict2[k],
|
|
293
|
+
}
|
|
294
|
+
)
|
|
295
|
+
else:
|
|
296
|
+
# 基于哈希的比较
|
|
297
|
+
def _hash_item(item):
|
|
298
|
+
return orjson.dumps(item, option=orjson.OPT_SORT_KEYS)
|
|
299
|
+
|
|
300
|
+
set1 = {_hash_item(item) for item in data1}
|
|
301
|
+
set2 = {_hash_item(item) for item in data2}
|
|
302
|
+
|
|
303
|
+
added = set2 - set1
|
|
304
|
+
removed = set1 - set2
|
|
305
|
+
unchanged = set1 & set2
|
|
306
|
+
|
|
307
|
+
result["summary"]["added"] = len(added)
|
|
308
|
+
result["summary"]["removed"] = len(removed)
|
|
309
|
+
result["summary"]["unchanged"] = len(unchanged)
|
|
310
|
+
|
|
311
|
+
# 详情
|
|
312
|
+
result["details"]["added"] = [orjson.loads(h) for h in list(added)[:10]]
|
|
313
|
+
result["details"]["removed"] = [orjson.loads(h) for h in list(removed)[:10]]
|
|
314
|
+
|
|
315
|
+
# 字段变化分析
|
|
316
|
+
fields1 = set()
|
|
317
|
+
fields2 = set()
|
|
318
|
+
for item in data1[:1000]: # 采样分析
|
|
319
|
+
fields1.update(item.keys())
|
|
320
|
+
for item in data2[:1000]:
|
|
321
|
+
fields2.update(item.keys())
|
|
322
|
+
|
|
323
|
+
result["field_changes"] = {
|
|
324
|
+
"added_fields": list(fields2 - fields1),
|
|
325
|
+
"removed_fields": list(fields1 - fields2),
|
|
326
|
+
"common_fields": list(fields1 & fields2),
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return result
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _print_diff_report(diff_result: Dict[str, Any], name1: str, name2: str) -> None:
|
|
333
|
+
"""打印差异报告"""
|
|
334
|
+
summary = diff_result["summary"]
|
|
335
|
+
field_changes = diff_result["field_changes"]
|
|
336
|
+
|
|
337
|
+
try:
|
|
338
|
+
from rich.console import Console
|
|
339
|
+
from rich.panel import Panel
|
|
340
|
+
from rich.table import Table
|
|
341
|
+
|
|
342
|
+
console = Console()
|
|
343
|
+
|
|
344
|
+
# 概览
|
|
345
|
+
overview = (
|
|
346
|
+
f"[bold]{name1}:[/bold] {summary['file1_count']:,} 条\n"
|
|
347
|
+
f"[bold]{name2}:[/bold] {summary['file2_count']:,} 条\n"
|
|
348
|
+
f"\n"
|
|
349
|
+
f"[green]+ 新增:[/green] {summary['added']:,} 条\n"
|
|
350
|
+
f"[red]- 删除:[/red] {summary['removed']:,} 条\n"
|
|
351
|
+
f"[yellow]~ 修改:[/yellow] {summary['modified']:,} 条\n"
|
|
352
|
+
f"[dim]= 未变:[/dim] {summary['unchanged']:,} 条"
|
|
353
|
+
)
|
|
354
|
+
console.print(Panel(overview, title="📊 差异概览", expand=False))
|
|
355
|
+
|
|
356
|
+
# 字段变化
|
|
357
|
+
if field_changes["added_fields"] or field_changes["removed_fields"]:
|
|
358
|
+
console.print("\n[bold]📋 字段变化:[/bold]")
|
|
359
|
+
if field_changes["added_fields"]:
|
|
360
|
+
console.print(
|
|
361
|
+
f" [green]+ 新增字段:[/green] {', '.join(field_changes['added_fields'])}"
|
|
362
|
+
)
|
|
363
|
+
if field_changes["removed_fields"]:
|
|
364
|
+
console.print(
|
|
365
|
+
f" [red]- 删除字段:[/red] {', '.join(field_changes['removed_fields'])}"
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
except ImportError:
|
|
369
|
+
print(f"\n{'=' * 50}")
|
|
370
|
+
print("📊 差异概览")
|
|
371
|
+
print(f"{'=' * 50}")
|
|
372
|
+
print(f"{name1}: {summary['file1_count']:,} 条")
|
|
373
|
+
print(f"{name2}: {summary['file2_count']:,} 条")
|
|
374
|
+
print()
|
|
375
|
+
print(f"+ 新增: {summary['added']:,} 条")
|
|
376
|
+
print(f"- 删除: {summary['removed']:,} 条")
|
|
377
|
+
print(f"~ 修改: {summary['modified']:,} 条")
|
|
378
|
+
print(f"= 未变: {summary['unchanged']:,} 条")
|
|
379
|
+
|
|
380
|
+
if field_changes["added_fields"] or field_changes["removed_fields"]:
|
|
381
|
+
print(f"\n📋 字段变化:")
|
|
382
|
+
if field_changes["added_fields"]:
|
|
383
|
+
print(f" + 新增字段: {', '.join(field_changes['added_fields'])}")
|
|
384
|
+
if field_changes["removed_fields"]:
|
|
385
|
+
print(f" - 删除字段: {', '.join(field_changes['removed_fields'])}")
|
dtflow/cli/lineage.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI 数据血缘追踪命令
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import orjson
|
|
8
|
+
|
|
9
|
+
from ..lineage import format_lineage_report, get_lineage_chain, has_lineage
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def history(
|
|
13
|
+
filename: str,
|
|
14
|
+
json: bool = False,
|
|
15
|
+
) -> None:
|
|
16
|
+
"""
|
|
17
|
+
显示数据文件的血缘历史。
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
filename: 数据文件路径
|
|
21
|
+
json: 以 JSON 格式输出
|
|
22
|
+
|
|
23
|
+
Examples:
|
|
24
|
+
dt history data.jsonl
|
|
25
|
+
dt history data.jsonl --json
|
|
26
|
+
"""
|
|
27
|
+
filepath = Path(filename)
|
|
28
|
+
|
|
29
|
+
if not filepath.exists():
|
|
30
|
+
print(f"错误: 文件不存在 - {filename}")
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
if not has_lineage(str(filepath)):
|
|
34
|
+
print(f"文件 {filename} 没有血缘记录")
|
|
35
|
+
print("\n提示: 使用 track_lineage=True 加载数据,并在保存时使用 lineage=True 来记录血缘")
|
|
36
|
+
print("示例:")
|
|
37
|
+
print(" dt = DataTransformer.load('data.jsonl', track_lineage=True)")
|
|
38
|
+
print(" dt.filter(...).transform(...).save('output.jsonl', lineage=True)")
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
if json:
|
|
42
|
+
# JSON 格式输出
|
|
43
|
+
chain = get_lineage_chain(str(filepath))
|
|
44
|
+
output = [record.to_dict() for record in chain]
|
|
45
|
+
print(orjson.dumps(output, option=orjson.OPT_INDENT_2).decode("utf-8"))
|
|
46
|
+
else:
|
|
47
|
+
# 格式化报告
|
|
48
|
+
report = format_lineage_report(str(filepath))
|
|
49
|
+
print(report)
|
dtflow/cli/pipeline.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI Pipeline 执行命令
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from ..pipeline import run_pipeline, validate_pipeline
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def run(
|
|
12
|
+
config: str,
|
|
13
|
+
input: Optional[str] = None,
|
|
14
|
+
output: Optional[str] = None,
|
|
15
|
+
) -> None:
|
|
16
|
+
"""
|
|
17
|
+
执行 Pipeline 配置文件。
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
config: Pipeline YAML 配置文件路径
|
|
21
|
+
input: 输入文件路径(覆盖配置中的 input)
|
|
22
|
+
output: 输出文件路径(覆盖配置中的 output)
|
|
23
|
+
|
|
24
|
+
Examples:
|
|
25
|
+
dt run pipeline.yaml
|
|
26
|
+
dt run pipeline.yaml --input=new_data.jsonl
|
|
27
|
+
dt run pipeline.yaml --input=data.jsonl --output=result.jsonl
|
|
28
|
+
"""
|
|
29
|
+
config_path = Path(config)
|
|
30
|
+
|
|
31
|
+
if not config_path.exists():
|
|
32
|
+
print(f"错误: 配置文件不存在 - {config}")
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
if config_path.suffix.lower() not in (".yaml", ".yml"):
|
|
36
|
+
print(f"错误: 配置文件必须是 YAML 格式 (.yaml 或 .yml)")
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
# 验证配置
|
|
40
|
+
errors = validate_pipeline(config)
|
|
41
|
+
if errors:
|
|
42
|
+
print("❌ 配置文件验证失败:")
|
|
43
|
+
for err in errors:
|
|
44
|
+
print(f" - {err}")
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
# 执行 pipeline
|
|
48
|
+
try:
|
|
49
|
+
run_pipeline(config, input_file=input, output_file=output, verbose=True)
|
|
50
|
+
except Exception as e:
|
|
51
|
+
print(f"错误: {e}")
|
|
52
|
+
import traceback
|
|
53
|
+
|
|
54
|
+
traceback.print_exc()
|