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
|
@@ -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
|
+
)
|