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
bitool/cmd/run.py ADDED
@@ -0,0 +1,300 @@
1
+ from __future__ import annotations
2
+
3
+ import shlex
4
+ import subprocess
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+ from typing import Callable, List, cast
8
+
9
+ from bitool.cmd import BaseCommand
10
+ from bitool.core import logger
11
+ from bitool.utils.executor import RunResult, execute
12
+ from bitool.utils.task import Task
13
+ from bitool.utils.task_group import TaskGroup
14
+
15
+
16
+ def _parse_shell_command(command: str) -> tuple[list[str], str | None, bool, bool]:
17
+ """安全地解析包含重定向的 shell 命令。
18
+
19
+ 手动解析常见的 shell 操作符(>, >>, |),避免使用 shell=True。
20
+
21
+ Args:
22
+ command: shell 命令字符串。
23
+
24
+ Returns:
25
+ 元组 (cmd_args, stdout_redirect, append_mode, has_pipe)
26
+ - cmd_args: 命令及其参数列表
27
+ - stdout_redirect: 标准输出重定向文件路径(如果有)
28
+ - append_mode: 是否为追加模式(>>)
29
+ - has_pipe: 是否包含管道(暂不支持)
30
+
31
+ Raises:
32
+ ValueError: 当命令包含不支持的操作符时。
33
+ """
34
+ # 检查管道操作符(暂不支持)
35
+ if "|" in command:
36
+ msg = "不支持管道操作符, 请使用多个命令分别执行"
37
+ raise ValueError(msg)
38
+
39
+ # 解析重定向操作符
40
+ stdout_redirect: str | None = None
41
+ append_mode = False
42
+
43
+ # 处理 >> (追加)
44
+ if ">>" in command:
45
+ parts_redirect: list[str] = command.split(">>", 1)
46
+ cmd_main: str = parts_redirect[0].strip()
47
+ stdout_redirect = parts_redirect[1].strip()
48
+ append_mode = True
49
+ # 处理 > (覆盖)
50
+ elif ">" in command:
51
+ parts_redirect = command.split(">", 1)
52
+ cmd_main = parts_redirect[0].strip()
53
+ stdout_redirect = parts_redirect[1].strip()
54
+ else:
55
+ cmd_main = command
56
+
57
+ # 使用 shlex 安全地分割命令
58
+ cmd_args: list[str] = shlex.split(cmd_main)
59
+
60
+ return cmd_args, stdout_redirect, append_mode, False
61
+
62
+
63
+ __all__ = [
64
+ "RunCommand",
65
+ "RunDAGCommands",
66
+ "RunParallelCommands",
67
+ "RunResult",
68
+ "RunSequentialCommands",
69
+ "RunShellCommand",
70
+ ]
71
+
72
+
73
+ @dataclass
74
+ class BaseRunCommand(BaseCommand):
75
+ cmd: list[str] | Callable[[], None] = field(default_factory=list)
76
+ cwd: Path | None = None
77
+ env: dict[str, str] | None = None
78
+ timeout: int | None = None
79
+ check: bool = False
80
+
81
+
82
+ @dataclass
83
+ class RunCommand(BaseRunCommand):
84
+ """执行单个命令"""
85
+
86
+ name = "run"
87
+ description = "执行单个命令"
88
+
89
+ def run(self) -> bool:
90
+ """执行单个命令"""
91
+ if isinstance(self.cmd, list):
92
+ cmd_list = cast(List[str], self.cmd)
93
+ cmd_str = " ".join(str(arg) for arg in cmd_list)
94
+ try:
95
+ execute(
96
+ cmd_list,
97
+ cwd=self.cwd,
98
+ env=self.env,
99
+ timeout=self.timeout,
100
+ check=self.check,
101
+ )
102
+ # 使用列表推导确保类型安全
103
+ logger.info(f"命令执行成功: `{cmd_str}`")
104
+ except Exception as e: # noqa: BLE001
105
+ logger.error(f"命令执行失败: `{cmd_str}`, 错误: {e}")
106
+ return False
107
+ else:
108
+ # 命令是 Callable[[], None] 类型,直接调用
109
+ self.cmd()
110
+ return True
111
+
112
+
113
+ @dataclass
114
+ class RunShellCommand(BaseRunCommand):
115
+ """执行shell命令字符串"""
116
+
117
+ name = "run_shell"
118
+ description = "执行shell命令字符串"
119
+ shell_cmd: str = ""
120
+
121
+ def run(self) -> bool:
122
+ """执行shell命令字符串(安全版本,支持重定向)。"""
123
+ try:
124
+ # 安全解析命令(避免使用 shell=True)
125
+ cmd_args, stdout_redirect, append_mode, _ = _parse_shell_command(
126
+ self.shell_cmd
127
+ )
128
+
129
+ if not cmd_args:
130
+ logger.error(f"Shell命令为空: `{self.shell_cmd}`")
131
+ return False
132
+
133
+ # 执行命令并处理重定向
134
+ if stdout_redirect:
135
+ # 确保输出目录存在
136
+ redirect_path = Path(stdout_redirect)
137
+ if redirect_path.parent != Path("."):
138
+ redirect_path.parent.mkdir(parents=True, exist_ok=True)
139
+
140
+ # 以追加或覆盖模式打开文件
141
+ mode = "a" if append_mode else "w"
142
+ with redirect_path.open(mode, encoding="utf-8") as outfile:
143
+ result = subprocess.run( # noqa: S603
144
+ cmd_args,
145
+ cwd=self.cwd,
146
+ timeout=self.timeout,
147
+ stdout=outfile,
148
+ stderr=subprocess.PIPE,
149
+ text=True,
150
+ check=False,
151
+ )
152
+
153
+ if result.returncode == 0:
154
+ logger.info(f"Shell命令执行成功: `{self.shell_cmd}`")
155
+ return True
156
+ else:
157
+ error_output = result.stderr.strip()
158
+ logger.error(
159
+ f"Shell命令执行失败: `{self.shell_cmd}`, 返回码: {result.returncode}, 错误: {error_output}"
160
+ )
161
+ return False
162
+ else:
163
+ # 无重定向,正常执行
164
+ result = subprocess.run( # noqa: S603
165
+ cmd_args,
166
+ cwd=self.cwd,
167
+ timeout=self.timeout,
168
+ capture_output=True,
169
+ text=True,
170
+ check=False,
171
+ )
172
+
173
+ if result.returncode == 0:
174
+ logger.info(f"Shell命令执行成功: `{self.shell_cmd}`")
175
+ if result.stdout.strip():
176
+ for line in result.stdout.strip().split("\n"):
177
+ if line.strip():
178
+ logger.info(f" 输出: `{line.strip()}`")
179
+ return True
180
+ else:
181
+ error_output = result.stderr.strip()
182
+ logger.error(
183
+ f"Shell命令执行失败: `{self.shell_cmd}`, 返回码: {result.returncode}, 错误: {error_output}"
184
+ )
185
+ return False
186
+
187
+ except subprocess.TimeoutExpired:
188
+ logger.error(f"Shell命令执行超时: `{self.shell_cmd}`")
189
+ return False
190
+ except ValueError as e:
191
+ logger.error(f"Shell命令解析失败: `{self.shell_cmd}`, 错误: {e}")
192
+ return False
193
+ except Exception as e: # noqa: BLE001
194
+ logger.error(f"Shell命令执行异常: `{self.shell_cmd}`, 错误: {e}")
195
+ return False
196
+
197
+
198
+ @dataclass
199
+ class BaseRunMultipleCommands(BaseCommand):
200
+ commands: list[list[str]] = field(default_factory=list)
201
+ stop_on_failure: bool = False
202
+ max_workers: int | None = None
203
+ results: list[RunResult] = field(default_factory=list)
204
+
205
+
206
+ @dataclass
207
+ class RunSequentialCommands(BaseRunMultipleCommands):
208
+ """顺序执行多个命令"""
209
+
210
+ name = "run_sequential"
211
+ description = "顺序执行多个命令"
212
+
213
+ def run(self) -> bool:
214
+ """顺序执行多个命令"""
215
+ if not self.commands:
216
+ logger.info("命令列表为空,直接返回")
217
+ return True
218
+
219
+ tasks = [Task(cmd=cmd) for cmd in self.commands]
220
+ group = TaskGroup(
221
+ name="sequential-execution",
222
+ commands=tasks,
223
+ parallel=False,
224
+ stop_on_failure=self.stop_on_failure,
225
+ )
226
+
227
+ logger.info(f"开始顺序执行 {len(self.commands)} 个命令")
228
+ self.results = group.execute()
229
+ return all(r.returncode == 0 for r in self.results)
230
+
231
+
232
+ @dataclass
233
+ class RunParallelCommands(BaseRunMultipleCommands):
234
+ """并行执行多个命令"""
235
+
236
+ name = "run_parallel"
237
+ description = "并行执行多个命令"
238
+
239
+ def run(self) -> bool:
240
+ """并行执行多个命令"""
241
+ if not self.commands:
242
+ logger.info("命令列表为空,直接返回")
243
+ return True
244
+
245
+ tasks = [Task(cmd=cmd, parallel=True) for cmd in self.commands]
246
+ group = TaskGroup(
247
+ name="parallel-execution",
248
+ commands=tasks,
249
+ parallel=True,
250
+ max_workers=self.max_workers,
251
+ stop_on_failure=False,
252
+ )
253
+
254
+ logger.info(f"开始并行执行 {len(self.commands)} 个命令")
255
+ self.results = group.execute()
256
+ return all(r.returncode == 0 for r in self.results)
257
+
258
+
259
+ @dataclass
260
+ class RunDAGCommands(BaseRunMultipleCommands):
261
+ """基于DAG依赖调度执行命令"""
262
+
263
+ name = "run_dag"
264
+ description = "基于DAG依赖调度执行命令"
265
+ tasks: list[dict[str, object]] = field(default_factory=list)
266
+
267
+ def run(self) -> bool:
268
+ """基于DAG依赖调度执行命令"""
269
+ if not self.tasks:
270
+ logger.info("任务列表为空,直接返回")
271
+ return True
272
+
273
+ task_objects = []
274
+ for task_config in self.tasks:
275
+ # 类型守卫: 从 dict 中提取具体类型
276
+ name = str(task_config.get("name", ""))
277
+ cmd = task_config.get("cmd")
278
+ func = task_config.get("func")
279
+ depends_on_val = task_config.get("depends_on", [])
280
+ description = str(task_config.get("description", ""))
281
+
282
+ task = Task(
283
+ name=name,
284
+ cmd=cmd if isinstance(cmd, list) else None, # type: ignore
285
+ func=func if callable(func) else None, # type: ignore
286
+ depends_on=depends_on_val if isinstance(depends_on_val, list) else [], # type: ignore
287
+ description=description,
288
+ )
289
+ task_objects.append(task)
290
+
291
+ group = TaskGroup(
292
+ name="dag-execution",
293
+ commands=task_objects,
294
+ max_workers=self.max_workers,
295
+ stop_on_failure=self.stop_on_failure,
296
+ )
297
+
298
+ logger.info(f"开始DAG调度执行 {len(self.tasks)} 个任务")
299
+ self.results = group.execute()
300
+ return all(r.returncode == 0 for r in self.results)
bitool/cmd/toml.py ADDED
@@ -0,0 +1,237 @@
1
+ """TOML文件操作命令模块.
2
+
3
+ 提供TOML文件的读取、写入和版本号更新等功能.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass, field
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ try:
13
+ import tomli
14
+ import tomli_w
15
+ except ImportError:
16
+ try:
17
+ import tomli_w
18
+ import tomllib as tomli # ty: ignore[unresolved-import]
19
+ except ImportError:
20
+ tomli = None # ty: ignore
21
+ tomli_w = None # ty: ignore
22
+
23
+ from bitool.cmd._base import BaseCommand
24
+ from bitool.core import logger
25
+
26
+
27
+ @dataclass
28
+ class ReadTomlCommand(BaseCommand):
29
+ """读取TOML文件命令.
30
+
31
+ Attributes:
32
+ filepath: TOML文件路径
33
+ data: 解析后的TOML数据
34
+ """
35
+
36
+ name: str = "read-toml"
37
+ description: str = "读取TOML文件"
38
+ filepath: Path = Path()
39
+ data: dict[str, Any] = field(default_factory=dict)
40
+
41
+ def run(self) -> bool:
42
+ """执行TOML文件读取.
43
+
44
+ Returns
45
+ -------
46
+ bool
47
+ 读取成功返回True, 否则返回False
48
+ """
49
+ if not self.filepath.exists():
50
+ logger.warning(f"TOML文件不存在: {self.filepath}")
51
+ return False
52
+
53
+ try:
54
+ with self.filepath.open("rb") as f:
55
+ if tomli is not None:
56
+ self.data = tomli.load(f)
57
+ else:
58
+ logger.error("未安装tomli或tomllib库")
59
+ return False
60
+ except Exception as e: # noqa: BLE001
61
+ logger.error(f"读取TOML文件失败: {e}")
62
+ return False
63
+ else:
64
+ logger.info(f"成功读取TOML文件: {self.filepath}")
65
+ return True
66
+
67
+
68
+ @dataclass
69
+ class WriteTomlCommand(BaseCommand):
70
+ """写入TOML文件命令.
71
+
72
+ Attributes:
73
+ filepath: TOML文件路径
74
+ data: 要写入的TOML数据
75
+ overwrite: 是否覆盖已存在的文件
76
+ backup: 写入前是否备份原文件
77
+ """
78
+
79
+ name: str = "write-toml"
80
+ description: str = "写入TOML文件"
81
+ filepath: Path = Path()
82
+ data: dict[str, Any] = field(default_factory=dict)
83
+ overwrite: bool = True
84
+ backup: bool = False
85
+
86
+ def run(self) -> bool:
87
+ """执行TOML文件写入.
88
+
89
+ Returns
90
+ -------
91
+ bool
92
+ 写入成功返回True, 否则返回False
93
+ """
94
+ if tomli_w is None:
95
+ logger.error("未安装tomli_w库")
96
+ return False
97
+
98
+ if self.filepath.exists() and not self.overwrite:
99
+ logger.warning(f"TOML文件已存在且不允许覆盖: {self.filepath}")
100
+ return False
101
+
102
+ try:
103
+ # 备份原文件
104
+ if self.backup and self.filepath.exists():
105
+ backup_path = self.filepath.with_suffix(self.filepath.suffix + ".bak")
106
+ backup_path.write_bytes(self.filepath.read_bytes())
107
+ logger.info(f"已备份TOML文件: {self.filepath} -> {backup_path}")
108
+
109
+ # 确保父目录存在
110
+ if not self.filepath.parent.exists():
111
+ self.filepath.parent.mkdir(parents=True, exist_ok=True)
112
+
113
+ # 写入TOML文件
114
+ with self.filepath.open("wb") as f:
115
+ tomli_w.dump(self.data, f)
116
+ except Exception as e: # noqa: BLE001
117
+ logger.error(f"写入TOML文件失败: {e}")
118
+ return False
119
+ else:
120
+ logger.info(f"成功写入TOML文件: {self.filepath}")
121
+ return True
122
+
123
+
124
+ @dataclass
125
+ class UpdateTomlVersionCommand(BaseCommand):
126
+ """更新TOML文件版本号命令.
127
+
128
+ Attributes:
129
+ filepath: TOML文件路径
130
+ version: 新版本号字符串
131
+ version_path: 版本号在TOML中的路径, 如 ["project", "version"]
132
+ """
133
+
134
+ name: str = "update-toml-version"
135
+ description: str = "更新TOML文件中的版本号"
136
+ filepath: Path = Path()
137
+ version: str = ""
138
+ version_path: list[str] = field(default_factory=lambda: ["project", "version"])
139
+
140
+ def run(self) -> bool:
141
+ """执行TOML文件版本号更新.
142
+
143
+ Returns
144
+ -------
145
+ bool
146
+ 更新成功返回True, 否则返回False
147
+ """
148
+ if tomli is None or tomli_w is None:
149
+ logger.error("未安装tomli或tomli_w库")
150
+ return False
151
+
152
+ if not self.filepath.exists():
153
+ logger.warning(f"TOML文件不存在: {self.filepath}")
154
+ return False
155
+
156
+ try:
157
+ # 读取原文件
158
+ with self.filepath.open("rb") as f:
159
+ data = tomli.load(f)
160
+
161
+ # 导航到版本号位置并更新
162
+ current = data
163
+ for key in self.version_path[:-1]:
164
+ if key not in current:
165
+ current[key] = {}
166
+ current = current[key]
167
+
168
+ old_version = current.get(self.version_path[-1], "未知")
169
+ current[self.version_path[-1]] = self.version
170
+
171
+ # 写回文件
172
+ with self.filepath.open("wb") as f:
173
+ tomli_w.dump(data, f)
174
+
175
+ logger.info(
176
+ f"已更新版本号: {self.filepath} ({old_version} -> {self.version})"
177
+ )
178
+ except Exception as e: # noqa: BLE001
179
+ logger.error(f"更新TOML版本号失败: {e}")
180
+ return False
181
+ else:
182
+ return True
183
+
184
+
185
+ @dataclass
186
+ class ReadTomlVersionCommand(BaseCommand):
187
+ """读取TOML文件版本号命令.
188
+
189
+ Attributes:
190
+ filepath: TOML文件路径
191
+ version: 读取到的版本号
192
+ version_path: 版本号在TOML中的路径
193
+ """
194
+
195
+ name: str = "read-toml-version"
196
+ description: str = "读取TOML文件中的版本号"
197
+ filepath: Path = Path()
198
+ version: str = ""
199
+ version_path: list[str] = field(default_factory=lambda: ["project", "version"])
200
+
201
+ def run(self) -> bool:
202
+ """执行TOML文件版本号读取.
203
+
204
+ Returns
205
+ -------
206
+ bool
207
+ 读取成功返回True, 否则返回False
208
+ """
209
+ if tomli is None:
210
+ logger.error("未安装tomli或tomllib库")
211
+ return False
212
+
213
+ if not self.filepath.exists():
214
+ logger.warning(f"TOML文件不存在: {self.filepath}")
215
+ return False
216
+
217
+ try:
218
+ with self.filepath.open("rb") as f:
219
+ data = tomli.load(f)
220
+
221
+ # 导航到版本号位置
222
+ current = data
223
+ for key in self.version_path:
224
+ if key not in current:
225
+ logger.warning(
226
+ f"在 {self.filepath} 中未找到版本号路径: {'.'.join(self.version_path)}"
227
+ )
228
+ return False
229
+ current = current[key]
230
+
231
+ self.version = str(current)
232
+ logger.info(f"读取到版本号: {self.filepath} -> {self.version}")
233
+ except Exception as e: # noqa: BLE001
234
+ logger.error(f"读取TOML版本号失败: {e}")
235
+ return False
236
+ else:
237
+ return True