butterbot-python 3.1.0.dev1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. butterbot/__init__.py +15 -0
  2. butterbot/app/__init__.py +41 -0
  3. butterbot/app/bot_app.py +374 -0
  4. butterbot/app/config.py +156 -0
  5. butterbot/app/source_manager.py +367 -0
  6. butterbot/core/__init__.py +46 -0
  7. butterbot/core/api/__init__.py +9 -0
  8. butterbot/core/api/base_api.py +32 -0
  9. butterbot/core/context/README.md +1 -0
  10. butterbot/core/context/__init__.py +9 -0
  11. butterbot/core/context/api_registry.py +109 -0
  12. butterbot/core/context/app_context.py +51 -0
  13. butterbot/core/context/config_provider.py +14 -0
  14. butterbot/core/data/__init__.py +15 -0
  15. butterbot/core/data/base_data.py +29 -0
  16. butterbot/core/data/base_model.py +197 -0
  17. butterbot/core/event/README.md +1 -0
  18. butterbot/core/event/__init__.py +10 -0
  19. butterbot/core/event/event.py +27 -0
  20. butterbot/core/event/event_bus.py +276 -0
  21. butterbot/core/event/subscriber.py +130 -0
  22. butterbot/core/exceptions.py +90 -0
  23. butterbot/core/filter/__init__.py +11 -0
  24. butterbot/core/filter/base_filter.py +74 -0
  25. butterbot/core/source/README.md +1 -0
  26. butterbot/core/source/__init__.py +9 -0
  27. butterbot/core/source/base_source.py +134 -0
  28. butterbot/core/types/__init__.py +9 -0
  29. butterbot/core/types/base_type.py +74 -0
  30. butterbot/sources/__init__.py +1 -0
  31. butterbot/sources/bilibili/README.md +10 -0
  32. butterbot/sources/bilibili/__init__.py +20 -0
  33. butterbot/sources/bilibili/api/__init__.py +5 -0
  34. butterbot/sources/bilibili/api/bili_api.py +123 -0
  35. butterbot/sources/bilibili/data/__init__.py +52 -0
  36. butterbot/sources/bilibili/data/danmaku_gift_data.py +135 -0
  37. butterbot/sources/bilibili/data/danmaku_guard_data.py +54 -0
  38. butterbot/sources/bilibili/data/danmaku_msg_data.py +71 -0
  39. butterbot/sources/bilibili/data/dto/__init__.py +59 -0
  40. butterbot/sources/bilibili/data/dto/danmaku_gift_dto.py +193 -0
  41. butterbot/sources/bilibili/data/dto/danmaku_guard_buy_dto.py +54 -0
  42. butterbot/sources/bilibili/data/dto/danmaku_msg_dto.py +123 -0
  43. butterbot/sources/bilibili/data/dto/dynamic_dto.py +276 -0
  44. butterbot/sources/bilibili/data/dto/live_room_dto.py +169 -0
  45. butterbot/sources/bilibili/data/dto/video_part_dto.py +18 -0
  46. butterbot/sources/bilibili/data/dynamic_data.py +362 -0
  47. butterbot/sources/bilibili/data/live_room_data.py +162 -0
  48. butterbot/sources/bilibili/data/video_part.py +46 -0
  49. butterbot/sources/bilibili/source/__init__.py +15 -0
  50. butterbot/sources/bilibili/source/base_polling_source.py +130 -0
  51. butterbot/sources/bilibili/source/bili_danmaku_source.py +230 -0
  52. butterbot/sources/bilibili/source/bili_dynamic_source.py +135 -0
  53. butterbot/sources/bilibili/source/bili_live_source.py +137 -0
  54. butterbot/sources/bilibili/types/__init__.py +11 -0
  55. butterbot/sources/bilibili/types/bili_type.py +32 -0
  56. butterbot/sources/napcat/README.md +10 -0
  57. butterbot/sources/napcat/__init__.py +20 -0
  58. butterbot/sources/napcat/api/__init__.py +3 -0
  59. butterbot/sources/napcat/api/napcat_api.py +316 -0
  60. butterbot/sources/napcat/data/__init__.py +179 -0
  61. butterbot/sources/napcat/data/event_data.py +441 -0
  62. butterbot/sources/napcat/data/segment_data.py +432 -0
  63. butterbot/sources/napcat/events.py +91 -0
  64. butterbot/sources/napcat/filters/__init__.py +19 -0
  65. butterbot/sources/napcat/filters/filters.py +202 -0
  66. butterbot/sources/napcat/source/__init__.py +5 -0
  67. butterbot/sources/napcat/source/napcat_source.py +58 -0
  68. butterbot/sources/napcat/types/__init__.py +5 -0
  69. butterbot/sources/napcat/types/napcat_type.py +61 -0
  70. butterbot/utils/README.md +15 -0
  71. butterbot/utils/__init__.py +11 -0
  72. butterbot/utils/data_pair.py +27 -0
  73. butterbot/utils/logging_config.py +521 -0
  74. butterbot/utils/terminal.py +308 -0
  75. butterbot/utils/websocket.py +1270 -0
  76. butterbot_python-3.1.0.dev1.dist-info/METADATA +769 -0
  77. butterbot_python-3.1.0.dev1.dist-info/RECORD +79 -0
  78. butterbot_python-3.1.0.dev1.dist-info/WHEEL +4 -0
  79. butterbot_python-3.1.0.dev1.dist-info/licenses/LICENSE +674 -0
