mirror of
https://github.com/KenanZhu/AutoLibrary.git
synced 2026-06-18 15:33:03 +08:00
chore(*): 重构项目结构
- 新增 src/boot 目录,用于存放启动时需要初始化的模块 - 新增 src/managers 目录,用于存放项目中的管理模块 - 新增 src/managers/config 目录,用于存放配置管理模块 - 新增 src/managers/log 目录,用于存放日志管理模块 - 新增 src/managers/driver 目录,用于存放浏览器驱动管理模块 - 修改对应文件中 import 导入路径
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
"""
|
||||
Managers module for the AutoLibrary project.
|
||||
|
||||
Here are the classes and modules in this package:
|
||||
- ConfigManager: Config manager for managing configuration files.
|
||||
- LogManager: Log manager for logging.
|
||||
- WebDriverManager: Web driver manager for managing web drivers.
|
||||
"""
|
||||
@@ -0,0 +1,245 @@
|
||||
# -*- 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:
|
||||
if not config_dir:
|
||||
raise ValueError("ConfigManager 需要配置目录参数")
|
||||
_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
|
||||
@@ -0,0 +1,6 @@
|
||||
"""
|
||||
Config managers module for the AutoLibrary project.
|
||||
|
||||
Here are the classes and modules in this package:
|
||||
- ConfigManager: Config manager for managing configuration files.
|
||||
"""
|
||||
@@ -0,0 +1,8 @@
|
||||
"""
|
||||
Driver managers module for the AutoLibrary project.
|
||||
|
||||
Here are the classes and modules in this package:
|
||||
- WebBrowserDetector: Web browser detector class.
|
||||
- WebDriverDownloader: Web driver downloader class.
|
||||
- WebDriverManager: Web driver manager class.
|
||||
"""
|
||||
@@ -0,0 +1,191 @@
|
||||
# -*- 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 需要日志目录参数")
|
||||
_log_manager_instance = LogManager(log_dir)
|
||||
else:
|
||||
if log_dir and _log_manager_instance.logDir() != os.path.abspath(log_dir):
|
||||
raise ValueError("LogManager 的实例已初始化,不能使用不同的日志目录")
|
||||
return _log_manager_instance
|
||||
|
||||
|
||||
def getLogger(
|
||||
name: Optional[str] = None
|
||||
) -> logging.Logger:
|
||||
|
||||
if _log_manager_instance is None:
|
||||
raise RuntimeError("LogManager 未初始化,请先调用 LogManager.instance(log_dir) 初始化")
|
||||
return _log_manager_instance.getLogger(name)
|
||||
@@ -0,0 +1,6 @@
|
||||
"""
|
||||
Log managers module for the AutoLibrary project.
|
||||
|
||||
Here are the classes and modules in this package:
|
||||
- LogManager: Log manager for logging.
|
||||
"""
|
||||
Reference in New Issue
Block a user