xutk 0.3.4__tar.gz
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.
- xutk-0.3.4/PKG-INFO +89 -0
- xutk-0.3.4/README.md +80 -0
- xutk-0.3.4/pyproject.toml +23 -0
- xutk-0.3.4/setup.cfg +4 -0
- xutk-0.3.4/xutk/__init__.py +18 -0
- xutk-0.3.4/xutk/log.py +175 -0
- xutk-0.3.4/xutk/mp_runner.py +464 -0
- xutk-0.3.4/xutk/perf.py +89 -0
- xutk-0.3.4/xutk/verchk.py +34 -0
- xutk-0.3.4/xutk.egg-info/PKG-INFO +89 -0
- xutk-0.3.4/xutk.egg-info/SOURCES.txt +12 -0
- xutk-0.3.4/xutk.egg-info/dependency_links.txt +1 -0
- xutk-0.3.4/xutk.egg-info/requires.txt +2 -0
- xutk-0.3.4/xutk.egg-info/top_level.txt +1 -0
xutk-0.3.4/PKG-INFO
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: xutk
|
|
3
|
+
Version: 0.3.4
|
|
4
|
+
Summary: Xulab toolkit in python
|
|
5
|
+
Requires-Python: <3.15,>=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: colorama>=0.4.6
|
|
8
|
+
Requires-Dist: psutil>=7.1.0
|
|
9
|
+
|
|
10
|
+
# Xulab Useful Toolkit in Python (xutk)
|
|
11
|
+
|
|
12
|
+

|
|
13
|
+

