aury-boot 0.0.30__py3-none-any.whl → 0.0.32__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 (30) hide show
  1. aury/boot/_version.py +2 -2
  2. aury/boot/application/__init__.py +2 -4
  3. aury/boot/application/app/components.py +2 -0
  4. aury/boot/application/config/settings.py +6 -0
  5. aury/boot/commands/templates/project/AGENTS.md.tpl +54 -0
  6. aury/boot/commands/templates/project/env_templates/messaging.tpl +21 -13
  7. aury/boot/commands/templates/project/env_templates/monitoring.tpl +2 -0
  8. aury/boot/infrastructure/__init__.py +4 -8
  9. aury/boot/infrastructure/channel/__init__.py +9 -8
  10. aury/boot/infrastructure/channel/backends/__init__.py +2 -6
  11. aury/boot/infrastructure/channel/backends/broadcaster.py +141 -0
  12. aury/boot/infrastructure/channel/base.py +5 -2
  13. aury/boot/infrastructure/channel/manager.py +25 -24
  14. aury/boot/infrastructure/events/__init__.py +4 -6
  15. aury/boot/infrastructure/events/backends/__init__.py +2 -4
  16. aury/boot/infrastructure/events/backends/broadcaster.py +189 -0
  17. aury/boot/infrastructure/events/base.py +9 -4
  18. aury/boot/infrastructure/events/manager.py +24 -20
  19. aury/boot/infrastructure/monitoring/alerting/manager.py +2 -0
  20. aury/boot/infrastructure/monitoring/alerting/rules.py +16 -0
  21. aury/boot/infrastructure/monitoring/tracing/processor.py +31 -1
  22. aury/boot/infrastructure/monitoring/tracing/provider.py +2 -0
  23. {aury_boot-0.0.30.dist-info → aury_boot-0.0.32.dist-info}/METADATA +4 -1
  24. {aury_boot-0.0.30.dist-info → aury_boot-0.0.32.dist-info}/RECORD +26 -28
  25. aury/boot/infrastructure/channel/backends/memory.py +0 -126
  26. aury/boot/infrastructure/channel/backends/redis.py +0 -130
  27. aury/boot/infrastructure/events/backends/memory.py +0 -86
  28. aury/boot/infrastructure/events/backends/redis.py +0 -169
  29. {aury_boot-0.0.30.dist-info → aury_boot-0.0.32.dist-info}/WHEEL +0 -0
  30. {aury_boot-0.0.30.dist-info → aury_boot-0.0.32.dist-info}/entry_points.txt +0 -0
