aury-boot 0.0.5__py3-none-any.whl → 0.0.8__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 (49) hide show
  1. aury/boot/_version.py +2 -2
  2. aury/boot/application/__init__.py +15 -0
  3. aury/boot/application/adapter/__init__.py +112 -0
  4. aury/boot/application/adapter/base.py +511 -0
  5. aury/boot/application/adapter/config.py +242 -0
  6. aury/boot/application/adapter/decorators.py +259 -0
  7. aury/boot/application/adapter/exceptions.py +202 -0
  8. aury/boot/application/adapter/http.py +325 -0
  9. aury/boot/application/app/middlewares.py +7 -4
  10. aury/boot/application/config/multi_instance.py +42 -26
  11. aury/boot/application/config/settings.py +111 -191
  12. aury/boot/application/middleware/logging.py +14 -1
  13. aury/boot/commands/generate.py +22 -22
  14. aury/boot/commands/init.py +41 -9
  15. aury/boot/commands/templates/project/AGENTS.md.tpl +8 -4
  16. aury/boot/commands/templates/project/aury_docs/01-model.md.tpl +17 -16
  17. aury/boot/commands/templates/project/aury_docs/11-logging.md.tpl +82 -43
  18. aury/boot/commands/templates/project/aury_docs/12-admin.md.tpl +14 -14
  19. aury/boot/commands/templates/project/aury_docs/13-channel.md.tpl +40 -28
  20. aury/boot/commands/templates/project/aury_docs/14-mq.md.tpl +9 -9
  21. aury/boot/commands/templates/project/aury_docs/15-events.md.tpl +8 -8
  22. aury/boot/commands/templates/project/aury_docs/16-adapter.md.tpl +403 -0
  23. aury/boot/commands/templates/project/aury_docs/99-cli.md.tpl +19 -19
  24. aury/boot/commands/templates/project/config.py.tpl +10 -10
  25. aury/boot/commands/templates/project/env_templates/_header.tpl +10 -0
  26. aury/boot/commands/templates/project/env_templates/admin.tpl +49 -0
  27. aury/boot/commands/templates/project/env_templates/cache.tpl +14 -0
  28. aury/boot/commands/templates/project/env_templates/database.tpl +22 -0
  29. aury/boot/commands/templates/project/env_templates/log.tpl +18 -0
  30. aury/boot/commands/templates/project/env_templates/messaging.tpl +46 -0
  31. aury/boot/commands/templates/project/env_templates/rpc.tpl +28 -0
  32. aury/boot/commands/templates/project/env_templates/scheduler.tpl +18 -0
  33. aury/boot/commands/templates/project/env_templates/service.tpl +18 -0
  34. aury/boot/commands/templates/project/env_templates/storage.tpl +38 -0
  35. aury/boot/commands/templates/project/env_templates/third_party.tpl +43 -0
  36. aury/boot/common/logging/__init__.py +26 -674
  37. aury/boot/common/logging/context.py +132 -0
  38. aury/boot/common/logging/decorators.py +118 -0
  39. aury/boot/common/logging/format.py +315 -0
  40. aury/boot/common/logging/setup.py +214 -0
  41. aury/boot/infrastructure/database/config.py +6 -14
  42. aury/boot/infrastructure/tasks/config.py +5 -13
  43. aury/boot/infrastructure/tasks/manager.py +8 -4
  44. aury/boot/testing/base.py +2 -2
  45. {aury_boot-0.0.5.dist-info → aury_boot-0.0.8.dist-info}/METADATA +2 -1
  46. {aury_boot-0.0.5.dist-info → aury_boot-0.0.8.dist-info}/RECORD +48 -27
  47. aury/boot/commands/templates/project/env.example.tpl +0 -281
  48. {aury_boot-0.0.5.dist-info → aury_boot-0.0.8.dist-info}/WHEEL +0 -0
  49. {aury_boot-0.0.5.dist-info → aury_boot-0.0.8.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
+ ]
@@ -30,14 +30,17 @@ class RequestLoggingMiddleware(Middleware):
30
30
 
31
31
  自动记录所有 HTTP 请求的详细信息,包括:
32
32
  - 请求方法、路径、查询参数
33
- - 客户端 IP、User-Agent
33
+ - 客户端IP、User-Agent
34
34
  - 响应状态码、耗时
35
35
  - 链路追踪 ID(X-Trace-ID / X-Request-ID)
36
+ - 请求上下文(user_id, tenant_id 等用户注册的字段)
37
+
38
+ 注意:用户的认证中间件应设置 order < 100,以便在日志记录前设置用户信息。
36
39
  """
37
40
 
38
41
  name = MiddlewareName.REQUEST_LOGGING
39
42
  enabled = True
40
- order = 0 # 最先执行,确保日志记录所有请求
43
+ order = 100 # 用户中间件可使用 0-99 在此之前执行
41
44
 
42
45
  def build(self, config: BaseConfig) -> StarletteMiddleware:
43
46
  """构建请求日志中间件实例。"""
@@ -56,7 +59,7 @@ class CORSMiddleware(Middleware):
56
59
 
57
60
  name = MiddlewareName.CORS
58
61
  enabled = True
59
- order = 10 # 在请求日志之后执行
62
+ order = 110 # 在日志中间件之后执行
60
63
 
61
64
  def can_enable(self, config: BaseConfig) -> bool:
62
65
  """仅当配置了 origins 时启用。"""
@@ -85,7 +88,7 @@ class WebSocketLoggingMiddleware(Middleware):
85
88
 
86
89
  name = MiddlewareName.WEBSOCKET_LOGGING
87
90
  enabled = True
88
- order = 1 # 紧随 HTTP 日志中间件
91
+ order = 101 # 紧随 HTTP 日志中间件
89
92
 
90
93
  def build(self, config: BaseConfig) -> StarletteMiddleware:
91
94
  """构建 WebSocket 日志中间件实例。"""
@@ -1,11 +1,12 @@
1
1
  """多实例配置解析工具。
