aury-boot 0.0.4__py3-none-any.whl → 0.0.5__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 (98) hide show
  1. aury/boot/__init__.py +2 -2
  2. aury/boot/_version.py +2 -2
  3. aury/boot/application/__init__.py +45 -36
  4. aury/boot/application/app/__init__.py +12 -8
  5. aury/boot/application/app/base.py +12 -0
  6. aury/boot/application/app/components.py +137 -44
  7. aury/boot/application/app/middlewares.py +2 -0
  8. aury/boot/application/app/startup.py +249 -0
  9. aury/boot/application/config/__init__.py +36 -1
  10. aury/boot/application/config/multi_instance.py +200 -0
  11. aury/boot/application/config/settings.py +341 -12
  12. aury/boot/application/constants/components.py +6 -0
  13. aury/boot/application/errors/handlers.py +17 -3
  14. aury/boot/application/middleware/logging.py +8 -120
  15. aury/boot/application/rpc/__init__.py +2 -2
  16. aury/boot/commands/__init__.py +30 -10
  17. aury/boot/commands/app.py +131 -1
  18. aury/boot/commands/docs.py +104 -17
  19. aury/boot/commands/init.py +27 -8
  20. aury/boot/commands/server/app.py +2 -3
  21. aury/boot/commands/templates/project/AGENTS.md.tpl +217 -0
  22. aury/boot/commands/templates/project/README.md.tpl +2 -2
  23. aury/boot/commands/templates/project/aury_docs/00-overview.md.tpl +59 -0
  24. aury/boot/commands/templates/project/aury_docs/01-model.md.tpl +183 -0
  25. aury/boot/commands/templates/project/aury_docs/02-repository.md.tpl +206 -0
  26. aury/boot/commands/templates/project/aury_docs/03-service.md.tpl +398 -0
  27. aury/boot/commands/templates/project/aury_docs/04-schema.md.tpl +95 -0
  28. aury/boot/commands/templates/project/aury_docs/05-api.md.tpl +116 -0
  29. aury/boot/commands/templates/project/aury_docs/06-exception.md.tpl +118 -0
  30. aury/boot/commands/templates/project/aury_docs/07-cache.md.tpl +122 -0
  31. aury/boot/commands/templates/project/aury_docs/08-scheduler.md.tpl +32 -0
  32. aury/boot/commands/templates/project/aury_docs/09-tasks.md.tpl +38 -0
  33. aury/boot/commands/templates/project/aury_docs/10-storage.md.tpl +115 -0
  34. aury/boot/commands/templates/project/aury_docs/11-logging.md.tpl +92 -0
  35. aury/boot/commands/templates/project/aury_docs/12-admin.md.tpl +56 -0
  36. aury/boot/commands/templates/project/aury_docs/13-channel.md.tpl +92 -0
  37. aury/boot/commands/templates/project/aury_docs/14-mq.md.tpl +102 -0
  38. aury/boot/commands/templates/project/aury_docs/15-events.md.tpl +147 -0
  39. aury/boot/commands/templates/project/config.py.tpl +1 -1
  40. aury/boot/commands/templates/project/env.example.tpl +73 -5
  41. aury/boot/commands/templates/project/modules/tasks.py.tpl +1 -1
  42. aury/boot/contrib/admin_console/auth.py +2 -3
  43. aury/boot/contrib/admin_console/install.py +1 -1
  44. aury/boot/domain/models/mixins.py +48 -1
  45. aury/boot/domain/pagination/__init__.py +94 -0
  46. aury/boot/domain/repository/impl.py +1 -1
  47. aury/boot/domain/repository/interface.py +1 -1
  48. aury/boot/domain/transaction/__init__.py +8 -9
  49. aury/boot/infrastructure/__init__.py +86 -29
  50. aury/boot/infrastructure/cache/backends.py +102 -18
  51. aury/boot/infrastructure/cache/base.py +12 -0
  52. aury/boot/infrastructure/cache/manager.py +153 -91
  53. aury/boot/infrastructure/channel/__init__.py +24 -0
  54. aury/boot/infrastructure/channel/backends/__init__.py +9 -0
  55. aury/boot/infrastructure/channel/backends/memory.py +83 -0
  56. aury/boot/infrastructure/channel/backends/redis.py +88 -0
  57. aury/boot/infrastructure/channel/base.py +92 -0
  58. aury/boot/infrastructure/channel/manager.py +203 -0
  59. aury/boot/infrastructure/clients/__init__.py +22 -0
  60. aury/boot/infrastructure/clients/rabbitmq/__init__.py +9 -0
  61. aury/boot/infrastructure/clients/rabbitmq/config.py +46 -0
  62. aury/boot/infrastructure/clients/rabbitmq/manager.py +288 -0
  63. aury/boot/infrastructure/clients/redis/__init__.py +28 -0
  64. aury/boot/infrastructure/clients/redis/config.py +51 -0
  65. aury/boot/infrastructure/clients/redis/manager.py +264 -0
  66. aury/boot/infrastructure/database/config.py +1 -2
  67. aury/boot/infrastructure/database/manager.py +16 -38
  68. aury/boot/infrastructure/events/__init__.py +18 -21
  69. aury/boot/infrastructure/events/backends/__init__.py +11 -0
  70. aury/boot/infrastructure/events/backends/memory.py +86 -0
  71. aury/boot/infrastructure/events/backends/rabbitmq.py +193 -0
  72. aury/boot/infrastructure/events/backends/redis.py +162 -0
  73. aury/boot/infrastructure/events/base.py +127 -0
  74. aury/boot/infrastructure/events/manager.py +224 -0
  75. aury/boot/infrastructure/mq/__init__.py +24 -0
  76. aury/boot/infrastructure/mq/backends/__init__.py +9 -0
  77. aury/boot/infrastructure/mq/backends/rabbitmq.py +179 -0
  78. aury/boot/infrastructure/mq/backends/redis.py +167 -0
  79. aury/boot/infrastructure/mq/base.py +143 -0
  80. aury/boot/infrastructure/mq/manager.py +239 -0
  81. aury/boot/infrastructure/scheduler/manager.py +7 -3
  82. aury/boot/infrastructure/storage/__init__.py +9 -9
  83. aury/boot/infrastructure/storage/base.py +17 -5
  84. aury/boot/infrastructure/storage/factory.py +0 -1
  85. aury/boot/infrastructure/tasks/__init__.py +2 -2
  86. aury/boot/infrastructure/tasks/manager.py +47 -29
  87. aury/boot/testing/base.py +2 -2
  88. {aury_boot-0.0.4.dist-info → aury_boot-0.0.5.dist-info}/METADATA +19 -2
  89. aury_boot-0.0.5.dist-info/RECORD +176 -0
  90. aury/boot/commands/templates/project/DEVELOPMENT.md.tpl +0 -1397
  91. aury/boot/infrastructure/events/bus.py +0 -362
  92. aury/boot/infrastructure/events/config.py +0 -52
  93. aury/boot/infrastructure/events/consumer.py +0 -134
  94. aury/boot/infrastructure/events/models.py +0 -63
  95. aury_boot-0.0.4.dist-info/RECORD +0 -137
  96. /aury/boot/commands/templates/project/{CLI.md.tpl → aury_docs/99-cli.md.tpl} +0 -0
  97. {aury_boot-0.0.4.dist-info → aury_boot-0.0.5.dist-info}/WHEEL +0 -0
  98. {aury_boot-0.0.4.dist-info → aury_boot-0.0.5.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,167 @@
1
+ """Redis 消息队列后端。
2
+
3
+ 使用 Redis List 实现简单的消息队列。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import asyncio
9
+ from collections.abc import Callable
10
+ import json
11
+ from typing import TYPE_CHECKING, Any
12
+
13
+ from aury.boot.common.logging import logger
14
+
15
+ from ..base import IMQ, MQMessage
16
+
17
+ if TYPE_CHECKING:
18
+ from aury.boot.infrastructure.clients.redis import RedisClient
19
+
20
+
21
+ class RedisMQ(IMQ):
22
+ """Redis 消息队列实现。
23
+
24
+ 使用 Redis List (LPUSH/BRPOP) 实现可靠的消息队列。
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ url: str | None = None,
30
+ *,
31
+ redis_client: RedisClient | None = None,
32
+ prefix: str = "mq:",
33
+ ) -> None:
34
+ """初始化 Redis 消息队列。
35
+
36
+ Args:
37
+ url: Redis 连接 URL(当 redis_client 为 None 时必须提供)
38
+ redis_client: RedisClient 实例(可选,优先使用)
39
+ prefix: 队列名称前缀
40
+
41
+ Raises:
42
+ ValueError: 当 url 和 redis_client 都为 None 时
43
+ """
44
+ if redis_client is None and url is None:
45
+ raise ValueError("Redis 消息队列需要提供 url 或 redis_client 参数")
46
+
47
+ self._url = url
48
+ self._client = redis_client
49
+ self._prefix = prefix
50
+ self._consuming = False
51
+ self._owns_client = False # 是否自己创建的客户端
52
+
53
+ async def _ensure_client(self) -> None:
54
+ """确保 Redis 客户端已初始化。"""
55
+ if self._client is None and self._url:
56
+ from aury.boot.infrastructure.clients.redis import RedisClient
57
+ self._client = RedisClient()
58
+ await self._client.initialize(url=self._url)
59
+ self._owns_client = True
60
+
61
+ def _queue_key(self, queue: str) -> str:
62
+ """获取队列的 Redis key。"""
63
+ return f"{self._prefix}{queue}"
64
+
65
+ def _processing_key(self, queue: str) -> str:
66
+ """获取处理中队列的 Redis key。"""
67
+ return f"{self._prefix}{queue}:processing"
68
+
69
+ async def send(self, queue: str, message: MQMessage) -> str:
70
+ """发送消息到队列。"""
71
+ await self._ensure_client()
72
+ message.queue = queue
73
+ data = json.dumps(message.to_dict())
74
+ await self._client.connection.lpush(self._queue_key(queue), data)
75
+ return message.id
76
+
77
+ async def receive(
78
+ self,
79
+ queue: str,
80
+ timeout: float | None = None,
81
+ ) -> MQMessage | None:
82
+ """从队列接收消息。"""
83
+ await self._ensure_client()
84
+ timeout_int = int(timeout) if timeout else 0
85
+ result = await self._client.connection.brpop(
86
+ self._queue_key(queue),
87
+ timeout=timeout_int,
88
+ )
89
+ if result is None:
90
+ return None
91
+
92
+ _, data = result
93
+ try:
94
+ msg_dict = json.loads(data)
95
+ message = MQMessage.from_dict(msg_dict)
96
+ # 将消息放入处理中队列
97
+ await self._client.connection.hset(
98
+ self._processing_key(queue),
99
+ message.id,
100
+ data,
101
+ )
102
+ return message
103
+ except (json.JSONDecodeError, KeyError) as e:
104
+ logger.error(f"解析消息失败: {e}")
105
+ return None
106
+
107
+ async def ack(self, message: MQMessage) -> None:
108
+ """确认消息已处理。"""
109
+ if message.queue:
110
+ await self._client.connection.hdel(
111
+ self._processing_key(message.queue),
112
+ message.id,
113
+ )
114
+
115
+ async def nack(self, message: MQMessage, requeue: bool = True) -> None:
116
+ """拒绝消息。"""
117
+ if message.queue:
118
+ # 从处理中队列移除
119
+ await self._client.connection.hdel(
120
+ self._processing_key(message.queue),
121
+ message.id,
122
+ )
123
+ if requeue and message.retry_count < message.max_retries:
124
+ # 重新入队
125
+ message.retry_count += 1
126
+ await self.send(message.queue, message)
127
+
128
+ async def consume(
129
+ self,
130
+ queue: str,
131
+ handler: Callable[[MQMessage], Any],
132
+ *,
133
+ prefetch: int = 1,
134
+ ) -> None:
135
+ """消费队列消息。"""
136
+ self._consuming = True
137
+ logger.info(f"开始消费队列: {queue}")
138
+
139
+ while self._consuming:
140
+ try:
141
+ message = await self.receive(queue, timeout=1.0)
142
+ if message is None:
143
+ continue
144
+
145
+ try:
146
+ result = handler(message)
147
+ if asyncio.iscoroutine(result):
148
+ await result
149
+ await self.ack(message)
150
+ except Exception as e:
151
+ logger.error(f"处理消息失败: {e}")
152
+ await self.nack(message, requeue=True)
153
+
154
+ except Exception as e:
155
+ logger.error(f"消费消息异常: {e}")
156
+ await asyncio.sleep(1)
157
+
158
+ async def close(self) -> None:
159
+ """关闭连接。"""
160
+ self._consuming = False
161
+ if self._owns_client and self._client:
162
+ await self._client.close()
163
+ self._client = None
164
+ logger.debug("Redis 消息队列已关闭")
165
+
166
+
167
+ __all__ = ["RedisMQ"]
@@ -0,0 +1,143 @@
1
+ """消息队列基础接口定义。
2
+
3
+ 提供消息队列的抽象接口,用于异步任务处理、服务间通信等场景。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from abc import ABC, abstractmethod
9
+ from collections.abc import Callable
10
+ from dataclasses import dataclass, field
11
+ from datetime import datetime
12
+ from enum import Enum
13
+ from typing import Any
14
+ import uuid
15
+
16
+
17
+ class MQBackend(Enum):
18
+ """消息队列后端类型。"""
19
+
20
+ REDIS = "redis"
21
+ RABBITMQ = "rabbitmq"
22
+
23
+
24
+ @dataclass
25
+ class MQMessage:
26
+ """消息队列消息。"""
27
+
28
+ body: Any
29
+ id: str = field(default_factory=lambda: str(uuid.uuid4()))
30
+ queue: str | None = None
31
+ headers: dict[str, Any] = field(default_factory=dict)
32
+ timestamp: datetime = field(default_factory=datetime.now)
33
+ retry_count: int = 0
34
+ max_retries: int = 3
35
+
36
+ def to_dict(self) -> dict[str, Any]:
37
+ """转换为字典。"""
38
+ return {
39
+ "id": self.id,
40
+ "body": self.body,
41
+ "queue": self.queue,
42
+ "headers": self.headers,
43
+ "timestamp": self.timestamp.isoformat(),
44
+ "retry_count": self.retry_count,
45
+ "max_retries": self.max_retries,
46
+ }
47
+
48
+ @classmethod
49
+ def from_dict(cls, data: dict[str, Any]) -> MQMessage:
50
+ """从字典创建消息。"""
51
+ return cls(
52
+ id=data.get("id", str(uuid.uuid4())),
53
+ body=data["body"],
54
+ queue=data.get("queue"),
55
+ headers=data.get("headers", {}),
56
+ timestamp=datetime.fromisoformat(data["timestamp"])
57
+ if data.get("timestamp")
58
+ else datetime.now(),
59
+ retry_count=data.get("retry_count", 0),
60
+ max_retries=data.get("max_retries", 3),
61
+ )
62
+
63
+
64
+ class IMQ(ABC):
65
+ """消息队列接口。"""
66
+
67
+ @abstractmethod
68
+ async def send(self, queue: str, message: MQMessage) -> str:
69
+ """发送消息到队列。
70
+
71
+ Args:
72
+ queue: 队列名称
73
+ message: 消息对象
74
+
75
+ Returns:
76
+ str: 消息 ID
77
+ """
78
+ ...
79
+
80
+ @abstractmethod
81
+ async def receive(
82
+ self,
83
+ queue: str,
84
+ timeout: float | None = None,
85
+ ) -> MQMessage | None:
86
+ """从队列接收消息。
87
+
88
+ Args:
89
+ queue: 队列名称
90
+ timeout: 超时时间(秒),None 表示阻塞等待
91
+
92
+ Returns:
93
+ MQMessage | None: 消息对象,超时返回 None
94
+ """
95
+ ...
96
+
97
+ @abstractmethod
98
+ async def ack(self, message: MQMessage) -> None:
99
+ """确认消息已处理。
100
+
101
+ Args:
102
+ message: 消息对象
103
+ """
104
+ ...
105
+
106
+ @abstractmethod
107
+ async def nack(self, message: MQMessage, requeue: bool = True) -> None:
108
+ """拒绝消息。
109
+
110
+ Args:
111
+ message: 消息对象
112
+ requeue: 是否重新入队
113
+ """
114
+ ...
115
+
116
+ @abstractmethod
117
+ async def consume(
118
+ self,
119
+ queue: str,
120
+ handler: Callable[[MQMessage], Any],
121
+ *,
122
+ prefetch: int = 1,
123
+ ) -> None:
124
+ """消费队列消息。
125
+
126
+ Args:
127
+ queue: 队列名称
128
+ handler: 消息处理函数
129
+ prefetch: 预取数量
130
+ """
131
+ ...
132
+
133
+ @abstractmethod
134
+ async def close(self) -> None:
135
+ """关闭连接。"""
136
+ ...
137
+
138
+
139
+ __all__ = [
140
+ "IMQ",
141
+ "MQBackend",
142
+ "MQMessage",
143
+ ]
@@ -0,0 +1,239 @@
1
+ """消息队列管理器 - 命名多实例模式。
2
+
3
+ 提供统一的消息队列管理接口,支持多后端和多实例。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from collections.abc import Callable
9
+ from typing import TYPE_CHECKING, Any
10
+
11
+ from aury.boot.common.logging import logger
12
+
13
+ from .backends.rabbitmq import RabbitMQ
14
+ from .backends.redis import RedisMQ
15
+ from .base import IMQ, MQBackend, MQMessage
16
+
17
+ if TYPE_CHECKING:
18
+ from aury.boot.application.config import MQInstanceConfig
19
+ from aury.boot.infrastructure.clients.redis import RedisClient
20
+
21
+
22
+ class MQManager:
23
+ """消息队列管理器(命名多实例)。
24
+
25
+ 提供统一的消息队列管理接口,支持:
26
+ - 多实例管理(如 tasks、notifications 各自独立)
27
+ - 多后端支持(redis、rabbitmq)
28
+ - 生产者/消费者模式
29
+
30
+ 使用示例:
31
+ # 默认实例
32
+ mq = MQManager.get_instance()
33
+ await mq.initialize(backend="redis", redis_client=redis_client)
34
+
35
+ # 命名实例
36
+ task_mq = MQManager.get_instance("tasks")
37
+ notification_mq = MQManager.get_instance("notifications")
38
+
39
+ # 发送消息
40
+ await mq.send("orders", MQMessage(body={"order_id": 123}))
41
+
42
+ # 消费消息
43
+ await mq.consume("orders", handler=process_order)
44
+ """
45
+
46
+ _instances: dict[str, MQManager] = {}
47
+
48
+ def __init__(self, name: str = "default") -> None:
49
+ """初始化消息队列管理器。
50
+
51
+ Args:
52
+ name: 实例名称
53
+ """
54
+ self.name = name
55
+ self._backend: IMQ | None = None
56
+ self._backend_type: MQBackend | None = None
57
+ self._initialized: bool = False
58
+
59
+ @classmethod
60
+ def get_instance(cls, name: str = "default") -> MQManager:
61
+ """获取指定名称的实例。
62
+
63
+ Args:
64
+ name: 实例名称,默认为 "default"
65
+
66
+ Returns:
67
+ MQManager: 消息队列管理器实例
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: MQBackend | str = MQBackend.REDIS,
90
+ *,
91
+ config: MQInstanceConfig | None = None,
92
+ redis_client: RedisClient | None = None,
93
+ url: str | None = None,
94
+ prefix: str = "mq:",
95
+ ) -> MQManager:
96
+ """初始化消息队列(链式调用)。
97
+
98
+ Args:
99
+ backend: 后端类型(当 config 不为 None 时忽略)
100
+ config: MQ 实例配置(推荐,自动根据 backend 初始化)
101
+ redis_client: Redis 客户端(当 backend=redis 且 config=None 时需要)
102
+ url: 连接 URL(当 config=None 时需要)
103
+ prefix: Redis 队列名称前缀
104
+
105
+ Returns:
106
+ self: 支持链式调用
107
+ """
108
+ if self._initialized:
109
+ logger.warning(f"消息队列管理器 [{self.name}] 已初始化,跳过")
110
+ return self
111
+
112
+ # 使用配置对象时,从配置中提取参数
113
+ if config is not None:
114
+ backend = config.backend
115
+ url = config.url
116
+
117
+ # 处理字符串类型的 backend
118
+ if isinstance(backend, str):
119
+ try:
120
+ backend = MQBackend(backend.lower())
121
+ except ValueError:
122
+ supported = ", ".join(b.value for b in MQBackend)
123
+ raise ValueError(f"不支持的消息队列后端: {backend}。支持: {supported}")
124
+
125
+ self._backend_type = backend
126
+
127
+ # 根据后端类型创建实例,参数校验由后端自己处理
128
+ if backend == MQBackend.REDIS:
129
+ self._backend = RedisMQ(url=url, redis_client=redis_client, prefix=prefix)
130
+ elif backend == MQBackend.RABBITMQ:
131
+ self._backend = RabbitMQ(url=url)
132
+ else:
133
+ supported = ", ".join(b.value for b in MQBackend)
134
+ raise ValueError(f"不支持的消息队列后端: {backend}。支持: {supported}")
135
+
136
+ self._initialized = True
137
+ logger.info(f"消息队列管理器 [{self.name}] 初始化完成: {backend.value}")
138
+ return self
139
+
140
+ @property
141
+ def backend(self) -> IMQ:
142
+ """获取消息队列后端。"""
143
+ if self._backend is None:
144
+ raise RuntimeError(
145
+ f"消息队列管理器 [{self.name}] 未初始化,请先调用 initialize()"
146
+ )
147
+ return self._backend
148
+
149
+ @property
150
+ def backend_type(self) -> str:
151
+ """获取当前后端类型。"""
152
+ return self._backend_type.value if self._backend_type else "unknown"
153
+
154
+ @property
155
+ def is_initialized(self) -> bool:
156
+ """检查是否已初始化。"""
157
+ return self._initialized
158
+
159
+ async def send(
160
+ self,
161
+ queue: str,
162
+ message: MQMessage | dict | Any,
163
+ *,
164
+ headers: dict[str, Any] | None = None,
165
+ ) -> str:
166
+ """发送消息到队列。
167
+
168
+ Args:
169
+ queue: 队列名称
170
+ message: 消息内容(MQMessage、字典或其他可序列化对象)
171
+ headers: 消息头
172
+
173
+ Returns:
174
+ str: 消息 ID
175
+ """
176
+ if isinstance(message, MQMessage):
177
+ msg = message
178
+ elif isinstance(message, dict):
179
+ msg = MQMessage(body=message, headers=headers or {})
180
+ else:
181
+ msg = MQMessage(body=message, headers=headers or {})
182
+
183
+ return await self.backend.send(queue, msg)
184
+
185
+ async def receive(
186
+ self,
187
+ queue: str,
188
+ timeout: float | None = None,
189
+ ) -> MQMessage | None:
190
+ """从队列接收消息。
191
+
192
+ Args:
193
+ queue: 队列名称
194
+ timeout: 超时时间(秒)
195
+
196
+ Returns:
197
+ MQMessage | None: 消息对象
198
+ """
199
+ return await self.backend.receive(queue, timeout)
200
+
201
+ async def ack(self, message: MQMessage) -> None:
202
+ """确认消息已处理。"""
203
+ await self.backend.ack(message)
204
+
205
+ async def nack(self, message: MQMessage, requeue: bool = True) -> None:
206
+ """拒绝消息。"""
207
+ await self.backend.nack(message, requeue)
208
+
209
+ async def consume(
210
+ self,
211
+ queue: str,
212
+ handler: Callable[[MQMessage], Any],
213
+ *,
214
+ prefetch: int = 1,
215
+ ) -> None:
216
+ """消费队列消息。
217
+
218
+ Args:
219
+ queue: 队列名称
220
+ handler: 消息处理函数
221
+ prefetch: 预取数量
222
+ """
223
+ await self.backend.consume(queue, handler, prefetch=prefetch)
224
+
225
+ async def cleanup(self) -> None:
226
+ """清理资源,关闭连接。"""
227
+ if self._backend:
228
+ await self._backend.close()
229
+ self._backend = None
230
+ self._initialized = False
231
+ logger.info(f"消息队列管理器 [{self.name}] 已关闭")
232
+
233
+ def __repr__(self) -> str:
234
+ """字符串表示。"""
235
+ status = "initialized" if self._initialized else "not initialized"
236
+ return f"<MQManager name={self.name} backend={self.backend_type} status={status}>"
237
+
238
+
239
+ __all__ = ["MQManager"]
@@ -117,10 +117,13 @@ class SchedulerManager:
117
117
  elif name in cls._instances:
118
118
  del cls._instances[name]
119
119
 
120
- async def initialize(self) -> None:
121
- """初始化调度器(已废弃,保留以保持后向兼容)。
120
+ async def initialize(self) -> SchedulerManager:
121
+ """初始化调度器(链式调用)。
122
122
 
123
- 调度器现在在 get_instance() 时同步初始化。
123
+ 调度器现在在 get_instance() 时同步初始化,此方法保留以保持后向兼容。
124
+
125
+ Returns:
126
+ self: 支持链式调用
124
127
  """
