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,412 @@
1
+ """CLI 命令组解析器模块.
2
+
3
+ 本模块提供通用的 CLI 命令组解析器,允许通过简单的配置
4
+ 来定义复杂的命令行工具.
5
+
6
+ 核心功能
7
+ --------
8
+ - 声明式 CLI 命令组定义
9
+ - 自动参数解析和验证
10
+ - 支持子命令和参数配置
11
+ - 与 TaskGroup 无缝集成
12
+ - 统一的错误处理和日志记录
13
+
14
+ 快速开始
15
+ --------
16
+ >>> from pytola_core.command.cli_parser import CliParser
17
+ >>> from pytola_core.command.task_group import TaskGroup, Task
18
+ >>>
19
+ >>> # 定义任务组工厂函数
20
+ >>> def create_hello_group() -> TaskGroup:
21
+ ... return TaskGroup(
22
+ ... name="hello",
23
+ ... commands=[
24
+ ... Task(cmd=["echo", "Hello, World!"]),
25
+ ... ],
26
+ ... )
27
+ >>>
28
+ >>> # 创建解析器
29
+ >>> parser = CliParser(
30
+ ... name="mytool",
31
+ ... description="我的命令行工具",
32
+ ... command_factories={
33
+ ... "hello": create_hello_group,
34
+ ... }
35
+ ... )
36
+ >>>
37
+ >>> # 运行 CLI (模拟)
38
+ >>> # parser.run() # 实际使用时调用
39
+ """
40
+
41
+ from __future__ import annotations
42
+
43
+ import argparse
44
+ import sys
45
+ from dataclasses import dataclass, field
46
+ from typing import Any, Callable
47
+
48
+ from ..core.logger import logger
49
+ from .executor import RunResult
50
+ from .task import Task
51
+ from .task_group import TaskGroup
52
+
53
+
54
+ @dataclass
55
+ class ArgumentConfig:
56
+ """命令行参数配置."""
57
+
58
+ args: list[str] = field(default_factory=list)
59
+ help: str = ""
60
+ action: str = "store"
61
+ dest: str | None = None
62
+ default: Any = None
63
+ type: Any = None
64
+ choices: list[Any] | None = None
65
+ required: bool = False
66
+ nargs: str | int | None = None
67
+ metavar: str | None = None
68
+
69
+
70
+ @dataclass
71
+ class CliCommandConfig:
72
+ """CLI 命令配置."""
73
+
74
+ help: str = ""
75
+ arguments: list[ArgumentConfig] = field(default_factory=list)
76
+
77
+
78
+ @dataclass
79
+ class CliParser:
80
+ """CLI 命令组解析器.
81
+
82
+ 用于将 TaskGroup 工厂函数映射到 CLI 子命令,
83
+ 提供统一的参数解析、验证和执行逻辑.
84
+
85
+ Attributes
86
+ ----------
87
+ name : str
88
+ 工具名称
89
+ description : str
90
+ 工具描述信息
91
+ command_factories : dict[str, Callable[[], TaskGroup]]
92
+ 命令名到 TaskGroup 工厂函数的映射
93
+ command_configs : dict[str, CliCommandConfig]
94
+ 命令配置,用于定义子命令的参数
95
+ """
96
+
97
+ name: str
98
+ description: str = ""
99
+ command_factories: dict[str, Callable[[], TaskGroup]] = field(default_factory=dict)
100
+ command_configs: dict[str, CliCommandConfig] = field(default_factory=dict)
101
+
102
+ def add_command(
103
+ self,
104
+ name: str,
105
+ factory: Callable[[], TaskGroup] | TaskGroup,
106
+ config: CliCommandConfig | None = None,
107
+ ) -> CliParser:
108
+ """添加命令到解析器.
109
+
110
+ Parameters
111
+ ----------
112
+ name : str
113
+ 命令名称
114
+ factory : Callable[[], TaskGroup] | TaskGroup
115
+ TaskGroup 工厂函数或 TaskGroup 实例
116
+ config : CliCommandConfig, optional
117
+ 命令配置,用于定义子命令的参数
118
+
119
+ Returns
120
+ -------
121
+ CliParser
122
+ 返回自身以支持链式调用
123
+ """
124
+ # 支持直接传入 TaskGroup 实例
125
+ if isinstance(factory, TaskGroup):
126
+ command_group = factory
127
+ self.command_factories[name] = lambda: command_group
128
+ else:
129
+ self.command_factories[name] = factory
130
+
131
+ if config is not None:
132
+ self.command_configs[name] = config
133
+ return self
134
+
135
+ def build_parser(self) -> argparse.ArgumentParser:
136
+ """构建 argparse 解析器.
137
+
138
+ Returns
139
+ -------
140
+ argparse.ArgumentParser
141
+ 配置好的参数解析器
142
+ """
143
+ parser = argparse.ArgumentParser(
144
+ prog=self.name,
145
+ description=self.description,
146
+ )
147
+ subparsers = parser.add_subparsers(
148
+ dest="command",
149
+ help="可用命令",
150
+ required=True,
151
+ )
152
+
153
+ # 为每个命令创建子解析器
154
+ for cmd_name in self.command_factories:
155
+ cmd_config = self.command_configs.get(cmd_name, CliCommandConfig())
156
+ cmd_help = cmd_config.help or f"执行 {cmd_name} 命令"
157
+
158
+ sub_parser = subparsers.add_parser(cmd_name, help=cmd_help)
159
+
160
+ # 添加命令特定的参数
161
+ for arg_config in cmd_config.arguments:
162
+ kwargs = arg_config.__dict__.copy()
163
+ args = kwargs.pop("args", [])
164
+ # 移除内部字段,不传递给 argparse
165
+ kwargs.pop("condition_value", None)
166
+ kwargs.pop("alternative_commands", None)
167
+ kwargs = {k: v for k, v in kwargs.items() if v is not None and k != "help"}
168
+
169
+ # 位置参数(不以 - 或 -- 开头)不支持 required 属性
170
+ if args and not args[0].startswith("-"):
171
+ kwargs.pop("required", None)
172
+
173
+ if arg_config.help:
174
+ kwargs["help"] = arg_config.help
175
+ sub_parser.add_argument(*args, **kwargs)
176
+
177
+ return parser
178
+
179
+ def parse_args(self, args: list[str] | None = None) -> argparse.Namespace:
180
+ """解析命令行参数.
181
+
182
+ Parameters
183
+ ----------
184
+ args : list[str], optional
185
+ 要解析的参数列表,默认使用 sys.argv[1:]
186
+
187
+ Returns
188
+ -------
189
+ argparse.Namespace
190
+ 解析后的参数
191
+ """
192
+ parser = self.build_parser()
193
+ return parser.parse_args(args)
194
+
195
+ def add_cmd(
196
+ self,
197
+ name: str,
198
+ cmd: list[str] | Callable[[], RunResult],
199
+ description: str = "",
200
+ config: CliCommandConfig | None = None,
201
+ **kwargs: Any, # noqa: ANN401
202
+ ) -> CliParser:
203
+ """快捷添加单命令任务.
204
+
205
+ 适用于只有一条命令的场景,免去嵌套 commands 列表.
206
+
207
+ Parameters
208
+ ----------
209
+ name : str
210
+ 命令名称
211
+ cmd : list[str] | Callable[[], RunResult]
212
+ 命令及其参数列表,或返回RunResult的可调用对象
213
+ description : str
214
+ 命令描述
215
+ config : CliCommandConfig, optional
216
+ 命令参数配置
217
+ **kwargs : Any
218
+ 传递给 TaskGroup 的其他参数 (如 enabled, stop_on_failure)
219
+
220
+ Returns
221
+ -------
222
+ CliParser
223
+ 返回自身以支持链式调用
224
+
225
+ Examples
226
+ --------
227
+ >>> # 使用命令列表
228
+ >>> parser.add_cmd("p", ["git", "push", "--all"], description="推送所有分支")
229
+ >>>
230
+ >>> # 使用可调用对象
231
+ >>> def my_task() -> RunResult:
232
+ ... return RunResult(returncode=0, stdout="Done", stderr="", execution_time=0.0)
233
+ >>> parser.add_cmd("t", my_task, description="执行自定义任务")
234
+ """
235
+ if callable(cmd):
236
+ # 可调用对象,直接传递给 add_multi_commands
237
+ return self.add_multi_commands(
238
+ name,
239
+ commands=[cmd],
240
+ description=description,
241
+ config=config,
242
+ **kwargs,
243
+ )
244
+ else:
245
+ # 命令列表,使用原有的字典方式
246
+ return self.add_multi_commands(
247
+ name,
248
+ commands=[{"cmd": cmd}],
249
+ description=description,
250
+ config=config,
251
+ **kwargs,
252
+ )
253
+
254
+ def add_multi_commands(
255
+ self,
256
+ name: str,
257
+ commands: list[dict | Task | Callable[[], RunResult] | list[str]] | None = None,
258
+ description: str = "",
259
+ config: CliCommandConfig | None = None,
260
+ **kwargs: Any, # noqa: ANN401
261
+ ) -> CliParser:
262
+ """添加简单命令(便捷方法).
263
+
264
+ 快速创建包含 Task 列表的任务组,适用于不需要动态生成的简单场景.
265
+
266
+ Parameters
267
+ ----------
268
+ name : str
269
+ 命令名称
270
+ description : str
271
+ 命令描述
272
+ commands : list, optional
273
+ Task 列表或可转换为 Task 的配置
274
+ config : CliCommandConfig, optional
275
+ 命令参数配置
276
+ **kwargs : Any
277
+ 传递给 TaskGroup 的其他参数
278
+
279
+ Returns
280
+ -------
281
+ CliParser
282
+ 返回自身以支持链式调用
283
+
284
+ Examples
285
+ --------
286
+ >>> parser.add_multi_commands(
287
+ ... "init",
288
+ ... description="初始化仓库",
289
+ ... commands=[
290
+ ... {"cmd": ["git", "init"], "description": "初始化"},
291
+ ... {"cmd": ["git", "add", "."], "description": "添加文件"},
292
+ ... ],
293
+ ... )
294
+
295
+ >>> # 使用工厂函数实现条件命令
296
+ >>> def create_init_group(args: argparse.Namespace) -> TaskGroup:
297
+ ... if getattr(args, "sub", False):
298
+ ... commands = [{"func": init_repo_with_sub, "description": "初始化仓库及子文件夹"}]
299
+ ... else:
300
+ ... commands = [{"func": init_repo, "description": "初始化仓库"}]
301
+ ... return TaskGroup(name="init", commands=[Task.from_dict(c) for c in commands])
302
+ >>>
303
+ >>> parser.add_command("i", create_init_group)
304
+ """
305
+ from .task_group import Task
306
+
307
+ # 创建工厂函数
308
+ def create_command_group(parsed_args: argparse.Namespace | None = None) -> TaskGroup:
309
+ """根据参数创建任务组."""
310
+ # 将字典或列表转换为 Task
311
+ items = []
312
+ if commands:
313
+ is_single = len(commands) == 1
314
+ for cmd in commands:
315
+ if isinstance(cmd, dict):
316
+ items.append(Task.from_dict(cmd)) # type: ignore[arg-type]
317
+ elif isinstance(cmd, Task):
318
+ items.append(cmd)
319
+ elif callable(cmd):
320
+ items.append(
321
+ Task.from_callable(cmd, description=description if is_single else ""), # type: ignore[arg-type]
322
+ )
323
+ elif isinstance(cmd, list):
324
+ items.append(Task.from_list(cmd, description=description if is_single else ""))
325
+
326
+ return TaskGroup(
327
+ name=f"{self.name}-{name}",
328
+ description=description,
329
+ commands=items,
330
+ **kwargs,
331
+ )
332
+
333
+ return self.add_command(name, create_command_group, config or CliCommandConfig(help=description))
334
+
335
+ def run(self, args: list[str] | None = None) -> None:
336
+ """运行 CLI 工具.
337
+
338
+ Parameters
339
+ ----------
340
+ args : list[str], optional
341
+ 要解析的参数列表,默认使用 sys.argv[1:]
342
+
343
+ Raises
344
+ ------
345
+ SystemExit
346
+ 如果指定了无效命令或执行失败
347
+ """
348
+ parsed_args = self.parse_args(args)
349
+ command_name = parsed_args.command
350
+ factory = self.command_factories.get(command_name)
351
+
352
+ if not factory:
353
+ logger.error("无效命令: %s", command_name)
354
+ sys.exit(1)
355
+
356
+ try:
357
+ # 创建并执行任务组
358
+ # 尝试检查工厂函数是否接受参数
359
+ import inspect
360
+
361
+ sig = inspect.signature(factory)
362
+ params = list(sig.parameters.values())
363
+
364
+ # 如果函数接受参数(除了 self),则传递 parsed_args
365
+ command_group = factory(parsed_args) if len(params) > 0 else factory() # type: ignore[call-arg]
366
+ results = command_group.execute()
367
+
368
+ # 检查执行结果
369
+ if results:
370
+ failed_count = sum(1 for r in results if not r.success)
371
+ if failed_count > 0:
372
+ logger.warning(f"命令执行完成,但有 {failed_count} 个命令失败")
373
+ sys.exit(1)
374
+
375
+ except (RuntimeError, ValueError, TypeError, OSError) as e:
376
+ logger.error(f"执行命令 '{command_name}' 时发生错误: {e}")
377
+ sys.exit(1)
378
+
379
+ @classmethod
380
+ def from_dict(cls, config: dict[str, Any]) -> CliParser:
381
+ """从字典配置创建解析器.
382
+
383
+ Parameters
384
+ ----------
385
+ config : dict[str, Any]
386
+ 解析器配置字典
387
+
388
+ Returns
389
+ -------
390
+ CliParser
391
+ CLI 解析器实例
392
+ """
393
+ # 转换 command_configs 为 CliCommandConfig 对象
394
+ command_configs = {}
395
+ raw_configs = config.get("command_configs", {})
396
+ for cmd_name, cmd_config in raw_configs.items():
397
+ if isinstance(cmd_config, CliCommandConfig):
398
+ command_configs[cmd_name] = cmd_config
399
+ else:
400
+ # 从字典创建 CliCommandConfig
401
+ arguments = [ArgumentConfig(**arg_dict) for arg_dict in cmd_config.get("arguments", [])]
402
+ command_configs[cmd_name] = CliCommandConfig(
403
+ help=cmd_config.get("help", ""),
404
+ arguments=arguments,
405
+ )
406
+
407
+ return cls(
408
+ name=config.get("name", "cli-tool"),
409
+ description=config.get("description", ""),
410
+ command_factories=config.get("command_factories", {}),
411
+ command_configs=command_configs,
412
+ )