infinity_bus 1.3.1__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.
event_bus/__init__.py ADDED
@@ -0,0 +1,45 @@
1
+ """event_bus - 基于 asyncio 的轻量级事件总线"""
2
+
3
+ __version__ = "1.3.0"
4
+
5
+ from .event import (
6
+ Event,
7
+ EventDeclaration,
8
+ EventRegistry,
9
+ )
10
+
11
+ from .handler import (
12
+ EventHandler,
13
+ EventHandlerRegistry,
14
+ )
15
+
16
+ from .middleware import (
17
+ Middleware,
18
+ MiddlewareChain,
19
+ )
20
+
21
+ from .bus import (
22
+ EventBus,
23
+ BusShuttingDown,
24
+ ShutdownEvent,
25
+ TaskErrorPayload,
26
+ TaskErrorEvent,
27
+ ShutdownConfig,
28
+ )
29
+
30
+ __all__: list[str] = [
31
+ "__version__",
32
+ "Event",
33
+ "EventDeclaration",
34
+ "EventRegistry",
35
+ "EventHandler",
36
+ "EventHandlerRegistry",
37
+ "Middleware",
38
+ "MiddlewareChain",
39
+ "EventBus",
40
+ "BusShuttingDown",
41
+ "ShutdownEvent",
42
+ "TaskErrorPayload",
43
+ "TaskErrorEvent",
44
+ "ShutdownConfig",
45
+ ]
event_bus/bus.py ADDED
@@ -0,0 +1,314 @@
1
+ import asyncio
2
+ import logging
3
+ import types
4
+ from datetime import datetime, timezone
5
+ from typing import Any, Dict, Optional, Set, Type, Union
6
+ from pydantic import BaseModel, Field
7
+
8
+ from .event import Event, EventDeclaration, EventRegistry
9
+ from .handler import EventHandler, EventHandlerRegistry
10
+ from .middleware import MiddlewareChain, OnPublishNext
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ class BusShuttingDown(Exception):
15
+ """总线正在停止,拒绝新发布,请求处理器执行清理并退出"""
16
+ pass
17
+
18
+ class ShutdownEvent(EventDeclaration):
19
+ name = "event_bus.__shutdown__"
20
+
21
+ class TaskErrorPayload(BaseModel):
22
+ error_event: Event = Field(description="发生异常的事件")
23
+ handler_id: Optional[str] = Field(default=None, description="发生异常的处理器内部ID")
24
+ handler_name: str = Field(description="发生异常的处理器类名")
25
+ error_type: str = Field(description="异常类型")
26
+ error_message: str = Field(description="异常消息")
27
+
28
+ class TaskErrorEvent(EventDeclaration):
29
+ name = "event_bus.__task_error__"
30
+ payload_type = TaskErrorPayload
31
+
32
+ class ShutdownConfig(BaseModel):
33
+ """总线停机配置"""
34
+ queue_timeout_min: float = Field(default=1.0, description="队列排空最小等待时间(秒)")
35
+ queue_timeout_max: float = Field(default=15.0, description="队列排空最大等待时间(秒)")
36
+ tasks_timeout: float = Field(default=15.0, description="活跃任务完成等待时间(秒)")
37
+ avg_wait_time: float = Field(default=0.05, description="每个事件平均处理时间估算(秒)")
38
+
39
+ class EventBus:
40
+ """
41
+ 异步事件总线,支持订阅/发布模式
42
+
43
+ 系统内置事件:
44
+ event_bus.__task_error__ 任务执行失败时发送,载荷为 TaskErrorPayload,发布者为 EventBusErrorReporter
45
+ event_bus.__shutdown__ 总线将要关闭时发送, 无载荷, 发布者为 EventBus
46
+ """
47
+
48
+ class Proxy:
49
+ """事件总线代理,提供给处理器调用以访问总线功能"""
50
+
51
+ def __init__(self, bus: 'EventBus', source: str, raw_event: Optional[Event] = None) -> None:
52
+ self._bus: EventBus = bus
53
+ self._source: str = source
54
+ self._raw_event: Optional[Event] = raw_event
55
+
56
+ async def publish(self, name: str, data: Optional[Union[Dict[str, Any], BaseModel]] = None) -> None:
57
+ await self._bus._publish(name, self._source, data, self._raw_event)
58
+
59
+ @property
60
+ def handlers_registry(self) -> EventHandlerRegistry:
61
+ return self._bus._handlers
62
+
63
+ @property
64
+ def events_registry(self) -> EventRegistry:
65
+ return self._bus._events
66
+
67
+ def __init__(self, event_registry: EventRegistry, handler_registry: EventHandlerRegistry, max_queue_size: int = 1024, max_handler_semaphore: int = 256, shutdown: ShutdownConfig = ShutdownConfig(), middleware_chain: Optional[MiddlewareChain] = None) -> None:
68
+ self._events: EventRegistry = event_registry
69
+ self._handlers: EventHandlerRegistry = handler_registry
70
+ self._mw_chain: MiddlewareChain = middleware_chain or MiddlewareChain()
71
+
72
+ if self._events.get(ShutdownEvent.name) is None:
73
+ self._events.register(ShutdownEvent)
74
+ if self._events.get(TaskErrorEvent.name) is None:
75
+ self._events.register(TaskErrorEvent)
76
+
77
+ self._state_lock: asyncio.Lock = asyncio.Lock()
78
+ self._enable_publish: asyncio.Event = asyncio.Event()
79
+ self._running: asyncio.Event = asyncio.Event()
80
+ self._queue: asyncio.Queue[Event] = asyncio.Queue(maxsize=max_queue_size)
81
+ self._dispatch_task: Optional[asyncio.Task[None]] = None
82
+
83
+ self._handler_semaphore = asyncio.Semaphore(max_handler_semaphore)
84
+ self._active_tasks: Set[asyncio.Task[Any]] = set()
85
+
86
+ self._shutdown: ShutdownConfig = shutdown
87
+
88
+ # 预构建中间件责任链(之后不再变更)
89
+ self._before_publish_chain = self._mw_chain.build_before_publish(self._core_publish)
90
+ self._on_publish_chain: OnPublishNext = self._mw_chain.build_on_publish(self._noop_on_publish)
91
+
92
+ async def _publish(self, name: str, source: str, data: Optional[Union[Dict[str, Any], BaseModel]] = None, old_event: Optional[Event] = None) -> None:
93
+ """发布事件到总线(经过中间件链)"""
94
+ if not self._enable_publish.is_set():
95
+ if self._running.is_set():
96
+ logger.warning("EventBus is stopping, cannot publish new events")
97
+ raise BusShuttingDown("EventBus is stopping, cannot publish new events")
98
+ else:
99
+ logger.warning("EventBus is not running, cannot publish events")
100
+ raise RuntimeError("EventBus is not running, cannot publish events")
101
+
102
+ try:
103
+ await self._before_publish_chain(self._events, name, source, data, old_event)
104
+ except Exception as e:
105
+ await self._mw_chain.on_publish_error(e, name, source, data)
106
+ raise
107
+
108
+ async def _core_publish(
109
+ self,
110
+ event_registry: EventRegistry,
111
+ name: str,
112
+ source: str,
113
+ data: Optional[Union[Dict[str, Any], BaseModel]],
114
+ old_event: Optional[Event],
115
+ ) -> None:
116
+ """before_publish 链的末端处理器:校验 → 构造 Event → 入队 → 触发 on_publish 链"""
117
+ event_declaration: Optional[Type[EventDeclaration]] = event_registry.get(name)
118
+ if not event_declaration:
119
+ logger.error(f"Unknown event type: {name}")
120
+ raise ValueError(f"Unknown event type: {name}")
121
+
122
+ payload: Optional[BaseModel] = None
123
+ if event_declaration.payload_type:
124
+ if data is None:
125
+ raise ValueError(f"Event {name} requires payload data, but none provided")
126
+
127
+ elif isinstance(data, BaseModel):
128
+ if not isinstance(data, event_declaration.payload_type):
129
+ raise TypeError(
130
+ f"Payload type mismatch for event '{name}': "
131
+ f"expected {event_declaration.payload_type.__name__}, got {type(data).__name__}"
132
+ )
133
+ payload = data.model_copy()
134
+
135
+ else:
136
+ payload = event_declaration.payload_type(**data)
137
+ else:
138
+ if data is not None:
139
+ raise ValueError(f"Event {name} does not accept payload data")
140
+
141
+ event = Event(
142
+ name=name,
143
+ data=payload,
144
+ sources=old_event.sources.copy() if old_event else [],
145
+ timestamps=old_event.timestamps.copy() if old_event else [],
146
+ event_ids=old_event.event_ids.copy() if old_event else [],
147
+ )
148
+ event.sources.append(source)
149
+ event.timestamps.append(datetime.now(timezone.utc))
150
+ event.event_ids.append(event.id)
151
+ await self._queue.put(event)
152
+ logger.debug(f"Event published: {event.name} (id={event.id})")
153
+
154
+ # 发布成功后,运行 on_publish 中间件链
155
+ await self._on_publish_chain(event)
156
+
157
+ @staticmethod
158
+ async def _noop_on_publish(
159
+ event: Event,
160
+ ) -> None:
161
+ """on_publish 链的末端处理器(空操作)"""
162
+
163
+ async def start(self) -> None:
164
+ """启动事件分发循环"""
165
+ async with self._state_lock:
166
+ if self._running.is_set():
167
+ return
168
+ try:
169
+ self._dispatch_task = asyncio.create_task(self._dispatch_loop())
170
+ except Exception:
171
+ logger.exception("Error occurred while starting event bus")
172
+ raise
173
+ self._running.set()
174
+ self._enable_publish.set()
175
+ await self._mw_chain.setup(self)
176
+ logger.info("EventBus started")
177
+
178
+ async def stop(self) -> None:
179
+ """停止事件总线"""
180
+ async with self._state_lock:
181
+ if not self._running.is_set():
182
+ return
183
+
184
+ await self._publish(ShutdownEvent.name, source="EventBus", data=None)
185
+
186
+ self._enable_publish.clear() # 阻止新消息入队
187
+
188
+ try:
189
+ timeout: float = max(self._shutdown.queue_timeout_min,min(self._shutdown.queue_timeout_max,self._queue.qsize() * self._shutdown.avg_wait_time))
190
+ await asyncio.wait_for(self._queue.join(), timeout=timeout) # 等待队列处理完毕,避免丢失事件
191
+ except asyncio.TimeoutError:
192
+ logger.warning("Timeout while waiting for event queue to drain during shutdown")
193
+
194
+ if self._dispatch_task:
195
+ self._dispatch_task.cancel()
196
+ try:
197
+ await self._dispatch_task
198
+ except asyncio.CancelledError:
199
+ pass
200
+
201
+ await self._wait_all_tasks_done() # 等待所有处理器任务完成
202
+
203
+ self._running.clear()
204
+ await self._mw_chain.teardown(self)
205
+ logger.info("EventBus stopped")
206
+
207
+ async def __aenter__(self) -> "EventBus":
208
+ """异步上下文管理器入口"""
209
+ await self.start()
210
+ return self
211
+
212
+ async def __aexit__(self, exc_type: Optional[type], exc_val: Optional[BaseException], exc_tb: Optional[types.TracebackType]) -> Optional[bool]:
213
+ """异步上下文管理器出口,确保总线资源被释放且不吞没异常"""
214
+ try:
215
+ await self.stop()
216
+ except Exception as stop_err:
217
+ if exc_val is not None:
218
+ logger.error(f"Error during EventBus shutdown (original exception will propagate): {stop_err}")
219
+ else:
220
+ raise
221
+ return None # 不抑制上下文体中的异常
222
+
223
+ def proxy(self, source: str, raw_event: Optional[Event] = None) -> Proxy:
224
+ """创建一个事件总线代理实例,供事件处理器调用"""
225
+ return EventBus.Proxy(self, source, raw_event)
226
+
227
+ async def _handler_wrapper(self, handler: EventHandler, handler_id: str, bus_proxy: 'EventBus.Proxy', event: Event) -> None:
228
+ """事件处理器包装器"""
229
+ try:
230
+ async with self._handler_semaphore: # 控制并发处理器数量,避免过载
231
+ async with asyncio.timeout(handler.handle_timeout):
232
+ await handler(bus_proxy, event)
233
+ except Exception as e:
234
+ if "EventBusErrorReporter" not in event.sources:
235
+ try:
236
+ await self._publish(
237
+ name=TaskErrorEvent.name,
238
+ source="EventBusErrorReporter",
239
+ data=TaskErrorPayload(
240
+ error_event=event,
241
+ handler_id=handler_id,
242
+ handler_name=handler.__class__.__name__,
243
+ error_type=type(e).__name__,
244
+ error_message=str(e)
245
+ ),
246
+ old_event = event
247
+ )
248
+ except BusShuttingDown as err:
249
+ logger.warning(f"Skipping task_error publish during shutdown: {err}")
250
+ except Exception:
251
+ logger.exception("Failed to publish task_error event")
252
+ raise e
253
+
254
+ async def _dispatch_loop(self) -> None:
255
+ """事件分发主循环"""
256
+ await self._running.wait() # 等待事件总线启动
257
+ while self._running.is_set():
258
+ event: Optional[Event] = None
259
+ try:
260
+ event = await self._queue.get()
261
+ for handler_id, handler in self._handlers.get_handlers(event.name):
262
+ self._register_task(asyncio.create_task(self._handler_wrapper(handler, handler_id, self.proxy(handler.__class__.__name__, event), event)))
263
+ except Exception:
264
+ logger.exception("Unexpected error in dispatch loop")
265
+ finally:
266
+ if event:
267
+ self._queue.task_done()
268
+
269
+ def _register_task(self, task: asyncio.Task[Any]) -> None:
270
+ self._active_tasks.add(task)
271
+ task.add_done_callback(self._on_task_done)
272
+
273
+ def _on_task_done(self, task: asyncio.Task[Any]) -> None:
274
+ """任务完成时的回调(在任务完成后立即触发)"""
275
+ self._active_tasks.discard(task)
276
+
277
+ try:
278
+ if exc := task.exception():
279
+ raise exc
280
+ except asyncio.CancelledError:
281
+ logger.debug(f"Handler task cancelled: {task.get_name()}")
282
+ except asyncio.InvalidStateError:
283
+ logger.warning(f"Task {task.get_name()} callback triggered in invalid state")
284
+ except Exception:
285
+ logger.exception(f"Handler task failed: {task.get_name()}")
286
+
287
+ async def _wait_all_tasks_done(self) -> None:
288
+ """等待所有未完成的任务完成,适用于事件总线停止时调用"""
289
+ if self._active_tasks:
290
+ try:
291
+ logger.info(f"Waiting for {len(self._active_tasks)} active handler tasks to complete...")
292
+ done, pending = await asyncio.wait(self._active_tasks.copy(), return_when=asyncio.ALL_COMPLETED, timeout=self._shutdown.tasks_timeout)
293
+ logger.info(f"All handler tasks completed. Total: {len(done)}")
294
+ if pending:
295
+ logger.warning(f"Timeout: {len(pending)} tasks pending, cancelling...")
296
+ for task in pending:
297
+ if not task.done():
298
+ task.cancel()
299
+ try:
300
+ await task
301
+ except asyncio.CancelledError:
302
+ pass
303
+ except Exception as e:
304
+ logger.exception("Unexpected error in wait all task done")
305
+ raise e
306
+
307
+ @property
308
+ def is_running(self) -> bool: return self._running.is_set()
309
+ @property
310
+ def is_publishing_enabled(self) -> bool: return self._enable_publish.is_set()
311
+ @property
312
+ def active_task_count(self) -> int: return len(self._active_tasks)
313
+ @property
314
+ def queue_size(self) -> int: return self._queue.qsize()
event_bus/event.py ADDED
@@ -0,0 +1,52 @@
1
+ import logging
2
+ import uuid
3
+ from datetime import datetime
4
+ from typing import Any, ClassVar, Dict, List, Optional, Type
5
+ from pydantic import BaseModel, Field
6
+ from abc import ABC
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ class Event(BaseModel):
11
+ """事件数据类"""
12
+ name: str = Field(description="事件类型")
13
+ data: Optional[BaseModel] = Field(default=None, description="事件附加数据")
14
+
15
+ # metadata
16
+ id: str = Field(default_factory=lambda: uuid.uuid4().hex, description="事件UUID")
17
+ sources: List[str] = Field(default_factory=list, description="事件处理链")
18
+ timestamps: List[datetime] = Field(default_factory=lambda: [], description="事件时间戳")
19
+ event_ids: List[str] = Field(default_factory=list, description="因果事件ID链,支持精准重建事件流转路径")
20
+
21
+ class EventDeclaration(ABC):
22
+ """事件声明抽象基类"""
23
+ name: ClassVar[str]
24
+ payload_type: ClassVar[Optional[Type[BaseModel]]] = None
25
+
26
+ def __init_subclass__(cls, **kwargs: Any) -> None:
27
+ super().__init_subclass__(**kwargs)
28
+ if not cls.name.strip():
29
+ raise TypeError(f"事件声明类 {cls.__name__} 必须定义非空的 `name` 属性")
30
+
31
+ class EventRegistry:
32
+ """事件注册表"""
33
+
34
+ def __init__(self) -> None:
35
+ self._events: Dict[str, Type[EventDeclaration]] = {}
36
+
37
+ def register(self, event_decl: Type[EventDeclaration]) -> None:
38
+ """手动注册事件声明"""
39
+ if event_decl.name in self._events:
40
+ raise ValueError(f"重复的事件声明 {event_decl.name}")
41
+ self._events[event_decl.name] = event_decl
42
+
43
+ def unregister(self, event_name: str) -> None:
44
+ """注销事件声明"""
45
+ if event_name in self._events:
46
+ del self._events[event_name]
47
+
48
+ def get(self, name: str) -> Optional[Type[EventDeclaration]]:
49
+ return self._events.get(name)
50
+
51
+ def list_names(self) -> List[str]:
52
+ return list(self._events.keys())
event_bus/handler.py ADDED
@@ -0,0 +1,90 @@
1
+ import logging
2
+ import re
3
+ import uuid
4
+ from collections import OrderedDict
5
+ from typing import Any, Dict, List, Optional, Pattern, TYPE_CHECKING
6
+ from pydantic import BaseModel
7
+ from abc import ABC, abstractmethod
8
+
9
+ from .event import Event
10
+
11
+ if TYPE_CHECKING:
12
+ from . import EventBus
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ class EventHandler(ABC):
17
+ """事件处理器基类,所有具体事件处理器应继承此类"""
18
+
19
+ def __init__(self, subscriptions: Optional[List[str]] = None, handle_timeout: Optional[float] = 1.0) -> None:
20
+ self.subscriptions: List[str] = subscriptions.copy() if subscriptions is not None else [] # 订阅的事件类型列表,支持正则表达式
21
+ self.handle_timeout: Optional[float] = handle_timeout
22
+
23
+ async def __call__(self, bus_proxy: 'EventBus.Proxy', event: Event) -> None:
24
+ """事件处理器入口,自动解包事件数据"""
25
+ await self.handle(event.data, bus_proxy, event)
26
+
27
+ @abstractmethod
28
+ async def handle(self, payload: Optional[BaseModel], bus_proxy: 'EventBus.Proxy', raw_event: Event) -> None: pass
29
+
30
+ class EventHandlerRegistry:
31
+ """事件处理器注册表,负责管理事件类型与处理器的映射关系"""
32
+
33
+ def __init__(self, regex_cache_maxsize: int = 256) -> None:
34
+ self._regex_cache: OrderedDict[str, Pattern[str]] = OrderedDict()
35
+ self._regex_cache_maxsize: int = regex_cache_maxsize
36
+ self._handlers: Dict[str, EventHandler] = {}
37
+
38
+ def register(self, handler: EventHandler) -> str:
39
+ """注册一个事件处理器实例"""
40
+ id = uuid.uuid4().hex
41
+ self._handlers[id] = handler
42
+ return id
43
+
44
+ def get(self, handler_id: str) -> Optional[EventHandler]:
45
+ """根据ID获取事件处理器实例"""
46
+ return self._handlers.get(handler_id)
47
+
48
+ def unregister(self, handler_id: str) -> bool:
49
+ """注销一个事件处理器实例"""
50
+ if handler_id in self._handlers:
51
+ del self._handlers[handler_id]
52
+ return True
53
+ return False
54
+
55
+ def get_handlers(self, event_type: str) -> List[tuple[str, EventHandler]]:
56
+ """获取匹配事件类型的所有处理器实例及其注册ID"""
57
+ matched: List[tuple[str, EventHandler]] = []
58
+ for handler_id, handler in self._handlers.items():
59
+ for pattern in handler.subscriptions:
60
+ if self._match_pattern(event_type, pattern):
61
+ matched.append((handler_id, handler))
62
+ break
63
+ return matched
64
+
65
+ def _match_pattern(self, event_type: str, pattern: str) -> bool:
66
+ """使用正则表达式匹配事件类型(LRU 缓存编译结果,防止无限膨胀)"""
67
+ if pattern not in self._regex_cache:
68
+ if len(self._regex_cache) >= self._regex_cache_maxsize:
69
+ self._regex_cache.popitem(last=False) # 淘汰最旧条目
70
+ self._regex_cache[pattern] = re.compile(pattern)
71
+ else:
72
+ self._regex_cache.move_to_end(pattern) # 命中则标记为最近使用
73
+ return re.fullmatch(self._regex_cache[pattern], event_type) is not None
74
+
75
+ @property
76
+ def handlers_count(self) -> int:
77
+ return len(self._handlers)
78
+
79
+ @property
80
+ def all_handlers(self) -> Dict[str, EventHandler]:
81
+ """获取所有注册的事件处理器实例"""
82
+ return self._handlers.copy()
83
+
84
+ @property
85
+ def regex_cache_info(self) -> Dict[str, Any]:
86
+ """获取正则表达式缓存的当前状态"""
87
+ return {
88
+ "size": len(self._regex_cache),
89
+ "max_size": self._regex_cache_maxsize,
90
+ }