1
1
mirror of https://github.com/KenanZhu/AutoLibrary.git synced 2026-06-18 07:23:03 +08:00

chore(*): 重构项目结构

- 新增 src/boot 目录,用于存放启动时需要初始化的模块
- 新增 src/managers 目录,用于存放项目中的管理模块
- 新增 src/managers/config 目录,用于存放配置管理模块
- 新增 src/managers/log 目录,用于存放日志管理模块
- 新增 src/managers/driver 目录,用于存放浏览器驱动管理模块
- 修改对应文件中 import 导入路径
This commit is contained in:
2026-03-20 19:19:34 +08:00
parent bf93cc2cbc
commit 706fc889f9
16 changed files with 93 additions and 18 deletions
-58
View File
@@ -1,58 +0,0 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2025 - 2026 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
import os
from PySide6.QtCore import QStandardPaths, QDir
from utils.ConfigManager import instance as configInstance
from utils.LogManager import instance as logInstance
def initializeConfigManager(
) -> bool:
logger = logInstance().getLogger("AppInitializer")
app_dir = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation)
old_config_dir = os.path.join(app_dir, "config")
new_config_dir = os.path.join(app_dir, "configs")
if QDir(old_config_dir).exists(): # old config dir exists
#we rename it to compatible with new version
logger.info("存在旧配置目录 %s,将其重命名为 %s", old_config_dir, new_config_dir)
if not QDir().rename(old_config_dir, new_config_dir):
logger.error("重命名旧配置目录 %s%s 失败", old_config_dir, new_config_dir)
return False
elif not QDir(new_config_dir).exists():
logger.info("初始化配置目录 %s", new_config_dir)
if not QDir().mkpath(new_config_dir):
logger.error("创建配置目录 %s 失败", new_config_dir)
return False
configInstance(new_config_dir)
return True
def initializeLogManager(
) -> bool:
app_dir = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation)
log_dir = os.path.join(app_dir, "logs")
if not QDir(log_dir).exists():
if not QDir().mkpath(log_dir):
return False
logInstance(log_dir)
return True
def initializeApp(
) -> bool:
if not initializeLogManager():
return False
if not initializeConfigManager():
return False
return True
-244
View File
@@ -1,244 +0,0 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2026 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
import os
import threading
from enum import Enum
from typing import Any, Optional
from utils.JSONReader import JSONReader
from utils.JSONWriter import JSONWriter
# This config manager class only responsible for global and other
# unconfigurable config files.
class ConfigType(Enum):
"""
Config type class. Values represent the default filename.
"""
GLOBAL = "autolibrary.json" # Global config file.
BULLETIN = "bulletin.json" # Bulletin board config file.
TIMERTASK = "timer_task.json" # Timer task config file.
class ConfigTemplate:
"""
Config template class.
"""
def __init__(
self,
config_type: ConfigType
):
self.__config_type = config_type
def template(
self
) -> dict:
"""
Get config template.
Returns:
dict: Config template.
"""
match self.__config_type:
case ConfigType.GLOBAL:
return {
"automation": {
"run_path": {
"current": 0,
"paths": []
},
"user_path": {
"current": 0,
"paths": []
}
}
}
case ConfigType.BULLETIN:
return {
"bulletin": [],
"last_sync_time": None
}
case ConfigType.TIMERTASK:
return {
"timer_tasks": []
}
case _:
return {}
class ConfigManager:
def __init__(
self,
config_dir: str
):
self.__config_dir = os.path.abspath(config_dir)
self.__config_lock = threading.Lock()
self.__config_data = {}
self.initialize()
def initialize(
self
):
for config_type in ConfigType:
self.load(config_type)
def load(
self,
config_type: ConfigType
):
config_path = os.path.join(self.__config_dir, config_type.value)
if os.path.exists(config_path):
try:
config_data = JSONReader(config_path).data()
self.__config_data[config_type.value] = config_data
return
except:
pass
self.__config_data[config_type.value] = ConfigTemplate(config_type).template()
JSONWriter(config_path, self.__config_data[config_type.value])
def get(
self,
config_type: ConfigType,
key: str = "",
default: Optional[Any] = None
) -> Any:
with self.__config_lock:
config_data = self.__config_data[config_type.value]
if key == "":
return config_data
keys = key.split('.')
for k in keys[:-1]:
config_data = config_data.get(k, None)
if config_data is None:
return default
return config_data.get(keys[-1], default)
def set(
self,
config_type: ConfigType,
key: str = "",
value: Any = None
):
with self.__config_lock:
root_data = self.__config_data[config_type.value]
if key == "":
self.__config_data[config_type.value] = value
else:
keys = key.split('.')
config_data = root_data
for k in keys[:-1]:
if k not in config_data:
config_data[k] = {}
config_data = config_data[k]
config_data[keys[-1]] = value
self.save(config_type)
def save(
self,
config_type: ConfigType
):
config_path = os.path.join(self.__config_dir, config_type.value)
JSONWriter(config_path, self.__config_data[config_type.value])
def configDir(
self
) -> str:
return self.__config_dir
# ConfigManager singleton instance.
_config_manager_instance = None
# Utility functions.
#
# Utility function to get validated automation config paths.
def getValidateAutomationConfigPaths(
) -> dict:
"""
Get validated automation config paths from ConfigManager instance.
These function will validate the config paths and return the validated paths in a dict.
Returns:
dict: Validated automation config paths.
"""
config_paths = {"run": "", "user": ""}
auto_config = _config_manager_instance.get(ConfigType.GLOBAL, "automation", {})
for cfg_type in ["run", "user"]:
paths = auto_config.get(f"{cfg_type}_path", {}).get("paths", [])
index = auto_config.get(f"{cfg_type}_path", {}).get("current", 0)
if paths == []:
paths.append(os.path.join(_config_manager_instance.configDir(), f"{cfg_type}.json"))
if index < 0:
index = 0
if index >= len(paths):
index = len(paths) - 1
config_paths[cfg_type] = paths[index]
data = {"current": index, "paths": paths}
auto_config[f"{cfg_type}_path"] = data
_config_manager_instance.set(ConfigType.GLOBAL, "automation", auto_config)
return config_paths
# Utility function to get base config directory.
def getBaseConfigDir(
) -> str:
"""
Get base config directory, on Windows, it is usually at :
'C:\\Users\\<username>\\AppData\\Local\\AutoLibrary\\config'.
Returns:
str: Base config directory.
"""
return _config_manager_instance.configDir()
# Singleton instance of ConfigManager.
_instance_lock = threading.Lock()
def instance(
config_dir: str = ""
) -> ConfigManager:
"""
Initialize ConfigManager singleton instance.
Args:
config_dir (str): Config directory.
"""
global _config_manager_instance
with _instance_lock:
if _config_manager_instance is None:
_config_manager_instance = ConfigManager(config_dir)
else:
if config_dir == "":
return _config_manager_instance
if getBaseConfigDir() != config_dir:
raise ValueError(
"ConfigManager 的实例已初始化,不能使用不同的配置目录。")
return _config_manager_instance
-191
View File
@@ -1,191 +0,0 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2026 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
import logging
import os
import threading
from logging.handlers import TimedRotatingFileHandler
from typing import Optional
class CallerInfoFormatter(logging.Formatter):
"""
Custom formatter to extract real caller information.
Skips MsgBase._showTrace to show the actual calling location.
Format:
- Logger name: left-aligned, max 15 chars
- Level name: left-aligned, max 8 chars
- Filename: left-aligned, max 20 chars
- Line number: left-aligned, max 4 digits
"""
def __init__(
self,
fmt=None,
datefmt=None,
style='%'
):
super().__init__(fmt, datefmt, style)
self.basefmt = fmt
def format(
self,
record
):
depth = 0
while depth < 10:
record.filename = os.path.basename(record.pathname)
if 'MsgBase.py' not in record.filename and record.funcName != '_showTrace':
break
if not hasattr(record, 'stack'):
record.stack = True
import traceback
record.stack_list = traceback.extract_stack()
depth += 1
if depth < len(record.stack_list):
frame = record.stack_list[-depth-1]
record.filename = os.path.basename(frame.filename)
record.lineno = frame.lineno
record.funcName = frame.name
record.name = record.name[-15:].ljust(15)
record.levelname = record.levelname.ljust(8)
record.filename = record.filename[-20:].ljust(20)
record.lineno = f"{record.lineno:04d}"
return super().format(record)
class LogManager:
"""
Log Manager Singleton Class
Args:
log_dir (str): The directory to store log files.
"""
def __init__(
self,
log_dir: str
):
self.__log_dir = os.path.abspath(log_dir)
self.__logger = None
self.__initialized = False
self.initialize()
def initialize(
self
):
if self.__initialized:
return
os.makedirs(self.__log_dir, exist_ok=True)
self.__logger = logging.getLogger("AutoLibrary")
self.__logger.setLevel(logging.DEBUG)
self.__logger.handlers.clear()
formatter = CallerInfoFormatter(
'[%(asctime)s] - [%(name)s] - [%(levelname)s] - [%(filename)s:%(lineno)s] - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(formatter)
self.__logger.addHandler(console_handler)
all_log_file = os.path.join(self.__log_dir, "all.log")
file_handler_all = TimedRotatingFileHandler(
all_log_file,
when='midnight',
interval=1,
backupCount=7,
encoding='utf-8'
)
file_handler_all.suffix = "%Y-%m-%d.log"
file_handler_all.setLevel(logging.DEBUG)
file_handler_all.setFormatter(formatter)
self.__logger.addHandler(file_handler_all)
error_log_file = os.path.join(self.__log_dir, "error.log")
file_handler_error = TimedRotatingFileHandler(
error_log_file,
when='midnight',
interval=1,
backupCount=14,
encoding='utf-8'
)
file_handler_error.suffix = "%Y-%m-%d.log"
file_handler_error.setLevel(logging.ERROR)
file_handler_error.setFormatter(formatter)
self.__logger.addHandler(file_handler_error)
self.__initialized = True
def getLogger(
self,
name: Optional[str] = None
) -> logging.Logger:
if name:
return self.__logger.getChild(name)
return self.__logger
def setLevel(
self,
level: int
):
if self.__logger:
self.__logger.setLevel(level)
def logDir(
self
) -> str:
return self.__log_dir
# LogManager singleton instance.
_log_manager_instance = None
# Singleton instance lock.
_instance_lock = threading.Lock()
def instance(
log_dir: str = ""
) -> LogManager:
global _log_manager_instance
with _instance_lock:
if _log_manager_instance is None:
if not log_dir:
raise ValueError("LogManager initialization requires log_dir parameter")
_log_manager_instance = LogManager(log_dir)
else:
if log_dir and _log_manager_instance.logDir() != os.path.abspath(log_dir):
raise ValueError("LogManager instance already initialized with a different log directory")
return _log_manager_instance
def getLogger(
name: Optional[str] = None
) -> logging.Logger:
if _log_manager_instance is None:
raise RuntimeError("LogManager not initialized, please call LogManager.instance(log_dir) first")
return _log_manager_instance.getLogger(name)
+1 -1
View File
@@ -2,7 +2,7 @@
Utils module for the AutoLibrary project.
Here are the classes and modules in this package:
- ConfigManager: Configuration manager class for the AutoLibrary project.
- TimerUtils: Timer utils class for the AutoLibrary project.
- JSONReader: JSON reader class for the AutoLibrary project.
- JSONWriter: JSON writer class for the AutoLibrary project.
"""