aury-boot 0.0.4__py3-none-any.whl → 0.0.7__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.
- aury/boot/__init__.py +2 -2
- aury/boot/_version.py +2 -2
- aury/boot/application/__init__.py +60 -36
- aury/boot/application/adapter/__init__.py +112 -0
- aury/boot/application/adapter/base.py +511 -0
- aury/boot/application/adapter/config.py +242 -0
- aury/boot/application/adapter/decorators.py +259 -0
- aury/boot/application/adapter/exceptions.py +202 -0
- aury/boot/application/adapter/http.py +325 -0
- aury/boot/application/app/__init__.py +12 -8
- aury/boot/application/app/base.py +12 -0
- aury/boot/application/app/components.py +137 -44
- aury/boot/application/app/middlewares.py +9 -4
- aury/boot/application/app/startup.py +249 -0
- aury/boot/application/config/__init__.py +36 -1
- aury/boot/application/config/multi_instance.py +216 -0
- aury/boot/application/config/settings.py +398 -149
- aury/boot/application/constants/components.py +6 -0
- aury/boot/application/errors/handlers.py +17 -3
- aury/boot/application/middleware/logging.py +21 -120
- aury/boot/application/rpc/__init__.py +2 -2
- aury/boot/commands/__init__.py +30 -10
- aury/boot/commands/app.py +131 -1
- aury/boot/commands/docs.py +104 -17
- aury/boot/commands/generate.py +22 -22
- aury/boot/commands/init.py +68 -17
- aury/boot/commands/server/app.py +2 -3
- aury/boot/commands/templates/project/AGENTS.md.tpl +221 -0
- aury/boot/commands/templates/project/README.md.tpl +2 -2
- aury/boot/commands/templates/project/aury_docs/00-overview.md.tpl +59 -0
- aury/boot/commands/templates/project/aury_docs/01-model.md.tpl +184 -0
- aury/boot/commands/templates/project/aury_docs/02-repository.md.tpl +206 -0
- aury/boot/commands/templates/project/aury_docs/03-service.md.tpl +398 -0
- aury/boot/commands/templates/project/aury_docs/04-schema.md.tpl +95 -0
- aury/boot/commands/templates/project/aury_docs/05-api.md.tpl +116 -0
- aury/boot/commands/templates/project/aury_docs/06-exception.md.tpl +118 -0
- aury/boot/commands/templates/project/aury_docs/07-cache.md.tpl +122 -0
- aury/boot/commands/templates/project/aury_docs/08-scheduler.md.tpl +32 -0
- aury/boot/commands/templates/project/aury_docs/09-tasks.md.tpl +38 -0
- aury/boot/commands/templates/project/aury_docs/10-storage.md.tpl +115 -0
- aury/boot/commands/templates/project/aury_docs/11-logging.md.tpl +131 -0
- aury/boot/commands/templates/project/aury_docs/12-admin.md.tpl +56 -0
- aury/boot/commands/templates/project/aury_docs/13-channel.md.tpl +104 -0
- aury/boot/commands/templates/project/aury_docs/14-mq.md.tpl +102 -0
- aury/boot/commands/templates/project/aury_docs/15-events.md.tpl +147 -0
- aury/boot/commands/templates/project/aury_docs/16-adapter.md.tpl +403 -0
- aury/boot/commands/templates/project/{CLI.md.tpl → aury_docs/99-cli.md.tpl} +19 -19
- aury/boot/commands/templates/project/config.py.tpl +10 -10
- aury/boot/commands/templates/project/env_templates/_header.tpl +10 -0
- aury/boot/commands/templates/project/env_templates/admin.tpl +49 -0
- aury/boot/commands/templates/project/env_templates/cache.tpl +14 -0
- aury/boot/commands/templates/project/env_templates/database.tpl +22 -0
- aury/boot/commands/templates/project/env_templates/log.tpl +18 -0
- aury/boot/commands/templates/project/env_templates/messaging.tpl +46 -0
- aury/boot/commands/templates/project/env_templates/rpc.tpl +28 -0
- aury/boot/commands/templates/project/env_templates/scheduler.tpl +18 -0
- aury/boot/commands/templates/project/env_templates/service.tpl +18 -0
- aury/boot/commands/templates/project/env_templates/storage.tpl +38 -0
- aury/boot/commands/templates/project/env_templates/third_party.tpl +43 -0
- aury/boot/commands/templates/project/modules/tasks.py.tpl +1 -1
- aury/boot/common/logging/__init__.py +26 -674
- aury/boot/common/logging/context.py +132 -0
- aury/boot/common/logging/decorators.py +118 -0
- aury/boot/common/logging/format.py +315 -0
- aury/boot/common/logging/setup.py +214 -0
- aury/boot/contrib/admin_console/auth.py +2 -3
- aury/boot/contrib/admin_console/install.py +1 -1
- aury/boot/domain/models/mixins.py +48 -1
- aury/boot/domain/pagination/__init__.py +94 -0
- aury/boot/domain/repository/impl.py +1 -1
- aury/boot/domain/repository/interface.py +1 -1
- aury/boot/domain/transaction/__init__.py +8 -9
- aury/boot/infrastructure/__init__.py +86 -29
- aury/boot/infrastructure/cache/backends.py +102 -18
- aury/boot/infrastructure/cache/base.py +12 -0
- aury/boot/infrastructure/cache/manager.py +153 -91
- aury/boot/infrastructure/channel/__init__.py +24 -0
- aury/boot/infrastructure/channel/backends/__init__.py +9 -0
- aury/boot/infrastructure/channel/backends/memory.py +83 -0
- aury/boot/infrastructure/channel/backends/redis.py +88 -0
- aury/boot/infrastructure/channel/base.py +92 -0
- aury/boot/infrastructure/channel/manager.py +203 -0
- aury/boot/infrastructure/clients/__init__.py +22 -0
- aury/boot/infrastructure/clients/rabbitmq/__init__.py +9 -0
- aury/boot/infrastructure/clients/rabbitmq/config.py +46 -0
- aury/boot/infrastructure/clients/rabbitmq/manager.py +288 -0
- aury/boot/infrastructure/clients/redis/__init__.py +28 -0
- aury/boot/infrastructure/clients/redis/config.py +51 -0
- aury/boot/infrastructure/clients/redis/manager.py +264 -0
- aury/boot/infrastructure/database/config.py +7 -16
- aury/boot/infrastructure/database/manager.py +16 -38
- aury/boot/infrastructure/events/__init__.py +18 -21
- aury/boot/infrastructure/events/backends/__init__.py +11 -0
- aury/boot/infrastructure/events/backends/memory.py +86 -0
- aury/boot/infrastructure/events/backends/rabbitmq.py +193 -0
- aury/boot/infrastructure/events/backends/redis.py +162 -0
- aury/boot/infrastructure/events/base.py +127 -0
- aury/boot/infrastructure/events/manager.py +224 -0
- aury/boot/infrastructure/mq/__init__.py +24 -0
- aury/boot/infrastructure/mq/backends/__init__.py +9 -0
- aury/boot/infrastructure/mq/backends/rabbitmq.py +179 -0
- aury/boot/infrastructure/mq/backends/redis.py +167 -0
- aury/boot/infrastructure/mq/base.py +143 -0
- aury/boot/infrastructure/mq/manager.py +239 -0
- aury/boot/infrastructure/scheduler/manager.py +7 -3
- aury/boot/infrastructure/storage/__init__.py +9 -9
- aury/boot/infrastructure/storage/base.py +17 -5
- aury/boot/infrastructure/storage/factory.py +0 -1
- aury/boot/infrastructure/tasks/__init__.py +2 -2
- aury/boot/infrastructure/tasks/config.py +5 -13
- aury/boot/infrastructure/tasks/manager.py +55 -33
- {aury_boot-0.0.4.dist-info → aury_boot-0.0.7.dist-info}/METADATA +20 -2
- aury_boot-0.0.7.dist-info/RECORD +197 -0
- aury/boot/commands/templates/project/DEVELOPMENT.md.tpl +0 -1397
- aury/boot/commands/templates/project/env.example.tpl +0 -213
- aury/boot/infrastructure/events/bus.py +0 -362
- aury/boot/infrastructure/events/config.py +0 -52
- aury/boot/infrastructure/events/consumer.py +0 -134
- aury/boot/infrastructure/events/models.py +0 -63
- aury_boot-0.0.4.dist-info/RECORD +0 -137
- {aury_boot-0.0.4.dist-info → aury_boot-0.0.7.dist-info}/WHEEL +0 -0
- {aury_boot-0.0.4.dist-info → aury_boot-0.0.7.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,88 @@
|
|
|
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 unsubscribe(self, channel: str) -> None:
|
|
76
|
+
"""取消订阅通道。"""
|
|
77
|
+
if self._pubsub:
|
|
78
|
+
await self._pubsub.unsubscribe(channel)
|
|
79
|
+
|
|
80
|
+
async def close(self) -> None:
|
|
81
|
+
"""关闭通道。"""
|
|
82
|
+
if self._pubsub:
|
|
83
|
+
await self._pubsub.close()
|
|
84
|
+
self._pubsub = None
|
|
85
|
+
logger.debug("Redis 通道已关闭")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
__all__ = ["RedisChannel"]
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""通道基础接口定义。
|
|
2
|
+
|
|
3
|
+
提供流式通道的抽象接口,用于 SSE、WebSocket 等实时通信场景。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from collections.abc import AsyncIterator
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ChannelBackend(Enum):
|
|
17
|
+
"""通道后端类型。"""
|
|
18
|
+
|
|
19
|
+
MEMORY = "memory"
|
|
20
|
+
REDIS = "redis"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class ChannelMessage:
|
|
25
|
+
"""通道消息。"""
|
|
26
|
+
|
|
27
|
+
data: Any
|
|
28
|
+
event: str | None = None
|
|
29
|
+
id: str | None = None
|
|
30
|
+
channel: str | None = None
|
|
31
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
32
|
+
|
|
33
|
+
def to_sse(self) -> str:
|
|
34
|
+
"""转换为 SSE 格式。"""
|
|
35
|
+
lines = []
|
|
36
|
+
if self.id:
|
|
37
|
+
lines.append(f"id: {self.id}")
|
|
38
|
+
if self.event:
|
|
39
|
+
lines.append(f"event: {self.event}")
|
|
40
|
+
# data 可能是多行
|
|
41
|
+
data_str = str(self.data) if not isinstance(self.data, str) else self.data
|
|
42
|
+
for line in data_str.split("\n"):
|
|
43
|
+
lines.append(f"data: {line}")
|
|
44
|
+
lines.append("") # 空行结束
|
|
45
|
+
return "\n".join(lines)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class IChannel(ABC):
|
|
49
|
+
"""通道接口。"""
|
|
50
|
+
|
|
51
|
+
@abstractmethod
|
|
52
|
+
async def publish(self, channel: str, message: ChannelMessage) -> None:
|
|
53
|
+
"""发布消息到通道。
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
channel: 通道名称
|
|
57
|
+
message: 消息对象
|
|
58
|
+
"""
|
|
59
|
+
...
|
|
60
|
+
|
|
61
|
+
@abstractmethod
|
|
62
|
+
async def subscribe(self, channel: str) -> AsyncIterator[ChannelMessage]:
|
|
63
|
+
"""订阅通道。
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
channel: 通道名称
|
|
67
|
+
|
|
68
|
+
Yields:
|
|
69
|
+
ChannelMessage: 接收到的消息
|
|
70
|
+
"""
|
|
71
|
+
...
|
|
72
|
+
|
|
73
|
+
@abstractmethod
|
|
74
|
+
async def unsubscribe(self, channel: str) -> None:
|
|
75
|
+
"""取消订阅通道。
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
channel: 通道名称
|
|
79
|
+
"""
|
|
80
|
+
...
|
|
81
|
+
|
|
82
|
+
@abstractmethod
|
|
83
|
+
async def close(self) -> None:
|
|
84
|
+
"""关闭通道连接。"""
|
|
85
|
+
...
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
__all__ = [
|
|
89
|
+
"ChannelBackend",
|
|
90
|
+
"ChannelMessage",
|
|
91
|
+
"IChannel",
|
|
92
|
+
]
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""通道管理器 - 命名多实例模式。
|
|
2
|
+
|
|
3
|
+
提供统一的通道管理接口,支持多后端和多实例。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from collections.abc import AsyncIterator
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from aury.boot.common.logging import logger
|
|
12
|
+
|
|
13
|
+
from .backends.memory import MemoryChannel
|
|
14
|
+
from .backends.redis import RedisChannel
|
|
15
|
+
from .base import ChannelBackend, ChannelMessage, IChannel
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from aury.boot.infrastructure.clients.redis import RedisClient
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ChannelManager:
|
|
22
|
+
"""通道管理器(命名多实例)。
|
|
23
|
+
|
|
24
|
+
提供统一的通道管理接口,支持:
|
|
25
|
+
- 多实例管理(如 sse、notification 各自独立)
|
|
26
|
+
- 多后端支持(memory、redis)
|
|
27
|
+
- 发布/订阅模式
|
|
28
|
+
|
|
29
|
+
使用示例:
|
|
30
|
+
# 默认实例
|
|
31
|
+
channel = ChannelManager.get_instance()
|
|
32
|
+
await channel.initialize(backend="memory")
|
|
33
|
+
|
|
34
|
+
# 命名实例
|
|
35
|
+
sse_channel = ChannelManager.get_instance("sse")
|
|
36
|
+
notification_channel = ChannelManager.get_instance("notification")
|
|
37
|
+
|
|
38
|
+
# 发布消息
|
|
39
|
+
await channel.publish("events", ChannelMessage(data="hello"))
|
|
40
|
+
|
|
41
|
+
# 订阅消息
|
|
42
|
+
async for msg in channel.subscribe("events"):
|
|
43
|
+
print(msg.data)
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
_instances: dict[str, ChannelManager] = {}
|
|
47
|
+
|
|
48
|
+
def __init__(self, name: str = "default") -> None:
|
|
49
|
+
"""初始化通道管理器。
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
name: 实例名称
|
|
53
|
+
"""
|
|
54
|
+
self.name = name
|
|
55
|
+
self._backend: IChannel | None = None
|
|
56
|
+
self._backend_type: ChannelBackend | None = None
|
|
57
|
+
self._initialized: bool = False
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def get_instance(cls, name: str = "default") -> ChannelManager:
|
|
61
|
+
"""获取指定名称的实例。
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
name: 实例名称,默认为 "default"
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
ChannelManager: 通道管理器实例
|
|
68
|
+
"""
|
|
69
|
+
if name not in cls._instances:
|
|
70
|
+
cls._instances[name] = cls(name)
|
|
71
|
+
return cls._instances[name]
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def reset_instance(cls, name: str | None = None) -> None:
|
|
75
|
+
"""重置实例(仅用于测试)。
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
name: 要重置的实例名称。如果为 None,则重置所有实例。
|
|
79
|
+
|
|
80
|
+
注意:调用此方法前应先调用 cleanup() 释放资源。
|
|
81
|
+
"""
|
|
82
|
+
if name is None:
|
|
83
|
+
cls._instances.clear()
|
|
84
|
+
elif name in cls._instances:
|
|
85
|
+
del cls._instances[name]
|
|
86
|
+
|
|
87
|
+
async def initialize(
|
|
88
|
+
self,
|
|
89
|
+
backend: ChannelBackend | str = ChannelBackend.MEMORY,
|
|
90
|
+
*,
|
|
91
|
+
redis_client: RedisClient | None = None,
|
|
92
|
+
max_subscribers: int = 1000,
|
|
93
|
+
) -> ChannelManager:
|
|
94
|
+
"""初始化通道(链式调用)。
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
backend: 后端类型
|
|
98
|
+
redis_client: Redis 客户端(当 backend=redis 时需要)
|
|
99
|
+
max_subscribers: 内存后端的最大订阅者数量
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
self: 支持链式调用
|
|
103
|
+
"""
|
|
104
|
+
if self._initialized:
|
|
105
|
+
logger.warning(f"通道管理器 [{self.name}] 已初始化,跳过")
|
|
106
|
+
return self
|
|
107
|
+
|
|
108
|
+
# 处理字符串类型的 backend
|
|
109
|
+
if isinstance(backend, str):
|
|
110
|
+
backend = ChannelBackend(backend.lower())
|
|
111
|
+
|
|
112
|
+
self._backend_type = backend
|
|
113
|
+
|
|
114
|
+
if backend == ChannelBackend.MEMORY:
|
|
115
|
+
self._backend = MemoryChannel(max_subscribers=max_subscribers)
|
|
116
|
+
elif backend == ChannelBackend.REDIS:
|
|
117
|
+
if redis_client is None:
|
|
118
|
+
raise ValueError("Redis 通道需要提供 redis_client 参数")
|
|
119
|
+
self._backend = RedisChannel(redis_client)
|
|
120
|
+
else:
|
|
121
|
+
raise ValueError(f"不支持的通道后端: {backend}")
|
|
122
|
+
|
|
123
|
+
self._initialized = True
|
|
124
|
+
logger.info(f"通道管理器 [{self.name}] 初始化完成: {backend.value}")
|
|
125
|
+
return self
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def backend(self) -> IChannel:
|
|
129
|
+
"""获取通道后端。"""
|
|
130
|
+
if self._backend is None:
|
|
131
|
+
raise RuntimeError(
|
|
132
|
+
f"通道管理器 [{self.name}] 未初始化,请先调用 initialize()"
|
|
133
|
+
)
|
|
134
|
+
return self._backend
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def backend_type(self) -> str:
|
|
138
|
+
"""获取当前后端类型。"""
|
|
139
|
+
return self._backend_type.value if self._backend_type else "unknown"
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def is_initialized(self) -> bool:
|
|
143
|
+
"""检查是否已初始化。"""
|
|
144
|
+
return self._initialized
|
|
145
|
+
|
|
146
|
+
async def publish(
|
|
147
|
+
self,
|
|
148
|
+
channel: str,
|
|
149
|
+
message: ChannelMessage | str | dict,
|
|
150
|
+
*,
|
|
151
|
+
event: str | None = None,
|
|
152
|
+
) -> None:
|
|
153
|
+
"""发布消息到通道。
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
channel: 通道名称
|
|
157
|
+
message: 消息内容(ChannelMessage、字符串或字典)
|
|
158
|
+
event: 事件类型(当 message 不是 ChannelMessage 时使用)
|
|
159
|
+
"""
|
|
160
|
+
if isinstance(message, ChannelMessage):
|
|
161
|
+
msg = message
|
|
162
|
+
elif isinstance(message, dict):
|
|
163
|
+
msg = ChannelMessage(data=message, event=event)
|
|
164
|
+
else:
|
|
165
|
+
msg = ChannelMessage(data=str(message), event=event)
|
|
166
|
+
|
|
167
|
+
await self.backend.publish(channel, msg)
|
|
168
|
+
|
|
169
|
+
async def subscribe(self, channel: str) -> AsyncIterator[ChannelMessage]:
|
|
170
|
+
"""订阅通道。
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
channel: 通道名称
|
|
174
|
+
|
|
175
|
+
Yields:
|
|
176
|
+
ChannelMessage: 接收到的消息
|
|
177
|
+
"""
|
|
178
|
+
async for message in self.backend.subscribe(channel):
|
|
179
|
+
yield message
|
|
180
|
+
|
|
181
|
+
async def unsubscribe(self, channel: str) -> None:
|
|
182
|
+
"""取消订阅通道。
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
channel: 通道名称
|
|
186
|
+
"""
|
|
187
|
+
await self.backend.unsubscribe(channel)
|
|
188
|
+
|
|
189
|
+
async def cleanup(self) -> None:
|
|
190
|
+
"""清理资源,关闭通道。"""
|
|
191
|
+
if self._backend:
|
|
192
|
+
await self._backend.close()
|
|
193
|
+
self._backend = None
|
|
194
|
+
self._initialized = False
|
|
195
|
+
logger.info(f"通道管理器 [{self.name}] 已关闭")
|
|
196
|
+
|
|
197
|
+
def __repr__(self) -> str:
|
|
198
|
+
"""字符串表示。"""
|
|
199
|
+
status = "initialized" if self._initialized else "not initialized"
|
|
200
|
+
return f"<ChannelManager name={self.name} backend={self.backend_type} status={status}>"
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
__all__ = ["ChannelManager"]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""客户端模块 - 外部服务连接管理。
|
|
2
|
+
|
|
3
|
+
提供统一的外部服务客户端,支持多实例。
|
|
4
|
+
|
|
5
|
+
包含:
|
|
6
|
+
- redis: Redis 客户端
|
|
7
|
+
- rabbitmq: RabbitMQ 客户端
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from .rabbitmq import RabbitMQClient, RabbitMQConfig
|
|
13
|
+
from .redis import RedisClient, RedisConfig
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
# RabbitMQ
|
|
17
|
+
"RabbitMQClient",
|
|
18
|
+
"RabbitMQConfig",
|
|
19
|
+
# Redis
|
|
20
|
+
"RedisClient",
|
|
21
|
+
"RedisConfig",
|
|
22
|
+
]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""RabbitMQ 客户端配置。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RabbitMQConfig(BaseModel):
|
|
9
|
+
"""RabbitMQ 连接配置。
|
|
10
|
+
|
|
11
|
+
Attributes:
|
|
12
|
+
url: AMQP 连接 URL,如 amqp://guest:guest@localhost:5672/
|
|
13
|
+
heartbeat: 心跳间隔(秒)
|
|
14
|
+
connection_timeout: 连接超时(秒)
|
|
15
|
+
blocked_connection_timeout: 阻塞连接超时(秒)
|
|
16
|
+
prefetch_count: 预取消息数量
|
|
17
|
+
publisher_confirms: 是否启用发布确认
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
url: str = Field(
|
|
21
|
+
default="amqp://guest:guest@localhost:5672/",
|
|
22
|
+
description="AMQP 连接 URL"
|
|
23
|
+
)
|
|
24
|
+
heartbeat: int = Field(
|
|
25
|
+
default=60,
|
|
26
|
+
description="心跳间隔(秒)"
|
|
27
|
+
)
|
|
28
|
+
connection_timeout: float = Field(
|
|
29
|
+
default=10.0,
|
|
30
|
+
description="连接超时(秒)"
|
|
31
|
+
)
|
|
32
|
+
blocked_connection_timeout: float = Field(
|
|
33
|
+
default=300.0,
|
|
34
|
+
description="阻塞连接超时(秒)"
|
|
35
|
+
)
|
|
36
|
+
prefetch_count: int = Field(
|
|
37
|
+
default=10,
|
|
38
|
+
description="预取消息数量(QoS)"
|
|
39
|
+
)
|
|
40
|
+
publisher_confirms: bool = Field(
|
|
41
|
+
default=True,
|
|
42
|
+
description="是否启用发布确认"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
__all__ = ["RabbitMQConfig"]
|