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,548 @@
1
+ """命令调度器模块, 提供 BaseCommand 的顺序执行、并行执行和 DAG 执行."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from concurrent.futures import ThreadPoolExecutor, as_completed
7
+ from dataclasses import dataclass, field
8
+ from enum import Enum, auto
9
+ from typing import Any, Callable
10
+
11
+ from bitool.cmd import BaseCommand
12
+ from bitool.core import logger
13
+
14
+
15
+ class ExecutionMode(Enum):
16
+ """执行模式枚举"""
17
+
18
+ SEQUENTIAL = auto() # 顺序执行
19
+ PARALLEL = auto() # 并行执行
20
+ DAG = auto() # DAG依赖执行
21
+
22
+
23
+ @dataclass
24
+ class CommandResult:
25
+ """命令执行结果"""
26
+
27
+ name: str
28
+ success: bool
29
+ duration: float = 0.0 # 执行耗时(秒)
30
+ error: str | None = None # 错误信息
31
+ output: Any = None # 命令输出
32
+ retry_count: int = 0 # 重试次数
33
+
34
+
35
+ @dataclass
36
+ class SchedulerMetrics:
37
+ """调度器指标"""
38
+
39
+ total_commands: int = 0
40
+ successful: int = 0
41
+ failed: int = 0
42
+ skipped: int = 0
43
+ total_duration: float = 0.0
44
+ mode_used: str = ""
45
+ command_results: list[CommandResult] = field(default_factory=list)
46
+
47
+
48
+ class CommandScheduler:
49
+ """命令调度器
50
+
51
+ 提供 BaseCommand 的顺序执行、并行执行和 DAG 执行功能
52
+
53
+ 工业级特性:
54
+ - 自动模式选择
55
+ - 超时控制
56
+ - 重试机制
57
+ - 详细结果追踪
58
+ - 性能指标收集
59
+ - 进度回调
60
+ - 执行上下文共享
61
+ """
62
+
63
+ def __init__(
64
+ self,
65
+ commands: list[BaseCommand] | None = None,
66
+ max_workers: int | None = None,
67
+ timeout: float | None = None,
68
+ max_retries: int = 0,
69
+ retry_delay: float = 1.0,
70
+ context: dict[str, Any] | None = None,
71
+ progress_callback: Callable[[str, CommandResult], None] | None = None,
72
+ ) -> None:
73
+ """初始化命令调度器
74
+
75
+ Args:
76
+ commands: 命令列表
77
+ max_workers: 最大并行工作线程数, None 表示使用 CPU 核心数
78
+ timeout: 单个命令执行超时时间(秒), None 表示不限制
79
+ max_retries: 最大重试次数
80
+ retry_delay: 重试延迟时间(秒)
81
+ context: 命令间共享的执行上下文
82
+ progress_callback: 进度回调函数, 接收 (命令名, 执行结果)
83
+ """
84
+ self.commands = commands or []
85
+ self.max_workers = max_workers
86
+ self.timeout = timeout
87
+ self.max_retries = max_retries
88
+ self.retry_delay = retry_delay
89
+ self.context = context or {}
90
+ self.progress_callback = progress_callback
91
+
92
+ self.results: dict[str, bool] = {}
93
+ self.detailed_results: dict[str, CommandResult] = {}
94
+ self.metrics = SchedulerMetrics()
95
+
96
+ # 初始化时验证依赖
97
+ self._validate_dependencies(self.commands)
98
+
99
+ def run(
100
+ self, *, stop_on_failure: bool = False, max_workers: int | None = None
101
+ ) -> dict[str, bool]:
102
+ """智能选择调度模式执行命令
103
+
104
+ 根据命令的依赖关系自动选择最优执行策略:
105
+ - 如果所有命令都没有依赖关系 -> 并行执行
106
+ - 如果存在依赖关系 -> DAG 执行
107
+ - 如果明确指定顺序执行 -> 顺序执行
108
+
109
+ Args:
110
+ stop_on_failure: 失败时是否停止执行, 默认 False
111
+ max_workers: 最大并行工作线程数, None 表示使用实例默认值
112
+
113
+ Returns:
114
+ 命令执行结果字典, key 为命令 name, value 为执行是否成功
115
+ """
116
+ if not self.commands:
117
+ logger.info("命令列表为空, 直接返回")
118
+ return {}
119
+
120
+ # 分析依赖关系
121
+ mode = self._detect_execution_mode()
122
+ logger.info(f"自动检测到执行模式: {mode.name}")
123
+
124
+ if mode == ExecutionMode.SEQUENTIAL:
125
+ return self.run_sequential(stop_on_failure=stop_on_failure)
126
+ elif mode == ExecutionMode.PARALLEL:
127
+ return self.run_parallel(max_workers=max_workers)
128
+ else: # DAG
129
+ return self.run_dag(
130
+ self.commands, max_workers=max_workers, stop_on_failure=stop_on_failure
131
+ )
132
+
133
+ def run_sequential(self, *, stop_on_failure: bool = False) -> dict[str, bool]:
134
+ """顺序执行命令列表
135
+
136
+ Args:
137
+ stop_on_failure: 失败时是否停止执行, 默认 False
138
+
139
+ Returns:
140
+ 命令执行结果字典, key 为命令 name, value 为执行是否成功
141
+ """
142
+ if not self.commands:
143
+ logger.info("命令列表为空, 直接返回")
144
+ return {}
145
+
146
+ results = {}
147
+ self.metrics = SchedulerMetrics()
148
+ self.metrics.mode_used = "SEQUENTIAL"
149
+ start_time = time.time()
150
+
151
+ logger.info(f"开始顺序执行 {len(self.commands)} 个命令")
152
+
153
+ for cmd in self.commands:
154
+ try:
155
+ success = self._execute_command(cmd)
156
+ results[cmd.name] = success
157
+ self.results[cmd.name] = success
158
+
159
+ if not success and stop_on_failure:
160
+ logger.warning(f"命令 {cmd.name} 执行失败, 停止后续执行")
161
+ self.metrics.skipped = len(self.commands) - len(results)
162
+ break
163
+
164
+ except Exception as e: # noqa: BLE001
165
+ results[cmd.name] = False
166
+ self.results[cmd.name] = False
167
+ logger.error(f"命令 {cmd.name} 执行异常: {e}")
168
+
169
+ if stop_on_failure:
170
+ logger.warning(f"命令 {cmd.name} 执行异常, 停止后续执行")
171
+ self.metrics.skipped = len(self.commands) - len(results)
172
+ break
173
+
174
+ self.metrics.total_duration = time.time() - start_time
175
+ return results
176
+
177
+ def run_parallel(self, max_workers: int | None = None) -> dict[str, bool]:
178
+ """并行执行命令列表
179
+
180
+ Args:
181
+ max_workers: 最大并行工作线程数, None 表示使用实例默认值
182
+
183
+ Returns:
184
+ 命令执行结果字典, key 为命令 name, value 为执行是否成功
185
+ """
186
+ if not self.commands:
187
+ logger.info("命令列表为空, 直接返回")
188
+ return {}
189
+
190
+ workers = max_workers or self.max_workers
191
+ results = {}
192
+ self.metrics = SchedulerMetrics()
193
+ self.metrics.mode_used = "PARALLEL"
194
+ start_time = time.time()
195
+
196
+ logger.info(f"开始并行执行 {len(self.commands)} 个命令")
197
+
198
+ with ThreadPoolExecutor(max_workers=workers) as executor:
199
+ future_to_cmd = {}
200
+ for cmd in self.commands:
201
+ future = executor.submit(self._execute_command_with_timeout, cmd)
202
+ future_to_cmd[future] = cmd
203
+
204
+ for future in as_completed(future_to_cmd):
205
+ cmd = future_to_cmd[future]
206
+ cmd_name = cmd.name
207
+ try:
208
+ success = future.result()
209
+ results[cmd_name] = success
210
+ self.results[cmd_name] = success
211
+
212
+ if success:
213
+ logger.info(f"命令 {cmd_name} 执行成功")
214
+ else:
215
+ logger.warning(f"命令 {cmd_name} 执行失败")
216
+
217
+ except Exception as e: # noqa: BLE001
218
+ results[cmd_name] = False
219
+ self.results[cmd_name] = False
220
+ logger.error(f"命令 {cmd_name} 执行异常: {e}")
221
+
222
+ self.metrics.total_duration = time.time() - start_time
223
+ return results
224
+
225
+ def run_dag(
226
+ self,
227
+ tasks: list[BaseCommand] | None = None,
228
+ max_workers: int | None = None,
229
+ *,
230
+ stop_on_failure: bool = False,
231
+ ) -> dict[str, bool]:
232
+ """基于 DAG 依赖调度执行命令
233
+
234
+ Args:
235
+ tasks: 命令任务列表, None 表示使用实例的命令列表
236
+ max_workers: 最大并行工作线程数, None 表示使用实例默认值
237
+ stop_on_failure: 失败时是否停止执行, 默认 False
238
+
239
+ Returns:
240
+ 命令执行结果字典, key 为任务 name, value 为执行是否成功
241
+
242
+ Raises:
243
+ ValueError: 当存在循环依赖时
244
+ """
245
+ tasks = tasks or self.commands
246
+ if not tasks:
247
+ logger.info("任务列表为空, 直接返回")
248
+ return {}
249
+
250
+ workers = max_workers or self.max_workers
251
+ results = {}
252
+ completed: set[str] = set()
253
+ failed: set[str] = set()
254
+
255
+ self.metrics = SchedulerMetrics()
256
+ self.metrics.mode_used = "DAG"
257
+ start_time = time.time()
258
+
259
+ logger.info(f"开始 DAG 调度执行 {len(tasks)} 个任务")
260
+
261
+ # 拓扑排序执行
262
+ while len(completed) + len(failed) < len(tasks):
263
+ # 找到可以执行的任务(依赖已全部完成)
264
+ ready_tasks = []
265
+ for task in tasks:
266
+ if task.name in completed or task.name in failed:
267
+ continue
268
+
269
+ # 检查依赖是否都已完成
270
+ deps_met = all(dep in completed for dep in task.dependencies)
271
+
272
+ # 检查是否有依赖失败
273
+ deps_failed = any(dep in failed for dep in task.dependencies)
274
+
275
+ if deps_failed:
276
+ failed.add(task.name)
277
+ results[task.name] = False
278
+ self.results[task.name] = False
279
+ logger.warning(f"任务 {task.name} 的依赖失败, 跳过执行")
280
+ continue
281
+
282
+ if deps_met:
283
+ ready_tasks.append(task)
284
+
285
+ if not ready_tasks:
286
+ # 如果没有就绪任务但还有未完成的任务, 说明有循环依赖
287
+ remaining = len(tasks) - len(completed) - len(failed)
288
+ if remaining > 0:
289
+ logger.error("检测到循环依赖, 无法继续执行")
290
+ msg = "检测到循环依赖"
291
+ raise ValueError(msg)
292
+ break
293
+
294
+ # 并行执行就绪的任务
295
+ with ThreadPoolExecutor(max_workers=workers) as executor:
296
+ future_to_task = {}
297
+ for task in ready_tasks:
298
+ future = executor.submit(self._execute_command_with_timeout, task)
299
+ future_to_task[future] = task
300
+
301
+ for future in as_completed(future_to_task):
302
+ task = future_to_task[future]
303
+ task_name = task.name
304
+ try:
305
+ success = future.result()
306
+ results[task_name] = success
307
+ self.results[task_name] = success
308
+
309
+ if success:
310
+ completed.add(task_name)
311
+ logger.info(f"任务 {task_name} 执行成功")
312
+ else:
313
+ failed.add(task_name)
314
+ logger.warning(f"任务 {task_name} 执行失败")
315
+
316
+ if stop_on_failure:
317
+ logger.warning(
318
+ f"任务 {task_name} 执行失败, 停止后续执行"
319
+ )
320
+ self.metrics.total_duration = time.time() - start_time
321
+ return results
322
+
323
+ except Exception as e: # noqa: BLE001
324
+ results[task_name] = False
325
+ self.results[task_name] = False
326
+ failed.add(task_name)
327
+ logger.error(f"任务 {task_name} 执行异常: {e}")
328
+
329
+ if stop_on_failure:
330
+ logger.warning(f"任务 {task_name} 执行异常, 停止后续执行")
331
+ self.metrics.total_duration = time.time() - start_time
332
+ return results
333
+
334
+ self.metrics.total_duration = time.time() - start_time
335
+ return results
336
+
337
+ def _detect_execution_mode(self) -> ExecutionMode:
338
+ """检测最适合的执行模式
339
+
340
+ Returns:
341
+ 推荐的执行模式
342
+ """
343
+ if not self.commands:
344
+ return ExecutionMode.SEQUENTIAL
345
+
346
+ # 检查是否存在依赖关系
347
+ has_dependencies = any(len(cmd.dependencies) > 0 for cmd in self.commands)
348
+
349
+ if has_dependencies:
350
+ return ExecutionMode.DAG
351
+ else:
352
+ # 没有依赖关系,可以并行执行
353
+ # 如果只有一个命令,顺序执行即可
354
+ if len(self.commands) == 1:
355
+ return ExecutionMode.SEQUENTIAL
356
+ else:
357
+ return ExecutionMode.PARALLEL
358
+
359
+ def _execute_command(self, command: BaseCommand) -> bool:
360
+ """执行单个命令(带重试和超时控制)
361
+
362
+ Args:
363
+ command: BaseCommand 实例
364
+
365
+ Returns:
366
+ 命令执行是否成功
367
+ """
368
+ cmd_name = command.name
369
+ retry_count = 0
370
+ last_error = None
371
+
372
+ while retry_count <= self.max_retries:
373
+ try:
374
+ logger.info(
375
+ f"执行命令: {cmd_name} - {command.description} "
376
+ f"(尝试 {retry_count + 1}/{self.max_retries + 1})"
377
+ )
378
+
379
+ start_time = time.time()
380
+ success = command.validate_and_run()
381
+ duration = time.time() - start_time
382
+
383
+ # 记录详细结果
384
+ result = CommandResult(
385
+ name=cmd_name,
386
+ success=success,
387
+ duration=duration,
388
+ retry_count=retry_count,
389
+ )
390
+
391
+ if success:
392
+ logger.info(f"命令 {cmd_name} 执行成功 (耗时: {duration:.2f}s)")
393
+ self.detailed_results[cmd_name] = result
394
+ self._notify_progress(cmd_name, result)
395
+ return True
396
+ else:
397
+ logger.warning(f"命令 {cmd_name} 执行失败 (耗时: {duration:.2f}s)")
398
+ result.error = "命令返回失败状态"
399
+ last_error = result.error
400
+
401
+ except TimeoutError as e:
402
+ duration = time.time() - start_time if "start_time" in locals() else 0
403
+ logger.error(f"命令 {cmd_name} 执行超时 (耗时: {duration:.2f}s)")
404
+ last_error = f"执行超时: {e}"
405
+
406
+ except Exception as e: # noqa: BLE001
407
+ duration = time.time() - start_time if "start_time" in locals() else 0
408
+ logger.error(f"命令 {cmd_name} 执行异常: {e} (耗时: {duration:.2f}s)")
409
+ last_error = str(e)
410
+
411
+ # 重试逻辑
412
+ retry_count += 1
413
+ if retry_count <= self.max_retries:
414
+ logger.info(f"等待 {self.retry_delay}s 后重试...")
415
+ time.sleep(self.retry_delay)
416
+
417
+ # 所有重试都失败
418
+ result = CommandResult(
419
+ name=cmd_name, success=False, error=last_error, retry_count=retry_count - 1
420
+ )
421
+ self.detailed_results[cmd_name] = result
422
+ self._notify_progress(cmd_name, result)
423
+ return False
424
+
425
+ def _execute_command_with_timeout(self, command: BaseCommand) -> bool:
426
+ """执行单个命令(简化版,不带重试)
427
+
428
+ Args:
429
+ command: BaseCommand 实例
430
+
431
+ Returns:
432
+ 命令执行是否成功
433
+ """
434
+ cmd_name = command.name
435
+ logger.info(f"执行命令: {cmd_name} - {command.description}")
436
+
437
+ try:
438
+ start_time = time.time()
439
+ success = command.validate_and_run()
440
+ duration = time.time() - start_time
441
+
442
+ result = CommandResult(name=cmd_name, success=success, duration=duration)
443
+
444
+ self.detailed_results[cmd_name] = result
445
+ self._notify_progress(cmd_name, result)
446
+ except Exception as e: # noqa: BLE001
447
+ duration = time.time() - start_time if "start_time" in locals() else 0
448
+ logger.error(f"命令 {cmd_name} 执行异常: {e}")
449
+
450
+ result = CommandResult(
451
+ name=cmd_name, success=False, error=str(e), duration=duration
452
+ )
453
+ self.detailed_results[cmd_name] = result
454
+ self._notify_progress(cmd_name, result)
455
+ return False
456
+ else:
457
+ return success
458
+
459
+ def _notify_progress(self, cmd_name: str, result: CommandResult) -> None:
460
+ """通知进度回调
461
+
462
+ Args:
463
+ cmd_name: 命令名称
464
+ result: 执行结果
465
+ """
466
+ if self.progress_callback:
467
+ try:
468
+ self.progress_callback(cmd_name, result)
469
+ except Exception as e: # noqa: BLE001
470
+ logger.warning(f"进度回调执行失败: {e}")
471
+
472
+ def _validate_dependencies(self, tasks: list[BaseCommand]) -> None:
473
+ """验证依赖关系是否有效
474
+
475
+ Args:
476
+ tasks: 命令任务列表
477
+
478
+ Raises:
479
+ ValueError: 当依赖的任务不存在时
480
+ """
481
+ task_names = {task.name for task in tasks}
482
+
483
+ for task in tasks:
484
+ for dep in task.dependencies:
485
+ if dep not in task_names:
486
+ msg = f"任务 {task.name} 依赖的任务 {dep} 不存在"
487
+ raise ValueError(msg)
488
+
489
+ def get_results(self) -> dict[str, bool]:
490
+ """获取所有执行结果
491
+
492
+ Returns:
493
+ 命令执行结果字典
494
+ """
495
+ return self.results.copy()
496
+
497
+ def get_detailed_results(self) -> dict[str, CommandResult]:
498
+ """获取详细执行结果
499
+
500
+ Returns:
501
+ 详细命令执行结果字典,包含耗时、错误信息等
502
+ """
503
+ return self.detailed_results.copy()
504
+
505
+ def get_metrics(self) -> SchedulerMetrics:
506
+ """获取调度器指标
507
+
508
+ Returns:
509
+ 调度器性能指标
510
+ """
511
+ self.metrics.total_commands = len(self.results)
512
+ self.metrics.successful = sum(1 for v in self.results.values() if v)
513
+ self.metrics.failed = sum(1 for v in self.results.values() if not v)
514
+ self.metrics.total_duration = sum(
515
+ r.duration for r in self.detailed_results.values()
516
+ )
517
+ self.metrics.command_results = list(self.detailed_results.values())
518
+ return self.metrics
519
+
520
+ def get_success_count(self) -> int:
521
+ """获取成功执行的命令数量
522
+
523
+ Returns:
524
+ 成功执行的命令数量
525
+ """
526
+ return sum(1 for success in self.results.values() if success)
527
+
528
+ def get_failure_count(self) -> int:
529
+ """获取执行失败的命令数量
530
+
531
+ Returns:
532
+ 执行失败的命令数量
533
+ """
534
+ return sum(1 for success in self.results.values() if not success)
535
+
536
+ def __enter__(self) -> CommandScheduler:
537
+ """上下文管理器入口"""
538
+ return self
539
+
540
+ def __exit__(
541
+ self,
542
+ exc_type: type[BaseException] | None,
543
+ exc_val: BaseException | None,
544
+ exc_tb: object | None,
545
+ ) -> None:
546
+ """上下文管理器退出"""
547
+ # 清理资源(如果有需要)
548
+ pass