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/io.py ADDED
@@ -0,0 +1,248 @@
1
+ """文件IO命令模块.
2
+
3
+ 提供文件读写相关的命令实现.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import shutil
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+
12
+ from bitool.cmd._base import BaseCommand
13
+ from bitool.core import logger
14
+
15
+
16
+ def _backup_file(filepath: Path, backup_path: Path | None = None) -> Path | None:
17
+ """备份文件辅助函数.
18
+
19
+ 参数:
20
+ filepath: 源文件路径
21
+ backup_path: 备份文件路径, 如果为 None 则自动生成
22
+
23
+ 返回:
24
+ 备份文件路径, 如果备份失败则返回 None
25
+ """
26
+ # 如果未指定备份路径, 自动生成带 .bak 后缀的路径
27
+ if backup_path is None or str(backup_path) == ".":
28
+ backup_path = filepath.with_suffix(filepath.suffix + ".bak")
29
+
30
+ try:
31
+ # 确保备份文件父目录存在
32
+ if not backup_path.parent.exists():
33
+ backup_path.parent.mkdir(parents=True, exist_ok=True)
34
+
35
+ shutil.copy2(filepath, backup_path)
36
+ logger.info(msg=f"已备份文件: `{filepath}` -> `{backup_path}`")
37
+ except OSError as e:
38
+ logger.error(msg=f"备份文件时出错: `{e}`", exc_info=True)
39
+ return None
40
+ else:
41
+ return backup_path
42
+
43
+
44
+ @dataclass
45
+ class WriteFileCommand(BaseCommand):
46
+ """写入文件命令."""
47
+
48
+ name = "write_file"
49
+ description = "写入文件命令"
50
+
51
+ filepath: Path = Path()
52
+ content: str = ""
53
+ overwrite: bool = True
54
+ backup: bool = True
55
+
56
+ def run(self) -> bool:
57
+ """写入文件."""
58
+ # 确保父目录存在
59
+ if not self.filepath.parent.exists():
60
+ self.filepath.parent.mkdir(parents=True, exist_ok=True)
61
+
62
+ # 处理文件已存在的情况
63
+ if self.filepath.exists():
64
+ if not self.overwrite:
65
+ logger.warning(msg=f"文件已存在且不允许覆盖: `{self.filepath}`")
66
+ return False
67
+
68
+ # 如果需要备份, 使用辅助函数创建备份文件
69
+ if self.backup and _backup_file(self.filepath) is None:
70
+ return False
71
+
72
+ # 写入文件内容
73
+ try:
74
+ self.filepath.write_text(data=self.content, encoding="utf-8")
75
+ except OSError as e:
76
+ logger.error(msg=f"写入文件时出错: `{e}`", exc_info=True)
77
+ return False
78
+ else:
79
+ logger.info(msg=f"写入`{self.filepath}` 成功")
80
+ return True
81
+
82
+
83
+ @dataclass
84
+ class ReadFileCommand(BaseCommand):
85
+ """读取文件命令."""
86
+
87
+ name = "read_file"
88
+ description = "读取文件命令"
89
+
90
+ filepath: Path = Path()
91
+ content: str = ""
92
+
93
+ def run(self) -> bool:
94
+ """读取文件内容."""
95
+ if not self.filepath.exists():
96
+ logger.warning(msg=f"文件不存在: `{self.filepath}`")
97
+ return False
98
+
99
+ if not self.filepath.is_file():
100
+ logger.warning(msg=f"路径不是文件: `{self.filepath}`")
101
+ return False
102
+
103
+ try:
104
+ self.content: str = self.filepath.read_text(encoding="utf-8")
105
+ logger.info(msg=f"读取`{self.filepath}` 成功")
106
+ except OSError as e:
107
+ logger.error(msg=f"读取文件时出错: `{e}`", exc_info=True)
108
+ return False
109
+ else:
110
+ return True
111
+
112
+
113
+ @dataclass
114
+ class BackupFileCommand(BaseCommand):
115
+ """备份文件命令."""
116
+
117
+ name = "backup_file"
118
+ description = "备份文件命令"
119
+
120
+ filepath: Path = Path()
121
+ backup_path: Path = Path()
122
+
123
+ def run(self) -> bool:
124
+ """备份文件."""
125
+ if not self.filepath.exists():
126
+ logger.warning(msg=f"文件不存在: `{self.filepath}`")
127
+ return False
128
+
129
+ if not self.filepath.is_file():
130
+ logger.warning(msg=f"路径不是文件: `{self.filepath}`")
131
+ return False
132
+
133
+ # 使用辅助函数进行备份
134
+ return _backup_file(self.filepath, self.backup_path) is not None
135
+
136
+
137
+ @dataclass
138
+ class CopyFileCommand(BaseCommand):
139
+ """复制文件命令."""
140
+
141
+ name = "copy_file"
142
+ description = "复制文件命令"
143
+
144
+ source: Path = Path()
145
+ destination: Path = Path()
146
+ overwrite: bool = True
147
+
148
+ def run(self) -> bool:
149
+ """复制文件."""
150
+ if not self.source.exists():
151
+ logger.warning(msg=f"源文件不存在: `{self.source}`")
152
+ return False
153
+
154
+ if not self.source.is_file():
155
+ logger.warning(msg=f"源路径不是文件: `{self.source}`")
156
+ return False
157
+
158
+ # 检查目标文件是否已存在
159
+ if self.destination.exists() and not self.overwrite:
160
+ logger.warning(msg=f"目标文件已存在且不允许覆盖: `{self.destination}`")
161
+ return False
162
+
163
+ try:
164
+ # 确保目标文件父目录存在
165
+ if not self.destination.parent.exists():
166
+ self.destination.parent.mkdir(parents=True, exist_ok=True)
167
+
168
+ shutil.copy2(self.source, self.destination)
169
+ logger.info(msg=f"已复制文件: `{self.source}` -> `{self.destination}`")
170
+ except OSError as e:
171
+ logger.error(msg=f"复制文件时出错: `{e}`", exc_info=True)
172
+ return False
173
+ else:
174
+ return True
175
+
176
+
177
+ @dataclass
178
+ class MoveFileCommand(BaseCommand):
179
+ """移动文件命令."""
180
+
181
+ name = "move_file"
182
+ description = "移动文件命令"
183
+
184
+ source: Path = Path()
185
+ destination: Path = Path()
186
+ overwrite: bool = True
187
+
188
+ def run(self) -> bool:
189
+ """移动文件."""
190
+ if not self.source.exists():
191
+ logger.warning(msg=f"源文件不存在: `{self.source}`")
192
+ return False
193
+
194
+ if not self.source.is_file():
195
+ logger.warning(msg=f"源路径不是文件: `{self.source}`")
196
+ return False
197
+
198
+ # 检查目标文件是否已存在
199
+ if self.destination.exists() and not self.overwrite:
200
+ logger.warning(msg=f"目标文件已存在且不允许覆盖: `{self.destination}`")
201
+ return False
202
+
203
+ try:
204
+ # 确保目标文件父目录存在
205
+ if not self.destination.parent.exists():
206
+ self.destination.parent.mkdir(parents=True, exist_ok=True)
207
+
208
+ shutil.move(str(self.source), str(self.destination))
209
+ logger.info(msg=f"已移动文件: `{self.source}` -> `{self.destination}`")
210
+ except OSError as e:
211
+ logger.error(msg=f"移动文件时出错: `{e}`", exc_info=True)
212
+ return False
213
+ else:
214
+ return True
215
+
216
+
217
+ @dataclass
218
+ class DeleteFileCommand(BaseCommand):
219
+ """删除文件命令."""
220
+
221
+ name = "delete_file"
222
+ description = "删除文件命令"
223
+
224
+ filepath: Path = Path()
225
+ force: bool = False
226
+
227
+ def run(self) -> bool:
228
+ """删除文件."""
229
+ if not self.filepath.exists():
230
+ if self.force:
231
+ logger.info(msg=f"文件不存在, 跳过删除: `{self.filepath}`")
232
+ return True
233
+ else:
234
+ logger.warning(msg=f"文件不存在: `{self.filepath}`")
235
+ return False
236
+
237
+ if not self.filepath.is_file():
238
+ logger.warning(msg=f"路径不是文件: `{self.filepath}`")
239
+ return False
240
+
241
+ try:
242
+ self.filepath.unlink()
243
+ logger.info(msg=f"已删除文件: `{self.filepath}`")
244
+ except OSError as e:
245
+ logger.error(msg=f"删除文件时出错: `{e}`", exc_info=True)
246
+ return False
247
+ else:
248
+ return True
bitool/cmd/pdf.py ADDED
@@ -0,0 +1,385 @@
1
+ """PDF操作模块, 提供用户友好的PDF操作接口."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+
8
+ try:
9
+ import fitz # PyMuPDF
10
+
11
+ HAS_PYMUPDF = True
12
+ except ImportError:
13
+ HAS_PYMUPDF = False
14
+
15
+ from bitool.cmd import BaseCommand
16
+ from bitool.core import logger
17
+
18
+ __all__ = [
19
+ "CompressPdfCommand",
20
+ "ImagesToPdfCommand",
21
+ "MergePdfsCommand",
22
+ "PdfToImagesCommand",
23
+ "SplitPdfCommand",
24
+ ]
25
+
26
+
27
+ @dataclass
28
+ class SplitPdfCommand(BaseCommand):
29
+ """按页数拆分PDF命令
30
+
31
+ 将PDF文件按照指定的页数拆分成多个小文件
32
+ """
33
+
34
+ name = "split_pdf"
35
+ description = "按页数拆分PDF"
36
+
37
+ input_path: Path = field(default_factory=Path.cwd)
38
+ output_dir: Path = field(default_factory=Path.cwd)
39
+ pages_per_file: int = 1
40
+ output_files: list[str] = field(default_factory=list)
41
+
42
+ def run(self) -> bool:
43
+ """执行PDF拆分操作
44
+
45
+ Returns:
46
+ 操作是否成功
47
+ """
48
+ if not HAS_PYMUPDF:
49
+ logger.error("未安装PyMuPDF库, 请安装: pip install PyMuPDF")
50
+ return False
51
+
52
+ if self.pages_per_file <= 0:
53
+ logger.error("每文件页数必须大于0")
54
+ return False
55
+
56
+ if not self.input_path.exists():
57
+ logger.error(f"输入文件不存在: {self.input_path}")
58
+ return False
59
+
60
+ try:
61
+ # 确保输出目录存在
62
+ self.output_dir.mkdir(parents=True, exist_ok=True)
63
+
64
+ logger.info(
65
+ f"开始拆分PDF: {self.input_path}, 每文件 {self.pages_per_file} 页"
66
+ )
67
+
68
+ # 打开PDF文档
69
+ doc = fitz.open(self.input_path)
70
+ total_pages = len(doc)
71
+ self.output_files = []
72
+
73
+ try:
74
+ # 计算需要拆分成多少个文件
75
+ num_files = (
76
+ total_pages + self.pages_per_file - 1
77
+ ) // self.pages_per_file
78
+
79
+ for i in range(num_files):
80
+ # 创建新文档
81
+ new_doc = fitz.open()
82
+ start_page = i * self.pages_per_file
83
+ end_page = min(start_page + self.pages_per_file, total_pages)
84
+
85
+ # 添加页面到新文档
86
+ for page_num in range(start_page, end_page):
87
+ new_doc.insert_pdf(doc, from_page=page_num, to_page=page_num)
88
+
89
+ # 保存新文档
90
+ output_path = self.output_dir / f"part_{i + 1}.pdf"
91
+ new_doc.save(str(output_path))
92
+ new_doc.close()
93
+ self.output_files.append(str(output_path))
94
+
95
+ logger.info(f"PDF拆分完成, 生成 {len(self.output_files)} 个文件")
96
+ finally:
97
+ doc.close()
98
+ except FileNotFoundError as e:
99
+ logger.error(f"输入文件不存在: {e}")
100
+ return False
101
+ except ValueError as e:
102
+ logger.error(f"参数错误: {e}")
103
+ return False
104
+ except Exception as e: # noqa: BLE001
105
+ logger.error(f"PDF拆分失败: {e}")
106
+ return False
107
+ else:
108
+ return len(self.output_files) > 0
109
+
110
+
111
+ @dataclass
112
+ class MergePdfsCommand(BaseCommand):
113
+ """合并多个PDF文件命令
114
+
115
+ 将多个PDF文件合并成一个完整的PDF文件
116
+ """
117
+
118
+ name = "merge_pdfs"
119
+ description = "合并多个PDF文件"
120
+
121
+ input_paths: list[Path] = field(default_factory=list)
122
+ output_path: Path = field(default_factory=lambda: Path.cwd())
123
+
124
+ def run(self) -> bool:
125
+ """执行PDF合并操作
126
+
127
+ Returns:
128
+ 操作是否成功
129
+ """
130
+ if not HAS_PYMUPDF:
131
+ logger.error("未安装PyMuPDF库, 请安装: pip install PyMuPDF")
132
+ return False
133
+
134
+ if not self.input_paths:
135
+ logger.error("输入文件列表为空")
136
+ return False
137
+
138
+ try:
139
+ logger.info(
140
+ f"开始合并 {len(self.input_paths)} 个PDF文件 -> {self.output_path}"
141
+ )
142
+
143
+ # 创建新文档
144
+ merged_doc = fitz.open()
145
+
146
+ try:
147
+ # 遍历所有输入文件
148
+ for input_path in self.input_paths:
149
+ if not input_path.exists():
150
+ logger.error(f"输入文件不存在: {input_path}")
151
+ return False
152
+ doc = fitz.open(str(input_path))
153
+ merged_doc.insert_pdf(doc)
154
+ doc.close()
155
+
156
+ # 确保输出目录存在
157
+ self.output_path.parent.mkdir(parents=True, exist_ok=True)
158
+
159
+ # 保存合并后的文档
160
+ merged_doc.save(str(self.output_path))
161
+ logger.info(f"PDF合并完成: {self.output_path}")
162
+ finally:
163
+ merged_doc.close()
164
+ except FileNotFoundError as e:
165
+ logger.error(f"输入文件不存在: {e}")
166
+ return False
167
+ except Exception as e: # noqa: BLE001
168
+ logger.error(f"PDF合并失败: {e}")
169
+ return False
170
+ else:
171
+ return True
172
+
173
+
174
+ @dataclass
175
+ class CompressPdfCommand(BaseCommand):
176
+ """压缩PDF文件命令
177
+
178
+ 对PDF文件进行压缩以减小文件大小
179
+ """
180
+
181
+ name = "compress_pdf"
182
+ description = "压缩PDF文件"
183
+
184
+ input_path: Path = field(default_factory=Path.cwd)
185
+ output_path: Path = field(default_factory=Path.cwd)
186
+ quality: int = 75
187
+
188
+ def run(self) -> bool:
189
+ """执行PDF压缩操作
190
+
191
+ Returns:
192
+ 操作是否成功
193
+ """
194
+ if not HAS_PYMUPDF:
195
+ logger.error("未安装PyMuPDF库, 请安装: pip install PyMuPDF")
196
+ return False
197
+
198
+ if not (1 <= self.quality <= 100):
199
+ logger.error("压缩质量必须在1-100之间")
200
+ return False
201
+
202
+ if not self.input_path.exists():
203
+ logger.error(f"输入文件不存在: {self.input_path}")
204
+ return False
205
+
206
+ try:
207
+ logger.info(f"开始压缩PDF: {self.input_path} -> {self.output_path}")
208
+
209
+ # 打开PDF文档
210
+ doc = fitz.open(self.input_path)
211
+
212
+ try:
213
+ # 确保输出目录存在
214
+ self.output_path.parent.mkdir(parents=True, exist_ok=True)
215
+
216
+ # 使用PyMuPDF的压缩功能
217
+ doc.save(
218
+ str(self.output_path),
219
+ garbage=4, # 删除未使用的对象
220
+ deflate=True, # 压缩流
221
+ clean=True, # 清理内容
222
+ )
223
+ logger.info(f"PDF压缩完成: {self.output_path}")
224
+ finally:
225
+ doc.close()
226
+ except FileNotFoundError as e:
227
+ logger.error(f"输入文件不存在: {e}")
228
+ return False
229
+ except ValueError as e:
230
+ logger.error(f"参数错误: {e}")
231
+ return False
232
+ except Exception as e: # noqa: BLE001
233
+ logger.error(f"PDF压缩失败: {e}")
234
+ return False
235
+ else:
236
+ return True
237
+
238
+
239
+ @dataclass
240
+ class PdfToImagesCommand(BaseCommand):
241
+ """将PDF转换为图片命令
242
+
243
+ 将PDF文件的每一页转换为图片格式
244
+ """
245
+
246
+ name = "pdf_to_images"
247
+ description = "将PDF转换为图片"
248
+
249
+ input_path: Path = field(default_factory=lambda: Path.cwd())
250
+ output_dir: Path = field(default_factory=lambda: Path.cwd())
251
+ fmt: str = "png"
252
+ dpi: int = 150
253
+ output_files: list[str] = field(default_factory=list)
254
+
255
+ def run(self) -> bool:
256
+ """执行PDF转图片操作
257
+
258
+ Returns:
259
+ 操作是否成功
260
+ """
261
+ if not HAS_PYMUPDF:
262
+ logger.error("未安装PyMuPDF库, 请安装: pip install PyMuPDF")
263
+ return False
264
+
265
+ if not self.input_path.exists():
266
+ logger.error(f"输入文件不存在: {self.input_path}")
267
+ return False
268
+
269
+ try:
270
+ logger.info(f"开始转换PDF为图片: {self.input_path} -> {self.output_dir}")
271
+
272
+ # 确保输出目录存在
273
+ self.output_dir.mkdir(parents=True, exist_ok=True)
274
+
275
+ # 打开PDF文档
276
+ doc = fitz.open(str(self.input_path))
277
+ self.output_files = []
278
+
279
+ try:
280
+ # 计算缩放比例 (DPI / 72是PDF的默认分辨率)
281
+ zoom = self.dpi / 72.0
282
+ mat = fitz.Matrix(zoom, zoom)
283
+
284
+ # 遍历每一页
285
+ for page_num in range(len(doc)):
286
+ page = doc[page_num]
287
+
288
+ # 渲染页面为图片
289
+ pix = page.get_pixmap(matrix=mat)
290
+
291
+ # 保存图片
292
+ output_path = self.output_dir / f"page_{page_num + 1}.{self.fmt}"
293
+ pix.save(str(output_path))
294
+ self.output_files.append(str(output_path))
295
+
296
+ logger.info(f"PDF转图片完成, 生成 {len(self.output_files)} 个文件")
297
+ finally:
298
+ doc.close()
299
+ except FileNotFoundError as e:
300
+ logger.error(f"输入文件不存在: {e}")
301
+ return False
302
+ except ValueError as e:
303
+ logger.error(f"参数错误: {e}")
304
+ return False
305
+ except Exception as e: # noqa: BLE001
306
+ logger.error(f"PDF转图片失败: {e}")
307
+ return False
308
+ else:
309
+ return len(self.output_files) > 0
310
+
311
+
312
+ @dataclass
313
+ class ImagesToPdfCommand(BaseCommand):
314
+ """将图片列表转换为PDF命令
315
+
316
+ 将多张图片合并成一个PDF文件
317
+ """
318
+
319
+ name = "images_to_pdf"
320
+ description = "将图片列表转换为PDF"
321
+
322
+ image_paths: list[Path] = field(default_factory=list)
323
+ output_path: Path = field(default_factory=lambda: Path.cwd())
324
+ page_size: str = "auto"
325
+
326
+ def run(self) -> bool:
327
+ """执行图片转PDF操作
328
+
329
+ Returns:
330
+ 操作是否成功
331
+ """
332
+ if not HAS_PYMUPDF:
333
+ logger.error("未安装PyMuPDF库, 请安装: pip install PyMuPDF")
334
+ return False
335
+
336
+ if not self.image_paths:
337
+ logger.error("图片列表为空")
338
+ return False
339
+
340
+ try:
341
+ logger.info(
342
+ f"开始将 {len(self.image_paths)} 张图片转换为PDF: {self.output_path}"
343
+ )
344
+
345
+ # 创建新PDF文档
346
+ pdf_doc = fitz.open()
347
+
348
+ try:
349
+ # 遍历所有图片
350
+ for image_path in self.image_paths:
351
+ if not image_path.exists():
352
+ logger.error(f"图片文件不存在: {image_path}")
353
+ return False
354
+ # 打开图片
355
+ img_doc = fitz.open(str(image_path))
356
+
357
+ # 将图片转换为PDF页面
358
+ pdf_bytes = img_doc.convert_to_pdf()
359
+ img_pdf = fitz.open("pdf", pdf_bytes)
360
+
361
+ # 插入到主文档
362
+ pdf_doc.insert_pdf(img_pdf)
363
+
364
+ img_doc.close()
365
+ img_pdf.close()
366
+
367
+ # 确保输出目录存在
368
+ self.output_path.parent.mkdir(parents=True, exist_ok=True)
369
+
370
+ # 保存PDF
371
+ pdf_doc.save(str(self.output_path))
372
+ logger.info(f"图片转PDF完成: {self.output_path}")
373
+ finally:
374
+ pdf_doc.close()
375
+ except FileNotFoundError as e:
376
+ logger.error(f"输入文件不存在: {e}")
377
+ return False
378
+ except ValueError as e:
379
+ logger.error(f"参数错误: {e}")
380
+ return False
381
+ except Exception as e: # noqa: BLE001
382
+ logger.error(f"图片转PDF失败: {e}")
383
+ return False
384
+ else:
385
+ return True