mirror of
https://github.com/LiteyukiStudio/LiteyukiBot.git
synced 2025-07-28 03:20:57 +00:00
🔧 重构配置管理,添加配置文件支持,更新日志系统,优化守护进程,完善测试用例
This commit is contained in:
@ -1,3 +1,7 @@
|
||||
from .daemon import Daemon
|
||||
from .log import logger
|
||||
|
||||
__all__ = ["Daemon"]
|
||||
__all__ = [
|
||||
"Daemon",
|
||||
"logger"
|
||||
]
|
@ -1,8 +1,5 @@
|
||||
import asyncio
|
||||
|
||||
import uvicorn
|
||||
from fastapi import FastAPI
|
||||
import hypercorn
|
||||
import hypercorn.run
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@ -11,10 +8,8 @@ async def root():
|
||||
return {"message": "Hello LiteyukiBot"}
|
||||
|
||||
|
||||
async def run_app():
|
||||
"""liteyukibot入口函数
|
||||
"""
|
||||
hypercorn.run.serve(app, config=hypercorn.Config.from_mapping(
|
||||
bind=["localhost:8000"],
|
||||
workers=1,
|
||||
))
|
||||
async def run_app(**kwargs):
|
||||
"""ASGI app 启动函数,在所有插件加载完后任务启动"""
|
||||
config = uvicorn.Config(app, **kwargs, log_config=None)
|
||||
server = uvicorn.Server(config)
|
||||
await server.serve()
|
@ -1,13 +1,13 @@
|
||||
import json
|
||||
import os
|
||||
import tomllib
|
||||
from typing import Any
|
||||
|
||||
import json
|
||||
import yaml
|
||||
import tomllib
|
||||
|
||||
config: dict[str, Any] = {} # 全局配置map
|
||||
flat_config: dict[str, Any] = {} # 扁平化配置map
|
||||
type RawConfig = dict[str, Any]
|
||||
|
||||
def load_from_yaml(file_path: str) -> dict[str, Any]:
|
||||
def load_from_yaml(file_path: str) -> RawConfig:
|
||||
"""从yaml文件中加载配置并返回字典
|
||||
|
||||
Args:
|
||||
@ -19,7 +19,7 @@ def load_from_yaml(file_path: str) -> dict[str, Any]:
|
||||
with open(file_path, "r", encoding="utf-8") as file:
|
||||
return yaml.safe_load(file)
|
||||
|
||||
def load_from_json(file_path: str) -> dict[str, Any]:
|
||||
def load_from_json(file_path: str) -> RawConfig:
|
||||
"""从json文件中加载配置并返回字典
|
||||
|
||||
Args:
|
||||
@ -32,7 +32,7 @@ def load_from_json(file_path: str) -> dict[str, Any]:
|
||||
with open(file_path, "r", encoding="utf-8") as file:
|
||||
return json.load(file)
|
||||
|
||||
def load_from_toml(file_path: str) -> dict[str, Any]:
|
||||
def load_from_toml(file_path: str) -> RawConfig:
|
||||
"""从toml文件中加载配置并返回字典
|
||||
|
||||
Args:
|
||||
@ -44,18 +44,26 @@ def load_from_toml(file_path: str) -> dict[str, Any]:
|
||||
with open(file_path, "rb") as file:
|
||||
return tomllib.load(file)
|
||||
|
||||
def merge_to_config(new_config: dict[str, Any], warn: bool=True) -> None:
|
||||
"""加载配置到全局配置字典,该函数有副作用,开发者尽量不要在多份配置文件中使用重复的配置项,否则会被覆盖
|
||||
def merge_dicts(base: RawConfig, new: RawConfig) -> RawConfig:
|
||||
"""递归合并两个字典
|
||||
|
||||
Args:
|
||||
new_config (dict[str, Any]): 新的字典
|
||||
warn (bool, optional): 是否启用重复键警告. 默认 True.
|
||||
base (dict[str, Any]): 原始字典
|
||||
new (dict[str, Any]): 新的字典
|
||||
|
||||
Returns:
|
||||
dict[str, Any]: 合并后的字典
|
||||
"""
|
||||
global config, flat_config
|
||||
config.update(new_config)
|
||||
flat_config = flatten_dict(config)
|
||||
for key, value in new.items():
|
||||
if key in base and isinstance(base[key], dict) and isinstance(value, dict):
|
||||
# 如果当前键对应的值是字典,则递归合并
|
||||
base[key] = merge_dicts(base[key], value)
|
||||
else:
|
||||
# 否则直接更新值
|
||||
base[key] = value
|
||||
return base
|
||||
|
||||
def flatten_dict(d: dict[str, Any], parent_key: str = '', sep: str = '.') -> dict[str, Any]:
|
||||
def flatten_dict(d: RawConfig, parent_key: str = '', sep: str = '.') -> RawConfig:
|
||||
"""将嵌套字典扁平化
|
||||
|
||||
Args:
|
||||
@ -89,10 +97,19 @@ def flatten_dict(d: dict[str, Any], parent_key: str = '', sep: str = '.') -> dic
|
||||
items.append((new_key, v))
|
||||
return dict(items)
|
||||
|
||||
def load_config_to_global(reset: bool = False) -> None:
|
||||
"""加载配置到全局配置字典
|
||||
def load_from_dir(dir_path: str) -> RawConfig:
|
||||
"""从目录中加载配置文件
|
||||
|
||||
Args:
|
||||
reset (bool, optional): 是否重置配置项. 默认 False.
|
||||
dir_path (str): 目录路径
|
||||
"""
|
||||
|
||||
config: RawConfig = {}
|
||||
for file_name in os.listdir(dir_path):
|
||||
if file_name.endswith(".yaml") or file_name.endswith(".yml"):
|
||||
config = merge_dicts(config, load_from_yaml(os.path.join(dir_path, file_name)) or {})
|
||||
elif file_name.endswith(".json"):
|
||||
config = merge_dicts(config, load_from_json(os.path.join(dir_path, file_name)) or {})
|
||||
elif file_name.endswith(".toml"):
|
||||
config = merge_dicts(config, load_from_toml(os.path.join(dir_path, file_name)) or {})
|
||||
|
||||
return config
|
@ -1,16 +1,71 @@
|
||||
import asyncio
|
||||
from typing import Type
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .asgi import run_app
|
||||
from .config import RawConfig, flatten_dict, load_from_dir, merge_dicts
|
||||
from .log import logger, set_level
|
||||
from .utils import pretty_format
|
||||
|
||||
|
||||
class Daemon:
|
||||
def __init__(self, **config):
|
||||
self.config = config
|
||||
|
||||
async def _run(self):
|
||||
"""liteyukibot入口函数
|
||||
"""Liteyuki 的 守护进程
|
||||
"""
|
||||
def __init__(self, **kwargs: RawConfig):
|
||||
"""Liteyuki Daemon Init
|
||||
Args:
|
||||
**kwargs: 其他配置项
|
||||
"""
|
||||
pass
|
||||
# 加载配置项
|
||||
self.config: RawConfig = kwargs
|
||||
# 获取配置文件目录
|
||||
if isinstance(config_dir := kwargs.get("config_dir", None), str):
|
||||
self.config = merge_dicts(self.config, load_from_dir(config_dir))
|
||||
# 插入扁平化配置
|
||||
self.config = merge_dicts(self.config, flatten_dict(self.config))
|
||||
|
||||
# 初始化日志
|
||||
set_level(self.config.get("log_level", "INFO"))
|
||||
|
||||
logger.debug(
|
||||
"configs: %s" % pretty_format(self.config, indent=2)
|
||||
)
|
||||
|
||||
|
||||
async def _run(self):
|
||||
"""liteyukibot事件循环入口
|
||||
"""
|
||||
# load plugins
|
||||
|
||||
# run asgi app
|
||||
asyncio.create_task(
|
||||
run_app(
|
||||
host=self.config.get("host", "127.0.0.1"),
|
||||
port=self.config.get("port", 8080),
|
||||
)
|
||||
)
|
||||
# 挂起
|
||||
logger.info("Liteyuki Daemon is running...")
|
||||
await asyncio.Event().wait()
|
||||
|
||||
def run(self):
|
||||
"""liteyukibot入口函数
|
||||
"""Daemon入口函数
|
||||
"""
|
||||
asyncio.run(self._run())
|
||||
try:
|
||||
asyncio.run(self._run())
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Liteyuki Daemon is exiting...")
|
||||
|
||||
def bind_config[T: BaseModel](self, model: Type[T]) -> T:
|
||||
"""将配置绑定到 Pydantic 模型,推荐使用`pydantic.Field`声明扁平化键字段名
|
||||
|
||||
Args:
|
||||
model (Type[T]): Pydantic 模型类
|
||||
|
||||
Returns:
|
||||
T: 绑定后的模型实例
|
||||
"""
|
||||
if not issubclass(model, BaseModel):
|
||||
raise TypeError("The provided model must be a subclass of BaseModel.")
|
||||
return model(**self.config)
|
39
liteyukibot/log.py
Normal file
39
liteyukibot/log.py
Normal file
@ -0,0 +1,39 @@
|
||||
import inspect
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from yukilog import default_debug_and_trace_format, default_format, get_logger
|
||||
|
||||
logger = get_logger("INFO")
|
||||
|
||||
class LoguruHandler(logging.Handler):
|
||||
def emit(self, record: logging.LogRecord):
|
||||
try:
|
||||
level = logger.level(record.levelname).name
|
||||
except ValueError:
|
||||
level = str(record.levelno)
|
||||
|
||||
frame, depth = inspect.currentframe(), 0
|
||||
while frame and (depth == 0 or frame.f_code.co_filename == logging.__file__):
|
||||
frame = frame.f_back
|
||||
depth += 1
|
||||
|
||||
logger.opt(depth=depth, exception=record.exc_info).log(
|
||||
level, record.getMessage()
|
||||
)
|
||||
|
||||
# 替换 logging 的全局日志器
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.handlers = [LoguruHandler()] # 只保留 LoguruHandler
|
||||
root_logger.setLevel(logging.INFO)
|
||||
|
||||
|
||||
def set_level(level: str):
|
||||
"""设置日志级别
|
||||
|
||||
Args:
|
||||
level (str): 日志级别
|
||||
"""
|
||||
logger.remove()
|
||||
logger.add(sys.stdout, format=default_format if level not in ["DEBUG", "TRACE"] else default_debug_and_trace_format, level=level)
|
||||
logging.getLogger().setLevel(level)
|
24
liteyukibot/utils/__init__.py
Normal file
24
liteyukibot/utils/__init__.py
Normal file
@ -0,0 +1,24 @@
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
|
||||
def pretty_print(obj: Any, indent: int=2) -> None:
|
||||
"""
|
||||
更好地打印对象
|
||||
|
||||
Args:
|
||||
obj (Any): 要打印的对象
|
||||
"""
|
||||
print(json.dumps(obj, indent=indent, ensure_ascii=False))
|
||||
|
||||
def pretty_format(obj: Any, indent: int =2 ) -> str:
|
||||
"""
|
||||
更好地格式化对象
|
||||
|
||||
Args:
|
||||
obj (Any): 要格式化的对象
|
||||
|
||||
Returns:
|
||||
str: 格式化后的字符串
|
||||
"""
|
||||
return json.dumps(obj, indent=indent, ensure_ascii=False)
|
Reference in New Issue
Block a user