@@ -1,126 +0,0 @@
1
- """内存通道后端。
2
-
3
- 适用于单进程场景,如开发环境或简单应用。
4
- """
5
-
6
- from __future__ import annotations
7
-
8
- import asyncio
9
- from collections.abc import AsyncIterator
10
- import contextlib
11
- import fnmatch
12
-
13
- from aury.boot.common.logging import logger
14
-
15
- from ..base import ChannelMessage, IChannel
16
-
17
-
18
- class MemoryChannel(IChannel):
19
- """内存通道实现。
20
-
21
- 使用 asyncio.Queue 实现进程内的发布/订阅。
22
-
23
- 注意:仅适用于单进程,不支持跨进程通信。
24
- """
25
-
26
- def __init__(self, max_subscribers: int = 1000) -> None:
27
- """初始化内存通道。
28
-
29
- Args:
30
- max_subscribers: 每个通道最大订阅者数量
31
- """
32
- self._max_subscribers = max_subscribers
33
- # channel -> list of queues
34
- self._subscribers: dict[str, list[asyncio.Queue[ChannelMessage]]] = {}
35
- # pattern -> list of queues (用于 psubscribe)
36
- self._pattern_subscribers: dict[str, list[asyncio.Queue[ChannelMessage]]] = {}
37
- self._lock = asyncio.Lock()
38
-
39
- async def publish(self, channel: str, message: ChannelMessage) -> None:
40
- """发布消息到通道。"""
41
- message.channel = channel
42
- async with self._lock:
43
- # 精确匹配订阅者
44
- subscribers = self._subscribers.get(channel, [])
45
- for queue in subscribers:
46
- try:
47
- queue.put_nowait(message)
48
- except asyncio.QueueFull:
49
- logger.warning(f"通道 [{channel}] 订阅者队列已满,消息被丢弃")
50
-
51
- # 模式匹配订阅者
52
- for pattern, queues in self._pattern_subscribers.items():
53
- if fnmatch.fnmatch(channel, pattern):
54
- for queue in queues:
55
- try:
56
- queue.put_nowait(message)
57
- except asyncio.QueueFull:
58
- logger.warning(f"模式 [{pattern}] 订阅者队列已满,消息被丢弃")
59
-
60
- async def subscribe(self, channel: str) -> AsyncIterator[ChannelMessage]:
61
- """订阅通道。"""
62
- queue: asyncio.Queue[ChannelMessage] = asyncio.Queue(maxsize=100)
63
-
64
- async with self._lock:
65
- if channel not in self._subscribers:
66
- self._subscribers[channel] = []
67
- if len(self._subscribers[channel]) >= self._max_subscribers:
68
- raise RuntimeError(f"通道 [{channel}] 订阅者数量已达上限")
69
- self._subscribers[channel].append(queue)
70
-
71
- try:
72
- while True:
73
- message = await queue.get()
74
- yield message
75
- finally:
76
- async with self._lock:
77
- if channel in self._subscribers:
78
- with contextlib.suppress(ValueError):
79
- self._subscribers[channel].remove(queue)
80
- if not self._subscribers[channel]:
81
- del self._subscribers[channel]
82
-
83
- async def psubscribe(self, pattern: str) -> AsyncIterator[ChannelMessage]:
84
- """模式订阅通道。
85
-
86
- 使用 fnmatch 风格的通配符:
87
- - `*` 匹配任意字符
88
- - `?` 匹配单个字符
89
- - `[seq]` 匹配 seq 中的任意字符
90
- """
91
- queue: asyncio.Queue[ChannelMessage] = asyncio.Queue(maxsize=100)
92
-
93
- async with self._lock:
94
- if pattern not in self._pattern_subscribers:
95
- self._pattern_subscribers[pattern] = []
96
- if len(self._pattern_subscribers[pattern]) >= self._max_subscribers:
97
- raise RuntimeError(f"模式 [{pattern}] 订阅者数量已达上限")
98
- self._pattern_subscribers[pattern].append(queue)
99
-
100
- try:
101
- while True:
102
- message = await queue.get()
103
- yield message
104
- finally:
105
- async with self._lock:
106
- if pattern in self._pattern_subscribers:
107
- with contextlib.suppress(ValueError):
108
- self._pattern_subscribers[pattern].remove(queue)
109
- if not self._pattern_subscribers[pattern]:
110
- del self._pattern_subscribers[pattern]
111
-
112
- async def unsubscribe(self, channel: str) -> None:
113
- """取消订阅通道(清除所有订阅者)。"""
114
- async with self._lock:
115
- if channel in self._subscribers:
116
- del self._subscribers[channel]
117
-
118
- async def close(self) -> None:
119
- """关闭通道,清理所有订阅。"""
120
- async with self._lock:
121
- self._subscribers.clear()
122
- self._pattern_subscribers.clear()
123
- logger.debug("内存通道已关闭")
124
-
125
-
126
- __all__ = ["MemoryChannel"]
@@ -1,130 +0,0 @@
1
- """Redis 通道后端。
2
-
3
- 适用于多进程/多实例场景,支持跨进程通信。
4
- """
5
-
6
- from __future__ import annotations
7
-
8
- from collections.abc import AsyncIterator
9
- from datetime import datetime
10
- import json
11
- from typing import TYPE_CHECKING
12
-
13
- from aury.boot.common.logging import logger
14
-
15
- from ..base import ChannelMessage, IChannel
16
-
17
- if TYPE_CHECKING:
18
- from aury.boot.infrastructure.clients.redis import RedisClient
19
-
20
-
21
- class RedisChannel(IChannel):
22
- """Redis 通道实现。
23
-
24
- 使用 Redis Pub/Sub 实现跨进程的发布/订阅。
25
- """
26
-
27
- def __init__(self, redis_client: RedisClient) -> None:
28
- """初始化 Redis 通道。
29
-
30
- Args:
31
- redis_client: RedisClient 实例
32
- """
33
- self._client = redis_client
34
- self._pubsub = None
35
-
36
- async def publish(self, channel: str, message: ChannelMessage) -> None:
37
- """发布消息到通道。"""
38
- message.channel = channel
39
- # 序列化消息
40
- data = {
41
- "data": message.data,
42
- "event": message.event,
43
- "id": message.id,
44
- "channel": message.channel,
45
- "timestamp": message.timestamp.isoformat(),
46
- }
47
- await self._client.connection.publish(channel, json.dumps(data))
48
-
49
- async def subscribe(self, channel: str) -> AsyncIterator[ChannelMessage]:
50
- """订阅通道。"""
51
- pubsub = self._client.connection.pubsub()
52
- await pubsub.subscribe(channel)
53
-
54
- try:
55
- async for raw_message in pubsub.listen():
56
- if raw_message["type"] == "message":
57
- try:
58
- data = json.loads(raw_message["data"])
59
- message = ChannelMessage(
60
- data=data.get("data"),
61
- event=data.get("event"),
62
- id=data.get("id"),
63
- channel=data.get("channel"),
64
- timestamp=datetime.fromisoformat(data["timestamp"])
65
- if data.get("timestamp")
66
- else datetime.now(),
67
- )
68
- yield message
69
- except (json.JSONDecodeError, KeyError) as e:
70
- logger.warning(f"解析通道消息失败: {e}")
71
- finally:
72
- await pubsub.unsubscribe(channel)
73
- await pubsub.close()
74
-
75
- async def psubscribe(self, pattern: str) -> AsyncIterator[ChannelMessage]:
76
- """模式订阅(通配符)。
77
-
78
- Args:
79
- pattern: 通道模式,支持 * 和 ? 通配符
80
- - * 匹配任意字符
81
- - ? 匹配单个字符
82
- - 示例: "space:123:*" 订阅 space:123 下所有事件
83
-
84
- Yields:
85
- ChannelMessage: 接收到的消息
86
- """
87
- pubsub = self._client.connection.pubsub()
88
- await pubsub.psubscribe(pattern)
89
-
90
- try:
91
- async for raw_message in pubsub.listen():
92
- # psubscribe 的消息类型是 "pmessage"
93
- if raw_message["type"] == "pmessage":
94
- try:
95
- data = json.loads(raw_message["data"])
96
- # pmessage 包含实际匹配的通道名
97
- actual_channel = raw_message.get("channel")
98
- if isinstance(actual_channel, bytes):
99
- actual_channel = actual_channel.decode("utf-8")
100
-
101
- message = ChannelMessage(
102
- data=data.get("data"),
103
- event=data.get("event"),
104
- id=data.get("id"),
105
- channel=actual_channel or data.get("channel"),
106
- timestamp=datetime.fromisoformat(data["timestamp"])
107
- if data.get("timestamp")
108
- else datetime.now(),
109
- )
110
- yield message
111
- except (json.JSONDecodeError, KeyError) as e:
112
- logger.warning(f"解析通道消息失败: {e}")
113
- finally:
114
- await pubsub.punsubscribe(pattern)
115
- await pubsub.close()
116
-
117
- async def unsubscribe(self, channel: str) -> None:
118
- """取消订阅通道。"""
119
- if self._pubsub:
120
- await self._pubsub.unsubscribe(channel)
121
-
122
- async def close(self) -> None:
123
- """关闭通道。"""
124
- if self._pubsub:
125
- await self._pubsub.close()
126
- self._pubsub = None
127
- logger.debug("Redis 通道已关闭")
128
-
129
-
130
- __all__ = ["RedisChannel"]
@@ -1,86 +0,0 @@
1
- """内存事件总线后端。
2
-
3
- 适用于单进程场景,如开发环境或简单应用。
4
- """
5
-
6
- from __future__ import annotations
7
-
8
- import asyncio
9
- from collections.abc import Callable
10
- from typing import Any
11
-
12
- from aury.boot.common.logging import logger
13
-
14
- from ..base import Event, EventHandler, IEventBus
15
-
16
-
17
- class MemoryEventBus(IEventBus):
18
- """内存事件总线实现。
19
-
20
- 使用内存中的字典存储订阅关系,支持同步和异步处理器。
21
-
22
- 注意:仅适用于单进程,不支持跨进程事件传递。
23
- """
24
-
25
- def __init__(self) -> None:
26
- """初始化内存事件总线。"""
27
- # event_name -> list of handlers
28
- self._handlers: dict[str, list[EventHandler]] = {}
29
-
30
- def _get_event_name(self, event_type: type[Event] | str) -> str:
31
- """获取事件名称。"""
32
- if isinstance(event_type, str):
33
- return event_type
34
- return event_type.__name__
35
-
36
- def subscribe(
37
- self,
38
- event_type: type[Event] | str,
39
- handler: EventHandler,
40
- ) -> None:
41
- """订阅事件。"""
42
- event_name = self._get_event_name(event_type)
43
- if event_name not in self._handlers:
44
- self._handlers[event_name] = []
45
- if handler not in self._handlers[event_name]:
46
- self._handlers[event_name].append(handler)
47
- logger.debug(f"订阅事件: {event_name} -> {handler.__name__}")
48
-
49
- def unsubscribe(
50
- self,
51
- event_type: type[Event] | str,
52
- handler: EventHandler,
53
- ) -> None:
54
- """取消订阅事件。"""
55
- event_name = self._get_event_name(event_type)
56
- if event_name in self._handlers:
57
- try:
58
- self._handlers[event_name].remove(handler)
59
- logger.debug(f"取消订阅事件: {event_name} -> {handler.__name__}")
60
- except ValueError:
61
- pass
62
-
63
- async def publish(self, event: Event) -> None:
64
- """发布事件。"""
65
- event_name = event.event_name
66
- handlers = self._handlers.get(event_name, [])
67
-
68
- if not handlers:
69
- logger.debug(f"事件 {event_name} 没有订阅者")
70
- return
71
-
72
- for handler in handlers:
73
- try:
74
- result = handler(event)
75
- if asyncio.iscoroutine(result):
76
- await result
77
- except Exception as e:
78
- logger.error(f"处理事件 {event_name} 失败: {e}")
79
-
80
- async def close(self) -> None:
81
- """关闭事件总线。"""
82
- self._handlers.clear()
83
- logger.debug("内存事件总线已关闭")
84
-
85
-
86
- __all__ = ["MemoryEventBus"]
@@ -1,169 +0,0 @@
1
- """Redis 事件总线后端。
2
-
3
- 适用于多进程/多实例场景,支持跨进程事件传递。
4
- """
5
-
6
- from __future__ import annotations
7
-
8
- import asyncio
9
- import json
10
- from typing import TYPE_CHECKING
11
-
12
- from aury.boot.common.logging import logger
13
-
14
- from ..base import Event, EventHandler, IEventBus
15
-
16
- if TYPE_CHECKING:
17
- from aury.boot.infrastructure.clients.redis import RedisClient
18
-
19
-
20
- # 框架默认前缀
21
- DEFAULT_CHANNEL_PREFIX = "aury:event:"
22
-
23
-
24
- class RedisEventBus(IEventBus):
25
- """Redis 事件总线实现。
26
-
27
- 使用 Redis Pub/Sub 实现跨进程的事件发布/订阅。
28
-
29
- 频道命名格式:{channel_prefix}{event_name}
30
- 默认:aury:event:user.created
31
- """
32
-
33
- def __init__(
34
- self,
35
- url: str | None = None,
36
- *,
37
- redis_client: RedisClient | None = None,
38
- channel_prefix: str = DEFAULT_CHANNEL_PREFIX,
39
- ) -> None:
40
- """初始化 Redis 事件总线。
41
-
42
- Args:
43
- url: Redis 连接 URL(当 redis_client 为 None 时必须提供)
44
- redis_client: RedisClient 实例(可选,优先使用)
45
- channel_prefix: 频道名称前缀,默认 "aury:event:"
46
-
47
- Raises:
48
- ValueError: 当 url 和 redis_client 都为 None 时
49
- """
50
- if redis_client is None and url is None:
51
- raise ValueError("Redis 事件总线需要提供 url 或 redis_client 参数")
52
-
53
- self._url = url
54
- self._client = redis_client
55
- self._channel_prefix = channel_prefix
56
- # event_name -> list of handlers (本地订阅)
57
- self._handlers: dict[str, list[EventHandler]] = {}
58
- self._pubsub = None
59
- self._listener_task: asyncio.Task | None = None
60
- self._running = False
61
- self._owns_client = False # 是否自己创建的客户端
62
-
63
- async def _ensure_client(self) -> None:
64
- """确保 Redis 客户端已初始化。"""
65
- if self._client is None and self._url:
66
- from aury.boot.infrastructure.clients.redis import RedisClient
67
- self._client = RedisClient()
68
- await self._client.initialize(url=self._url)
69
- self._owns_client = True
70
-
71
- def _get_event_name(self, event_type: type[Event] | str) -> str:
72
- """获取事件名称。"""
73
- if isinstance(event_type, str):
74
- return event_type
75
- return event_type.__name__
76
-
77
- def _get_channel(self, event_name: str) -> str:
78
- """获取 Redis 频道名称。"""
79
- return f"{self._channel_prefix}{event_name}"
80
-
81
- def subscribe(
82
- self,
83
- event_type: type[Event] | str,
84
- handler: EventHandler,
85
- ) -> None:
86
- """订阅事件。"""
87
- event_name = self._get_event_name(event_type)
88
- if event_name not in self._handlers:
89
- self._handlers[event_name] = []
90
- if handler not in self._handlers[event_name]:
91
- self._handlers[event_name].append(handler)
92
- logger.debug(f"订阅事件: {event_name} -> {handler.__name__}")
93
-
94
- def unsubscribe(
95
- self,
96
- event_type: type[Event] | str,
97
- handler: EventHandler,
98
- ) -> None:
99
- """取消订阅事件。"""
100
- event_name = self._get_event_name(event_type)
101
- if event_name in self._handlers:
102
- try:
103
- self._handlers[event_name].remove(handler)
104
- logger.debug(f"取消订阅事件: {event_name} -> {handler.__name__}")
105
- except ValueError:
106
- pass
107
-
108
- async def publish(self, event: Event) -> None:
109
- """发布事件。"""
110
- await self._ensure_client()
111
- event_name = event.event_name
112
- channel = self._get_channel(event_name)
113
- data = json.dumps(event.to_dict())
114
- await self._client.connection.publish(channel, data)
115
-
116
- async def start_listening(self) -> None:
117
- """开始监听事件(需要在后台任务中运行)。"""
118
- if self._running:
119
- return
120
-
121
- await self._ensure_client()
122
- self._pubsub = self._client.connection.pubsub()
123
- self._running = True
124
-
125
- # 订阅所有已注册事件的频道
126
- channels = [self._get_channel(name) for name in self._handlers]
127
- if channels:
128
- await self._pubsub.subscribe(*channels)
129
-
130
- # 监听消息
131
- async for message in self._pubsub.listen():
132
- if not self._running:
133
- break
134
-
135
- if message["type"] == "message":
136
- try:
137
- data = json.loads(message["data"])
138
- event_name = data.get("event_name")
139
- handlers = self._handlers.get(event_name, [])
140
-
141
- for handler in handlers:
142
- try:
143
- # 创建事件对象
144
- event = Event.from_dict(data)
145
- result = handler(event)
146
- if asyncio.iscoroutine(result):
147
- await result
148
- except Exception as e:
149
- logger.error(f"处理事件 {event_name} 失败: {e}")
150
- except (json.JSONDecodeError, KeyError) as e:
151
- logger.warning(f"解析事件消息失败: {e}")
152
-
153
- async def close(self) -> None:
154
- """关闭事件总线。"""
155
- self._running = False
156
- if self._pubsub:
157
- await self._pubsub.close()
158
- self._pubsub = None
159
- if self._listener_task:
160
- self._listener_task.cancel()
161
- self._listener_task = None
162
- if self._owns_client and self._client:
163
- await self._client.close()
164
- self._client = None
165
- self._handlers.clear()
166
- logger.debug("Redis 事件总线已关闭")
167
-
168
-
169
- __all__ = ["RedisEventBus"]