diff --git a/liteyuki/__init__.py b/liteyuki/__init__.py
index ad7651f3..4ace66e1 100644
--- a/liteyuki/__init__.py
+++ b/liteyuki/__init__.py
@@ -2,15 +2,20 @@ from liteyuki.bot import (
LiteyukiBot,
get_bot
)
+
+from liteyuki.comm import (
+ Channel,
+ chan,
+ Event
+)
+
from liteyuki.plugin import (
load_plugin,
load_plugins
)
-# def get_bot_instance() -> LiteyukiBot | None:
-# """
-# 获取轻雪实例
-# Returns:
-# LiteyukiBot: 当前的轻雪实例
-# """
-# return _BOT_INSTANCE
+from liteyuki.log import (
+ logger,
+ init_log
+
+)
diff --git a/liteyuki/bot/__init__.py b/liteyuki/bot/__init__.py
index 0efe18d9..43074963 100644
--- a/liteyuki/bot/__init__.py
+++ b/liteyuki/bot/__init__.py
@@ -1,20 +1,25 @@
import asyncio
import multiprocessing
+import time
from typing import Any, Coroutine, Optional
import nonebot
import liteyuki
from liteyuki.plugin.load import load_plugin, load_plugins
+from liteyuki.utils import run_coroutine
+from liteyuki.log import logger, init_log
+
from src.utils import (
adapter_manager,
driver_manager,
)
-from src.utils.base.log import logger
+
from liteyuki.bot.lifespan import (
Lifespan,
LIFESPAN_FUNC,
)
+
from liteyuki.core.spawn_process import nb_run, ProcessingManager
__all__ = [
@@ -22,21 +27,18 @@ __all__ = [
"get_bot"
]
-_MAIN_PROCESS = multiprocessing.current_process().name == "MainProcess"
+"""是否为主进程"""
+IS_MAIN_PROCESS = multiprocessing.current_process().name == "MainProcess"
class LiteyukiBot:
def __init__(self, *args, **kwargs):
-
global _BOT_INSTANCE
_BOT_INSTANCE = self # 引用
- self.running = False
- self.config: dict[str, Any] = kwargs
- self.lifespan: Lifespan = Lifespan()
- self.init(**self.config) # 初始化
-
- if not _MAIN_PROCESS:
- pass
+ if not IS_MAIN_PROCESS:
+ self.config: dict[str, Any] = kwargs
+ self.lifespan: Lifespan = Lifespan()
+ self.init(**self.config) # 初始化
else:
print("\033[34m" + r"""
__ ______ ________ ________ __ __ __ __ __ __ ______
@@ -51,96 +53,57 @@ $$$$$$$$/ $$$$$$/ $$/ $$$$$$$$/ $$/ $$$$$$/ $$/ $$/ $$$$$$/
""" + "\033[0m")
def run(self, *args, **kwargs):
- if _MAIN_PROCESS:
- load_plugins("liteyuki/plugins")
- asyncio.run(self.lifespan.before_start())
+ if IS_MAIN_PROCESS:
self._run_nb_in_spawn_process(*args, **kwargs)
else:
# 子进程启动
-
+ load_plugins("liteyuki/plugins") # 加载轻雪插件
driver_manager.init(config=self.config)
adapter_manager.init(self.config)
adapter_manager.register()
nonebot.load_plugin("src.liteyuki_main")
+ run_coroutine(self.lifespan.after_start()) # 启动前
def _run_nb_in_spawn_process(self, *args, **kwargs):
"""
- 在新的进程中运行nonebot.run方法
+ 在新的进程中运行nonebot.run方法,该函数在主进程中被调用
Args:
*args:
**kwargs:
Returns:
"""
-
- timeout_limit: int = 20
- should_exit = False
-
- while not should_exit:
- ctx = multiprocessing.get_context("spawn")
- event = ctx.Event()
- ProcessingManager.event = event
- process = ctx.Process(
- target=nb_run,
- args=(event,) + args,
- kwargs=kwargs,
- )
- process.start() # 启动进程
-
- asyncio.run(self.lifespan.after_start())
+ if IS_MAIN_PROCESS:
+ timeout_limit: int = 20
+ should_exit = False
while not should_exit:
- if ProcessingManager.event.wait(1):
- logger.info("Receive reboot event")
- process.terminate()
- process.join(timeout_limit)
- if process.is_alive():
- logger.warning(
- f"Process {process.pid} is still alive after {timeout_limit} seconds, force kill it."
- )
- process.kill()
- break
- elif process.is_alive():
- continue
- else:
- should_exit = True
+ ctx = multiprocessing.get_context("spawn")
+ event = ctx.Event()
+ ProcessingManager.event = event
+ process = ctx.Process(
+ target=nb_run,
+ args=(event,) + args,
+ kwargs=kwargs,
+ )
+ process.start() # 启动进程
- @staticmethod
- def _run_coroutine(*coro: Coroutine):
- """
- 运行协程
- Args:
- coro:
-
- Returns:
-
- """
- # 检测是否有现有的事件循环
- new_loop = False
- try:
- loop = asyncio.get_event_loop()
- except RuntimeError:
- new_loop = True
- loop = asyncio.new_event_loop()
- asyncio.set_event_loop(loop)
-
- if new_loop:
- for c in coro:
- loop.run_until_complete(c)
- loop.close()
-
- else:
- for c in coro:
- loop.create_task(c)
-
- @property
- def status(self) -> int:
- """
- 获取轻雪状态
- Returns:
- int: 0:未启动 1:运行中
- """
- return 1 if self.running else 0
+ while not should_exit:
+ if ProcessingManager.event.wait(1):
+ logger.info("Receive reboot event")
+ process.terminate()
+ process.join(timeout_limit)
+ if process.is_alive():
+ logger.warning(
+ f"Process {process.pid} is still alive after {timeout_limit} seconds, force kill it."
+ )
+ process.kill()
+ break
+ elif process.is_alive():
+ liteyuki.chan.send("轻雪进程正常运行", "sub")
+ continue
+ else:
+ should_exit = True
def restart(self):
"""
@@ -149,14 +112,12 @@ $$$$$$$$/ $$$$$$/ $$/ $$$$$$$$/ $$/ $$$$$$/ $$/ $$/ $$$$$$/
"""
logger.info("Stopping LiteyukiBot...")
-
logger.debug("Running before_restart functions...")
- self._run_coroutine(self.lifespan.before_restart())
+ run_coroutine(self.lifespan.before_restart())
logger.debug("Running before_shutdown functions...")
- self._run_coroutine(self.lifespan.before_shutdown())
+ run_coroutine(self.lifespan.before_shutdown())
ProcessingManager.restart()
- self.running = False
def init(self, *args, **kwargs):
"""
@@ -166,13 +127,12 @@ $$$$$$$$/ $$$$$$/ $$/ $$$$$$$$/ $$/ $$$$$$/ $$/ $$/ $$$$$$/
"""
self.init_config()
self.init_logger()
- if not _MAIN_PROCESS:
+ if not IS_MAIN_PROCESS:
nonebot.init(**kwargs)
asyncio.run(self.lifespan.after_nonebot_init())
def init_logger(self):
- from src.utils.base.log import init_log
- init_log()
+ init_log(config=self.config)
def init_config(self):
pass
diff --git a/liteyuki/comm/__init__.py b/liteyuki/comm/__init__.py
new file mode 100644
index 00000000..fd19662a
--- /dev/null
+++ b/liteyuki/comm/__init__.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+"""
+Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
+
+@Time : 2024/7/26 下午10:36
+@Author : snowykami
+@Email : snowykami@outlook.com
+@File : __init__.py
+@Software: PyCharm
+该模块用于轻雪主进程和Nonebot子进程之间的通信
+"""
+from liteyuki.comm.channel import Channel, chan
+from liteyuki.comm.event import Event
+
+__all__ = [
+ "Channel",
+ "chan",
+ "Event",
+]
diff --git a/liteyuki/comm/channel.py b/liteyuki/comm/channel.py
new file mode 100644
index 00000000..3dcb8ec7
--- /dev/null
+++ b/liteyuki/comm/channel.py
@@ -0,0 +1,179 @@
+# -*- coding: utf-8 -*-
+"""
+Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
+
+@Time : 2024/7/26 下午11:21
+@Author : snowykami
+@Email : snowykami@outlook.com
+@File : channel.py
+@Software: PyCharm
+
+本模块定义了一个通用的通道类,用于进程间通信
+"""
+import threading
+from multiprocessing import Queue
+from queue import Empty, Full
+from typing import Any, Awaitable, Callable, List, Optional, TypeAlias
+
+from nonebot import logger
+
+from liteyuki.utils import is_coroutine_callable, run_coroutine
+
+SYNC_ON_RECEIVE_FUNC: TypeAlias = Callable[[Any], Any]
+ASYNC_ON_RECEIVE_FUNC: TypeAlias = Callable[[Any], Awaitable[Any]]
+ON_RECEIVE_FUNC: TypeAlias = SYNC_ON_RECEIVE_FUNC | ASYNC_ON_RECEIVE_FUNC
+
+SYNC_FILTER_FUNC: TypeAlias = Callable[[Any], bool]
+ASYNC_FILTER_FUNC: TypeAlias = Callable[[Any], Awaitable[bool]]
+FILTER_FUNC: TypeAlias = SYNC_FILTER_FUNC | ASYNC_FILTER_FUNC
+
+
+class Channel:
+ def __init__(self, buffer_size: int = 0):
+ self._queue = Queue(buffer_size)
+ self._closed = False
+ self._on_receive_funcs: List[ON_RECEIVE_FUNC] = []
+ self._on_receive_funcs_with_receiver: dict[str, List[ON_RECEIVE_FUNC]] = {}
+
+ self._receiving_thread = threading.Thread(target=self._start_receiver, daemon=True)
+ self._receiving_thread.start()
+
+ def send(
+ self,
+ data: Any,
+ receiver: Optional[str] = None,
+ block: bool = True,
+ timeout: Optional[float] = None
+ ):
+ """
+ 发送数据
+ Args:
+ data: 数据
+ receiver: 接收者,如果为None则广播
+ block: 是否阻塞
+ timeout: 超时时间
+
+ Returns:
+
+ """
+ print(f"send {data} -> {receiver}")
+ if self._closed:
+ raise RuntimeError("Cannot send to a closed channel")
+ try:
+ self._queue.put((data, receiver), block, timeout)
+ except Full:
+ logger.warning("Channel buffer is full, send operation is blocked")
+
+ def receive(
+ self,
+ receiver: str = None,
+ block: bool = True,
+ timeout: Optional[float] = None
+ ) -> Any:
+ """
+ 接收数据
+ Args:
+ receiver: 接收者,如果为None则接收任意数据
+ block: 是否阻塞
+ timeout: 超时时间
+
+ Returns:
+
+ """
+ if self._closed:
+ raise RuntimeError("Cannot receive from a closed channel")
+ try:
+ while True:
+ data, data_receiver = self._queue.get(block, timeout)
+ if receiver is None or receiver == data_receiver:
+ return data
+ except Empty:
+ if not block:
+ return None
+ raise
+
+ def close(self):
+ """
+ 关闭通道
+ Returns:
+
+ """
+ self._closed = True
+ self._queue.close()
+ while not self._queue.empty():
+ self._queue.get()
+
+ def on_receive(
+ self,
+ filter_func: Optional[FILTER_FUNC] = None,
+ receiver: Optional[str] = None,
+ ) -> Callable[[ON_RECEIVE_FUNC], ON_RECEIVE_FUNC]:
+ """
+ 接收数据并执行函数
+ Args:
+ filter_func: 过滤函数,为None则不过滤
+ receiver: 接收者, 为None则接收任意数据
+ Returns:
+ 装饰器,装饰一个函数在接收到数据后执行
+ """
+
+ def decorator(func: ON_RECEIVE_FUNC) -> ON_RECEIVE_FUNC:
+ async def wrapper(data: Any) -> Any:
+ if filter_func is not None:
+ if is_coroutine_callable(filter_func):
+ if not await filter_func(data):
+ return
+ else:
+ if not filter_func(data):
+ return
+ return await func(data)
+
+ if receiver is None:
+ self._on_receive_funcs.append(wrapper)
+ else:
+ if receiver not in self._on_receive_funcs_with_receiver:
+ self._on_receive_funcs_with_receiver[receiver] = []
+ self._on_receive_funcs_with_receiver[receiver].append(wrapper)
+ return func
+
+ return decorator
+
+ def _start_receiver(self):
+ """
+ 使用多线程启动接收循环,在通道实例化时自动启动
+ Returns:
+ """
+ while True:
+ data, receiver = self._queue.get(block=True, timeout=None)
+ self._run_on_receive_funcs(data, receiver)
+
+ def _run_on_receive_funcs(self, data: Any, receiver: Optional[str] = None):
+ """
+ 运行接收函数
+ Args:
+ data: 数据
+ Returns:
+
+ """
+ if receiver is None:
+ for func in self._on_receive_funcs:
+ if is_coroutine_callable(func):
+ run_coroutine(func(data))
+ else:
+ func(data)
+ else:
+ for func in self._on_receive_funcs_with_receiver.get(receiver, []):
+ if is_coroutine_callable(func):
+ run_coroutine(func(data))
+ else:
+ func(data)
+
+ def __iter__(self):
+ return self
+
+ def __next__(self, timeout: Optional[float] = None) -> Any:
+ return self.receive(block=True, timeout=timeout)
+
+
+"""默认通道实例,可直接从模块导入使用"""
+chan = Channel()
diff --git a/liteyuki/comm/event.py b/liteyuki/comm/event.py
new file mode 100644
index 00000000..c3bddbf3
--- /dev/null
+++ b/liteyuki/comm/event.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+"""
+Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
+
+@Time : 2024/7/26 下午10:47
+@Author : snowykami
+@Email : snowykami@outlook.com
+@File : event.py
+@Software: PyCharm
+"""
+from typing import Any
+
+
+class Event:
+ """
+ 事件类
+ """
+
+ def __init__(self, name: str, data: dict[str, Any]):
+ self.name = name
+ self.data = data
diff --git a/liteyuki/log.py b/liteyuki/log.py
new file mode 100644
index 00000000..a779a149
--- /dev/null
+++ b/liteyuki/log.py
@@ -0,0 +1,84 @@
+# -*- coding: utf-8 -*-
+"""
+Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
+
+@Time : 2024/7/27 上午9:12
+@Author : snowykami
+@Email : snowykami@outlook.com
+@File : log.py
+@Software: PyCharm
+"""
+import sys
+import loguru
+from typing import TYPE_CHECKING
+
+logger = loguru.logger
+if TYPE_CHECKING:
+ # avoid sphinx autodoc resolve annotation failed
+ # because loguru module do not have `Logger` class actually
+ from loguru import Record
+
+
+def default_filter(record: "Record"):
+ """默认的日志过滤器,根据 `config.log_level` 配置改变日志等级。"""
+ log_level = record["extra"].get("nonebot_log_level", "INFO")
+ levelno = logger.level(log_level).no if isinstance(log_level, str) else log_level
+ return record["level"].no >= levelno
+
+
+# DEBUG日志格式
+debug_format: str = (
+ "{time:YYYY-MM-DD HH:mm:ss} "
+ "[{level.icon}] "
+ "<{name}.{module}.{function}:{line}> "
+ "{message}"
+)
+
+# 默认日志格式
+default_format: str = (
+ "{time:MM-DD HH:mm:ss} "
+ "[{level.icon}] "
+ "<{name}> "
+ "{message}"
+)
+
+
+def get_format(level: str) -> str:
+ if level == "DEBUG":
+ return debug_format
+ else:
+ return default_format
+
+
+logger = loguru.logger.bind()
+
+
+def init_log(config: dict):
+ """
+ 在语言加载完成后执行
+ Returns:
+
+ """
+ global logger
+
+ logger.remove()
+ logger.add(
+ sys.stdout,
+ level=0,
+ diagnose=False,
+ filter=default_filter,
+ format=get_format(config.get("log_level", "INFO")),
+ )
+ show_icon = config.get("log_icon", True)
+
+ # debug = lang.get("log.debug", default="==DEBUG")
+ # info = lang.get("log.info", default="===INFO")
+ # success = lang.get("log.success", default="SUCCESS")
+ # warning = lang.get("log.warning", default="WARNING")
+ # error = lang.get("log.error", default="==ERROR")
+ #
+ # logger.level("DEBUG", color="", icon=f"{'🐛' if show_icon else ''}{debug}")
+ # logger.level("INFO", color="", icon=f"{'ℹ️' if show_icon else ''}{info}")
+ # logger.level("SUCCESS", color="", icon=f"{'✅' if show_icon else ''}{success}")
+ # logger.level("WARNING", color="", icon=f"{'⚠️' if show_icon else ''}{warning}")
+ # logger.level("ERROR", color="", icon=f"{'⭕' if show_icon else ''}{error}")
diff --git a/liteyuki/plugins/plugin_loader/__init__.py b/liteyuki/plugins/plugin_loader/__init__.py
index 11992593..1da9a131 100644
--- a/liteyuki/plugins/plugin_loader/__init__.py
+++ b/liteyuki/plugins/plugin_loader/__init__.py
@@ -1,8 +1,11 @@
+import asyncio
import multiprocessing
import time
+from apscheduler.schedulers.asyncio import AsyncIOScheduler
+
from liteyuki.plugin import PluginMetadata
-from liteyuki import get_bot
+from liteyuki import get_bot, chan
__plugin_metadata__ = PluginMetadata(
name="plugin_loader",
@@ -13,6 +16,7 @@ __plugin_metadata__ = PluginMetadata(
)
from src.utils import TempConfig, common_db
+
liteyuki = get_bot()
@@ -31,6 +35,19 @@ def _():
print("轻雪启动中")
+@liteyuki.on_after_start
+async def _():
+ print("轻雪启动完成")
+ chan.send("轻雪启动完成")
+
+
@liteyuki.on_after_nonebot_init
async def _():
print("NoneBot初始化完成")
+
+
+@chan.on_receive(receiver="main")
+async def _(data):
+ print("收到消息", data)
+ await asyncio.sleep(5)
+
diff --git a/liteyuki/utils.py b/liteyuki/utils.py
index af95af97..3f3bfab3 100644
--- a/liteyuki/utils.py
+++ b/liteyuki/utils.py
@@ -2,9 +2,11 @@
"""
一些常用的工具类,部分来源于 nonebot 并遵循其许可进行修改
"""
+import asyncio
import inspect
+import threading
from pathlib import Path
-from typing import Any, Callable
+from typing import Any, Callable, Coroutine
def is_coroutine_callable(call: Callable[..., Any]) -> bool:
@@ -23,6 +25,39 @@ def is_coroutine_callable(call: Callable[..., Any]) -> bool:
return inspect.iscoroutinefunction(func_)
+def run_coroutine(*coro: Coroutine):
+ """
+ 运行协程
+ Args:
+ coro:
+
+ Returns:
+
+ """
+
+ # 检测是否有现有的事件循环
+
+ try:
+ loop = asyncio.get_event_loop()
+ if loop.is_running():
+ # 如果事件循环正在运行,创建任务
+ for c in coro:
+ asyncio.ensure_future(c)
+ else:
+ # 如果事件循环未运行,运行直到完成
+ for c in coro:
+ loop.run_until_complete(c)
+ except RuntimeError:
+ # 如果没有找到事件循环,创建一个新的
+ loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(loop)
+ loop.run_until_complete(asyncio.gather(*coro))
+ loop.close()
+ except Exception as e:
+ # 捕获其他异常,防止协程被重复等待
+ print(f"Exception occurred: {e}")
+
+
def path_to_module_name(path: Path) -> str:
"""
转换路径为模块名
diff --git a/src/liteyuki_main/core.py b/src/liteyuki_main/core.py
index 0304d502..e751fb3c 100644
--- a/src/liteyuki_main/core.py
+++ b/src/liteyuki_main/core.py
@@ -88,7 +88,7 @@ async def _(matcher: Matcher, bot: T_Bot, event: T_MessageEvent):
"reload_bot_id" : bot.self_id,
"reload_session_type": event_utils.get_message_type(event),
"reload_session_id" : (event.group_id if event.message_type == "group" else event.user_id) if not isinstance(event,
- satori.event.Event) else event.channel.id,
+ satori.event.Event) else event.chan.id,
"delta_time" : 0
}
)
diff --git a/src/liteyuki_main/loader.py b/src/liteyuki_main/loader.py
index 55ebccea..020b3ade 100644
--- a/src/liteyuki_main/loader.py
+++ b/src/liteyuki_main/loader.py
@@ -1,3 +1,5 @@
+import asyncio
+
import nonebot.plugin
from nonebot import get_driver
from src.utils import init_log
@@ -6,7 +8,9 @@ from src.utils.base.data_manager import InstalledPlugin, plugin_db
from src.utils.base.resource import load_resources
from src.utils.message.tools import check_for_package
-from liteyuki import get_bot
+from liteyuki import get_bot, chan
+
+from nonebot_plugin_apscheduler import scheduler
load_resources()
init_log()
@@ -32,33 +36,3 @@ async def load_plugins():
nonebot.plugin.load_plugins("plugins")
else:
nonebot.logger.info("Safe mode is on, no plugin loaded.")
-
-
-@liteyuki_bot.on_before_start
-async def _():
- print("启动前")
-
-
-@liteyuki_bot.on_after_start
-async def _():
- print("启动后")
-
-
-@liteyuki_bot.on_before_shutdown
-async def _():
- print("停止前")
-
-
-@liteyuki_bot.on_after_shutdown
-async def _():
- print("停止后")
-
-
-@liteyuki_bot.on_before_restart
-async def _():
- print("重启前")
-
-
-@liteyuki_bot.on_after_restart
-async def _():
- print("重启后")
diff --git a/src/plugins/packmanv2/npm/data_source.py b/src/plugins/packmanv2/npm/data_source.py
index f142d786..49468675 100644
--- a/src/plugins/packmanv2/npm/data_source.py
+++ b/src/plugins/packmanv2/npm/data_source.py
@@ -7,6 +7,9 @@ from pydantic import BaseModel
from src.utils.base.config import get_config
from src.utils.io import fetch
+NONEBOT_PLUGIN_STORE_URL: str = "https://registry.nonebot.dev/plugins.json" # NoneBot商店地址
+LITEYUKI_PLUGIN_STORE_URL: str = "https://bot.liteyuki.icu/assets/plugins.json" # 轻雪商店地址
+
class Session:
def __init__(self, session_type: str, session_id: int | str):
diff --git a/src/utils/base/data.py b/src/utils/base/data.py
index e5588488..2699e4ca 100644
--- a/src/utils/base/data.py
+++ b/src/utils/base/data.py
@@ -1,12 +1,12 @@
+import inspect
import os
import pickle
import sqlite3
from types import NoneType
from typing import Any, Callable
-from packaging.version import parse
-import inspect
-import nonebot
-import pydantic
+
+from nonebot import logger
+from nonebot.compat import PYDANTIC_V2
from pydantic import BaseModel
@@ -15,10 +15,10 @@ class LiteModel(BaseModel):
id: int = None
def dump(self, *args, **kwargs):
- if parse(pydantic.__version__) < parse("2.0.0"):
- return self.dict(*args, **kwargs)
- else:
+ if PYDANTIC_V2:
return self.model_dump(*args, **kwargs)
+ else:
+ return self.dict(*args, **kwargs)
class Database:
@@ -60,7 +60,7 @@ class Database:
"""
table_name = model.TABLE_NAME
model_type = type(model)
- nonebot.logger.debug(f"Selecting {model.TABLE_NAME} WHERE {condition.replace('?', '%s') % args}")
+ logger.debug(f"Selecting {model.TABLE_NAME} WHERE {condition.replace('?', '%s') % args}")
if not table_name:
raise ValueError(f"数据模型{model_type.__name__}未提供表名")
@@ -88,7 +88,7 @@ class Database:
"""
table_list = [item[0] for item in self.cursor.execute("SELECT name FROM sqlite_master WHERE type='table'").fetchall()]
for model in args:
- nonebot.logger.debug(f"Upserting {model}")
+ logger.debug(f"Upserting {model}")
if not model.TABLE_NAME:
raise ValueError(f"数据模型 {model.__class__.__name__} 未提供表名")
elif model.TABLE_NAME not in table_list:
@@ -206,7 +206,7 @@ class Database:
"""
table_name = model.TABLE_NAME
- nonebot.logger.debug(f"Deleting {model} WHERE {condition} {args}")
+ logger.debug(f"Deleting {model} WHERE {condition} {args}")
if not table_name:
raise ValueError(f"数据模型{model.__class__.__name__}未提供表名")
if model.id is not None:
diff --git a/test/test_core.py b/tests/test_core.py
similarity index 100%
rename from test/test_core.py
rename to tests/test_core.py
diff --git a/test/test_lyapi.py b/tests/test_lyapi.py
similarity index 100%
rename from test/test_lyapi.py
rename to tests/test_lyapi.py
diff --git a/test/test_lyfunc.py b/tests/test_lyfunc.py
similarity index 100%
rename from test/test_lyfunc.py
rename to tests/test_lyfunc.py