aury-boot 0.0.5__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/_version.py +2 -2
- aury/boot/application/__init__.py +15 -0
- 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/middlewares.py +7 -4
- aury/boot/application/config/multi_instance.py +42 -26
- aury/boot/application/config/settings.py +111 -191
- aury/boot/application/middleware/logging.py +14 -1
- aury/boot/commands/generate.py +22 -22
- aury/boot/commands/init.py +41 -9
- aury/boot/commands/templates/project/AGENTS.md.tpl +8 -4
- aury/boot/commands/templates/project/aury_docs/01-model.md.tpl +17 -16
- aury/boot/commands/templates/project/aury_docs/11-logging.md.tpl +82 -43
- aury/boot/commands/templates/project/aury_docs/12-admin.md.tpl +14 -14
- aury/boot/commands/templates/project/aury_docs/13-channel.md.tpl +40 -28
- aury/boot/commands/templates/project/aury_docs/14-mq.md.tpl +9 -9
- aury/boot/commands/templates/project/aury_docs/15-events.md.tpl +8 -8
- aury/boot/commands/templates/project/aury_docs/16-adapter.md.tpl +403 -0
- aury/boot/commands/templates/project/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/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/infrastructure/database/config.py +6 -14
- aury/boot/infrastructure/tasks/config.py +5 -13
- aury/boot/infrastructure/tasks/manager.py +8 -4
- aury/boot/testing/base.py +2 -2
- {aury_boot-0.0.5.dist-info → aury_boot-0.0.7.dist-info}/METADATA +2 -1
- {aury_boot-0.0.5.dist-info → aury_boot-0.0.7.dist-info}/RECORD +48 -27
- aury/boot/commands/templates/project/env.example.tpl +0 -281
- {aury_boot-0.0.5.dist-info → aury_boot-0.0.7.dist-info}/WHEEL +0 -0
- {aury_boot-0.0.5.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
|
+
]
|
|
@@ -30,14 +30,17 @@ class RequestLoggingMiddleware(Middleware):
|
|
|
30
30
|
|
|
31
31
|
自动记录所有 HTTP 请求的详细信息,包括:
|
|
32
32
|
- 请求方法、路径、查询参数
|
|
33
|
-
- 客户端
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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}
|
|
3
|
+
支持从环境变量解析 {PREFIX}__{INSTANCE}__{FIELD} 格式的多实例配置。
|
|
4
|
+
使用双下划线 (__) 作为层级分隔符,符合行业标准。
|
|
4
5
|
|
|
5
6
|
示例:
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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:
|
|
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"
|
|
47
|
+
>>> parse_multi_instance_env("DATABASE")
|
|
44
48
|
{
|
|
45
|
-
"default": {"url": "postgresql://...", "pool_size":
|
|
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
|
-
#
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
62
|
-
if
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|