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.
- bitool/__init__.py +27 -0
- bitool/cmd/__init__.py +65 -0
- bitool/cmd/_base.py +105 -0
- bitool/cmd/_condition.py +60 -0
- bitool/cmd/_scheduler.py +548 -0
- bitool/cmd/env.py +454 -0
- bitool/cmd/git.py +123 -0
- bitool/cmd/io.py +248 -0
- bitool/cmd/pdf.py +385 -0
- bitool/cmd/run.py +300 -0
- bitool/cmd/toml.py +237 -0
- bitool/cmd/version.py +630 -0
- bitool/consts.py +14 -0
- bitool/core/__init__.py +7 -0
- bitool/core/app.py +142 -0
- bitool/core/commands.py +194 -0
- bitool/core/config.py +647 -0
- bitool/core/env.py +18 -0
- bitool/core/logger.py +237 -0
- bitool/core/plugin.py +117 -0
- bitool/core/workspace.py +76 -0
- bitool/models/__init__.py +3 -0
- bitool/models/version.py +173 -0
- bitool/scripts/__init__.py +1 -0
- bitool/scripts/bumpversion.py +189 -0
- bitool/scripts/clearscreen.py +37 -0
- bitool/scripts/envpy.py +161 -0
- bitool/scripts/envrs.py +119 -0
- bitool/scripts/filedate.py +246 -0
- bitool/scripts/filelevel.py +191 -0
- bitool/scripts/gittool.py +178 -0
- bitool/scripts/img2pdf.py +151 -0
- bitool/scripts/pdf2img.py +139 -0
- bitool/scripts/piptool.py +130 -0
- bitool/scripts/pymake.py +345 -0
- bitool/scripts/sshcopyid.py +491 -0
- bitool/scripts/taskkill.py +366 -0
- bitool/scripts/which.py +227 -0
- bitool/types.py +7 -0
- bitool/utils/__init__.py +9 -0
- bitool/utils/cli_parser.py +412 -0
- bitool/utils/executor.py +881 -0
- bitool/utils/profiler.py +369 -0
- bitool/utils/task.py +133 -0
- bitool/utils/task_group.py +668 -0
- bitool/utils/tests/__init__.py +0 -0
- bitool/utils/tests/test_profiler.py +487 -0
- bitool-0.1.2.dist-info/METADATA +154 -0
- bitool-0.1.2.dist-info/RECORD +51 -0
- bitool-0.1.2.dist-info/WHEEL +4 -0
- bitool-0.1.2.dist-info/entry_points.txt +15 -0
bitool/utils/profiler.py
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
"""性能分析工具模块.
|
|
2
|
+
|
|
3
|
+
提供基于装饰器的性能分析功能,追踪函数运行时间和内存使用。
|
|
4
|
+
|
|
5
|
+
示例:
|
|
6
|
+
>>> from bitool import profile
|
|
7
|
+
>>> @profile
|
|
8
|
+
... def my_function():
|
|
9
|
+
... pass
|
|
10
|
+
>>> my_function()
|
|
11
|
+
|
|
12
|
+
性能测试应用:
|
|
13
|
+
通过环境变量 BITOOL_PROFILE=1 启用性能分析,避免影响发布脚本性能。
|
|
14
|
+
在性能测试脚本中使用,而不是在生产脚本中使用。
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import contextlib
|
|
20
|
+
import functools
|
|
21
|
+
import threading
|
|
22
|
+
import time
|
|
23
|
+
import tracemalloc
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
from functools import cached_property
|
|
26
|
+
|
|
27
|
+
from rich.console import Console
|
|
28
|
+
from rich.table import Table
|
|
29
|
+
|
|
30
|
+
from ..core import env_check_enabled, logger
|
|
31
|
+
from ..types import FunctionType, ParamType, ReturnType
|
|
32
|
+
|
|
33
|
+
# 延迟初始化 tracemalloc,避免影响正常性能
|
|
34
|
+
_tracemalloc_started = False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# 常量定义
|
|
38
|
+
_MS_PER_SEC = 1000 # 每秒毫秒数
|
|
39
|
+
_μS_PER_MS = 1000 # 每毫秒微秒数
|
|
40
|
+
_MEMORY_THRESHOLD_MB = 0.01 # 内存变化阈值(MB)
|
|
41
|
+
_MAX_FUNC_NAME_LENGTH = 28 # 函数名最大长度
|
|
42
|
+
_FUNC_NAME_TRUNCATE_LENGTH = 25 # 函数名截断长度
|
|
43
|
+
|
|
44
|
+
# 显示配置常量
|
|
45
|
+
_CONSOLE_WIDTH = 120 # 控制台宽度
|
|
46
|
+
_TABLE_TITLE_STYLE = "bold magenta" # 表格标题样式
|
|
47
|
+
_TABLE_BORDER_STYLE = "blue" # 表格边框样式
|
|
48
|
+
_TABLE_HEADER_STYLE = "bold cyan" # 表头样式
|
|
49
|
+
_COL_FUNC_NAME_STYLE = "green" # 函数名列样式
|
|
50
|
+
_COL_DURATION_STYLE = "yellow" # 耗时列样式
|
|
51
|
+
_COL_PERCENTAGE_STYLE = "blue" # 占比列样式
|
|
52
|
+
_COL_CALL_COUNT_STYLE = "magenta" # 调用次数列样式
|
|
53
|
+
_COL_MEMORY_STYLE = "red" # 内存列样式
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
_USE_RUST_PROFILER = False
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
__all__ = ["profile"]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class _PyProfileRecord:
|
|
64
|
+
"""Python 实现的性能记录(用于 Rust 不可用时的降级)."""
|
|
65
|
+
|
|
66
|
+
name: str
|
|
67
|
+
duration_secs: float
|
|
68
|
+
memory_before_mb: float = 0.0
|
|
69
|
+
memory_after_mb: float = 0.0
|
|
70
|
+
memory_delta_mb: float = 0.0
|
|
71
|
+
timestamp: str = ""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class _PythonPerformanceProfiler:
|
|
75
|
+
"""Python 实现的性能分析器(用于 Rust 不可用时的降级)."""
|
|
76
|
+
|
|
77
|
+
def __init__(self) -> None:
|
|
78
|
+
self._records: list[_PyProfileRecord] = []
|
|
79
|
+
self._active_timers: dict[str, float] = {}
|
|
80
|
+
self._call_counts: dict[str, int] = {}
|
|
81
|
+
self._ensure_tracemalloc_started()
|
|
82
|
+
|
|
83
|
+
def _ensure_tracemalloc_started(self) -> None:
|
|
84
|
+
"""确保 tracemalloc 已启动(延迟初始化)."""
|
|
85
|
+
global _tracemalloc_started
|
|
86
|
+
if not _tracemalloc_started:
|
|
87
|
+
try:
|
|
88
|
+
tracemalloc.start()
|
|
89
|
+
_tracemalloc_started = True
|
|
90
|
+
logger.debug("已启动 tracemalloc 内存追踪")
|
|
91
|
+
except RuntimeError as e:
|
|
92
|
+
logger.warning(f"启动 tracemalloc 失败: {e}")
|
|
93
|
+
|
|
94
|
+
def start_timer(self, name: str) -> None:
|
|
95
|
+
"""开始计时."""
|
|
96
|
+
self._active_timers[name] = time.perf_counter()
|
|
97
|
+
# 记录当前内存使用
|
|
98
|
+
try:
|
|
99
|
+
current, _peak = tracemalloc.get_traced_memory()
|
|
100
|
+
self._active_timers[f"{name}_memory"] = current / (1024 * 1024)
|
|
101
|
+
except RuntimeError as e:
|
|
102
|
+
logger.debug(f"获取内存信息失败: {e}")
|
|
103
|
+
|
|
104
|
+
def stop_timer(self, name: str) -> _PyProfileRecord | None:
|
|
105
|
+
"""停止计时并记录性能数据."""
|
|
106
|
+
start_time = self._active_timers.pop(name, None)
|
|
107
|
+
if start_time is None:
|
|
108
|
+
logger.warning(f"未找到计时器: {name}")
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
duration = time.perf_counter() - start_time
|
|
112
|
+
|
|
113
|
+
# 获取内存信息
|
|
114
|
+
memory_before = self._active_timers.pop(f"{name}_memory", 0.0)
|
|
115
|
+
memory_after = 0.0
|
|
116
|
+
memory_delta = 0.0
|
|
117
|
+
try:
|
|
118
|
+
current, _peak = tracemalloc.get_traced_memory()
|
|
119
|
+
memory_after = current / (1024 * 1024)
|
|
120
|
+
memory_delta = memory_after - memory_before
|
|
121
|
+
except RuntimeError as e:
|
|
122
|
+
logger.debug(f"获取内存信息失败: {e}")
|
|
123
|
+
|
|
124
|
+
record = _PyProfileRecord(
|
|
125
|
+
name=name,
|
|
126
|
+
duration_secs=duration,
|
|
127
|
+
memory_before_mb=memory_before,
|
|
128
|
+
memory_after_mb=memory_after,
|
|
129
|
+
memory_delta_mb=memory_delta,
|
|
130
|
+
timestamp=time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
131
|
+
)
|
|
132
|
+
self._records.append(record)
|
|
133
|
+
self._call_counts[name] = self._call_counts.get(name, 0) + 1
|
|
134
|
+
return record
|
|
135
|
+
|
|
136
|
+
def get_records(self) -> list[_PyProfileRecord]:
|
|
137
|
+
"""获取所有性能记录."""
|
|
138
|
+
return self._records.copy()
|
|
139
|
+
|
|
140
|
+
def get_call_count(self, name: str) -> int:
|
|
141
|
+
"""获取调用次数."""
|
|
142
|
+
return self._call_counts.get(name, 0)
|
|
143
|
+
|
|
144
|
+
def clear(self) -> None:
|
|
145
|
+
"""清除所有记录."""
|
|
146
|
+
self._records.clear()
|
|
147
|
+
self._active_timers.clear()
|
|
148
|
+
self._call_counts.clear()
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@dataclass
|
|
152
|
+
class _FuncStats:
|
|
153
|
+
"""函数性能统计数据."""
|
|
154
|
+
|
|
155
|
+
total_time: float = 0.0 # 总耗时(包含子调用)
|
|
156
|
+
self_time: float = 0.0 # 自身耗时(不包含子调用)
|
|
157
|
+
call_count: int = 0
|
|
158
|
+
memory_delta: float = 0.0
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@dataclass
|
|
162
|
+
class _PerformanceProfilerWrapper:
|
|
163
|
+
"""性能分析器包装器."""
|
|
164
|
+
|
|
165
|
+
_call_depth: dict[int, int] = field(default_factory=dict)
|
|
166
|
+
_lock: threading.Lock = field(default_factory=threading.Lock)
|
|
167
|
+
_child_time: dict[int, float] = field(default_factory=dict)
|
|
168
|
+
_self_times: dict[str, float] = field(default_factory=dict)
|
|
169
|
+
|
|
170
|
+
def profile(self, func: FunctionType) -> FunctionType:
|
|
171
|
+
"""性能分析装饰器."""
|
|
172
|
+
# 动态检查环境变量,支持运行时启用
|
|
173
|
+
if not env_check_enabled("BITOOL_PROFILE"):
|
|
174
|
+
return func
|
|
175
|
+
|
|
176
|
+
func_name: str = getattr(func, "__name__", "Unknown")
|
|
177
|
+
|
|
178
|
+
@functools.wraps(func)
|
|
179
|
+
def wrapper(*args: ParamType.args, **kwargs: ParamType.kwargs) -> ReturnType:
|
|
180
|
+
thread_id: int = threading.current_thread().ident or 0
|
|
181
|
+
|
|
182
|
+
with self._lock:
|
|
183
|
+
self._call_depth.setdefault(thread_id, 0)
|
|
184
|
+
self._call_depth[thread_id] += 1
|
|
185
|
+
is_top_level = self._call_depth[thread_id] == 1
|
|
186
|
+
# 记录进入函数前的子调用耗时(用于计算自身耗时)
|
|
187
|
+
self._child_time.setdefault(thread_id, 0.0)
|
|
188
|
+
entry_child_time = self._child_time[thread_id]
|
|
189
|
+
|
|
190
|
+
self.profiler.start_timer(func_name)
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
result = func(*args, **kwargs)
|
|
194
|
+
except BaseException:
|
|
195
|
+
# 异常情况下也要停止计时器, 避免计时器泄漏
|
|
196
|
+
with contextlib.suppress(BaseException):
|
|
197
|
+
self.profiler.stop_timer(func_name)
|
|
198
|
+
raise
|
|
199
|
+
else:
|
|
200
|
+
# 正常执行完成,停止计时器
|
|
201
|
+
record = self.profiler.stop_timer(func_name)
|
|
202
|
+
|
|
203
|
+
# 计算自身耗时
|
|
204
|
+
if record is not None:
|
|
205
|
+
# 退出时的子调用耗时 - 进入时的子调用耗时 = 当前函数的子调用耗时
|
|
206
|
+
with self._lock:
|
|
207
|
+
exit_child_time = self._child_time[thread_id]
|
|
208
|
+
children_time = exit_child_time - entry_child_time
|
|
209
|
+
self_time = record.duration_secs - children_time
|
|
210
|
+
|
|
211
|
+
# 保存自身耗时到字典中
|
|
212
|
+
self._self_times[func_name] = (
|
|
213
|
+
self._self_times.get(func_name, 0.0) + self_time
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# 将当前函数的总耗时累加到父函数的子调用耗时中
|
|
217
|
+
with self._lock:
|
|
218
|
+
if self._call_depth[thread_id] > 1: # 如果有父函数
|
|
219
|
+
self._child_time[thread_id] += record.duration_secs
|
|
220
|
+
|
|
221
|
+
return result
|
|
222
|
+
finally:
|
|
223
|
+
with self._lock:
|
|
224
|
+
self._call_depth[thread_id] -= 1
|
|
225
|
+
if is_top_level and self._call_depth[thread_id] == 0:
|
|
226
|
+
self._show_summary()
|
|
227
|
+
|
|
228
|
+
return wrapper # type: ignore[return-value]
|
|
229
|
+
|
|
230
|
+
def _show_summary(self) -> None:
|
|
231
|
+
"""显示性能分析摘要."""
|
|
232
|
+
if not self.records:
|
|
233
|
+
logger.info("无性能数据")
|
|
234
|
+
return
|
|
235
|
+
|
|
236
|
+
# 使用最后一条记录(顶层函数)的耗时作为基准
|
|
237
|
+
baseline_time = self.records[-1].duration_secs
|
|
238
|
+
|
|
239
|
+
# 创建 Rich 表格(使用缓存的 Console 实例)
|
|
240
|
+
console = Console(width=_CONSOLE_WIDTH)
|
|
241
|
+
table = Table(
|
|
242
|
+
title="性能分析报告",
|
|
243
|
+
title_style=_TABLE_TITLE_STYLE,
|
|
244
|
+
border_style=_TABLE_BORDER_STYLE,
|
|
245
|
+
show_header=True,
|
|
246
|
+
header_style=_TABLE_HEADER_STYLE,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
# 添加列
|
|
250
|
+
table.add_column("函数名", style=_COL_FUNC_NAME_STYLE, max_width=30)
|
|
251
|
+
table.add_column("总耗时", justify="right", style=_COL_DURATION_STYLE)
|
|
252
|
+
table.add_column("自身耗时", justify="right", style=_COL_DURATION_STYLE)
|
|
253
|
+
table.add_column("占比", justify="right", style=_COL_PERCENTAGE_STYLE)
|
|
254
|
+
table.add_column("调用次数", justify="right", style=_COL_CALL_COUNT_STYLE)
|
|
255
|
+
table.add_column("内存变化", justify="right", style=_COL_MEMORY_STYLE)
|
|
256
|
+
|
|
257
|
+
# 添加数据行
|
|
258
|
+
for name, stats in self.sorted_funcs:
|
|
259
|
+
# 格式化总耗时
|
|
260
|
+
total_time_str = self._format_duration(stats.total_time)
|
|
261
|
+
# 格式化自身耗时
|
|
262
|
+
self_time_str = self._format_duration(stats.self_time)
|
|
263
|
+
|
|
264
|
+
# 格式化占比(相对于顶层函数耗时,使用自身耗时)
|
|
265
|
+
percentage = (
|
|
266
|
+
stats.self_time / baseline_time * 100 if baseline_time > 0 else 0
|
|
267
|
+
)
|
|
268
|
+
percentage_str = f"{percentage:.1f}%"
|
|
269
|
+
|
|
270
|
+
# 格式化调用次数
|
|
271
|
+
call_str = f"{stats.call_count}x"
|
|
272
|
+
|
|
273
|
+
# 格式化内存变化
|
|
274
|
+
mem_str = self._format_memory(stats.memory_delta)
|
|
275
|
+
|
|
276
|
+
# 截断过长的函数名
|
|
277
|
+
display_name = self._truncate_func_name(name)
|
|
278
|
+
|
|
279
|
+
table.add_row(
|
|
280
|
+
display_name,
|
|
281
|
+
total_time_str,
|
|
282
|
+
self_time_str,
|
|
283
|
+
percentage_str,
|
|
284
|
+
call_str,
|
|
285
|
+
mem_str,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
# 添加总计行
|
|
289
|
+
total_str = self._format_duration(baseline_time)
|
|
290
|
+
|
|
291
|
+
# 计算总调用次数
|
|
292
|
+
total_calls = sum(stats.call_count for _, stats in self.func_stats.items())
|
|
293
|
+
|
|
294
|
+
table.add_row(
|
|
295
|
+
"总计", total_str, "", "100.0%", f"{total_calls}次", "", style="bold"
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
# 输出表格
|
|
299
|
+
console.print(table)
|
|
300
|
+
|
|
301
|
+
@cached_property
|
|
302
|
+
def profiler(self) -> _PythonPerformanceProfiler:
|
|
303
|
+
"""获取性能分析器实例."""
|
|
304
|
+
return _PythonPerformanceProfiler()
|
|
305
|
+
|
|
306
|
+
@cached_property
|
|
307
|
+
def records(self) -> list[_PyProfileRecord]:
|
|
308
|
+
"""获取所有性能记录."""
|
|
309
|
+
return self.profiler.get_records()
|
|
310
|
+
|
|
311
|
+
@cached_property
|
|
312
|
+
def total_time(self) -> float:
|
|
313
|
+
"""获取总耗时(秒)."""
|
|
314
|
+
return sum(record.duration_secs for record in self.records)
|
|
315
|
+
|
|
316
|
+
@cached_property
|
|
317
|
+
def func_stats(self) -> dict[str, _FuncStats]:
|
|
318
|
+
"""获取所有函数的性能统计数据."""
|
|
319
|
+
from collections import defaultdict
|
|
320
|
+
|
|
321
|
+
func_stats: dict[str, _FuncStats] = defaultdict(_FuncStats)
|
|
322
|
+
for record in self.records:
|
|
323
|
+
name = record.name
|
|
324
|
+
func_stats[name].total_time += record.duration_secs
|
|
325
|
+
func_stats[name].call_count += 1
|
|
326
|
+
func_stats[name].memory_delta += record.memory_delta_mb
|
|
327
|
+
# 使用字典中的自身耗时
|
|
328
|
+
func_stats[name].self_time = self._self_times.get(name, 0.0)
|
|
329
|
+
return dict(func_stats)
|
|
330
|
+
|
|
331
|
+
@cached_property
|
|
332
|
+
def sorted_funcs(self) -> list[tuple[str, _FuncStats]]:
|
|
333
|
+
"""获取按总耗时排序的函数性能统计数据."""
|
|
334
|
+
return sorted(
|
|
335
|
+
self.func_stats.items(), key=lambda x: x[1].total_time, reverse=True
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
def _format_duration(self, seconds: float) -> str:
|
|
339
|
+
"""格式化时间显示."""
|
|
340
|
+
total_ms = seconds * _MS_PER_SEC
|
|
341
|
+
if total_ms >= _MS_PER_SEC:
|
|
342
|
+
return f"{total_ms / _MS_PER_SEC:.2f}s"
|
|
343
|
+
if total_ms >= 1:
|
|
344
|
+
return f"{total_ms:.0f}ms"
|
|
345
|
+
return f"{total_ms * _μS_PER_MS:.0f}μs"
|
|
346
|
+
|
|
347
|
+
def _format_memory(self, delta_mb: float) -> str:
|
|
348
|
+
"""格式化内存变化显示."""
|
|
349
|
+
if abs(delta_mb) > _MEMORY_THRESHOLD_MB:
|
|
350
|
+
return f"{delta_mb:+.2f}MB"
|
|
351
|
+
return "~0MB"
|
|
352
|
+
|
|
353
|
+
def _truncate_func_name(self, name: str) -> str:
|
|
354
|
+
"""截断过长的函数名."""
|
|
355
|
+
if len(name) > _MAX_FUNC_NAME_LENGTH:
|
|
356
|
+
return name[:_FUNC_NAME_TRUNCATE_LENGTH] + "..."
|
|
357
|
+
return name
|
|
358
|
+
|
|
359
|
+
def clear(self) -> None:
|
|
360
|
+
"""清除所有性能记录."""
|
|
361
|
+
self.profiler.clear()
|
|
362
|
+
with self._lock:
|
|
363
|
+
self._call_depth.clear()
|
|
364
|
+
self._child_time.clear()
|
|
365
|
+
self._self_times.clear()
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
_profiler_wrapper = _PerformanceProfilerWrapper()
|
|
369
|
+
profile = _profiler_wrapper.profile
|
bitool/utils/task.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from functools import cached_property
|
|
5
|
+
from typing import Any, Callable
|
|
6
|
+
|
|
7
|
+
from .executor import RunResult
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class Task:
|
|
12
|
+
"""单个任务项.
|
|
13
|
+
|
|
14
|
+
Attributes
|
|
15
|
+
----------
|
|
16
|
+
name : str
|
|
17
|
+
任务唯一标识,用于依赖引用
|
|
18
|
+
cmd : list[str], optional
|
|
19
|
+
命令及其参数列表
|
|
20
|
+
func : Callable[[], RunResult], optional
|
|
21
|
+
可执行的函数
|
|
22
|
+
condition : Callable[[], bool], optional
|
|
23
|
+
执行条件函数,返回True才执行
|
|
24
|
+
description : str
|
|
25
|
+
任务描述信息
|
|
26
|
+
parallel : bool
|
|
27
|
+
是否允许并行执行,默认 False (顺序执行)
|
|
28
|
+
depends_on : list[str]
|
|
29
|
+
依赖的任务名称列表,用于DAG调度
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
cmd: list[str] | None = None
|
|
33
|
+
func: Callable[[], RunResult] | None = None
|
|
34
|
+
condition: Callable[[], bool] | None = None
|
|
35
|
+
description: str = ""
|
|
36
|
+
parallel: bool = False
|
|
37
|
+
name: str = ""
|
|
38
|
+
depends_on: list[str] = field(default_factory=list)
|
|
39
|
+
|
|
40
|
+
def __post_init__(self) -> None:
|
|
41
|
+
"""验证任务项配置."""
|
|
42
|
+
if self.cmd is None and self.func is None:
|
|
43
|
+
msg = "Task 必须提供 cmd 或 func 参数"
|
|
44
|
+
raise ValueError(msg)
|
|
45
|
+
|
|
46
|
+
# 如果声明了依赖,必须有名称
|
|
47
|
+
if self.depends_on and not self.name:
|
|
48
|
+
msg = "Task 如果声明了 depends_on,必须提供 name 参数"
|
|
49
|
+
raise ValueError(msg)
|
|
50
|
+
|
|
51
|
+
@cached_property
|
|
52
|
+
def is_executable(self) -> bool:
|
|
53
|
+
"""检查是否应该执行此任务."""
|
|
54
|
+
if self.condition is None:
|
|
55
|
+
return True
|
|
56
|
+
|
|
57
|
+
return self.condition()
|
|
58
|
+
|
|
59
|
+
@cached_property
|
|
60
|
+
def executable_cmd(self) -> list[str] | Callable[[], RunResult] | None:
|
|
61
|
+
"""转换为可执行对象."""
|
|
62
|
+
if self.cmd is not None:
|
|
63
|
+
return self.cmd
|
|
64
|
+
|
|
65
|
+
return self.func
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def from_dict(cls, config: dict[str, Any]) -> Task:
|
|
69
|
+
"""从字典配置创建任务.
|
|
70
|
+
|
|
71
|
+
Parameters
|
|
72
|
+
----------
|
|
73
|
+
config : dict[str, Any]
|
|
74
|
+
任务配置字典
|
|
75
|
+
|
|
76
|
+
Returns
|
|
77
|
+
-------
|
|
78
|
+
Task
|
|
79
|
+
任务实例
|
|
80
|
+
"""
|
|
81
|
+
return cls(
|
|
82
|
+
name=config.get("name", ""),
|
|
83
|
+
cmd=config.get("cmd"),
|
|
84
|
+
func=config.get("func"),
|
|
85
|
+
condition=config.get("condition"),
|
|
86
|
+
description=config.get("description", ""),
|
|
87
|
+
parallel=config.get("parallel", False),
|
|
88
|
+
depends_on=config.get("depends_on", []),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def from_callable(
|
|
93
|
+
cls,
|
|
94
|
+
func: Callable[[], RunResult],
|
|
95
|
+
description: str = "",
|
|
96
|
+
) -> Task:
|
|
97
|
+
"""从可调用对象创建任务.
|
|
98
|
+
|
|
99
|
+
Parameters
|
|
100
|
+
----------
|
|
101
|
+
func : Callable[[], RunResult]
|
|
102
|
+
可执行函数
|
|
103
|
+
description : str
|
|
104
|
+
任务描述
|
|
105
|
+
|
|
106
|
+
Returns
|
|
107
|
+
-------
|
|
108
|
+
Task
|
|
109
|
+
任务实例
|
|
110
|
+
"""
|
|
111
|
+
return cls(func=func, description=description)
|
|
112
|
+
|
|
113
|
+
@classmethod
|
|
114
|
+
def from_list(
|
|
115
|
+
cls,
|
|
116
|
+
cmd: list[str],
|
|
117
|
+
description: str = "",
|
|
118
|
+
) -> Task:
|
|
119
|
+
"""从命令列表创建任务.
|
|
120
|
+
|
|
121
|
+
Parameters
|
|
122
|
+
----------
|
|
123
|
+
cmd : list[str]
|
|
124
|
+
命令及其参数列表
|
|
125
|
+
description : str
|
|
126
|
+
任务描述
|
|
127
|
+
|
|
128
|
+
Returns
|
|
129
|
+
-------
|
|
130
|
+
Task
|
|
131
|
+
任务实例
|
|
132
|
+
"""
|
|
133
|
+
return cls(cmd=cmd, description=description)
|