2
2
 
3
- 支持从环境变量解析 {PREFIX}_{INSTANCE}_{FIELD} 格式的多实例配置。
3
+ 支持从环境变量解析 {PREFIX}__{INSTANCE}__{FIELD} 格式的多实例配置。
4
+ 使用双下划线 (__) 作为层级分隔符,符合行业标准。
4
5
 
5
6
  示例:
6
- DATABASE_DEFAULT_URL=postgresql://main...
7
- DATABASE_DEFAULT_POOL_SIZE=10
8
- DATABASE_ANALYTICS_URL=postgresql://analytics...
7
+ DATABASE__DEFAULT__URL=postgresql://main...
8
+ DATABASE__DEFAULT__POOL_SIZE=10
9
+ DATABASE__ANALYTICS__URL=postgresql://analytics...
9
10
 
10
11
  解析后:
11
12
  {
@@ -25,50 +26,65 @@ from pydantic import BaseModel
25
26
 
26
27
  def parse_multi_instance_env(
27
28
  prefix: str,
28
- fields: list[str],
29
+ fields: list[str] | None = None,
29
30
  *,
30
31
  type_hints: dict[str, type] | None = None,
31
32
  ) -> dict[str, dict[str, Any]]:
32
33
  """从环境变量解析多实例配置。
33
34
 
35
+ 使用双下划线 (__) 作为层级分隔符:
36
+ - {PREFIX}__{INSTANCE}__{FIELD}=value
37
+
34
38
  Args:
35
39
  prefix: 环境变量前缀,如 "DATABASE"
36
- fields: 支持的字段列表,如 ["url", "pool_size", "echo"]
40
+ fields: 支持的字段列表(可选,用于过滤)
37
41
  type_hints: 字段类型提示,用于类型转换
38
42
 
39
43
  Returns:
40
44
  dict[str, dict[str, Any]]: 实例名 -> 配置字典
41
45
 
42
46
  示例:
43
- >>> parse_multi_instance_env("DATABASE", ["url", "pool_size"])
47
+ >>> parse_multi_instance_env("DATABASE")
44
48
  {
45
- "default": {"url": "postgresql://...", "pool_size": "10"},
49
+ "default": {"url": "postgresql://...", "pool_size": 10},
46
50
  "analytics": {"url": "postgresql://..."}
47
51
  }
48
52
  """
49
53
  instances: dict[str, dict[str, Any]] = {}
50
54
  type_hints = type_hints or {}
55
+ prefix_with_sep = f"{prefix}__"
51
56
 
52
- # 构建正则:{PREFIX}_{INSTANCE}_{FIELD}
53
- # INSTANCE FIELD 都是大写字母和下划线
54
- fields_pattern = "|".join(re.escape(f.upper()) for f in fields)
55
- pattern = re.compile(
56
- rf"^{prefix}_([A-Z][A-Z0-9_]*)_({fields_pattern})$",
57
- re.IGNORECASE
58
- )
57
+ # 将 fields 转为大写集合用于过滤
58
+ valid_fields: set[str] | None = None
59
+ if fields:
60
+ valid_fields = {f.upper() for f in fields}
59
61
 
60
62
  for key, value in os.environ.items():
61
- match = pattern.match(key)
62
- if match:
63
- instance_name = match.group(1).lower()
64
- field_name = match.group(2).lower()
65
-
66
- # 类型转换
67
- converted_value = _convert_value(value, type_hints.get(field_name))
68
-
69
- if instance_name not in instances:
70
- instances[instance_name] = {}
71
- instances[instance_name][field_name] = converted_value
63
+ # 检查前缀
64
+ if not key.upper().startswith(prefix_with_sep):
65
+ continue
66
+
67
+ # 移除前缀后分割
68
+ remainder = key[len(prefix_with_sep):]
69
+ parts = remainder.split("__", 1) # 只分割一次:INSTANCE__FIELD
70
+
71
+ if len(parts) != 2:
72
+ continue
73
+
74
+ instance_name = parts[0].lower()
75
+ field_name = parts[1].lower()
76
+ field_name_upper = parts[1].upper()
77
+
78
+ # 如果指定了字段列表,进行过滤
79
+ if valid_fields and field_name_upper not in valid_fields:
80
+ continue
81
+
82
+ # 类型转换
83
+ converted_value = _convert_value(value, type_hints.get(field_name))
84
+
85
+ if instance_name not in instances:
86
+ instances[instance_name] = {}
87
+ instances[instance_name][field_name] = converted_value
72
88
 
73
89
  return instances
74
90