125
128
  if not self._initialized:
126
129
  # 如果还未初始化(理论上不会发生),进行初始化
@@ -131,6 +134,7 @@ class SchedulerManager:
131
134
  self._scheduler = AsyncIOScheduler()
132
135
  self._initialized = True
133
136
  logger.debug("调度器已就绪")
137
+ return self
134
138
 
135
139
  @property
136
140
  def scheduler(self) -> AsyncIOScheduler:
@@ -3,10 +3,6 @@
3
3
  本包基于 aury-sdk-storage 提供的实现,对外暴露统一接口与管理器。
4
4
  """
5
5
 
6
- from .base import StorageManager
7
- from .exceptions import StorageBackendError, StorageError, StorageNotFoundError
8
- from .factory import StorageFactory
9
-
10
6
  # 从 SDK 直接导出核心类型
11
7
  from aury.sdk.storage.storage import (
12
8
  IStorage,
@@ -18,21 +14,25 @@ from aury.sdk.storage.storage import (
18
14
  UploadResult,
19
15
  )
20
16
 
17
+ from .base import StorageManager
18
+ from .exceptions import StorageBackendError, StorageError, StorageNotFoundError
19
+ from .factory import StorageFactory
20
+
21
21
  __all__ = [
22
22
  # SDK 类型
23
23
  "IStorage",
24
24
  "LocalStorage",
25
25
  "S3Storage",
26
26
  "StorageBackend",
27
+ "StorageBackendError",
27
28
  "StorageConfig",
29
+ # 异常
30
+ "StorageError",
31
+ "StorageFactory",
28
32
  "StorageFile",
29
- "UploadResult",
30
33
  # 管理器与工厂
31
34
  "StorageManager",
32
- "StorageFactory",
33
- # 异常
34
- "StorageError",
35
- "StorageBackendError",
36
35
  "StorageNotFoundError",
36
+ "UploadResult",
37
37
  ]
38
38
 
@@ -5,6 +5,7 @@
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
+ from aury.boot.common.logging import logger
8
9
  from aury.sdk.storage.storage import (
9
10
  IStorage,
10
11
  LocalStorage,
@@ -14,8 +15,6 @@ from aury.sdk.storage.storage import (
14
15
  StorageFile,
15
16
  )
16
17
 
17
- from aury.boot.common.logging import logger
18
-
19
18
 
20
19
  class StorageManager:
21
20
  """存储管理器(命名多实例)。
