diff --git a/docs/dev/api/bot/README.md b/docs/dev/api/bot/README.md index 8f027bf5..9f019444 100644 --- a/docs/dev/api/bot/README.md +++ b/docs/dev/api/bot/README.md @@ -15,6 +15,26 @@ Returns: LiteyukiBot: 当前的轻雪实例 +
+源代码 + +```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.") +``` +
+ ### ***def*** `get_config(key: str, default: Any) -> Any` 获取配置 @@ -31,6 +51,24 @@ Returns: Any: 配置值 +
+源代码 + +```python +def get_config(key: str, default: Any=None) -> Any: + """ + 获取配置 + Args: + key: 配置键 + default: 默认值 + + Returns: + Any: 配置值 + """ + return get_bot().config.get(key, default) +``` +
+ ### ***def*** `get_config_with_compat(key: str, compat_keys: tuple[str], default: Any) -> Any` 获取配置,兼容旧版本 @@ -49,10 +87,44 @@ Returns: Any: 配置值 +
+源代码 + +```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 +``` +
+ ### ***def*** `print_logo() -> None` +
+源代码 + +```python +def print_logo(): + print('\x1b[34m' + '\n __ ______ ________ ________ __ __ __ __ __ __ ______ \n / | / |/ |/ |/ \\ / |/ | / |/ | / |/ |\n $$ | $$$$$$/ $$$$$$$$/ $$$$$$$$/ $$ \\ /$$/ $$ | $$ |$$ | /$$/ $$$$$$/ \n $$ | $$ | $$ | $$ |__ $$ \\/$$/ $$ | $$ |$$ |/$$/ $$ | \n $$ | $$ | $$ | $$ | $$ $$/ $$ | $$ |$$ $$< $$ | \n $$ | $$ | $$ | $$$$$/ $$$$/ $$ | $$ |$$$$$ \\ $$ | \n $$ |_____ _$$ |_ $$ | $$ |_____ $$ | $$ \\__$$ |$$ |$$ \\ _$$ |_ \n $$ |/ $$ | $$ | $$ | $$ | $$ $$/ $$ | $$ |/ $$ |\n $$$$$$$$/ $$$$$$/ $$/ $$$$$$$$/ $$/ $$$$$$/ $$/ $$/ $$$$$$/ \n ' + '\x1b[0m') +``` +
+ ### ***class*** `LiteyukiBot` @@ -67,22 +139,124 @@ Args: **kwargs: 配置 +
+源代码 + +```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) +``` +
+ ###   ***def*** `run(self) -> None`  启动逻辑 +
+源代码 + +```python +def run(self): + """ + 启动逻辑 + """ + self.lifespan.before_start() + self.process_manager.start_all() + self.lifespan.after_start() + self.keep_alive() +``` +
+ ###   ***def*** `keep_alive(self) -> None`  保持轻雪运行 Returns: +
+源代码 + +```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() +``` +
+ ###   ***def*** `restart(self, delay: int) -> None`  重启轻雪本体 Returns: +
+源代码 + +```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 +``` +
+ ###   ***def*** `restart_process(self, name: Optional[str]) -> None`  停止轻雪 @@ -93,22 +267,83 @@ Args: Returns: +
+源代码 + +```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) +``` +
+ ###   ***def*** `init(self) -> None`  初始化轻雪, 自动调用 Returns: +
+源代码 + +```python +def init(self, *args, **kwargs): + """ + 初始化轻雪, 自动调用 + Returns: + + """ + self.init_logger() +``` +
+ ###   ***def*** `init_logger(self) -> None`   +
+源代码 + +```python +def init_logger(self): + init_log(config=self.config) +``` +
+ ###   ***def*** `stop(self) -> None`  停止轻雪 Returns: +
+源代码 + +```python +def stop(self): + """ + 停止轻雪 + Returns: + + """ + self.stop_event.set() + self.loop.stop() +``` +
+ ###   ***def*** `on_before_start(self, func: LIFESPAN_FUNC) -> None`  注册启动前的函数 @@ -121,6 +356,23 @@ Args: Returns: +
+源代码 + +```python +def on_before_start(self, func: LIFESPAN_FUNC): + """ + 注册启动前的函数 + Args: + func: + + Returns: + + """ + return self.lifespan.on_before_start(func) +``` +
+ ###   ***def*** `on_after_start(self, func: LIFESPAN_FUNC) -> None`  注册启动后的函数 @@ -133,6 +385,23 @@ Args: Returns: +
+源代码 + +```python +def on_after_start(self, func: LIFESPAN_FUNC): + """ + 注册启动后的函数 + Args: + func: + + Returns: + + """ + return self.lifespan.on_after_start(func) +``` +
+ ###   ***def*** `on_after_shutdown(self, func: LIFESPAN_FUNC) -> None`  注册停止后的函数:未实现 @@ -145,6 +414,23 @@ Args: Returns: +
+源代码 + +```python +def on_after_shutdown(self, func: LIFESPAN_FUNC): + """ + 注册停止后的函数:未实现 + Args: + func: + + Returns: + + """ + return self.lifespan.on_after_shutdown(func) +``` +
+ ###   ***def*** `on_before_process_shutdown(self, func: LIFESPAN_FUNC) -> None`  注册进程停止前的函数,为子进程停止时调用 @@ -157,6 +443,23 @@ Args: Returns: +
+源代码 + +```python +def on_before_process_shutdown(self, func: LIFESPAN_FUNC): + """ + 注册进程停止前的函数,为子进程停止时调用 + Args: + func: + + Returns: + + """ + return self.lifespan.on_before_process_shutdown(func) +``` +
+ ###   ***def*** `on_before_process_restart(self, func: LIFESPAN_FUNC) -> None`  注册进程重启前的函数,为子进程重启时调用 @@ -169,6 +472,23 @@ Args: Returns: +
+源代码 + +```python +def on_before_process_restart(self, func: LIFESPAN_FUNC): + """ + 注册进程重启前的函数,为子进程重启时调用 + Args: + func: + + Returns: + + """ + return self.lifespan.on_before_process_restart(func) +``` +
+ ###   ***def*** `on_after_restart(self, func: LIFESPAN_FUNC) -> None`  注册重启后的函数:未实现 @@ -181,6 +501,23 @@ Args: Returns: +
+源代码 + +```python +def on_after_restart(self, func: LIFESPAN_FUNC): + """ + 注册重启后的函数:未实现 + Args: + func: + + Returns: + + """ + return self.lifespan.on_after_restart(func) +``` +
+ ###   ***def*** `on_after_nonebot_init(self, func: LIFESPAN_FUNC) -> None`  注册nonebot初始化后的函数 @@ -193,6 +530,23 @@ Args: Returns: +
+源代码 + +```python +def on_after_nonebot_init(self, func: LIFESPAN_FUNC): + """ + 注册nonebot初始化后的函数 + Args: + func: + + Returns: + + """ + return self.lifespan.on_after_nonebot_init(func) +``` +
+ ### ***var*** `executable = sys.executable` diff --git a/docs/dev/api/bot/lifespan.md b/docs/dev/api/bot/lifespan.md index 396403b4..6466bd52 100644 --- a/docs/dev/api/bot/lifespan.md +++ b/docs/dev/api/bot/lifespan.md @@ -15,6 +15,33 @@ Args: Returns: +
+源代码 + +```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)) +``` +
+ ### ***class*** `Lifespan` @@ -23,6 +50,25 @@ Returns:  轻雪生命周期管理,启动、停止、重启 +
+源代码 + +```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] = [] +``` +
+ ###   ***@staticmethod*** ###   ***def*** `run_funcs(funcs: list[LIFESPAN_FUNC | PROCESS_LIFESPAN_FUNC]) -> None` @@ -34,6 +80,33 @@ Args: Returns: +
+源代码 + +```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)) +``` +
+ ###   ***def*** `on_before_start(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`  注册启动时的函数 @@ -46,6 +119,23 @@ Returns: LIFESPAN_FUNC: +
+源代码 + +```python +def on_before_start(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC: + """ + 注册启动时的函数 + Args: + func: + Returns: + LIFESPAN_FUNC: + """ + self._before_start_funcs.append(func) + return func +``` +
+ ###   ***def*** `on_after_start(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`  注册启动时的函数 @@ -58,6 +148,23 @@ Returns: LIFESPAN_FUNC: +
+源代码 + +```python +def on_after_start(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC: + """ + 注册启动时的函数 + Args: + func: + Returns: + LIFESPAN_FUNC: + """ + self._after_start_funcs.append(func) + return func +``` +
+ ###   ***def*** `on_before_process_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`  注册停止前的函数 @@ -70,6 +177,23 @@ Returns: LIFESPAN_FUNC: +
+源代码 + +```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 +``` +
+ ###   ***def*** `on_after_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`  注册停止后的函数 @@ -84,6 +208,25 @@ Returns: LIFESPAN_FUNC: +
+源代码 + +```python +def on_after_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC: + """ + 注册停止后的函数 + Args: + func: + + Returns: + LIFESPAN_FUNC: + + """ + self._after_shutdown_funcs.append(func) + return func +``` +
+ ###   ***def*** `on_before_process_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`  注册重启时的函数 @@ -96,6 +239,23 @@ Returns: LIFESPAN_FUNC: +
+源代码 + +```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 +``` +
+ ###   ***def*** `on_after_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`  注册重启后的函数 @@ -108,6 +268,23 @@ Returns: LIFESPAN_FUNC: +
+源代码 + +```python +def on_after_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC: + """ + 注册重启后的函数 + Args: + func: + Returns: + LIFESPAN_FUNC: + """ + self._after_restart_funcs.append(func) + return func +``` +
+ ###   ***def*** `on_after_nonebot_init(self, func: Any) -> None`  注册 NoneBot 初始化后的函数 @@ -120,42 +297,145 @@ Args: Returns: +
+源代码 + +```python +def on_after_nonebot_init(self, func): + """ + 注册 NoneBot 初始化后的函数 + Args: + func: + + Returns: + + """ + self._after_nonebot_init_funcs.append(func) + return func +``` +
+ ###   ***def*** `before_start(self) -> None`  启动前 Returns: +
+源代码 + +```python +def before_start(self) -> None: + """ + 启动前 + Returns: + """ + logger.debug('Running before_start functions') + self.run_funcs(self._before_start_funcs) +``` +
+ ###   ***def*** `after_start(self) -> None`  启动后 Returns: +
+源代码 + +```python +def after_start(self) -> None: + """ + 启动后 + Returns: + """ + logger.debug('Running after_start functions') + self.run_funcs(self._after_start_funcs) +``` +
+ ###   ***def*** `before_process_shutdown(self) -> None`  停止前 Returns: +
+源代码 + +```python +def before_process_shutdown(self) -> None: + """ + 停止前 + Returns: + """ + logger.debug('Running before_shutdown functions') + self.run_funcs(self._before_process_shutdown_funcs) +``` +
+ ###   ***def*** `after_shutdown(self) -> None`  停止后 Returns: +
+源代码 + +```python +def after_shutdown(self) -> None: + """ + 停止后 + Returns: + """ + logger.debug('Running after_shutdown functions') + self.run_funcs(self._after_shutdown_funcs) +``` +
+ ###   ***def*** `before_process_restart(self) -> None`  重启前 Returns: +
+源代码 + +```python +def before_process_restart(self) -> None: + """ + 重启前 + Returns: + """ + logger.debug('Running before_restart functions') + self.run_funcs(self._before_process_restart_funcs) +``` +
+ ###   ***def*** `after_restart(self) -> None`  重启后 Returns: +
+源代码 + +```python +def after_restart(self) -> None: + """ + 重启后 + Returns: + + """ + logger.debug('Running after_restart functions') + self.run_funcs(self._after_restart_funcs) +``` +
+ ### ***var*** `tasks = []` diff --git a/docs/dev/api/comm/channel.md b/docs/dev/api/comm/channel.md index f34d7054..8e5e170f 100644 --- a/docs/dev/api/comm/channel.md +++ b/docs/dev/api/comm/channel.md @@ -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: 通道实例 +
+源代码 + +```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})) +``` +
+ ### ***def*** `set_channels(channels: dict[str, Channel]) -> None` 设置通道实例 @@ -23,6 +43,21 @@ Args: channels: 通道名称 +
+源代码 + +```python +def set_channels(channels: dict[str, Channel]): + """ + 设置通道实例 + Args: + channels: 通道名称 + """ + for name, channel in channels.items(): + set_channel(name, channel) +``` +
+ ### ***def*** `get_channel(name: str) -> Channel` 获取通道实例 @@ -33,39 +68,156 @@ Args: Returns: +
+源代码 + +```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() +``` +
+ ### ***def*** `get_channels() -> dict[str, Channel]` 获取通道实例 Returns: +
+源代码 + +```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() +``` +
+ ### ***def*** `on_set_channel(data: tuple[str, dict[str, Any]]) -> None` +
+源代码 + +```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) +``` +
+ ### ***def*** `on_get_channel(data: tuple[str, dict[str, Any]]) -> None` +
+源代码 + +```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)) +``` +
+ ### ***def*** `on_get_channels(data: tuple[str, dict[str, Any]]) -> None` +
+源代码 + +```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()) +``` +
+ ### ***def*** `decorator(func: Callable[[T], Any]) -> Callable[[T], Any]` +
+源代码 + +```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 +``` +
+ ### ***async def*** `wrapper(data: T) -> Any` +
+源代码 + +```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) +``` +
+ ### ***class*** `Channel(Generic[T])` 通道类,可以在进程间和进程内通信,双向但同时只能有一个发送者和一个接收者 有两种接收工作方式,但是只能选择一种,主动接收和被动接收,主动接收使用 `receive` 方法,被动接收使用 `on_receive` 装饰器 -###   ***def*** `__init__(self, _id: str, type_check: bool) -> None` +###   ***def*** `__init__(self, _id: str, type_check: Optional[bool]) -> None`  初始化通道 @@ -73,6 +225,35 @@ Args: _id: 通道ID + type_check: 是否开启类型检查, 若为空,则传入泛型默认开启,否则默认关闭 + +
+源代码 + +```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 +``` +
+ ###   ***def*** `send(self, data: T) -> None`  发送数据 @@ -81,16 +262,67 @@ Args: data: 数据 +
+源代码 + +```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) +``` +
+ ###   ***def*** `receive(self) -> T`  接收数据 Args: +
+源代码 + +```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 +``` +
+ ###   ***def*** `close(self) -> None`  关闭通道 +
+源代码 + +```python +def close(self): + """ + 关闭通道 + """ + self._closed = True + self.conn_send.close() + self.conn_recv.close() +``` +
+ ###   ***def*** `on_receive(self, filter_func: Optional[FILTER_FUNC]) -> Callable[[Callable[[T], Any]], Callable[[T], Any]]`  接收数据并执行函数 @@ -103,6 +335,48 @@ Returns: 装饰器,装饰一个函数在接收到数据后执行 +
+源代码 + +```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 +``` +
+ ### ***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()` diff --git a/docs/dev/api/comm/event.md b/docs/dev/api/comm/event.md index a5dbf619..a2a15f55 100644 --- a/docs/dev/api/comm/event.md +++ b/docs/dev/api/comm/event.md @@ -13,3 +13,13 @@ category: API   +
+源代码 + +```python +def __init__(self, name: str, data: dict[str, Any]): + self.name = name + self.data = data +``` +
+ diff --git a/docs/dev/api/comm/storage.md b/docs/dev/api/comm/storage.md index 9042d3a1..cd19dbb7 100644 --- a/docs/dev/api/comm/storage.md +++ b/docs/dev/api/comm/storage.md @@ -5,22 +5,200 @@ icon: laptop-code category: API --- +### ***def*** `run_subscriber_receive_funcs(channel_: str, data: Any) -> None` + +运行订阅者接收函数 + +Args: + + channel_: 频道 + + data: 数据 + +
+源代码 + +```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_]]) +``` +
+ ### ***def*** `on_get(data: tuple[str, dict[str, Any]]) -> None` +
+源代码 + +```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)) +``` +
+ ### ***def*** `on_set(data: tuple[str, dict[str, Any]]) -> None` +
+源代码 + +```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) +``` +
+ ### ***def*** `on_delete(data: tuple[str, dict[str, Any]]) -> None` +
+源代码 + +```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) +``` +
+ ### ***def*** `on_get_all(data: tuple[str, dict[str, Any]]) -> None` +
+源代码 + +```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()) +``` +
+ +### ***def*** `on_publish(data: tuple[str, Any]) -> None` + + + +
+源代码 + +```python +@channel.publish_channel.on_receive() +def on_publish(data: tuple[str, Any]): + channel_, data = data + shared_memory.run_subscriber_receive_funcs(channel_, data) +``` +
+ +### ***def*** `decorator(func: ON_RECEIVE_FUNC) -> ON_RECEIVE_FUNC` + + + +
+源代码 + +```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 +``` +
+ +### ***async def*** `wrapper(data: Any) -> None` + + + +
+源代码 + +```python +async def wrapper(data: Any): + if is_coroutine_callable(func): + await func(data) + else: + func(data) +``` +
+ +### ***class*** `Subscriber` + + + +###   ***def*** `__init__(self) -> None` + +  + +
+源代码 + +```python +def __init__(self): + self._subscribers = {} +``` +
+ +###   ***def*** `receive(self) -> Any` + +  + +
+源代码 + +```python +def receive(self) -> Any: + pass +``` +
+ +###   ***def*** `unsubscribe(self) -> None` + +  + +
+源代码 + +```python +def unsubscribe(self) -> None: + pass +``` +
+ ### ***class*** `KeyValueStore` @@ -29,6 +207,20 @@ category: API   +
+源代码 + +```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 +``` +
+ ###   ***def*** `set(self, key: str, value: Any) -> None`  设置键值对 @@ -39,6 +231,27 @@ Args: value: 值 +
+源代码 + +```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})) +``` +
+ ###   ***def*** `get(self, key: str, default: Optional[Any]) -> Optional[Any]`  获取键值对 @@ -55,6 +268,31 @@ Returns: Any: 值 +
+源代码 + +```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() +``` +
+ ###   ***def*** `delete(self, key: str, ignore_key_error: bool) -> None`  删除键值对 @@ -69,6 +307,34 @@ Args: Returns: +
+源代码 + +```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})) +``` +
+ ###   ***def*** `get_all(self) -> dict[str, Any]`  获取所有键值对 @@ -77,6 +343,141 @@ Returns: dict[str, Any]: 键值对 +
+源代码 + +```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() +``` +
+ +###   ***def*** `publish(self, channel_: str, data: Any) -> None` + + 发布消息 + +Args: + + channel_: 频道 + + data: 数据 + + + +Returns: + +
+源代码 + +```python +def publish(self, channel_: str, data: Any) -> None: + """ + 发布消息 + Args: + channel_: 频道 + data: 数据 + + Returns: + """ + self.active_chan.send(('publish', {'channel': channel_, 'data': data})) +``` +
+ +###   ***def*** `on_subscriber_receive(self, channel_: str) -> Callable[[ON_RECEIVE_FUNC], ON_RECEIVE_FUNC]` + + 订阅者接收消息时的回调 + +Args: + + channel_: 频道 + + + +Returns: + + 装饰器 + +
+源代码 + +```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 +``` +
+ +###   ***@staticmethod*** +###   ***def*** `run_subscriber_receive_funcs(channel_: str, data: Any) -> None` + + 运行订阅者接收函数 + +Args: + + channel_: 频道 + + data: 数据 + +
+源代码 + +```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_]]) +``` +
+ ### ***class*** `GlobalKeyValueStore` @@ -86,6 +487,20 @@ Returns:   +
+源代码 + +```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 +``` +
+ ###   ***attr*** `_instance: None` ###   ***attr*** `_lock: threading.Lock()` @@ -138,3 +553,11 @@ Returns: +### ***var*** `data = self.active_chan.receive()` + + + +### ***var*** `data = self.publish_channel.receive()` + + + diff --git a/docs/dev/api/config.md b/docs/dev/api/config.md index 30066528..24afd6c9 100644 --- a/docs/dev/api/config.md +++ b/docs/dev/api/config.md @@ -23,24 +23,117 @@ Returns: 扁平化后的配置文件,但也包含原有的键值对 +
+源代码 + +```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 +``` +
+ ### ***def*** `load_from_yaml(file: str) -> dict[str, Any]` Load config from yaml file +
+源代码 + +```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 {}) +``` +
+ ### ***def*** `load_from_json(file: str) -> dict[str, Any]` Load config from json file +
+源代码 + +```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 {}) +``` +
+ ### ***def*** `load_from_toml(file: str) -> dict[str, Any]` Load config from toml file +
+源代码 + +```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 {}) +``` +
+ ### ***def*** `load_from_files() -> dict[str, Any]` 从指定文件加载配置项,会自动识别文件格式 默认执行扁平化选项 +
+源代码 + +```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 +``` +
+ ### ***def*** `load_configs_from_dirs() -> dict[str, Any]` 从目录下加载配置文件,不递归 @@ -49,6 +142,29 @@ Load config from toml file 默认执行扁平化选项 +
+源代码 + +```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 +``` +
+ ### ***def*** `load_config_in_default(no_waring: bool) -> dict[str, Any]` 从一个标准的轻雪项目加载配置文件 @@ -57,6 +173,22 @@ Load config from toml file 项目目录下的配置文件优先 +
+源代码 + +```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 +``` +
+ ### ***class*** `SatoriNodeConfig(BaseModel)` diff --git a/docs/dev/api/core/manager.md b/docs/dev/api/core/manager.md index ed8f2f18..52397375 100644 --- a/docs/dev/api/core/manager.md +++ b/docs/dev/api/core/manager.md @@ -9,10 +9,23 @@ category: API -###   ***def*** `__init__(self, active: Channel[Any], passive: Channel[Any], channel_deliver_active: Channel[Channel[Any]], channel_deliver_passive: Channel[tuple[str, dict]]) -> None` +###   ***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`   +
+源代码 + +```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 +``` +
+ ### ***class*** `ProcessManager` 进程管理器 @@ -21,6 +34,17 @@ category: API   +
+源代码 + +```python +def __init__(self, lifespan: 'Lifespan'): + self.lifespan = lifespan + self.targets: dict[str, tuple[Callable, tuple, dict]] = {} + self.processes: dict[str, Process] = {} +``` +
+ ###   ***def*** `start(self, name: str) -> None`  开启后自动监控进程,并添加到进程字典中 @@ -31,10 +55,63 @@ Args: Returns: +
+源代码 + +```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.') +``` +
+ ###   ***def*** `start_all(self) -> None`  启动所有进程 +
+源代码 + +```python +def start_all(self): + """ + 启动所有进程 + """ + for name in self.targets: + threading.Thread(target=self.start, args=(name,), daemon=True).start() +``` +
+ ###   ***def*** `add_target(self, name: str, target: TARGET_FUNC, args: tuple, kwargs: Any) -> None`  添加进程 @@ -49,10 +126,43 @@ Args: kwargs: 进程函数关键字参数,通常会默认传入chan_active和chan_passive +
+源代码 + +```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}) +``` +
+ ###   ***def*** `join_all(self) -> None`   +
+源代码 + +```python +def join_all(self): + for name, process in self.targets: + process.join() +``` +
+ ###   ***def*** `terminate(self, name: str) -> None`  终止进程并从进程字典中删除 @@ -65,10 +175,45 @@ Args: Returns: +
+源代码 + +```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.') +``` +
+ ###   ***def*** `terminate_all(self) -> None`   +
+源代码 + +```python +def terminate_all(self): + for name in self.targets: + self.terminate(name) +``` +
+ ###   ***def*** `is_process_alive(self, name: str) -> bool`  检查进程是否存活 @@ -81,6 +226,25 @@ Args: Returns: +
+源代码 + +```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() +``` +
+ ### ***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)` diff --git a/docs/dev/api/dev/observer.md b/docs/dev/api/dev/observer.md index f0f5643b..2005bbf2 100644 --- a/docs/dev/api/dev/observer.md +++ b/docs/dev/api/dev/observer.md @@ -9,6 +9,29 @@ category: API 防抖函数 +
+源代码 + +```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 +``` +
+ ### ***def*** `on_file_system_event(directories: tuple[str], recursive: bool, event_filter: FILTER_FUNC) -> Callable[[CALLBACK_FUNC], CALLBACK_FUNC]` 注册文件系统变化监听器 @@ -25,22 +48,111 @@ Returns: 装饰器,装饰一个函数在接收到数据后执行 +
+源代码 + +```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 +``` +
+ ### ***def*** `decorator(func: Any) -> None` +
+源代码 + +```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 +``` +
+ ### ***def*** `decorator(func: CALLBACK_FUNC) -> CALLBACK_FUNC` +
+源代码 + +```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 +``` +
+ ### ***def*** `wrapper() -> None` +
+源代码 + +```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) +``` +
+ ### ***def*** `wrapper(event: FileSystemEvent) -> None` +
+源代码 + +```python +def wrapper(event: FileSystemEvent): + if event_filter is not None and (not event_filter(event)): + return + func(event) +``` +
+ ### ***class*** `CodeModifiedHandler(FileSystemEventHandler)` Handler for code file changes @@ -49,22 +161,68 @@ Handler for code file changes   +
+源代码 + +```python +@debounce(1) +def on_modified(self, event): + raise NotImplementedError('on_modified must be implemented') +``` +
+ ###   ***def*** `on_created(self, event: Any) -> None`   +
+源代码 + +```python +def on_created(self, event): + self.on_modified(event) +``` +
+ ###   ***def*** `on_deleted(self, event: Any) -> None`   +
+源代码 + +```python +def on_deleted(self, event): + self.on_modified(event) +``` +
+ ###   ***def*** `on_moved(self, event: Any) -> None`   +
+源代码 + +```python +def on_moved(self, event): + self.on_modified(event) +``` +
+ ###   ***def*** `on_any_event(self, event: Any) -> None`   +
+源代码 + +```python +def on_any_event(self, event): + self.on_modified(event) +``` +
+ ### ***var*** `liteyuki_bot = get_bot()` diff --git a/docs/dev/api/dev/plugin.md b/docs/dev/api/dev/plugin.md index a93c922b..caafa5ea 100644 --- a/docs/dev/api/dev/plugin.md +++ b/docs/dev/api/dev/plugin.md @@ -13,6 +13,25 @@ Args: module_path: 插件路径,参考`liteyuki.load_plugin`的函数签名 +
+源代码 + +```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() +``` +
+ ### ***var*** `cfg = load_config_in_default()` diff --git a/docs/dev/api/log.md b/docs/dev/api/log.md index c698f3ba..af382275 100644 --- a/docs/dev/api/log.md +++ b/docs/dev/api/log.md @@ -9,12 +9,49 @@ category: API +
+源代码 + +```python +def get_format(level: str) -> str: + if level == 'DEBUG': + return debug_format + else: + return default_format +``` +
+ ### ***def*** `init_log(config: dict) -> None` 在语言加载完成后执行 Returns: +
+源代码 + +```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='', icon=f"{('🐛' if show_icon else '')}DEBUG") + logger.level('INFO', color='', icon=f"{('ℹ️' if show_icon else '')}INFO") + logger.level('SUCCESS', color='', icon=f"{('✅' if show_icon else '')}SUCCESS") + logger.level('WARNING', color='', icon=f"{('⚠️' if show_icon else '')}WARNING") + logger.level('ERROR', color='', icon=f"{('⭕' if show_icon else '')}ERROR") +``` +
+ +### ***var*** `logger = loguru.logger` + + + ### ***var*** `show_icon = config.get('log_icon', True)` diff --git a/docs/dev/api/message/README.md b/docs/dev/api/message/README.md new file mode 100644 index 00000000..0d664851 --- /dev/null +++ b/docs/dev/api/message/README.md @@ -0,0 +1,7 @@ +--- +title: liteyuki.message +index: true +icon: laptop-code +category: API +--- + diff --git a/docs/dev/api/message/event.md b/docs/dev/api/message/event.md new file mode 100644 index 00000000..740ff8bd --- /dev/null +++ b/docs/dev/api/message/event.md @@ -0,0 +1,106 @@ +--- +title: liteyuki.message.event +order: 1 +icon: laptop-code +category: API +--- + +### ***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: + + + + 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: 附加数据 + +
+源代码 + +```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 +``` +
+ +###   ***def*** `reply(self, message: str | dict[str, Any]) -> None` + + 回复消息 + +Args: + + message: + +Returns: + +
+源代码 + +```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) +``` +
+ +### ***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 = {}` + + + diff --git a/docs/dev/api/message/matcher.md b/docs/dev/api/message/matcher.md new file mode 100644 index 00000000..4a6341b6 --- /dev/null +++ b/docs/dev/api/message/matcher.md @@ -0,0 +1,71 @@ +--- +title: liteyuki.message.matcher +order: 1 +icon: laptop-code +category: API +--- + +### ***class*** `Matcher` + + + +###   ***def*** `__init__(self, rule: Rule, priority: int, block: bool) -> None` + + 匹配器 + +Args: + + rule: 规则 + + priority: 优先级 >= 0 + + block: 是否阻断后续优先级更低的匹配器 + +
+源代码 + +```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] = [] +``` +
+ +###   ***def*** `handle(self, handler: EventHandler) -> EventHandler` + + 添加处理函数,装饰器 + +Args: + + handler: + +Returns: + + EventHandler + +
+源代码 + +```python +def handle(self, handler: EventHandler) -> EventHandler: + """ + 添加处理函数,装饰器 + Args: + handler: + Returns: + EventHandler + """ + self.handlers.append(handler) + return handler +``` +
+ diff --git a/docs/dev/api/message/on.md b/docs/dev/api/message/on.md new file mode 100644 index 00000000..4f3c7230 --- /dev/null +++ b/docs/dev/api/message/on.md @@ -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` + + + +
+源代码 + +```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 +``` +
+ +### ***var*** `current_priority = -1` + + + +### ***var*** `matcher = Matcher(rule, priority, block)` + + + +### ***var*** `current_priority = matcher.priority` + + + diff --git a/docs/dev/api/message/rule.md b/docs/dev/api/message/rule.md new file mode 100644 index 00000000..a135f740 --- /dev/null +++ b/docs/dev/api/message/rule.md @@ -0,0 +1,24 @@ +--- +title: liteyuki.message.rule +order: 1 +icon: laptop-code +category: API +--- + +### ***class*** `Rule` + + + +###   ***def*** `__init__(self, handler: Optional[RuleHandler]) -> None` + +  + +
+源代码 + +```python +def __init__(self, handler: Optional[RuleHandler]=None): + self.handler = handler +``` +
+ diff --git a/docs/dev/api/message/session.md b/docs/dev/api/message/session.md new file mode 100644 index 00000000..f8fcad6f --- /dev/null +++ b/docs/dev/api/message/session.md @@ -0,0 +1,7 @@ +--- +title: liteyuki.message.session +order: 1 +icon: laptop-code +category: API +--- + diff --git a/docs/dev/api/mkdoc.md b/docs/dev/api/mkdoc.md index 765d9a81..802aff3c 100644 --- a/docs/dev/api/mkdoc.md +++ b/docs/dev/api/mkdoc.md @@ -15,6 +15,21 @@ Args: target_path: 目标路径 +
+源代码 + +```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) +``` +
+ ### ***def*** `write_to_files(file_data: dict[str, str]) -> None` 输出文件 @@ -23,10 +38,42 @@ Args: file_data: 文件数据 相对路径 +
+源代码 + +```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) +``` +
+ ### ***def*** `get_file_list(module_folder: str) -> None` +
+源代码 + +```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 +``` +
+ ### ***def*** `get_module_info_normal(file_path: str, ignore_private: bool) -> ModuleInfo` 获取函数和类 @@ -41,6 +88,67 @@ Returns: 模块信息 +
+源代码 + +```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 +``` +
+ ### ***def*** `generate_markdown(module_info: ModuleInfo, front_matter: Any) -> str` 生成模块的Markdown @@ -57,6 +165,60 @@ Returns: Markdown 字符串 +
+源代码 + +```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'
\n源代码\n\n```python\n{func.source_code}\n```\n
\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'###   ***@{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"###   ***{('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' {method.docstring}\n\n' + content += f'
\n源代码\n\n```python\n{method.source_code}\n```\n
\n\n' + for attr in cls.attributes: + content += f'###   ***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 +``` +
+ ### ***def*** `generate_docs(module_folder: str, output_dir: str, with_top: bool, ignored_paths: Any) -> None` 生成文档 @@ -71,6 +233,46 @@ Args: ignored_paths: 忽略的路径 +
+源代码 + +```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) +``` +
+ ### ***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))` diff --git a/docs/dev/api/plugin/README.md b/docs/dev/api/plugin/README.md index 2e7020f9..6c67f485 100644 --- a/docs/dev/api/plugin/README.md +++ b/docs/dev/api/plugin/README.md @@ -13,3 +13,17 @@ Returns: dict[str, Plugin]: 插件字典 +
+源代码 + +```python +def get_loaded_plugins() -> dict[str, Plugin]: + """ + 获取已加载的插件 + Returns: + dict[str, Plugin]: 插件字典 + """ + return _plugins +``` +
+ diff --git a/docs/dev/api/plugin/load.md b/docs/dev/api/plugin/load.md index 9669642e..b999f5e1 100644 --- a/docs/dev/api/plugin/load.md +++ b/docs/dev/api/plugin/load.md @@ -17,6 +17,34 @@ category: API 或插件路径 `pathlib.Path(path/to/your/plugin)` +
+源代码 + +```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 "{module_path}"') + traceback.print_exc() + return None +``` +
+ ### ***def*** `load_plugins() -> set[Plugin]` 导入文件夹下多个插件 @@ -29,6 +57,46 @@ category: API ignore_warning: 是否忽略警告,通常是目录不存在或目录为空 +
+源代码 + +```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 +``` +
+ ### ***def*** `format_display_name(display_name: str, plugin_type: PluginType) -> str` 设置插件名称颜色,根据不同类型插件设置颜色 @@ -45,6 +113,34 @@ Returns: str: 设置后的插件名称 name +
+源代码 + +```python +def format_display_name(display_name: str, plugin_type: PluginType) -> str: + """ + 设置插件名称颜色,根据不同类型插件设置颜色 + Args: + display_name: 插件名称 + plugin_type: 插件类型 + + Returns: + str: 设置后的插件名称 name + """ + 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}]' +``` +
+ ### ***var*** `module_path = path_to_module_name(Path(module_path)) if isinstance(module_path, Path) else module_path` diff --git a/docs/dev/api/plugin/model.md b/docs/dev/api/plugin/model.md index fd8fec0c..6f986049 100644 --- a/docs/dev/api/plugin/model.md +++ b/docs/dev/api/plugin/model.md @@ -13,12 +13,12 @@ category: API ###   ***attr*** `SERVICE: 'service'` -###   ***attr*** `IMPLEMENTATION: 'implementation'` - ###   ***attr*** `MODULE: 'module'` ###   ***attr*** `UNCLASSIFIED: 'unclassified'` +###   ***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}` diff --git a/docs/dev/api/utils.md b/docs/dev/api/utils.md index b215a53d..84532f46 100644 --- a/docs/dev/api/utils.md +++ b/docs/dev/api/utils.md @@ -17,6 +17,27 @@ Returns: bool: 是否为协程可调用对象 +
+源代码 + +```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_) +``` +
+ ### ***def*** `run_coroutine() -> None` 运行协程 @@ -29,6 +50,37 @@ Args: Returns: +
+源代码 + +```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}') +``` +
+ ### ***def*** `path_to_module_name(path: Path) -> str` 转换路径为模块名 @@ -41,6 +93,26 @@ Returns: str: 模块名 +
+源代码 + +```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,)) +``` +
+ ### ***def*** `async_wrapper(func: Callable[..., Any]) -> Callable[..., Coroutine]` 异步包装器 @@ -53,10 +125,39 @@ Returns: Coroutine: Asynchronous Callable +
+源代码 + +```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 +``` +
+ ### ***async def*** `wrapper() -> None` +
+源代码 + +```python +async def wrapper(*args, **kwargs): + return func(*args, **kwargs) +``` +
+ ### ***var*** `IS_MAIN_PROCESS = multiprocessing.current_process().name == 'MainProcess'` diff --git a/docs/en/dev/api/README.md b/docs/en/dev/api/README.md new file mode 100644 index 00000000..181ec73d --- /dev/null +++ b/docs/en/dev/api/README.md @@ -0,0 +1,7 @@ +--- +title: liteyuki +index: true +icon: laptop-code +category: API +--- + diff --git a/docs/en/dev/api/bot/README.md b/docs/en/dev/api/bot/README.md new file mode 100644 index 00000000..9f019444 --- /dev/null +++ b/docs/en/dev/api/bot/README.md @@ -0,0 +1,581 @@ +--- +title: liteyuki.bot +index: true +icon: laptop-code +category: API +--- + +### ***def*** `get_bot() -> LiteyukiBot` + +获取轻雪实例 + + + +Returns: + + LiteyukiBot: 当前的轻雪实例 + +
+源代码 + +```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.") +``` +
+ +### ***def*** `get_config(key: str, default: Any) -> Any` + +获取配置 + +Args: + + key: 配置键 + + default: 默认值 + + + +Returns: + + Any: 配置值 + +
+源代码 + +```python +def get_config(key: str, default: Any=None) -> Any: + """ + 获取配置 + Args: + key: 配置键 + default: 默认值 + + Returns: + Any: 配置值 + """ + return get_bot().config.get(key, default) +``` +
+ +### ***def*** `get_config_with_compat(key: str, compat_keys: tuple[str], default: Any) -> Any` + +获取配置,兼容旧版本 + +Args: + + key: 配置键 + + compat_keys: 兼容键 + + default: 默认值 + + + +Returns: + + Any: 配置值 + +
+源代码 + +```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 +``` +
+ +### ***def*** `print_logo() -> None` + + + +
+源代码 + +```python +def print_logo(): + print('\x1b[34m' + '\n __ ______ ________ ________ __ __ __ __ __ __ ______ \n / | / |/ |/ |/ \\ / |/ | / |/ | / |/ |\n $$ | $$$$$$/ $$$$$$$$/ $$$$$$$$/ $$ \\ /$$/ $$ | $$ |$$ | /$$/ $$$$$$/ \n $$ | $$ | $$ | $$ |__ $$ \\/$$/ $$ | $$ |$$ |/$$/ $$ | \n $$ | $$ | $$ | $$ | $$ $$/ $$ | $$ |$$ $$< $$ | \n $$ | $$ | $$ | $$$$$/ $$$$/ $$ | $$ |$$$$$ \\ $$ | \n $$ |_____ _$$ |_ $$ | $$ |_____ $$ | $$ \\__$$ |$$ |$$ \\ _$$ |_ \n $$ |/ $$ | $$ | $$ | $$ | $$ $$/ $$ | $$ |/ $$ |\n $$$$$$$$/ $$$$$$/ $$/ $$$$$$$$/ $$/ $$$$$$/ $$/ $$/ $$$$$$/ \n ' + '\x1b[0m') +``` +
+ +### ***class*** `LiteyukiBot` + + + +###   ***def*** `__init__(self) -> None` + + 初始化轻雪实例 + +Args: + + *args: + + **kwargs: 配置 + +
+源代码 + +```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) +``` +
+ +###   ***def*** `run(self) -> None` + + 启动逻辑 + +
+源代码 + +```python +def run(self): + """ + 启动逻辑 + """ + self.lifespan.before_start() + self.process_manager.start_all() + self.lifespan.after_start() + self.keep_alive() +``` +
+ +###   ***def*** `keep_alive(self) -> None` + + 保持轻雪运行 + +Returns: + +
+源代码 + +```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() +``` +
+ +###   ***def*** `restart(self, delay: int) -> None` + + 重启轻雪本体 + +Returns: + +
+源代码 + +```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 +``` +
+ +###   ***def*** `restart_process(self, name: Optional[str]) -> None` + + 停止轻雪 + +Args: + + name: 进程名称, 默认为None, 所有进程 + +Returns: + +
+源代码 + +```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) +``` +
+ +###   ***def*** `init(self) -> None` + + 初始化轻雪, 自动调用 + +Returns: + +
+源代码 + +```python +def init(self, *args, **kwargs): + """ + 初始化轻雪, 自动调用 + Returns: + + """ + self.init_logger() +``` +
+ +###   ***def*** `init_logger(self) -> None` + +  + +
+源代码 + +```python +def init_logger(self): + init_log(config=self.config) +``` +
+ +###   ***def*** `stop(self) -> None` + + 停止轻雪 + +Returns: + +
+源代码 + +```python +def stop(self): + """ + 停止轻雪 + Returns: + + """ + self.stop_event.set() + self.loop.stop() +``` +
+ +###   ***def*** `on_before_start(self, func: LIFESPAN_FUNC) -> None` + + 注册启动前的函数 + +Args: + + func: + + + +Returns: + +
+源代码 + +```python +def on_before_start(self, func: LIFESPAN_FUNC): + """ + 注册启动前的函数 + Args: + func: + + Returns: + + """ + return self.lifespan.on_before_start(func) +``` +
+ +###   ***def*** `on_after_start(self, func: LIFESPAN_FUNC) -> None` + + 注册启动后的函数 + +Args: + + func: + + + +Returns: + +
+源代码 + +```python +def on_after_start(self, func: LIFESPAN_FUNC): + """ + 注册启动后的函数 + Args: + func: + + Returns: + + """ + return self.lifespan.on_after_start(func) +``` +
+ +###   ***def*** `on_after_shutdown(self, func: LIFESPAN_FUNC) -> None` + + 注册停止后的函数:未实现 + +Args: + + func: + + + +Returns: + +
+源代码 + +```python +def on_after_shutdown(self, func: LIFESPAN_FUNC): + """ + 注册停止后的函数:未实现 + Args: + func: + + Returns: + + """ + return self.lifespan.on_after_shutdown(func) +``` +
+ +###   ***def*** `on_before_process_shutdown(self, func: LIFESPAN_FUNC) -> None` + + 注册进程停止前的函数,为子进程停止时调用 + +Args: + + func: + + + +Returns: + +
+源代码 + +```python +def on_before_process_shutdown(self, func: LIFESPAN_FUNC): + """ + 注册进程停止前的函数,为子进程停止时调用 + Args: + func: + + Returns: + + """ + return self.lifespan.on_before_process_shutdown(func) +``` +
+ +###   ***def*** `on_before_process_restart(self, func: LIFESPAN_FUNC) -> None` + + 注册进程重启前的函数,为子进程重启时调用 + +Args: + + func: + + + +Returns: + +
+源代码 + +```python +def on_before_process_restart(self, func: LIFESPAN_FUNC): + """ + 注册进程重启前的函数,为子进程重启时调用 + Args: + func: + + Returns: + + """ + return self.lifespan.on_before_process_restart(func) +``` +
+ +###   ***def*** `on_after_restart(self, func: LIFESPAN_FUNC) -> None` + + 注册重启后的函数:未实现 + +Args: + + func: + + + +Returns: + +
+源代码 + +```python +def on_after_restart(self, func: LIFESPAN_FUNC): + """ + 注册重启后的函数:未实现 + Args: + func: + + Returns: + + """ + return self.lifespan.on_after_restart(func) +``` +
+ +###   ***def*** `on_after_nonebot_init(self, func: LIFESPAN_FUNC) -> None` + + 注册nonebot初始化后的函数 + +Args: + + func: + + + +Returns: + +
+源代码 + +```python +def on_after_nonebot_init(self, func: LIFESPAN_FUNC): + """ + 注册nonebot初始化后的函数 + Args: + func: + + Returns: + + """ + return self.lifespan.on_after_nonebot_init(func) +``` +
+ +### ***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'` + + + diff --git a/docs/en/dev/api/bot/lifespan.md b/docs/en/dev/api/bot/lifespan.md new file mode 100644 index 00000000..6466bd52 --- /dev/null +++ b/docs/en/dev/api/bot/lifespan.md @@ -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: + +
+源代码 + +```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)) +``` +
+ +### ***class*** `Lifespan` + + + +###   ***def*** `__init__(self) -> None` + + 轻雪生命周期管理,启动、停止、重启 + +
+源代码 + +```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] = [] +``` +
+ +###   ***@staticmethod*** +###   ***def*** `run_funcs(funcs: list[LIFESPAN_FUNC | PROCESS_LIFESPAN_FUNC]) -> None` + + 运行函数 + +Args: + + funcs: + +Returns: + +
+源代码 + +```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)) +``` +
+ +###   ***def*** `on_before_start(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC` + + 注册启动时的函数 + +Args: + + func: + +Returns: + + LIFESPAN_FUNC: + +
+源代码 + +```python +def on_before_start(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC: + """ + 注册启动时的函数 + Args: + func: + Returns: + LIFESPAN_FUNC: + """ + self._before_start_funcs.append(func) + return func +``` +
+ +###   ***def*** `on_after_start(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC` + + 注册启动时的函数 + +Args: + + func: + +Returns: + + LIFESPAN_FUNC: + +
+源代码 + +```python +def on_after_start(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC: + """ + 注册启动时的函数 + Args: + func: + Returns: + LIFESPAN_FUNC: + """ + self._after_start_funcs.append(func) + return func +``` +
+ +###   ***def*** `on_before_process_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC` + + 注册停止前的函数 + +Args: + + func: + +Returns: + + LIFESPAN_FUNC: + +
+源代码 + +```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 +``` +
+ +###   ***def*** `on_after_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC` + + 注册停止后的函数 + +Args: + + func: + + + +Returns: + + LIFESPAN_FUNC: + +
+源代码 + +```python +def on_after_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC: + """ + 注册停止后的函数 + Args: + func: + + Returns: + LIFESPAN_FUNC: + + """ + self._after_shutdown_funcs.append(func) + return func +``` +
+ +###   ***def*** `on_before_process_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC` + + 注册重启时的函数 + +Args: + + func: + +Returns: + + LIFESPAN_FUNC: + +
+源代码 + +```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 +``` +
+ +###   ***def*** `on_after_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC` + + 注册重启后的函数 + +Args: + + func: + +Returns: + + LIFESPAN_FUNC: + +
+源代码 + +```python +def on_after_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC: + """ + 注册重启后的函数 + Args: + func: + Returns: + LIFESPAN_FUNC: + """ + self._after_restart_funcs.append(func) + return func +``` +
+ +###   ***def*** `on_after_nonebot_init(self, func: Any) -> None` + + 注册 NoneBot 初始化后的函数 + +Args: + + func: + + + +Returns: + +
+源代码 + +```python +def on_after_nonebot_init(self, func): + """ + 注册 NoneBot 初始化后的函数 + Args: + func: + + Returns: + + """ + self._after_nonebot_init_funcs.append(func) + return func +``` +
+ +###   ***def*** `before_start(self) -> None` + + 启动前 + +Returns: + +
+源代码 + +```python +def before_start(self) -> None: + """ + 启动前 + Returns: + """ + logger.debug('Running before_start functions') + self.run_funcs(self._before_start_funcs) +``` +
+ +###   ***def*** `after_start(self) -> None` + + 启动后 + +Returns: + +
+源代码 + +```python +def after_start(self) -> None: + """ + 启动后 + Returns: + """ + logger.debug('Running after_start functions') + self.run_funcs(self._after_start_funcs) +``` +
+ +###   ***def*** `before_process_shutdown(self) -> None` + + 停止前 + +Returns: + +
+源代码 + +```python +def before_process_shutdown(self) -> None: + """ + 停止前 + Returns: + """ + logger.debug('Running before_shutdown functions') + self.run_funcs(self._before_process_shutdown_funcs) +``` +
+ +###   ***def*** `after_shutdown(self) -> None` + + 停止后 + +Returns: + +
+源代码 + +```python +def after_shutdown(self) -> None: + """ + 停止后 + Returns: + """ + logger.debug('Running after_shutdown functions') + self.run_funcs(self._after_shutdown_funcs) +``` +
+ +###   ***def*** `before_process_restart(self) -> None` + + 重启前 + +Returns: + +
+源代码 + +```python +def before_process_restart(self) -> None: + """ + 重启前 + Returns: + """ + logger.debug('Running before_restart functions') + self.run_funcs(self._before_process_restart_funcs) +``` +
+ +###   ***def*** `after_restart(self) -> None` + + 重启后 + +Returns: + +
+源代码 + +```python +def after_restart(self) -> None: + """ + 重启后 + Returns: + + """ + logger.debug('Running after_restart functions') + self.run_funcs(self._after_restart_funcs) +``` +
+ +### ***var*** `tasks = []` + + + +### ***var*** `loop = asyncio.get_event_loop()` + + + +### ***var*** `loop = asyncio.new_event_loop()` + + + diff --git a/docs/en/dev/api/comm/README.md b/docs/en/dev/api/comm/README.md new file mode 100644 index 00000000..09bccc34 --- /dev/null +++ b/docs/en/dev/api/comm/README.md @@ -0,0 +1,7 @@ +--- +title: liteyuki.comm +index: true +icon: laptop-code +category: API +--- + diff --git a/docs/en/dev/api/comm/channel.md b/docs/en/dev/api/comm/channel.md new file mode 100644 index 00000000..8e5e170f --- /dev/null +++ b/docs/en/dev/api/comm/channel.md @@ -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: 通道实例 + +
+源代码 + +```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})) +``` +
+ +### ***def*** `set_channels(channels: dict[str, Channel]) -> None` + +设置通道实例 + +Args: + + channels: 通道名称 + +
+源代码 + +```python +def set_channels(channels: dict[str, Channel]): + """ + 设置通道实例 + Args: + channels: 通道名称 + """ + for name, channel in channels.items(): + set_channel(name, channel) +``` +
+ +### ***def*** `get_channel(name: str) -> Channel` + +获取通道实例 + +Args: + + name: 通道名称 + +Returns: + +
+源代码 + +```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() +``` +
+ +### ***def*** `get_channels() -> dict[str, Channel]` + +获取通道实例 + +Returns: + +
+源代码 + +```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() +``` +
+ +### ***def*** `on_set_channel(data: tuple[str, dict[str, Any]]) -> None` + + + +
+源代码 + +```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) +``` +
+ +### ***def*** `on_get_channel(data: tuple[str, dict[str, Any]]) -> None` + + + +
+源代码 + +```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)) +``` +
+ +### ***def*** `on_get_channels(data: tuple[str, dict[str, Any]]) -> None` + + + +
+源代码 + +```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()) +``` +
+ +### ***def*** `decorator(func: Callable[[T], Any]) -> Callable[[T], Any]` + + + +
+源代码 + +```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 +``` +
+ +### ***async def*** `wrapper(data: T) -> Any` + + + +
+源代码 + +```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) +``` +
+ +### ***class*** `Channel(Generic[T])` + +通道类,可以在进程间和进程内通信,双向但同时只能有一个发送者和一个接收者 + +有两种接收工作方式,但是只能选择一种,主动接收和被动接收,主动接收使用 `receive` 方法,被动接收使用 `on_receive` 装饰器 + +###   ***def*** `__init__(self, _id: str, type_check: Optional[bool]) -> None` + + 初始化通道 + +Args: + + _id: 通道ID + + type_check: 是否开启类型检查, 若为空,则传入泛型默认开启,否则默认关闭 + +
+源代码 + +```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 +``` +
+ +###   ***def*** `send(self, data: T) -> None` + + 发送数据 + +Args: + + data: 数据 + +
+源代码 + +```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) +``` +
+ +###   ***def*** `receive(self) -> T` + + 接收数据 + +Args: + +
+源代码 + +```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 +``` +
+ +###   ***def*** `close(self) -> None` + + 关闭通道 + +
+源代码 + +```python +def close(self): + """ + 关闭通道 + """ + self._closed = True + self.conn_send.close() + self.conn_recv.close() +``` +
+ +###   ***def*** `on_receive(self, filter_func: Optional[FILTER_FUNC]) -> Callable[[Callable[[T], Any]], Callable[[T], Any]]` + + 接收数据并执行函数 + +Args: + + filter_func: 过滤函数,为None则不过滤 + +Returns: + + 装饰器,装饰一个函数在接收到数据后执行 + +
+源代码 + +```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 +``` +
+ +### ***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()` + + + diff --git a/docs/en/dev/api/comm/event.md b/docs/en/dev/api/comm/event.md new file mode 100644 index 00000000..a2a15f55 --- /dev/null +++ b/docs/en/dev/api/comm/event.md @@ -0,0 +1,25 @@ +--- +title: liteyuki.comm.event +order: 1 +icon: laptop-code +category: API +--- + +### ***class*** `Event` + +事件类 + +###   ***def*** `__init__(self, name: str, data: dict[str, Any]) -> None` + +  + +
+源代码 + +```python +def __init__(self, name: str, data: dict[str, Any]): + self.name = name + self.data = data +``` +
+ diff --git a/docs/en/dev/api/comm/storage.md b/docs/en/dev/api/comm/storage.md new file mode 100644 index 00000000..cd19dbb7 --- /dev/null +++ b/docs/en/dev/api/comm/storage.md @@ -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: 数据 + +
+源代码 + +```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_]]) +``` +
+ +### ***def*** `on_get(data: tuple[str, dict[str, Any]]) -> None` + + + +
+源代码 + +```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)) +``` +
+ +### ***def*** `on_set(data: tuple[str, dict[str, Any]]) -> None` + + + +
+源代码 + +```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) +``` +
+ +### ***def*** `on_delete(data: tuple[str, dict[str, Any]]) -> None` + + + +
+源代码 + +```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) +``` +
+ +### ***def*** `on_get_all(data: tuple[str, dict[str, Any]]) -> None` + + + +
+源代码 + +```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()) +``` +
+ +### ***def*** `on_publish(data: tuple[str, Any]) -> None` + + + +
+源代码 + +```python +@channel.publish_channel.on_receive() +def on_publish(data: tuple[str, Any]): + channel_, data = data + shared_memory.run_subscriber_receive_funcs(channel_, data) +``` +
+ +### ***def*** `decorator(func: ON_RECEIVE_FUNC) -> ON_RECEIVE_FUNC` + + + +
+源代码 + +```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 +``` +
+ +### ***async def*** `wrapper(data: Any) -> None` + + + +
+源代码 + +```python +async def wrapper(data: Any): + if is_coroutine_callable(func): + await func(data) + else: + func(data) +``` +
+ +### ***class*** `Subscriber` + + + +###   ***def*** `__init__(self) -> None` + +  + +
+源代码 + +```python +def __init__(self): + self._subscribers = {} +``` +
+ +###   ***def*** `receive(self) -> Any` + +  + +
+源代码 + +```python +def receive(self) -> Any: + pass +``` +
+ +###   ***def*** `unsubscribe(self) -> None` + +  + +
+源代码 + +```python +def unsubscribe(self) -> None: + pass +``` +
+ +### ***class*** `KeyValueStore` + + + +###   ***def*** `__init__(self) -> None` + +  + +
+源代码 + +```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 +``` +
+ +###   ***def*** `set(self, key: str, value: Any) -> None` + + 设置键值对 + +Args: + + key: 键 + + value: 值 + +
+源代码 + +```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})) +``` +
+ +###   ***def*** `get(self, key: str, default: Optional[Any]) -> Optional[Any]` + + 获取键值对 + +Args: + + key: 键 + + default: 默认值 + + + +Returns: + + Any: 值 + +
+源代码 + +```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() +``` +
+ +###   ***def*** `delete(self, key: str, ignore_key_error: bool) -> None` + + 删除键值对 + +Args: + + key: 键 + + ignore_key_error: 是否忽略键不存在的错误 + + + +Returns: + +
+源代码 + +```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})) +``` +
+ +###   ***def*** `get_all(self) -> dict[str, Any]` + + 获取所有键值对 + +Returns: + + dict[str, Any]: 键值对 + +
+源代码 + +```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() +``` +
+ +###   ***def*** `publish(self, channel_: str, data: Any) -> None` + + 发布消息 + +Args: + + channel_: 频道 + + data: 数据 + + + +Returns: + +
+源代码 + +```python +def publish(self, channel_: str, data: Any) -> None: + """ + 发布消息 + Args: + channel_: 频道 + data: 数据 + + Returns: + """ + self.active_chan.send(('publish', {'channel': channel_, 'data': data})) +``` +
+ +###   ***def*** `on_subscriber_receive(self, channel_: str) -> Callable[[ON_RECEIVE_FUNC], ON_RECEIVE_FUNC]` + + 订阅者接收消息时的回调 + +Args: + + channel_: 频道 + + + +Returns: + + 装饰器 + +
+源代码 + +```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 +``` +
+ +###   ***@staticmethod*** +###   ***def*** `run_subscriber_receive_funcs(channel_: str, data: Any) -> None` + + 运行订阅者接收函数 + +Args: + + channel_: 频道 + + data: 数据 + +
+源代码 + +```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_]]) +``` +
+ +### ***class*** `GlobalKeyValueStore` + + + +###   ***@classmethod*** +###   ***def*** `get_instance(cls: Any) -> None` + +  + +
+源代码 + +```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 +``` +
+ +###   ***attr*** `_instance: None` + +###   ***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()` + + + diff --git a/docs/en/dev/api/config.md b/docs/en/dev/api/config.md new file mode 100644 index 00000000..24afd6c9 --- /dev/null +++ b/docs/en/dev/api/config.md @@ -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: + + 扁平化后的配置文件,但也包含原有的键值对 + +
+源代码 + +```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 +``` +
+ +### ***def*** `load_from_yaml(file: str) -> dict[str, Any]` + +Load config from yaml file + +
+源代码 + +```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 {}) +``` +
+ +### ***def*** `load_from_json(file: str) -> dict[str, Any]` + +Load config from json file + +
+源代码 + +```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 {}) +``` +
+ +### ***def*** `load_from_toml(file: str) -> dict[str, Any]` + +Load config from toml file + +
+源代码 + +```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 {}) +``` +
+ +### ***def*** `load_from_files() -> dict[str, Any]` + +从指定文件加载配置项,会自动识别文件格式 + +默认执行扁平化选项 + +
+源代码 + +```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 +``` +
+ +### ***def*** `load_configs_from_dirs() -> dict[str, Any]` + +从目录下加载配置文件,不递归 + +按照读取文件的优先级反向覆盖 + +默认执行扁平化选项 + +
+源代码 + +```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 +``` +
+ +### ***def*** `load_config_in_default(no_waring: bool) -> dict[str, Any]` + +从一个标准的轻雪项目加载配置文件 + +项目目录下的config.*和config目录下的所有配置文件 + +项目目录下的配置文件优先 + +
+源代码 + +```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 +``` +
+ +### ***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)` + + + diff --git a/docs/en/dev/api/core/README.md b/docs/en/dev/api/core/README.md new file mode 100644 index 00000000..26c3500b --- /dev/null +++ b/docs/en/dev/api/core/README.md @@ -0,0 +1,7 @@ +--- +title: liteyuki.core +index: true +icon: laptop-code +category: API +--- + diff --git a/docs/en/dev/api/core/manager.md b/docs/en/dev/api/core/manager.md new file mode 100644 index 00000000..52397375 --- /dev/null +++ b/docs/en/dev/api/core/manager.md @@ -0,0 +1,275 @@ +--- +title: liteyuki.core.manager +order: 1 +icon: laptop-code +category: API +--- + +### ***class*** `ChannelDeliver` + + + +###   ***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` + +  + +
+源代码 + +```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 +``` +
+ +### ***class*** `ProcessManager` + +进程管理器 + +###   ***def*** `__init__(self, lifespan: 'Lifespan') -> None` + +  + +
+源代码 + +```python +def __init__(self, lifespan: 'Lifespan'): + self.lifespan = lifespan + self.targets: dict[str, tuple[Callable, tuple, dict]] = {} + self.processes: dict[str, Process] = {} +``` +
+ +###   ***def*** `start(self, name: str) -> None` + + 开启后自动监控进程,并添加到进程字典中 + +Args: + + name: + +Returns: + +
+源代码 + +```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.') +``` +
+ +###   ***def*** `start_all(self) -> None` + + 启动所有进程 + +
+源代码 + +```python +def start_all(self): + """ + 启动所有进程 + """ + for name in self.targets: + threading.Thread(target=self.start, args=(name,), daemon=True).start() +``` +
+ +###   ***def*** `add_target(self, name: str, target: TARGET_FUNC, args: tuple, kwargs: Any) -> None` + + 添加进程 + +Args: + + name: 进程名,用于获取和唯一标识 + + target: 进程函数 + + args: 进程函数参数 + + kwargs: 进程函数关键字参数,通常会默认传入chan_active和chan_passive + +
+源代码 + +```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}) +``` +
+ +###   ***def*** `join_all(self) -> None` + +  + +
+源代码 + +```python +def join_all(self): + for name, process in self.targets: + process.join() +``` +
+ +###   ***def*** `terminate(self, name: str) -> None` + + 终止进程并从进程字典中删除 + +Args: + + name: + + + +Returns: + +
+源代码 + +```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.') +``` +
+ +###   ***def*** `terminate_all(self) -> None` + +  + +
+源代码 + +```python +def terminate_all(self): + for name in self.targets: + self.terminate(name) +``` +
+ +###   ***def*** `is_process_alive(self, name: str) -> bool` + + 检查进程是否存活 + +Args: + + name: + + + +Returns: + +
+源代码 + +```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() +``` +
+ +### ***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 = {}` + + + diff --git a/docs/en/dev/api/dev/README.md b/docs/en/dev/api/dev/README.md new file mode 100644 index 00000000..6d883442 --- /dev/null +++ b/docs/en/dev/api/dev/README.md @@ -0,0 +1,7 @@ +--- +title: liteyuki.dev +index: true +icon: laptop-code +category: API +--- + diff --git a/docs/en/dev/api/dev/observer.md b/docs/en/dev/api/dev/observer.md new file mode 100644 index 00000000..2005bbf2 --- /dev/null +++ b/docs/en/dev/api/dev/observer.md @@ -0,0 +1,249 @@ +--- +title: liteyuki.dev.observer +order: 1 +icon: laptop-code +category: API +--- + +### ***def*** `debounce(wait: Any) -> None` + +防抖函数 + +
+源代码 + +```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 +``` +
+ +### ***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: + + 装饰器,装饰一个函数在接收到数据后执行 + +
+源代码 + +```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 +``` +
+ +### ***def*** `decorator(func: Any) -> None` + + + +
+源代码 + +```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 +``` +
+ +### ***def*** `decorator(func: CALLBACK_FUNC) -> CALLBACK_FUNC` + + + +
+源代码 + +```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 +``` +
+ +### ***def*** `wrapper() -> None` + + + +
+源代码 + +```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) +``` +
+ +### ***def*** `wrapper(event: FileSystemEvent) -> None` + + + +
+源代码 + +```python +def wrapper(event: FileSystemEvent): + if event_filter is not None and (not event_filter(event)): + return + func(event) +``` +
+ +### ***class*** `CodeModifiedHandler(FileSystemEventHandler)` + +Handler for code file changes + +###   ***def*** `on_modified(self, event: Any) -> None` + +  + +
+源代码 + +```python +@debounce(1) +def on_modified(self, event): + raise NotImplementedError('on_modified must be implemented') +``` +
+ +###   ***def*** `on_created(self, event: Any) -> None` + +  + +
+源代码 + +```python +def on_created(self, event): + self.on_modified(event) +``` +
+ +###   ***def*** `on_deleted(self, event: Any) -> None` + +  + +
+源代码 + +```python +def on_deleted(self, event): + self.on_modified(event) +``` +
+ +###   ***def*** `on_moved(self, event: Any) -> None` + +  + +
+源代码 + +```python +def on_moved(self, event): + self.on_modified(event) +``` +
+ +###   ***def*** `on_any_event(self, event: Any) -> None` + +  + +
+源代码 + +```python +def on_any_event(self, event): + self.on_modified(event) +``` +
+ +### ***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` + + + diff --git a/docs/en/dev/api/dev/plugin.md b/docs/en/dev/api/dev/plugin.md new file mode 100644 index 00000000..caafa5ea --- /dev/null +++ b/docs/en/dev/api/dev/plugin.md @@ -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`的函数签名 + +
+源代码 + +```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() +``` +
+ +### ***var*** `cfg = load_config_in_default()` + + + +### ***var*** `plugins = cfg.get('liteyuki.plugins', [])` + + + +### ***var*** `bot = LiteyukiBot(**cfg)` + + + diff --git a/docs/en/dev/api/exception.md b/docs/en/dev/api/exception.md new file mode 100644 index 00000000..469c00d6 --- /dev/null +++ b/docs/en/dev/api/exception.md @@ -0,0 +1,11 @@ +--- +title: liteyuki.exception +order: 1 +icon: laptop-code +category: API +--- + +### ***class*** `LiteyukiException(BaseException)` + +Liteyuki的异常基类。 + diff --git a/docs/en/dev/api/log.md b/docs/en/dev/api/log.md new file mode 100644 index 00000000..af382275 --- /dev/null +++ b/docs/en/dev/api/log.md @@ -0,0 +1,58 @@ +--- +title: liteyuki.log +order: 1 +icon: laptop-code +category: API +--- + +### ***def*** `get_format(level: str) -> str` + + + +
+源代码 + +```python +def get_format(level: str) -> str: + if level == 'DEBUG': + return debug_format + else: + return default_format +``` +
+ +### ***def*** `init_log(config: dict) -> None` + +在语言加载完成后执行 + +Returns: + +
+源代码 + +```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='', icon=f"{('🐛' if show_icon else '')}DEBUG") + logger.level('INFO', color='', icon=f"{('ℹ️' if show_icon else '')}INFO") + logger.level('SUCCESS', color='', icon=f"{('✅' if show_icon else '')}SUCCESS") + logger.level('WARNING', color='', icon=f"{('⚠️' if show_icon else '')}WARNING") + logger.level('ERROR', color='', icon=f"{('⭕' if show_icon else '')}ERROR") +``` +
+ +### ***var*** `logger = loguru.logger` + + + +### ***var*** `show_icon = config.get('log_icon', True)` + + + diff --git a/docs/en/dev/api/message/README.md b/docs/en/dev/api/message/README.md new file mode 100644 index 00000000..0d664851 --- /dev/null +++ b/docs/en/dev/api/message/README.md @@ -0,0 +1,7 @@ +--- +title: liteyuki.message +index: true +icon: laptop-code +category: API +--- + diff --git a/docs/en/dev/api/message/event.md b/docs/en/dev/api/message/event.md new file mode 100644 index 00000000..740ff8bd --- /dev/null +++ b/docs/en/dev/api/message/event.md @@ -0,0 +1,106 @@ +--- +title: liteyuki.message.event +order: 1 +icon: laptop-code +category: API +--- + +### ***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: + + + + 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: 附加数据 + +
+源代码 + +```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 +``` +
+ +###   ***def*** `reply(self, message: str | dict[str, Any]) -> None` + + 回复消息 + +Args: + + message: + +Returns: + +
+源代码 + +```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) +``` +
+ +### ***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 = {}` + + + diff --git a/docs/en/dev/api/message/matcher.md b/docs/en/dev/api/message/matcher.md new file mode 100644 index 00000000..4a6341b6 --- /dev/null +++ b/docs/en/dev/api/message/matcher.md @@ -0,0 +1,71 @@ +--- +title: liteyuki.message.matcher +order: 1 +icon: laptop-code +category: API +--- + +### ***class*** `Matcher` + + + +###   ***def*** `__init__(self, rule: Rule, priority: int, block: bool) -> None` + + 匹配器 + +Args: + + rule: 规则 + + priority: 优先级 >= 0 + + block: 是否阻断后续优先级更低的匹配器 + +
+源代码 + +```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] = [] +``` +
+ +###   ***def*** `handle(self, handler: EventHandler) -> EventHandler` + + 添加处理函数,装饰器 + +Args: + + handler: + +Returns: + + EventHandler + +
+源代码 + +```python +def handle(self, handler: EventHandler) -> EventHandler: + """ + 添加处理函数,装饰器 + Args: + handler: + Returns: + EventHandler + """ + self.handlers.append(handler) + return handler +``` +
+ diff --git a/docs/en/dev/api/message/on.md b/docs/en/dev/api/message/on.md new file mode 100644 index 00000000..4f3c7230 --- /dev/null +++ b/docs/en/dev/api/message/on.md @@ -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` + + + +
+源代码 + +```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 +``` +
+ +### ***var*** `current_priority = -1` + + + +### ***var*** `matcher = Matcher(rule, priority, block)` + + + +### ***var*** `current_priority = matcher.priority` + + + diff --git a/docs/en/dev/api/message/rule.md b/docs/en/dev/api/message/rule.md new file mode 100644 index 00000000..a135f740 --- /dev/null +++ b/docs/en/dev/api/message/rule.md @@ -0,0 +1,24 @@ +--- +title: liteyuki.message.rule +order: 1 +icon: laptop-code +category: API +--- + +### ***class*** `Rule` + + + +###   ***def*** `__init__(self, handler: Optional[RuleHandler]) -> None` + +  + +
+源代码 + +```python +def __init__(self, handler: Optional[RuleHandler]=None): + self.handler = handler +``` +
+ diff --git a/docs/en/dev/api/message/session.md b/docs/en/dev/api/message/session.md new file mode 100644 index 00000000..f8fcad6f --- /dev/null +++ b/docs/en/dev/api/message/session.md @@ -0,0 +1,7 @@ +--- +title: liteyuki.message.session +order: 1 +icon: laptop-code +category: API +--- + diff --git a/docs/en/dev/api/mkdoc.md b/docs/en/dev/api/mkdoc.md new file mode 100644 index 00000000..802aff3c --- /dev/null +++ b/docs/en/dev/api/mkdoc.md @@ -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: 目标路径 + +
+源代码 + +```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) +``` +
+ +### ***def*** `write_to_files(file_data: dict[str, str]) -> None` + +输出文件 + +Args: + + file_data: 文件数据 相对路径 + +
+源代码 + +```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) +``` +
+ +### ***def*** `get_file_list(module_folder: str) -> None` + + + +
+源代码 + +```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 +``` +
+ +### ***def*** `get_module_info_normal(file_path: str, ignore_private: bool) -> ModuleInfo` + +获取函数和类 + +Args: + + file_path: Python 文件路径 + + ignore_private: 忽略私有函数和类 + +Returns: + + 模块信息 + +
+源代码 + +```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 +``` +
+ +### ***def*** `generate_markdown(module_info: ModuleInfo, front_matter: Any) -> str` + +生成模块的Markdown + +你可在此自定义生成的Markdown格式 + +Args: + + module_info: 模块信息 + + front_matter: 自定义选项title, index, icon, category + +Returns: + + Markdown 字符串 + +
+源代码 + +```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'
\n源代码\n\n```python\n{func.source_code}\n```\n
\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'###   ***@{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"###   ***{('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' {method.docstring}\n\n' + content += f'
\n源代码\n\n```python\n{method.source_code}\n```\n
\n\n' + for attr in cls.attributes: + content += f'###   ***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 +``` +
+ +### ***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: 忽略的路径 + +
+源代码 + +```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) +``` +
+ +### ***class*** `DefType(Enum)` + + + +###   ***attr*** `FUNCTION: 'function'` + +###   ***attr*** `METHOD: 'method'` + +###   ***attr*** `STATIC_METHOD: 'staticmethod'` + +###   ***attr*** `CLASS_METHOD: 'classmethod'` + +###   ***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` + + + diff --git a/docs/en/dev/api/plugin/README.md b/docs/en/dev/api/plugin/README.md new file mode 100644 index 00000000..6c67f485 --- /dev/null +++ b/docs/en/dev/api/plugin/README.md @@ -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]: 插件字典 + +
+源代码 + +```python +def get_loaded_plugins() -> dict[str, Plugin]: + """ + 获取已加载的插件 + Returns: + dict[str, Plugin]: 插件字典 + """ + return _plugins +``` +
+ diff --git a/docs/en/dev/api/plugin/load.md b/docs/en/dev/api/plugin/load.md new file mode 100644 index 00000000..b999f5e1 --- /dev/null +++ b/docs/en/dev/api/plugin/load.md @@ -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)` + +
+源代码 + +```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 "{module_path}"') + traceback.print_exc() + return None +``` +
+ +### ***def*** `load_plugins() -> set[Plugin]` + +导入文件夹下多个插件 + + + +参数: + + plugin_dir: 文件夹路径 + + ignore_warning: 是否忽略警告,通常是目录不存在或目录为空 + +
+源代码 + +```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 +``` +
+ +### ***def*** `format_display_name(display_name: str, plugin_type: PluginType) -> str` + +设置插件名称颜色,根据不同类型插件设置颜色 + +Args: + + display_name: 插件名称 + + plugin_type: 插件类型 + + + +Returns: + + str: 设置后的插件名称 name + +
+源代码 + +```python +def format_display_name(display_name: str, plugin_type: PluginType) -> str: + """ + 设置插件名称颜色,根据不同类型插件设置颜色 + Args: + display_name: 插件名称 + plugin_type: 插件类型 + + Returns: + str: 设置后的插件名称 name + """ + 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}]' +``` +
+ +### ***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)` + + + diff --git a/docs/en/dev/api/plugin/manager.md b/docs/en/dev/api/plugin/manager.md new file mode 100644 index 00000000..7d4d951c --- /dev/null +++ b/docs/en/dev/api/plugin/manager.md @@ -0,0 +1,7 @@ +--- +title: liteyuki.plugin.manager +order: 1 +icon: laptop-code +category: API +--- + diff --git a/docs/en/dev/api/plugin/model.md b/docs/en/dev/api/plugin/model.md new file mode 100644 index 00000000..6f986049 --- /dev/null +++ b/docs/en/dev/api/plugin/model.md @@ -0,0 +1,89 @@ +--- +title: liteyuki.plugin.model +order: 1 +icon: laptop-code +category: API +--- + +### ***class*** `PluginType(Enum)` + +插件类型枚举值 + +###   ***attr*** `APPLICATION: 'application'` + +###   ***attr*** `SERVICE: 'service'` + +###   ***attr*** `MODULE: 'module'` + +###   ***attr*** `UNCLASSIFIED: 'unclassified'` + +###   ***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)` + +存储插件信息 + +###   ***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}` + + + diff --git a/docs/en/dev/api/utils.md b/docs/en/dev/api/utils.md new file mode 100644 index 00000000..84532f46 --- /dev/null +++ b/docs/en/dev/api/utils.md @@ -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: 是否为协程可调用对象 + +
+源代码 + +```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_) +``` +
+ +### ***def*** `run_coroutine() -> None` + +运行协程 + +Args: + + coro: + + + +Returns: + +
+源代码 + +```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}') +``` +
+ +### ***def*** `path_to_module_name(path: Path) -> str` + +转换路径为模块名 + +Args: + + path: 路径a/b/c/d -> a.b.c.d + +Returns: + + str: 模块名 + +
+源代码 + +```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,)) +``` +
+ +### ***def*** `async_wrapper(func: Callable[..., Any]) -> Callable[..., Coroutine]` + +异步包装器 + +Args: + + func: Sync Callable + +Returns: + + Coroutine: Asynchronous Callable + +
+源代码 + +```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 +``` +
+ +### ***async def*** `wrapper() -> None` + + + +
+源代码 + +```python +async def wrapper(*args, **kwargs): + return func(*args, **kwargs) +``` +
+ +### ***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()` + + + diff --git a/liteyuki/mkdoc.py b/liteyuki/mkdoc.py index b62ba5b6..fb179473 100644 --- a/liteyuki/mkdoc.py +++ b/liteyuki/mkdoc.py @@ -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): @@ -223,8 +226,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 +234,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"
\n源代码\n\n```python\n{func.source_code}\n```\n
\n\n" + # 类 for cls in module_info.classes: if cls.inherit: @@ -256,6 +260,8 @@ def generate_markdown(module_info: ModuleInfo, front_matter=None) -> str: method.docstring = method.docstring.replace("\n", "\n\n") content += f" {method.docstring}\n\n" + # 函数源代码可展开区域 + content += f"
\n源代码\n\n```python\n{method.source_code}\n```\n
\n\n" for attr in cls.attributes: content += f"###   ***attr*** `{attr.name}: {attr.type}`\n\n"