Initial commit: SenSu Alpha 0.2.0

- 13-service async plugin framework
- Textual TUI with CLI fallback
- Plugin hot-reload + permission system
- Web management panel (aiohttp)
- Bridge-based inter-module communication
- 10 regression tests

Fixes applied:
- PBKDF2-SHA256 auth (was plain SHA256)
- Auth bypass removed (was allow-all on fail)
- Bare excepts replaced with logged errors
- CatFramework/DreamSu -> SenSu naming unified
- ServiceManager: health checks + startup_order
- Env var credentials (SENSU_ADMIN_PASSWORD etc)
This commit is contained in:
2026-06-10 12:27:14 +08:00
commit e6875f0b4b
78 changed files with 14843 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
框架功能集 - 提供各种工具函数和工具类
"""
from .file_utils import FileUtils
from .config_utils import ConfigUtils
from .validation_utils import ValidationUtils
from .network_utils import NetworkUtils
from .plugin_utils import PluginUtils
__all__ = [
'FileUtils',
'ConfigUtils',
'ValidationUtils',
'NetworkUtils',
'PluginUtils'
]
+203
View File
@@ -0,0 +1,203 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
import yaml
import json
from pathlib import Path
from typing import Dict, Any, Optional
import copy
logger = logging.getLogger(__name__)
class ConfigUtils:
"""配置工具类"""
@staticmethod
def load_yaml_config(file_path: str, default_config: Dict = None) -> Dict:
"""加载YAML配置文件"""
try:
path = Path(file_path)
if not path.exists():
logger.warning(f"YAML配置文件不存在: {file_path}")
if default_config:
ConfigUtils.save_yaml_config(file_path, default_config)
logger.debug(f"已创建默认YAML配置: {file_path}")
return default_config or {}
with open(path, 'r', encoding='utf-8') as f:
config = yaml.safe_load(f)
logger.debug(f"YAML配置加载成功: {file_path}")
return config or {}
except yaml.YAMLError as e:
logger.error(f"YAML配置文件解析错误 {file_path}: {str(e)}", exc_info=True)
return default_config or {}
except Exception as e:
logger.error(f"加载YAML配置时出错 {file_path}: {str(e)}", exc_info=True)
return default_config or {}
@staticmethod
def save_yaml_config(file_path: str, config: Dict) -> bool:
"""保存YAML配置文件"""
try:
path = Path(file_path)
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, 'w', encoding='utf-8') as f:
yaml.dump(config, f, default_flow_style=False, allow_unicode=True, indent=2)
logger.debug(f"YAML配置保存成功: {file_path}")
return True
except Exception as e:
logger.error(f"保存YAML配置时出错 {file_path}: {str(e)}", exc_info=True)
return False
@staticmethod
def load_json_config(file_path: str, default_config: Dict = None) -> Dict:
"""加载JSON配置文件"""
try:
path = Path(file_path)
if not path.exists():
logger.warning(f"JSON配置文件不存在: {file_path}")
if default_config:
ConfigUtils.save_json_config(file_path, default_config)
logger.debug(f"已创建默认JSON配置: {file_path}")
return default_config or {}
with open(path, 'r', encoding='utf-8') as f:
config = json.load(f)
logger.debug(f"JSON配置加载成功: {file_path}")
return config
except json.JSONDecodeError as e:
logger.error(f"JSON配置文件解析错误 {file_path}: {str(e)}", exc_info=True)
return default_config or {}
except Exception as e:
logger.error(f"加载JSON配置时出错 {file_path}: {str(e)}", exc_info=True)
return default_config or {}
@staticmethod
def save_json_config(file_path: str, config: Dict) -> bool:
"""保存JSON配置文件"""
try:
path = Path(file_path)
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, 'w', encoding='utf-8') as f:
json.dump(config, f, ensure_ascii=False, indent=2)
logger.debug(f"JSON配置保存成功: {file_path}")
return True
except Exception as e:
logger.error(f"保存JSON配置时出错 {file_path}: {str(e)}", exc_info=True)
return False
@staticmethod
def get_nested_value(config: Dict, key_path: str, default: Any = None) -> Any:
"""获取嵌套配置值"""
try:
keys = key_path.split('.')
current = config
for key in keys:
if isinstance(current, dict) and key in current:
current = current[key]
else:
logger.debug(f"配置键不存在: {key_path}")
return default
logger.debug(f"获取嵌套配置值: {key_path} -> {current}")
return current
except Exception as e:
logger.error(f"获取嵌套配置值时出错 {key_path}: {str(e)}", exc_info=True)
return default
@staticmethod
def set_nested_value(config: Dict, key_path: str, value: Any) -> bool:
"""设置嵌套配置值"""
try:
keys = key_path.split('.')
current = config
# 遍历到最后一个键的父级
for key in keys[:-1]:
if key not in current or not isinstance(current[key], dict):
current[key] = {}
current = current[key]
# 设置值
current[keys[-1]] = value
logger.debug(f"设置嵌套配置值: {key_path} -> {value}")
return True
except Exception as e:
logger.error(f"设置嵌套配置值时出错 {key_path}: {str(e)}", exc_info=True)
return False
@staticmethod
def merge_configs(base_config: Dict, override_config: Dict) -> Dict:
"""合并配置(深度合并)"""
try:
result = copy.deepcopy(base_config)
for key, value in override_config.items():
if (key in result and
isinstance(result[key], dict) and
isinstance(value, dict)):
# 递归合并字典
result[key] = ConfigUtils.merge_configs(result[key], value)
else:
# 直接覆盖
result[key] = copy.deepcopy(value)
logger.debug("配置合并完成")
return result
except Exception as e:
logger.error(f"合并配置时出错: {str(e)}", exc_info=True)
return base_config
@staticmethod
def validate_config_structure(config: Dict, schema: Dict) -> bool:
"""验证配置结构"""
try:
def _validate(current_config, current_schema, path=""):
for key, expected_type in current_schema.items():
full_path = f"{path}.{key}" if path else key
if key not in current_config:
logger.error(f"配置缺少必要字段: {full_path}")
return False
actual_value = current_config[key]
expected_type_name = expected_type.__name__ if hasattr(expected_type, '__name__') else str(expected_type)
if not isinstance(actual_value, expected_type):
logger.error(f"配置类型错误 {full_path}: 期望 {expected_type_name}, 实际 {type(actual_value).__name__}")
return False
# 如果是字典且schema有嵌套定义,递归验证
if (isinstance(expected_type, dict) and
isinstance(actual_value, dict)):
if not _validate(actual_value, expected_type, full_path):
return False
return True
result = _validate(config, schema)
if result:
logger.debug("配置结构验证通过")
else:
logger.error("配置结构验证失败")
return result
except Exception as e:
logger.error(f"验证配置结构时出错: {str(e)}", exc_info=True)
return False
+144
View File
@@ -0,0 +1,144 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
import os
import shutil
from pathlib import Path
from typing import List, Optional
import hashlib
logger = logging.getLogger(__name__)
class FileUtils:
"""文件操作工具类"""
@staticmethod
def ensure_directory(directory_path: str) -> bool:
"""确保目录存在"""
try:
path = Path(directory_path)
path.mkdir(parents=True, exist_ok=True)
logger.debug(f"目录已确保存在: {directory_path}")
return True
except Exception as e:
logger.error(f"创建目录时出错 {directory_path}: {str(e)}", exc_info=True)
return False
@staticmethod
def safe_write(file_path: str, content: str, backup: bool = True) -> bool:
"""安全写入文件(支持备份)"""
try:
path = Path(file_path)
# 备份原文件
if backup and path.exists():
backup_path = path.with_suffix(path.suffix + '.bak')
shutil.copy2(path, backup_path)
logger.debug(f"文件已备份: {backup_path}")
# 写入新内容
with open(path, 'w', encoding='utf-8') as f:
f.write(content)
logger.debug(f"文件写入成功: {file_path}, 大小: {len(content)} 字节")
return True
except Exception as e:
logger.error(f"写入文件时出错 {file_path}: {str(e)}", exc_info=True)
return False
@staticmethod
def safe_read(file_path: str, default: str = "") -> str:
"""安全读取文件"""
try:
path = Path(file_path)
if not path.exists():
logger.warning(f"文件不存在: {file_path}")
return default
with open(path, 'r', encoding='utf-8') as f:
content = f.read()
logger.debug(f"文件读取成功: {file_path}, 大小: {len(content)} 字节")
return content
except Exception as e:
logger.error(f"读取文件时出错 {file_path}: {str(e)}", exc_info=True)
return default
@staticmethod
def list_files(directory: str, pattern: str = "*", recursive: bool = False) -> List[Path]:
"""列出目录中的文件"""
try:
path = Path(directory)
if not path.exists():
logger.warning(f"目录不存在: {directory}")
return []
if recursive:
files = list(path.rglob(pattern))
else:
files = list(path.glob(pattern))
# 过滤出文件(非目录)
files = [f for f in files if f.is_file()]
logger.debug(f"列出文件: {directory}, 模式: {pattern}, 找到 {len(files)} 个文件")
return files
except Exception as e:
logger.error(f"列出文件时出错 {directory}: {str(e)}", exc_info=True)
return []
@staticmethod
def calculate_file_hash(file_path: str, algorithm: str = "md5") -> Optional[str]:
"""计算文件哈希值"""
try:
path = Path(file_path)
if not path.exists():
logger.warning(f"文件不存在: {file_path}")
return None
hash_func = getattr(hashlib, algorithm)()
with open(path, 'rb') as f:
for chunk in iter(lambda: f.read(4096), b""):
hash_func.update(chunk)
file_hash = hash_func.hexdigest()
logger.debug(f"文件哈希计算完成: {file_path} -> {algorithm}:{file_hash}")
return file_hash
except Exception as e:
logger.error(f"计算文件哈希时出错 {file_path}: {str(e)}", exc_info=True)
return None
@staticmethod
def cleanup_old_files(directory: str, pattern: str, keep_count: int) -> int:
"""清理旧文件,保留指定数量的最新文件"""
try:
files = FileUtils.list_files(directory, pattern)
if len(files) <= keep_count:
logger.debug(f"文件数量未超过限制,无需清理: {directory}")
return 0
# 按修改时间排序
files.sort(key=lambda x: x.stat().st_mtime, reverse=True)
# 删除旧文件
removed_count = 0
for file_to_remove in files[keep_count:]:
try:
file_to_remove.unlink()
removed_count += 1
logger.debug(f"删除旧文件: {file_to_remove}")
except Exception as e:
logger.error(f"删除文件时出错 {file_to_remove}: {str(e)}", exc_info=True)
logger.info(f"文件清理完成: {directory}, 删除 {removed_count} 个文件")
return removed_count
except Exception as e:
logger.error(f"清理旧文件时出错 {directory}: {str(e)}", exc_info=True)
return 0
+111
View File
@@ -0,0 +1,111 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
import socket
import asyncio
import re
from typing import Optional, Tuple
import aiohttp
import ssl
logger = logging.getLogger(__name__)
class NetworkUtils:
"""网络工具类"""
@staticmethod
async def check_port_available(host: str, port: int) -> bool:
"""检查端口是否可用"""
try:
# 尝试创建socket连接
reader, writer = await asyncio.open_connection(host, port)
writer.close()
await writer.wait_closed()
logger.debug(f"端口 {host}:{port} 已被占用")
return False
except (ConnectionRefusedError, asyncio.TimeoutError):
logger.debug(f"端口 {host}:{port} 可用")
return True
except Exception as e:
logger.error(f"检查端口可用性时出错 {host}:{port}: {str(e)}", exc_info=True)
return False
@staticmethod
async def find_available_port(host: str = "localhost", start_port: int = 8000,
max_attempts: int = 100) -> Optional[int]:
"""查找可用端口"""
try:
for port in range(start_port, start_port + max_attempts):
if await NetworkUtils.check_port_available(host, port):
logger.debug(f"找到可用端口: {host}:{port}")
return port
logger.warning(f"在范围 {start_port}-{start_port + max_attempts} 内未找到可用端口")
return None
except Exception as e:
logger.error(f"查找可用端口时出错: {str(e)}", exc_info=True)
return None
@staticmethod
async def http_request(url: str, method: str = "GET", headers: dict = None,
data: dict = None, timeout: int = 30) -> Tuple[bool, dict]:
"""发送HTTP请求"""
try:
logger.debug(f"发送HTTP请求: {method} {url}")
timeout_obj = aiohttp.ClientTimeout(total=timeout)
async with aiohttp.ClientSession(timeout=timeout_obj) as session:
async with session.request(method, url, headers=headers, json=data) as response:
response_data = await response.text()
result = {
"status": response.status,
"headers": dict(response.headers),
"data": response_data,
"url": str(response.url)
}
logger.debug(f"HTTP请求完成: {method} {url} -> 状态 {response.status}")
return True, result
except asyncio.TimeoutError:
logger.error(f"HTTP请求超时: {method} {url}")
return False, {"error": "请求超时"}
except aiohttp.ClientError as e:
logger.error(f"HTTP客户端错误: {method} {url} -> {str(e)}")
return False, {"error": str(e)}
except Exception as e:
logger.error(f"HTTP请求时出错: {method} {url} -> {str(e)}", exc_info=True)
return False, {"error": str(e)}
@staticmethod
def get_local_ip() -> str:
"""获取本地IP地址"""
try:
# 创建一个socket连接来获取本地IP
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.connect(("8.8.8.8", 80))
local_ip = s.getsockname()[0]
logger.debug(f"获取本地IP: {local_ip}")
return local_ip
except Exception as e:
logger.error(f"获取本地IP时出错: {str(e)}", exc_info=True)
return "127.0.0.1"
@staticmethod
def is_valid_hostname(hostname: str) -> bool:
"""验证主机名格式"""
try:
if len(hostname) > 255:
return False
if hostname[-1] == ".":
hostname = hostname[:-1]
allowed = re.compile(r"(?!-)[A-Z\d-]{1,63}(?<!-)$", re.IGNORECASE)
return all(allowed.match(x) for x in hostname.split("."))
except Exception as e:
logger.error(f"验证主机名时出错: {str(e)}", exc_info=True)
return False
+260
View File
@@ -0,0 +1,260 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
import inspect
import re
from typing import Dict, List, Any, Callable
from pathlib import Path
import importlib
logger = logging.getLogger(__name__)
class PluginUtils:
"""插件工具类"""
@staticmethod
def validate_plugin_structure(plugin_path: Path) -> bool:
"""验证插件结构"""
try:
logger.debug(f"验证插件结构: {plugin_path}")
required_files = [
"__init__.py",
"config.yaml",
"permissions.yaml"
]
# 检查必需文件
for file_name in required_files:
if not (plugin_path / file_name).exists():
logger.error(f"插件缺少必需文件: {file_name}")
return False
# 检查主模块是否有Plugin类
try:
spec = importlib.util.spec_from_file_location("plugin_module", plugin_path / "__init__.py")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
if not hasattr(module, 'Plugin'):
logger.error("插件主模块缺少Plugin类")
return False
# 检查Plugin类是否有必要方法
plugin_class = module.Plugin
required_methods = ['initialize', 'shutdown']
for method_name in required_methods:
if not hasattr(plugin_class, method_name):
logger.error(f"Plugin类缺少必要方法: {method_name}")
return False
logger.debug(f"插件结构验证通过: {plugin_path.name}")
return True
except Exception as e:
logger.error(f"验证插件类时出错: {str(e)}", exc_info=True)
return False
except Exception as e:
logger.error(f"验证插件结构时出错: {str(e)}", exc_info=True)
return False
@staticmethod
def get_plugin_dependencies(plugin_path: Path) -> List[str]:
"""获取插件依赖"""
try:
config_file = plugin_path / "config.yaml"
if not config_file.exists():
return []
import yaml
with open(config_file, 'r', encoding='utf-8') as f:
config = yaml.safe_load(f)
dependencies = config.get('dependencies', [])
if isinstance(dependencies, list):
logger.debug(f"获取插件依赖: {plugin_path.name} -> {dependencies}")
return dependencies
else:
logger.warning(f"插件依赖格式错误: {plugin_path.name}")
return []
except Exception as e:
logger.error(f"获取插件依赖时出错: {str(e)}", exc_info=True)
return []
@staticmethod
def scan_plugin_methods(plugin_instance) -> Dict[str, List[str]]:
"""扫描插件方法"""
try:
logger.debug(f"扫描插件方法: {type(plugin_instance).__name__}")
methods_info = {
"public_methods": [],
"private_methods": [],
"async_methods": [],
"event_handlers": []
}
for name, method in inspect.getmembers(plugin_instance, predicate=inspect.ismethod):
# 跳过特殊方法
if name.startswith('_') and not name.startswith('__'):
methods_info["private_methods"].append(name)
elif not name.startswith('_'):
methods_info["public_methods"].append(name)
# 检查是否为异步方法
if inspect.iscoroutinefunction(method):
methods_info["async_methods"].append(name)
# 检查是否为事件处理器
if name.startswith('handle_') or name.startswith('on_'):
methods_info["event_handlers"].append(name)
logger.debug(f"插件方法扫描完成: 公共{len(methods_info['public_methods'])}个, 私有{len(methods_info['private_methods'])}")
return methods_info
except Exception as e:
logger.error(f"扫描插件方法时出错: {str(e)}", exc_info=True)
return {}
@staticmethod
def create_plugin_skeleton(plugin_name: str, plugin_path: Path) -> bool:
"""创建插件骨架"""
try:
logger.debug(f"创建插件骨架: {plugin_name} -> {plugin_path}")
# 创建插件目录
plugin_path.mkdir(parents=True, exist_ok=True)
# 创建主模块文件
init_content = '''#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
import asyncio
from typing import Dict, Any
logger = logging.getLogger(__name__)
class Plugin:
"""{plugin_name} 插件"""
def __init__(self, plugin_name: str, config: Dict, bridge):
self.plugin_name = plugin_name
self.config = config
self.bridge = bridge
self.is_running = False
logger.debug(f"插件初始化: {{plugin_name}}")
async def initialize(self):
"""初始化插件"""
try:
logger.info(f"初始化插件: {{self.plugin_name}}")
# 在这里注册事件处理器和命令
# 示例: self.bridge.subscribe_plugin(self.plugin_name, "event.name", self.handler)
self.is_running = True
logger.debug(f"插件初始化完成: {{self.plugin_name}}")
except Exception as e:
logger.error(f"初始化插件时出错: {{str(e)}}", exc_info=True)
raise
async def shutdown(self):
"""关闭插件"""
try:
logger.info(f"关闭插件: {{self.plugin_name}}")
self.is_running = False
# 清理资源
self.bridge.cleanup_plugin_subscriptions(self.plugin_name)
logger.debug(f"插件关闭完成: {{self.plugin_name}}")
except Exception as e:
logger.error(f"关闭插件时出错: {{str(e)}}", exc_info=True)
# 在这里添加你的插件方法
async def example_method(self, message: str) -> str:
"""示例方法"""
try:
logger.debug(f"插件方法调用: {{message}}")
return f"插件响应: {{message}}"
except Exception as e:
logger.error(f"插件方法调用出错: {{str(e)}}", exc_info=True)
raise
'''.format(plugin_name=plugin_name)
with open(plugin_path / "__init__.py", 'w', encoding='utf-8') as f:
f.write(init_content)
# 创建配置文件
config_content = f'''# {plugin_name} 插件配置
name: "{plugin_name}"
version: "1.0.0"
description: "{plugin_name} 插件描述"
author: "插件作者"
# 插件特定配置
settings:
enabled: true
auto_start: true
log_level: "INFO"
# 依赖配置
dependencies: []
'''
with open(plugin_path / "config.yaml", 'w', encoding='utf-8') as f:
f.write(config_content)
# 创建权限文件
permissions_content = f'''# {plugin_name} 插件权限申请
plugin_name: "{plugin_name}"
permissions:
- "plugin.{plugin_name}.read"
- "plugin.{plugin_name}.write"
# 权限说明
permission_descriptions:
plugin.{plugin_name}.read: "读取{plugin_name}插件数据"
plugin.{plugin_name}.write: "写入{plugin_name}插件数据"
'''
with open(plugin_path / "permissions.yaml", 'w', encoding='utf-8') as f:
f.write(permissions_content)
logger.info(f"插件骨架创建完成: {plugin_name}")
return True
except Exception as e:
logger.error(f"创建插件骨架时出错: {str(e)}", exc_info=True)
return False
@staticmethod
def validate_plugin_permissions(plugin_path: Path, requested_permissions: List[str]) -> bool:
"""验证插件权限申请"""
try:
logger.debug(f"验证插件权限: {plugin_path.name}")
# 检查权限格式
for permission in requested_permissions:
if not isinstance(permission, str):
logger.error(f"权限格式错误: {permission}")
return False
# 检查权限命名规范
if not re.match(r'^[a-z][a-z0-9_.]*$', permission):
logger.error(f"权限命名不规范: {permission}")
return False
logger.debug(f"插件权限验证通过: {len(requested_permissions)} 个权限")
return True
except Exception as e:
logger.error(f"验证插件权限时出错: {str(e)}", exc_info=True)
return False
+206
View File
@@ -0,0 +1,206 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
import re
import os
from typing import Any, List, Optional, Callable, Dict
from urllib.parse import urlparse
import ipaddress
logger = logging.getLogger(__name__)
class ValidationUtils:
"""验证工具类"""
@staticmethod
def is_valid_email(email: str) -> bool:
"""验证邮箱格式"""
try:
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
result = bool(re.match(pattern, email))
logger.debug(f"邮箱验证: {email} -> {result}")
return result
except Exception as e:
logger.error(f"验证邮箱时出错: {str(e)}", exc_info=True)
return False
@staticmethod
def is_valid_url(url: str) -> bool:
"""验证URL格式"""
try:
result = urlparse(url)
is_valid = all([result.scheme, result.netloc])
logger.debug(f"URL验证: {url} -> {is_valid}")
return is_valid
except Exception as e:
logger.error(f"验证URL时出错: {str(e)}", exc_info=True)
return False
@staticmethod
def is_valid_ip(ip: str) -> bool:
"""验证IP地址格式"""
try:
ipaddress.ip_address(ip)
logger.debug(f"IP地址验证: {ip} -> True")
return True
except ValueError:
logger.debug(f"IP地址验证: {ip} -> False")
return False
except Exception as e:
logger.error(f"验证IP地址时出错: {str(e)}", exc_info=True)
return False
@staticmethod
def is_valid_port(port: int) -> bool:
"""验证端口号"""
try:
is_valid = 1 <= port <= 65535
logger.debug(f"端口验证: {port} -> {is_valid}")
return is_valid
except Exception as e:
logger.error(f"验证端口时出错: {str(e)}", exc_info=True)
return False
@staticmethod
def validate_string(value: Any, min_length: int = 0, max_length: int = None,
pattern: str = None) -> bool:
"""验证字符串"""
try:
if not isinstance(value, str):
logger.debug(f"字符串验证失败: 不是字符串类型")
return False
if len(value) < min_length:
logger.debug(f"字符串验证失败: 长度小于 {min_length}")
return False
if max_length and len(value) > max_length:
logger.debug(f"字符串验证失败: 长度大于 {max_length}")
return False
if pattern and not re.match(pattern, value):
logger.debug(f"字符串验证失败: 不匹配模式 {pattern}")
return False
logger.debug(f"字符串验证通过: 长度 {len(value)}")
return True
except Exception as e:
logger.error(f"验证字符串时出错: {str(e)}", exc_info=True)
return False
@staticmethod
def validate_number(value: Any, min_value: float = None, max_value: float = None) -> bool:
"""验证数字"""
try:
if not isinstance(value, (int, float)):
# 尝试转换
try:
value = float(value)
except (ValueError, TypeError):
logger.debug(f"数字验证失败: 无法转换为数字")
return False
if min_value is not None and value < min_value:
logger.debug(f"数字验证失败: 值小于 {min_value}")
return False
if max_value is not None and value > max_value:
logger.debug(f"数字验证失败: 值大于 {max_value}")
return False
logger.debug(f"数字验证通过: {value}")
return True
except Exception as e:
logger.error(f"验证数字时出错: {str(e)}", exc_info=True)
return False
@staticmethod
def validate_list(value: Any, min_length: int = 0, max_length: int = None,
item_validator: Callable = None) -> bool:
"""验证列表"""
try:
if not isinstance(value, list):
logger.debug(f"列表验证失败: 不是列表类型")
return False
if len(value) < min_length:
logger.debug(f"列表验证失败: 长度小于 {min_length}")
return False
if max_length and len(value) > max_length:
logger.debug(f"列表验证失败: 长度大于 {max_length}")
return False
if item_validator:
for i, item in enumerate(value):
if not item_validator(item):
logger.debug(f"列表验证失败: 第 {i} 项验证失败")
return False
logger.debug(f"列表验证通过: 长度 {len(value)}")
return True
except Exception as e:
logger.error(f"验证列表时出错: {str(e)}", exc_info=True)
return False
@staticmethod
def validate_dict(value: Any, required_keys: List[str] = None,
key_validators: Dict[str, Callable] = None) -> bool:
"""验证字典"""
try:
if not isinstance(value, dict):
logger.debug(f"字典验证失败: 不是字典类型")
return False
# 检查必需键
if required_keys:
for key in required_keys:
if key not in value:
logger.debug(f"字典验证失败: 缺少必需键 {key}")
return False
# 检查键值验证器
if key_validators:
for key, validator in key_validators.items():
if key in value and not validator(value[key]):
logger.debug(f"字典验证失败: 键 {key} 的值验证失败")
return False
logger.debug(f"字典验证通过: 键数 {len(value)}")
return True
except Exception as e:
logger.error(f"验证字典时出错: {str(e)}", exc_info=True)
return False
@staticmethod
def validate_file_path(file_path: str, check_exists: bool = True,
check_readable: bool = False, check_writable: bool = False) -> bool:
"""验证文件路径"""
try:
from pathlib import Path
path = Path(file_path)
if check_exists and not path.exists():
logger.debug(f"文件路径验证失败: 文件不存在")
return False
if check_readable and not os.access(path, os.R_OK):
logger.debug(f"文件路径验证失败: 文件不可读")
return False
if check_writable and not os.access(path, os.W_OK):
logger.debug(f"文件路径验证失败: 文件不可写")
return False
logger.debug(f"文件路径验证通过: {file_path}")
return True
except Exception as e:
logger.error(f"验证文件路径时出错: {str(e)}", exc_info=True)
return False