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,325 @@
|
|
|
1
|
+
"""HTTP 类第三方接口适配器。
|
|
2
|
+
|
|
3
|
+
本模块提供 HTTP REST API 类第三方接口的便捷基类,适用于大部分第三方服务
|
|
4
|
+
(如支付接口、短信接口、地图API、云服务API等)。
|
|
5
|
+
|
|
6
|
+
基于 toolkit.http.HttpClient 封装,核心功能:
|
|
7
|
+
- 自动管理 HttpClient 生命周期(连接池、重试、超时)
|
|
8
|
+
- 统一的请求头处理(认证、签名、trace-id)
|
|
9
|
+
- 请求/响应日志和链路追踪
|
|
10
|
+
- HTTP 错误转换为 AdapterError
|
|
11
|
+
- 自动根据 mode 选择 base_url(生产)或 sandbox_url(沙箱)
|
|
12
|
+
|
|
13
|
+
如果第三方提供的是 SDK 而非 HTTP API(如微信支付 SDK、阿里云 SDK),
|
|
14
|
+
或者使用 gRPC 等非 HTTP 协议,请直接继承 BaseAdapter。
|
|
15
|
+
|
|
16
|
+
典型第三方接口举例:
|
|
17
|
+
- 支付:微信支付、支付宝、Stripe 等
|
|
18
|
+
- 短信:阿里云短信、腾讯云短信、Twilio 等
|
|
19
|
+
- 云存储:七牛、又拍云、AWS S3 等
|
|
20
|
+
- 社交:微信开放平台、企业微信、钉钉等
|
|
21
|
+
- 地图:高德、百度、Google Maps 等
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
import httpx
|
|
29
|
+
|
|
30
|
+
from aury.boot.common.logging import get_trace_id, logger
|
|
31
|
+
from aury.boot.toolkit.http import HttpClient, RetryConfig
|
|
32
|
+
|
|
33
|
+
from .base import BaseAdapter
|
|
34
|
+
from .config import AdapterSettings
|
|
35
|
+
from .exceptions import AdapterError, AdapterTimeoutError
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class HttpAdapter(BaseAdapter):
|
|
39
|
+
"""HTTP 类第三方 Adapter 基类。
|
|
40
|
+
|
|
41
|
+
封装 HttpClient,提供统一的 HTTP 请求方法和错误处理。
|
|
42
|
+
|
|
43
|
+
核心功能:
|
|
44
|
+
- 自动根据 settings.mode 选择 base_url / sandbox_url
|
|
45
|
+
- 统一的请求头处理(认证、签名、trace-id)
|
|
46
|
+
- 请求/响应日志
|
|
47
|
+
- 超时和重试配置
|
|
48
|
+
- 错误转换为 AdapterError
|
|
49
|
+
|
|
50
|
+
使用示例:
|
|
51
|
+
class PaymentAdapter(HttpAdapter):
|
|
52
|
+
@adapter_method("create")
|
|
53
|
+
async def create_order(self, amount: int, order_id: str) -> dict:
|
|
54
|
+
return await self._request(
|
|
55
|
+
"POST", "/v1/charges",
|
|
56
|
+
json={"amount": amount, "order_id": order_id}
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
@create_order.mock
|
|
60
|
+
async def create_order_mock(self, amount: int, order_id: str) -> dict:
|
|
61
|
+
return {"success": True, "mock": True, "charge_id": "ch_mock_123"}
|
|
62
|
+
|
|
63
|
+
# 自定义请求头(如签名)
|
|
64
|
+
def _prepare_headers(self, extra: dict | None = None) -> dict:
|
|
65
|
+
headers = super()._prepare_headers(extra)
|
|
66
|
+
headers["X-Signature"] = self._sign_request(...)
|
|
67
|
+
return headers
|
|
68
|
+
|
|
69
|
+
Attributes:
|
|
70
|
+
_client: HttpClient 实例(mode 为 real/sandbox 时可用)
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(
|
|
74
|
+
self,
|
|
75
|
+
name: str,
|
|
76
|
+
settings: AdapterSettings,
|
|
77
|
+
*,
|
|
78
|
+
client: HttpClient | None = None,
|
|
79
|
+
) -> None:
|
|
80
|
+
"""初始化 HTTP Adapter。
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
name: Adapter 名称
|
|
84
|
+
settings: 集成配置
|
|
85
|
+
client: 自定义 HttpClient(可选,默认根据配置自动创建)
|
|
86
|
+
"""
|
|
87
|
+
super().__init__(name, settings)
|
|
88
|
+
self._client: HttpClient | None = client
|
|
89
|
+
self._owns_client = client is None # 是否由本类创建(需要自己清理)
|
|
90
|
+
|
|
91
|
+
# ========== 生命周期 ==========
|
|
92
|
+
|
|
93
|
+
async def _on_initialize(self) -> None:
|
|
94
|
+
"""初始化 HttpClient。"""
|
|
95
|
+
if self._client is not None:
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
# 只有 real / sandbox 模式需要真实客户端
|
|
99
|
+
if self.settings.mode not in ("real", "sandbox"):
|
|
100
|
+
logger.debug(f"HttpAdapter {self.name} 处于 {self.settings.mode} 模式,跳过 HttpClient 初始化")
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
effective_url = self.settings.get_effective_url()
|
|
104
|
+
if not effective_url:
|
|
105
|
+
logger.warning(
|
|
106
|
+
f"HttpAdapter {self.name} 未配置 base_url/sandbox_url,"
|
|
107
|
+
f"真实调用可能失败"
|
|
108
|
+
)
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
# 创建 HttpClient
|
|
112
|
+
retry_config = RetryConfig(max_retries=self.settings.retry_times)
|
|
113
|
+
self._client = HttpClient(
|
|
114
|
+
base_url=effective_url,
|
|
115
|
+
timeout=float(self.settings.timeout),
|
|
116
|
+
retry_config=retry_config,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
logger.debug(
|
|
120
|
+
f"HttpAdapter {self.name} 初始化 HttpClient: "
|
|
121
|
+
f"url={effective_url}, timeout={self.settings.timeout}"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
async def _on_cleanup(self) -> None:
|
|
125
|
+
"""清理 HttpClient。"""
|
|
126
|
+
if self._client and self._owns_client:
|
|
127
|
+
await self._client.close()
|
|
128
|
+
self._client = None
|
|
129
|
+
logger.debug(f"HttpAdapter {self.name} 关闭 HttpClient")
|
|
130
|
+
|
|
131
|
+
# ========== 请求方法 ==========
|
|
132
|
+
|
|
133
|
+
def _prepare_headers(self, extra: dict[str, str] | None = None) -> dict[str, str]:
|
|
134
|
+
"""准备请求头。
|
|
135
|
+
|
|
136
|
+
默认包含:
|
|
137
|
+
- Content-Type: application/json
|
|
138
|
+
- X-Trace-Id: 链路追踪 ID
|
|
139
|
+
- Authorization: Bearer {api_key}(如果配置了 api_key)
|
|
140
|
+
|
|
141
|
+
子类可覆盖此方法添加自定义头(如签名)。
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
extra: 额外的请求头
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
dict: 合并后的请求头
|
|
148
|
+
"""
|
|
149
|
+
headers: dict[str, str] = {
|
|
150
|
+
"Content-Type": "application/json",
|
|
151
|
+
"X-Trace-Id": get_trace_id(),
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
# 认证
|
|
155
|
+
if self.settings.api_key:
|
|
156
|
+
headers["Authorization"] = f"Bearer {self.settings.api_key}"
|
|
157
|
+
|
|
158
|
+
# 合并额外头
|
|
159
|
+
if extra:
|
|
160
|
+
headers.update(extra)
|
|
161
|
+
|
|
162
|
+
return headers
|
|
163
|
+
|
|
164
|
+
async def _request(
|
|
165
|
+
self,
|
|
166
|
+
method: str,
|
|
167
|
+
path: str,
|
|
168
|
+
*,
|
|
169
|
+
headers: dict[str, str] | None = None,
|
|
170
|
+
params: dict[str, Any] | None = None,
|
|
171
|
+
json: Any = None,
|
|
172
|
+
data: Any = None,
|
|
173
|
+
files: Any = None,
|
|
174
|
+
**kwargs: Any,
|
|
175
|
+
) -> dict[str, Any]:
|
|
176
|
+
"""发送 HTTP 请求。
|
|
177
|
+
|
|
178
|
+
这是 HttpGateway 的核心方法,子类的 @operation 方法通常调用此方法。
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
method: HTTP 方法(GET/POST/PUT/DELETE 等)
|
|
182
|
+
path: 请求路径(相对于 base_url)
|
|
183
|
+
headers: 额外请求头(会与默认头合并)
|
|
184
|
+
params: URL 查询参数
|
|
185
|
+
json: JSON 请求体
|
|
186
|
+
data: 表单数据
|
|
187
|
+
files: 上传文件
|
|
188
|
+
**kwargs: 其他 httpx 参数
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
dict: 响应 JSON
|
|
192
|
+
|
|
193
|
+
Raises:
|
|
194
|
+
GatewayError: 请求失败
|
|
195
|
+
GatewayTimeoutError: 请求超时
|
|
196
|
+
"""
|
|
197
|
+
if self._client is None:
|
|
198
|
+
# 尝试延迟初始化
|
|
199
|
+
await self.initialize()
|
|
200
|
+
if self._client is None:
|
|
201
|
+
raise AdapterError(
|
|
202
|
+
f"HttpClient 未初始化,请检查 {self.name} 的 base_url 配置",
|
|
203
|
+
adapter_name=self.name,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
merged_headers = self._prepare_headers(headers)
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
response = await self._client.request(
|
|
210
|
+
method=method,
|
|
211
|
+
url=path,
|
|
212
|
+
headers=merged_headers,
|
|
213
|
+
params=params,
|
|
214
|
+
json=json,
|
|
215
|
+
data=data,
|
|
216
|
+
files=files,
|
|
217
|
+
**kwargs,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# 尝试解析 JSON
|
|
221
|
+
try:
|
|
222
|
+
return response.json()
|
|
223
|
+
except Exception:
|
|
224
|
+
# 非 JSON 响应,返回包装后的结果
|
|
225
|
+
return {
|
|
226
|
+
"success": response.is_success,
|
|
227
|
+
"status_code": response.status_code,
|
|
228
|
+
"content": response.text,
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
except httpx.TimeoutException as exc:
|
|
232
|
+
raise AdapterTimeoutError(
|
|
233
|
+
f"请求超时: {method} {path}",
|
|
234
|
+
adapter_name=self.name,
|
|
235
|
+
timeout_seconds=self.settings.timeout,
|
|
236
|
+
cause=exc,
|
|
237
|
+
) from exc
|
|
238
|
+
|
|
239
|
+
except httpx.HTTPStatusError as exc:
|
|
240
|
+
# HTTP 错误状态码
|
|
241
|
+
response = exc.response
|
|
242
|
+
try:
|
|
243
|
+
error_data = response.json()
|
|
244
|
+
third_party_code = error_data.get("code") or error_data.get("error_code")
|
|
245
|
+
third_party_message = error_data.get("message") or error_data.get("error")
|
|
246
|
+
except Exception:
|
|
247
|
+
third_party_code = None
|
|
248
|
+
third_party_message = response.text
|
|
249
|
+
|
|
250
|
+
raise AdapterError(
|
|
251
|
+
f"HTTP 错误: {response.status_code} {method} {path}",
|
|
252
|
+
adapter_name=self.name,
|
|
253
|
+
third_party_code=third_party_code,
|
|
254
|
+
third_party_message=third_party_message,
|
|
255
|
+
cause=exc,
|
|
256
|
+
) from exc
|
|
257
|
+
|
|
258
|
+
except Exception as exc:
|
|
259
|
+
raise AdapterError(
|
|
260
|
+
f"请求失败: {method} {path} - {type(exc).__name__}: {exc}",
|
|
261
|
+
adapter_name=self.name,
|
|
262
|
+
cause=exc,
|
|
263
|
+
) from exc
|
|
264
|
+
|
|
265
|
+
# ========== 便捷方法 ==========
|
|
266
|
+
|
|
267
|
+
async def _get(
|
|
268
|
+
self,
|
|
269
|
+
path: str,
|
|
270
|
+
*,
|
|
271
|
+
params: dict[str, Any] | None = None,
|
|
272
|
+
headers: dict[str, str] | None = None,
|
|
273
|
+
**kwargs: Any,
|
|
274
|
+
) -> dict[str, Any]:
|
|
275
|
+
"""GET 请求。"""
|
|
276
|
+
return await self._request("GET", path, params=params, headers=headers, **kwargs)
|
|
277
|
+
|
|
278
|
+
async def _post(
|
|
279
|
+
self,
|
|
280
|
+
path: str,
|
|
281
|
+
*,
|
|
282
|
+
json: Any = None,
|
|
283
|
+
data: Any = None,
|
|
284
|
+
headers: dict[str, str] | None = None,
|
|
285
|
+
**kwargs: Any,
|
|
286
|
+
) -> dict[str, Any]:
|
|
287
|
+
"""POST 请求。"""
|
|
288
|
+
return await self._request("POST", path, json=json, data=data, headers=headers, **kwargs)
|
|
289
|
+
|
|
290
|
+
async def _put(
|
|
291
|
+
self,
|
|
292
|
+
path: str,
|
|
293
|
+
*,
|
|
294
|
+
json: Any = None,
|
|
295
|
+
headers: dict[str, str] | None = None,
|
|
296
|
+
**kwargs: Any,
|
|
297
|
+
) -> dict[str, Any]:
|
|
298
|
+
"""PUT 请求。"""
|
|
299
|
+
return await self._request("PUT", path, json=json, headers=headers, **kwargs)
|
|
300
|
+
|
|
301
|
+
async def _patch(
|
|
302
|
+
self,
|
|
303
|
+
path: str,
|
|
304
|
+
*,
|
|
305
|
+
json: Any = None,
|
|
306
|
+
headers: dict[str, str] | None = None,
|
|
307
|
+
**kwargs: Any,
|
|
308
|
+
) -> dict[str, Any]:
|
|
309
|
+
"""PATCH 请求。"""
|
|
310
|
+
return await self._request("PATCH", path, json=json, headers=headers, **kwargs)
|
|
311
|
+
|
|
312
|
+
async def _delete(
|
|
313
|
+
self,
|
|
314
|
+
path: str,
|
|
315
|
+
*,
|
|
316
|
+
headers: dict[str, str] | None = None,
|
|
317
|
+
**kwargs: Any,
|
|
318
|
+
) -> dict[str, Any]:
|
|
319
|
+
"""DELETE 请求。"""
|
|
320
|
+
return await self._request("DELETE", path, headers=headers, **kwargs)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
__all__ = [
|
|
324
|
+
"HttpAdapter",
|
|
325
|
+
]
|
|
@@ -8,6 +8,8 @@ from .components import (
|
|
|
8
8
|
AdminConsoleComponent,
|
|
9
9
|
CacheComponent,
|
|
10
10
|
DatabaseComponent,
|
|
11
|
+
EventBusComponent,
|
|
12
|
+
MessageQueueComponent,
|
|
11
13
|
MigrationComponent,
|
|
12
14
|
SchedulerComponent,
|
|
13
15
|
TaskComponent,
|
|
@@ -18,19 +20,21 @@ from .middlewares import (
|
|
|
18
20
|
)
|
|
19
21
|
|
|
20
22
|
__all__ = [
|
|
21
|
-
# 应用框架
|
|
22
|
-
"FoundationApp",
|
|
23
|
-
# 基类
|
|
24
|
-
"Component",
|
|
25
|
-
"Middleware",
|
|
26
|
-
# 中间件
|
|
27
|
-
"CORSMiddleware",
|
|
28
|
-
"RequestLoggingMiddleware",
|
|
29
23
|
# 组件
|
|
30
24
|
"AdminConsoleComponent",
|
|
25
|
+
# 中间件
|
|
26
|
+
"CORSMiddleware",
|
|
31
27
|
"CacheComponent",
|
|
28
|
+
# 基类
|
|
29
|
+
"Component",
|
|
32
30
|
"DatabaseComponent",
|
|
31
|
+
"EventBusComponent",
|
|
32
|
+
# 应用框架
|
|
33
|
+
"FoundationApp",
|
|
34
|
+
"MessageQueueComponent",
|
|
35
|
+
"Middleware",
|
|
33
36
|
"MigrationComponent",
|
|
37
|
+
"RequestLoggingMiddleware",
|
|
34
38
|
"SchedulerComponent",
|
|
35
39
|
"TaskComponent",
|
|
36
40
|
]
|
|
@@ -349,6 +349,18 @@ class FoundationApp(FastAPI):
|
|
|
349
349
|
raise
|
|
350
350
|
|
|
351
351
|
logger.info("应用启动完成")
|
|
352
|
+
|
|
353
|
+
# 打印启动横幅和组件状态
|
|
354
|
+
from aury.boot.application.app.startup import (
|
|
355
|
+
collect_component_status,
|
|
356
|
+
print_startup_banner,
|
|
357
|
+
)
|
|
358
|
+
components = collect_component_status()
|
|
359
|
+
print_startup_banner(
|
|
360
|
+
app_name=self.title,
|
|
361
|
+
version=self.version,
|
|
362
|
+
components=components,
|
|
363
|
+
)
|
|
352
364
|
|
|
353
365
|
except Exception as e:
|
|
354
366
|
logger.error(f"应用启动异常: {e}")
|
|
@@ -16,6 +16,8 @@ from aury.boot.application.migrations import MigrationManager
|
|
|
16
16
|
from aury.boot.common.logging import logger
|
|
17
17
|
from aury.boot.infrastructure.cache import CacheManager
|
|
18
18
|
from aury.boot.infrastructure.database import DatabaseManager
|
|
19
|
+
from aury.boot.infrastructure.events import EventBusManager
|
|
20
|
+
from aury.boot.infrastructure.mq import MQManager
|
|
19
21
|
from aury.boot.infrastructure.scheduler import SchedulerManager
|
|
20
22
|
from aury.boot.infrastructure.storage import StorageManager
|
|
21
23
|
from aury.boot.infrastructure.tasks import TaskManager
|
|
@@ -74,12 +76,12 @@ class CacheComponent(Component):
|
|
|
74
76
|
"""初始化缓存。"""
|
|
75
77
|
try:
|
|
76
78
|
cache_manager = CacheManager.get_instance()
|
|
77
|
-
if not cache_manager.
|
|
78
|
-
await cache_manager.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
79
|
+
if not cache_manager.is_initialized:
|
|
80
|
+
await cache_manager.initialize(
|
|
81
|
+
backend=config.cache.cache_type,
|
|
82
|
+
url=config.cache.url,
|
|
83
|
+
max_size=config.cache.max_size,
|
|
84
|
+
)
|
|
83
85
|
except Exception as e:
|
|
84
86
|
logger.warning(f"缓存初始化失败(非关键): {e}")
|
|
85
87
|
|
|
@@ -87,7 +89,7 @@ class CacheComponent(Component):
|
|
|
87
89
|
"""关闭缓存。"""
|
|
88
90
|
try:
|
|
89
91
|
cache_manager = CacheManager.get_instance()
|
|
90
|
-
if cache_manager.
|
|
92
|
+
if cache_manager.is_initialized:
|
|
91
93
|
await cache_manager.cleanup()
|
|
92
94
|
except Exception as e:
|
|
93
95
|
logger.warning(f"缓存关闭失败: {e}")
|
|
@@ -95,53 +97,53 @@ class CacheComponent(Component):
|
|
|
95
97
|
|
|
96
98
|
class StorageComponent(Component):
|
|
97
99
|
"""对象存储组件。
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
- 建议由应用的 `StorageSettings` 读取环境变量并构造 SDK 的 StorageConfig
|
|
102
|
-
"""
|
|
100
|
+
|
|
101
|
+
支持多实例配置,通过环境变量 STORAGE_{INSTANCE}_{FIELD} 配置。
|
|
102
|
+
"""
|
|
103
103
|
|
|
104
104
|
name = ComponentName.STORAGE
|
|
105
105
|
enabled = True
|
|
106
106
|
depends_on: ClassVar[list[str]] = []
|
|
107
107
|
|
|
108
108
|
def can_enable(self, config: BaseConfig) -> bool:
|
|
109
|
-
|
|
110
|
-
return self.enabled and bool(
|
|
109
|
+
"""当配置了 Storage 实例时启用。"""
|
|
110
|
+
return self.enabled and bool(config.get_storages())
|
|
111
111
|
|
|
112
112
|
async def setup(self, app: FoundationApp, config: BaseConfig) -> None:
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
logger.warning(f"存储初始化失败(非关键): {e}")
|
|
113
|
+
"""初始化存储。"""
|
|
114
|
+
from aury.boot.infrastructure.storage import StorageBackend, StorageConfig
|
|
115
|
+
|
|
116
|
+
storage_configs = config.get_storages()
|
|
117
|
+
if not storage_configs:
|
|
118
|
+
logger.debug("未配置 Storage 实例,跳过存储初始化")
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
for name, st_config in storage_configs.items():
|
|
122
|
+
try:
|
|
123
|
+
storage_manager = StorageManager.get_instance(name)
|
|
124
|
+
if not storage_manager.is_initialized:
|
|
125
|
+
storage_config = StorageConfig(
|
|
126
|
+
backend=StorageBackend(st_config.backend),
|
|
127
|
+
access_key_id=st_config.access_key_id,
|
|
128
|
+
access_key_secret=st_config.access_key_secret,
|
|
129
|
+
endpoint=st_config.endpoint,
|
|
130
|
+
region=st_config.region,
|
|
131
|
+
bucket_name=st_config.bucket_name,
|
|
132
|
+
base_path=st_config.base_path,
|
|
133
|
+
)
|
|
134
|
+
await storage_manager.initialize(storage_config)
|
|
135
|
+
except Exception as e:
|
|
136
|
+
logger.warning(f"存储 [{name}] 初始化失败(非关键): {e}")
|
|
138
137
|
|
|
139
138
|
async def teardown(self, app: FoundationApp) -> None:
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
139
|
+
"""关闭所有存储实例。"""
|
|
140
|
+
for name in list(StorageManager._instances.keys()):
|
|
141
|
+
try:
|
|
142
|
+
storage_manager = StorageManager.get_instance(name)
|
|
143
|
+
if storage_manager.is_initialized:
|
|
144
|
+
await storage_manager.cleanup()
|
|
145
|
+
except Exception as e:
|
|
146
|
+
logger.warning(f"存储 [{name}] 关闭失败: {e}")
|
|
145
147
|
|
|
146
148
|
|
|
147
149
|
class TaskComponent(Component):
|
|
@@ -410,6 +412,93 @@ class AdminConsoleComponent(Component):
|
|
|
410
412
|
pass
|
|
411
413
|
|
|
412
414
|
|
|
415
|
+
class MessageQueueComponent(Component):
|
|
416
|
+
"""消息队列组件。
|
|
417
|
+
|
|
418
|
+
提供统一的消息队列接口,支持多种后端(Redis、RabbitMQ)。
|
|
419
|
+
与 TaskComponent(基于 Dramatiq)的区别:
|
|
420
|
+
- Task: 异步任务处理(API 发送,Worker 执行)
|
|
421
|
+
- MQ: 通用消息队列(生产者/消费者模式,服务间通信)
|
|
422
|
+
"""
|
|
423
|
+
|
|
424
|
+
name = ComponentName.MESSAGE_QUEUE
|
|
425
|
+
enabled = True
|
|
426
|
+
depends_on: ClassVar[list[str]] = []
|
|
427
|
+
|
|
428
|
+
def can_enable(self, config: BaseConfig) -> bool:
|
|
429
|
+
"""当配置了 MQ 实例时启用。"""
|
|
430
|
+
return self.enabled and bool(config.get_mqs())
|
|
431
|
+
|
|
432
|
+
async def setup(self, app: FoundationApp, config: BaseConfig) -> None:
|
|
433
|
+
"""初始化消息队列。"""
|
|
434
|
+
from aury.boot.application.config import MQInstanceConfig
|
|
435
|
+
|
|
436
|
+
# 从多实例配置加载
|
|
437
|
+
mq_configs = config.get_mqs()
|
|
438
|
+
if not mq_configs:
|
|
439
|
+
logger.debug("未配置 MQ 实例,跳过消息队列初始化")
|
|
440
|
+
return
|
|
441
|
+
|
|
442
|
+
for name, mq_config in mq_configs.items():
|
|
443
|
+
try:
|
|
444
|
+
mq_manager = MQManager.get_instance(name)
|
|
445
|
+
if not mq_manager.is_initialized:
|
|
446
|
+
await mq_manager.initialize(config=mq_config)
|
|
447
|
+
except Exception as e:
|
|
448
|
+
logger.warning(f"消息队列 [{name}] 初始化失败(非关键): {e}")
|
|
449
|
+
|
|
450
|
+
async def teardown(self, app: FoundationApp) -> None:
|
|
451
|
+
"""关闭所有消息队列实例。"""
|
|
452
|
+
for name in list(MQManager._instances.keys()):
|
|
453
|
+
try:
|
|
454
|
+
mq_manager = MQManager.get_instance(name)
|
|
455
|
+
if mq_manager.is_initialized:
|
|
456
|
+
await mq_manager.cleanup()
|
|
457
|
+
except Exception as e:
|
|
458
|
+
logger.warning(f"消息队列 [{name}] 关闭失败: {e}")
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
class EventBusComponent(Component):
|
|
462
|
+
"""事件总线组件。
|
|
463
|
+
|
|
464
|
+
提供发布/订阅模式的事件总线功能,支持多种后端(memory、Redis、RabbitMQ)。
|
|
465
|
+
"""
|
|
466
|
+
|
|
467
|
+
name = ComponentName.EVENT_BUS
|
|
468
|
+
enabled = True
|
|
469
|
+
depends_on: ClassVar[list[str]] = []
|
|
470
|
+
|
|
471
|
+
def can_enable(self, config: BaseConfig) -> bool:
|
|
472
|
+
"""当配置了 Event 实例时启用。"""
|
|
473
|
+
return self.enabled and bool(config.get_events())
|
|
474
|
+
|
|
475
|
+
async def setup(self, app: FoundationApp, config: BaseConfig) -> None:
|
|
476
|
+
"""初始化事件总线。"""
|
|
477
|
+
# 从多实例配置加载
|
|
478
|
+
event_configs = config.get_events()
|
|
479
|
+
if not event_configs:
|
|
480
|
+
logger.debug("未配置 Event 实例,跳过事件总线初始化")
|
|
481
|
+
return
|
|
482
|
+
|
|
483
|
+
for name, event_config in event_configs.items():
|
|
484
|
+
try:
|
|
485
|
+
event_manager = EventBusManager.get_instance(name)
|
|
486
|
+
if not event_manager.is_initialized:
|
|
487
|
+
await event_manager.initialize(config=event_config)
|
|
488
|
+
except Exception as e:
|
|
489
|
+
logger.warning(f"事件总线 [{name}] 初始化失败(非关键): {e}")
|
|
490
|
+
|
|
491
|
+
async def teardown(self, app: FoundationApp) -> None:
|
|
492
|
+
"""关闭所有事件总线实例。"""
|
|
493
|
+
for name in list(EventBusManager._instances.keys()):
|
|
494
|
+
try:
|
|
495
|
+
event_manager = EventBusManager.get_instance(name)
|
|
496
|
+
if event_manager.is_initialized:
|
|
497
|
+
await event_manager.cleanup()
|
|
498
|
+
except Exception as e:
|
|
499
|
+
logger.warning(f"事件总线 [{name}] 关闭失败: {e}")
|
|
500
|
+
|
|
501
|
+
|
|
413
502
|
# 设置默认组件
|
|
414
503
|
FoundationApp.components = [
|
|
415
504
|
DatabaseComponent,
|
|
@@ -418,6 +507,8 @@ FoundationApp.components = [
|
|
|
418
507
|
CacheComponent,
|
|
419
508
|
StorageComponent,
|
|
420
509
|
TaskComponent,
|
|
510
|
+
MessageQueueComponent,
|
|
511
|
+
EventBusComponent,
|
|
421
512
|
SchedulerComponent,
|
|
422
513
|
]
|
|
423
514
|
|
|
@@ -426,6 +517,8 @@ __all__ = [
|
|
|
426
517
|
"AdminConsoleComponent",
|
|
427
518
|
"CacheComponent",
|
|
428
519
|
"DatabaseComponent",
|
|
520
|
+
"EventBusComponent",
|
|
521
|
+
"MessageQueueComponent",
|
|
429
522
|
"MigrationComponent",
|
|
430
523
|
"SchedulerComponent",
|
|
431
524
|
"StorageComponent",
|
|
@@ -13,6 +13,8 @@ from aury.boot.application.config import BaseConfig
|
|
|
13
13
|
from aury.boot.application.constants import MiddlewareName
|
|
14
14
|
from aury.boot.application.middleware.logging import (
|
|
15
15
|
RequestLoggingMiddleware as StarletteRequestLoggingMiddleware,
|
|
16
|
+
)
|
|
17
|
+
from aury.boot.application.middleware.logging import (
|
|
16
18
|
WebSocketLoggingMiddleware as StarletteWebSocketLoggingMiddleware,
|
|
17
19
|
)
|
|
18
20
|
|
|
@@ -28,14 +30,17 @@ class RequestLoggingMiddleware(Middleware):
|
|
|
28
30
|
|
|
29
31
|
自动记录所有 HTTP 请求的详细信息,包括:
|
|
30
32
|
- 请求方法、路径、查询参数
|
|
31
|
-
- 客户端
|
|
33
|
+
- 客户端IP、User-Agent
|
|
32
34
|
- 响应状态码、耗时
|
|
33
35
|
- 链路追踪 ID(X-Trace-ID / X-Request-ID)
|
|
36
|
+
- 请求上下文(user_id, tenant_id 等用户注册的字段)
|
|
37
|
+
|
|
38
|
+
注意:用户的认证中间件应设置 order < 100,以便在日志记录前设置用户信息。
|
|
34
39
|
"""
|
|
35
40
|
|
|
36
41
|
name = MiddlewareName.REQUEST_LOGGING
|
|
37
42
|
enabled = True
|
|
38
|
-
order =
|
|
43
|
+
order = 100 # 用户中间件可使用 0-99 在此之前执行
|
|
39
44
|
|
|
40
45
|
def build(self, config: BaseConfig) -> StarletteMiddleware:
|
|
41
46
|
"""构建请求日志中间件实例。"""
|
|
@@ -54,7 +59,7 @@ class CORSMiddleware(Middleware):
|
|
|
54
59
|
|
|
55
60
|
name = MiddlewareName.CORS
|
|
56
61
|
enabled = True
|
|
57
|
-
order =
|
|
62
|
+
order = 110 # 在日志中间件之后执行
|
|
58
63
|
|
|
59
64
|
def can_enable(self, config: BaseConfig) -> bool:
|
|
60
65
|
"""仅当配置了 origins 时启用。"""
|
|
@@ -83,7 +88,7 @@ class WebSocketLoggingMiddleware(Middleware):
|
|
|
83
88
|
|
|
84
89
|
name = MiddlewareName.WEBSOCKET_LOGGING
|
|
85
90
|
enabled = True
|
|
86
|
-
order =
|
|
91
|
+
order = 101 # 紧随 HTTP 日志中间件
|
|
87
92
|
|
|
88
93
|
def build(self, config: BaseConfig) -> StarletteMiddleware:
|
|
89
94
|
"""构建 WebSocket 日志中间件实例。"""
|