|
|
14
|
+
|
|
15
|
+
## 导入项目
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install \
|
|
19
|
+
--trusted-host "gitea.xulab.ion.ac.cn" \
|
|
20
|
+
--extra-index-url "http://gitea.xulab.ion.ac.cn/api/packages/zelin2001/pypi/simple/" \
|
|
21
|
+
xutk
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
或者使用 uv:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
uv add --index http://gitea.xulab.ion.ac.cn/api/packages/zelin2001/pypi/simple xutk
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## 快速开始
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from xutk import verchk, log, mp_runner
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
|
|
36
|
+
# 版本检查
|
|
37
|
+
verchk.check_version("0.2.0", "some_package")
|
|
38
|
+
|
|
39
|
+
# 日志记录
|
|
40
|
+
logger = log.CtxLogger("my_app")
|
|
41
|
+
logger.info({"module": "processor", "file": "data.csv"}, "Processing started")
|
|
42
|
+
|
|
43
|
+
# 多进程运行器
|
|
44
|
+
runner = mp_runner.mprunner_factory(2, log_file=Path("process.log")) # 进程队列,取 2 个运行
|
|
45
|
+
runner.batch_run(["echo", "-E", f"test{num}"] for num in range(1,25)) # 排队 25 个进程
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**建议在项目 `__init__.py` 创建 CtxLogger("my_app"),避免实例化顺序问题。**
|
|
49
|
+
|
|
50
|
+
## 项目结构
|
|
51
|
+
|
|
52
|
+
```text
|
|
53
|
+
xutk/
|
|
54
|
+
├── xutk/
|
|
55
|
+
│ ├── __init__.py # 包初始化
|
|
56
|
+
│ ├── log.py # 日志记录功能
|
|
57
|
+
│ ├── mp_runner.py # 多进程运行管理
|
|
58
|
+
│ ├── perf.py # 快速资源检查
|
|
59
|
+
│ └── verchk.py # 版本检查功能
|
|
60
|
+
├── test/
|
|
61
|
+
│ ├── integration/ # 集成测试
|
|
62
|
+
│ └── unit/ # 单元测试
|
|
63
|
+
├── gitea/
|
|
64
|
+
│ └── workflows/ # CI 工作流
|
|
65
|
+
├── pyproject.toml # 项目配置
|
|
66
|
+
└── README.md # 项目文档
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## 开发
|
|
70
|
+
|
|
71
|
+
### 安装开发依赖
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
uv sync --all-extras
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 运行测试
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
uv run pytest
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 代码检查
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
uv run ruff check # 代码风格检查
|
|
87
|
+
uv run ty check # 类型检查
|
|
88
|
+
uv run pytest # 样例测试
|
|
89
|
+
```
|
xutk-0.3.4/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Xulab Useful Toolkit in Python (xutk)
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+
|
|
6
|
+
## 导入项目
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install \
|
|
10
|
+
--trusted-host "gitea.xulab.ion.ac.cn" \
|
|
11
|
+
--extra-index-url "http://gitea.xulab.ion.ac.cn/api/packages/zelin2001/pypi/simple/" \
|
|
12
|
+
xutk
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
或者使用 uv:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
uv add --index http://gitea.xulab.ion.ac.cn/api/packages/zelin2001/pypi/simple xutk
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## 快速开始
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from xutk import verchk, log, mp_runner
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
# 版本检查
|
|
28
|
+
verchk.check_version("0.2.0", "some_package")
|
|
29
|
+
|
|
30
|
+
# 日志记录
|
|
31
|
+
logger = log.CtxLogger("my_app")
|
|
32
|
+
logger.info({"module": "processor", "file": "data.csv"}, "Processing started")
|
|
33
|
+
|
|
34
|
+
# 多进程运行器
|
|
35
|
+
runner = mp_runner.mprunner_factory(2, log_file=Path("process.log")) # 进程队列,取 2 个运行
|
|
36
|
+
runner.batch_run(["echo", "-E", f"test{num}"] for num in range(1,25)) # 排队 25 个进程
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**建议在项目 `__init__.py` 创建 CtxLogger("my_app"),避免实例化顺序问题。**
|
|
40
|
+
|
|
41
|
+
## 项目结构
|
|
42
|
+
|
|
43
|
+
```text
|
|
44
|
+
xutk/
|
|
45
|
+
├── xutk/
|
|
46
|
+
│ ├── __init__.py # 包初始化
|
|
47
|
+
│ ├── log.py # 日志记录功能
|
|
48
|
+
│ ├── mp_runner.py # 多进程运行管理
|
|
49
|
+
│ ├── perf.py # 快速资源检查
|
|
50
|
+
│ └── verchk.py # 版本检查功能
|
|
51
|
+
├── test/
|
|
52
|
+
│ ├── integration/ # 集成测试
|
|
53
|
+
│ └── unit/ # 单元测试
|
|
54
|
+
├── gitea/
|
|
55
|
+
│ └── workflows/ # CI 工作流
|
|
56
|
+
├── pyproject.toml # 项目配置
|
|
57
|
+
└── README.md # 项目文档
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## 开发
|
|
61
|
+
|
|
62
|
+
### 安装开发依赖
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
uv sync --all-extras
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 运行测试
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
uv run pytest
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 代码检查
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
uv run ruff check # 代码风格检查
|
|
78
|
+
uv run ty check # 类型检查
|
|
79
|
+
uv run pytest # 样例测试
|
|
80
|
+
```
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "xutk"
|
|
3
|
+
version = "0.3.4"
|
|
4
|
+
description = "Xulab toolkit in python"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11,<3.15"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"colorama>=0.4.6",
|
|
9
|
+
"psutil>=7.1.0",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[dependency-groups]
|
|
13
|
+
dev = [
|
|
14
|
+
"pytest>=8.4.1",
|
|
15
|
+
"ruff>=0.12.4",
|
|
16
|
+
"ty>=0.0.1a14",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
# [[tool.uv.index]]
|
|
20
|
+
# name = "gitea-zelin2001"
|
|
21
|
+
# url = "http://gitea.xulab.ion.ac.cn/api/packages/zelin2001/pypi/simple"
|
|
22
|
+
# publish-url = "http://gitea.xulab.ion.ac.cn/api/packages/zelin2001/pypi"
|
|
23
|
+
|
xutk-0.3.4/setup.cfg
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Xulab Useful Toolkits for Python.
|
|
2
|
+
|
|
3
|
+
- module `log`:
|
|
4
|
+
- class CtxLogger
|
|
5
|
+
- set_log_level()
|
|
6
|
+
- module `mp_runner`:
|
|
7
|
+
- class MPRunner
|
|
8
|
+
- class ThreadPoolRunner
|
|
9
|
+
- dataclass SLURMConfig
|
|
10
|
+
- class SLURMRunner
|
|
11
|
+
- mprunner_factory()
|
|
12
|
+
- module `perf`:
|
|
13
|
+
- class Perf
|
|
14
|
+
- fadvise_remove_cache()
|
|
15
|
+
- module `verchk`:
|
|
16
|
+
- check_version()
|
|
17
|
+
|
|
18
|
+
"""
|
xutk-0.3.4/xutk/log.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Logging for A package with level-specific, contex aware messages.
|
|
2
|
+
|
|
3
|
+
- Use `XUTK_LOG_LEVEL` environment variable to set logging level
|
|
4
|
+
level: One of 'DEBUG', 'INFO', 'ARNING', 'ERROR'
|
|
5
|
+
- Alternatively, use function `set_log_level()`
|
|
6
|
+
- Use `CtxLogger` to initialize specific logger
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import colorama
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
from typing import Dict, Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class _ColorfulFormatter(logging.Formatter):
|
|
17
|
+
"""A custom logging formatter that adds color to log messages.
|
|
18
|
+
|
|
19
|
+
This formatter uses colorama to add ANSI color codes to log messages
|
|
20
|
+
based on their severity level, making them easier to distinguish
|
|
21
|
+
in terminal output.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def formatMessage(self, record: logging.LogRecord) -> str:
|
|
25
|
+
"""Format the message part of the log record with color.
|
|
26
|
+
|
|
27
|
+
This method is called by the parent format() method and only
|
|
28
|
+
adds color to the levelname while preserving all other standard
|
|
29
|
+
formatting behavior.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
record: The log record to format
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
A formatted string with colored level name
|
|
36
|
+
"""
|
|
37
|
+
# Color mapping for log levels
|
|
38
|
+
log_colors = {
|
|
39
|
+
"NOTSET": colorama.Fore.WHITE,
|
|
40
|
+
"DEBUG": colorama.Fore.CYAN,
|
|
41
|
+
"INFO": colorama.Fore.GREEN,
|
|
42
|
+
"WARNING": colorama.Fore.YELLOW + colorama.Style.BRIGHT,
|
|
43
|
+
"ERROR": colorama.Fore.RED + colorama.Style.BRIGHT,
|
|
44
|
+
"CRITICAL": colorama.Fore.RED + colorama.Style.BRIGHT,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Passing colored levelname to parent's formatMessage
|
|
48
|
+
original_levelname = record.levelname
|
|
49
|
+
record.levelname = (
|
|
50
|
+
f"{log_colors.get(original_levelname, '')}"
|
|
51
|
+
+ f"{original_levelname}{colorama.Style.RESET_ALL}"
|
|
52
|
+
)
|
|
53
|
+
try:
|
|
54
|
+
# Use parent's formatMessage to handle the actual formatting
|
|
55
|
+
result = super().formatMessage(record)
|
|
56
|
+
finally:
|
|
57
|
+
# Restore original levelname
|
|
58
|
+
record.levelname = original_levelname
|
|
59
|
+
|
|
60
|
+
return result
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class CtxLogger:
|
|
64
|
+
"""Logger with built-in context formatting and level methods."""
|
|
65
|
+
|
|
66
|
+
caller_names: set[str]
|
|
67
|
+
_logger: logging.Logger
|
|
68
|
+
_color_flag: bool
|
|
69
|
+
|
|
70
|
+
def __init__(self, name: str = "xutk") -> None:
|
|
71
|
+
"""Initialize the CtxLogger with basic configuration.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
name: Logger name (defaults to 'xutk'). Creates a new logger or returns
|
|
75
|
+
existing one if name matches.
|
|
76
|
+
|
|
77
|
+
Configures logging level from XUTK_LOG_LEVEL (PKG_LOG_LEVEL)
|
|
78
|
+
environment variable or set_log_level() (defaults to INFO)
|
|
79
|
+
and sets up a basic console handler with standard formatting.
|
|
80
|
+
|
|
81
|
+
Also override color output setting by environment variable
|
|
82
|
+
XUTK_LOG_COLOR (PKG_LOG_COLOR), True (true) or False (false).
|
|
83
|
+
"""
|
|
84
|
+
self._logger = logging.getLogger(name)
|
|
85
|
+
self.caller_names = {"caller", "plotter", "loader", "processor"}
|
|
86
|
+
self._init_handler_and_color(name)
|
|
87
|
+
|
|
88
|
+
def _init_handler_and_color(self, name: str) -> None:
|
|
89
|
+
color_opt = os.getenv(name.upper() + "_LOG_COLOR", "auto").lower()
|
|
90
|
+
color_flag = False
|
|
91
|
+
if "auto" == color_opt:
|
|
92
|
+
color_flag = sys.stdout.isatty()
|
|
93
|
+
elif color_opt in {"true", "1", "yes", "y"}:
|
|
94
|
+
color_flag = True
|
|
95
|
+
if color_flag:
|
|
96
|
+
colorama.init()
|
|
97
|
+
self._color_flag = color_flag
|
|
98
|
+
|
|
99
|
+
if not self._logger.handlers:
|
|
100
|
+
# Set level from environment or default to INFO
|
|
101
|
+
logger_level = name.upper() + "_LOG_LEVEL"
|
|
102
|
+
level = os.getenv(logger_level, "INFO").upper()
|
|
103
|
+
self._logger.setLevel(getattr(logging, level, logging.INFO))
|
|
104
|
+
|
|
105
|
+
handler = logging.StreamHandler()
|
|
106
|
+
formatter = (
|
|
107
|
+
_ColorfulFormatter(
|
|
108
|
+
"%(asctime)s [%(levelname)s] %(message)s", datefmt="%H:%M:%S"
|
|
109
|
+
)
|
|
110
|
+
if self._color_flag
|
|
111
|
+
else logging.Formatter(
|
|
112
|
+
"%(asctime)s [%(levelname)s] %(message)s", datefmt="%H:%M:%S"
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
handler.setFormatter(formatter)
|
|
116
|
+
self._logger.addHandler(handler)
|
|
117
|
+
|
|
118
|
+
def _log_with_context(
|
|
119
|
+
self, level: int, context: Dict[str, Any], message: str
|
|
120
|
+
) -> None:
|
|
121
|
+
"""Add to log internally."""
|
|
122
|
+
callers = [(k, v) for k, v in context.items() if k in self.caller_names]
|
|
123
|
+
caller_str = ""
|
|
124
|
+
try:
|
|
125
|
+
caller_str = callers[0][1]
|
|
126
|
+
except IndexError:
|
|
127
|
+
pass
|
|
128
|
+
|
|
129
|
+
if self._color_flag:
|
|
130
|
+
message = (
|
|
131
|
+
str(colorama.Style.BRIGHT) + message + str(colorama.Style.RESET_ALL)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
ctx_items = [
|
|
135
|
+
f"{k}: {v}" for k, v in context.items() if k not in self.caller_names
|
|
136
|
+
]
|
|
137
|
+
ctx_str = "; ".join(ctx_items)
|
|
138
|
+
|
|
139
|
+
self._logger.log(
|
|
140
|
+
level,
|
|
141
|
+
f"{caller_str}: {message}"
|
|
142
|
+
+ f"{' | ' if len(ctx_str) > 0 else ''}{ctx_str}",
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def debug(self, context: Dict[str, Any], message: str) -> None:
|
|
146
|
+
"""Log debug message with context."""
|
|
147
|
+
self._log_with_context(logging.DEBUG, context, message)
|
|
148
|
+
|
|
149
|
+
def info(self, context: Dict[str, Any], message: str) -> None:
|
|
150
|
+
"""Log info message with context."""
|
|
151
|
+
self._log_with_context(logging.INFO, context, message)
|
|
152
|
+
|
|
153
|
+
def warning(self, context: Dict[str, Any], message: str) -> None:
|
|
154
|
+
"""Log warning message with context."""
|
|
155
|
+
self._log_with_context(logging.WARNING, context, message)
|
|
156
|
+
|
|
157
|
+
def error(self, context: Dict[str, Any], message: str) -> None:
|
|
158
|
+
"""Log error message with context."""
|
|
159
|
+
self._log_with_context(logging.ERROR, context, message)
|
|
160
|
+
|
|
161
|
+
def get_log_level(self) -> int:
|
|
162
|
+
"""Get log level (number)."""
|
|
163
|
+
return self._logger.getEffectiveLevel()
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def set_log_level(level: str, logger_name: str = "xutk") -> None:
|
|
167
|
+
"""Set log level programmatically.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
level: One of 'DEBUG', 'INFO', 'WARNING', 'ERROR'
|
|
171
|
+
(Neither 'NOTSET' nor 'CRITICAL' are provided)
|
|
172
|
+
logger_name: Name of the logger to configure (defaults to 'xutk')
|
|
173
|
+
"""
|
|
174
|
+
logger = logging.getLogger(logger_name)
|
|
175
|
+
logger.setLevel(getattr(logging, level.upper(), logging.INFO))
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
"""Parallel computing runner module.
|
|
2
|
+
|
|
3
|
+
This module provides a flexible interface for executing tasks
|
|
4
|
+
using different parallel computing backends. It implements a factory pattern
|
|
5
|
+
through mprunner_factory that creates appropriate runners based on the client
|
|
6
|
+
type provided.
|
|
7
|
+
|
|
8
|
+
The module supports multiple execution modes:
|
|
9
|
+
1. ThreadPoolExecutor: For local multi-threaded processing
|
|
10
|
+
2. SLURMConfig: For job submission to SLURM clusters
|
|
11
|
+
3. DaskClient (future): For distributed computing with Dask
|
|
12
|
+
|
|
13
|
+
Key Components:
|
|
14
|
+
- MPRunner: Abstract base class defining the interface for all runners
|
|
15
|
+
- ThreadPoolRunner: Implementation for local thread-based parallelism
|
|
16
|
+
- SLURMRunner: Implementation for SLURM cluster job submission
|
|
17
|
+
- SLURMConfig: Configuration class for SLURM job parameters
|
|
18
|
+
- mprunner_factory: Factory function to create appropriate runner instances
|
|
19
|
+
- MPClient: Type alias for Union[ThreadPoolExecutor, SLURMConfig]
|
|
20
|
+
|
|
21
|
+
The API provides two main execution methods:
|
|
22
|
+
- batch_run: For executing command-line operations
|
|
23
|
+
- batch_function_run: For executing Python functions
|
|
24
|
+
|
|
25
|
+
Each runner implementation handles the execution details while providing
|
|
26
|
+
a consistent interface. The abstract methods _submit_command, _submit_function,
|
|
27
|
+
_join_futures, and _cancel_futures provide extension points for new runners.
|
|
28
|
+
|
|
29
|
+
Usage:
|
|
30
|
+
```python
|
|
31
|
+
# For local processing with limited workers
|
|
32
|
+
executor = ThreadPoolExecutor(max_workers=4)
|
|
33
|
+
runner = mprunner_factory(executor, log_file)
|
|
34
|
+
runner.batch_run(cmd_args_list)
|
|
35
|
+
runner.batch_function_run(process_video, video_paths, presets)
|
|
36
|
+
|
|
37
|
+
# For SLURM cluster processing
|
|
38
|
+
config = SLURMConfig(partition="batch", num_cpus=12, memory="8G", output=log_file)
|
|
39
|
+
runner = mprunner_factory(config, log_file)
|
|
40
|
+
runner.batch_run(cmd_args_list)
|
|
41
|
+
|
|
42
|
+
# For future Dask distributed processing
|
|
43
|
+
client = Client("scheduler-address:8786")
|
|
44
|
+
runner = mprunner_factory(client, log_file)
|
|
45
|
+
runner.batch_function_run(process_video, video_paths, presets)
|
|
46
|
+
|
|
47
|
+
# Using the MPClient type alias for better type hints
|
|
48
|
+
def process_videos(client: MPClient, videos: list[str]) -> None:
|
|
49
|
+
runner = mprunner_factory(client)
|
|
50
|
+
runner.batch_run(create_ffmpeg_commands(videos))
|
|
51
|
+
```
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
from abc import ABC, abstractmethod
|
|
55
|
+
from dataclasses import dataclass
|
|
56
|
+
from concurrent.futures import Future, ThreadPoolExecutor
|
|
57
|
+
from pathlib import Path
|
|
58
|
+
import queue
|
|
59
|
+
import shlex
|
|
60
|
+
from subprocess import run, CompletedProcess
|
|
61
|
+
import threading
|
|
62
|
+
from typing import Callable
|
|
63
|
+
|
|
64
|
+
from xutk.log import CtxLogger as Logger
|
|
65
|
+
|
|
66
|
+
vidtidy_logger = Logger("vidtidy")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class MPRunner(ABC):
|
|
70
|
+
"""Handles video processing with string parameters."""
|
|
71
|
+
|
|
72
|
+
def __init__(self, client, log_file: Path | None) -> None: # noqa: ANN001
|
|
73
|
+
"""Initialize with basic file patterns and optional config.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
client: Configuration or executor for the runner
|
|
77
|
+
log_file: Path to the log file (optional, if None logging is disabled)
|
|
78
|
+
|
|
79
|
+
"""
|
|
80
|
+
self._log_queue = queue.Queue()
|
|
81
|
+
self._log_lock = threading.Lock()
|
|
82
|
+
|
|
83
|
+
if log_file:
|
|
84
|
+
self._log_file = log_file
|
|
85
|
+
self._log_thread = threading.Thread(target=self._log_writer, daemon=True)
|
|
86
|
+
self._log_thread.start()
|
|
87
|
+
else:
|
|
88
|
+
self._log_file = Path()
|
|
89
|
+
self._log_thread = None
|
|
90
|
+
|
|
91
|
+
self.client = client
|
|
92
|
+
|
|
93
|
+
def _log_writer(self) -> None:
|
|
94
|
+
"""Background thread that writes logs sequentially.
|
|
95
|
+
|
|
96
|
+
This method runs as a daemon thread, continuously processing log messages
|
|
97
|
+
from the queue and writing them to the log file. Only active when log_file
|
|
98
|
+
is not None.
|
|
99
|
+
"""
|
|
100
|
+
if not self._log_file:
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
while True:
|
|
104
|
+
log_msg = self._log_queue.get()
|
|
105
|
+
try:
|
|
106
|
+
with self._log_file.open("a") as f:
|
|
107
|
+
f.write(log_msg)
|
|
108
|
+
except Exception as e:
|
|
109
|
+
vidtidy_logger.error(
|
|
110
|
+
{"caller": self.__class__.__name__}, f"Failed to write log: {e}"
|
|
111
|
+
)
|
|
112
|
+
self._log_queue.task_done()
|
|
113
|
+
|
|
114
|
+
@abstractmethod
|
|
115
|
+
def batch_run(self, cmd_args_list: list[list[str]]) -> list:
|
|
116
|
+
"""Run multiple commands synchronously, blocking until completion.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
cmd_args_list: List of command argument lists to execute
|
|
120
|
+
|
|
121
|
+
"""
|
|
122
|
+
...
|
|
123
|
+
|
|
124
|
+
@abstractmethod
|
|
125
|
+
def batch_function_run(
|
|
126
|
+
self,
|
|
127
|
+
func: Callable,
|
|
128
|
+
*args_lists: list,
|
|
129
|
+
**common_kwargs, # noqa: ANN003
|
|
130
|
+
) -> list:
|
|
131
|
+
"""Run multiple instances of a function together, blocking until completion.
|
|
132
|
+
|
|
133
|
+
Supports two calling conventions:
|
|
134
|
+
1. Multiple argument lists: batch_function_run
|
|
135
|
+
(func, arg1_list, arg2_list, kwarg=value)
|
|
136
|
+
2. List of argument tuples: batch_function_run
|
|
137
|
+
(func, [(arg1, arg2), ...], kwarg=value)
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
func: The function to execute
|
|
141
|
+
*args_lists: Variable length argument lists
|
|
142
|
+
**common_kwargs: Common keyword arguments for all function calls
|
|
143
|
+
|
|
144
|
+
"""
|
|
145
|
+
...
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class ThreadPoolRunner(MPRunner):
|
|
149
|
+
"""Handles video processing with string parameters."""
|
|
150
|
+
|
|
151
|
+
client: ThreadPoolExecutor
|
|
152
|
+
|
|
153
|
+
def batch_run(self, cmd_args_list: list[list[str]]) -> list[CompletedProcess[str]]: # noqa: D102
|
|
154
|
+
futures: list[Future[CompletedProcess[str]]] = []
|
|
155
|
+
results: list[CompletedProcess[str]] = []
|
|
156
|
+
try:
|
|
157
|
+
with self.client as executor:
|
|
158
|
+
for cmd_item in cmd_args_list:
|
|
159
|
+
future = executor.submit(self._thread_pool_execute, cmd_item)
|
|
160
|
+
futures.append(future)
|
|
161
|
+
# Wait for all futures and logs to complete
|
|
162
|
+
try:
|
|
163
|
+
results.extend(future.result() for future in futures)
|
|
164
|
+
self._log_queue.join()
|
|
165
|
+
except KeyboardInterrupt:
|
|
166
|
+
vidtidy_logger.warning(
|
|
167
|
+
{"caller": self.__class__.__name__},
|
|
168
|
+
"\nProcessing interrupted by user. Cleaning up...",
|
|
169
|
+
)
|
|
170
|
+
# Cancel all pending futures on interrupt
|
|
171
|
+
for future in futures:
|
|
172
|
+
future.cancel()
|
|
173
|
+
# Wait for any running tasks to complete
|
|
174
|
+
for future in futures:
|
|
175
|
+
if not future.done():
|
|
176
|
+
future.result()
|
|
177
|
+
self._log_queue.join()
|
|
178
|
+
raise
|
|
179
|
+
except KeyboardInterrupt:
|
|
180
|
+
vidtidy_logger.warning(
|
|
181
|
+
{"caller": self.__class__.__name__},
|
|
182
|
+
"\nJob scheduling interrupted by user. Cleaning up...",
|
|
183
|
+
)
|
|
184
|
+
raise
|
|
185
|
+
return results
|
|
186
|
+
|
|
187
|
+
def _thread_pool_execute(self, cmd_args: list[str]) -> CompletedProcess[str]:
|
|
188
|
+
process = run(cmd_args, check=True, capture_output=True, text=True)
|
|
189
|
+
with self._log_lock:
|
|
190
|
+
# FFmpeg uses stderr for normal output and stdout for progress
|
|
191
|
+
# Use the last argument as the processing file if available,
|
|
192
|
+
# otherwise use command name
|
|
193
|
+
target_file = cmd_args[-1] if len(cmd_args) > 1 else cmd_args[0]
|
|
194
|
+
self._log_queue.put(
|
|
195
|
+
f"=== Processing {target_file} ===\n"
|
|
196
|
+
f"Output:\n{process.stdout}\n"
|
|
197
|
+
f"Errors:\n{process.stderr}\n\n"
|
|
198
|
+
)
|
|
199
|
+
return process
|
|
200
|
+
|
|
201
|
+
def batch_function_run( # noqa: D102
|
|
202
|
+
self,
|
|
203
|
+
func: Callable,
|
|
204
|
+
*args_lists: list,
|
|
205
|
+
**common_kwargs, # noqa: ANN003
|
|
206
|
+
) -> list:
|
|
207
|
+
if not args_lists:
|
|
208
|
+
vidtidy_logger.warning(
|
|
209
|
+
{"caller": self.__class__.__name__},
|
|
210
|
+
"\nBatch runner got args_list with different length.",
|
|
211
|
+
)
|
|
212
|
+
return []
|
|
213
|
+
|
|
214
|
+
arg_list_tuple = self._batch_function_parse_args(args_lists)
|
|
215
|
+
|
|
216
|
+
# === Build Tasks and Submit ===
|
|
217
|
+
futures: list[Future] = []
|
|
218
|
+
results: list = []
|
|
219
|
+
try:
|
|
220
|
+
with self.client as executor:
|
|
221
|
+
for args in arg_list_tuple:
|
|
222
|
+
future = executor.submit(func, *args, **common_kwargs)
|
|
223
|
+
futures.append(future)
|
|
224
|
+
|
|
225
|
+
# Wait for completion and collect results in order
|
|
226
|
+
try:
|
|
227
|
+
# Wait for all futures and collect results in original order
|
|
228
|
+
results.extend(future.result() for future in futures)
|
|
229
|
+
self._log_queue.join()
|
|
230
|
+
except KeyboardInterrupt:
|
|
231
|
+
vidtidy_logger.warning(
|
|
232
|
+
{"caller": self.__class__.__name__},
|
|
233
|
+
"\nProcessing interrupted by user. Cancelling tasks...",
|
|
234
|
+
)
|
|
235
|
+
for future in futures:
|
|
236
|
+
future.cancel()
|
|
237
|
+
# Allow running tasks to finish cleanly
|
|
238
|
+
for future in futures:
|
|
239
|
+
if not future.done():
|
|
240
|
+
try:
|
|
241
|
+
future.result()
|
|
242
|
+
except: # noqa: E722
|
|
243
|
+
pass
|
|
244
|
+
self._log_queue.join()
|
|
245
|
+
raise
|
|
246
|
+
|
|
247
|
+
return results
|
|
248
|
+
|
|
249
|
+
except KeyboardInterrupt:
|
|
250
|
+
vidtidy_logger.warning(
|
|
251
|
+
{"caller": self.__class__.__name__},
|
|
252
|
+
"\nJob Scheduling interrupted by user. Cleaning up...",
|
|
253
|
+
)
|
|
254
|
+
raise
|
|
255
|
+
|
|
256
|
+
@staticmethod
|
|
257
|
+
def _batch_function_parse_args(args_lists: tuple[list, ...]) -> list[tuple]:
|
|
258
|
+
"""Parse input arguments into list of argument tuples.
|
|
259
|
+
|
|
260
|
+
Supports two calling conventions:
|
|
261
|
+
1. Multiple argument lists: [a1,a2], [b1,b2] -> [(a1,b1), (a2,b2)]
|
|
262
|
+
2. List of tuples: [(a1,b1), (a2,b2)] -> [(a1,b1), (a2,b2)]
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
args_lists: Tuple of argument lists (from *args)
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
List of argument tuples for function calls
|
|
269
|
+
|
|
270
|
+
Raises:
|
|
271
|
+
ValueError: If argument lists have different lengths or invalid format
|
|
272
|
+
|
|
273
|
+
"""
|
|
274
|
+
if not args_lists:
|
|
275
|
+
return []
|
|
276
|
+
|
|
277
|
+
first_arg = args_lists[0]
|
|
278
|
+
|
|
279
|
+
if len(args_lists) == 1 and isinstance(first_arg, list) and first_arg:
|
|
280
|
+
# Case 1: Single list of tuples - [(arg1, arg2), ...]
|
|
281
|
+
if isinstance(first_arg[0], tuple):
|
|
282
|
+
return first_arg # Already in (arg1, arg2) format
|
|
283
|
+
else:
|
|
284
|
+
# Case 2: Single list of non-tuples, treat as single argument
|
|
285
|
+
return [(x,) for x in first_arg]
|
|
286
|
+
else:
|
|
287
|
+
# Case 3: Multiple argument lists - [a1,a2], [b1,b2]
|
|
288
|
+
n_args = len(args_lists[0])
|
|
289
|
+
if any(len(lst) != n_args for lst in args_lists):
|
|
290
|
+
raise ValueError("All argument lists must have the same length.")
|
|
291
|
+
return list(zip(*args_lists))
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@dataclass
|
|
295
|
+
class SLURMConfig:
|
|
296
|
+
"""Configuration for SLURM job submission.
|
|
297
|
+
|
|
298
|
+
Attributes:
|
|
299
|
+
output: Path for stdout log file
|
|
300
|
+
error: Path for stderr log file
|
|
301
|
+
partition: SLURM partition to use
|
|
302
|
+
node: Specific node to run on (optional)
|
|
303
|
+
num_cpus: Number of CPU cores to allocate per task
|
|
304
|
+
memory: Memory allocation in GB
|
|
305
|
+
job_name: Name for the SLURM job
|
|
306
|
+
time_limit: Time limit for the job (HH:MM:SS format)
|
|
307
|
+
|
|
308
|
+
"""
|
|
309
|
+
|
|
310
|
+
output: Path | None = None
|
|
311
|
+
error: Path | None = None
|
|
312
|
+
partition: str = "batch"
|
|
313
|
+
node: str | None = None
|
|
314
|
+
num_cpus: int = 12
|
|
315
|
+
memory: str = "8G"
|
|
316
|
+
job_name: str = "vidtidy_job"
|
|
317
|
+
time_limit: str = "02:00:00"
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
class SLURMRunner(MPRunner):
|
|
321
|
+
"""Handles SLURM job submission for video processing tasks."""
|
|
322
|
+
|
|
323
|
+
client: SLURMConfig
|
|
324
|
+
|
|
325
|
+
def batch_run(self, cmd_args_list: list[list[str]]) -> list[int]:
|
|
326
|
+
"""Submit multiple commands as SLURM jobs using the client configuration.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
cmd_args_list: List of command argument lists to execute
|
|
330
|
+
|
|
331
|
+
Raises:
|
|
332
|
+
RuntimeError: If any job submission fails
|
|
333
|
+
|
|
334
|
+
"""
|
|
335
|
+
job_ids: list[int] = []
|
|
336
|
+
try:
|
|
337
|
+
for i, cmd_args in enumerate(cmd_args_list):
|
|
338
|
+
# Create unique job name for each task
|
|
339
|
+
job_name = f"{self.client.job_name}_{i}"
|
|
340
|
+
|
|
341
|
+
# Build sbatch command with options from config
|
|
342
|
+
sbatch_cmd = [
|
|
343
|
+
"sbatch",
|
|
344
|
+
"--job-name",
|
|
345
|
+
job_name,
|
|
346
|
+
"--time",
|
|
347
|
+
self.client.time_limit,
|
|
348
|
+
"--partition",
|
|
349
|
+
self.client.partition,
|
|
350
|
+
"--cpus-per-task",
|
|
351
|
+
str(self.client.num_cpus),
|
|
352
|
+
"--mem",
|
|
353
|
+
self.client.memory,
|
|
354
|
+
]
|
|
355
|
+
|
|
356
|
+
# Add node specification if provided
|
|
357
|
+
if self.client.node:
|
|
358
|
+
sbatch_cmd.extend(["--nodelist", self.client.node])
|
|
359
|
+
|
|
360
|
+
# Add output and error log paths if specified
|
|
361
|
+
if self.client.output:
|
|
362
|
+
sbatch_cmd.extend(
|
|
363
|
+
[
|
|
364
|
+
"--output",
|
|
365
|
+
str(
|
|
366
|
+
self.client.output.with_suffix(
|
|
367
|
+
f".{i}{self.client.output.suffix}"
|
|
368
|
+
)
|
|
369
|
+
),
|
|
370
|
+
]
|
|
371
|
+
)
|
|
372
|
+
if self.client.error:
|
|
373
|
+
sbatch_cmd.extend(
|
|
374
|
+
[
|
|
375
|
+
"--error",
|
|
376
|
+
str(
|
|
377
|
+
self.client.error.with_suffix(
|
|
378
|
+
f".{i}{self.client.error.suffix}"
|
|
379
|
+
)
|
|
380
|
+
),
|
|
381
|
+
]
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
# Add the actual command to run
|
|
385
|
+
sbatch_cmd.extend(["--wrap", shlex.join(cmd_args)])
|
|
386
|
+
|
|
387
|
+
# Submit the job
|
|
388
|
+
result = run(sbatch_cmd, capture_output=True, text=True, check=True)
|
|
389
|
+
|
|
390
|
+
if result.returncode != 0:
|
|
391
|
+
raise RuntimeError(f"SLURM job submission failed: {result.stderr}")
|
|
392
|
+
|
|
393
|
+
# Extract job ID from output (format: "Submitted batch job <job_id>")
|
|
394
|
+
job_id_line = result.stdout.strip()
|
|
395
|
+
if "Submitted batch job" in job_id_line:
|
|
396
|
+
job_id = int(job_id_line.split()[-1])
|
|
397
|
+
job_ids.append(job_id)
|
|
398
|
+
self._log_queue.put(
|
|
399
|
+
f"Submitted SLURM job {job_id} for "
|
|
400
|
+
+ f"command: {shlex.join(cmd_args)}\n"
|
|
401
|
+
)
|
|
402
|
+
else:
|
|
403
|
+
raise RuntimeError(
|
|
404
|
+
f"Could not parse SLURM job ID from: {job_id_line}"
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
except Exception:
|
|
408
|
+
# Cancel all submitted jobs if there's an error
|
|
409
|
+
for job_id in job_ids:
|
|
410
|
+
run(["scancel", str(job_id)], capture_output=True, check=True)
|
|
411
|
+
raise
|
|
412
|
+
|
|
413
|
+
# Wait for all jobs to complete and logs to be written
|
|
414
|
+
try:
|
|
415
|
+
self._log_queue.join()
|
|
416
|
+
except KeyboardInterrupt:
|
|
417
|
+
# Cancel all jobs on interrupt
|
|
418
|
+
for job_id in job_ids:
|
|
419
|
+
run(["scancel", str(job_id)], capture_output=True, check=True)
|
|
420
|
+
self._log_queue.join()
|
|
421
|
+
raise
|
|
422
|
+
|
|
423
|
+
return job_ids
|
|
424
|
+
|
|
425
|
+
def batch_function_run( # noqa: D102
|
|
426
|
+
self,
|
|
427
|
+
func: Callable,
|
|
428
|
+
*args_lists: list,
|
|
429
|
+
**common_kwargs, # noqa: ANN003
|
|
430
|
+
) -> None:
|
|
431
|
+
raise NotImplementedError(
|
|
432
|
+
"""
|
|
433
|
+
SLURMRunner does not support function execution.
|
|
434
|
+
Use batch_run with shell commands.
|
|
435
|
+
"""
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
MPClient = int | ThreadPoolExecutor | SLURMConfig
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def mprunner_factory(mp_client: MPClient, log_file: Path | None = None) -> MPRunner:
|
|
443
|
+
"""Create appropriate runner based on client type.
|
|
444
|
+
|
|
445
|
+
Args:
|
|
446
|
+
mp_client: A multi-processing implement instance
|
|
447
|
+
log_file: Path to the log file
|
|
448
|
+
|
|
449
|
+
Returns:
|
|
450
|
+
Appropriate runner instance
|
|
451
|
+
(ThreadPoolRunner, SLURMRunner, or future DaskRunner)
|
|
452
|
+
|
|
453
|
+
Raises:
|
|
454
|
+
NotImplementedError: If client type is not supported
|
|
455
|
+
|
|
456
|
+
"""
|
|
457
|
+
if isinstance(mp_client, int) and mp_client > 0:
|
|
458
|
+
return ThreadPoolRunner(ThreadPoolExecutor(max_workers=mp_client), log_file)
|
|
459
|
+
elif isinstance(mp_client, ThreadPoolExecutor):
|
|
460
|
+
return ThreadPoolRunner(mp_client, log_file)
|
|
461
|
+
if isinstance(mp_client, SLURMConfig):
|
|
462
|
+
return SLURMRunner(mp_client, log_file)
|
|
463
|
+
else:
|
|
464
|
+
raise NotImplementedError(f"Unsupported client type: {type(mp_client)}")
|
xutk-0.3.4/xutk/perf.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Performance measurement utilities.
|
|
2
|
+
|
|
3
|
+
This module provides tools for measuring and analyzing code performance,
|
|
4
|
+
including execution time and memory usage.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import time
|
|
9
|
+
import tracemalloc
|
|
10
|
+
import psutil
|
|
11
|
+
from contextlib import ContextDecorator
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Perf(ContextDecorator):
|
|
16
|
+
"""Performance measurement context manager and decorator.
|
|
17
|
+
|
|
18
|
+
Measures wall time, CPU time, memory usage and allocations.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, label: str | None = None, print_output: bool = True) -> None:
|
|
22
|
+
"""Initialize performance measurement context.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
label: Optional label for the measurement
|
|
26
|
+
print_output: Whether to print results automatically
|
|
27
|
+
|
|
28
|
+
"""
|
|
29
|
+
self.label = label
|
|
30
|
+
self.cpu = 0
|
|
31
|
+
self.wall = 0
|
|
32
|
+
self.ratio = 0
|
|
33
|
+
self.rss = 0
|
|
34
|
+
self.peak = 0
|
|
35
|
+
self.final_rss = 0
|
|
36
|
+
self.print = print_output
|
|
37
|
+
|
|
38
|
+
self.label_str = ""
|
|
39
|
+
|
|
40
|
+
def __enter__(self) -> "Perf":
|
|
41
|
+
"""Enter the performance measurement context.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
The Perf instance itself
|
|
45
|
+
|
|
46
|
+
"""
|
|
47
|
+
self.proc = psutil.Process(os.getpid())
|
|
48
|
+
self.t0_cpu = time.process_time()
|
|
49
|
+
self.t0_wall = time.time()
|
|
50
|
+
self.t0_rss = self.proc.memory_info().rss
|
|
51
|
+
tracemalloc.start()
|
|
52
|
+
return self
|
|
53
|
+
|
|
54
|
+
def __exit__(self, *exc) -> None: # noqa: ANN002
|
|
55
|
+
"""Exit the performance measurement context.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
*exc: Exception information if any
|
|
59
|
+
|
|
60
|
+
"""
|
|
61
|
+
self.cpu = time.process_time() - self.t0_cpu
|
|
62
|
+
self.wall = time.time() - self.t0_wall
|
|
63
|
+
self.ratio = self.cpu / self.wall if self.wall > 0 else 0
|
|
64
|
+
self.rss = self.proc.memory_info().rss - self.t0_rss
|
|
65
|
+
self.peak = tracemalloc.get_traced_memory()[1] / 1_048_576
|
|
66
|
+
self.final_rss = self.proc.memory_info().rss / 1_048_576
|
|
67
|
+
tracemalloc.stop()
|
|
68
|
+
|
|
69
|
+
self.label_str = f"[{self.label}]" if self.label is not None else ""
|
|
70
|
+
self.label_str += f"\tWall Time: {self.wall:.3f}s"
|
|
71
|
+
self.label_str += f"\tCPU Time: {self.cpu:.3f}s({self.ratio:.2f}x)"
|
|
72
|
+
self.label_str += f"\tΔRSS: {self.rss / 1_048_576:.1f} MiB"
|
|
73
|
+
self.label_str += f"\tPeak alloc: {self.peak:.1f} MiB"
|
|
74
|
+
self.label_str += f"\tFinal RSS: {self.final_rss:.1f} MiB"
|
|
75
|
+
|
|
76
|
+
if self.print:
|
|
77
|
+
print(self.label_str)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def fadvise_remove_cache(path: str) -> None:
|
|
81
|
+
"""Advise OS to remove file from cache.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
path: Path to the file to remove from cache
|
|
85
|
+
|
|
86
|
+
"""
|
|
87
|
+
with Path(path).open("rb") as f:
|
|
88
|
+
fd = f.fileno()
|
|
89
|
+
os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_DONTNEED)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Manage packages and versions."""
|
|
2
|
+
|
|
3
|
+
from importlib import metadata
|
|
4
|
+
import packaging.version as pv
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def check_version(version_need: str, package_name: str) -> None:
|
|
8
|
+
"""Check if the installed version of a package satisfies the required version.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
version_need (str): The minimum required version string (e.g., '0.3.0a1').
|
|
12
|
+
package_name (str): The name of the package to check (e.g., 'plotma').
|
|
13
|
+
|
|
14
|
+
Raises:
|
|
15
|
+
ImportError: If the package is not installed or the version is incompatible.
|
|
16
|
+
|
|
17
|
+
"""
|
|
18
|
+
# Get versions: required, installed
|
|
19
|
+
required = pv.parse(version_need)
|
|
20
|
+
try:
|
|
21
|
+
installed = pv.parse(metadata.version(package_name))
|
|
22
|
+
except metadata.PackageNotFoundError:
|
|
23
|
+
raise ImportError(f"错误: {package_name} 包未安装。")
|
|
24
|
+
|
|
25
|
+
# Check version compatibility
|
|
26
|
+
if installed >= required: # satisfied minimum version required
|
|
27
|
+
if installed.release[0] == required.release[0]: # satisfied MAJOR
|
|
28
|
+
if required.release[0] > 0 or installed.release[1] == required.release[1]:
|
|
29
|
+
# version compatible
|
|
30
|
+
return
|
|
31
|
+
raise ImportError(
|
|
32
|
+
f"版本不兼容:需要 {package_name} = {version_need}, 当前为 {installed}"
|
|
33
|
+
)
|
|
34
|
+
return
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: xutk
|
|
3
|
+
Version: 0.3.4
|
|
4
|
+
Summary: Xulab toolkit in python
|
|
5
|
+
Requires-Python: <3.15,>=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: colorama>=0.4.6
|
|
8
|
+
Requires-Dist: psutil>=7.1.0
|
|
9
|
+
|
|
10
|
+
# Xulab Useful Toolkit in Python (xutk)
|
|
11
|
+
|
|
12
|
+

|
|
13
|
+

|
|
14
|
+
|
|
15
|
+
## 导入项目
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install \
|
|
19
|
+
--trusted-host "gitea.xulab.ion.ac.cn" \
|
|
20
|
+
--extra-index-url "http://gitea.xulab.ion.ac.cn/api/packages/zelin2001/pypi/simple/" \
|
|
21
|
+
xutk
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
或者使用 uv:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
uv add --index http://gitea.xulab.ion.ac.cn/api/packages/zelin2001/pypi/simple xutk
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## 快速开始
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from xutk import verchk, log, mp_runner
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
|
|
36
|
+
# 版本检查
|
|
37
|
+
verchk.check_version("0.2.0", "some_package")
|
|
38
|
+
|
|
39
|
+
# 日志记录
|
|
40
|
+
logger = log.CtxLogger("my_app")
|
|
41
|
+
logger.info({"module": "processor", "file": "data.csv"}, "Processing started")
|
|
42
|
+
|
|
43
|
+
# 多进程运行器
|
|
44
|
+
runner = mp_runner.mprunner_factory(2, log_file=Path("process.log")) # 进程队列,取 2 个运行
|
|
45
|
+
runner.batch_run(["echo", "-E", f"test{num}"] for num in range(1,25)) # 排队 25 个进程
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**建议在项目 `__init__.py` 创建 CtxLogger("my_app"),避免实例化顺序问题。**
|
|
49
|
+
|
|
50
|
+
## 项目结构
|
|
51
|
+
|
|
52
|
+
```text
|
|
53
|
+
xutk/
|
|
54
|
+
├── xutk/
|
|
55
|
+
│ ├── __init__.py # 包初始化
|
|
56
|
+
│ ├── log.py # 日志记录功能
|
|
57
|
+
│ ├── mp_runner.py # 多进程运行管理
|
|
58
|
+
│ ├── perf.py # 快速资源检查
|
|
59
|
+
│ └── verchk.py # 版本检查功能
|
|
60
|
+
├── test/
|
|
61
|
+
│ ├── integration/ # 集成测试
|
|
62
|
+
│ └── unit/ # 单元测试
|
|
63
|
+
├── gitea/
|
|
64
|
+
│ └── workflows/ # CI 工作流
|
|
65
|
+
├── pyproject.toml # 项目配置
|
|
66
|
+
└── README.md # 项目文档
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## 开发
|
|
70
|
+
|
|
71
|
+
### 安装开发依赖
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
uv sync --all-extras
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 运行测试
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
uv run pytest
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 代码检查
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
uv run ruff check # 代码风格检查
|
|
87
|
+
uv run ty check # 类型检查
|
|
88
|
+
uv run pytest # 样例测试
|
|
89
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
xutk/__init__.py
|
|
4
|
+
xutk/log.py
|
|
5
|
+
xutk/mp_runner.py
|
|
6
|
+
xutk/perf.py
|
|
7
|
+
xutk/verchk.py
|
|
8
|
+
xutk.egg-info/PKG-INFO
|
|
9
|
+
xutk.egg-info/SOURCES.txt
|
|
10
|
+
xutk.egg-info/dependency_links.txt
|
|
11
|
+
xutk.egg-info/requires.txt
|
|
12
|
+
xutk.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
xutk
|