bitool 0.1.2__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 (51) hide show
  1. bitool/__init__.py +27 -0
  2. bitool/cmd/__init__.py +65 -0
  3. bitool/cmd/_base.py +105 -0
  4. bitool/cmd/_condition.py +60 -0
  5. bitool/cmd/_scheduler.py +548 -0
  6. bitool/cmd/env.py +454 -0
  7. bitool/cmd/git.py +123 -0
  8. bitool/cmd/io.py +248 -0
  9. bitool/cmd/pdf.py +385 -0
  10. bitool/cmd/run.py +300 -0
  11. bitool/cmd/toml.py +237 -0
  12. bitool/cmd/version.py +630 -0
  13. bitool/consts.py +14 -0
  14. bitool/core/__init__.py +7 -0
  15. bitool/core/app.py +142 -0
  16. bitool/core/commands.py +194 -0
  17. bitool/core/config.py +647 -0
  18. bitool/core/env.py +18 -0
  19. bitool/core/logger.py +237 -0
  20. bitool/core/plugin.py +117 -0
  21. bitool/core/workspace.py +76 -0
  22. bitool/models/__init__.py +3 -0
  23. bitool/models/version.py +173 -0
  24. bitool/scripts/__init__.py +1 -0
  25. bitool/scripts/bumpversion.py +189 -0
  26. bitool/scripts/clearscreen.py +37 -0
  27. bitool/scripts/envpy.py +161 -0
  28. bitool/scripts/envrs.py +119 -0
  29. bitool/scripts/filedate.py +246 -0
  30. bitool/scripts/filelevel.py +191 -0
  31. bitool/scripts/gittool.py +178 -0
  32. bitool/scripts/img2pdf.py +151 -0
  33. bitool/scripts/pdf2img.py +139 -0
  34. bitool/scripts/piptool.py +130 -0
  35. bitool/scripts/pymake.py +345 -0
  36. bitool/scripts/sshcopyid.py +491 -0
  37. bitool/scripts/taskkill.py +366 -0
  38. bitool/scripts/which.py +227 -0
  39. bitool/types.py +7 -0
  40. bitool/utils/__init__.py +9 -0
  41. bitool/utils/cli_parser.py +412 -0
  42. bitool/utils/executor.py +881 -0
  43. bitool/utils/profiler.py +369 -0
  44. bitool/utils/task.py +133 -0
  45. bitool/utils/task_group.py +668 -0
  46. bitool/utils/tests/__init__.py +0 -0
  47. bitool/utils/tests/test_profiler.py +487 -0
  48. bitool-0.1.2.dist-info/METADATA +154 -0
  49. bitool-0.1.2.dist-info/RECORD +51 -0
  50. bitool-0.1.2.dist-info/WHEEL +4 -0
  51. bitool-0.1.2.dist-info/entry_points.txt +15 -0