@@ -0,0 +1,197 @@
1
+ from typing import ClassVar, Generic, Self, TypeVar
2
+
3
+ from pydantic import BaseModel, ConfigDict, RootModel, model_validator
4
+ from pydantic._internal._model_construction import ModelMetaclass
5
+
6
+ from .base_data import BaseDataMixin
7
+
8
+
9
+ class MetaDataModel(ModelMetaclass):
10
+ """pydantic数据模型元类,支持单层和多层继承的 discriminator 注册机制.
11
+ 1. 根类定义 discriminator_field,子类定义 discriminator_value,自动注册到 registry
12
+ 2. 支持属性嵌套
13
+ 3. 完全自动注册和分发
14
+ """
15
+
16
+ def __new__(mcls, name, bases, namespace, **kwargs):
17
+ cls = super().__new__(mcls, name, bases, namespace, **kwargs)
18
+
19
+ # 沿每个直接父类的 MRO 查找最近的分发根。这样既支持
20
+ # “分发根 → 共享字段基类 → 叶子类型”的间接注册,也不会把
21
+ # BaseDataModel 的全局 registry 误当作普通 DTO 的分发目标。
22
+ base_with_registry = None
23
+ for base in bases:
24
+ for ancestor in base.__mro__:
25
+ if (
26
+ "discriminator_field" in ancestor.__dict__
27
+ and "_registry" in ancestor.__dict__
28
+ ):
29
+ base_with_registry = ancestor
30
+ break
31
+ if base_with_registry is not None:
32
+ break
33
+
34
+ discriminator_value = namespace.get("discriminator_value")
35
+ discriminator_field = namespace.get("discriminator_field")
36
+
37
+ # 如果子类有 discriminator_value,注册到父类的 registry
38
+ if base_with_registry and discriminator_value is not None:
39
+ if isinstance(discriminator_value, (list, tuple, set)):
40
+ for value in discriminator_value:
41
+ base_with_registry._registry[value] = cls
42
+ else:
43
+ base_with_registry._registry[discriminator_value] = cls
44
+
45
+ # 如果当前类定义了 discriminator_field,初始化自己的 registry(用于二级分发)
46
+ if discriminator_field is not None:
47
+ cls._registry = {}
48
+
49
+ return cls
50
+
51
+
52
+ class BaseDataModel(BaseModel, BaseDataMixin, metaclass=MetaDataModel):
53
+ """
54
+ 基于BaseDataMixin实现的领域模型基类
55
+ 1. 使用pydantic进行数据校验
56
+ 2. 支持单层和多层继承的 discriminator 注册机制
57
+ 3. 根类定义 discriminator_field,子类定义 discriminator_value,自动注册到 registry
58
+ """
59
+
60
+ model_config = ConfigDict(
61
+ strict=False, # 强制转换数据
62
+ frozen=True, # 实例不可变,自动生成 __hash__
63
+ extra="ignore", # 额外字段将被忽略
64
+ )
65
+
66
+ # discriminator_field: ClassVar[str] = "data_type"
67
+ # 作为分发依据的"键",必须在根/基类定义
68
+ # discriminator_value: ClassVar[str] = "group"
69
+ # 作为分发依据的"值",子类可选定义,若定义则认为是可直接构造模型,否则则认为是属性嵌套的分发模型
70
+
71
+ _registry: ClassVar[dict] = {}
72
+
73
+ @classmethod
74
+ def from_raw(cls, raw: dict) -> Self:
75
+ """从原始数据构造实例,可实现复杂构造逻辑
76
+
77
+ Args:
78
+ raw: 原始数据字典
79
+
80
+ Returns:
81
+ 对应子类的实例
82
+ """
83
+ ...
84
+
85
+ @classmethod
86
+ def from_dict(cls, raw: dict) -> Self:
87
+ """从dict自动构造实例,支持多层分发
88
+
89
+ Args:
90
+ raw: 原始数据字典
91
+
92
+ Returns:
93
+ 对应子类的实例
94
+ """
95
+ field = getattr(cls, "discriminator_field", None)
96
+ if field is None:
97
+ # 没有 discriminator_field,直接构造
98
+ return cls.model_validate(raw)
99
+
100
+ value = raw.get(field)
101
+ if value is None:
102
+ raise ValueError("Missing discriminator field: %s" % field)
103
+
104
+ subclass = cls._registry.get(value)
105
+ if subclass is None:
106
+ raise ValueError("Unknown type value: %s" % value)
107
+
108
+ # 递归检查子类是否还有自己的 discriminator_field(多层分发)
109
+ sub_field = getattr(subclass, "discriminator_field", None)
110
+ if (
111
+ sub_field is not None
112
+ and sub_field != field
113
+ and hasattr(subclass, "_registry")
114
+ and subclass._registry
115
+ ):
116
+ # 子类有自己的 discriminator_field,继续递归分发
117
+ return subclass.from_dict(raw)
118
+ else:
119
+ # 子类没有 discriminator_field,直接构造
120
+ return subclass.model_validate(raw)
121
+
122
+ @classmethod
123
+ def from_type(cls, data: dict, type_value: str, raw: bool = True) -> Self:
124
+ """根据指定的type_value从dict构造实例
125
+
126
+ Args:
127
+ data: 原始数据字典
128
+ type_value: discriminator_value的值,用于匹配具体子类
129
+ raw: 是否从model_validate方法构造实例,
130
+ 否则调用子类的from_raw方法.
131
+
132
+ Returns:
133
+ 对应子类的实例
134
+ """
135
+ subclass = cls._registry.get(type_value)
136
+ if subclass is None:
137
+ raise ValueError("Unknown type value: %s" % type_value)
138
+ if raw:
139
+ return subclass.model_validate(data)
140
+ else:
141
+ return subclass.from_raw(data)
142
+
143
+ def __repr__(self):
144
+ # 覆盖BaseModel的__repr__,使用BaseDataMixin的实现
145
+ return BaseDataMixin.__repr__(self)
146
+
147
+ def __str__(self):
148
+ return self.__repr__()
149
+
150
+
151
+ BaseDataModelT = TypeVar("BaseDataModelT", bound=BaseDataModel)
152
+
153
+
154
+ class AutoDispatchList(RootModel[list[BaseDataModelT]], Generic[BaseDataModelT]):
155
+ """
156
+ 自动对 list 中的 dict 进行 registry 分发
157
+ """
158
+
159
+ # root: list[BaseDataModelT]
160
+
161
+ # 子类必须指定 element_type
162
+ @classmethod
163
+ def element_type(cls) -> type[BaseDataModelT]: ...
164
+
165
+ @model_validator(mode="before")
166
+ @classmethod
167
+ def dispatch_elements(cls, value):
168
+ if not isinstance(value, list):
169
+ return value
170
+
171
+ result = []
172
+ for item in value:
173
+ if isinstance(item, dict):
174
+ result.append(cls.element_type().from_dict(item))
175
+ else:
176
+ result.append(item)
177
+
178
+ return result
179
+
180
+ # 辅助方法,方便调试和展示
181
+
182
+ def __repr__(self):
183
+ core_properties_str: str = self._get_core_properties_str()
184
+ return "%s(%s)" % (self.__class__.__name__, core_properties_str)
185
+
186
+ def __str__(self):
187
+ return self.__repr__()
188
+
189
+ def _get_core_properties_str(self) -> str:
190
+ excludes = {"raw_data"}
191
+ props = {
192
+ k: v
193
+ for k, v in vars(self).items()
194
+ if not k.startswith("_") and k not in excludes
195
+ }
196
+ parts = ["%s=%r" % (k, v) for k, v in props.items()]
197
+ return ", ".join(parts)
@@ -0,0 +1 @@
1
+ --- 待施工 ---
@@ -0,0 +1,10 @@
1
+ from .event import Event
2
+ from .event_bus import EventBus
3
+ from .subscriber import Subscriber, SubscriberGroup
4
+
5
+ __all__ = [
6
+ "Event",
7
+ "EventBus",
8
+ "Subscriber",
9
+ "SubscriberGroup",
10
+ ]
@@ -0,0 +1,27 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Generic
3
+ from uuid import uuid4
4
+
5
+ from butterbot.core.data import BaseDataT
6
+ from butterbot.core.types import BaseType
7
+
8
+
9
+ @dataclass
10
+ class Event(Generic[BaseDataT]):
11
+ """事件类,包含数据和状态.
12
+
13
+ Attributes:
14
+ data: 事件数据
15
+ status: 事件状态
16
+ id: 事件唯一标识符
17
+ """
18
+
19
+ data: BaseDataT
20
+ status: BaseType
21
+ id: str = field(default_factory=lambda: str(uuid4()))
22
+
23
+ def __repr__(self) -> str:
24
+ return "Event(data=%s, status=%s)" % (self.data, self.status)
25
+
26
+ def __str__(self) -> str:
27
+ return self.__repr__()
@@ -0,0 +1,276 @@
1
+ import asyncio
2
+ import inspect
3
+ import re
4
+ from collections.abc import Callable, Coroutine
5
+ from functools import wraps
6
+ from logging import getLogger
7
+ from typing import TYPE_CHECKING, Any, Union
8
+ from uuid import UUID
9
+
10
+ from .event import Event
11
+ from .subscriber import Subscriber, SubscriberGroup
12
+
13
+ if TYPE_CHECKING:
14
+ from butterbot.core.filter import BaseFilter
15
+ from butterbot.core.types import BaseType
16
+
17
+ _log = getLogger(__name__)
18
+
19
+
20
+ class EventBus:
21
+ """事件总线,管理事件的订阅和发布."""
22
+
23
+ def __init__(self):
24
+ """初始化事件总线."""
25
+ # 使用订阅组管理订阅者
26
+ self._subscriber_group = SubscriberGroup()
27
+ # 持有 create_task 返回的 Task 强引用,防止 GC 回收未完成的任务
28
+ self._background_tasks: set[asyncio.Task] = set()
29
+ # 已关闭的总线不再接受 publish
30
+ self._closed = False
31
+
32
+ @property
33
+ def closed(self) -> bool:
34
+ """是否已关闭(关闭后 :meth:`publish` 不再派发事件)."""
35
+ return self._closed
36
+
37
+ @property
38
+ def pending_callbacks(self) -> int:
39
+ """当前尚未完成的回调数量."""
40
+ return sum(1 for task in self._background_tasks if not task.done())
41
+
42
+ def _wrap_callback(
43
+ self,
44
+ func: Callable,
45
+ event_filter: "BaseFilter | None" = None,
46
+ ) -> Callable[[Event], Coroutine[Any, Any, None]]:
47
+ """检查并包装回调函数.
48
+
49
+ 验证函数是否为协程函数,并用 @wraps 保留原函数元信息。
50
+ 若提供了 event_filter,将过滤逻辑一并包装到回调中。
51
+
52
+ Args:
53
+ func: 原始回调函数
54
+ event_filter: 可选的事件内容过滤器
55
+
56
+ Returns:
57
+ 包装后的回调函数
58
+
59
+ Raises:
60
+ TypeError: 如果回调函数不是协程函数
61
+ """
62
+ if not inspect.iscoroutinefunction(func):
63
+ raise TypeError("回调函数 '%s' 必须是协程函数" % func.__name__)
64
+
65
+ if event_filter is not None:
66
+
67
+ @wraps(func)
68
+ async def wrapper(event: Event) -> None:
69
+ if event_filter.check(event):
70
+ return await func(event)
71
+ _log.debug("事件%s被过滤器 %s 拦截", event.id, event_filter)
72
+
73
+ return wrapper
74
+
75
+ @wraps(func)
76
+ async def wrapper(event: Event) -> None:
77
+ return await func(event)
78
+
79
+ return wrapper
80
+
81
+ def add_subscriber(
82
+ self,
83
+ uuid: UUID,
84
+ callback: Callable[[Event], Coroutine[Any, Any, None]],
85
+ status: Union[str, re.Pattern[str], "BaseType"],
86
+ supported_types: "type[BaseType] | None" = None,
87
+ *,
88
+ event_filter: "BaseFilter | None" = None,
89
+ ) -> None:
90
+ """添加订阅者.
91
+
92
+ Args:
93
+ uuid: 发布器的唯一标识符
94
+ callback: 回调函数,接收 Event 参数
95
+ status: 状态过滤器(``BaseType`` 枚举或 ``str`` 正则)
96
+ supported_types: 事件源声明的 ``BaseType`` 枚举类。
97
+ 订阅规则将在注册期编译为具体状态到回调的映射。
98
+ event_filter: 可选的事件内容过滤器,只有通过过滤器的事件才触发回调。
99
+ """
100
+ wrapper = self._wrap_callback(callback, event_filter=event_filter)
101
+
102
+ subscriber = Subscriber(
103
+ callback=wrapper,
104
+ status_filter=status,
105
+ event_filter=event_filter,
106
+ )
107
+ self._subscriber_group.add(uuid, subscriber, supported_types)
108
+ _log.debug(
109
+ "为 '%s' 注册订阅者 callback=%s, status_filter=%s)",
110
+ uuid,
111
+ callback.__name__,
112
+ status,
113
+ )
114
+
115
+ def subscribe(
116
+ self,
117
+ uuid: UUID,
118
+ status: Union[str, re.Pattern[str], "BaseType"],
119
+ supported_types: "type[BaseType] | None" = None,
120
+ *,
121
+ event_filter: "BaseFilter | None" = None,
122
+ ) -> Callable:
123
+ """装饰器:订阅事件.
124
+
125
+ Args:
126
+ uuid: 发布器的唯一标识符
127
+ status: 状态过滤器(``BaseType`` 枚举或 ``str`` 正则)
128
+ supported_types: 事件源声明的 ``BaseType`` 枚举类
129
+ event_filter: 可选的事件内容过滤器,只有通过过滤器的事件才触发回调
130
+
131
+ Returns:
132
+ 装饰器函数
133
+
134
+ Usage:
135
+ @bus.subscribe(keys=[123456], status=LiveType.OPEN)
136
+ async def on_open(event: Event):
137
+ print(event)
138
+ """
139
+
140
+ def decorator(func: Callable[[Event], Coroutine[Any, Any, None]]) -> Callable:
141
+ self.add_subscriber(
142
+ uuid, func, status, supported_types, event_filter=event_filter
143
+ )
144
+ return func
145
+
146
+ return decorator
147
+
148
+ def remove_subscribers(self, uuid: UUID) -> int:
149
+ """移除某个事件源的全部订阅.
150
+
151
+ 事件源被移除时必须调用,否则派发表中该 uuid 的回调会永久残留。
152
+
153
+ Args:
154
+ uuid: 发布器的唯一标识符
155
+
156
+ Returns:
157
+ 被移除的回调数量
158
+ """
159
+ return self._subscriber_group.remove(uuid)
160
+
161
+ async def close(self, timeout: float = 5.0) -> None:
162
+ """关闭事件总线:停止接受新事件,并排空正在执行的回调.
163
+
164
+ 关闭序列:
165
+
166
+ 1. 置 ``closed`` —— 之后 :meth:`publish` 只记录警告、不再派发;
167
+ 2. 等待所有 in-flight 回调完成,最长 ``timeout`` 秒;
168
+ 3. 超时未完成的回调被 ``cancel()`` 并 ``await`` 到真正结束。
169
+
170
+ 不做这件事的后果是进程退出时 pending 回调被 GC,
171
+ Python 会打印 ``Task was destroyed but it is pending!``,
172
+ 且用户回调可能执行到一半被掐断。
173
+
174
+ 该方法幂等;从回调内部调用时会跳过调用者自身的 task,不会自我等待。
175
+
176
+ Args:
177
+ timeout: 等待回调完成的秒数,超时后强制取消
178
+ """
179
+ if self._closed:
180
+ return
181
+ self._closed = True
182
+
183
+ current = asyncio.current_task()
184
+ pending = [
185
+ task
186
+ for task in self._background_tasks
187
+ if not task.done() and task is not current
188
+ ]
189
+ if not pending:
190
+ self._background_tasks.clear()
191
+ _log.debug("EventBus 已关闭(无待完成回调)")
192
+ return
193
+
194
+ _log.info(
195
+ "EventBus 正在等待 %s 个回调完成(超时 %.1fs)", len(pending), timeout
196
+ )
197
+ _, not_done = await asyncio.wait(pending, timeout=timeout)
198
+
199
+ if not_done:
200
+ _log.warning("%s 个回调在 %.1fs 内未完成,强制取消", len(not_done), timeout)
201
+ for task in not_done:
202
+ task.cancel()
203
+ await asyncio.gather(*not_done, return_exceptions=True)
204
+
205
+ self._background_tasks.clear()
206
+ _log.info("EventBus 已关闭")
207
+
208
+ def _task_done_callback(
209
+ self,
210
+ task: asyncio.Task,
211
+ uuid: UUID,
212
+ callback_name: str,
213
+ status_value: str,
214
+ ) -> None:
215
+ """后台任务完成回调,检查并记录异常.
216
+
217
+ Args:
218
+ task: 已完成的 asyncio.Task
219
+ uuid: 发布器的唯一标识符
220
+ callback_name: 回调函数名
221
+ status_value: 事件状态值
222
+ """
223
+ self._background_tasks.discard(task)
224
+
225
+ try:
226
+ exc = task.exception()
227
+ except asyncio.CancelledError:
228
+ return
229
+
230
+ if exc is not None:
231
+ _log.exception(
232
+ "订阅者回调执行失败 (uuid=%s, callback=%s, status=%s)",
233
+ uuid,
234
+ callback_name,
235
+ status_value,
236
+ exc_info=exc,
237
+ )
238
+
239
+ async def publish(self, uuid: UUID, event: Event) -> None:
240
+ """发布事件.
241
+
242
+ 发布指定发布器的事件,触发所有匹配的订阅者回调。
243
+
244
+ 总线已关闭时事件被丢弃(记录警告),避免在关闭排空过程中又派生新回调。
245
+
246
+ Args:
247
+ uuid: 发布器的唯一标识符
248
+ event: 要发布的事件
249
+ """
250
+ if self._closed:
251
+ _log.warning(
252
+ "EventBus 已关闭,丢弃事件 (uuid=%s, status=%s)",
253
+ uuid,
254
+ event.status.value,
255
+ )
256
+ return
257
+
258
+ # 查表派发:根据 uuid + 状态值直接获取所有已编译的回调
259
+ callbacks = self._subscriber_group.get_callbacks(uuid, event.status)
260
+
261
+ for callback in callbacks:
262
+ callback_name = getattr(callback, "__name__", "<lambda>")
263
+ # 异步执行回调,保留强引用防止 GC 回收
264
+ task = asyncio.create_task(callback(event))
265
+ self._background_tasks.add(task)
266
+ task.add_done_callback(
267
+ lambda t, u=uuid, cn=callback_name, sv=event.status.value: (
268
+ self._task_done_callback(t, u, cn, sv)
269
+ )
270
+ )
271
+ _log.debug(
272
+ "触发订阅者 (uuid=%s, callback=%s, status=%s)",
273
+ uuid,
274
+ callback_name,
275
+ event.status.value,
276
+ )
@@ -0,0 +1,130 @@
1
+ import re
2
+ from collections.abc import Callable, Coroutine
3
+ from dataclasses import dataclass
4
+ from logging import getLogger
5
+ from typing import TYPE_CHECKING, Any, Union
6
+ from uuid import UUID
7
+
8
+ from butterbot.core.exceptions import SubscriptionError
9
+ from butterbot.core.types import BaseType
10
+
11
+ if TYPE_CHECKING:
12
+ from butterbot.core.filter import BaseFilter
13
+
14
+ from .event import Event
15
+
16
+ _log = getLogger(__name__)
17
+
18
+
19
+ @dataclass
20
+ class Subscriber:
21
+ """订阅者信息.
22
+
23
+ Attributes:
24
+ callback: 回调函数
25
+ status_filter: 状态过滤器(``BaseType`` 枚举、``str`` 或 ``re.Pattern[str]`` 正则)
26
+ event_filter: 事件过滤器(``BaseFilter`` 实例,仅用于调试/内省)
27
+ """
28
+
29
+ callback: Callable[[Event], Coroutine[Any, Any, None]]
30
+ status_filter: Union[str, re.Pattern[str], "BaseType"]
31
+ event_filter: "BaseFilter | None" = None
32
+
33
+
34
+ class SubscriberGroup:
35
+ """订阅组类,管理订阅者的注册与派发.
36
+
37
+ 订阅规则在注册时编译展开:根据事件源的 ``supported_types`` 枚举类,
38
+ 将订阅规则匹配到所有具体的状态值,建立 ``uuid→status_value→callbacks``
39
+ 的派发表。``publish`` 时只需 O(1) 查表,无需运行时遍历和匹配。
40
+
41
+ 未提供 ``supported_types`` 的调用将抛出 ``TypeError``。
42
+ """
43
+
44
+ def __init__(self):
45
+ """初始化订阅组."""
46
+ # 编译派发表:uuid -> concrete_status -> [callback, ...]
47
+ self._dispatch_table: dict[UUID, dict[BaseType, list[Callable]]] = {}
48
+
49
+ def add(
50
+ self,
51
+ uuid: UUID,
52
+ subscriber: Subscriber,
53
+ supported_types: "type[BaseType] | None" = None,
54
+ ) -> None:
55
+ """添加订阅者,将订阅规则编译展开存入派发表.
56
+
57
+ Args:
58
+ uuid: 发布器的唯一标识符
59
+ subscriber: 订阅者对象
60
+ supported_types: 事件源声明的 ``BaseType`` 枚举类。
61
+ 为 ``None`` 时抛出 ``TypeError``。
62
+
63
+ Raises:
64
+ TypeError: 未提供 ``supported_types``
65
+ SubscriptionError: 订阅规则在 ``supported_types`` 中无任何匹配
66
+ """
67
+ if supported_types is None:
68
+ raise TypeError(
69
+ "订阅规则编译需要事件源声明 supported_types。"
70
+ "请在事件源类中设置 supported_types = <BaseType 子类>"
71
+ "(例如 class MySource(BaseSource): supported_types = MyType)。"
72
+ )
73
+
74
+ # 将订阅规则展开到所有匹配的具体状态
75
+ matched = supported_types.matching_statuses(subscriber.status_filter)
76
+
77
+ # 无匹配意味着这个订阅永远不会触发——几乎总是状态值或正则写错了。
78
+ # 静默丢弃会让用户面对"回调不执行"却无从下手,因此直接报错。
79
+ if not matched:
80
+ raise SubscriptionError(
81
+ "订阅规则 %r 在 %s 中无匹配的具体状态,该订阅永远不会触发。"
82
+ "可用的状态值:%s"
83
+ % (
84
+ subscriber.status_filter,
85
+ supported_types.__name__,
86
+ [member.value for member in supported_types],
87
+ )
88
+ )
89
+
90
+ status_map = self._dispatch_table.setdefault(uuid, {})
91
+ for status in matched:
92
+ status_map.setdefault(status, []).append(subscriber.callback)
93
+
94
+ def remove(self, uuid: UUID) -> int:
95
+ """移除某个事件源的全部订阅.
96
+
97
+ 用于事件源被移除时清理派发表——否则该 uuid 的回调会永久残留,
98
+ 且同一 uuid 的新事件源会意外继承旧订阅。
99
+
100
+ Args:
101
+ uuid: 发布器的唯一标识符
102
+
103
+ Returns:
104
+ 被移除的回调数量(含同一回调注册到多个状态的重复计数)
105
+ """
106
+ status_map = self._dispatch_table.pop(uuid, None)
107
+ if not status_map:
108
+ return 0
109
+ removed = sum(len(callbacks) for callbacks in status_map.values())
110
+ _log.debug("移除 '%s' 的 %s 个订阅回调", uuid, removed)
111
+ return removed
112
+
113
+ def get_callbacks(self, uuid: UUID, status: BaseType) -> tuple[Callable, ...]:
114
+ """获取指定事件源和状态值对应的所有回调函数快照.
115
+
116
+ 返回不可变元组作为快照,确保 publish 遍历期间不受并发 add 影响。
117
+
118
+ Args:
119
+ uuid: 发布器的唯一标识符
120
+ status: 事件状态(``BaseType`` 枚举成员)
121
+
122
+ Returns:
123
+ 回调函数元组(无匹配时返回空元组)
124
+ """
125
+ return tuple(self._dispatch_table.get(uuid, {}).get(status, []))
126
+
127
+ @property
128
+ def uids(self) -> list[UUID]:
129
+ """获取所有已注册的发布器列表."""
130
+ return list(self._dispatch_table.keys())