7 Commits

Author SHA1 Message Date
4bf8512a7d 新增on_keywords 2024-08-22 09:35:02 +08:00
a3a31a2c94 🔥 移除通道的部分特性 2024-08-22 07:23:44 +08:00
9ed4c1abb1 📝 文档新增源代码展示 2024-08-21 18:05:04 +08:00
a9c6ea0452 🐛 修复npm无法显示的问题 2024-08-21 17:59:21 +08:00
9e2bbe2e5c 🐛 修复npm无法显示的问题 2024-08-20 21:39:01 +08:00
598bff8c49 擴展event字段 2024-08-20 20:38:10 +08:00
eb7c8300fa 擴展event字段 2024-08-20 20:30:50 +08:00
75 changed files with 6980 additions and 690 deletions

3
.gitignore vendored
View File

@ -53,4 +53,5 @@ dist
doc
mkdoc2.py
mkdoc2.py
result.json

View File

@ -15,6 +15,26 @@ Returns:
LiteyukiBot: 当前的轻雪实例
<details>
<summary>源代码</summary>
```python
def get_bot() -> LiteyukiBot:
"""
获取轻雪实例
Returns:
LiteyukiBot: 当前的轻雪实例
"""
if IS_MAIN_PROCESS:
if _BOT_INSTANCE is None:
raise RuntimeError('Liteyuki instance not initialized.')
return _BOT_INSTANCE
else:
raise RuntimeError("Can't get bot instance in sub process.")
```
</details>
### ***def*** `get_config(key: str, default: Any) -> Any`
获取配置
@ -31,6 +51,24 @@ Returns:
Any: 配置值
<details>
<summary>源代码</summary>
```python
def get_config(key: str, default: Any=None) -> Any:
"""
获取配置
Args:
key: 配置键
default: 默认值
Returns:
Any: 配置值
"""
return get_bot().config.get(key, default)
```
</details>
### ***def*** `get_config_with_compat(key: str, compat_keys: tuple[str], default: Any) -> Any`
获取配置,兼容旧版本
@ -49,10 +87,44 @@ Returns:
Any: 配置值
<details>
<summary>源代码</summary>
```python
def get_config_with_compat(key: str, compat_keys: tuple[str], default: Any=None) -> Any:
"""
获取配置,兼容旧版本
Args:
key: 配置键
compat_keys: 兼容键
default: 默认值
Returns:
Any: 配置值
"""
if key in get_bot().config:
return get_bot().config[key]
for compat_key in compat_keys:
if compat_key in get_bot().config:
logger.warning(f'Config key "{compat_key}" will be deprecated, use "{key}" instead.')
return get_bot().config[compat_key]
return default
```
</details>
### ***def*** `print_logo() -> None`
<details>
<summary>源代码</summary>
```python
def print_logo():
print('\x1b[34m' + '\n __ ______ ________ ________ __ __ __ __ __ __ ______ \n / | / |/ |/ |/ \\ / |/ | / |/ | / |/ |\n $$ | $$$$$$/ $$$$$$$$/ $$$$$$$$/ $$ \\ /$$/ $$ | $$ |$$ | /$$/ $$$$$$/ \n $$ | $$ | $$ | $$ |__ $$ \\/$$/ $$ | $$ |$$ |/$$/ $$ | \n $$ | $$ | $$ | $$ | $$ $$/ $$ | $$ |$$ $$< $$ | \n $$ | $$ | $$ | $$$$$/ $$$$/ $$ | $$ |$$$$$ \\ $$ | \n $$ |_____ _$$ |_ $$ | $$ |_____ $$ | $$ \\__$$ |$$ |$$ \\ _$$ |_ \n $$ |/ $$ | $$ | $$ | $$ | $$ $$/ $$ | $$ |/ $$ |\n $$$$$$$$/ $$$$$$/ $$/ $$$$$$$$/ $$/ $$$$$$/ $$/ $$/ $$$$$$/ \n ' + '\x1b[0m')
```
</details>
### ***class*** `LiteyukiBot`
@ -67,22 +139,124 @@ Args:
**kwargs: 配置
<details>
<summary>源代码</summary>
```python
def __init__(self, *args, **kwargs) -> None:
"""
初始化轻雪实例
Args:
*args:
**kwargs: 配置
"""
'常规操作'
print_logo()
global _BOT_INSTANCE
_BOT_INSTANCE = self
'配置'
self.config: dict[str, Any] = kwargs
'初始化'
self.init(**self.config)
logger.info('Liteyuki is initializing...')
'生命周期管理'
self.lifespan = Lifespan()
self.process_manager: ProcessManager = ProcessManager(lifespan=self.lifespan)
'事件循环'
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
self.stop_event = threading.Event()
self.call_restart_count = 0
'加载插件加载器'
load_plugin('liteyuki.plugins.plugin_loader')
'信号处理'
signal.signal(signal.SIGINT, self._handle_exit)
signal.signal(signal.SIGTERM, self._handle_exit)
atexit.register(self.process_manager.terminate_all)
```
</details>
### &emsp; ***def*** `run(self) -> None`
&emsp;启动逻辑
<details>
<summary>源代码</summary>
```python
def run(self):
"""
启动逻辑
"""
self.lifespan.before_start()
self.process_manager.start_all()
self.lifespan.after_start()
self.keep_alive()
```
</details>
### &emsp; ***def*** `keep_alive(self) -> None`
&emsp;保持轻雪运行
Returns:
<details>
<summary>源代码</summary>
```python
def keep_alive(self):
"""
保持轻雪运行
Returns:
"""
try:
while not self.stop_event.is_set():
time.sleep(0.5)
except KeyboardInterrupt:
logger.info('Liteyuki is stopping...')
self.stop()
```
</details>
### &emsp; ***def*** `restart(self, delay: int) -> None`
&emsp;重启轻雪本体
Returns:
<details>
<summary>源代码</summary>
```python
def restart(self, delay: int=0):
"""
重启轻雪本体
Returns:
"""
if self.call_restart_count < 1:
executable = sys.executable
args = sys.argv
logger.info('Restarting LiteyukiBot...')
time.sleep(delay)
if platform.system() == 'Windows':
cmd = 'start'
elif platform.system() == 'Linux':
cmd = 'nohup'
elif platform.system() == 'Darwin':
cmd = 'open'
else:
cmd = 'nohup'
self.process_manager.terminate_all()
threading.Thread(target=os.system, args=(f"{cmd} {executable} {' '.join(args)}",)).start()
sys.exit(0)
self.call_restart_count += 1
```
</details>
### &emsp; ***def*** `restart_process(self, name: Optional[str]) -> None`
&emsp;停止轻雪
@ -93,22 +267,83 @@ Args:
Returns:
<details>
<summary>源代码</summary>
```python
def restart_process(self, name: Optional[str]=None):
"""
停止轻雪
Args:
name: 进程名称, 默认为None, 所有进程
Returns:
"""
self.lifespan.before_process_shutdown()
self.lifespan.before_process_shutdown()
if name is not None:
chan_active = get_channel(f'{name}-active')
chan_active.send(1)
else:
for process_name in self.process_manager.processes:
chan_active = get_channel(f'{process_name}-active')
chan_active.send(1)
```
</details>
### &emsp; ***def*** `init(self) -> None`
&emsp;初始化轻雪, 自动调用
Returns:
<details>
<summary>源代码</summary>
```python
def init(self, *args, **kwargs):
"""
初始化轻雪, 自动调用
Returns:
"""
self.init_logger()
```
</details>
### &emsp; ***def*** `init_logger(self) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def init_logger(self):
init_log(config=self.config)
```
</details>
### &emsp; ***def*** `stop(self) -> None`
&emsp;停止轻雪
Returns:
<details>
<summary>源代码</summary>
```python
def stop(self):
"""
停止轻雪
Returns:
"""
self.stop_event.set()
self.loop.stop()
```
</details>
### &emsp; ***def*** `on_before_start(self, func: LIFESPAN_FUNC) -> None`
&emsp;注册启动前的函数
@ -121,6 +356,23 @@ Args:
Returns:
<details>
<summary>源代码</summary>
```python
def on_before_start(self, func: LIFESPAN_FUNC):
"""
注册启动前的函数
Args:
func:
Returns:
"""
return self.lifespan.on_before_start(func)
```
</details>
### &emsp; ***def*** `on_after_start(self, func: LIFESPAN_FUNC) -> None`
&emsp;注册启动后的函数
@ -133,6 +385,23 @@ Args:
Returns:
<details>
<summary>源代码</summary>
```python
def on_after_start(self, func: LIFESPAN_FUNC):
"""
注册启动后的函数
Args:
func:
Returns:
"""
return self.lifespan.on_after_start(func)
```
</details>
### &emsp; ***def*** `on_after_shutdown(self, func: LIFESPAN_FUNC) -> None`
&emsp;注册停止后的函数:未实现
@ -145,6 +414,23 @@ Args:
Returns:
<details>
<summary>源代码</summary>
```python
def on_after_shutdown(self, func: LIFESPAN_FUNC):
"""
注册停止后的函数:未实现
Args:
func:
Returns:
"""
return self.lifespan.on_after_shutdown(func)
```
</details>
### &emsp; ***def*** `on_before_process_shutdown(self, func: LIFESPAN_FUNC) -> None`
&emsp;注册进程停止前的函数,为子进程停止时调用
@ -157,6 +443,23 @@ Args:
Returns:
<details>
<summary>源代码</summary>
```python
def on_before_process_shutdown(self, func: LIFESPAN_FUNC):
"""
注册进程停止前的函数,为子进程停止时调用
Args:
func:
Returns:
"""
return self.lifespan.on_before_process_shutdown(func)
```
</details>
### &emsp; ***def*** `on_before_process_restart(self, func: LIFESPAN_FUNC) -> None`
&emsp;注册进程重启前的函数,为子进程重启时调用
@ -169,6 +472,23 @@ Args:
Returns:
<details>
<summary>源代码</summary>
```python
def on_before_process_restart(self, func: LIFESPAN_FUNC):
"""
注册进程重启前的函数,为子进程重启时调用
Args:
func:
Returns:
"""
return self.lifespan.on_before_process_restart(func)
```
</details>
### &emsp; ***def*** `on_after_restart(self, func: LIFESPAN_FUNC) -> None`
&emsp;注册重启后的函数:未实现
@ -181,6 +501,23 @@ Args:
Returns:
<details>
<summary>源代码</summary>
```python
def on_after_restart(self, func: LIFESPAN_FUNC):
"""
注册重启后的函数:未实现
Args:
func:
Returns:
"""
return self.lifespan.on_after_restart(func)
```
</details>
### &emsp; ***def*** `on_after_nonebot_init(self, func: LIFESPAN_FUNC) -> None`
&emsp;注册nonebot初始化后的函数
@ -193,6 +530,23 @@ Args:
Returns:
<details>
<summary>源代码</summary>
```python
def on_after_nonebot_init(self, func: LIFESPAN_FUNC):
"""
注册nonebot初始化后的函数
Args:
func:
Returns:
"""
return self.lifespan.on_after_nonebot_init(func)
```
</details>
### ***var*** `executable = sys.executable`

View File

@ -15,6 +15,33 @@ Args:
Returns:
<details>
<summary>源代码</summary>
```python
@staticmethod
def run_funcs(funcs: list[LIFESPAN_FUNC | PROCESS_LIFESPAN_FUNC], *args, **kwargs) -> None:
"""
运行函数
Args:
funcs:
Returns:
"""
try:
loop = asyncio.get_event_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
tasks = []
for func in funcs:
if is_coroutine_callable(func):
tasks.append(func(*args, **kwargs))
else:
tasks.append(async_wrapper(func)(*args, **kwargs))
loop.run_until_complete(asyncio.gather(*tasks))
```
</details>
### ***class*** `Lifespan`
@ -23,6 +50,25 @@ Returns:
&emsp;轻雪生命周期管理,启动、停止、重启
<details>
<summary>源代码</summary>
```python
def __init__(self) -> None:
"""
轻雪生命周期管理,启动、停止、重启
"""
self.life_flag: int = 0
self._before_start_funcs: list[LIFESPAN_FUNC] = []
self._after_start_funcs: list[LIFESPAN_FUNC] = []
self._before_process_shutdown_funcs: list[LIFESPAN_FUNC] = []
self._after_shutdown_funcs: list[LIFESPAN_FUNC] = []
self._before_process_restart_funcs: list[LIFESPAN_FUNC] = []
self._after_restart_funcs: list[LIFESPAN_FUNC] = []
self._after_nonebot_init_funcs: list[LIFESPAN_FUNC] = []
```
</details>
### &emsp; ***@staticmethod***
### &emsp; ***def*** `run_funcs(funcs: list[LIFESPAN_FUNC | PROCESS_LIFESPAN_FUNC]) -> None`
@ -34,6 +80,33 @@ Args:
Returns:
<details>
<summary>源代码</summary>
```python
@staticmethod
def run_funcs(funcs: list[LIFESPAN_FUNC | PROCESS_LIFESPAN_FUNC], *args, **kwargs) -> None:
"""
运行函数
Args:
funcs:
Returns:
"""
try:
loop = asyncio.get_event_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
tasks = []
for func in funcs:
if is_coroutine_callable(func):
tasks.append(func(*args, **kwargs))
else:
tasks.append(async_wrapper(func)(*args, **kwargs))
loop.run_until_complete(asyncio.gather(*tasks))
```
</details>
### &emsp; ***def*** `on_before_start(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`
&emsp;注册启动时的函数
@ -46,6 +119,23 @@ Returns:
LIFESPAN_FUNC:
<details>
<summary>源代码</summary>
```python
def on_before_start(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
"""
注册启动时的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
"""
self._before_start_funcs.append(func)
return func
```
</details>
### &emsp; ***def*** `on_after_start(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`
&emsp;注册启动时的函数
@ -58,6 +148,23 @@ Returns:
LIFESPAN_FUNC:
<details>
<summary>源代码</summary>
```python
def on_after_start(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
"""
注册启动时的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
"""
self._after_start_funcs.append(func)
return func
```
</details>
### &emsp; ***def*** `on_before_process_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`
&emsp;注册停止前的函数
@ -70,6 +177,23 @@ Returns:
LIFESPAN_FUNC:
<details>
<summary>源代码</summary>
```python
def on_before_process_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
"""
注册停止前的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
"""
self._before_process_shutdown_funcs.append(func)
return func
```
</details>
### &emsp; ***def*** `on_after_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`
&emsp;注册停止后的函数
@ -84,6 +208,25 @@ Returns:
LIFESPAN_FUNC:
<details>
<summary>源代码</summary>
```python
def on_after_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
"""
注册停止后的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
"""
self._after_shutdown_funcs.append(func)
return func
```
</details>
### &emsp; ***def*** `on_before_process_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`
&emsp;注册重启时的函数
@ -96,6 +239,23 @@ Returns:
LIFESPAN_FUNC:
<details>
<summary>源代码</summary>
```python
def on_before_process_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
"""
注册重启时的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
"""
self._before_process_restart_funcs.append(func)
return func
```
</details>
### &emsp; ***def*** `on_after_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`
&emsp;注册重启后的函数
@ -108,6 +268,23 @@ Returns:
LIFESPAN_FUNC:
<details>
<summary>源代码</summary>
```python
def on_after_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
"""
注册重启后的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
"""
self._after_restart_funcs.append(func)
return func
```
</details>
### &emsp; ***def*** `on_after_nonebot_init(self, func: Any) -> None`
&emsp;注册 NoneBot 初始化后的函数
@ -120,42 +297,145 @@ Args:
Returns:
<details>
<summary>源代码</summary>
```python
def on_after_nonebot_init(self, func):
"""
注册 NoneBot 初始化后的函数
Args:
func:
Returns:
"""
self._after_nonebot_init_funcs.append(func)
return func
```
</details>
### &emsp; ***def*** `before_start(self) -> None`
&emsp;启动前
Returns:
<details>
<summary>源代码</summary>
```python
def before_start(self) -> None:
"""
启动前
Returns:
"""
logger.debug('Running before_start functions')
self.run_funcs(self._before_start_funcs)
```
</details>
### &emsp; ***def*** `after_start(self) -> None`
&emsp;启动后
Returns:
<details>
<summary>源代码</summary>
```python
def after_start(self) -> None:
"""
启动后
Returns:
"""
logger.debug('Running after_start functions')
self.run_funcs(self._after_start_funcs)
```
</details>
### &emsp; ***def*** `before_process_shutdown(self) -> None`
&emsp;停止前
Returns:
<details>
<summary>源代码</summary>
```python
def before_process_shutdown(self) -> None:
"""
停止前
Returns:
"""
logger.debug('Running before_shutdown functions')
self.run_funcs(self._before_process_shutdown_funcs)
```
</details>
### &emsp; ***def*** `after_shutdown(self) -> None`
&emsp;停止后
Returns:
<details>
<summary>源代码</summary>
```python
def after_shutdown(self) -> None:
"""
停止后
Returns:
"""
logger.debug('Running after_shutdown functions')
self.run_funcs(self._after_shutdown_funcs)
```
</details>
### &emsp; ***def*** `before_process_restart(self) -> None`
&emsp;重启前
Returns:
<details>
<summary>源代码</summary>
```python
def before_process_restart(self) -> None:
"""
重启前
Returns:
"""
logger.debug('Running before_restart functions')
self.run_funcs(self._before_process_restart_funcs)
```
</details>
### &emsp; ***def*** `after_restart(self) -> None`
&emsp;重启后
Returns:
<details>
<summary>源代码</summary>
```python
def after_restart(self) -> None:
"""
重启后
Returns:
"""
logger.debug('Running after_restart functions')
self.run_funcs(self._after_restart_funcs)
```
</details>
### ***var*** `tasks = []`

View File

@ -1,5 +1,5 @@
---
title: liteyuki.comm.channel_
title: liteyuki.comm.channel
order: 1
icon: laptop-code
category: API
@ -15,6 +15,26 @@ Args:
channel: 通道实例
<details>
<summary>源代码</summary>
```python
def set_channel(name: str, channel: Channel):
"""
设置通道实例
Args:
name: 通道名称
channel: 通道实例
"""
if not isinstance(channel, Channel):
raise TypeError(f'channel_ must be an instance of Channel, {type(channel)} found')
if IS_MAIN_PROCESS:
_channel[name] = channel
else:
channel_deliver_passive_channel.send(('set_channel', {'name': name, 'channel_': channel}))
```
</details>
### ***def*** `set_channels(channels: dict[str, Channel]) -> None`
设置通道实例
@ -23,6 +43,21 @@ Args:
channels: 通道名称
<details>
<summary>源代码</summary>
```python
def set_channels(channels: dict[str, Channel]):
"""
设置通道实例
Args:
channels: 通道名称
"""
for name, channel in channels.items():
set_channel(name, channel)
```
</details>
### ***def*** `get_channel(name: str) -> Channel`
获取通道实例
@ -33,39 +68,156 @@ Args:
Returns:
<details>
<summary>源代码</summary>
```python
def get_channel(name: str) -> Channel:
"""
获取通道实例
Args:
name: 通道名称
Returns:
"""
if IS_MAIN_PROCESS:
return _channel[name]
else:
recv_chan = Channel[Channel[Any]]('recv_chan')
channel_deliver_passive_channel.send(('get_channel', {'name': name, 'recv_chan': recv_chan}))
return recv_chan.receive()
```
</details>
### ***def*** `get_channels() -> dict[str, Channel]`
获取通道实例
Returns:
<details>
<summary>源代码</summary>
```python
def get_channels() -> dict[str, Channel]:
"""
获取通道实例
Returns:
"""
if IS_MAIN_PROCESS:
return _channel
else:
recv_chan = Channel[dict[str, Channel[Any]]]('recv_chan')
channel_deliver_passive_channel.send(('get_channels', {'recv_chan': recv_chan}))
return recv_chan.receive()
```
</details>
### ***def*** `on_set_channel(data: tuple[str, dict[str, Any]]) -> None`
<details>
<summary>源代码</summary>
```python
@channel_deliver_passive_channel.on_receive(filter_func=lambda data: data[0] == 'set_channel')
def on_set_channel(data: tuple[str, dict[str, Any]]):
name, channel = (data[1]['name'], data[1]['channel_'])
set_channel(name, channel)
```
</details>
### ***def*** `on_get_channel(data: tuple[str, dict[str, Any]]) -> None`
<details>
<summary>源代码</summary>
```python
@channel_deliver_passive_channel.on_receive(filter_func=lambda data: data[0] == 'get_channel')
def on_get_channel(data: tuple[str, dict[str, Any]]):
name, recv_chan = (data[1]['name'], data[1]['recv_chan'])
recv_chan.send(get_channel(name))
```
</details>
### ***def*** `on_get_channels(data: tuple[str, dict[str, Any]]) -> None`
<details>
<summary>源代码</summary>
```python
@channel_deliver_passive_channel.on_receive(filter_func=lambda data: data[0] == 'get_channels')
def on_get_channels(data: tuple[str, dict[str, Any]]):
recv_chan = data[1]['recv_chan']
recv_chan.send(get_channels())
```
</details>
### ***def*** `decorator(func: Callable[[T], Any]) -> Callable[[T], Any]`
<details>
<summary>源代码</summary>
```python
def decorator(func: Callable[[T], Any]) -> Callable[[T], Any]:
global _func_id
async def wrapper(data: T) -> Any:
if filter_func is not None:
if is_coroutine_callable(filter_func):
if not await filter_func(data):
return
elif not filter_func(data):
return
if is_coroutine_callable(func):
return await func(data)
else:
return func(data)
_callback_funcs[_func_id] = wrapper
if IS_MAIN_PROCESS:
self._on_main_receive_funcs.append(_func_id)
else:
self._on_sub_receive_funcs.append(_func_id)
_func_id += 1
return func
```
</details>
### ***async def*** `wrapper(data: T) -> Any`
<details>
<summary>源代码</summary>
```python
async def wrapper(data: T) -> Any:
if filter_func is not None:
if is_coroutine_callable(filter_func):
if not await filter_func(data):
return
elif not filter_func(data):
return
if is_coroutine_callable(func):
return await func(data)
else:
return func(data)
```
</details>
### ***class*** `Channel(Generic[T])`
通道类,可以在进程间和进程内通信,双向但同时只能有一个发送者和一个接收者
有两种接收工作方式,但是只能选择一种,主动接收和被动接收,主动接收使用 `receive` 方法,被动接收使用 `on_receive` 装饰器
### &emsp; ***def*** `__init__(self, _id: str, type_check: bool) -> None`
### &emsp; ***def*** `__init__(self, _id: str, type_check: Optional[bool]) -> None`
&emsp;初始化通道
@ -73,6 +225,35 @@ Args:
_id: 通道ID
type_check: 是否开启类型检查, 若为空,则传入泛型默认开启,否则默认关闭
<details>
<summary>源代码</summary>
```python
def __init__(self, _id: str, type_check: Optional[bool]=None):
"""
初始化通道
Args:
_id: 通道ID
type_check: 是否开启类型检查, 若为空,则传入泛型默认开启,否则默认关闭
"""
self.conn_send, self.conn_recv = Pipe()
self._closed = False
self._on_main_receive_funcs: list[int] = []
self._on_sub_receive_funcs: list[int] = []
self.name: str = _id
self.is_main_receive_loop_running = False
self.is_sub_receive_loop_running = False
if type_check is None:
type_check = self._get_generic_type() is not None
elif type_check:
if self._get_generic_type() is None:
raise TypeError('Type hint is required for enforcing type check.')
self.type_check = type_check
```
</details>
### &emsp; ***def*** `send(self, data: T) -> None`
&emsp;发送数据
@ -81,16 +262,67 @@ Args:
data: 数据
<details>
<summary>源代码</summary>
```python
def send(self, data: T):
"""
发送数据
Args:
data: 数据
"""
if self.type_check:
_type = self._get_generic_type()
if _type is not None and (not self._validate_structure(data, _type)):
raise TypeError(f'Data must be an instance of {_type}, {type(data)} found')
if self._closed:
raise RuntimeError('Cannot send to a closed channel_')
self.conn_send.send(data)
```
</details>
### &emsp; ***def*** `receive(self) -> T`
&emsp;接收数据
Args:
<details>
<summary>源代码</summary>
```python
def receive(self) -> T:
"""
接收数据
Args:
"""
if self._closed:
raise RuntimeError('Cannot receive from a closed channel_')
while True:
data = self.conn_recv.recv()
return data
```
</details>
### &emsp; ***def*** `close(self) -> None`
&emsp;关闭通道
<details>
<summary>源代码</summary>
```python
def close(self):
"""
关闭通道
"""
self._closed = True
self.conn_send.close()
self.conn_recv.close()
```
</details>
### &emsp; ***def*** `on_receive(self, filter_func: Optional[FILTER_FUNC]) -> Callable[[Callable[[T], Any]], Callable[[T], Any]]`
&emsp;接收数据并执行函数
@ -103,6 +335,48 @@ Returns:
装饰器,装饰一个函数在接收到数据后执行
<details>
<summary>源代码</summary>
```python
def on_receive(self, filter_func: Optional[FILTER_FUNC]=None) -> Callable[[Callable[[T], Any]], Callable[[T], Any]]:
"""
接收数据并执行函数
Args:
filter_func: 过滤函数为None则不过滤
Returns:
装饰器,装饰一个函数在接收到数据后执行
"""
if not self.is_sub_receive_loop_running and (not IS_MAIN_PROCESS):
threading.Thread(target=self._start_sub_receive_loop, daemon=True).start()
if not self.is_main_receive_loop_running and IS_MAIN_PROCESS:
threading.Thread(target=self._start_main_receive_loop, daemon=True).start()
def decorator(func: Callable[[T], Any]) -> Callable[[T], Any]:
global _func_id
async def wrapper(data: T) -> Any:
if filter_func is not None:
if is_coroutine_callable(filter_func):
if not await filter_func(data):
return
elif not filter_func(data):
return
if is_coroutine_callable(func):
return await func(data)
else:
return func(data)
_callback_funcs[_func_id] = wrapper
if IS_MAIN_PROCESS:
self._on_main_receive_funcs.append(_func_id)
else:
self._on_sub_receive_funcs.append(_func_id)
_func_id += 1
return func
return decorator
```
</details>
### ***var*** `T = TypeVar('T')`
@ -127,6 +401,10 @@ Returns:
### ***var*** `type_check = self._get_generic_type() is not None`
### ***var*** `data = self.conn_recv.recv()`

View File

@ -13,3 +13,13 @@ category: API
&emsp;
<details>
<summary>源代码</summary>
```python
def __init__(self, name: str, data: dict[str, Any]):
self.name = name
self.data = data
```
</details>

View File

@ -5,22 +5,200 @@ icon: laptop-code
category: API
---
### ***def*** `run_subscriber_receive_funcs(channel_: str, data: Any) -> None`
运行订阅者接收函数
Args:
channel_: 频道
data: 数据
<details>
<summary>源代码</summary>
```python
@staticmethod
def run_subscriber_receive_funcs(channel_: str, data: Any):
"""
运行订阅者接收函数
Args:
channel_: 频道
data: 数据
"""
if IS_MAIN_PROCESS:
if channel_ in _on_main_subscriber_receive_funcs and _on_main_subscriber_receive_funcs[channel_]:
run_coroutine(*[func(data) for func in _on_main_subscriber_receive_funcs[channel_]])
elif channel_ in _on_sub_subscriber_receive_funcs and _on_sub_subscriber_receive_funcs[channel_]:
run_coroutine(*[func(data) for func in _on_sub_subscriber_receive_funcs[channel_]])
```
</details>
### ***def*** `on_get(data: tuple[str, dict[str, Any]]) -> None`
<details>
<summary>源代码</summary>
```python
@shared_memory.passive_chan.on_receive(lambda d: d[0] == 'get')
def on_get(data: tuple[str, dict[str, Any]]):
key = data[1]['key']
default = data[1]['default']
recv_chan = data[1]['recv_chan']
recv_chan.send(shared_memory.get(key, default))
```
</details>
### ***def*** `on_set(data: tuple[str, dict[str, Any]]) -> None`
<details>
<summary>源代码</summary>
```python
@shared_memory.passive_chan.on_receive(lambda d: d[0] == 'set')
def on_set(data: tuple[str, dict[str, Any]]):
key = data[1]['key']
value = data[1]['value']
shared_memory.set(key, value)
```
</details>
### ***def*** `on_delete(data: tuple[str, dict[str, Any]]) -> None`
<details>
<summary>源代码</summary>
```python
@shared_memory.passive_chan.on_receive(lambda d: d[0] == 'delete')
def on_delete(data: tuple[str, dict[str, Any]]):
key = data[1]['key']
shared_memory.delete(key)
```
</details>
### ***def*** `on_get_all(data: tuple[str, dict[str, Any]]) -> None`
<details>
<summary>源代码</summary>
```python
@shared_memory.passive_chan.on_receive(lambda d: d[0] == 'get_all')
def on_get_all(data: tuple[str, dict[str, Any]]):
recv_chan = data[1]['recv_chan']
recv_chan.send(shared_memory.get_all())
```
</details>
### ***def*** `on_publish(data: tuple[str, Any]) -> None`
<details>
<summary>源代码</summary>
```python
@channel.publish_channel.on_receive()
def on_publish(data: tuple[str, Any]):
channel_, data = data
shared_memory.run_subscriber_receive_funcs(channel_, data)
```
</details>
### ***def*** `decorator(func: ON_RECEIVE_FUNC) -> ON_RECEIVE_FUNC`
<details>
<summary>源代码</summary>
```python
def decorator(func: ON_RECEIVE_FUNC) -> ON_RECEIVE_FUNC:
async def wrapper(data: Any):
if is_coroutine_callable(func):
await func(data)
else:
func(data)
if IS_MAIN_PROCESS:
if channel_ not in _on_main_subscriber_receive_funcs:
_on_main_subscriber_receive_funcs[channel_] = []
_on_main_subscriber_receive_funcs[channel_].append(wrapper)
else:
if channel_ not in _on_sub_subscriber_receive_funcs:
_on_sub_subscriber_receive_funcs[channel_] = []
_on_sub_subscriber_receive_funcs[channel_].append(wrapper)
return wrapper
```
</details>
### ***async def*** `wrapper(data: Any) -> None`
<details>
<summary>源代码</summary>
```python
async def wrapper(data: Any):
if is_coroutine_callable(func):
await func(data)
else:
func(data)
```
</details>
### ***class*** `Subscriber`
### &emsp; ***def*** `__init__(self) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def __init__(self):
self._subscribers = {}
```
</details>
### &emsp; ***def*** `receive(self) -> Any`
&emsp;
<details>
<summary>源代码</summary>
```python
def receive(self) -> Any:
pass
```
</details>
### &emsp; ***def*** `unsubscribe(self) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def unsubscribe(self) -> None:
pass
```
</details>
### ***class*** `KeyValueStore`
@ -29,6 +207,20 @@ category: API
&emsp;
<details>
<summary>源代码</summary>
```python
def __init__(self):
self._store = {}
self.active_chan = Channel[tuple[str, Optional[dict[str, Any]]]](_id='shared_memory-active')
self.passive_chan = Channel[tuple[str, Optional[dict[str, Any]]]](_id='shared_memory-passive')
self.publish_channel = Channel[tuple[str, Any]](_id='shared_memory-publish')
self.is_main_receive_loop_running = False
self.is_sub_receive_loop_running = False
```
</details>
### &emsp; ***def*** `set(self, key: str, value: Any) -> None`
&emsp;设置键值对
@ -39,6 +231,27 @@ Args:
value: 值
<details>
<summary>源代码</summary>
```python
def set(self, key: str, value: Any) -> None:
"""
设置键值对
Args:
key: 键
value: 值
"""
if IS_MAIN_PROCESS:
lock = _get_lock(key)
with lock:
self._store[key] = value
else:
self.passive_chan.send(('set', {'key': key, 'value': value}))
```
</details>
### &emsp; ***def*** `get(self, key: str, default: Optional[Any]) -> Optional[Any]`
&emsp;获取键值对
@ -55,6 +268,31 @@ Returns:
Any: 值
<details>
<summary>源代码</summary>
```python
def get(self, key: str, default: Optional[Any]=None) -> Optional[Any]:
"""
获取键值对
Args:
key: 键
default: 默认值
Returns:
Any: 值
"""
if IS_MAIN_PROCESS:
lock = _get_lock(key)
with lock:
return self._store.get(key, default)
else:
recv_chan = Channel[Optional[Any]]('recv_chan')
self.passive_chan.send(('get', {'key': key, 'default': default, 'recv_chan': recv_chan}))
return recv_chan.receive()
```
</details>
### &emsp; ***def*** `delete(self, key: str, ignore_key_error: bool) -> None`
&emsp;删除键值对
@ -69,6 +307,34 @@ Args:
Returns:
<details>
<summary>源代码</summary>
```python
def delete(self, key: str, ignore_key_error: bool=True) -> None:
"""
删除键值对
Args:
key: 键
ignore_key_error: 是否忽略键不存在的错误
Returns:
"""
if IS_MAIN_PROCESS:
lock = _get_lock(key)
with lock:
if key in self._store:
try:
del self._store[key]
del _locks[key]
except KeyError as e:
if not ignore_key_error:
raise e
else:
self.passive_chan.send(('delete', {'key': key}))
```
</details>
### &emsp; ***def*** `get_all(self) -> dict[str, Any]`
&emsp;获取所有键值对
@ -77,6 +343,141 @@ Returns:
dict[str, Any]: 键值对
<details>
<summary>源代码</summary>
```python
def get_all(self) -> dict[str, Any]:
"""
获取所有键值对
Returns:
dict[str, Any]: 键值对
"""
if IS_MAIN_PROCESS:
return self._store
else:
recv_chan = Channel[dict[str, Any]]('recv_chan')
self.passive_chan.send(('get_all', {'recv_chan': recv_chan}))
return recv_chan.receive()
```
</details>
### &emsp; ***def*** `publish(self, channel_: str, data: Any) -> None`
&emsp;发布消息
Args:
channel_: 频道
data: 数据
Returns:
<details>
<summary>源代码</summary>
```python
def publish(self, channel_: str, data: Any) -> None:
"""
发布消息
Args:
channel_: 频道
data: 数据
Returns:
"""
self.active_chan.send(('publish', {'channel': channel_, 'data': data}))
```
</details>
### &emsp; ***def*** `on_subscriber_receive(self, channel_: str) -> Callable[[ON_RECEIVE_FUNC], ON_RECEIVE_FUNC]`
&emsp;订阅者接收消息时的回调
Args:
channel_: 频道
Returns:
装饰器
<details>
<summary>源代码</summary>
```python
def on_subscriber_receive(self, channel_: str) -> Callable[[ON_RECEIVE_FUNC], ON_RECEIVE_FUNC]:
"""
订阅者接收消息时的回调
Args:
channel_: 频道
Returns:
装饰器
"""
if IS_MAIN_PROCESS and (not self.is_main_receive_loop_running):
threading.Thread(target=self._start_receive_loop, daemon=True).start()
shared_memory.is_main_receive_loop_running = True
elif not IS_MAIN_PROCESS and (not self.is_sub_receive_loop_running):
threading.Thread(target=self._start_receive_loop, daemon=True).start()
shared_memory.is_sub_receive_loop_running = True
def decorator(func: ON_RECEIVE_FUNC) -> ON_RECEIVE_FUNC:
async def wrapper(data: Any):
if is_coroutine_callable(func):
await func(data)
else:
func(data)
if IS_MAIN_PROCESS:
if channel_ not in _on_main_subscriber_receive_funcs:
_on_main_subscriber_receive_funcs[channel_] = []
_on_main_subscriber_receive_funcs[channel_].append(wrapper)
else:
if channel_ not in _on_sub_subscriber_receive_funcs:
_on_sub_subscriber_receive_funcs[channel_] = []
_on_sub_subscriber_receive_funcs[channel_].append(wrapper)
return wrapper
return decorator
```
</details>
### &emsp; ***@staticmethod***
### &emsp; ***def*** `run_subscriber_receive_funcs(channel_: str, data: Any) -> None`
&emsp;运行订阅者接收函数
Args:
channel_: 频道
data: 数据
<details>
<summary>源代码</summary>
```python
@staticmethod
def run_subscriber_receive_funcs(channel_: str, data: Any):
"""
运行订阅者接收函数
Args:
channel_: 频道
data: 数据
"""
if IS_MAIN_PROCESS:
if channel_ in _on_main_subscriber_receive_funcs and _on_main_subscriber_receive_funcs[channel_]:
run_coroutine(*[func(data) for func in _on_main_subscriber_receive_funcs[channel_]])
elif channel_ in _on_sub_subscriber_receive_funcs and _on_sub_subscriber_receive_funcs[channel_]:
run_coroutine(*[func(data) for func in _on_sub_subscriber_receive_funcs[channel_]])
```
</details>
### ***class*** `GlobalKeyValueStore`
@ -86,6 +487,20 @@ Returns:
&emsp;
<details>
<summary>源代码</summary>
```python
@classmethod
def get_instance(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = KeyValueStore()
return cls._instance
```
</details>
### &emsp; ***attr*** `_instance: None`
### &emsp; ***attr*** `_lock: threading.Lock()`
@ -138,3 +553,11 @@ Returns:
### ***var*** `data = self.active_chan.receive()`
### ***var*** `data = self.publish_channel.receive()`

View File

@ -23,24 +23,117 @@ Returns:
扁平化后的配置文件,但也包含原有的键值对
<details>
<summary>源代码</summary>
```python
def flat_config(config: dict[str, Any]) -> dict[str, Any]:
"""
扁平化配置文件
{a:{b:{c:1}}} -> {"a.b.c": 1}
Args:
config: 配置项目
Returns:
扁平化后的配置文件,但也包含原有的键值对
"""
new_config = copy.deepcopy(config)
for key, value in config.items():
if isinstance(value, dict):
for k, v in flat_config(value).items():
new_config[f'{key}.{k}'] = v
return new_config
```
</details>
### ***def*** `load_from_yaml(file: str) -> dict[str, Any]`
Load config from yaml file
<details>
<summary>源代码</summary>
```python
def load_from_yaml(file: str) -> dict[str, Any]:
"""
Load config from yaml file
"""
logger.debug(f'Loading YAML config from {file}')
config = yaml.safe_load(open(file, 'r', encoding='utf-8'))
return flat_config(config if config is not None else {})
```
</details>
### ***def*** `load_from_json(file: str) -> dict[str, Any]`
Load config from json file
<details>
<summary>源代码</summary>
```python
def load_from_json(file: str) -> dict[str, Any]:
"""
Load config from json file
"""
logger.debug(f'Loading JSON config from {file}')
config = json.load(open(file, 'r', encoding='utf-8'))
return flat_config(config if config is not None else {})
```
</details>
### ***def*** `load_from_toml(file: str) -> dict[str, Any]`
Load config from toml file
<details>
<summary>源代码</summary>
```python
def load_from_toml(file: str) -> dict[str, Any]:
"""
Load config from toml file
"""
logger.debug(f'Loading TOML config from {file}')
config = toml.load(open(file, 'r', encoding='utf-8'))
return flat_config(config if config is not None else {})
```
</details>
### ***def*** `load_from_files() -> dict[str, Any]`
从指定文件加载配置项,会自动识别文件格式
默认执行扁平化选项
<details>
<summary>源代码</summary>
```python
def load_from_files(*files: str, no_warning: bool=False) -> dict[str, Any]:
"""
从指定文件加载配置项,会自动识别文件格式
默认执行扁平化选项
"""
config = {}
for file in files:
if os.path.exists(file):
if file.endswith(('.yaml', 'yml')):
config.update(load_from_yaml(file))
elif file.endswith('.json'):
config.update(load_from_json(file))
elif file.endswith('.toml'):
config.update(load_from_toml(file))
elif not no_warning:
logger.warning(f'Unsupported config file format: {file}')
elif not no_warning:
logger.warning(f'Config file not found: {file}')
return config
```
</details>
### ***def*** `load_configs_from_dirs() -> dict[str, Any]`
从目录下加载配置文件,不递归
@ -49,6 +142,29 @@ Load config from toml file
默认执行扁平化选项
<details>
<summary>源代码</summary>
```python
def load_configs_from_dirs(*directories: str, no_waring: bool=False) -> dict[str, Any]:
"""
从目录下加载配置文件,不递归
按照读取文件的优先级反向覆盖
默认执行扁平化选项
"""
config = {}
for directory in directories:
if not os.path.exists(directory):
if not no_waring:
logger.warning(f'Directory not found: {directory}')
continue
for file in os.listdir(directory):
if file.endswith(_SUPPORTED_CONFIG_FORMATS):
config.update(load_from_files(os.path.join(directory, file), no_warning=no_waring))
return config
```
</details>
### ***def*** `load_config_in_default(no_waring: bool) -> dict[str, Any]`
从一个标准的轻雪项目加载配置文件
@ -57,6 +173,22 @@ Load config from toml file
项目目录下的配置文件优先
<details>
<summary>源代码</summary>
```python
def load_config_in_default(no_waring: bool=False) -> dict[str, Any]:
"""
从一个标准的轻雪项目加载配置文件
项目目录下的config.*和config目录下的所有配置文件
项目目录下的配置文件优先
"""
config = load_configs_from_dirs('config', no_waring=no_waring)
config.update(load_from_files('config.yaml', 'config.toml', 'config.json', 'config.yml', no_warning=no_waring))
return config
```
</details>
### ***class*** `SatoriNodeConfig(BaseModel)`

View File

@ -9,10 +9,23 @@ category: API
### &emsp; ***def*** `__init__(self, active: Channel[Any], passive: Channel[Any], channel_deliver_active: Channel[Channel[Any]], channel_deliver_passive: Channel[tuple[str, dict]]) -> None`
### &emsp; ***def*** `__init__(self, active: Channel[Any], passive: Channel[Any], channel_deliver_active: Channel[Channel[Any]], channel_deliver_passive: Channel[tuple[str, dict]], publish: Channel[tuple[str, Any]]) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def __init__(self, active: Channel[Any], passive: Channel[Any], channel_deliver_active: Channel[Channel[Any]], channel_deliver_passive: Channel[tuple[str, dict]], publish: Channel[tuple[str, Any]]):
self.active = active
self.passive = passive
self.channel_deliver_active = channel_deliver_active
self.channel_deliver_passive = channel_deliver_passive
self.publish = publish
```
</details>
### ***class*** `ProcessManager`
进程管理器
@ -21,6 +34,17 @@ category: API
&emsp;
<details>
<summary>源代码</summary>
```python
def __init__(self, lifespan: 'Lifespan'):
self.lifespan = lifespan
self.targets: dict[str, tuple[Callable, tuple, dict]] = {}
self.processes: dict[str, Process] = {}
```
</details>
### &emsp; ***def*** `start(self, name: str) -> None`
&emsp;开启后自动监控进程,并添加到进程字典中
@ -31,10 +55,63 @@ Args:
Returns:
<details>
<summary>源代码</summary>
```python
def start(self, name: str):
"""
开启后自动监控进程,并添加到进程字典中
Args:
name:
Returns:
"""
if name not in self.targets:
raise KeyError(f'Process {name} not found.')
chan_active = get_channel(f'{name}-active')
def _start_process():
process = Process(target=self.targets[name][0], args=self.targets[name][1], kwargs=self.targets[name][2], daemon=True)
self.processes[name] = process
process.start()
_start_process()
while True:
data = chan_active.receive()
if data == 0:
logger.info(f'Stopping process {name}')
self.lifespan.before_process_shutdown()
self.terminate(name)
break
elif data == 1:
logger.info(f'Restarting process {name}')
self.lifespan.before_process_shutdown()
self.lifespan.before_process_restart()
self.terminate(name)
_start_process()
continue
else:
logger.warning('Unknown data received, ignored.')
```
</details>
### &emsp; ***def*** `start_all(self) -> None`
&emsp;启动所有进程
<details>
<summary>源代码</summary>
```python
def start_all(self):
"""
启动所有进程
"""
for name in self.targets:
threading.Thread(target=self.start, args=(name,), daemon=True).start()
```
</details>
### &emsp; ***def*** `add_target(self, name: str, target: TARGET_FUNC, args: tuple, kwargs: Any) -> None`
&emsp;添加进程
@ -49,10 +126,43 @@ Args:
kwargs: 进程函数关键字参数通常会默认传入chan_active和chan_passive
<details>
<summary>源代码</summary>
```python
def add_target(self, name: str, target: TARGET_FUNC, args: tuple=(), kwargs=None):
"""
添加进程
Args:
name: 进程名,用于获取和唯一标识
target: 进程函数
args: 进程函数参数
kwargs: 进程函数关键字参数通常会默认传入chan_active和chan_passive
"""
if kwargs is None:
kwargs = {}
chan_active: Channel = Channel(_id=f'{name}-active')
chan_passive: Channel = Channel(_id=f'{name}-passive')
channel_deliver = ChannelDeliver(active=chan_active, passive=chan_passive, channel_deliver_active=channel_deliver_active_channel, channel_deliver_passive=channel_deliver_passive_channel, publish=publish_channel)
self.targets[name] = (_delivery_channel_wrapper, (target, channel_deliver, shared_memory, *args), kwargs)
set_channels({f'{name}-active': chan_active, f'{name}-passive': chan_passive})
```
</details>
### &emsp; ***def*** `join_all(self) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def join_all(self):
for name, process in self.targets:
process.join()
```
</details>
### &emsp; ***def*** `terminate(self, name: str) -> None`
&emsp;终止进程并从进程字典中删除
@ -65,10 +175,45 @@ Args:
Returns:
<details>
<summary>源代码</summary>
```python
def terminate(self, name: str):
"""
终止进程并从进程字典中删除
Args:
name:
Returns:
"""
if name not in self.processes:
logger.warning(f'Process {name} not found.')
return
process = self.processes[name]
process.terminate()
process.join(TIMEOUT)
if process.is_alive():
process.kill()
logger.success(f'Process {name} terminated.')
```
</details>
### &emsp; ***def*** `terminate_all(self) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def terminate_all(self):
for name in self.targets:
self.terminate(name)
```
</details>
### &emsp; ***def*** `is_process_alive(self, name: str) -> bool`
&emsp;检查进程是否存活
@ -81,6 +226,25 @@ Args:
Returns:
<details>
<summary>源代码</summary>
```python
def is_process_alive(self, name: str) -> bool:
"""
检查进程是否存活
Args:
name:
Returns:
"""
if name not in self.targets:
logger.warning(f'Process {name} not found.')
return self.processes[name].is_alive()
```
</details>
### ***var*** `TIMEOUT = 10`
@ -89,7 +253,7 @@ Returns:
### ***var*** `channel_deliver = ChannelDeliver(active=chan_active, passive=chan_passive, channel_deliver_active=channel_deliver_active_channel, channel_deliver_passive=channel_deliver_passive_channel)`
### ***var*** `channel_deliver = ChannelDeliver(active=chan_active, passive=chan_passive, channel_deliver_active=channel_deliver_active_channel, channel_deliver_passive=channel_deliver_passive_channel, publish=publish_channel)`

View File

@ -9,6 +9,29 @@ category: API
防抖函数
<details>
<summary>源代码</summary>
```python
def debounce(wait):
"""
防抖函数
"""
def decorator(func):
def wrapper(*args, **kwargs):
nonlocal last_call_time
current_time = time.time()
if current_time - last_call_time > wait:
last_call_time = current_time
return func(*args, **kwargs)
last_call_time = None
return wrapper
return decorator
```
</details>
### ***def*** `on_file_system_event(directories: tuple[str], recursive: bool, event_filter: FILTER_FUNC) -> Callable[[CALLBACK_FUNC], CALLBACK_FUNC]`
注册文件系统变化监听器
@ -25,22 +48,111 @@ Returns:
装饰器,装饰一个函数在接收到数据后执行
<details>
<summary>源代码</summary>
```python
def on_file_system_event(directories: tuple[str], recursive: bool=True, event_filter: FILTER_FUNC=None) -> Callable[[CALLBACK_FUNC], CALLBACK_FUNC]:
"""
注册文件系统变化监听器
Args:
directories: 监听目录们
recursive: 是否递归监听子目录
event_filter: 事件过滤器, 返回True则执行回调函数
Returns:
装饰器,装饰一个函数在接收到数据后执行
"""
def decorator(func: CALLBACK_FUNC) -> CALLBACK_FUNC:
def wrapper(event: FileSystemEvent):
if event_filter is not None and (not event_filter(event)):
return
func(event)
code_modified_handler = CodeModifiedHandler()
code_modified_handler.on_modified = wrapper
for directory in directories:
observer.schedule(code_modified_handler, directory, recursive=recursive)
return func
return decorator
```
</details>
### ***def*** `decorator(func: Any) -> None`
<details>
<summary>源代码</summary>
```python
def decorator(func):
def wrapper(*args, **kwargs):
nonlocal last_call_time
current_time = time.time()
if current_time - last_call_time > wait:
last_call_time = current_time
return func(*args, **kwargs)
last_call_time = None
return wrapper
```
</details>
### ***def*** `decorator(func: CALLBACK_FUNC) -> CALLBACK_FUNC`
<details>
<summary>源代码</summary>
```python
def decorator(func: CALLBACK_FUNC) -> CALLBACK_FUNC:
def wrapper(event: FileSystemEvent):
if event_filter is not None and (not event_filter(event)):
return
func(event)
code_modified_handler = CodeModifiedHandler()
code_modified_handler.on_modified = wrapper
for directory in directories:
observer.schedule(code_modified_handler, directory, recursive=recursive)
return func
```
</details>
### ***def*** `wrapper() -> None`
<details>
<summary>源代码</summary>
```python
def wrapper(*args, **kwargs):
nonlocal last_call_time
current_time = time.time()
if current_time - last_call_time > wait:
last_call_time = current_time
return func(*args, **kwargs)
```
</details>
### ***def*** `wrapper(event: FileSystemEvent) -> None`
<details>
<summary>源代码</summary>
```python
def wrapper(event: FileSystemEvent):
if event_filter is not None and (not event_filter(event)):
return
func(event)
```
</details>
### ***class*** `CodeModifiedHandler(FileSystemEventHandler)`
Handler for code file changes
@ -49,22 +161,68 @@ Handler for code file changes
&emsp;
<details>
<summary>源代码</summary>
```python
@debounce(1)
def on_modified(self, event):
raise NotImplementedError('on_modified must be implemented')
```
</details>
### &emsp; ***def*** `on_created(self, event: Any) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def on_created(self, event):
self.on_modified(event)
```
</details>
### &emsp; ***def*** `on_deleted(self, event: Any) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def on_deleted(self, event):
self.on_modified(event)
```
</details>
### &emsp; ***def*** `on_moved(self, event: Any) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def on_moved(self, event):
self.on_modified(event)
```
</details>
### &emsp; ***def*** `on_any_event(self, event: Any) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def on_any_event(self, event):
self.on_modified(event)
```
</details>
### ***var*** `liteyuki_bot = get_bot()`

View File

@ -13,6 +13,25 @@ Args:
module_path: 插件路径,参考`liteyuki.load_plugin`的函数签名
<details>
<summary>源代码</summary>
```python
def run_plugins(*module_path: str | Path):
"""
运行插件无需手动初始化bot
Args:
module_path: 插件路径,参考`liteyuki.load_plugin`的函数签名
"""
cfg = load_config_in_default()
plugins = cfg.get('liteyuki.plugins', [])
plugins.extend(module_path)
cfg['liteyuki.plugins'] = plugins
bot = LiteyukiBot(**cfg)
bot.run()
```
</details>
### ***var*** `cfg = load_config_in_default()`

View File

@ -9,12 +9,49 @@ category: API
<details>
<summary>源代码</summary>
```python
def get_format(level: str) -> str:
if level == 'DEBUG':
return debug_format
else:
return default_format
```
</details>
### ***def*** `init_log(config: dict) -> None`
在语言加载完成后执行
Returns:
<details>
<summary>源代码</summary>
```python
def init_log(config: dict):
"""
在语言加载完成后执行
Returns:
"""
logger.remove()
logger.add(sys.stdout, level=0, diagnose=False, format=get_format(config.get('log_level', 'INFO')))
show_icon = config.get('log_icon', True)
logger.level('DEBUG', color='<blue>', icon=f"{('🐛' if show_icon else '')}DEBUG")
logger.level('INFO', color='<normal>', icon=f"{('' if show_icon else '')}INFO")
logger.level('SUCCESS', color='<green>', icon=f"{('✅' if show_icon else '')}SUCCESS")
logger.level('WARNING', color='<yellow>', icon=f"{('⚠️' if show_icon else '')}WARNING")
logger.level('ERROR', color='<red>', icon=f"{('⭕' if show_icon else '')}ERROR")
```
</details>
### ***var*** `logger = loguru.logger`
### ***var*** `show_icon = config.get('log_icon', True)`

View File

@ -0,0 +1,7 @@
---
title: liteyuki.message
index: true
icon: laptop-code
category: API
---

View File

@ -0,0 +1,106 @@
---
title: liteyuki.message.event
order: 1
icon: laptop-code
category: API
---
### ***class*** `MessageEvent`
### &emsp; ***def*** `__init__(self, bot_id: str, message: list[dict[str, Any]] | str, message_type: str, raw_message: str, session_id: str, session_type: str, receive_channel: str, data: Optional[dict[str, Any]]) -> None`
&emsp;轻雪抽象消息事件
Args:
bot_id: 机器人ID
message: 消息,消息段数组[{type: str, data: dict[str, Any]}]
raw_message: 原始消息(通常为纯文本的格式)
message_type: 消息类型(private, group, other)
session_id: 会话ID(私聊通常为用户ID群聊通常为群ID)
session_type: 会话类型(private, group)
receive_channel: 接收频道(用于回复消息)
data: 附加数据
<details>
<summary>源代码</summary>
```python
def __init__(self, bot_id: str, message: list[dict[str, Any]] | str, message_type: str, raw_message: str, session_id: str, session_type: str, receive_channel: str, data: Optional[dict[str, Any]]=None):
"""
轻雪抽象消息事件
Args:
bot_id: 机器人ID
message: 消息,消息段数组[{type: str, data: dict[str, Any]}]
raw_message: 原始消息(通常为纯文本的格式)
message_type: 消息类型(private, group, other)
session_id: 会话ID(私聊通常为用户ID群聊通常为群ID)
session_type: 会话类型(private, group)
receive_channel: 接收频道(用于回复消息)
data: 附加数据
"""
if data is None:
data = {}
self.message_type = message_type
self.data = data
self.bot_id = bot_id
self.message = message
self.raw_message = raw_message
self.session_id = session_id
self.session_type = session_type
self.receive_channel = receive_channel
```
</details>
### &emsp; ***def*** `reply(self, message: str | dict[str, Any]) -> None`
&emsp;回复消息
Args:
message:
Returns:
<details>
<summary>源代码</summary>
```python
def reply(self, message: str | dict[str, Any]):
"""
回复消息
Args:
message:
Returns:
"""
reply_event = MessageEvent(message_type=self.session_type, message=message, raw_message='', data={'message': message}, bot_id=self.bot_id, session_id=self.session_id, session_type=self.session_type, receive_channel='_')
shared_memory.publish(self.receive_channel, reply_event)
```
</details>
### ***var*** `reply_event = MessageEvent(message_type=self.session_type, message=message, raw_message='', data={'message': message}, bot_id=self.bot_id, session_id=self.session_id, session_type=self.session_type, receive_channel='_')`
### ***var*** `data = {}`

View File

@ -0,0 +1,71 @@
---
title: liteyuki.message.matcher
order: 1
icon: laptop-code
category: API
---
### ***class*** `Matcher`
### &emsp; ***def*** `__init__(self, rule: Rule, priority: int, block: bool) -> None`
&emsp;匹配器
Args:
rule: 规则
priority: 优先级 >= 0
block: 是否阻断后续优先级更低的匹配器
<details>
<summary>源代码</summary>
```python
def __init__(self, rule: Rule, priority: int, block: bool):
"""
匹配器
Args:
rule: 规则
priority: 优先级 >= 0
block: 是否阻断后续优先级更低的匹配器
"""
self.rule = rule
self.priority = priority
self.block = block
self.handlers: list[EventHandler] = []
```
</details>
### &emsp; ***def*** `handle(self, handler: EventHandler) -> EventHandler`
&emsp;添加处理函数,装饰器
Args:
handler:
Returns:
EventHandler
<details>
<summary>源代码</summary>
```python
def handle(self, handler: EventHandler) -> EventHandler:
"""
添加处理函数,装饰器
Args:
handler:
Returns:
EventHandler
"""
self.handlers.append(handler)
return handler
```
</details>

View File

@ -0,0 +1,39 @@
---
title: liteyuki.message.on
order: 1
icon: laptop-code
category: API
---
### ***def*** `on_message(rule: Rule, priority: int, block: bool) -> Matcher`
<details>
<summary>源代码</summary>
```python
def on_message(rule: Rule=Rule(), priority: int=0, block: bool=True) -> Matcher:
matcher = Matcher(rule, priority, block)
for i, m in enumerate(_matcher_list):
if m.priority < matcher.priority:
_matcher_list.insert(i, matcher)
break
else:
_matcher_list.append(matcher)
return matcher
```
</details>
### ***var*** `current_priority = -1`
### ***var*** `matcher = Matcher(rule, priority, block)`
### ***var*** `current_priority = matcher.priority`

View File

@ -0,0 +1,24 @@
---
title: liteyuki.message.rule
order: 1
icon: laptop-code
category: API
---
### ***class*** `Rule`
### &emsp; ***def*** `__init__(self, handler: Optional[RuleHandler]) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def __init__(self, handler: Optional[RuleHandler]=None):
self.handler = handler
```
</details>

View File

@ -0,0 +1,7 @@
---
title: liteyuki.message.session
order: 1
icon: laptop-code
category: API
---

View File

@ -15,6 +15,21 @@ Args:
target_path: 目标路径
<details>
<summary>源代码</summary>
```python
def get_relative_path(base_path: str, target_path: str) -> str:
"""
获取相对路径
Args:
base_path: 基础路径
target_path: 目标路径
"""
return os.path.relpath(target_path, base_path)
```
</details>
### ***def*** `write_to_files(file_data: dict[str, str]) -> None`
输出文件
@ -23,10 +38,42 @@ Args:
file_data: 文件数据 相对路径
<details>
<summary>源代码</summary>
```python
def write_to_files(file_data: dict[str, str]):
"""
输出文件
Args:
file_data: 文件数据 相对路径
"""
for rp, data in file_data.items():
if not os.path.exists(os.path.dirname(rp)):
os.makedirs(os.path.dirname(rp))
with open(rp, 'w', encoding='utf-8') as f:
f.write(data)
```
</details>
### ***def*** `get_file_list(module_folder: str) -> None`
<details>
<summary>源代码</summary>
```python
def get_file_list(module_folder: str):
file_list = []
for root, dirs, files in os.walk(module_folder):
for file in files:
if file.endswith(('.py', '.pyi')):
file_list.append(os.path.join(root, file))
return file_list
```
</details>
### ***def*** `get_module_info_normal(file_path: str, ignore_private: bool) -> ModuleInfo`
获取函数和类
@ -41,6 +88,67 @@ Returns:
模块信息
<details>
<summary>源代码</summary>
```python
def get_module_info_normal(file_path: str, ignore_private: bool=True) -> ModuleInfo:
"""
获取函数和类
Args:
file_path: Python 文件路径
ignore_private: 忽略私有函数和类
Returns:
模块信息
"""
with open(file_path, 'r', encoding='utf-8') as file:
file_content = file.read()
tree = ast.parse(file_content)
dot_sep_module_path = file_path.replace(os.sep, '.').replace('.py', '').replace('.pyi', '')
module_docstring = ast.get_docstring(tree)
module_info = ModuleInfo(module_path=dot_sep_module_path, functions=[], classes=[], attributes=[], docstring=module_docstring if module_docstring else '')
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
if not any((isinstance(parent, ast.ClassDef) for parent in ast.iter_child_nodes(node))) and (not ignore_private or not node.name.startswith('_')):
if node.args.args:
first_arg = node.args.args[0]
if first_arg.arg in ('self', 'cls'):
continue
function_docstring = ast.get_docstring(node)
func_info = FunctionInfo(name=node.name, args=[(arg.arg, ast.unparse(arg.annotation) if arg.annotation else NO_TYPE_ANY) for arg in node.args.args], return_type=ast.unparse(node.returns) if node.returns else 'None', docstring=function_docstring if function_docstring else '', type=DefType.FUNCTION, is_async=isinstance(node, ast.AsyncFunctionDef), source_code=ast.unparse(node))
module_info.functions.append(func_info)
elif isinstance(node, ast.ClassDef):
class_docstring = ast.get_docstring(node)
class_info = ClassInfo(name=node.name, docstring=class_docstring if class_docstring else '', methods=[], attributes=[], inherit=[ast.unparse(base) for base in node.bases])
for class_node in node.body:
if isinstance(class_node, ast.FunctionDef) and (not ignore_private or not class_node.name.startswith('_') or class_node.name == '__init__'):
method_docstring = ast.get_docstring(class_node)
def_type = DefType.METHOD
if class_node.decorator_list:
if any((isinstance(decorator, ast.Name) and decorator.id == 'staticmethod' for decorator in class_node.decorator_list)):
def_type = DefType.STATIC_METHOD
elif any((isinstance(decorator, ast.Name) and decorator.id == 'classmethod' for decorator in class_node.decorator_list)):
def_type = DefType.CLASS_METHOD
elif any((isinstance(decorator, ast.Name) and decorator.id == 'property' for decorator in class_node.decorator_list)):
def_type = DefType.PROPERTY
class_info.methods.append(FunctionInfo(name=class_node.name, args=[(arg.arg, ast.unparse(arg.annotation) if arg.annotation else NO_TYPE_ANY) for arg in class_node.args.args], return_type=ast.unparse(class_node.returns) if class_node.returns else 'None', docstring=method_docstring if method_docstring else '', type=def_type, is_async=isinstance(class_node, ast.AsyncFunctionDef), source_code=ast.unparse(class_node)))
elif isinstance(class_node, ast.Assign):
for target in class_node.targets:
if isinstance(target, ast.Name):
class_info.attributes.append(AttributeInfo(name=target.id, type=ast.unparse(class_node.value)))
module_info.classes.append(class_info)
elif isinstance(node, ast.Assign):
if not any((isinstance(parent, (ast.ClassDef, ast.FunctionDef)) for parent in ast.iter_child_nodes(node))):
for target in node.targets:
if isinstance(target, ast.Name) and (not ignore_private or not target.id.startswith('_')):
attr_type = NO_TYPE_HINT
if isinstance(node.value, ast.AnnAssign) and node.value.annotation:
attr_type = ast.unparse(node.value.annotation)
module_info.attributes.append(AttributeInfo(name=target.id, type=attr_type, value=ast.unparse(node.value) if node.value else None))
return module_info
```
</details>
### ***def*** `generate_markdown(module_info: ModuleInfo, front_matter: Any) -> str`
生成模块的Markdown
@ -57,6 +165,60 @@ Returns:
Markdown 字符串
<details>
<summary>源代码</summary>
```python
def generate_markdown(module_info: ModuleInfo, front_matter=None) -> str:
"""
生成模块的Markdown
你可在此自定义生成的Markdown格式
Args:
module_info: 模块信息
front_matter: 自定义选项title, index, icon, category
Returns:
Markdown 字符串
"""
content = ''
front_matter = '---\n' + '\n'.join([f'{k}: {v}' for k, v in front_matter.items()]) + '\n---\n\n'
content += front_matter
for func in module_info.functions:
args_with_type = [f'{arg[0]}: {arg[1]}' if arg[1] else arg[0] for arg in func.args]
content += f"### ***{('async ' if func.is_async else '')}def*** `{func.name}({', '.join(args_with_type)}) -> {func.return_type}`\n\n"
func.docstring = func.docstring.replace('\n', '\n\n')
content += f'{func.docstring}\n\n'
content += f'<details>\n<summary>源代码</summary>\n\n```python\n{func.source_code}\n```\n</details>\n\n'
for cls in module_info.classes:
if cls.inherit:
inherit = f"({', '.join(cls.inherit)})" if cls.inherit else ''
content += f'### ***class*** `{cls.name}{inherit}`\n\n'
else:
content += f'### ***class*** `{cls.name}`\n\n'
cls.docstring = cls.docstring.replace('\n', '\n\n')
content += f'{cls.docstring}\n\n'
for method in cls.methods:
if method.type != DefType.METHOD:
args_with_type = [f'{arg[0]}: {arg[1]}' if arg[1] else arg[0] for arg in method.args]
content += f'### &emsp; ***@{method.type.value}***\n'
else:
args_with_type = [f'{arg[0]}: {arg[1]}' if arg[1] and arg[0] != 'self' else arg[0] for arg in method.args]
content += f"### &emsp; ***{('async ' if method.is_async else '')}def*** `{method.name}({', '.join(args_with_type)}) -> {method.return_type}`\n\n"
method.docstring = method.docstring.replace('\n', '\n\n')
content += f'&emsp;{method.docstring}\n\n'
content += f'<details>\n<summary>源代码</summary>\n\n```python\n{method.source_code}\n```\n</details>\n\n'
for attr in cls.attributes:
content += f'### &emsp; ***attr*** `{attr.name}: {attr.type}`\n\n'
for attr in module_info.attributes:
if attr.type == NO_TYPE_HINT:
content += f'### ***var*** `{attr.name} = {attr.value}`\n\n'
else:
content += f'### ***var*** `{attr.name}: {attr.type} = {attr.value}`\n\n'
attr.docstring = attr.docstring.replace('\n', '\n\n')
content += f'{attr.docstring}\n\n'
return content
```
</details>
### ***def*** `generate_docs(module_folder: str, output_dir: str, with_top: bool, ignored_paths: Any) -> None`
生成文档
@ -71,6 +233,46 @@ Args:
ignored_paths: 忽略的路径
<details>
<summary>源代码</summary>
```python
def generate_docs(module_folder: str, output_dir: str, with_top: bool=False, ignored_paths=None):
"""
生成文档
Args:
module_folder: 模块文件夹
output_dir: 输出文件夹
with_top: 是否包含顶层文件夹 False时例如docs/api/module_a, docs/api/module_b True时例如docs/api/module/module_a.md docs/api/module/module_b.md
ignored_paths: 忽略的路径
"""
if ignored_paths is None:
ignored_paths = []
file_data: dict[str, str] = {}
file_list = get_file_list(module_folder)
shutil.rmtree(output_dir, ignore_errors=True)
os.mkdir(output_dir)
replace_data = {'__init__': 'README', '.py': '.md'}
for pyfile_path in file_list:
if any((ignored_path.replace('\\', '/') in pyfile_path.replace('\\', '/') for ignored_path in ignored_paths)):
continue
no_module_name_pyfile_path = get_relative_path(module_folder, pyfile_path)
rel_md_path = pyfile_path if with_top else no_module_name_pyfile_path
for rk, rv in replace_data.items():
rel_md_path = rel_md_path.replace(rk, rv)
abs_md_path = os.path.join(output_dir, rel_md_path)
module_info = get_module_info_normal(pyfile_path)
if 'README' in abs_md_path:
front_matter = {'title': module_info.module_path.replace('.__init__', '').replace('_', '\\n'), 'index': 'true', 'icon': 'laptop-code', 'category': 'API'}
else:
front_matter = {'title': module_info.module_path.replace('_', '\\n'), 'order': '1', 'icon': 'laptop-code', 'category': 'API'}
md_content = generate_markdown(module_info, front_matter)
print(f'Generate {pyfile_path} -> {abs_md_path}')
file_data[abs_md_path] = md_content
write_to_files(file_data)
```
</details>
### ***class*** `DefType(Enum)`
@ -217,7 +419,7 @@ Args:
### ***var*** `func_info = FunctionInfo(name=node.name, args=[(arg.arg, ast.unparse(arg.annotation) if arg.annotation else NO_TYPE_ANY) for arg in node.args.args], return_type=ast.unparse(node.returns) if node.returns else 'None', docstring=function_docstring if function_docstring else '', type=DefType.FUNCTION, is_async=isinstance(node, ast.AsyncFunctionDef))`
### ***var*** `func_info = FunctionInfo(name=node.name, args=[(arg.arg, ast.unparse(arg.annotation) if arg.annotation else NO_TYPE_ANY) for arg in node.args.args], return_type=ast.unparse(node.returns) if node.returns else 'None', docstring=function_docstring if function_docstring else '', type=DefType.FUNCTION, is_async=isinstance(node, ast.AsyncFunctionDef), source_code=ast.unparse(node))`

View File

@ -13,3 +13,17 @@ Returns:
dict[str, Plugin]: 插件字典
<details>
<summary>源代码</summary>
```python
def get_loaded_plugins() -> dict[str, Plugin]:
"""
获取已加载的插件
Returns:
dict[str, Plugin]: 插件字典
"""
return _plugins
```
</details>

View File

@ -17,6 +17,34 @@ category: API
或插件路径 `pathlib.Path(path/to/your/plugin)`
<details>
<summary>源代码</summary>
```python
def load_plugin(module_path: str | Path) -> Optional[Plugin]:
"""加载单个插件,可以是本地插件或是通过 `pip` 安装的插件。
参数:
module_path: 插件名称 `path.to.your.plugin`
或插件路径 `pathlib.Path(path/to/your/plugin)`
"""
module_path = path_to_module_name(Path(module_path)) if isinstance(module_path, Path) else module_path
try:
module = import_module(module_path)
_plugins[module.__name__] = Plugin(name=module.__name__, module=module, module_name=module_path, metadata=module.__dict__.get('__plugin_metadata__', None))
display_name = module.__name__.split('.')[-1]
if module.__dict__.get('__plugin_meta__'):
metadata: 'PluginMetadata' = module.__dict__['__plugin_meta__']
display_name = format_display_name(f"{metadata.name}({module.__name__.split('.')[-1]})", metadata.type)
logger.opt(colors=True).success(f'Succeeded to load liteyuki plugin "{display_name}"')
return _plugins[module.__name__]
except Exception as e:
logger.opt(colors=True).success(f'Failed to load liteyuki plugin "<r>{module_path}</r>"')
traceback.print_exc()
return None
```
</details>
### ***def*** `load_plugins() -> set[Plugin]`
导入文件夹下多个插件
@ -29,6 +57,46 @@ category: API
ignore_warning: 是否忽略警告,通常是目录不存在或目录为空
<details>
<summary>源代码</summary>
```python
def load_plugins(*plugin_dir: str, ignore_warning: bool=True) -> set[Plugin]:
"""导入文件夹下多个插件
参数:
plugin_dir: 文件夹路径
ignore_warning: 是否忽略警告,通常是目录不存在或目录为空
"""
plugins = set()
for dir_path in plugin_dir:
if not os.path.exists(dir_path):
if not ignore_warning:
logger.warning(f"Plugins dir '{dir_path}' does not exist.")
continue
if not os.listdir(dir_path):
if not ignore_warning:
logger.warning(f"Plugins dir '{dir_path}' is empty.")
continue
if not os.path.isdir(dir_path):
if not ignore_warning:
logger.warning(f"Plugins dir '{dir_path}' is not a directory.")
continue
for f in os.listdir(dir_path):
path = Path(os.path.join(dir_path, f))
module_name = None
if os.path.isfile(path) and f.endswith('.py') and (f != '__init__.py'):
module_name = f'{path_to_module_name(Path(dir_path))}.{f[:-3]}'
elif os.path.isdir(path) and os.path.exists(os.path.join(path, '__init__.py')):
module_name = path_to_module_name(path)
if module_name:
load_plugin(module_name)
if _plugins.get(module_name):
plugins.add(_plugins[module_name])
return plugins
```
</details>
### ***def*** `format_display_name(display_name: str, plugin_type: PluginType) -> str`
设置插件名称颜色,根据不同类型插件设置颜色
@ -45,6 +113,34 @@ Returns:
str: 设置后的插件名称 <y>name</y>
<details>
<summary>源代码</summary>
```python
def format_display_name(display_name: str, plugin_type: PluginType) -> str:
"""
设置插件名称颜色,根据不同类型插件设置颜色
Args:
display_name: 插件名称
plugin_type: 插件类型
Returns:
str: 设置后的插件名称 <y>name</y>
"""
color = 'y'
match plugin_type:
case PluginType.APPLICATION:
color = 'm'
case PluginType.TEST:
color = 'g'
case PluginType.MODULE:
color = 'e'
case PluginType.SERVICE:
color = 'c'
return f'<{color}>{display_name} [{plugin_type.name}]</{color}>'
```
</details>
### ***var*** `module_path = path_to_module_name(Path(module_path)) if isinstance(module_path, Path) else module_path`

View File

@ -13,12 +13,12 @@ category: API
### &emsp; ***attr*** `SERVICE: 'service'`
### &emsp; ***attr*** `IMPLEMENTATION: 'implementation'`
### &emsp; ***attr*** `MODULE: 'module'`
### &emsp; ***attr*** `UNCLASSIFIED: 'unclassified'`
### &emsp; ***attr*** `TEST: 'test'`
### ***class*** `PluginMetadata(BaseModel)`
轻雪插件元数据由插件编写者提供name为必填项
@ -71,10 +71,6 @@ extra: dict[str, Any]
### ***var*** `IMPLEMENTATION = 'implementation'`
### ***var*** `MODULE = 'module'`
@ -83,6 +79,10 @@ extra: dict[str, Any]
### ***var*** `TEST = 'test'`
### ***var*** `model_config = {'arbitrary_types_allowed': True}`

View File

@ -17,6 +17,27 @@ Returns:
bool: 是否为协程可调用对象
<details>
<summary>源代码</summary>
```python
def is_coroutine_callable(call: Callable[..., Any]) -> bool:
"""
判断是否为协程可调用对象
Args:
call: 可调用对象
Returns:
bool: 是否为协程可调用对象
"""
if inspect.isroutine(call):
return inspect.iscoroutinefunction(call)
if inspect.isclass(call):
return False
func_ = getattr(call, '__call__', None)
return inspect.iscoroutinefunction(func_)
```
</details>
### ***def*** `run_coroutine() -> None`
运行协程
@ -29,6 +50,37 @@ Args:
Returns:
<details>
<summary>源代码</summary>
```python
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:
logger.error(f'Exception occurred: {e}')
```
</details>
### ***def*** `path_to_module_name(path: Path) -> str`
转换路径为模块名
@ -41,6 +93,26 @@ Returns:
str: 模块名
<details>
<summary>源代码</summary>
```python
def path_to_module_name(path: Path) -> str:
"""
转换路径为模块名
Args:
path: 路径a/b/c/d -> a.b.c.d
Returns:
str: 模块名
"""
rel_path = path.resolve().relative_to(Path.cwd().resolve())
if rel_path.stem == '__init__':
return '.'.join(rel_path.parts[:-1])
else:
return '.'.join(rel_path.parts[:-1] + (rel_path.stem,))
```
</details>
### ***def*** `async_wrapper(func: Callable[..., Any]) -> Callable[..., Coroutine]`
异步包装器
@ -53,10 +125,39 @@ Returns:
Coroutine: Asynchronous Callable
<details>
<summary>源代码</summary>
```python
def async_wrapper(func: Callable[..., Any]) -> Callable[..., Coroutine]:
"""
异步包装器
Args:
func: Sync Callable
Returns:
Coroutine: Asynchronous Callable
"""
async def wrapper(*args, **kwargs):
return func(*args, **kwargs)
wrapper.__signature__ = inspect.signature(func)
return wrapper
```
</details>
### ***async def*** `wrapper() -> None`
<details>
<summary>源代码</summary>
```python
async def wrapper(*args, **kwargs):
return func(*args, **kwargs)
```
</details>
### ***var*** `IS_MAIN_PROCESS = multiprocessing.current_process().name == 'MainProcess'`

View File

@ -0,0 +1,7 @@
---
title: liteyuki
index: true
icon: laptop-code
category: API
---

View File

@ -0,0 +1,581 @@
---
title: liteyuki.bot
index: true
icon: laptop-code
category: API
---
### ***def*** `get_bot() -> LiteyukiBot`
获取轻雪实例
Returns:
LiteyukiBot: 当前的轻雪实例
<details>
<summary>源代码</summary>
```python
def get_bot() -> LiteyukiBot:
"""
获取轻雪实例
Returns:
LiteyukiBot: 当前的轻雪实例
"""
if IS_MAIN_PROCESS:
if _BOT_INSTANCE is None:
raise RuntimeError('Liteyuki instance not initialized.')
return _BOT_INSTANCE
else:
raise RuntimeError("Can't get bot instance in sub process.")
```
</details>
### ***def*** `get_config(key: str, default: Any) -> Any`
获取配置
Args:
key: 配置键
default: 默认值
Returns:
Any: 配置值
<details>
<summary>源代码</summary>
```python
def get_config(key: str, default: Any=None) -> Any:
"""
获取配置
Args:
key: 配置键
default: 默认值
Returns:
Any: 配置值
"""
return get_bot().config.get(key, default)
```
</details>
### ***def*** `get_config_with_compat(key: str, compat_keys: tuple[str], default: Any) -> Any`
获取配置,兼容旧版本
Args:
key: 配置键
compat_keys: 兼容键
default: 默认值
Returns:
Any: 配置值
<details>
<summary>源代码</summary>
```python
def get_config_with_compat(key: str, compat_keys: tuple[str], default: Any=None) -> Any:
"""
获取配置,兼容旧版本
Args:
key: 配置键
compat_keys: 兼容键
default: 默认值
Returns:
Any: 配置值
"""
if key in get_bot().config:
return get_bot().config[key]
for compat_key in compat_keys:
if compat_key in get_bot().config:
logger.warning(f'Config key "{compat_key}" will be deprecated, use "{key}" instead.')
return get_bot().config[compat_key]
return default
```
</details>
### ***def*** `print_logo() -> None`
<details>
<summary>源代码</summary>
```python
def print_logo():
print('\x1b[34m' + '\n __ ______ ________ ________ __ __ __ __ __ __ ______ \n / | / |/ |/ |/ \\ / |/ | / |/ | / |/ |\n $$ | $$$$$$/ $$$$$$$$/ $$$$$$$$/ $$ \\ /$$/ $$ | $$ |$$ | /$$/ $$$$$$/ \n $$ | $$ | $$ | $$ |__ $$ \\/$$/ $$ | $$ |$$ |/$$/ $$ | \n $$ | $$ | $$ | $$ | $$ $$/ $$ | $$ |$$ $$< $$ | \n $$ | $$ | $$ | $$$$$/ $$$$/ $$ | $$ |$$$$$ \\ $$ | \n $$ |_____ _$$ |_ $$ | $$ |_____ $$ | $$ \\__$$ |$$ |$$ \\ _$$ |_ \n $$ |/ $$ | $$ | $$ | $$ | $$ $$/ $$ | $$ |/ $$ |\n $$$$$$$$/ $$$$$$/ $$/ $$$$$$$$/ $$/ $$$$$$/ $$/ $$/ $$$$$$/ \n ' + '\x1b[0m')
```
</details>
### ***class*** `LiteyukiBot`
### &emsp; ***def*** `__init__(self) -> None`
&emsp;初始化轻雪实例
Args:
*args:
**kwargs: 配置
<details>
<summary>源代码</summary>
```python
def __init__(self, *args, **kwargs) -> None:
"""
初始化轻雪实例
Args:
*args:
**kwargs: 配置
"""
'常规操作'
print_logo()
global _BOT_INSTANCE
_BOT_INSTANCE = self
'配置'
self.config: dict[str, Any] = kwargs
'初始化'
self.init(**self.config)
logger.info('Liteyuki is initializing...')
'生命周期管理'
self.lifespan = Lifespan()
self.process_manager: ProcessManager = ProcessManager(lifespan=self.lifespan)
'事件循环'
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
self.stop_event = threading.Event()
self.call_restart_count = 0
'加载插件加载器'
load_plugin('liteyuki.plugins.plugin_loader')
'信号处理'
signal.signal(signal.SIGINT, self._handle_exit)
signal.signal(signal.SIGTERM, self._handle_exit)
atexit.register(self.process_manager.terminate_all)
```
</details>
### &emsp; ***def*** `run(self) -> None`
&emsp;启动逻辑
<details>
<summary>源代码</summary>
```python
def run(self):
"""
启动逻辑
"""
self.lifespan.before_start()
self.process_manager.start_all()
self.lifespan.after_start()
self.keep_alive()
```
</details>
### &emsp; ***def*** `keep_alive(self) -> None`
&emsp;保持轻雪运行
Returns:
<details>
<summary>源代码</summary>
```python
def keep_alive(self):
"""
保持轻雪运行
Returns:
"""
try:
while not self.stop_event.is_set():
time.sleep(0.5)
except KeyboardInterrupt:
logger.info('Liteyuki is stopping...')
self.stop()
```
</details>
### &emsp; ***def*** `restart(self, delay: int) -> None`
&emsp;重启轻雪本体
Returns:
<details>
<summary>源代码</summary>
```python
def restart(self, delay: int=0):
"""
重启轻雪本体
Returns:
"""
if self.call_restart_count < 1:
executable = sys.executable
args = sys.argv
logger.info('Restarting LiteyukiBot...')
time.sleep(delay)
if platform.system() == 'Windows':
cmd = 'start'
elif platform.system() == 'Linux':
cmd = 'nohup'
elif platform.system() == 'Darwin':
cmd = 'open'
else:
cmd = 'nohup'
self.process_manager.terminate_all()
threading.Thread(target=os.system, args=(f"{cmd} {executable} {' '.join(args)}",)).start()
sys.exit(0)
self.call_restart_count += 1
```
</details>
### &emsp; ***def*** `restart_process(self, name: Optional[str]) -> None`
&emsp;停止轻雪
Args:
name: 进程名称, 默认为None, 所有进程
Returns:
<details>
<summary>源代码</summary>
```python
def restart_process(self, name: Optional[str]=None):
"""
停止轻雪
Args:
name: 进程名称, 默认为None, 所有进程
Returns:
"""
self.lifespan.before_process_shutdown()
self.lifespan.before_process_shutdown()
if name is not None:
chan_active = get_channel(f'{name}-active')
chan_active.send(1)
else:
for process_name in self.process_manager.processes:
chan_active = get_channel(f'{process_name}-active')
chan_active.send(1)
```
</details>
### &emsp; ***def*** `init(self) -> None`
&emsp;初始化轻雪, 自动调用
Returns:
<details>
<summary>源代码</summary>
```python
def init(self, *args, **kwargs):
"""
初始化轻雪, 自动调用
Returns:
"""
self.init_logger()
```
</details>
### &emsp; ***def*** `init_logger(self) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def init_logger(self):
init_log(config=self.config)
```
</details>
### &emsp; ***def*** `stop(self) -> None`
&emsp;停止轻雪
Returns:
<details>
<summary>源代码</summary>
```python
def stop(self):
"""
停止轻雪
Returns:
"""
self.stop_event.set()
self.loop.stop()
```
</details>
### &emsp; ***def*** `on_before_start(self, func: LIFESPAN_FUNC) -> None`
&emsp;注册启动前的函数
Args:
func:
Returns:
<details>
<summary>源代码</summary>
```python
def on_before_start(self, func: LIFESPAN_FUNC):
"""
注册启动前的函数
Args:
func:
Returns:
"""
return self.lifespan.on_before_start(func)
```
</details>
### &emsp; ***def*** `on_after_start(self, func: LIFESPAN_FUNC) -> None`
&emsp;注册启动后的函数
Args:
func:
Returns:
<details>
<summary>源代码</summary>
```python
def on_after_start(self, func: LIFESPAN_FUNC):
"""
注册启动后的函数
Args:
func:
Returns:
"""
return self.lifespan.on_after_start(func)
```
</details>
### &emsp; ***def*** `on_after_shutdown(self, func: LIFESPAN_FUNC) -> None`
&emsp;注册停止后的函数:未实现
Args:
func:
Returns:
<details>
<summary>源代码</summary>
```python
def on_after_shutdown(self, func: LIFESPAN_FUNC):
"""
注册停止后的函数:未实现
Args:
func:
Returns:
"""
return self.lifespan.on_after_shutdown(func)
```
</details>
### &emsp; ***def*** `on_before_process_shutdown(self, func: LIFESPAN_FUNC) -> None`
&emsp;注册进程停止前的函数,为子进程停止时调用
Args:
func:
Returns:
<details>
<summary>源代码</summary>
```python
def on_before_process_shutdown(self, func: LIFESPAN_FUNC):
"""
注册进程停止前的函数,为子进程停止时调用
Args:
func:
Returns:
"""
return self.lifespan.on_before_process_shutdown(func)
```
</details>
### &emsp; ***def*** `on_before_process_restart(self, func: LIFESPAN_FUNC) -> None`
&emsp;注册进程重启前的函数,为子进程重启时调用
Args:
func:
Returns:
<details>
<summary>源代码</summary>
```python
def on_before_process_restart(self, func: LIFESPAN_FUNC):
"""
注册进程重启前的函数,为子进程重启时调用
Args:
func:
Returns:
"""
return self.lifespan.on_before_process_restart(func)
```
</details>
### &emsp; ***def*** `on_after_restart(self, func: LIFESPAN_FUNC) -> None`
&emsp;注册重启后的函数:未实现
Args:
func:
Returns:
<details>
<summary>源代码</summary>
```python
def on_after_restart(self, func: LIFESPAN_FUNC):
"""
注册重启后的函数:未实现
Args:
func:
Returns:
"""
return self.lifespan.on_after_restart(func)
```
</details>
### &emsp; ***def*** `on_after_nonebot_init(self, func: LIFESPAN_FUNC) -> None`
&emsp;注册nonebot初始化后的函数
Args:
func:
Returns:
<details>
<summary>源代码</summary>
```python
def on_after_nonebot_init(self, func: LIFESPAN_FUNC):
"""
注册nonebot初始化后的函数
Args:
func:
Returns:
"""
return self.lifespan.on_after_nonebot_init(func)
```
</details>
### ***var*** `executable = sys.executable`
### ***var*** `args = sys.argv`
### ***var*** `chan_active = get_channel(f'{name}-active')`
### ***var*** `cmd = 'start'`
### ***var*** `chan_active = get_channel(f'{process_name}-active')`
### ***var*** `cmd = 'nohup'`
### ***var*** `cmd = 'open'`
### ***var*** `cmd = 'nohup'`

View File

@ -0,0 +1,450 @@
---
title: liteyuki.bot.lifespan
order: 1
icon: laptop-code
category: API
---
### ***def*** `run_funcs(funcs: list[LIFESPAN_FUNC | PROCESS_LIFESPAN_FUNC]) -> None`
运行函数
Args:
funcs:
Returns:
<details>
<summary>源代码</summary>
```python
@staticmethod
def run_funcs(funcs: list[LIFESPAN_FUNC | PROCESS_LIFESPAN_FUNC], *args, **kwargs) -> None:
"""
运行函数
Args:
funcs:
Returns:
"""
try:
loop = asyncio.get_event_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
tasks = []
for func in funcs:
if is_coroutine_callable(func):
tasks.append(func(*args, **kwargs))
else:
tasks.append(async_wrapper(func)(*args, **kwargs))
loop.run_until_complete(asyncio.gather(*tasks))
```
</details>
### ***class*** `Lifespan`
### &emsp; ***def*** `__init__(self) -> None`
&emsp;轻雪生命周期管理,启动、停止、重启
<details>
<summary>源代码</summary>
```python
def __init__(self) -> None:
"""
轻雪生命周期管理,启动、停止、重启
"""
self.life_flag: int = 0
self._before_start_funcs: list[LIFESPAN_FUNC] = []
self._after_start_funcs: list[LIFESPAN_FUNC] = []
self._before_process_shutdown_funcs: list[LIFESPAN_FUNC] = []
self._after_shutdown_funcs: list[LIFESPAN_FUNC] = []
self._before_process_restart_funcs: list[LIFESPAN_FUNC] = []
self._after_restart_funcs: list[LIFESPAN_FUNC] = []
self._after_nonebot_init_funcs: list[LIFESPAN_FUNC] = []
```
</details>
### &emsp; ***@staticmethod***
### &emsp; ***def*** `run_funcs(funcs: list[LIFESPAN_FUNC | PROCESS_LIFESPAN_FUNC]) -> None`
&emsp;运行函数
Args:
funcs:
Returns:
<details>
<summary>源代码</summary>
```python
@staticmethod
def run_funcs(funcs: list[LIFESPAN_FUNC | PROCESS_LIFESPAN_FUNC], *args, **kwargs) -> None:
"""
运行函数
Args:
funcs:
Returns:
"""
try:
loop = asyncio.get_event_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
tasks = []
for func in funcs:
if is_coroutine_callable(func):
tasks.append(func(*args, **kwargs))
else:
tasks.append(async_wrapper(func)(*args, **kwargs))
loop.run_until_complete(asyncio.gather(*tasks))
```
</details>
### &emsp; ***def*** `on_before_start(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`
&emsp;注册启动时的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
<details>
<summary>源代码</summary>
```python
def on_before_start(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
"""
注册启动时的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
"""
self._before_start_funcs.append(func)
return func
```
</details>
### &emsp; ***def*** `on_after_start(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`
&emsp;注册启动时的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
<details>
<summary>源代码</summary>
```python
def on_after_start(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
"""
注册启动时的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
"""
self._after_start_funcs.append(func)
return func
```
</details>
### &emsp; ***def*** `on_before_process_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`
&emsp;注册停止前的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
<details>
<summary>源代码</summary>
```python
def on_before_process_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
"""
注册停止前的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
"""
self._before_process_shutdown_funcs.append(func)
return func
```
</details>
### &emsp; ***def*** `on_after_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`
&emsp;注册停止后的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
<details>
<summary>源代码</summary>
```python
def on_after_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
"""
注册停止后的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
"""
self._after_shutdown_funcs.append(func)
return func
```
</details>
### &emsp; ***def*** `on_before_process_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`
&emsp;注册重启时的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
<details>
<summary>源代码</summary>
```python
def on_before_process_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
"""
注册重启时的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
"""
self._before_process_restart_funcs.append(func)
return func
```
</details>
### &emsp; ***def*** `on_after_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`
&emsp;注册重启后的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
<details>
<summary>源代码</summary>
```python
def on_after_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
"""
注册重启后的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
"""
self._after_restart_funcs.append(func)
return func
```
</details>
### &emsp; ***def*** `on_after_nonebot_init(self, func: Any) -> None`
&emsp;注册 NoneBot 初始化后的函数
Args:
func:
Returns:
<details>
<summary>源代码</summary>
```python
def on_after_nonebot_init(self, func):
"""
注册 NoneBot 初始化后的函数
Args:
func:
Returns:
"""
self._after_nonebot_init_funcs.append(func)
return func
```
</details>
### &emsp; ***def*** `before_start(self) -> None`
&emsp;启动前
Returns:
<details>
<summary>源代码</summary>
```python
def before_start(self) -> None:
"""
启动前
Returns:
"""
logger.debug('Running before_start functions')
self.run_funcs(self._before_start_funcs)
```
</details>
### &emsp; ***def*** `after_start(self) -> None`
&emsp;启动后
Returns:
<details>
<summary>源代码</summary>
```python
def after_start(self) -> None:
"""
启动后
Returns:
"""
logger.debug('Running after_start functions')
self.run_funcs(self._after_start_funcs)
```
</details>
### &emsp; ***def*** `before_process_shutdown(self) -> None`
&emsp;停止前
Returns:
<details>
<summary>源代码</summary>
```python
def before_process_shutdown(self) -> None:
"""
停止前
Returns:
"""
logger.debug('Running before_shutdown functions')
self.run_funcs(self._before_process_shutdown_funcs)
```
</details>
### &emsp; ***def*** `after_shutdown(self) -> None`
&emsp;停止后
Returns:
<details>
<summary>源代码</summary>
```python
def after_shutdown(self) -> None:
"""
停止后
Returns:
"""
logger.debug('Running after_shutdown functions')
self.run_funcs(self._after_shutdown_funcs)
```
</details>
### &emsp; ***def*** `before_process_restart(self) -> None`
&emsp;重启前
Returns:
<details>
<summary>源代码</summary>
```python
def before_process_restart(self) -> None:
"""
重启前
Returns:
"""
logger.debug('Running before_restart functions')
self.run_funcs(self._before_process_restart_funcs)
```
</details>
### &emsp; ***def*** `after_restart(self) -> None`
&emsp;重启后
Returns:
<details>
<summary>源代码</summary>
```python
def after_restart(self) -> None:
"""
重启后
Returns:
"""
logger.debug('Running after_restart functions')
self.run_funcs(self._after_restart_funcs)
```
</details>
### ***var*** `tasks = []`
### ***var*** `loop = asyncio.get_event_loop()`
### ***var*** `loop = asyncio.new_event_loop()`

View File

@ -0,0 +1,7 @@
---
title: liteyuki.comm
index: true
icon: laptop-code
category: API
---

View File

@ -0,0 +1,427 @@
---
title: liteyuki.comm.channel
order: 1
icon: laptop-code
category: API
---
### ***def*** `set_channel(name: str, channel: Channel) -> None`
设置通道实例
Args:
name: 通道名称
channel: 通道实例
<details>
<summary>源代码</summary>
```python
def set_channel(name: str, channel: Channel):
"""
设置通道实例
Args:
name: 通道名称
channel: 通道实例
"""
if not isinstance(channel, Channel):
raise TypeError(f'channel_ must be an instance of Channel, {type(channel)} found')
if IS_MAIN_PROCESS:
_channel[name] = channel
else:
channel_deliver_passive_channel.send(('set_channel', {'name': name, 'channel_': channel}))
```
</details>
### ***def*** `set_channels(channels: dict[str, Channel]) -> None`
设置通道实例
Args:
channels: 通道名称
<details>
<summary>源代码</summary>
```python
def set_channels(channels: dict[str, Channel]):
"""
设置通道实例
Args:
channels: 通道名称
"""
for name, channel in channels.items():
set_channel(name, channel)
```
</details>
### ***def*** `get_channel(name: str) -> Channel`
获取通道实例
Args:
name: 通道名称
Returns:
<details>
<summary>源代码</summary>
```python
def get_channel(name: str) -> Channel:
"""
获取通道实例
Args:
name: 通道名称
Returns:
"""
if IS_MAIN_PROCESS:
return _channel[name]
else:
recv_chan = Channel[Channel[Any]]('recv_chan')
channel_deliver_passive_channel.send(('get_channel', {'name': name, 'recv_chan': recv_chan}))
return recv_chan.receive()
```
</details>
### ***def*** `get_channels() -> dict[str, Channel]`
获取通道实例
Returns:
<details>
<summary>源代码</summary>
```python
def get_channels() -> dict[str, Channel]:
"""
获取通道实例
Returns:
"""
if IS_MAIN_PROCESS:
return _channel
else:
recv_chan = Channel[dict[str, Channel[Any]]]('recv_chan')
channel_deliver_passive_channel.send(('get_channels', {'recv_chan': recv_chan}))
return recv_chan.receive()
```
</details>
### ***def*** `on_set_channel(data: tuple[str, dict[str, Any]]) -> None`
<details>
<summary>源代码</summary>
```python
@channel_deliver_passive_channel.on_receive(filter_func=lambda data: data[0] == 'set_channel')
def on_set_channel(data: tuple[str, dict[str, Any]]):
name, channel = (data[1]['name'], data[1]['channel_'])
set_channel(name, channel)
```
</details>
### ***def*** `on_get_channel(data: tuple[str, dict[str, Any]]) -> None`
<details>
<summary>源代码</summary>
```python
@channel_deliver_passive_channel.on_receive(filter_func=lambda data: data[0] == 'get_channel')
def on_get_channel(data: tuple[str, dict[str, Any]]):
name, recv_chan = (data[1]['name'], data[1]['recv_chan'])
recv_chan.send(get_channel(name))
```
</details>
### ***def*** `on_get_channels(data: tuple[str, dict[str, Any]]) -> None`
<details>
<summary>源代码</summary>
```python
@channel_deliver_passive_channel.on_receive(filter_func=lambda data: data[0] == 'get_channels')
def on_get_channels(data: tuple[str, dict[str, Any]]):
recv_chan = data[1]['recv_chan']
recv_chan.send(get_channels())
```
</details>
### ***def*** `decorator(func: Callable[[T], Any]) -> Callable[[T], Any]`
<details>
<summary>源代码</summary>
```python
def decorator(func: Callable[[T], Any]) -> Callable[[T], Any]:
global _func_id
async def wrapper(data: T) -> Any:
if filter_func is not None:
if is_coroutine_callable(filter_func):
if not await filter_func(data):
return
elif not filter_func(data):
return
if is_coroutine_callable(func):
return await func(data)
else:
return func(data)
_callback_funcs[_func_id] = wrapper
if IS_MAIN_PROCESS:
self._on_main_receive_funcs.append(_func_id)
else:
self._on_sub_receive_funcs.append(_func_id)
_func_id += 1
return func
```
</details>
### ***async def*** `wrapper(data: T) -> Any`
<details>
<summary>源代码</summary>
```python
async def wrapper(data: T) -> Any:
if filter_func is not None:
if is_coroutine_callable(filter_func):
if not await filter_func(data):
return
elif not filter_func(data):
return
if is_coroutine_callable(func):
return await func(data)
else:
return func(data)
```
</details>
### ***class*** `Channel(Generic[T])`
通道类,可以在进程间和进程内通信,双向但同时只能有一个发送者和一个接收者
有两种接收工作方式,但是只能选择一种,主动接收和被动接收,主动接收使用 `receive` 方法,被动接收使用 `on_receive` 装饰器
### &emsp; ***def*** `__init__(self, _id: str, type_check: Optional[bool]) -> None`
&emsp;初始化通道
Args:
_id: 通道ID
type_check: 是否开启类型检查, 若为空,则传入泛型默认开启,否则默认关闭
<details>
<summary>源代码</summary>
```python
def __init__(self, _id: str, type_check: Optional[bool]=None):
"""
初始化通道
Args:
_id: 通道ID
type_check: 是否开启类型检查, 若为空,则传入泛型默认开启,否则默认关闭
"""
self.conn_send, self.conn_recv = Pipe()
self._closed = False
self._on_main_receive_funcs: list[int] = []
self._on_sub_receive_funcs: list[int] = []
self.name: str = _id
self.is_main_receive_loop_running = False
self.is_sub_receive_loop_running = False
if type_check is None:
type_check = self._get_generic_type() is not None
elif type_check:
if self._get_generic_type() is None:
raise TypeError('Type hint is required for enforcing type check.')
self.type_check = type_check
```
</details>
### &emsp; ***def*** `send(self, data: T) -> None`
&emsp;发送数据
Args:
data: 数据
<details>
<summary>源代码</summary>
```python
def send(self, data: T):
"""
发送数据
Args:
data: 数据
"""
if self.type_check:
_type = self._get_generic_type()
if _type is not None and (not self._validate_structure(data, _type)):
raise TypeError(f'Data must be an instance of {_type}, {type(data)} found')
if self._closed:
raise RuntimeError('Cannot send to a closed channel_')
self.conn_send.send(data)
```
</details>
### &emsp; ***def*** `receive(self) -> T`
&emsp;接收数据
Args:
<details>
<summary>源代码</summary>
```python
def receive(self) -> T:
"""
接收数据
Args:
"""
if self._closed:
raise RuntimeError('Cannot receive from a closed channel_')
while True:
data = self.conn_recv.recv()
return data
```
</details>
### &emsp; ***def*** `close(self) -> None`
&emsp;关闭通道
<details>
<summary>源代码</summary>
```python
def close(self):
"""
关闭通道
"""
self._closed = True
self.conn_send.close()
self.conn_recv.close()
```
</details>
### &emsp; ***def*** `on_receive(self, filter_func: Optional[FILTER_FUNC]) -> Callable[[Callable[[T], Any]], Callable[[T], Any]]`
&emsp;接收数据并执行函数
Args:
filter_func: 过滤函数为None则不过滤
Returns:
装饰器,装饰一个函数在接收到数据后执行
<details>
<summary>源代码</summary>
```python
def on_receive(self, filter_func: Optional[FILTER_FUNC]=None) -> Callable[[Callable[[T], Any]], Callable[[T], Any]]:
"""
接收数据并执行函数
Args:
filter_func: 过滤函数为None则不过滤
Returns:
装饰器,装饰一个函数在接收到数据后执行
"""
if not self.is_sub_receive_loop_running and (not IS_MAIN_PROCESS):
threading.Thread(target=self._start_sub_receive_loop, daemon=True).start()
if not self.is_main_receive_loop_running and IS_MAIN_PROCESS:
threading.Thread(target=self._start_main_receive_loop, daemon=True).start()
def decorator(func: Callable[[T], Any]) -> Callable[[T], Any]:
global _func_id
async def wrapper(data: T) -> Any:
if filter_func is not None:
if is_coroutine_callable(filter_func):
if not await filter_func(data):
return
elif not filter_func(data):
return
if is_coroutine_callable(func):
return await func(data)
else:
return func(data)
_callback_funcs[_func_id] = wrapper
if IS_MAIN_PROCESS:
self._on_main_receive_funcs.append(_func_id)
else:
self._on_sub_receive_funcs.append(_func_id)
_func_id += 1
return func
return decorator
```
</details>
### ***var*** `T = TypeVar('T')`
### ***var*** `channel_deliver_active_channel = Channel(_id='channel_deliver_active_channel')`
### ***var*** `channel_deliver_passive_channel = Channel(_id='channel_deliver_passive_channel')`
### ***var*** `recv_chan = data[1]['recv_chan']`
### ***var*** `recv_chan = Channel[Channel[Any]]('recv_chan')`
### ***var*** `recv_chan = Channel[dict[str, Channel[Any]]]('recv_chan')`
### ***var*** `type_check = self._get_generic_type() is not None`
### ***var*** `data = self.conn_recv.recv()`
### ***var*** `func = _callback_funcs[func_id]`
### ***var*** `func = _callback_funcs[func_id]`
### ***var*** `data = self.conn_recv.recv()`
### ***var*** `data = self.conn_recv.recv()`

View File

@ -0,0 +1,25 @@
---
title: liteyuki.comm.event
order: 1
icon: laptop-code
category: API
---
### ***class*** `Event`
事件类
### &emsp; ***def*** `__init__(self, name: str, data: dict[str, Any]) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def __init__(self, name: str, data: dict[str, Any]):
self.name = name
self.data = data
```
</details>

View File

@ -0,0 +1,563 @@
---
title: liteyuki.comm.storage
order: 1
icon: laptop-code
category: API
---
### ***def*** `run_subscriber_receive_funcs(channel_: str, data: Any) -> None`
运行订阅者接收函数
Args:
channel_: 频道
data: 数据
<details>
<summary>源代码</summary>
```python
@staticmethod
def run_subscriber_receive_funcs(channel_: str, data: Any):
"""
运行订阅者接收函数
Args:
channel_: 频道
data: 数据
"""
if IS_MAIN_PROCESS:
if channel_ in _on_main_subscriber_receive_funcs and _on_main_subscriber_receive_funcs[channel_]:
run_coroutine(*[func(data) for func in _on_main_subscriber_receive_funcs[channel_]])
elif channel_ in _on_sub_subscriber_receive_funcs and _on_sub_subscriber_receive_funcs[channel_]:
run_coroutine(*[func(data) for func in _on_sub_subscriber_receive_funcs[channel_]])
```
</details>
### ***def*** `on_get(data: tuple[str, dict[str, Any]]) -> None`
<details>
<summary>源代码</summary>
```python
@shared_memory.passive_chan.on_receive(lambda d: d[0] == 'get')
def on_get(data: tuple[str, dict[str, Any]]):
key = data[1]['key']
default = data[1]['default']
recv_chan = data[1]['recv_chan']
recv_chan.send(shared_memory.get(key, default))
```
</details>
### ***def*** `on_set(data: tuple[str, dict[str, Any]]) -> None`
<details>
<summary>源代码</summary>
```python
@shared_memory.passive_chan.on_receive(lambda d: d[0] == 'set')
def on_set(data: tuple[str, dict[str, Any]]):
key = data[1]['key']
value = data[1]['value']
shared_memory.set(key, value)
```
</details>
### ***def*** `on_delete(data: tuple[str, dict[str, Any]]) -> None`
<details>
<summary>源代码</summary>
```python
@shared_memory.passive_chan.on_receive(lambda d: d[0] == 'delete')
def on_delete(data: tuple[str, dict[str, Any]]):
key = data[1]['key']
shared_memory.delete(key)
```
</details>
### ***def*** `on_get_all(data: tuple[str, dict[str, Any]]) -> None`
<details>
<summary>源代码</summary>
```python
@shared_memory.passive_chan.on_receive(lambda d: d[0] == 'get_all')
def on_get_all(data: tuple[str, dict[str, Any]]):
recv_chan = data[1]['recv_chan']
recv_chan.send(shared_memory.get_all())
```
</details>
### ***def*** `on_publish(data: tuple[str, Any]) -> None`
<details>
<summary>源代码</summary>
```python
@channel.publish_channel.on_receive()
def on_publish(data: tuple[str, Any]):
channel_, data = data
shared_memory.run_subscriber_receive_funcs(channel_, data)
```
</details>
### ***def*** `decorator(func: ON_RECEIVE_FUNC) -> ON_RECEIVE_FUNC`
<details>
<summary>源代码</summary>
```python
def decorator(func: ON_RECEIVE_FUNC) -> ON_RECEIVE_FUNC:
async def wrapper(data: Any):
if is_coroutine_callable(func):
await func(data)
else:
func(data)
if IS_MAIN_PROCESS:
if channel_ not in _on_main_subscriber_receive_funcs:
_on_main_subscriber_receive_funcs[channel_] = []
_on_main_subscriber_receive_funcs[channel_].append(wrapper)
else:
if channel_ not in _on_sub_subscriber_receive_funcs:
_on_sub_subscriber_receive_funcs[channel_] = []
_on_sub_subscriber_receive_funcs[channel_].append(wrapper)
return wrapper
```
</details>
### ***async def*** `wrapper(data: Any) -> None`
<details>
<summary>源代码</summary>
```python
async def wrapper(data: Any):
if is_coroutine_callable(func):
await func(data)
else:
func(data)
```
</details>
### ***class*** `Subscriber`
### &emsp; ***def*** `__init__(self) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def __init__(self):
self._subscribers = {}
```
</details>
### &emsp; ***def*** `receive(self) -> Any`
&emsp;
<details>
<summary>源代码</summary>
```python
def receive(self) -> Any:
pass
```
</details>
### &emsp; ***def*** `unsubscribe(self) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def unsubscribe(self) -> None:
pass
```
</details>
### ***class*** `KeyValueStore`
### &emsp; ***def*** `__init__(self) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def __init__(self):
self._store = {}
self.active_chan = Channel[tuple[str, Optional[dict[str, Any]]]](_id='shared_memory-active')
self.passive_chan = Channel[tuple[str, Optional[dict[str, Any]]]](_id='shared_memory-passive')
self.publish_channel = Channel[tuple[str, Any]](_id='shared_memory-publish')
self.is_main_receive_loop_running = False
self.is_sub_receive_loop_running = False
```
</details>
### &emsp; ***def*** `set(self, key: str, value: Any) -> None`
&emsp;设置键值对
Args:
key: 键
value: 值
<details>
<summary>源代码</summary>
```python
def set(self, key: str, value: Any) -> None:
"""
设置键值对
Args:
key: 键
value: 值
"""
if IS_MAIN_PROCESS:
lock = _get_lock(key)
with lock:
self._store[key] = value
else:
self.passive_chan.send(('set', {'key': key, 'value': value}))
```
</details>
### &emsp; ***def*** `get(self, key: str, default: Optional[Any]) -> Optional[Any]`
&emsp;获取键值对
Args:
key: 键
default: 默认值
Returns:
Any: 值
<details>
<summary>源代码</summary>
```python
def get(self, key: str, default: Optional[Any]=None) -> Optional[Any]:
"""
获取键值对
Args:
key: 键
default: 默认值
Returns:
Any: 值
"""
if IS_MAIN_PROCESS:
lock = _get_lock(key)
with lock:
return self._store.get(key, default)
else:
recv_chan = Channel[Optional[Any]]('recv_chan')
self.passive_chan.send(('get', {'key': key, 'default': default, 'recv_chan': recv_chan}))
return recv_chan.receive()
```
</details>
### &emsp; ***def*** `delete(self, key: str, ignore_key_error: bool) -> None`
&emsp;删除键值对
Args:
key: 键
ignore_key_error: 是否忽略键不存在的错误
Returns:
<details>
<summary>源代码</summary>
```python
def delete(self, key: str, ignore_key_error: bool=True) -> None:
"""
删除键值对
Args:
key: 键
ignore_key_error: 是否忽略键不存在的错误
Returns:
"""
if IS_MAIN_PROCESS:
lock = _get_lock(key)
with lock:
if key in self._store:
try:
del self._store[key]
del _locks[key]
except KeyError as e:
if not ignore_key_error:
raise e
else:
self.passive_chan.send(('delete', {'key': key}))
```
</details>
### &emsp; ***def*** `get_all(self) -> dict[str, Any]`
&emsp;获取所有键值对
Returns:
dict[str, Any]: 键值对
<details>
<summary>源代码</summary>
```python
def get_all(self) -> dict[str, Any]:
"""
获取所有键值对
Returns:
dict[str, Any]: 键值对
"""
if IS_MAIN_PROCESS:
return self._store
else:
recv_chan = Channel[dict[str, Any]]('recv_chan')
self.passive_chan.send(('get_all', {'recv_chan': recv_chan}))
return recv_chan.receive()
```
</details>
### &emsp; ***def*** `publish(self, channel_: str, data: Any) -> None`
&emsp;发布消息
Args:
channel_: 频道
data: 数据
Returns:
<details>
<summary>源代码</summary>
```python
def publish(self, channel_: str, data: Any) -> None:
"""
发布消息
Args:
channel_: 频道
data: 数据
Returns:
"""
self.active_chan.send(('publish', {'channel': channel_, 'data': data}))
```
</details>
### &emsp; ***def*** `on_subscriber_receive(self, channel_: str) -> Callable[[ON_RECEIVE_FUNC], ON_RECEIVE_FUNC]`
&emsp;订阅者接收消息时的回调
Args:
channel_: 频道
Returns:
装饰器
<details>
<summary>源代码</summary>
```python
def on_subscriber_receive(self, channel_: str) -> Callable[[ON_RECEIVE_FUNC], ON_RECEIVE_FUNC]:
"""
订阅者接收消息时的回调
Args:
channel_: 频道
Returns:
装饰器
"""
if IS_MAIN_PROCESS and (not self.is_main_receive_loop_running):
threading.Thread(target=self._start_receive_loop, daemon=True).start()
shared_memory.is_main_receive_loop_running = True
elif not IS_MAIN_PROCESS and (not self.is_sub_receive_loop_running):
threading.Thread(target=self._start_receive_loop, daemon=True).start()
shared_memory.is_sub_receive_loop_running = True
def decorator(func: ON_RECEIVE_FUNC) -> ON_RECEIVE_FUNC:
async def wrapper(data: Any):
if is_coroutine_callable(func):
await func(data)
else:
func(data)
if IS_MAIN_PROCESS:
if channel_ not in _on_main_subscriber_receive_funcs:
_on_main_subscriber_receive_funcs[channel_] = []
_on_main_subscriber_receive_funcs[channel_].append(wrapper)
else:
if channel_ not in _on_sub_subscriber_receive_funcs:
_on_sub_subscriber_receive_funcs[channel_] = []
_on_sub_subscriber_receive_funcs[channel_].append(wrapper)
return wrapper
return decorator
```
</details>
### &emsp; ***@staticmethod***
### &emsp; ***def*** `run_subscriber_receive_funcs(channel_: str, data: Any) -> None`
&emsp;运行订阅者接收函数
Args:
channel_: 频道
data: 数据
<details>
<summary>源代码</summary>
```python
@staticmethod
def run_subscriber_receive_funcs(channel_: str, data: Any):
"""
运行订阅者接收函数
Args:
channel_: 频道
data: 数据
"""
if IS_MAIN_PROCESS:
if channel_ in _on_main_subscriber_receive_funcs and _on_main_subscriber_receive_funcs[channel_]:
run_coroutine(*[func(data) for func in _on_main_subscriber_receive_funcs[channel_]])
elif channel_ in _on_sub_subscriber_receive_funcs and _on_sub_subscriber_receive_funcs[channel_]:
run_coroutine(*[func(data) for func in _on_sub_subscriber_receive_funcs[channel_]])
```
</details>
### ***class*** `GlobalKeyValueStore`
### &emsp; ***@classmethod***
### &emsp; ***def*** `get_instance(cls: Any) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
@classmethod
def get_instance(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = KeyValueStore()
return cls._instance
```
</details>
### &emsp; ***attr*** `_instance: None`
### &emsp; ***attr*** `_lock: threading.Lock()`
### ***var*** `key = data[1]['key']`
### ***var*** `default = data[1]['default']`
### ***var*** `recv_chan = data[1]['recv_chan']`
### ***var*** `key = data[1]['key']`
### ***var*** `value = data[1]['value']`
### ***var*** `key = data[1]['key']`
### ***var*** `recv_chan = data[1]['recv_chan']`
### ***var*** `lock = _get_lock(key)`
### ***var*** `lock = _get_lock(key)`
### ***var*** `recv_chan = Channel[Optional[Any]]('recv_chan')`
### ***var*** `lock = _get_lock(key)`
### ***var*** `recv_chan = Channel[dict[str, Any]]('recv_chan')`
### ***var*** `data = self.active_chan.receive()`
### ***var*** `data = self.publish_channel.receive()`

231
docs/en/dev/api/config.md Normal file
View File

@ -0,0 +1,231 @@
---
title: liteyuki.config
order: 1
icon: laptop-code
category: API
---
### ***def*** `flat_config(config: dict[str, Any]) -> dict[str, Any]`
扁平化配置文件
{a:{b:{c:1}}} -> {"a.b.c": 1}
Args:
config: 配置项目
Returns:
扁平化后的配置文件,但也包含原有的键值对
<details>
<summary>源代码</summary>
```python
def flat_config(config: dict[str, Any]) -> dict[str, Any]:
"""
扁平化配置文件
{a:{b:{c:1}}} -> {"a.b.c": 1}
Args:
config: 配置项目
Returns:
扁平化后的配置文件,但也包含原有的键值对
"""
new_config = copy.deepcopy(config)
for key, value in config.items():
if isinstance(value, dict):
for k, v in flat_config(value).items():
new_config[f'{key}.{k}'] = v
return new_config
```
</details>
### ***def*** `load_from_yaml(file: str) -> dict[str, Any]`
Load config from yaml file
<details>
<summary>源代码</summary>
```python
def load_from_yaml(file: str) -> dict[str, Any]:
"""
Load config from yaml file
"""
logger.debug(f'Loading YAML config from {file}')
config = yaml.safe_load(open(file, 'r', encoding='utf-8'))
return flat_config(config if config is not None else {})
```
</details>
### ***def*** `load_from_json(file: str) -> dict[str, Any]`
Load config from json file
<details>
<summary>源代码</summary>
```python
def load_from_json(file: str) -> dict[str, Any]:
"""
Load config from json file
"""
logger.debug(f'Loading JSON config from {file}')
config = json.load(open(file, 'r', encoding='utf-8'))
return flat_config(config if config is not None else {})
```
</details>
### ***def*** `load_from_toml(file: str) -> dict[str, Any]`
Load config from toml file
<details>
<summary>源代码</summary>
```python
def load_from_toml(file: str) -> dict[str, Any]:
"""
Load config from toml file
"""
logger.debug(f'Loading TOML config from {file}')
config = toml.load(open(file, 'r', encoding='utf-8'))
return flat_config(config if config is not None else {})
```
</details>
### ***def*** `load_from_files() -> dict[str, Any]`
从指定文件加载配置项,会自动识别文件格式
默认执行扁平化选项
<details>
<summary>源代码</summary>
```python
def load_from_files(*files: str, no_warning: bool=False) -> dict[str, Any]:
"""
从指定文件加载配置项,会自动识别文件格式
默认执行扁平化选项
"""
config = {}
for file in files:
if os.path.exists(file):
if file.endswith(('.yaml', 'yml')):
config.update(load_from_yaml(file))
elif file.endswith('.json'):
config.update(load_from_json(file))
elif file.endswith('.toml'):
config.update(load_from_toml(file))
elif not no_warning:
logger.warning(f'Unsupported config file format: {file}')
elif not no_warning:
logger.warning(f'Config file not found: {file}')
return config
```
</details>
### ***def*** `load_configs_from_dirs() -> dict[str, Any]`
从目录下加载配置文件,不递归
按照读取文件的优先级反向覆盖
默认执行扁平化选项
<details>
<summary>源代码</summary>
```python
def load_configs_from_dirs(*directories: str, no_waring: bool=False) -> dict[str, Any]:
"""
从目录下加载配置文件,不递归
按照读取文件的优先级反向覆盖
默认执行扁平化选项
"""
config = {}
for directory in directories:
if not os.path.exists(directory):
if not no_waring:
logger.warning(f'Directory not found: {directory}')
continue
for file in os.listdir(directory):
if file.endswith(_SUPPORTED_CONFIG_FORMATS):
config.update(load_from_files(os.path.join(directory, file), no_warning=no_waring))
return config
```
</details>
### ***def*** `load_config_in_default(no_waring: bool) -> dict[str, Any]`
从一个标准的轻雪项目加载配置文件
项目目录下的config.*和config目录下的所有配置文件
项目目录下的配置文件优先
<details>
<summary>源代码</summary>
```python
def load_config_in_default(no_waring: bool=False) -> dict[str, Any]:
"""
从一个标准的轻雪项目加载配置文件
项目目录下的config.*和config目录下的所有配置文件
项目目录下的配置文件优先
"""
config = load_configs_from_dirs('config', no_waring=no_waring)
config.update(load_from_files('config.yaml', 'config.toml', 'config.json', 'config.yml', no_warning=no_waring))
return config
```
</details>
### ***class*** `SatoriNodeConfig(BaseModel)`
### ***class*** `SatoriConfig(BaseModel)`
### ***class*** `BasicConfig(BaseModel)`
### ***var*** `new_config = copy.deepcopy(config)`
### ***var*** `config = yaml.safe_load(open(file, 'r', encoding='utf-8'))`
### ***var*** `config = json.load(open(file, 'r', encoding='utf-8'))`
### ***var*** `config = toml.load(open(file, 'r', encoding='utf-8'))`
### ***var*** `config = {}`
### ***var*** `config = {}`
### ***var*** `config = load_configs_from_dirs('config', no_waring=no_waring)`

View File

@ -0,0 +1,7 @@
---
title: liteyuki.core
index: true
icon: laptop-code
category: API
---

View File

@ -0,0 +1,275 @@
---
title: liteyuki.core.manager
order: 1
icon: laptop-code
category: API
---
### ***class*** `ChannelDeliver`
### &emsp; ***def*** `__init__(self, active: Channel[Any], passive: Channel[Any], channel_deliver_active: Channel[Channel[Any]], channel_deliver_passive: Channel[tuple[str, dict]], publish: Channel[tuple[str, Any]]) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def __init__(self, active: Channel[Any], passive: Channel[Any], channel_deliver_active: Channel[Channel[Any]], channel_deliver_passive: Channel[tuple[str, dict]], publish: Channel[tuple[str, Any]]):
self.active = active
self.passive = passive
self.channel_deliver_active = channel_deliver_active
self.channel_deliver_passive = channel_deliver_passive
self.publish = publish
```
</details>
### ***class*** `ProcessManager`
进程管理器
### &emsp; ***def*** `__init__(self, lifespan: 'Lifespan') -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def __init__(self, lifespan: 'Lifespan'):
self.lifespan = lifespan
self.targets: dict[str, tuple[Callable, tuple, dict]] = {}
self.processes: dict[str, Process] = {}
```
</details>
### &emsp; ***def*** `start(self, name: str) -> None`
&emsp;开启后自动监控进程,并添加到进程字典中
Args:
name:
Returns:
<details>
<summary>源代码</summary>
```python
def start(self, name: str):
"""
开启后自动监控进程,并添加到进程字典中
Args:
name:
Returns:
"""
if name not in self.targets:
raise KeyError(f'Process {name} not found.')
chan_active = get_channel(f'{name}-active')
def _start_process():
process = Process(target=self.targets[name][0], args=self.targets[name][1], kwargs=self.targets[name][2], daemon=True)
self.processes[name] = process
process.start()
_start_process()
while True:
data = chan_active.receive()
if data == 0:
logger.info(f'Stopping process {name}')
self.lifespan.before_process_shutdown()
self.terminate(name)
break
elif data == 1:
logger.info(f'Restarting process {name}')
self.lifespan.before_process_shutdown()
self.lifespan.before_process_restart()
self.terminate(name)
_start_process()
continue
else:
logger.warning('Unknown data received, ignored.')
```
</details>
### &emsp; ***def*** `start_all(self) -> None`
&emsp;启动所有进程
<details>
<summary>源代码</summary>
```python
def start_all(self):
"""
启动所有进程
"""
for name in self.targets:
threading.Thread(target=self.start, args=(name,), daemon=True).start()
```
</details>
### &emsp; ***def*** `add_target(self, name: str, target: TARGET_FUNC, args: tuple, kwargs: Any) -> None`
&emsp;添加进程
Args:
name: 进程名,用于获取和唯一标识
target: 进程函数
args: 进程函数参数
kwargs: 进程函数关键字参数通常会默认传入chan_active和chan_passive
<details>
<summary>源代码</summary>
```python
def add_target(self, name: str, target: TARGET_FUNC, args: tuple=(), kwargs=None):
"""
添加进程
Args:
name: 进程名,用于获取和唯一标识
target: 进程函数
args: 进程函数参数
kwargs: 进程函数关键字参数通常会默认传入chan_active和chan_passive
"""
if kwargs is None:
kwargs = {}
chan_active: Channel = Channel(_id=f'{name}-active')
chan_passive: Channel = Channel(_id=f'{name}-passive')
channel_deliver = ChannelDeliver(active=chan_active, passive=chan_passive, channel_deliver_active=channel_deliver_active_channel, channel_deliver_passive=channel_deliver_passive_channel, publish=publish_channel)
self.targets[name] = (_delivery_channel_wrapper, (target, channel_deliver, shared_memory, *args), kwargs)
set_channels({f'{name}-active': chan_active, f'{name}-passive': chan_passive})
```
</details>
### &emsp; ***def*** `join_all(self) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def join_all(self):
for name, process in self.targets:
process.join()
```
</details>
### &emsp; ***def*** `terminate(self, name: str) -> None`
&emsp;终止进程并从进程字典中删除
Args:
name:
Returns:
<details>
<summary>源代码</summary>
```python
def terminate(self, name: str):
"""
终止进程并从进程字典中删除
Args:
name:
Returns:
"""
if name not in self.processes:
logger.warning(f'Process {name} not found.')
return
process = self.processes[name]
process.terminate()
process.join(TIMEOUT)
if process.is_alive():
process.kill()
logger.success(f'Process {name} terminated.')
```
</details>
### &emsp; ***def*** `terminate_all(self) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def terminate_all(self):
for name in self.targets:
self.terminate(name)
```
</details>
### &emsp; ***def*** `is_process_alive(self, name: str) -> bool`
&emsp;检查进程是否存活
Args:
name:
Returns:
<details>
<summary>源代码</summary>
```python
def is_process_alive(self, name: str) -> bool:
"""
检查进程是否存活
Args:
name:
Returns:
"""
if name not in self.targets:
logger.warning(f'Process {name} not found.')
return self.processes[name].is_alive()
```
</details>
### ***var*** `TIMEOUT = 10`
### ***var*** `chan_active = get_channel(f'{name}-active')`
### ***var*** `channel_deliver = ChannelDeliver(active=chan_active, passive=chan_passive, channel_deliver_active=channel_deliver_active_channel, channel_deliver_passive=channel_deliver_passive_channel, publish=publish_channel)`
### ***var*** `process = self.processes[name]`
### ***var*** `process = Process(target=self.targets[name][0], args=self.targets[name][1], kwargs=self.targets[name][2], daemon=True)`
### ***var*** `data = chan_active.receive()`
### ***var*** `kwargs = {}`

View File

@ -0,0 +1,7 @@
---
title: liteyuki.dev
index: true
icon: laptop-code
category: API
---

View File

@ -0,0 +1,249 @@
---
title: liteyuki.dev.observer
order: 1
icon: laptop-code
category: API
---
### ***def*** `debounce(wait: Any) -> None`
防抖函数
<details>
<summary>源代码</summary>
```python
def debounce(wait):
"""
防抖函数
"""
def decorator(func):
def wrapper(*args, **kwargs):
nonlocal last_call_time
current_time = time.time()
if current_time - last_call_time > wait:
last_call_time = current_time
return func(*args, **kwargs)
last_call_time = None
return wrapper
return decorator
```
</details>
### ***def*** `on_file_system_event(directories: tuple[str], recursive: bool, event_filter: FILTER_FUNC) -> Callable[[CALLBACK_FUNC], CALLBACK_FUNC]`
注册文件系统变化监听器
Args:
directories: 监听目录们
recursive: 是否递归监听子目录
event_filter: 事件过滤器, 返回True则执行回调函数
Returns:
装饰器,装饰一个函数在接收到数据后执行
<details>
<summary>源代码</summary>
```python
def on_file_system_event(directories: tuple[str], recursive: bool=True, event_filter: FILTER_FUNC=None) -> Callable[[CALLBACK_FUNC], CALLBACK_FUNC]:
"""
注册文件系统变化监听器
Args:
directories: 监听目录们
recursive: 是否递归监听子目录
event_filter: 事件过滤器, 返回True则执行回调函数
Returns:
装饰器,装饰一个函数在接收到数据后执行
"""
def decorator(func: CALLBACK_FUNC) -> CALLBACK_FUNC:
def wrapper(event: FileSystemEvent):
if event_filter is not None and (not event_filter(event)):
return
func(event)
code_modified_handler = CodeModifiedHandler()
code_modified_handler.on_modified = wrapper
for directory in directories:
observer.schedule(code_modified_handler, directory, recursive=recursive)
return func
return decorator
```
</details>
### ***def*** `decorator(func: Any) -> None`
<details>
<summary>源代码</summary>
```python
def decorator(func):
def wrapper(*args, **kwargs):
nonlocal last_call_time
current_time = time.time()
if current_time - last_call_time > wait:
last_call_time = current_time
return func(*args, **kwargs)
last_call_time = None
return wrapper
```
</details>
### ***def*** `decorator(func: CALLBACK_FUNC) -> CALLBACK_FUNC`
<details>
<summary>源代码</summary>
```python
def decorator(func: CALLBACK_FUNC) -> CALLBACK_FUNC:
def wrapper(event: FileSystemEvent):
if event_filter is not None and (not event_filter(event)):
return
func(event)
code_modified_handler = CodeModifiedHandler()
code_modified_handler.on_modified = wrapper
for directory in directories:
observer.schedule(code_modified_handler, directory, recursive=recursive)
return func
```
</details>
### ***def*** `wrapper() -> None`
<details>
<summary>源代码</summary>
```python
def wrapper(*args, **kwargs):
nonlocal last_call_time
current_time = time.time()
if current_time - last_call_time > wait:
last_call_time = current_time
return func(*args, **kwargs)
```
</details>
### ***def*** `wrapper(event: FileSystemEvent) -> None`
<details>
<summary>源代码</summary>
```python
def wrapper(event: FileSystemEvent):
if event_filter is not None and (not event_filter(event)):
return
func(event)
```
</details>
### ***class*** `CodeModifiedHandler(FileSystemEventHandler)`
Handler for code file changes
### &emsp; ***def*** `on_modified(self, event: Any) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
@debounce(1)
def on_modified(self, event):
raise NotImplementedError('on_modified must be implemented')
```
</details>
### &emsp; ***def*** `on_created(self, event: Any) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def on_created(self, event):
self.on_modified(event)
```
</details>
### &emsp; ***def*** `on_deleted(self, event: Any) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def on_deleted(self, event):
self.on_modified(event)
```
</details>
### &emsp; ***def*** `on_moved(self, event: Any) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def on_moved(self, event):
self.on_modified(event)
```
</details>
### &emsp; ***def*** `on_any_event(self, event: Any) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def on_any_event(self, event):
self.on_modified(event)
```
</details>
### ***var*** `liteyuki_bot = get_bot()`
### ***var*** `observer = Observer()`
### ***var*** `last_call_time = None`
### ***var*** `code_modified_handler = CodeModifiedHandler()`
### ***var*** `current_time = time.time()`
### ***var*** `last_call_time = current_time`

View File

@ -0,0 +1,46 @@
---
title: liteyuki.dev.plugin
order: 1
icon: laptop-code
category: API
---
### ***def*** `run_plugins() -> None`
运行插件无需手动初始化bot
Args:
module_path: 插件路径,参考`liteyuki.load_plugin`的函数签名
<details>
<summary>源代码</summary>
```python
def run_plugins(*module_path: str | Path):
"""
运行插件无需手动初始化bot
Args:
module_path: 插件路径,参考`liteyuki.load_plugin`的函数签名
"""
cfg = load_config_in_default()
plugins = cfg.get('liteyuki.plugins', [])
plugins.extend(module_path)
cfg['liteyuki.plugins'] = plugins
bot = LiteyukiBot(**cfg)
bot.run()
```
</details>
### ***var*** `cfg = load_config_in_default()`
### ***var*** `plugins = cfg.get('liteyuki.plugins', [])`
### ***var*** `bot = LiteyukiBot(**cfg)`

View File

@ -0,0 +1,11 @@
---
title: liteyuki.exception
order: 1
icon: laptop-code
category: API
---
### ***class*** `LiteyukiException(BaseException)`
Liteyuki的异常基类。

58
docs/en/dev/api/log.md Normal file
View File

@ -0,0 +1,58 @@
---
title: liteyuki.log
order: 1
icon: laptop-code
category: API
---
### ***def*** `get_format(level: str) -> str`
<details>
<summary>源代码</summary>
```python
def get_format(level: str) -> str:
if level == 'DEBUG':
return debug_format
else:
return default_format
```
</details>
### ***def*** `init_log(config: dict) -> None`
在语言加载完成后执行
Returns:
<details>
<summary>源代码</summary>
```python
def init_log(config: dict):
"""
在语言加载完成后执行
Returns:
"""
logger.remove()
logger.add(sys.stdout, level=0, diagnose=False, format=get_format(config.get('log_level', 'INFO')))
show_icon = config.get('log_icon', True)
logger.level('DEBUG', color='<blue>', icon=f"{('🐛' if show_icon else '')}DEBUG")
logger.level('INFO', color='<normal>', icon=f"{('' if show_icon else '')}INFO")
logger.level('SUCCESS', color='<green>', icon=f"{('✅' if show_icon else '')}SUCCESS")
logger.level('WARNING', color='<yellow>', icon=f"{('⚠️' if show_icon else '')}WARNING")
logger.level('ERROR', color='<red>', icon=f"{('⭕' if show_icon else '')}ERROR")
```
</details>
### ***var*** `logger = loguru.logger`
### ***var*** `show_icon = config.get('log_icon', True)`

View File

@ -0,0 +1,7 @@
---
title: liteyuki.message
index: true
icon: laptop-code
category: API
---

View File

@ -0,0 +1,106 @@
---
title: liteyuki.message.event
order: 1
icon: laptop-code
category: API
---
### ***class*** `MessageEvent`
### &emsp; ***def*** `__init__(self, bot_id: str, message: list[dict[str, Any]] | str, message_type: str, raw_message: str, session_id: str, session_type: str, receive_channel: str, data: Optional[dict[str, Any]]) -> None`
&emsp;轻雪抽象消息事件
Args:
bot_id: 机器人ID
message: 消息,消息段数组[{type: str, data: dict[str, Any]}]
raw_message: 原始消息(通常为纯文本的格式)
message_type: 消息类型(private, group, other)
session_id: 会话ID(私聊通常为用户ID群聊通常为群ID)
session_type: 会话类型(private, group)
receive_channel: 接收频道(用于回复消息)
data: 附加数据
<details>
<summary>源代码</summary>
```python
def __init__(self, bot_id: str, message: list[dict[str, Any]] | str, message_type: str, raw_message: str, session_id: str, session_type: str, receive_channel: str, data: Optional[dict[str, Any]]=None):
"""
轻雪抽象消息事件
Args:
bot_id: 机器人ID
message: 消息,消息段数组[{type: str, data: dict[str, Any]}]
raw_message: 原始消息(通常为纯文本的格式)
message_type: 消息类型(private, group, other)
session_id: 会话ID(私聊通常为用户ID群聊通常为群ID)
session_type: 会话类型(private, group)
receive_channel: 接收频道(用于回复消息)
data: 附加数据
"""
if data is None:
data = {}
self.message_type = message_type
self.data = data
self.bot_id = bot_id
self.message = message
self.raw_message = raw_message
self.session_id = session_id
self.session_type = session_type
self.receive_channel = receive_channel
```
</details>
### &emsp; ***def*** `reply(self, message: str | dict[str, Any]) -> None`
&emsp;回复消息
Args:
message:
Returns:
<details>
<summary>源代码</summary>
```python
def reply(self, message: str | dict[str, Any]):
"""
回复消息
Args:
message:
Returns:
"""
reply_event = MessageEvent(message_type=self.session_type, message=message, raw_message='', data={'message': message}, bot_id=self.bot_id, session_id=self.session_id, session_type=self.session_type, receive_channel='_')
shared_memory.publish(self.receive_channel, reply_event)
```
</details>
### ***var*** `reply_event = MessageEvent(message_type=self.session_type, message=message, raw_message='', data={'message': message}, bot_id=self.bot_id, session_id=self.session_id, session_type=self.session_type, receive_channel='_')`
### ***var*** `data = {}`

View File

@ -0,0 +1,71 @@
---
title: liteyuki.message.matcher
order: 1
icon: laptop-code
category: API
---
### ***class*** `Matcher`
### &emsp; ***def*** `__init__(self, rule: Rule, priority: int, block: bool) -> None`
&emsp;匹配器
Args:
rule: 规则
priority: 优先级 >= 0
block: 是否阻断后续优先级更低的匹配器
<details>
<summary>源代码</summary>
```python
def __init__(self, rule: Rule, priority: int, block: bool):
"""
匹配器
Args:
rule: 规则
priority: 优先级 >= 0
block: 是否阻断后续优先级更低的匹配器
"""
self.rule = rule
self.priority = priority
self.block = block
self.handlers: list[EventHandler] = []
```
</details>
### &emsp; ***def*** `handle(self, handler: EventHandler) -> EventHandler`
&emsp;添加处理函数,装饰器
Args:
handler:
Returns:
EventHandler
<details>
<summary>源代码</summary>
```python
def handle(self, handler: EventHandler) -> EventHandler:
"""
添加处理函数,装饰器
Args:
handler:
Returns:
EventHandler
"""
self.handlers.append(handler)
return handler
```
</details>

View File

@ -0,0 +1,39 @@
---
title: liteyuki.message.on
order: 1
icon: laptop-code
category: API
---
### ***def*** `on_message(rule: Rule, priority: int, block: bool) -> Matcher`
<details>
<summary>源代码</summary>
```python
def on_message(rule: Rule=Rule(), priority: int=0, block: bool=True) -> Matcher:
matcher = Matcher(rule, priority, block)
for i, m in enumerate(_matcher_list):
if m.priority < matcher.priority:
_matcher_list.insert(i, matcher)
break
else:
_matcher_list.append(matcher)
return matcher
```
</details>
### ***var*** `current_priority = -1`
### ***var*** `matcher = Matcher(rule, priority, block)`
### ***var*** `current_priority = matcher.priority`

View File

@ -0,0 +1,24 @@
---
title: liteyuki.message.rule
order: 1
icon: laptop-code
category: API
---
### ***class*** `Rule`
### &emsp; ***def*** `__init__(self, handler: Optional[RuleHandler]) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def __init__(self, handler: Optional[RuleHandler]=None):
self.handler = handler
```
</details>

View File

@ -0,0 +1,7 @@
---
title: liteyuki.message.session
order: 1
icon: laptop-code
category: API
---

473
docs/en/dev/api/mkdoc.md Normal file
View File

@ -0,0 +1,473 @@
---
title: liteyuki.mkdoc
order: 1
icon: laptop-code
category: API
---
### ***def*** `get_relative_path(base_path: str, target_path: str) -> str`
获取相对路径
Args:
base_path: 基础路径
target_path: 目标路径
<details>
<summary>源代码</summary>
```python
def get_relative_path(base_path: str, target_path: str) -> str:
"""
获取相对路径
Args:
base_path: 基础路径
target_path: 目标路径
"""
return os.path.relpath(target_path, base_path)
```
</details>
### ***def*** `write_to_files(file_data: dict[str, str]) -> None`
输出文件
Args:
file_data: 文件数据 相对路径
<details>
<summary>源代码</summary>
```python
def write_to_files(file_data: dict[str, str]):
"""
输出文件
Args:
file_data: 文件数据 相对路径
"""
for rp, data in file_data.items():
if not os.path.exists(os.path.dirname(rp)):
os.makedirs(os.path.dirname(rp))
with open(rp, 'w', encoding='utf-8') as f:
f.write(data)
```
</details>
### ***def*** `get_file_list(module_folder: str) -> None`
<details>
<summary>源代码</summary>
```python
def get_file_list(module_folder: str):
file_list = []
for root, dirs, files in os.walk(module_folder):
for file in files:
if file.endswith(('.py', '.pyi')):
file_list.append(os.path.join(root, file))
return file_list
```
</details>
### ***def*** `get_module_info_normal(file_path: str, ignore_private: bool) -> ModuleInfo`
获取函数和类
Args:
file_path: Python 文件路径
ignore_private: 忽略私有函数和类
Returns:
模块信息
<details>
<summary>源代码</summary>
```python
def get_module_info_normal(file_path: str, ignore_private: bool=True) -> ModuleInfo:
"""
获取函数和类
Args:
file_path: Python 文件路径
ignore_private: 忽略私有函数和类
Returns:
模块信息
"""
with open(file_path, 'r', encoding='utf-8') as file:
file_content = file.read()
tree = ast.parse(file_content)
dot_sep_module_path = file_path.replace(os.sep, '.').replace('.py', '').replace('.pyi', '')
module_docstring = ast.get_docstring(tree)
module_info = ModuleInfo(module_path=dot_sep_module_path, functions=[], classes=[], attributes=[], docstring=module_docstring if module_docstring else '')
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
if not any((isinstance(parent, ast.ClassDef) for parent in ast.iter_child_nodes(node))) and (not ignore_private or not node.name.startswith('_')):
if node.args.args:
first_arg = node.args.args[0]
if first_arg.arg in ('self', 'cls'):
continue
function_docstring = ast.get_docstring(node)
func_info = FunctionInfo(name=node.name, args=[(arg.arg, ast.unparse(arg.annotation) if arg.annotation else NO_TYPE_ANY) for arg in node.args.args], return_type=ast.unparse(node.returns) if node.returns else 'None', docstring=function_docstring if function_docstring else '', type=DefType.FUNCTION, is_async=isinstance(node, ast.AsyncFunctionDef), source_code=ast.unparse(node))
module_info.functions.append(func_info)
elif isinstance(node, ast.ClassDef):
class_docstring = ast.get_docstring(node)
class_info = ClassInfo(name=node.name, docstring=class_docstring if class_docstring else '', methods=[], attributes=[], inherit=[ast.unparse(base) for base in node.bases])
for class_node in node.body:
if isinstance(class_node, ast.FunctionDef) and (not ignore_private or not class_node.name.startswith('_') or class_node.name == '__init__'):
method_docstring = ast.get_docstring(class_node)
def_type = DefType.METHOD
if class_node.decorator_list:
if any((isinstance(decorator, ast.Name) and decorator.id == 'staticmethod' for decorator in class_node.decorator_list)):
def_type = DefType.STATIC_METHOD
elif any((isinstance(decorator, ast.Name) and decorator.id == 'classmethod' for decorator in class_node.decorator_list)):
def_type = DefType.CLASS_METHOD
elif any((isinstance(decorator, ast.Name) and decorator.id == 'property' for decorator in class_node.decorator_list)):
def_type = DefType.PROPERTY
class_info.methods.append(FunctionInfo(name=class_node.name, args=[(arg.arg, ast.unparse(arg.annotation) if arg.annotation else NO_TYPE_ANY) for arg in class_node.args.args], return_type=ast.unparse(class_node.returns) if class_node.returns else 'None', docstring=method_docstring if method_docstring else '', type=def_type, is_async=isinstance(class_node, ast.AsyncFunctionDef), source_code=ast.unparse(class_node)))
elif isinstance(class_node, ast.Assign):
for target in class_node.targets:
if isinstance(target, ast.Name):
class_info.attributes.append(AttributeInfo(name=target.id, type=ast.unparse(class_node.value)))
module_info.classes.append(class_info)
elif isinstance(node, ast.Assign):
if not any((isinstance(parent, (ast.ClassDef, ast.FunctionDef)) for parent in ast.iter_child_nodes(node))):
for target in node.targets:
if isinstance(target, ast.Name) and (not ignore_private or not target.id.startswith('_')):
attr_type = NO_TYPE_HINT
if isinstance(node.value, ast.AnnAssign) and node.value.annotation:
attr_type = ast.unparse(node.value.annotation)
module_info.attributes.append(AttributeInfo(name=target.id, type=attr_type, value=ast.unparse(node.value) if node.value else None))
return module_info
```
</details>
### ***def*** `generate_markdown(module_info: ModuleInfo, front_matter: Any) -> str`
生成模块的Markdown
你可在此自定义生成的Markdown格式
Args:
module_info: 模块信息
front_matter: 自定义选项title, index, icon, category
Returns:
Markdown 字符串
<details>
<summary>源代码</summary>
```python
def generate_markdown(module_info: ModuleInfo, front_matter=None) -> str:
"""
生成模块的Markdown
你可在此自定义生成的Markdown格式
Args:
module_info: 模块信息
front_matter: 自定义选项title, index, icon, category
Returns:
Markdown 字符串
"""
content = ''
front_matter = '---\n' + '\n'.join([f'{k}: {v}' for k, v in front_matter.items()]) + '\n---\n\n'
content += front_matter
for func in module_info.functions:
args_with_type = [f'{arg[0]}: {arg[1]}' if arg[1] else arg[0] for arg in func.args]
content += f"### ***{('async ' if func.is_async else '')}def*** `{func.name}({', '.join(args_with_type)}) -> {func.return_type}`\n\n"
func.docstring = func.docstring.replace('\n', '\n\n')
content += f'{func.docstring}\n\n'
content += f'<details>\n<summary>源代码</summary>\n\n```python\n{func.source_code}\n```\n</details>\n\n'
for cls in module_info.classes:
if cls.inherit:
inherit = f"({', '.join(cls.inherit)})" if cls.inherit else ''
content += f'### ***class*** `{cls.name}{inherit}`\n\n'
else:
content += f'### ***class*** `{cls.name}`\n\n'
cls.docstring = cls.docstring.replace('\n', '\n\n')
content += f'{cls.docstring}\n\n'
for method in cls.methods:
if method.type != DefType.METHOD:
args_with_type = [f'{arg[0]}: {arg[1]}' if arg[1] else arg[0] for arg in method.args]
content += f'### &emsp; ***@{method.type.value}***\n'
else:
args_with_type = [f'{arg[0]}: {arg[1]}' if arg[1] and arg[0] != 'self' else arg[0] for arg in method.args]
content += f"### &emsp; ***{('async ' if method.is_async else '')}def*** `{method.name}({', '.join(args_with_type)}) -> {method.return_type}`\n\n"
method.docstring = method.docstring.replace('\n', '\n\n')
content += f'&emsp;{method.docstring}\n\n'
content += f'<details>\n<summary>源代码</summary>\n\n```python\n{method.source_code}\n```\n</details>\n\n'
for attr in cls.attributes:
content += f'### &emsp; ***attr*** `{attr.name}: {attr.type}`\n\n'
for attr in module_info.attributes:
if attr.type == NO_TYPE_HINT:
content += f'### ***var*** `{attr.name} = {attr.value}`\n\n'
else:
content += f'### ***var*** `{attr.name}: {attr.type} = {attr.value}`\n\n'
attr.docstring = attr.docstring.replace('\n', '\n\n')
content += f'{attr.docstring}\n\n'
return content
```
</details>
### ***def*** `generate_docs(module_folder: str, output_dir: str, with_top: bool, ignored_paths: Any) -> None`
生成文档
Args:
module_folder: 模块文件夹
output_dir: 输出文件夹
with_top: 是否包含顶层文件夹 False时例如docs/api/module_a, docs/api/module_b True时例如docs/api/module/module_a.md docs/api/module/module_b.md
ignored_paths: 忽略的路径
<details>
<summary>源代码</summary>
```python
def generate_docs(module_folder: str, output_dir: str, with_top: bool=False, ignored_paths=None):
"""
生成文档
Args:
module_folder: 模块文件夹
output_dir: 输出文件夹
with_top: 是否包含顶层文件夹 False时例如docs/api/module_a, docs/api/module_b True时例如docs/api/module/module_a.md docs/api/module/module_b.md
ignored_paths: 忽略的路径
"""
if ignored_paths is None:
ignored_paths = []
file_data: dict[str, str] = {}
file_list = get_file_list(module_folder)
shutil.rmtree(output_dir, ignore_errors=True)
os.mkdir(output_dir)
replace_data = {'__init__': 'README', '.py': '.md'}
for pyfile_path in file_list:
if any((ignored_path.replace('\\', '/') in pyfile_path.replace('\\', '/') for ignored_path in ignored_paths)):
continue
no_module_name_pyfile_path = get_relative_path(module_folder, pyfile_path)
rel_md_path = pyfile_path if with_top else no_module_name_pyfile_path
for rk, rv in replace_data.items():
rel_md_path = rel_md_path.replace(rk, rv)
abs_md_path = os.path.join(output_dir, rel_md_path)
module_info = get_module_info_normal(pyfile_path)
if 'README' in abs_md_path:
front_matter = {'title': module_info.module_path.replace('.__init__', '').replace('_', '\\n'), 'index': 'true', 'icon': 'laptop-code', 'category': 'API'}
else:
front_matter = {'title': module_info.module_path.replace('_', '\\n'), 'order': '1', 'icon': 'laptop-code', 'category': 'API'}
md_content = generate_markdown(module_info, front_matter)
print(f'Generate {pyfile_path} -> {abs_md_path}')
file_data[abs_md_path] = md_content
write_to_files(file_data)
```
</details>
### ***class*** `DefType(Enum)`
### &emsp; ***attr*** `FUNCTION: 'function'`
### &emsp; ***attr*** `METHOD: 'method'`
### &emsp; ***attr*** `STATIC_METHOD: 'staticmethod'`
### &emsp; ***attr*** `CLASS_METHOD: 'classmethod'`
### &emsp; ***attr*** `PROPERTY: 'property'`
### ***class*** `FunctionInfo(BaseModel)`
### ***class*** `AttributeInfo(BaseModel)`
### ***class*** `ClassInfo(BaseModel)`
### ***class*** `ModuleInfo(BaseModel)`
### ***var*** `NO_TYPE_ANY = 'Any'`
### ***var*** `NO_TYPE_HINT = 'NoTypeHint'`
### ***var*** `FUNCTION = 'function'`
### ***var*** `METHOD = 'method'`
### ***var*** `STATIC_METHOD = 'staticmethod'`
### ***var*** `CLASS_METHOD = 'classmethod'`
### ***var*** `PROPERTY = 'property'`
### ***var*** `file_list = []`
### ***var*** `dot_sep_module_path = file_path.replace(os.sep, '.').replace('.py', '').replace('.pyi', '')`
### ***var*** `module_docstring = ast.get_docstring(tree)`
### ***var*** `module_info = ModuleInfo(module_path=dot_sep_module_path, functions=[], classes=[], attributes=[], docstring=module_docstring if module_docstring else '')`
### ***var*** `content = ''`
### ***var*** `front_matter = '---\n' + '\n'.join([f'{k}: {v}' for k, v in front_matter.items()]) + '\n---\n\n'`
### ***var*** `file_list = get_file_list(module_folder)`
### ***var*** `replace_data = {'__init__': 'README', '.py': '.md'}`
### ***var*** `file_content = file.read()`
### ***var*** `tree = ast.parse(file_content)`
### ***var*** `args_with_type = [f'{arg[0]}: {arg[1]}' if arg[1] else arg[0] for arg in func.args]`
### ***var*** `ignored_paths = []`
### ***var*** `no_module_name_pyfile_path = get_relative_path(module_folder, pyfile_path)`
### ***var*** `rel_md_path = pyfile_path if with_top else no_module_name_pyfile_path`
### ***var*** `abs_md_path = os.path.join(output_dir, rel_md_path)`
### ***var*** `module_info = get_module_info_normal(pyfile_path)`
### ***var*** `md_content = generate_markdown(module_info, front_matter)`
### ***var*** `inherit = f"({', '.join(cls.inherit)})" if cls.inherit else ''`
### ***var*** `rel_md_path = rel_md_path.replace(rk, rv)`
### ***var*** `front_matter = {'title': module_info.module_path.replace('.__init__', '').replace('_', '\\n'), 'index': 'true', 'icon': 'laptop-code', 'category': 'API'}`
### ***var*** `front_matter = {'title': module_info.module_path.replace('_', '\\n'), 'order': '1', 'icon': 'laptop-code', 'category': 'API'}`
### ***var*** `function_docstring = ast.get_docstring(node)`
### ***var*** `func_info = FunctionInfo(name=node.name, args=[(arg.arg, ast.unparse(arg.annotation) if arg.annotation else NO_TYPE_ANY) for arg in node.args.args], return_type=ast.unparse(node.returns) if node.returns else 'None', docstring=function_docstring if function_docstring else '', type=DefType.FUNCTION, is_async=isinstance(node, ast.AsyncFunctionDef), source_code=ast.unparse(node))`
### ***var*** `class_docstring = ast.get_docstring(node)`
### ***var*** `class_info = ClassInfo(name=node.name, docstring=class_docstring if class_docstring else '', methods=[], attributes=[], inherit=[ast.unparse(base) for base in node.bases])`
### ***var*** `args_with_type = [f'{arg[0]}: {arg[1]}' if arg[1] else arg[0] for arg in method.args]`
### ***var*** `args_with_type = [f'{arg[0]}: {arg[1]}' if arg[1] and arg[0] != 'self' else arg[0] for arg in method.args]`
### ***var*** `first_arg = node.args.args[0]`
### ***var*** `method_docstring = ast.get_docstring(class_node)`
### ***var*** `def_type = DefType.METHOD`
### ***var*** `def_type = DefType.STATIC_METHOD`
### ***var*** `attr_type = NO_TYPE_HINT`
### ***var*** `def_type = DefType.CLASS_METHOD`
### ***var*** `attr_type = ast.unparse(node.value.annotation)`
### ***var*** `def_type = DefType.PROPERTY`

View File

@ -0,0 +1,29 @@
---
title: liteyuki.plugin
index: true
icon: laptop-code
category: API
---
### ***def*** `get_loaded_plugins() -> dict[str, Plugin]`
获取已加载的插件
Returns:
dict[str, Plugin]: 插件字典
<details>
<summary>源代码</summary>
```python
def get_loaded_plugins() -> dict[str, Plugin]:
"""
获取已加载的插件
Returns:
dict[str, Plugin]: 插件字典
"""
return _plugins
```
</details>

View File

@ -0,0 +1,199 @@
---
title: liteyuki.plugin.load
order: 1
icon: laptop-code
category: API
---
### ***def*** `load_plugin(module_path: str | Path) -> Optional[Plugin]`
加载单个插件,可以是本地插件或是通过 `pip` 安装的插件。
参数:
module_path: 插件名称 `path.to.your.plugin`
或插件路径 `pathlib.Path(path/to/your/plugin)`
<details>
<summary>源代码</summary>
```python
def load_plugin(module_path: str | Path) -> Optional[Plugin]:
"""加载单个插件,可以是本地插件或是通过 `pip` 安装的插件。
参数:
module_path: 插件名称 `path.to.your.plugin`
或插件路径 `pathlib.Path(path/to/your/plugin)`
"""
module_path = path_to_module_name(Path(module_path)) if isinstance(module_path, Path) else module_path
try:
module = import_module(module_path)
_plugins[module.__name__] = Plugin(name=module.__name__, module=module, module_name=module_path, metadata=module.__dict__.get('__plugin_metadata__', None))
display_name = module.__name__.split('.')[-1]
if module.__dict__.get('__plugin_meta__'):
metadata: 'PluginMetadata' = module.__dict__['__plugin_meta__']
display_name = format_display_name(f"{metadata.name}({module.__name__.split('.')[-1]})", metadata.type)
logger.opt(colors=True).success(f'Succeeded to load liteyuki plugin "{display_name}"')
return _plugins[module.__name__]
except Exception as e:
logger.opt(colors=True).success(f'Failed to load liteyuki plugin "<r>{module_path}</r>"')
traceback.print_exc()
return None
```
</details>
### ***def*** `load_plugins() -> set[Plugin]`
导入文件夹下多个插件
参数:
plugin_dir: 文件夹路径
ignore_warning: 是否忽略警告,通常是目录不存在或目录为空
<details>
<summary>源代码</summary>
```python
def load_plugins(*plugin_dir: str, ignore_warning: bool=True) -> set[Plugin]:
"""导入文件夹下多个插件
参数:
plugin_dir: 文件夹路径
ignore_warning: 是否忽略警告,通常是目录不存在或目录为空
"""
plugins = set()
for dir_path in plugin_dir:
if not os.path.exists(dir_path):
if not ignore_warning:
logger.warning(f"Plugins dir '{dir_path}' does not exist.")
continue
if not os.listdir(dir_path):
if not ignore_warning:
logger.warning(f"Plugins dir '{dir_path}' is empty.")
continue
if not os.path.isdir(dir_path):
if not ignore_warning:
logger.warning(f"Plugins dir '{dir_path}' is not a directory.")
continue
for f in os.listdir(dir_path):
path = Path(os.path.join(dir_path, f))
module_name = None
if os.path.isfile(path) and f.endswith('.py') and (f != '__init__.py'):
module_name = f'{path_to_module_name(Path(dir_path))}.{f[:-3]}'
elif os.path.isdir(path) and os.path.exists(os.path.join(path, '__init__.py')):
module_name = path_to_module_name(path)
if module_name:
load_plugin(module_name)
if _plugins.get(module_name):
plugins.add(_plugins[module_name])
return plugins
```
</details>
### ***def*** `format_display_name(display_name: str, plugin_type: PluginType) -> str`
设置插件名称颜色,根据不同类型插件设置颜色
Args:
display_name: 插件名称
plugin_type: 插件类型
Returns:
str: 设置后的插件名称 <y>name</y>
<details>
<summary>源代码</summary>
```python
def format_display_name(display_name: str, plugin_type: PluginType) -> str:
"""
设置插件名称颜色,根据不同类型插件设置颜色
Args:
display_name: 插件名称
plugin_type: 插件类型
Returns:
str: 设置后的插件名称 <y>name</y>
"""
color = 'y'
match plugin_type:
case PluginType.APPLICATION:
color = 'm'
case PluginType.TEST:
color = 'g'
case PluginType.MODULE:
color = 'e'
case PluginType.SERVICE:
color = 'c'
return f'<{color}>{display_name} [{plugin_type.name}]</{color}>'
```
</details>
### ***var*** `module_path = path_to_module_name(Path(module_path)) if isinstance(module_path, Path) else module_path`
### ***var*** `plugins = set()`
### ***var*** `color = 'y'`
### ***var*** `module = import_module(module_path)`
### ***var*** `display_name = module.__name__.split('.')[-1]`
### ***var*** `display_name = format_display_name(f"{metadata.name}({module.__name__.split('.')[-1]})", metadata.type)`
### ***var*** `path = Path(os.path.join(dir_path, f))`
### ***var*** `module_name = None`
### ***var*** `color = 'm'`
### ***var*** `color = 'g'`
### ***var*** `color = 'e'`
### ***var*** `color = 'c'`
### ***var*** `module_name = f'{path_to_module_name(Path(dir_path))}.{f[:-3]}'`
### ***var*** `module_name = path_to_module_name(path)`

View File

@ -0,0 +1,7 @@
---
title: liteyuki.plugin.manager
order: 1
icon: laptop-code
category: API
---

View File

@ -0,0 +1,89 @@
---
title: liteyuki.plugin.model
order: 1
icon: laptop-code
category: API
---
### ***class*** `PluginType(Enum)`
插件类型枚举值
### &emsp; ***attr*** `APPLICATION: 'application'`
### &emsp; ***attr*** `SERVICE: 'service'`
### &emsp; ***attr*** `MODULE: 'module'`
### &emsp; ***attr*** `UNCLASSIFIED: 'unclassified'`
### &emsp; ***attr*** `TEST: 'test'`
### ***class*** `PluginMetadata(BaseModel)`
轻雪插件元数据由插件编写者提供name为必填项
Attributes:
----------
name: str
插件名称
description: str
插件描述
usage: str
插件使用方法
type: str
插件类型
author: str
插件作者
homepage: str
插件主页
extra: dict[str, Any]
额外信息
### ***class*** `Plugin(BaseModel)`
存储插件信息
### &emsp; ***attr*** `model_config: {'arbitrary_types_allowed': True}`
### ***var*** `APPLICATION = 'application'`
### ***var*** `SERVICE = 'service'`
### ***var*** `MODULE = 'module'`
### ***var*** `UNCLASSIFIED = 'unclassified'`
### ***var*** `TEST = 'test'`
### ***var*** `model_config = {'arbitrary_types_allowed': True}`

180
docs/en/dev/api/utils.md Normal file
View File

@ -0,0 +1,180 @@
---
title: liteyuki.utils
order: 1
icon: laptop-code
category: API
---
### ***def*** `is_coroutine_callable(call: Callable[..., Any]) -> bool`
判断是否为协程可调用对象
Args:
call: 可调用对象
Returns:
bool: 是否为协程可调用对象
<details>
<summary>源代码</summary>
```python
def is_coroutine_callable(call: Callable[..., Any]) -> bool:
"""
判断是否为协程可调用对象
Args:
call: 可调用对象
Returns:
bool: 是否为协程可调用对象
"""
if inspect.isroutine(call):
return inspect.iscoroutinefunction(call)
if inspect.isclass(call):
return False
func_ = getattr(call, '__call__', None)
return inspect.iscoroutinefunction(func_)
```
</details>
### ***def*** `run_coroutine() -> None`
运行协程
Args:
coro:
Returns:
<details>
<summary>源代码</summary>
```python
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:
logger.error(f'Exception occurred: {e}')
```
</details>
### ***def*** `path_to_module_name(path: Path) -> str`
转换路径为模块名
Args:
path: 路径a/b/c/d -> a.b.c.d
Returns:
str: 模块名
<details>
<summary>源代码</summary>
```python
def path_to_module_name(path: Path) -> str:
"""
转换路径为模块名
Args:
path: 路径a/b/c/d -> a.b.c.d
Returns:
str: 模块名
"""
rel_path = path.resolve().relative_to(Path.cwd().resolve())
if rel_path.stem == '__init__':
return '.'.join(rel_path.parts[:-1])
else:
return '.'.join(rel_path.parts[:-1] + (rel_path.stem,))
```
</details>
### ***def*** `async_wrapper(func: Callable[..., Any]) -> Callable[..., Coroutine]`
异步包装器
Args:
func: Sync Callable
Returns:
Coroutine: Asynchronous Callable
<details>
<summary>源代码</summary>
```python
def async_wrapper(func: Callable[..., Any]) -> Callable[..., Coroutine]:
"""
异步包装器
Args:
func: Sync Callable
Returns:
Coroutine: Asynchronous Callable
"""
async def wrapper(*args, **kwargs):
return func(*args, **kwargs)
wrapper.__signature__ = inspect.signature(func)
return wrapper
```
</details>
### ***async def*** `wrapper() -> None`
<details>
<summary>源代码</summary>
```python
async def wrapper(*args, **kwargs):
return func(*args, **kwargs)
```
</details>
### ***var*** `IS_MAIN_PROCESS = multiprocessing.current_process().name == 'MainProcess'`
### ***var*** `func_ = getattr(call, '__call__', None)`
### ***var*** `rel_path = path.resolve().relative_to(Path.cwd().resolve())`
### ***var*** `loop = asyncio.get_event_loop()`
### ***var*** `loop = asyncio.new_event_loop()`

View File

@ -33,7 +33,9 @@ __all__ = [
"logger",
]
__version__ = "6.3.8" # 测试版本号
__version__ = "6.3.9" # 测试版本号
# 6.3.9
# 更改了on语法
# 6.3.8
# 1. 初步添加对聊天的支持

View File

@ -38,7 +38,7 @@ class Channel(Generic[T]):
有两种接收工作方式,但是只能选择一种,主动接收和被动接收,主动接收使用 `receive` 方法,被动接收使用 `on_receive` 装饰器
"""
def __init__(self, _id: str, type_check: Optional[bool] = None):
def __init__(self, _id: str = "", type_check: Optional[bool] = None):
"""
初始化通道
Args:
@ -217,16 +217,6 @@ class Channel(Generic[T]):
data = self.conn_recv.recv()
self._run_on_sub_receive_funcs(data)
def __iter__(self):
return self
def __next__(self) -> Any:
return self.receive()
def __del__(self):
self.close()
logger.debug(f"Channel {self.name} deleted.")
"""子进程可用的主动和被动通道"""
active_channel: Optional["Channel"] = None

View File

@ -8,7 +8,7 @@ from typing import Any, Coroutine, Optional, TypeAlias, Callable
from liteyuki.comm import channel
from liteyuki.comm.channel import Channel, ON_RECEIVE_FUNC, ASYNC_ON_RECEIVE_FUNC
from liteyuki.utils import IS_MAIN_PROCESS, is_coroutine_callable, run_coroutine
from liteyuki.utils import IS_MAIN_PROCESS, is_coroutine_callable, run_coroutine, run_coroutine_in_thread
if IS_MAIN_PROCESS:
_locks = {}
@ -220,10 +220,10 @@ class KeyValueStore:
"""
if IS_MAIN_PROCESS:
if channel_ in _on_main_subscriber_receive_funcs and _on_main_subscriber_receive_funcs[channel_]:
run_coroutine(*[func(data) for func in _on_main_subscriber_receive_funcs[channel_]])
run_coroutine_in_thread(*[func(data) for func in _on_main_subscriber_receive_funcs[channel_]])
else:
if channel_ in _on_sub_subscriber_receive_funcs and _on_sub_subscriber_receive_funcs[channel_]:
run_coroutine(*[func(data) for func in _on_sub_subscriber_receive_funcs[channel_]])
run_coroutine_in_thread(*[func(data) for func in _on_sub_subscriber_receive_funcs[channel_]])
def _start_receive_loop(self):
"""

View File

@ -8,32 +8,57 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@File : event.py
@Software: PyCharm
"""
from typing import Any
from typing import Any, Optional
from liteyuki.comm.storage import shared_memory
class Event:
def __init__(self, type: str, data: dict[str, Any], bot_id: str, session_id: str, session_type: str, receive_channel: str = "event_to_nonebot"):
class MessageEvent:
def __init__(
self,
bot_id: str,
message: list[dict[str, Any]] | str,
message_type: str,
raw_message: str,
session_id: str,
session_type: str,
receive_channel: str,
data: Optional[dict[str, Any]] = None,
):
"""
事件
轻雪抽象消息事件
Args:
type: 类型
data: 数据
bot_id: 机器人ID
session_id: 会话ID
session_type: 会话类型
receive_channel: 接收频道
message: 消息,消息段数组[{type: str, data: dict[str, Any]}]
raw_message: 原始消息(通常为纯文本的格式)
message_type: 消息类型(private, group, other)
session_id: 会话ID(私聊通常为用户ID群聊通常为群ID)
session_type: 会话类型(private, group)
receive_channel: 接收频道(用于回复消息)
data: 附加数据
"""
self.type = type
if data is None:
data = {}
self.message_type = message_type
self.data = data
self.bot_id = bot_id
self.message = message
self.raw_message = raw_message
self.session_id = session_id
self.session_type = session_type
self.receive_channel = receive_channel
def __str__(self):
return f"Event(type={self.type}, data={self.data}, bot_id={self.bot_id}, session_id={self.session_id}, session_type={self.session_type})"
return (f"Event(message_type={self.message_type}, data={self.data}, bot_id={self.bot_id}, "
f"session_id={self.session_id}, session_type={self.session_type})")
def reply(self, message: str | dict[str, Any]):
"""
@ -42,8 +67,10 @@ class Event:
message:
Returns:
"""
to_nonebot_event = Event(
type=self.session_type,
reply_event = MessageEvent(
message_type=self.session_type,
message=message,
raw_message="",
data={
"message": message
},
@ -52,5 +79,4 @@ class Event:
session_type=self.session_type,
receive_channel="_"
)
print(to_nonebot_event)
shared_memory.publish(self.receive_channel, to_nonebot_event)
shared_memory.publish(self.receive_channel, reply_event)

View File

@ -11,10 +11,10 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
import traceback
from typing import Any, TypeAlias, Callable, Coroutine
from liteyuki import Event
from liteyuki.message.event import MessageEvent
from liteyuki.message.rule import Rule
EventHandler: TypeAlias = Callable[[Event], Coroutine[None, None, Any]]
EventHandler: TypeAlias = Callable[[MessageEvent], Coroutine[None, None, Any]]
class Matcher:
@ -34,18 +34,19 @@ class Matcher:
def __str__(self):
return f"Matcher(rule={self.rule}, priority={self.priority}, block={self.block})"
def handle(self, handler: EventHandler) -> EventHandler:
def handle(self) -> Callable[[EventHandler], EventHandler]:
"""
添加处理函数,装饰器
Args:
handler:
Returns:
EventHandler
装饰器 handler
"""
self.handlers.append(handler)
return handler
def decorator(handler: EventHandler) -> EventHandler:
self.handlers.append(handler)
return handler
async def run(self, event: Event) -> None:
return decorator
async def run(self, event: MessageEvent) -> None:
"""
运行处理函数
Args:

View File

@ -8,22 +8,21 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@File : on.py
@Software: PyCharm
"""
import threading
from queue import Queue
from liteyuki.comm.storage import shared_memory
from liteyuki.log import logger
from liteyuki.message.event import Event
from liteyuki.message.event import MessageEvent
from liteyuki.message.matcher import Matcher
from liteyuki.message.rule import Rule
from liteyuki.message.rule import Rule, empty_rule
_matcher_list: list[Matcher] = []
_queue: Queue = Queue()
@shared_memory.on_subscriber_receive("event_to_liteyuki")
async def _(event: Event):
async def _(event: MessageEvent):
current_priority = -1
for i, matcher in enumerate(_matcher_list):
logger.info(f"Running matcher {matcher} for event: {event}")
@ -35,7 +34,7 @@ async def _(event: Event):
break
def on_message(rule: Rule = Rule(), priority: int = 0, block: bool = True) -> Matcher:
def on_message(rule: Rule = empty_rule, priority: int = 0, block: bool = False) -> Matcher:
matcher = Matcher(rule, priority, block)
# 按照优先级插入
for i, m in enumerate(_matcher_list):
@ -45,3 +44,10 @@ def on_message(rule: Rule = Rule(), priority: int = 0, block: bool = True) -> Ma
else:
_matcher_list.append(matcher)
return matcher
def on_keywords(keywords: list[str], rule=empty_rule, priority: int = 0, block: bool = False) -> Matcher:
@Rule
async def on_keywords_rule(event: MessageEvent):
return any(keyword in event.raw_message for keyword in keywords)
return on_message(on_keywords_rule & rule, priority, block)

View File

@ -8,26 +8,37 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@File : rule.py
@Software: PyCharm
"""
import inspect
from typing import Optional, TypeAlias, Callable, Coroutine
from liteyuki.message.event import Event
from liteyuki.message.event import MessageEvent
RuleHandler: TypeAlias = Callable[[Event], Coroutine[None, None, bool]]
RuleHandlerFunc: TypeAlias = Callable[[MessageEvent], Coroutine[None, None, bool]]
"""规则函数签名"""
class Rule:
def __init__(self, handler: Optional[RuleHandler] = None):
def __init__(self, handler: RuleHandlerFunc):
self.handler = handler
def __or__(self, other: "Rule") -> "Rule":
return Rule(lambda event: self.handler(event) or other.handler(event))
async def combined_handler(event: MessageEvent) -> bool:
return await self.handler(event) or await other.handler(event)
return Rule(combined_handler)
def __and__(self, other: "Rule") -> "Rule":
return Rule(lambda event: self.handler(event) and other.handler(event))
async def combined_handler(event: MessageEvent) -> bool:
return await self.handler(event) and await other.handler(event)
async def __call__(self, event: Event) -> bool:
return Rule(combined_handler)
async def __call__(self, event: MessageEvent) -> bool:
if self.handler is None:
return True
return await self.handler(event)
@Rule
async def empty_rule(event: MessageEvent) -> bool:
return True

View File

@ -33,6 +33,7 @@ class FunctionInfo(BaseModel):
args: list[tuple[str, str]]
return_type: str
docstring: str
source_code: str = ""
type: DefType
"""若为类中def则有"""
@ -142,7 +143,8 @@ def get_module_info_normal(file_path: str, ignore_private: bool = True) -> Modul
return_type=ast.unparse(node.returns) if node.returns else "None",
docstring=function_docstring if function_docstring else "",
type=DefType.FUNCTION,
is_async=isinstance(node, ast.AsyncFunctionDef)
is_async=isinstance(node, ast.AsyncFunctionDef),
source_code=ast.unparse(node)
)
module_info.functions.append(func_info)
@ -176,7 +178,8 @@ def get_module_info_normal(file_path: str, ignore_private: bool = True) -> Modul
return_type=ast.unparse(class_node.returns) if class_node.returns else "None",
docstring=method_docstring if method_docstring else "",
type=def_type,
is_async=isinstance(class_node, ast.AsyncFunctionDef)
is_async=isinstance(class_node, ast.AsyncFunctionDef),
source_code=ast.unparse(class_node)
))
# attributes
elif isinstance(class_node, ast.Assign):
@ -206,13 +209,14 @@ def get_module_info_normal(file_path: str, ignore_private: bool = True) -> Modul
return module_info
def generate_markdown(module_info: ModuleInfo, front_matter=None) -> str:
def generate_markdown(module_info: ModuleInfo, front_matter=None, lang: str = "zh-CN") -> str:
"""
生成模块的Markdown
你可在此自定义生成的Markdown格式
Args:
module_info: 模块信息
front_matter: 自定义选项title, index, icon, category
lang: 语言
Returns:
Markdown 字符串
"""
@ -223,8 +227,6 @@ def generate_markdown(module_info: ModuleInfo, front_matter=None) -> str:
content += front_matter
# 模块函数
for func in module_info.functions:
args_with_type = [f"{arg[0]}: {arg[1]}" if arg[1] else arg[0] for arg in func.args]
@ -233,6 +235,9 @@ def generate_markdown(module_info: ModuleInfo, front_matter=None) -> str:
func.docstring = func.docstring.replace("\n", "\n\n")
content += f"{func.docstring}\n\n"
# 函数源代码可展开区域
content += f"<details>\n<summary>源代码</summary>\n\n```python\n{func.source_code}\n```\n</details>\n\n"
# 类
for cls in module_info.classes:
if cls.inherit:
@ -256,6 +261,14 @@ def generate_markdown(module_info: ModuleInfo, front_matter=None) -> str:
method.docstring = method.docstring.replace("\n", "\n\n")
content += f"&emsp;{method.docstring}\n\n"
# 函数源代码可展开区域
if lang == "zh-CN":
TEXT_SOURCE_CODE = "源代码"
else:
TEXT_SOURCE_CODE = "Source Code"
content += f"<details>\n<summary>{TEXT_SOURCE_CODE}</summary>\n\n```python\n{method.source_code}\n```\n</details>\n\n"
for attr in cls.attributes:
content += f"### &emsp; ***attr*** `{attr.name}: {attr.type}`\n\n"
@ -272,7 +285,7 @@ def generate_markdown(module_info: ModuleInfo, front_matter=None) -> str:
return content
def generate_docs(module_folder: str, output_dir: str, with_top: bool = False, ignored_paths=None):
def generate_docs(module_folder: str, output_dir: str, with_top: bool = False, lang: str = "zh-CN", ignored_paths=None):
"""
生成文档
Args:
@ -280,6 +293,7 @@ def generate_docs(module_folder: str, output_dir: str, with_top: bool = False, i
output_dir: 输出文件夹
with_top: 是否包含顶层文件夹 False时例如docs/api/module_a, docs/api/module_b True时例如docs/api/module/module_a.md docs/api/module/module_b.md
ignored_paths: 忽略的路径
lang: 语言
"""
if ignored_paths is None:
ignored_paths = []
@ -339,5 +353,5 @@ def generate_docs(module_folder: str, output_dir: str, with_top: bool = False, i
# 入口脚本
if __name__ == '__main__':
# 这里填入你的模块路径
generate_docs('liteyuki', 'docs/dev/api', with_top=False, ignored_paths=["liteyuki/plugins"])
generate_docs('liteyuki', 'docs/en/dev/api', with_top=False, ignored_paths=["liteyuki/plugins"])
generate_docs('liteyuki', 'docs/dev/api', with_top=False, ignored_paths=["liteyuki/plugins"], lang="zh-CN")
generate_docs('liteyuki', 'docs/en/dev/api', with_top=False, ignored_paths=["liteyuki/plugins"], lang="en")

View File

@ -5,6 +5,7 @@
import asyncio
import inspect
import multiprocessing
import threading
from pathlib import Path
from typing import Any, Callable, Coroutine
@ -61,6 +62,16 @@ def run_coroutine(*coro: Coroutine):
# 捕获其他异常,防止协程被重复等待
logger.error(f"Exception occurred: {e}")
def run_coroutine_in_thread(*coro: Coroutine):
"""
在新线程中运行协程
Args:
coro:
Returns:
"""
threading.Thread(target=run_coroutine, args=coro, daemon=True).start()
def path_to_module_name(path: Path) -> str:
"""

View File

@ -25,7 +25,9 @@ from ..utils.base.ly_function import get_function
require("nonebot_plugin_alconna")
require("nonebot_plugin_apscheduler")
from nonebot_plugin_alconna import on_alconna, Alconna, Args, Arparma, MultiVar
require("nonebot_plugin_htmlrender")
from nonebot_plugin_htmlrender import md_to_pic
from nonebot_plugin_alconna import UniMessage, on_alconna, Alconna, Args, Arparma, MultiVar
from nonebot_plugin_apscheduler import scheduler
driver = get_driver()
@ -54,7 +56,7 @@ async def _(bot: T_Bot, matcher: Matcher, result: Arparma):
permission=SUPERUSER
).handle()
# Satori OK
async def _(bot: T_Bot, event: T_MessageEvent):
async def _(bot: T_Bot, event: T_MessageEvent, matcher: Matcher):
# 使用git pull更新
ulang = get_user_lang(str(event.user.id if isinstance(event, satori.event.Event) else event.user_id))
@ -64,7 +66,9 @@ async def _(bot: T_Bot, event: T_MessageEvent):
btn_restart = md.btn_cmd(ulang.get("liteyuki.restart_now"), "reload-liteyuki")
pip.main(["install", "-r", "requirements.txt"])
reply += f"{ulang.get('liteyuki.update_restart', RESTART=btn_restart)}"
await md.send_md(reply, bot, event=event, at_sender=False)
# await md.send_md(reply, bot)
img_bytes = await md_to_pic(reply)
await UniMessage.send(UniMessage.image(raw=img_bytes))
@on_alconna(

View File

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Time : 2024/8/22 上午9:06
@Author : snowykami
@Email : snowykami@outlook.com
@File : anti_dislink.py
@Software: PyCharm
"""
import random
from liteyuki.plugin import PluginMetadata, PluginType
from liteyuki.message.on import on_keywords
__plugin_meta__ = PluginMetadata(
name="严禁断联化",
type=PluginType.APPLICATION
)
@on_keywords(["看看你的", "看看j", "给我看看"]).handle()
async def _(event):
event.reply(random.choice(["No dislink", "严禁断联化"]))

View File

@ -10,15 +10,15 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
"""
from liteyuki.plugin import PluginMetadata, PluginType
from liteyuki.message.on import on_message
from liteyuki.message.event import Event
from liteyuki.message.event import MessageEvent
__plugin_meta__ = PluginMetadata(
name="你好轻雪",
type=PluginType.TEST
type=PluginType.APPLICATION
)
@on_message().handle
async def _(event: Event):
if str(event.data["raw_message"]) == "你好轻雪":
@on_message().handle()
async def _(event: MessageEvent):
if str(event.raw_message) == "你好轻雪":
event.reply("你好呀")

View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Time : 2024/8/22 上午8:37
@Author : snowykami
@Email : snowykami@outlook.com
@File : ts_chan_main.py
@Software: PyCharm
"""
import asyncio
from liteyuki.comm import Channel, set_channel, get_channel
from liteyuki import get_bot
set_channel("chan-main", Channel("chan-main"))
set_channel("chan-sub", Channel("chan-sub"))
chan_main = get_channel("chan-main")
# @get_bot().on_after_start
# async def _():
# while True:
# chan_main.send("Hello, World!")
# await asyncio.sleep(5)

View File

@ -1,125 +0,0 @@
import nonebot
from nonebot import on_message, require
from nonebot.plugin import PluginMetadata
from src.utils.base.data import Database, LiteModel
from src.utils.base.ly_typing import T_Bot, T_MessageEvent
from src.utils.message.message import MarkdownMessage as md
require("nonebot_plugin_alconna")
from nonebot_plugin_alconna import on_alconna
from arclet.alconna import Arparma, Alconna, Args, Option, Subcommand
class Node(LiteModel):
TABLE_NAME: str = "node"
bot_id: str = ""
session_type: str = ""
session_id: str = ""
def __str__(self):
return f"{self.bot_id}.{self.session_type}.{self.session_id}"
class Push(LiteModel):
TABLE_NAME: str = "push"
source: Node = Node()
target: Node = Node()
inde: int = 0
pushes_db = Database("data/pushes.ldb")
pushes_db.auto_migrate(Push(), Node())
alc = Alconna(
"lep",
Subcommand(
"add",
Args["source", str],
Args["target", str],
Option("bidirectional", Args["bidirectional", bool])
),
Subcommand(
"rm",
Args["index", int],
),
Subcommand(
"list",
)
)
add_push = on_alconna(alc)
@add_push.handle()
async def _(result: Arparma):
"""bot_id.session_type.session_id"""
if result.subcommands.get("add"):
source = result.subcommands["add"].args.get("source")
target = result.subcommands["add"].args.get("target")
if source and target:
source = source.split(".")
target = target.split(".")
push1 = Push(
source=Node(bot_id=source[0], session_type=source[1], session_id=source[2]),
target=Node(bot_id=target[0], session_type=target[1], session_id=target[2]),
inde=len(pushes_db.where_all(Push(), default=[]))
)
pushes_db.save(push1)
if result.subcommands["add"].args.get("bidirectional"):
push2 = Push(
source=Node(bot_id=target[0], session_type=target[1], session_id=target[2]),
target=Node(bot_id=source[0], session_type=source[1], session_id=source[2]),
inde=len(pushes_db.where_all(Push(), default=[]))
)
pushes_db.save(push2)
await add_push.finish("添加成功")
else:
await add_push.finish("参数缺失")
elif result.subcommands.get("rm"):
index = result.subcommands["rm"].args.get("index")
if index is not None:
try:
pushes_db.delete(Push(), "inde = ?", index)
await add_push.finish("删除成功")
except IndexError:
await add_push.finish("索引错误")
else:
await add_push.finish("参数缺失")
elif result.subcommands.get("list"):
await add_push.finish(
"\n".join([f"{push.inde} {push.source.bot_id}.{push.source.session_type}.{push.source.session_id} -> "
f"{push.target.bot_id}.{push.target.session_type}.{push.target.session_id}" for i, push in
enumerate(pushes_db.where_all(Push(), default=[]))]))
else:
await add_push.finish("参数错误")
@on_message(block=False).handle()
async def _(event: T_MessageEvent, bot: T_Bot):
for push in pushes_db.where_all(Push(), default=[]):
if str(push.source) == f"{bot.self_id}.{event.message_type}.{event.user_id if event.message_type == 'private' else event.group_id}":
bot2 = nonebot.get_bot(push.target.bot_id)
msg_formatted = ""
for line in str(event.message).split("\n"):
msg_formatted += f"**{line.strip()}**\n"
push_message = (
f"> From {event.sender.nickname}@{push.source.session_type}.{push.source.session_id}\n> Bot {bot.self_id}\n\n"
f"{msg_formatted}")
await md.send_md(push_message, bot2, message_type=push.target.session_type,
session_id=push.target.session_id)
return
__author__ = "snowykami"
__plugin_meta__ = PluginMetadata(
name="轻雪事件推送",
description="事件推送插件支持单向和双向推送支持跨Bot推送",
usage="",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki": True,
}
)

View File

@ -1,52 +0,0 @@
from nonebot import on_command, require
from nonebot.adapters.onebot.v11 import MessageSegment
from nonebot.params import CommandArg
from nonebot.permission import SUPERUSER
from nonebot.plugin import PluginMetadata
from src.utils.base.ly_typing import T_Bot, T_MessageEvent, v11
from src.utils.message.message import MarkdownMessage as md, broadcast_to_superusers
from src.utils.message.html_tool import *
md_test = on_command("mdts", permission=SUPERUSER)
btn_test = on_command("btnts", permission=SUPERUSER)
latex_test = on_command("latex", permission=SUPERUSER)
@md_test.handle()
async def _(bot: T_Bot, event: T_MessageEvent, arg: v11.Message = CommandArg()):
await md.send_md(
v11.utils.unescape(str(arg)),
bot,
message_type=event.message_type,
session_id=event.user_id if event.message_type == "private" else event.group_id
)
@btn_test.handle()
async def _(bot: T_Bot, event: T_MessageEvent, arg: v11.Message = CommandArg()):
await md.send_btn(
str(arg),
bot,
message_type=event.message_type,
session_id=event.user_id if event.message_type == "private" else event.group_id
)
@latex_test.handle()
async def _(bot: T_Bot, event: T_MessageEvent, arg: v11.Message = CommandArg()):
latex_text = f"$${v11.utils.unescape(str(arg))}$$"
img = await md_to_pic(latex_text)
await bot.send(event=event, message=MessageSegment.image(img))
__author__ = "snowykami"
__plugin_meta__ = PluginMetadata(
name="轻雪Markdown测试",
description="用于测试Markdown的插件",
usage="",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki": True,
}
)

View File

@ -1,15 +0,0 @@
from nonebot.plugin import PluginMetadata
from .minesweeper import *
__plugin_meta__ = PluginMetadata(
name="轻雪小游戏",
description="内置了一些小游戏",
usage="",
type="application",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki": True,
"toggleable" : True,
"default_enable" : True,
}
)

View File

@ -1,168 +0,0 @@
import random
from pydantic import BaseModel
from src.utils.message.message import MarkdownMessage as md
class Dot(BaseModel):
row: int
col: int
mask: bool = True
value: int = 0
flagged: bool = False
class Minesweeper:
# 0-8: number of mines around, 9: mine, -1: undefined
NUMS = "⓪①②③④⑤⑥⑦⑧🅑⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳"
MASK = "🅜"
FLAG = "🅕"
MINE = "🅑"
def __init__(self, rows, cols, num_mines, session_type, session_id):
assert rows > 0 and cols > 0 and 0 < num_mines < rows * cols
self.session_type = session_type
self.session_id = session_id
self.rows = rows
self.cols = cols
self.num_mines = num_mines
self.board: list[list[Dot]] = [[Dot(row=i, col=j) for j in range(cols)] for i in range(rows)]
self.is_first = True
def reveal(self, row, col) -> bool:
"""
展开
Args:
row:
col:
Returns:
游戏是否继续
"""
if self.is_first:
# 第一次展开,生成地雷
self.generate_board(self.board[row][col])
self.is_first = False
if self.board[row][col].value == 9:
self.board[row][col].mask = False
return False
if not self.board[row][col].mask:
return True
self.board[row][col].mask = False
if self.board[row][col].value == 0:
self.reveal_neighbors(row, col)
return True
def is_win(self) -> bool:
"""
是否胜利
Returns:
"""
for row in range(self.rows):
for col in range(self.cols):
if self.board[row][col].mask and self.board[row][col].value != 9:
return False
return True
def generate_board(self, first_dot: Dot):
"""
避开第一个点,生成地雷
Args:
first_dot: 第一个点
Returns:
"""
generate_count = 0
while generate_count < self.num_mines:
row = random.randint(0, self.rows - 1)
col = random.randint(0, self.cols - 1)
if self.board[row][col].value == 9 or (row, col) == (first_dot.row, first_dot.col):
continue
self.board[row][col] = Dot(row=row, col=col, mask=True, value=9)
generate_count += 1
for row in range(self.rows):
for col in range(self.cols):
if self.board[row][col].value != 9:
self.board[row][col].value = self.count_adjacent_mines(row, col)
def count_adjacent_mines(self, row, col):
"""
计算周围地雷数量
Args:
row:
col:
Returns:
"""
count = 0
for r in range(max(0, row - 1), min(self.rows, row + 2)):
for c in range(max(0, col - 1), min(self.cols, col + 2)):
if self.board[r][c].value == 9:
count += 1
return count
def reveal_neighbors(self, row, col):
"""
递归展开,使用深度优先搜索
Args:
row:
col:
Returns:
"""
for r in range(max(0, row - 1), min(self.rows, row + 2)):
for c in range(max(0, col - 1), min(self.cols, col + 2)):
if self.board[r][c].mask:
self.board[r][c].mask = False
if self.board[r][c].value == 0:
self.reveal_neighbors(r, c)
def mark(self, row, col) -> bool:
"""
标记
Args:
row:
col:
Returns:
是否标记成功,如果已经展开则无法标记
"""
if self.board[row][col].mask:
self.board[row][col].flagged = not self.board[row][col].flagged
return self.board[row][col].flagged
def board_markdown(self) -> str:
"""
打印地雷板
Returns:
"""
dis = " "
start = "> " if self.cols >= 10 else ""
text = start + self.NUMS[0] + dis*2
# 横向两个雷之间的间隔字符
# 生成横向索引
for i in range(self.cols):
text += f"{self.NUMS[i]}" + dis
text += "\n\n"
for i, row in enumerate(self.board):
text += start + f"{self.NUMS[i]}" + dis*2
for dot in row:
if dot.mask and not dot.flagged:
text += md.btn_cmd(self.MASK, f"minesweeper reveal {dot.row} {dot.col}")
elif dot.flagged:
text += md.btn_cmd(self.FLAG, f"minesweeper mark {dot.row} {dot.col}")
else:
text += self.NUMS[dot.value]
text += dis
text += "\n"
btn_mark = md.btn_cmd("标记", f"minesweeper mark ", enter=False)
btn_end = md.btn_cmd("结束", "minesweeper end", enter=True)
text += f" {btn_mark} {btn_end}"
return text

View File

@ -1,103 +0,0 @@
from nonebot import require
from src.utils.base.ly_typing import T_Bot, T_MessageEvent
from src.utils.message.message import MarkdownMessage as md
require("nonebot_plugin_alconna")
from .game import Minesweeper
from nonebot_plugin_alconna import Alconna, on_alconna, Subcommand, Args, Arparma
minesweeper = on_alconna(
aliases={"扫雷"},
command=Alconna(
"minesweeper",
Subcommand(
"start",
Args["row", int, 8]["col", int, 8]["mines", int, 10],
alias=["开始"],
),
Subcommand(
"end",
alias=["结束"]
),
Subcommand(
"reveal",
Args["row", int]["col", int],
alias=["展开"]
),
Subcommand(
"mark",
Args["row", int]["col", int],
alias=["标记"]
),
),
)
minesweeper_cache: list[Minesweeper] = []
def get_minesweeper_cache(event: T_MessageEvent) -> Minesweeper | None:
for i in minesweeper_cache:
if i.session_type == event.message_type:
if i.session_id == event.user_id or i.session_id == event.group_id:
return i
return None
@minesweeper.handle()
async def _(event: T_MessageEvent, result: Arparma, bot: T_Bot):
game = get_minesweeper_cache(event)
if result.subcommands.get("start"):
if game:
await minesweeper.finish("当前会话不能同时进行多个扫雷游戏")
else:
try:
new_game = Minesweeper(
rows=result.subcommands["start"].args["row"],
cols=result.subcommands["start"].args["col"],
num_mines=result.subcommands["start"].args["mines"],
session_type=event.message_type,
session_id=event.user_id if event.message_type == "private" else event.group_id,
)
minesweeper_cache.append(new_game)
await minesweeper.send("游戏开始")
await md.send_md(new_game.board_markdown(), bot, event=event)
except AssertionError:
await minesweeper.finish("参数错误")
elif result.subcommands.get("end"):
if game:
minesweeper_cache.remove(game)
await minesweeper.finish("游戏结束")
else:
await minesweeper.finish("当前没有扫雷游戏")
elif result.subcommands.get("reveal"):
if not game:
await minesweeper.finish("当前没有扫雷游戏")
else:
row = result.subcommands["reveal"].args["row"]
col = result.subcommands["reveal"].args["col"]
if not (0 <= row < game.rows and 0 <= col < game.cols):
await minesweeper.finish("参数错误")
if not game.reveal(row, col):
minesweeper_cache.remove(game)
await md.send_md(game.board_markdown(), bot, event=event)
await minesweeper.finish("游戏结束")
await md.send_md(game.board_markdown(), bot, event=event)
if game.is_win():
minesweeper_cache.remove(game)
await minesweeper.finish("游戏胜利")
elif result.subcommands.get("mark"):
if not game:
await minesweeper.finish("当前没有扫雷游戏")
else:
row = result.subcommands["mark"].args["row"]
col = result.subcommands["mark"].args["col"]
if not (0 <= row < game.rows and 0 <= col < game.cols):
await minesweeper.finish("参数错误")
game.board[row][col].flagged = not game.board[row][col].flagged
await md.send_md(game.board_markdown(), bot, event=event)
else:
await minesweeper.finish("参数错误")

View File

@ -14,6 +14,7 @@ from nonebot.permission import SUPERUSER
from nonebot.plugin import Plugin, PluginMetadata
from nonebot.utils import run_sync
from src.utils.base.data_manager import InstalledPlugin
from src.utils.base.language import get_user_lang
from src.utils.base.ly_typing import T_Bot
@ -24,8 +25,10 @@ from src.utils.message.tools import clamp
from .common import *
require("nonebot_plugin_alconna")
require("nonebot_plugin_htmlrender")
from nonebot_plugin_htmlrender import md_to_pic
from nonebot_plugin_alconna import (
on_alconna,
UniMessage, on_alconna,
Alconna,
Args,
Arparma,
@ -292,7 +295,8 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot, npm: Matcher):
reply += f"\n{ulang.get('npm.too_many_results', HIDE_NUM=len(rs) - max_show)}"
else:
reply = ulang.get("npm.search_no_result")
await md.send_md(reply, bot, event=event)
img_bytes = await md_to_pic(reply)
await UniMessage.send(UniMessage.image(raw=img_bytes))
elif sc.get("install") and perm_s:
plugin_name: str = result.subcommands["install"].args.get("plugin_name")
@ -320,7 +324,7 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot, npm: Matcher):
info = md.escape(
ulang.get("npm.install_success", NAME=store_plugin.name)
) # markdown转义
await md.send_md(f"{info}\n\n" f"```\n{log}\n```", bot, event=event)
await npm.send(f"{info}\n\n" + f"\n{log}\n")
else:
await npm.finish(
ulang.get(
@ -331,12 +335,12 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot, npm: Matcher):
info = ulang.get(
"npm.load_failed", NAME=plugin_name, HOMEPAGE=homepage_btn
).replace("_", r"\\_")
await md.send_md(f"{info}\n\n" f"```\n{log}\n```\n", bot, event=event)
await npm.finish(f"{info}\n\n" f"```\n{log}\n```\n")
else:
info = ulang.get(
"npm.install_failed", NAME=plugin_name, HOMEPAGE=homepage_btn
).replace("_", r"\\_")
await md.send_md(f"{info}\n\n" f"```\n{log}\n```", bot, event=event)
await npm.send(f"{info}\n\n" f"```\n{log}\n```")
elif sc.get("uninstall") and perm_s:
plugin_name: str = result.subcommands["uninstall"].args.get("plugin_name") # type: ignore
@ -464,7 +468,8 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot, npm: Matcher):
else ulang.get("npm.next_page")
)
reply += f"\n{btn_prev} {page}/{total} {btn_next}"
await md.send_md(reply, bot, event=event)
img_bytes = await md_to_pic(reply)
await UniMessage.send(UniMessage.image(raw=img_bytes))
else:
if await SUPERUSER(bot, event):
@ -517,7 +522,8 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot, npm: Matcher):
f"\n\n>page为页数num为每页显示数量"
f"\n\n>*{md.escape('npm list [page] [num]')}*"
)
await md.send_md(reply, bot, event=event)
img_bytes = await md_to_pic(reply)
await UniMessage.send(UniMessage.image(raw=img_bytes))
else:
btn_list = md.btn_cmd(
@ -539,7 +545,8 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot, npm: Matcher):
f"\n\n>page为页数num为每页显示数量"
f"\n\n>*{md.escape('npm list [page] [num]')}*"
)
await md.send_md(reply, bot, event=event)
img_bytes = await md_to_pic(reply)
await UniMessage.send(UniMessage.image(raw=img_bytes))
@on_alconna(
@ -679,7 +686,7 @@ async def _(result: Arparma, matcher: Matcher, event: T_MessageEvent, bot: T_Bot
else mdc.paragraph(ulang.get("npm.homepage"))
),
]
await md.send_md(compile_md(reply), bot, event=event)
await matcher.finish(compile_md(reply))
else:
await matcher.finish(ulang.get("npm.plugin_not_found", NAME=plugin_name))
else:

View File

@ -181,6 +181,6 @@ async def _(bot: T_Bot, event: T_MessageEvent, result: Arparma, matcher: Matcher
else:
pass
if send_as_md:
await md.send_md(reply, bot, event=event)
await matcher.send(reply)
else:
await matcher.finish(reply)

View File

@ -1,7 +1,6 @@
import nonebot
from nonebot.message import event_preprocessor
# from nonebot_plugin_alconna.typings import Event
from src.utils.base.ly_typing import T_MessageEvent
from src.utils import satori_utils
from nonebot.adapters import satori

View File

@ -12,7 +12,9 @@ from .const import representative_timezones_list
from src.utils import event as event_utils
require("nonebot_plugin_alconna")
require("nonebot_plugin_htmlrender")
from nonebot_plugin_alconna import Alconna, Args, Arparma, Subcommand, on_alconna
from nonebot_plugin_htmlrender import md_to_pic
profile_alc = on_alconna(
Alconna(
@ -65,7 +67,8 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot):
# 未输入值,尝试呼出菜单
menu = get_profile_menu(result.args["key"], ulang)
if menu:
await md.send_md(menu, bot, event=event)
img_bytes = await md_to_pic(menu)
await profile_alc.finish(menu)
else:
await profile_alc.finish(ulang.get("user.profile.input_value", ATTR=ulang.get(f"user.profile.{result.args['key']}")))
@ -97,7 +100,8 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot):
reply += (f"\n**{key_text}** **{val}**\n"
f"\n> {ulang.get(f'user.profile.{key}.desc')}"
f"\n> {btn_set} \n\n***\n")
await md.send_md(reply, bot, event=event)
img_bytes = await md_to_pic(reply)
await profile_alc.finish(reply)
def get_profile_menu(key: str, ulang: Language) -> Optional[str]:

View File

@ -1,18 +0,0 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Time : 2024/8/19 下午10:30
@Author : snowykami
@Email : snowykami@outlook.com
@File : __init__.py.py
@Software: PyCharm
"""
from nonebot import require
from liteyuki.comm.storage import shared_memory
require("nonebot_plugin_alconna")
from nonebot_plugin_alconna import UniMessage, Command, on_alconna

View File

@ -8,11 +8,13 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@File : to_liteyuki.py
@Software: PyCharm
"""
import asyncio
from nonebot import Bot, get_bot, on_message
from nonebot.plugin import PluginMetadata
from nonebot.adapters.onebot.v11 import MessageEvent, Bot
from liteyuki.comm.storage import shared_memory
from liteyuki.message.event import Event
from liteyuki.message.event import MessageEvent as LiteyukiMessageEvent
__plugin_meta__ = PluginMetadata(
name="轻雪物流",
@ -23,8 +25,10 @@ __plugin_meta__ = PluginMetadata(
@on_message().handle()
async def _(bot: Bot, event: MessageEvent):
liteyuki_event = Event(
type=event.message_type,
liteyuki_event = LiteyukiMessageEvent(
message_type=event.message_type,
message=event.dict()["message"],
raw_message=event.raw_message,
data=event.dict(),
bot_id=bot.self_id,
session_id=str(event.user_id if event.message_type == "private" else event.group_id),
@ -35,6 +39,6 @@ async def _(bot: Bot, event: MessageEvent):
@shared_memory.on_subscriber_receive("event_to_nonebot")
async def _(event: Event):
async def _(event: LiteyukiMessageEvent):
bot: Bot = get_bot(event.bot_id)
await bot.send_msg(message_type=event.type, user_id=int(event.session_id), group_id=int(event.session_id), message=event.data["message"])
await bot.send_msg(message_type=event.message_type, user_id=int(event.session_id), group_id=int(event.session_id), message=event.data["message"])

View File

@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Time : 2024/8/22 上午8:39
@Author : snowykami
@Email : snowykami@outlook.com
@File : ts_chan_sub.py
@Software: PyCharm
"""
import asyncio
from liteyuki.comm import Channel, get_channel
from nonebot import get_bot
from nonebot.adapters.onebot.v11 import Bot
chan_main = get_channel("chan-main")
# @chan_main.on_receive()
# async def _(data: str):
# print("Received data from chan-main:", data)
# try:
# bot: Bot = get_bot("2443429204") # type: ignore
#
# def send_msg():
#
# bot.send_msg(message_type="private", user_id=2443429204, message=data)
#
# print("tsA")
# print("tsA1")
# await asyncio.ensure_future(c)
# print("tsB")
# except Exception as e:
# print(e)
# pass

View File

@ -1,18 +1,15 @@
import base64
import io
from typing import Any
from urllib.parse import quote
import aiofiles
from PIL import Image
import aiohttp
import nonebot
from PIL import Image
from nonebot import require
from nonebot.adapters import satori
from nonebot.adapters.onebot import v11
from typing import Any, Type
from nonebot.internal.adapter import MessageSegment
from nonebot.internal.adapter.message import TM
from .. import load_from_yaml
from ..base.ly_typing import T_Bot, T_Message, T_MessageEvent
@ -22,12 +19,6 @@ from nonebot_plugin_htmlrender import md_to_pic
config = load_from_yaml("config.yml")
can_send_markdown = {} # 用于存储机器人是否支持发送markdown消息id->bool
class TencentBannedMarkdownError(BaseException):
pass
async def broadcast_to_superusers(message: str | T_Message, markdown: bool = False):
"""广播消息给超级用户"""
@ -45,10 +36,7 @@ class MarkdownMessage:
markdown: str,
bot: T_Bot, *,
message_type: str = None,
session_id: str | int = None,
event: T_MessageEvent = None,
retry_as_image: bool = True,
**kwargs
session_id: str | int = None
) -> dict[str, Any] | None:
"""
发送Markdown消息支持自动转为图片发送
@ -57,89 +45,22 @@ class MarkdownMessage:
bot:
message_type:
session_id:
event:
retry_as_image: 发送失败后是否尝试以图片形式发送否则失败返回None
**kwargs:
Returns:
"""
formatted_md = v11.unescape(markdown).replace("\n", r"\n").replace('"', r'\\\"')
if event is not None and message_type is None:
if isinstance(event, satori.event.Event):
message_type = "private" if event.guild is None else "group"
group_id = event.guild.id if event.guild is not None else None
else:
assert event is not None
message_type = event.message_type
group_id = event.group_id if message_type == "group" else None
user_id = event.user.id if isinstance(event, satori.event.Event) else event.user_id
session_id = user_id if message_type == "private" else group_id
else:
pass
try:
raise TencentBannedMarkdownError("Tencent banned markdown")
forward_id = await bot.call_api(
"send_private_forward_msg",
messages=[
{
"type": "node",
"data": {
"content": [
{
"data": {
"content": "{\"content\":\"%s\"}" % formatted_md,
},
"type": "markdown"
}
],
"name": "[]",
"uin": bot.self_id
}
}
],
user_id=bot.self_id
)
data = await bot.send_msg(
user_id=session_id,
group_id=session_id,
message_type=message_type,
message=[
{
"type": "longmsg",
"data": {
"id": forward_id
}
},
],
**kwargs
)
except BaseException as e:
nonebot.logger.error(f"send markdown error, retry as image: {e}")
# 发送失败,渲染为图片发送
# if not retry_as_image:
# return None
plain_markdown = markdown.replace("[🔗", "[")
md_image_bytes = await md_to_pic(
md=plain_markdown,
width=540,
device_scale_factor=4
)
if isinstance(bot, satori.Bot):
msg_seg = satori.MessageSegment.image(raw=md_image_bytes,mime="image/png")
data = await bot.send(
event=event,
message=msg_seg
)
else:
data = await bot.send_msg(
message_type=message_type,
group_id=session_id,
user_id=session_id,
message=v11.MessageSegment.image(md_image_bytes),
)
plain_markdown = markdown.replace("[🔗", "[")
md_image_bytes = await md_to_pic(
md=plain_markdown,
width=540,
device_scale_factor=4
)
print(md_image_bytes)
data = await bot.send_msg(
message_type=message_type,
group_id=session_id,
user_id=session_id,
message=v11.MessageSegment.image(md_image_bytes),
)
return data
@staticmethod
@ -156,48 +77,36 @@ class MarkdownMessage:
Args:
image: 图片字节流或图片本地路径链接请使用Markdown.image_async方法获取后通过send_md发送
bot: bot instance
message_type: message type
message_type: message message_type
session_id: session id
event: event
kwargs: other arguments
Returns:
dict: response data
"""
if isinstance(image, str):
async with aiofiles.open(image, "rb") as f:
image = await f.read()
method = 2
# 1.轻雪图床方案
# if method == 1:
# image_url = await liteyuki_api.upload_image(image)
# image_size = Image.open(io.BytesIO(image)).size
# image_md = Markdown.image(image_url, image_size)
# data = await Markdown.send_md(image_md, bot, message_type=message_type, session_id=session_id, event=event,
# retry_as_image=False,
# **kwargs)
# Lagrange.OneBot方案
if method == 2:
base64_string = base64.b64encode(image).decode("utf-8")
data = await bot.call_api("upload_image", file=f"base64://{base64_string}")
await MarkdownMessage.send_md(MarkdownMessage.image(data, Image.open(io.BytesIO(image)).size), bot,
event=event, message_type=message_type,
session_id=session_id, **kwargs)
message_type=message_type,
session_id=session_id)
# 其他实现端方案
else:
image_message_id = (await bot.send_private_msg(
user_id=bot.self_id,
message=[
v11.MessageSegment.image(file=image)
v11.MessageSegment.image(file=image)
]
))["message_id"]
image_url = (await bot.get_msg(message_id=image_message_id))["message"][0]["data"]["url"]
image_size = Image.open(io.BytesIO(image)).size
image_md = MarkdownMessage.image(image_url, image_size)
return await MarkdownMessage.send_md(image_md, bot, message_type=message_type, session_id=session_id,
event=event, **kwargs)
return await MarkdownMessage.send_md(image_md, bot, message_type=message_type, session_id=session_id)
if data is None:
data = await bot.send_msg(