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
bitool/core/logger.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""Bitool 的日志模块.
|
|
2
|
+
|
|
3
|
+
本模块提供一个简单的、类似 loguru 的日志系统,具有彩色输出、文件轮转和易于使用的 API.
|
|
4
|
+
|
|
5
|
+
快速开始
|
|
6
|
+
--------
|
|
7
|
+
>>> from bitool.core import logger
|
|
8
|
+
>>> logger.info("Hello, World!") # 彩色输出到控制台和文件
|
|
9
|
+
>>> logger.debug("看不到调试信息")
|
|
10
|
+
>>> logger.setLevel(logger.DEBUG)
|
|
11
|
+
>>> logger.debug("现在可以看到调试信息了")
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import contextlib
|
|
17
|
+
import logging
|
|
18
|
+
import logging.handlers
|
|
19
|
+
import platform
|
|
20
|
+
import sys
|
|
21
|
+
import threading
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import ClassVar, Final
|
|
24
|
+
|
|
25
|
+
__all__ = ["logger"]
|
|
26
|
+
|
|
27
|
+
# 默认日志格式
|
|
28
|
+
_DEFAULT_CONSOLE_FORMAT: Final[str] = "%(asctime)s | %(levelname)-8s | %(message)s"
|
|
29
|
+
_DEFAULT_FILE_FORMAT: Final[str] = (
|
|
30
|
+
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
31
|
+
)
|
|
32
|
+
_DEFAULT_CONSOLE_DATEFMT: Final[str] = "%H:%M:%S"
|
|
33
|
+
_DEFAULT_FILE_DATEFMT: Final[str] = "%Y-%m-%d %H:%M:%S"
|
|
34
|
+
|
|
35
|
+
# 默认日志路径
|
|
36
|
+
_DEFAULT_LOG_DIR: Final[Path] = Path.home() / ".bitool" / "logs"
|
|
37
|
+
_DEFAULT_LOG_FILE: Final[Path] = _DEFAULT_LOG_DIR / "bitool.log"
|
|
38
|
+
_DEFAULT_MAX_BYTES: Final[int] = 10 * 1024 * 1024 # 10MB
|
|
39
|
+
_DEFAULT_BACKUP_COUNT: Final[int] = 5
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class _ColoredFormatter(logging.Formatter):
|
|
43
|
+
"""自定义格式化器,为日志消息添加 ANSI 颜色."""
|
|
44
|
+
|
|
45
|
+
COLORS: ClassVar[dict[str, str]] = {
|
|
46
|
+
"DEBUG": "\033[36m", # 青色
|
|
47
|
+
"INFO": "\033[32m", # 绿色
|
|
48
|
+
"WARNING": "\033[33m", # 黄色
|
|
49
|
+
"ERROR": "\033[31m", # 红色
|
|
50
|
+
"CRITICAL": "\033[41m", # 红底白字
|
|
51
|
+
"RESET": "\033[0m", # 重置颜色
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
def __init__(self, fmt: str | None = None, datefmt: str | None = None) -> None:
|
|
55
|
+
"""初始化格式化器,检测是否支持 ANSI 颜色."""
|
|
56
|
+
super().__init__(fmt, datefmt)
|
|
57
|
+
# 检测是否启用彩色输出
|
|
58
|
+
self._enable_colors = self._check_color_support()
|
|
59
|
+
|
|
60
|
+
@staticmethod
|
|
61
|
+
def _check_color_support() -> bool:
|
|
62
|
+
"""检查当前环境是否支持 ANSI 颜色."""
|
|
63
|
+
# Windows 10 及以上版本支持 ANSI 转义序列
|
|
64
|
+
if platform.system() == "Windows":
|
|
65
|
+
try:
|
|
66
|
+
# 获取 Windows 版本号
|
|
67
|
+
version = platform.version()
|
|
68
|
+
# Windows 版本号格式为 "major.minor.build"
|
|
69
|
+
major_version = int(version.split(".")[0])
|
|
70
|
+
# Windows 10 的版本号为 10, Windows 7 为 6
|
|
71
|
+
|
|
72
|
+
except (ValueError, IndexError):
|
|
73
|
+
# 如果无法解析版本号,保守起见禁用颜色
|
|
74
|
+
return False
|
|
75
|
+
else:
|
|
76
|
+
return major_version >= 10
|
|
77
|
+
|
|
78
|
+
# 非 Windows 系统通常支持 ANSI 颜色
|
|
79
|
+
return True
|
|
80
|
+
|
|
81
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
82
|
+
"""使用适当的颜色格式化日志记录."""
|
|
83
|
+
message = super().format(record)
|
|
84
|
+
if not self._enable_colors:
|
|
85
|
+
return message
|
|
86
|
+
log_color = self.COLORS.get(record.levelname, self.COLORS["RESET"])
|
|
87
|
+
return f"{log_color}{message}{self.COLORS['RESET']}"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class _Logger:
|
|
91
|
+
"""一个简单的、类似 loguru 的日志器.
|
|
92
|
+
|
|
93
|
+
提供干净、最小化的 API,用于带有自动控制台颜色和文件轮转的日志记录。
|
|
94
|
+
|
|
95
|
+
示例
|
|
96
|
+
--------
|
|
97
|
+
>>> from bitool.core import logger
|
|
98
|
+
>>> logger.info("Hello, World!")
|
|
99
|
+
>>> logger.debug("Debug message") # 仅在启用调试时显示
|
|
100
|
+
>>> logger.error("Error occurred")
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
_instance: ClassVar[logging.Logger | None] = None
|
|
104
|
+
_setup_lock: ClassVar[threading.Lock] = threading.Lock()
|
|
105
|
+
_is_setup: ClassVar[bool] = False
|
|
106
|
+
|
|
107
|
+
# 日志级别常量 (与 Python logging 模块兼容)
|
|
108
|
+
DEBUG: Final[int] = logging.DEBUG
|
|
109
|
+
INFO: Final[int] = logging.INFO
|
|
110
|
+
WARNING: Final[int] = logging.WARNING
|
|
111
|
+
ERROR: Final[int] = logging.ERROR
|
|
112
|
+
CRITICAL: Final[int] = logging.CRITICAL
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def _setup(
|
|
116
|
+
cls,
|
|
117
|
+
name: str = "bitool",
|
|
118
|
+
level: int = logging.INFO,
|
|
119
|
+
console_format: str | None = None,
|
|
120
|
+
file_format: str | None = None,
|
|
121
|
+
) -> logging.Logger:
|
|
122
|
+
"""设置带有控制台和文件处理器的日志器.
|
|
123
|
+
|
|
124
|
+
此方法应在应用程序启动时调用一次。
|
|
125
|
+
后续调用将返回现有的日志器实例。
|
|
126
|
+
|
|
127
|
+
参数
|
|
128
|
+
----------
|
|
129
|
+
name : str
|
|
130
|
+
日志器名称。默认为 "bitool"
|
|
131
|
+
level : int
|
|
132
|
+
最低日志级别。默认为 INFO
|
|
133
|
+
使用 logger.DEBUG 获取更详细的输出
|
|
134
|
+
console_format : str, optional
|
|
135
|
+
控制台日志格式。默认为 "%(asctime)s | %(levelname)-8s | %(message)s"
|
|
136
|
+
file_format : str, optional
|
|
137
|
+
文件日志格式。默认为 "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
138
|
+
|
|
139
|
+
返回
|
|
140
|
+
-------
|
|
141
|
+
logging.Logger
|
|
142
|
+
配置好的带有控制台和文件处理器的日志器
|
|
143
|
+
"""
|
|
144
|
+
# 线程安全的单例初始化
|
|
145
|
+
with cls._setup_lock:
|
|
146
|
+
if cls._is_setup and cls._instance is not None:
|
|
147
|
+
return cls._instance
|
|
148
|
+
|
|
149
|
+
# 确保日志目录存在
|
|
150
|
+
with contextlib.suppress(OSError):
|
|
151
|
+
_DEFAULT_LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
152
|
+
|
|
153
|
+
# 创建日志器
|
|
154
|
+
logger_instance = logging.getLogger(name)
|
|
155
|
+
logger_instance.setLevel(logging.DEBUG) # 捕获所有级别,在处理器级别过滤
|
|
156
|
+
|
|
157
|
+
# 控制台处理器
|
|
158
|
+
console_handler = logging.StreamHandler(sys.stdout)
|
|
159
|
+
console_handler.setLevel(level)
|
|
160
|
+
console_fmt = console_format if console_format else _DEFAULT_CONSOLE_FORMAT
|
|
161
|
+
console_formatter = _ColoredFormatter(
|
|
162
|
+
console_fmt, datefmt=_DEFAULT_CONSOLE_DATEFMT
|
|
163
|
+
)
|
|
164
|
+
console_handler.setFormatter(console_formatter)
|
|
165
|
+
|
|
166
|
+
# 文件处理器 - 捕获所有 DEBUG+ 日志并带轮转功能
|
|
167
|
+
file_handler = logging.handlers.RotatingFileHandler(
|
|
168
|
+
_DEFAULT_LOG_FILE,
|
|
169
|
+
maxBytes=_DEFAULT_MAX_BYTES,
|
|
170
|
+
backupCount=_DEFAULT_BACKUP_COUNT,
|
|
171
|
+
encoding="utf-8",
|
|
172
|
+
)
|
|
173
|
+
file_handler.setLevel(logging.DEBUG)
|
|
174
|
+
file_fmt = file_format if file_format else _DEFAULT_FILE_FORMAT
|
|
175
|
+
file_formatter = logging.Formatter(file_fmt, datefmt=_DEFAULT_FILE_DATEFMT)
|
|
176
|
+
file_handler.setFormatter(file_formatter)
|
|
177
|
+
|
|
178
|
+
# 添加处理器
|
|
179
|
+
logger_instance.addHandler(console_handler)
|
|
180
|
+
logger_instance.addHandler(file_handler)
|
|
181
|
+
|
|
182
|
+
# 防止多次添加处理器
|
|
183
|
+
logger_instance.propagate = False
|
|
184
|
+
|
|
185
|
+
cls._instance = logger_instance
|
|
186
|
+
cls._is_setup = True
|
|
187
|
+
return cls._instance
|
|
188
|
+
|
|
189
|
+
@classmethod
|
|
190
|
+
def _get_logger(cls, name: str = "bitool") -> logging.Logger:
|
|
191
|
+
"""获取日志器实例.
|
|
192
|
+
|
|
193
|
+
如果日志器尚未设置,此方法将使用默认设置进行设置。
|
|
194
|
+
如需自定义配置,请显式调用 setup()。
|
|
195
|
+
|
|
196
|
+
参数
|
|
197
|
+
----------
|
|
198
|
+
name : str
|
|
199
|
+
日志器名称。默认为 "bitool"
|
|
200
|
+
|
|
201
|
+
返回
|
|
202
|
+
-------
|
|
203
|
+
logging.Logger
|
|
204
|
+
配置好的日志器实例
|
|
205
|
+
"""
|
|
206
|
+
if cls._instance is None:
|
|
207
|
+
return cls._setup(name)
|
|
208
|
+
return cls._instance
|
|
209
|
+
|
|
210
|
+
@classmethod
|
|
211
|
+
def setLevel(cls, level: int) -> None:
|
|
212
|
+
"""设置日志级别,同时更新 Logger 和所有 Handler 的级别.
|
|
213
|
+
|
|
214
|
+
解决 Python logging 的两级过滤问题:
|
|
215
|
+
- Logger 级别:决定哪些消息进入日志系统
|
|
216
|
+
- Handler 级别:决定哪些消息实际输出
|
|
217
|
+
|
|
218
|
+
参数
|
|
219
|
+
----------
|
|
220
|
+
level : int
|
|
221
|
+
日志级别,如 logging.DEBUG, logging.INFO 等
|
|
222
|
+
|
|
223
|
+
示例
|
|
224
|
+
--------
|
|
225
|
+
>>> from bitool.core import logger
|
|
226
|
+
>>> logger.setLevel(logger.DEBUG) # 启用调试日志
|
|
227
|
+
>>> logger.debug("调试信息") # 现在会正常输出
|
|
228
|
+
"""
|
|
229
|
+
if cls._instance is not None:
|
|
230
|
+
logging.Logger.setLevel(cls._instance, level)
|
|
231
|
+
for handler in cls._instance.handlers:
|
|
232
|
+
handler.setLevel(level)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# 预配置的日志器实例,便于导入
|
|
236
|
+
logger: Final[logging.Logger] = _Logger._get_logger()
|
|
237
|
+
logger.setLevel = _Logger.setLevel # type: ignore
|
bitool/core/plugin.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""插件系统基础框架."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.util
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Callable
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class PluginMetadata:
|
|
14
|
+
"""插件元数据."""
|
|
15
|
+
|
|
16
|
+
name: str
|
|
17
|
+
version: str = "0.1.0"
|
|
18
|
+
description: str = ""
|
|
19
|
+
author: str = ""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Plugin(ABC):
|
|
23
|
+
"""插件基类."""
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def metadata(self) -> PluginMetadata:
|
|
28
|
+
"""返回插件元数据."""
|
|
29
|
+
...
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def register(self, registry: PluginRegistry) -> None:
|
|
33
|
+
"""注册插件提供的命令和能力."""
|
|
34
|
+
...
|
|
35
|
+
|
|
36
|
+
def initialize(self, context: PluginContext) -> None: # noqa: B027
|
|
37
|
+
"""插件初始化钩子(可选覆盖)."""
|
|
38
|
+
# 默认实现:无需初始化操作
|
|
39
|
+
|
|
40
|
+
def shutdown(self) -> None: # noqa: B027
|
|
41
|
+
"""插件关闭钩子(可选覆盖)."""
|
|
42
|
+
# 默认实现:无需清理操作
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class PluginContext:
|
|
47
|
+
"""插件上下文,提供插件运行所需的环境."""
|
|
48
|
+
|
|
49
|
+
workspace: Any = None
|
|
50
|
+
config: Any = None
|
|
51
|
+
registry: Any = None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class PluginRegistry:
|
|
56
|
+
"""插件注册表."""
|
|
57
|
+
|
|
58
|
+
_plugins: dict[str, Plugin] = field(default_factory=dict)
|
|
59
|
+
_commands: dict[str, Callable[..., Any]] = field(default_factory=dict)
|
|
60
|
+
|
|
61
|
+
def register_plugin(self, plugin: Plugin) -> None:
|
|
62
|
+
"""注册插件."""
|
|
63
|
+
meta = plugin.metadata
|
|
64
|
+
if meta.name in self._plugins:
|
|
65
|
+
raise ValueError(f"插件 {meta.name} 已注册")
|
|
66
|
+
self._plugins[meta.name] = plugin
|
|
67
|
+
|
|
68
|
+
def register_command(self, name: str, handler: Callable[..., Any]) -> None:
|
|
69
|
+
"""注册命令."""
|
|
70
|
+
if name in self._commands:
|
|
71
|
+
raise ValueError(f"命令 {name} 已注册")
|
|
72
|
+
self._commands[name] = handler
|
|
73
|
+
|
|
74
|
+
def get_plugin(self, name: str) -> Plugin | None:
|
|
75
|
+
"""获取插件."""
|
|
76
|
+
return self._plugins.get(name)
|
|
77
|
+
|
|
78
|
+
def get_command(self, name: str) -> Callable[..., Any] | None:
|
|
79
|
+
"""获取命令处理器."""
|
|
80
|
+
return self._commands.get(name)
|
|
81
|
+
|
|
82
|
+
def list_plugins(self) -> list[str]:
|
|
83
|
+
"""列出所有已注册的插件."""
|
|
84
|
+
return list(self._plugins.keys())
|
|
85
|
+
|
|
86
|
+
def list_commands(self) -> list[str]:
|
|
87
|
+
"""列出所有已注册的命令."""
|
|
88
|
+
return list(self._commands.keys())
|
|
89
|
+
|
|
90
|
+
def load_plugins_from_directory(self, directory: str | Path) -> None:
|
|
91
|
+
"""从目录加载插件."""
|
|
92
|
+
directory = Path(directory).expanduser()
|
|
93
|
+
if not directory.exists():
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
for plugin_file in directory.glob("*.py"):
|
|
97
|
+
self._load_plugin_module(plugin_file)
|
|
98
|
+
|
|
99
|
+
def _load_plugin_module(self, plugin_file: Path) -> None:
|
|
100
|
+
"""加载单个插件模块."""
|
|
101
|
+
import sys
|
|
102
|
+
|
|
103
|
+
spec = importlib.util.spec_from_file_location(plugin_file.stem, plugin_file)
|
|
104
|
+
if spec is None or spec.loader is None:
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
module = importlib.util.module_from_spec(spec)
|
|
108
|
+
sys.modules[plugin_file.stem] = module
|
|
109
|
+
spec.loader.exec_module(module)
|
|
110
|
+
|
|
111
|
+
# 查找 Plugin 子类并实例化
|
|
112
|
+
for attr_name in dir(module):
|
|
113
|
+
attr = getattr(module, attr_name)
|
|
114
|
+
if isinstance(attr, type) and issubclass(attr, Plugin) and attr is not Plugin:
|
|
115
|
+
plugin = attr()
|
|
116
|
+
self.register_plugin(plugin)
|
|
117
|
+
plugin.register(self)
|
bitool/core/workspace.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""工作空间管理模块."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class WorkspaceManager:
|
|
9
|
+
"""工作空间管理器."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, root: str | Path | None = None) -> None:
|
|
12
|
+
"""初始化工作空间管理器.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
root: 工作空间根路径
|
|
16
|
+
"""
|
|
17
|
+
self._root = Path(root) if root else Path.cwd()
|
|
18
|
+
self._variables: dict[str, str] = {}
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def root(self) -> str:
|
|
22
|
+
"""获取工作空间根路径."""
|
|
23
|
+
return str(self._root)
|
|
24
|
+
|
|
25
|
+
@root.setter
|
|
26
|
+
def root(self, value: str | Path) -> None:
|
|
27
|
+
"""设置工作空间根路径."""
|
|
28
|
+
self._root = Path(value)
|
|
29
|
+
|
|
30
|
+
def set_var(self, name: str, value: str) -> None:
|
|
31
|
+
"""设置工作空间变量.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
name: 变量名
|
|
35
|
+
value: 变量值
|
|
36
|
+
"""
|
|
37
|
+
self._variables[name] = value
|
|
38
|
+
|
|
39
|
+
def get_var(self, name: str, default: str = "") -> str:
|
|
40
|
+
"""获取工作空间变量.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
name: 变量名
|
|
44
|
+
default: 默认值
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
变量值
|
|
48
|
+
"""
|
|
49
|
+
return self._variables.get(name, default)
|
|
50
|
+
|
|
51
|
+
def resolve_path(self, relative_path: str | Path) -> Path:
|
|
52
|
+
"""解析相对于工作空间根目录的路径.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
relative_path: 相对路径
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
绝对路径
|
|
59
|
+
"""
|
|
60
|
+
path = Path(relative_path)
|
|
61
|
+
if path.is_absolute():
|
|
62
|
+
return path
|
|
63
|
+
return self._root / path
|
|
64
|
+
|
|
65
|
+
def ensure_dir(self, relative_path: str | Path) -> Path:
|
|
66
|
+
"""确保目录存在.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
relative_path: 相对路径
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
绝对路径
|
|
73
|
+
"""
|
|
74
|
+
path = self.resolve_path(relative_path)
|
|
75
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
76
|
+
return path
|
bitool/models/version.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from functools import lru_cache
|
|
7
|
+
from re import Pattern
|
|
8
|
+
from typing import Final
|
|
9
|
+
|
|
10
|
+
VERSION_PATTERN: Final[Pattern[str]] = re.compile(
|
|
11
|
+
r"(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class VersionPart(Enum):
|
|
16
|
+
"""可更新的版本号部分."""
|
|
17
|
+
|
|
18
|
+
MAJOR = "major"
|
|
19
|
+
MINOR = "minor"
|
|
20
|
+
PATCH = "patch"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class VersionError(Exception):
|
|
24
|
+
"""版本号相关异常."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class Version:
|
|
29
|
+
"""语义化版本号, 遵循 SemVer 2.0.0 规范.
|
|
30
|
+
|
|
31
|
+
Attributes
|
|
32
|
+
----------
|
|
33
|
+
major: 主版本号 (破坏性变更时递增)
|
|
34
|
+
minor: 次版本号 (向后兼容的功能新增时递增)
|
|
35
|
+
patch: 补丁号 (向后兼容的 bug 修复时递增)
|
|
36
|
+
prerelease: 预发布标识 (如 "alpha", "beta.1", "rc")
|
|
37
|
+
buildmetadata: 构建元数据 (不参与版本比较)
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
major: int
|
|
41
|
+
minor: int
|
|
42
|
+
patch: int
|
|
43
|
+
prerelease: str | None = None
|
|
44
|
+
buildmetadata: str | None = None
|
|
45
|
+
|
|
46
|
+
def __str__(self) -> str:
|
|
47
|
+
"""返回版本号的字符串表示."""
|
|
48
|
+
version = f"{self.major}.{self.minor}.{self.patch}"
|
|
49
|
+
if self.prerelease:
|
|
50
|
+
version += f"-{self.prerelease}"
|
|
51
|
+
if self.buildmetadata:
|
|
52
|
+
version += f"+{self.buildmetadata}"
|
|
53
|
+
return version
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
@lru_cache(maxsize=128)
|
|
57
|
+
def parse(cls, version_string: str) -> Version:
|
|
58
|
+
"""解析版本号字符串为 Version 对象.
|
|
59
|
+
|
|
60
|
+
Parameters
|
|
61
|
+
----------
|
|
62
|
+
version_string : str
|
|
63
|
+
版本号字符串
|
|
64
|
+
|
|
65
|
+
Returns
|
|
66
|
+
-------
|
|
67
|
+
Version
|
|
68
|
+
解析后的 Version 对象
|
|
69
|
+
|
|
70
|
+
Raises
|
|
71
|
+
------
|
|
72
|
+
VersionError
|
|
73
|
+
版本号格式无效时抛出
|
|
74
|
+
"""
|
|
75
|
+
if not version_string:
|
|
76
|
+
msg = "版本号字符串不能为空"
|
|
77
|
+
raise VersionError(msg)
|
|
78
|
+
|
|
79
|
+
match = VERSION_PATTERN.match(version_string)
|
|
80
|
+
if not match:
|
|
81
|
+
msg = f"无效的版本号格式: {version_string}"
|
|
82
|
+
raise VersionError(msg)
|
|
83
|
+
|
|
84
|
+
return cls(
|
|
85
|
+
major=int(match.group("major")),
|
|
86
|
+
minor=int(match.group("minor")),
|
|
87
|
+
patch=int(match.group("patch")),
|
|
88
|
+
prerelease=match.group("prerelease"),
|
|
89
|
+
buildmetadata=match.group("buildmetadata"),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def bump(
|
|
93
|
+
self,
|
|
94
|
+
part: VersionPart,
|
|
95
|
+
*,
|
|
96
|
+
reset_prerelease: bool = True,
|
|
97
|
+
prerelease: str | None = None,
|
|
98
|
+
) -> Version:
|
|
99
|
+
"""返回递增指定部分后的新版本号.
|
|
100
|
+
|
|
101
|
+
Parameters
|
|
102
|
+
----------
|
|
103
|
+
part : VersionPart
|
|
104
|
+
要递增的版本号部分 (major, minor, patch)
|
|
105
|
+
reset_prerelease : bool
|
|
106
|
+
是否重置预发布标识, 默认是
|
|
107
|
+
prerelease : str | None
|
|
108
|
+
可选的预发布标识, 默认 None
|
|
109
|
+
|
|
110
|
+
Returns
|
|
111
|
+
-------
|
|
112
|
+
Version
|
|
113
|
+
递增后的新版本号
|
|
114
|
+
"""
|
|
115
|
+
# 确定新的预发布标识值
|
|
116
|
+
new_prerelease = None
|
|
117
|
+
if prerelease is not None:
|
|
118
|
+
new_prerelease = prerelease
|
|
119
|
+
elif not reset_prerelease:
|
|
120
|
+
new_prerelease = self.prerelease
|
|
121
|
+
|
|
122
|
+
if part == VersionPart.MAJOR:
|
|
123
|
+
return Version(
|
|
124
|
+
major=self.major + 1,
|
|
125
|
+
minor=0,
|
|
126
|
+
patch=0,
|
|
127
|
+
prerelease=new_prerelease,
|
|
128
|
+
buildmetadata=None, # 总是重置构建元数据
|
|
129
|
+
)
|
|
130
|
+
if part == VersionPart.MINOR:
|
|
131
|
+
return Version(
|
|
132
|
+
major=self.major,
|
|
133
|
+
minor=self.minor + 1,
|
|
134
|
+
patch=0,
|
|
135
|
+
prerelease=new_prerelease,
|
|
136
|
+
buildmetadata=None,
|
|
137
|
+
)
|
|
138
|
+
if part == VersionPart.PATCH:
|
|
139
|
+
return Version(
|
|
140
|
+
major=self.major,
|
|
141
|
+
minor=self.minor,
|
|
142
|
+
patch=self.patch + 1,
|
|
143
|
+
prerelease=new_prerelease,
|
|
144
|
+
buildmetadata=None,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
msg = f"不支持的版本号部分: {part}"
|
|
148
|
+
raise VersionError(msg)
|
|
149
|
+
|
|
150
|
+
def set_prerelease(self, prerelease: str) -> Version:
|
|
151
|
+
"""返回设置预发布标识后的新版本号.
|
|
152
|
+
|
|
153
|
+
Parameters
|
|
154
|
+
----------
|
|
155
|
+
prerelease : str
|
|
156
|
+
预发布标识
|
|
157
|
+
|
|
158
|
+
Returns
|
|
159
|
+
-------
|
|
160
|
+
Version
|
|
161
|
+
设置预发布标识后的新版本号
|
|
162
|
+
"""
|
|
163
|
+
if not prerelease:
|
|
164
|
+
msg = "预发布标识不能为空"
|
|
165
|
+
raise VersionError(msg)
|
|
166
|
+
|
|
167
|
+
return Version(
|
|
168
|
+
major=self.major,
|
|
169
|
+
minor=self.minor,
|
|
170
|
+
patch=self.patch,
|
|
171
|
+
prerelease=prerelease,
|
|
172
|
+
buildmetadata=None,
|
|
173
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Scripts 工具模块."""
|