@@ -69,8 +68,15 @@ class StorageManager:
69
68
  elif name in cls._instances:
70
69
  del cls._instances[name]
71
70
 
72
- async def init(self, config: StorageConfig) -> None:
73
- """使用 SDK 的 StorageConfig 初始化存储后端。"""
71
+ async def initialize(self, config: StorageConfig) -> StorageManager:
72
+ """初始化存储后端(链式调用)。
73
+
74
+ Args:
75
+ config: 存储配置对象
76
+
77
+ Returns:
78
+ self: 支持链式调用
79
+ """
74
80
  self._config = config
75
81
  if config.backend == StorageBackend.LOCAL:
76
82
  self._backend = LocalStorage(base_path=config.base_path or "./storage")
@@ -78,11 +84,17 @@ class StorageManager:
78
84
  # S3/COS/OSS/MinIO 统一走 S3Storage
79
85
  self._backend = S3Storage(config)
80
86
  logger.info(f"存储管理器初始化完成: {config.backend.value}")
87
+ return self
81
88
 
89
+ @property
90
+ def is_initialized(self) -> bool:
91
+ """检查是否已初始化。"""
92
+ return self._backend is not None
93
+
82
94
  @property
83
95
  def backend(self) -> IStorage:
84
96
  if self._backend is None:
85
- raise RuntimeError("存储管理器未初始化,请先调用 init()")
97
+ raise RuntimeError("存储管理器未初始化,请先调用 initialize()")
86
98
  return self._backend
87
99
 
88
100
  async def upload_file(
@@ -8,7 +8,6 @@ from __future__ import annotations
8
8
  from typing import ClassVar
9
9
 
10
10
  from aury.boot.common.logging import logger
11
-
12
11
  from aury.sdk.storage.storage import IStorage, LocalStorage
13
12
 
14
13
 
@@ -9,7 +9,7 @@ from .exceptions import (
9
9
  TaskExecutionError,
10
10
  TaskQueueError,
11
11
  )
12
- from .manager import TaskManager, TaskProxy, conditional_actor
12
+ from .manager import TaskManager, TaskProxy, conditional_task
13
13
 
14
14
  __all__ = [
15
15
  "TaskError",
@@ -19,6 +19,6 @@ __all__ = [
19
19
  "TaskQueueError",
20
20
  "TaskQueueName",
21
21
  "TaskRunMode",
22
- "conditional_actor",
22
+ "conditional_task",
23
23
  ]
24
24