@@ -0,0 +1,668 @@
1
+ """任务编排模块 - 用于定义和管理任务流.
2
+
3
+ 本模块提供通用的任务组数据结构,允许通过简单的配置
4
+ 来定义复杂的命令执行流程.
5
+
6
+ 核心功能
7
+ --------
8
+ - 声明式任务组定义
9
+ - 支持命令列表和可调用对象
10
+ - 支持顺序执行和并行执行
11
+ - 灵活的条件执行控制
12
+ - 任务依赖管理 (DAG调度)
13
+
14
+ 快速开始
15
+ --------
16
+ >>> from pytola_core.command.task_group import TaskGroup, Task
17
+ >>> group = TaskGroup(
18
+ ... name="git-init",
19
+ ... commands=[
20
+ ... Task(cmd=["git", "init"]),
21
+ ... Task(cmd=["git", "add", "."]),
22
+ ... Task(cmd=["git", "commit", "-m", "initial commit"]),
23
+ ... ]
24
+ ... )
25
+ >>> group.execute()
26
+
27
+ DAG 依赖调度
28
+ -----------
29
+ >>> # 定义带依赖关系的任务流
30
+ >>> group = TaskGroup(
31
+ ... name="build-pipeline",
32
+ ... commands=[
33
+ ... Task(name="init", cmd=["git", "init"]),
34
+ ... Task(name="install", cmd=["pip", "install", "-r", "requirements.txt"],
35
+ ... depends_on=["init"]),
36
+ ... Task(name="lint", cmd=["ruff", "check", "."], depends_on=["install"]),
37
+ ... Task(name="test", cmd=["pytest"], depends_on=["install"]),
38
+ ... Task(name="build", cmd=["python", "-m", "build"],
39
+ ... depends_on=["lint", "test"]),
40
+ ... ],
41
+ ... max_workers=4,
42
+ ... )
43
+ >>> # lint 和 test 将并行执行(都依赖 install,都被 build 依赖)
44
+ >>> group.execute()
45
+ """
46
+
47
+ from __future__ import annotations
48
+
49
+ from dataclasses import dataclass, field
50
+ from typing import Any, Callable
51
+
52
+ from bitool.utils import execute
53
+
54
+ from ..core import logger
55
+ from .executor import RunResult
56
+ from .task import Task
57
+
58
+
59
+ @dataclass
60
+ class TaskGroup:
61
+ """任务组 - 用于组织和管理相关任务.
62
+
63
+ Attributes
64
+ ----------
65
+ name : str
66
+ 任务组名称
67
+ commands : list[Task]
68
+ 任务项列表
69
+ description : str
70
+ 任务组描述
71
+ enabled : bool
72
+ 是否启用此任务组
73
+ parallel : bool
74
+ 是否启用并行执行,默认 False (顺序执行)
75
+ max_workers : int | None
76
+ 最大并行工作线程数,None 表示使用默认值
77
+ stop_on_failure : bool
78
+ 失败时是否停止执行,默认 False
79
+ """
80
+
81
+ name: str
82
+ commands: list[Task] = field(default_factory=list)
83
+ description: str = ""
84
+ enabled: bool = True
85
+ parallel: bool = False
86
+ max_workers: int | None = None
87
+ stop_on_failure: bool = False
88
+
89
+ def add_command(
90
+ self,
91
+ cmd: list[str] | None = None,
92
+ func: Callable[[], RunResult] | None = None,
93
+ condition: Callable[[], bool] | None = None,
94
+ description: str = "",
95
+ *,
96
+ parallel: bool = False,
97
+ name: str = "",
98
+ depends_on: list[str] | None = None,
99
+ ) -> TaskGroup:
100
+ """添加任务项.
101
+
102
+ Parameters
103
+ ----------
104
+ cmd : list[str], optional
105
+ 命令及其参数列表
106
+ func : Callable[[], RunResult], optional
107
+ 可执行的函数
108
+ condition : Callable[[], bool], optional
109
+ 执行条件函数
110
+ description : str
111
+ 任务描述
112
+ parallel : bool
113
+ 是否允许并行执行
114
+ name : str
115
+ 任务唯一标识,用于依赖引用
116
+ depends_on : list[str], optional
117
+ 依赖的任务名称列表
118
+
119
+ Returns
120
+ -------
121
+ TaskGroup
122
+ 返回自身以支持链式调用
123
+ """
124
+ if depends_on is None:
125
+ depends_on = []
126
+ self.commands.append(
127
+ Task(
128
+ cmd=cmd,
129
+ func=func,
130
+ condition=condition,
131
+ description=description,
132
+ parallel=parallel,
133
+ name=name,
134
+ depends_on=depends_on,
135
+ )
136
+ )
137
+ return self
138
+
139
+ def add_commands(self, commands: list[Task]) -> TaskGroup:
140
+ """批量添加任务项.
141
+
142
+ Parameters
143
+ ----------
144
+ commands : list[Task]
145
+ 任务项列表
146
+
147
+ Returns
148
+ -------
149
+ TaskGroup
150
+ 返回自身以支持链式调用
151
+ """
152
+ self.commands.extend(commands)
153
+ return self
154
+
155
+ def execute(self) -> list[RunResult]:
156
+ """执行任务组.
157
+
158
+ Returns
159
+ -------
160
+ list[RunResult]
161
+ 任务执行结果列表
162
+ """
163
+ if not self.enabled:
164
+ logger.info(f"任务组 '{self.name}' 已禁用,跳过执行")
165
+ return []
166
+
167
+ if not self.commands:
168
+ logger.warning(f"任务组 '{self.name}' 没有可执行的任务")
169
+ return []
170
+
171
+ logger.info(f"开始执行任务组: '{self.name}' ({len(self.commands)} 个任务)")
172
+
173
+ # 过滤需要执行的任务
174
+ executable_commands = []
175
+ for item in self.commands:
176
+ if item.is_executable:
177
+ exec_item = item.executable_cmd
178
+ if exec_item is not None:
179
+ executable_commands.append((item, exec_item))
180
+ else:
181
+ logger.debug(
182
+ f"跳过任务 (条件不满足): {item.description or item.cmd or item.func}"
183
+ )
184
+
185
+ if not executable_commands:
186
+ logger.info(f"任务组 '{self.name}' 没有满足条件的任务")
187
+ return []
188
+
189
+ # 智能选择执行策略
190
+ has_dependencies = any(item.depends_on for item, _ in executable_commands)
191
+
192
+ if has_dependencies:
193
+ # 使用 DAG 调度
194
+ logger.info(f"任务组 '{self.name}' 使用 DAG 依赖调度")
195
+ results = self._execute_with_dag(executable_commands)
196
+ elif self.parallel:
197
+ # 使用原有并行逻辑
198
+ results = self._execute_parallel(executable_commands)
199
+ else:
200
+ # 使用原有串行逻辑
201
+ results = self._execute_sequential(executable_commands)
202
+
203
+ # 统计执行结果
204
+ success_count = sum(1 for r in results if r.success)
205
+ total_count = len(results)
206
+
207
+ logger.info(
208
+ f"任务组 '{self.name}' 执行完成 | "
209
+ f"总数:{total_count} | 成功:{success_count} | 失败:{total_count - success_count}"
210
+ )
211
+
212
+ return results
213
+
214
+ def _execute_sequential(
215
+ self, commands: list[tuple[Task, list[str] | Callable[[], RunResult]]]
216
+ ) -> list[RunResult]:
217
+ """顺序执行任务.
218
+
219
+ Parameters
220
+ ----------
221
+ commands : list[tuple[Task, list[str] | Callable[[], RunResult]]]
222
+ 任务项和可执行对象的元组列表
223
+
224
+ Returns
225
+ -------
226
+ list[RunResult]
227
+ 任务执行结果列表
228
+ """
229
+ results = []
230
+ for item, _executable in commands:
231
+ # 优先使用 description,其次使用命令列表,最后使用函数名
232
+ if item.description:
233
+ task_desc = item.description
234
+ elif item.cmd:
235
+ task_desc = " ".join(item.cmd)
236
+ elif item.func and hasattr(item.func, "__name__"):
237
+ task_desc = item.func.__name__
238
+ else:
239
+ task_desc = str(item.func) if item.func else "unknown"
240
+
241
+ logger.info(f"执行: `{task_desc}`")
242
+
243
+ # 执行单个任务:通过 cmd/func 属性明确区分任务类型
244
+ if item.cmd is not None:
245
+ result = execute(item.cmd)
246
+ elif item.func is not None:
247
+ result = item.func()
248
+ else:
249
+ msg = "任务未定义 cmd 或 func"
250
+ raise ValueError(msg)
251
+ results.append(result)
252
+
253
+ # 检查是否需要停止
254
+ if not result.success and self.stop_on_failure:
255
+ logger.warning(f"任务失败,停止执行: `{task_desc}`")
256
+ break
257
+
258
+ return results
259
+
260
+ def _find_command(
261
+ self,
262
+ commands: list[tuple[Task, list[str] | Callable[[], RunResult]]],
263
+ identifier: str | int,
264
+ ) -> tuple[Task, list[str] | Callable[[], RunResult]]:
265
+ """根据标识查找任务.
266
+
267
+ Parameters
268
+ ----------
269
+ commands : list[tuple[Task, list[str] | Callable[[], RunResult]]]
270
+ 任务项和可执行对象的元组列表
271
+ identifier : str | int
272
+ 任务名称或ID
273
+
274
+ Returns
275
+ -------
276
+ tuple[Task, list[str] | Callable[[], RunResult]]
277
+ 任务项和可执行对象的元组
278
+ """
279
+ for item, executable in commands:
280
+ if (item.name or id(item)) == identifier:
281
+ return item, executable
282
+ msg = f"找不到任务: {identifier}"
283
+ raise ValueError(msg)
284
+
285
+ def _update_dependents(
286
+ self,
287
+ name: str | int,
288
+ graph: dict[str | int, list[str | int]],
289
+ in_degree: dict[str | int, int],
290
+ ready_queue: list[str | int],
291
+ ) -> None:
292
+ """更新依赖节点,将入度为0的加入就绪队列.
293
+
294
+ Parameters
295
+ ----------
296
+ name : str | int
297
+ 已完成的任务ID
298
+ graph : dict[str | int, list[str | int]]
299
+ 依赖图邻接表
300
+ in_degree : dict[str | int, int]
301
+ 各节点入度
302
+ ready_queue : list[str | int]
303
+ 就绪队列
304
+ """
305
+ for dependent_id in graph.get(name, []):
306
+ in_degree[dependent_id] -= 1
307
+ if in_degree[dependent_id] == 0:
308
+ ready_queue.append(dependent_id)
309
+
310
+ def _build_dependency_graph(
311
+ self, commands: list[tuple[Task, list[str] | Callable[[], RunResult]]]
312
+ ) -> tuple[dict[str | int, list[str | int]], dict[str | int, int]]:
313
+ """构建依赖图,检测循环依赖.
314
+
315
+ Parameters
316
+ ----------
317
+ commands : list[tuple[Task, list[str] | Callable[[], RunResult]]]
318
+ 任务项和可执行对象的元组列表
319
+
320
+ Returns
321
+ -------
322
+ tuple[dict[str | int, list[str | int]], dict[str | int, int]]
323
+ 依赖图邻接表和入度表
324
+
325
+ Raises
326
+ ------
327
+ ValueError
328
+ 如果存在循环依赖或引用不存在的任务名称
329
+ """
330
+ graph: dict[str | int, list[str | int]] = {}
331
+ in_degree: dict[str | int, int] = {}
332
+
333
+ # 初始化图
334
+ for item, _ in commands:
335
+ name = item.name or id(item)
336
+ graph[name] = []
337
+ in_degree[name] = len(item.depends_on)
338
+
339
+ # 建立邻接表
340
+ for item, _ in commands:
341
+ name = item.name or id(item)
342
+ for dep_id in item.depends_on:
343
+ # 查找依赖的ID
344
+ dep_found = False
345
+ for dep_item, _ in commands:
346
+ if dep_item.name == dep_id:
347
+ dep_found = True
348
+ graph[dep_id].append(name)
349
+ break
350
+
351
+ if not dep_found:
352
+ msg = f"依赖的任务名称不存在: `{dep_id}`"
353
+ raise ValueError(msg)
354
+
355
+ # 检测循环依赖
356
+ if self._detect_cycle(graph):
357
+ msg = "检测到循环依赖,请检查任务的 depends_on 配置"
358
+ raise ValueError(msg)
359
+
360
+ return graph, in_degree
361
+
362
+ def _detect_cycle(self, graph: dict[str | int, list[str | int]]) -> bool:
363
+ """检测循环依赖.
364
+
365
+ Parameters
366
+ ----------
367
+ graph : dict[str | int, list[str | int]]
368
+ 依赖图邻接表
369
+
370
+ Returns
371
+ -------
372
+ bool
373
+ 如果存在循环依赖返回True
374
+ """
375
+ visited: set[str | int] = set()
376
+ rec_stack: set[str | int] = set()
377
+
378
+ def dfs(node: str | int) -> bool:
379
+ visited.add(node)
380
+ rec_stack.add(node)
381
+
382
+ for neighbor in graph.get(node, []):
383
+ if neighbor not in visited:
384
+ if dfs(neighbor):
385
+ return True
386
+ elif neighbor in rec_stack:
387
+ return True
388
+
389
+ rec_stack.remove(node)
390
+ return False
391
+
392
+ return any(node not in visited and dfs(node) for node in graph)
393
+
394
+ def _execute_parallel(
395
+ self, commands: list[tuple[Task, list[str] | Callable[[], RunResult]]]
396
+ ) -> list[RunResult]:
397
+ """并行执行任务.
398
+
399
+ Parameters
400
+ ----------
401
+ commands : list[tuple[Task, list[str] | Callable[[], RunResult]]]
402
+ 任务项和可执行对象的元组列表
403
+
404
+ Returns
405
+ -------
406
+ list[RunResult]
407
+ 任务执行结果列表
408
+ """
409
+ from concurrent.futures import ThreadPoolExecutor, as_completed
410
+
411
+ # 分离顺序和并行任务
412
+ sequential_commands = [
413
+ (item, exec_cmd) for item, exec_cmd in commands if not item.parallel
414
+ ]
415
+ parallel_commands = [
416
+ (item, exec_cmd) for item, exec_cmd in commands if item.parallel
417
+ ]
418
+
419
+ results = []
420
+
421
+ # 先执行顺序任务
422
+ if sequential_commands:
423
+ logger.info(f"顺序执行 {len(sequential_commands)} 个任务")
424
+ seq_results = self._execute_sequential(sequential_commands)
425
+ results.extend(seq_results)
426
+
427
+ # 如果失败且设置了停止标志,则不执行并行任务
428
+ if self.stop_on_failure and any(not r.success for r in seq_results):
429
+ logger.warning("顺序任务失败,跳过并行任务")
430
+ return results
431
+
432
+ # 并行执行任务
433
+ if parallel_commands:
434
+ logger.info(f"并行执行 {len(parallel_commands)} 个任务")
435
+ max_workers = self.max_workers if self.max_workers is not None else 4
436
+
437
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
438
+ future_to_item = {}
439
+ for item, _ in parallel_commands:
440
+ # 通过 cmd/func 属性明确区分任务类型
441
+ if item.cmd is not None:
442
+ from bitool.utils.executor import execute
443
+
444
+ future = executor.submit(execute, item.cmd)
445
+ elif item.func is not None:
446
+ future = executor.submit(item.func)
447
+ else:
448
+ continue
449
+ future_to_item[future] = item
450
+
451
+ # 收集结果
452
+ for future in as_completed(future_to_item):
453
+ item = future_to_item[future]
454
+ try:
455
+ result = future.result()
456
+ results.append(result)
457
+ # 优先使用 description,其次使用命令列表,最后使用函数名
458
+ if item.description:
459
+ task_desc = item.description
460
+ elif item.cmd:
461
+ task_desc = " ".join(item.cmd)
462
+ elif item.func and hasattr(item.func, "__name__"):
463
+ task_desc = item.func.__name__
464
+ else:
465
+ task_desc = str(item.func) if item.func else "unknown"
466
+ logger.info(f"完成: {task_desc}")
467
+ except (OSError, RuntimeError, ValueError) as e:
468
+ if item.description:
469
+ task_desc = item.description
470
+ elif item.cmd:
471
+ task_desc = " ".join(item.cmd)
472
+ elif item.func and hasattr(item.func, "__name__"):
473
+ task_desc = item.func.__name__
474
+ else:
475
+ task_desc = str(item.func) if item.func else "unknown"
476
+ logger.error(f"并行任务异常: {task_desc}, 错误:{e}")
477
+ results.append(
478
+ RunResult(
479
+ returncode=-1,
480
+ stdout="",
481
+ stderr=str(e),
482
+ execution_time=0.0,
483
+ mode="failed",
484
+ cmd=item.cmd if item.cmd else [],
485
+ )
486
+ )
487
+
488
+ return results
489
+
490
+ def _execute_with_dag(
491
+ self, commands: list[tuple[Task, list[str] | Callable[[], RunResult]]]
492
+ ) -> list[RunResult]:
493
+ """基于 DAG 的智能调度执行.
494
+
495
+ 根据任务间的依赖关系,自动识别可并行执行的任务,
496
+ 并按依赖顺序调度执行.
497
+
498
+ Parameters
499
+ ----------
500
+ commands : list[tuple[Task, list[str] | Callable[[], RunResult]]]
501
+ 任务项和可执行对象的元组列表
502
+
503
+ Returns
504
+ -------
505
+ list[RunResult]
506
+ 任务执行结果列表
507
+ """
508
+ from concurrent.futures import ThreadPoolExecutor, as_completed
509
+
510
+ graph, in_degree = self._build_dependency_graph(commands)
511
+
512
+ # 找到所有入度为0的节点(无依赖)
513
+ ready_queue: list[str | int] = [
514
+ name for name, degree in in_degree.items() if degree == 0
515
+ ]
516
+
517
+ results: dict[str | int, RunResult] = {}
518
+ stopped = False
519
+
520
+ while ready_queue and not stopped:
521
+ # 当前批次可以并行执行的任务
522
+ current_batch = ready_queue[:]
523
+ ready_queue.clear()
524
+
525
+ # 并行执行当前批次
526
+ max_workers = (
527
+ self.max_workers if self.max_workers is not None else len(current_batch)
528
+ )
529
+
530
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
531
+ future_to_cmd: dict[Any, str | int] = {}
532
+
533
+ for name in current_batch:
534
+ item, _ = self._find_command(commands, name)
535
+
536
+ # 检查条件
537
+ if not item.is_executable:
538
+ logger.debug(
539
+ f"跳过任务 (条件不满足): {item.description or name}"
540
+ )
541
+ # 标记为已完成,更新依赖
542
+ self._update_dependents(name, graph, in_degree, ready_queue)
543
+ continue
544
+
545
+ logger.info(f"DAG调度执行: {item.description or name}")
546
+
547
+ # 提交任务
548
+ if item.cmd is not None:
549
+ from bitool.utils.executor import execute
550
+
551
+ future = executor.submit(execute, item.cmd)
552
+ elif item.func is not None:
553
+ future = executor.submit(item.func)
554
+ else:
555
+ continue
556
+
557
+ future_to_cmd[future] = name
558
+
559
+ # 收集结果
560
+ for future in as_completed(future_to_cmd):
561
+ name = future_to_cmd[future]
562
+ item, _ = self._find_command(commands, name)
563
+
564
+ try:
565
+ result = future.result()
566
+ results[name] = result
567
+ task_desc = item.description or name
568
+ logger.info(f"DAG完成: {task_desc}")
569
+
570
+ # 检查失败策略
571
+ if not result.success and self.stop_on_failure:
572
+ task_desc = item.description or name
573
+ logger.warning(f"任务失败,停止执行: {task_desc}")
574
+ stopped = True
575
+ # 不再处理剩余任务
576
+ break
577
+
578
+ except (OSError, RuntimeError, ValueError) as e:
579
+ task_desc = item.description or name
580
+ logger.error(f"DAG任务异常: {task_desc}, 错误:{e}")
581
+ results[name] = RunResult(
582
+ returncode=-1,
583
+ stdout="",
584
+ stderr=str(e),
585
+ execution_time=0.0,
586
+ mode="failed",
587
+ cmd=item.cmd if item.cmd else [],
588
+ )
589
+ stopped = self.stop_on_failure
590
+
591
+ # 更新依赖(除非已停止)
592
+ if not stopped:
593
+ self._update_dependents(name, graph, in_degree, ready_queue)
594
+
595
+ return list(results.values())
596
+
597
+ @classmethod
598
+ def from_dict(cls, config: dict[str, Any]) -> TaskGroup:
599
+ """从字典配置创建任务组.
600
+
601
+ Parameters
602
+ ----------
603
+ config : dict[str, Any]
604
+ 任务组配置字典
605
+
606
+ Returns
607
+ -------
608
+ TaskGroup
609
+ 任务组实例
610
+ """
611
+ name = config.get("name", "unnamed-group")
612
+ description = config.get("description", "")
613
+ enabled = config.get("enabled", True)
614
+ parallel = config.get("parallel", False)
615
+ max_workers = config.get("max_workers")
616
+ stop_on_failure = config.get("stop_on_failure", False)
617
+
618
+ commands = []
619
+ for cmd_config in config.get("commands", []):
620
+ if isinstance(cmd_config, dict):
621
+ task_item = Task(
622
+ name=cmd_config.get("name", ""),
623
+ depends_on=cmd_config.get("depends_on", []),
624
+ cmd=cmd_config.get("cmd"),
625
+ func=cmd_config.get("func"),
626
+ condition=cmd_config.get("condition"),
627
+ description=cmd_config.get("description", ""),
628
+ parallel=cmd_config.get("parallel", False),
629
+ )
630
+ commands.append(task_item)
631
+ elif isinstance(cmd_config, list):
632
+ # 直接是命令列表
633
+ commands.append(Task(cmd=cmd_config))
634
+ elif callable(cmd_config):
635
+ # 直接是可调用对象
636
+ commands.append(Task(func=cmd_config))
637
+
638
+ return cls(
639
+ name=name,
640
+ commands=commands,
641
+ description=description,
642
+ enabled=enabled,
643
+ parallel=parallel,
644
+ max_workers=max_workers,
645
+ stop_on_failure=stop_on_failure,
646
+ )
647
+
648
+ def to_dict(self) -> dict[str, Any]:
649
+ """转换为字典配置."""
650
+ return {
651
+ "name": self.name,
652
+ "description": self.description,
653
+ "enabled": self.enabled,
654
+ "parallel": self.parallel,
655
+ "max_workers": self.max_workers,
656
+ "stop_on_failure": self.stop_on_failure,
657
+ "commands": [
658
+ {
659
+ "name": item.name,
660
+ "cmd": item.cmd,
661
+ "depends_on": item.depends_on,
662
+ "description": item.description,
663
+ "parallel": item.parallel,
664
+ # 注意: func 和 condition 无法序列化
665
+ }
666
+ for item in self.commands
667
+ ],
668
+ }
File without changes