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,242 @@
|
|
|
1
|
+
"""第三方接口适配器配置模型。
|
|
2
|
+
|
|
3
|
+
提供统一的配置结构,支持:
|
|
4
|
+
- 多模式切换(real/sandbox/mock/disabled)
|
|
5
|
+
- 按方法级别的模式覆盖
|
|
6
|
+
- 通用连接参数(URL、认证、超时等)
|
|
7
|
+
- 挡板(Mock)策略配置
|
|
8
|
+
|
|
9
|
+
模式说明:
|
|
10
|
+
- real: 调用真实第三方接口(生产环境)
|
|
11
|
+
- sandbox: 调用第三方提供的沙箱环境
|
|
12
|
+
- mock: 使用本地挡板实现,不发出真实请求(测试/开发环境)
|
|
13
|
+
- disabled: 禁用该接口,调用时抛出 AdapterDisabledError
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import Any, Literal
|
|
19
|
+
|
|
20
|
+
from pydantic import BaseModel, Field
|
|
21
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
22
|
+
|
|
23
|
+
# 适配器模式类型
|
|
24
|
+
# - real: 真实调用第三方接口
|
|
25
|
+
# - sandbox: 调用第三方沙箱环境
|
|
26
|
+
# - mock: 使用本地挡板实现
|
|
27
|
+
# - disabled: 禁用,调用时抛出异常
|
|
28
|
+
AdapterMode = Literal["real", "sandbox", "mock", "disabled"]
|
|
29
|
+
|
|
30
|
+
# 挡板(Mock)策略类型
|
|
31
|
+
# - success: 返回成功响应
|
|
32
|
+
# - failure: 返回失败响应
|
|
33
|
+
# - echo: 回显请求参数
|
|
34
|
+
# - noop: 空操作,返回空结果
|
|
35
|
+
# - custom: 自定义,使用 @method.mock 定义的处理函数
|
|
36
|
+
MockStrategy = Literal["success", "failure", "echo", "noop", "custom"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class AdapterSettings(BaseModel):
|
|
40
|
+
"""第三方接口适配器配置。
|
|
41
|
+
|
|
42
|
+
通用的第三方服务配置模型,支持多模式切换和按方法覆盖。
|
|
43
|
+
|
|
44
|
+
配置项分类:
|
|
45
|
+
1. 基础配置: enabled, mode
|
|
46
|
+
2. 连接配置: base_url, sandbox_url, api_key, api_secret, timeout, retry_times
|
|
47
|
+
3. 模式覆盖: method_modes - 按方法名覆盖全局模式
|
|
48
|
+
4. 挡板配置: mock_strategy, mock_default_response, mock_delay, mock_method_responses
|
|
49
|
+
|
|
50
|
+
环境变量配置示例:
|
|
51
|
+
# 基础配置
|
|
52
|
+
THIRD_PARTY_PAYMENT_ENABLED=true
|
|
53
|
+
THIRD_PARTY_PAYMENT_MODE=mock # 测试环境用挡板
|
|
54
|
+
|
|
55
|
+
# 连接配置
|
|
56
|
+
THIRD_PARTY_PAYMENT_BASE_URL=https://api.payment.com
|
|
57
|
+
THIRD_PARTY_PAYMENT_SANDBOX_URL=https://sandbox.payment.com
|
|
58
|
+
THIRD_PARTY_PAYMENT_API_KEY=sk_live_xxx
|
|
59
|
+
THIRD_PARTY_PAYMENT_TIMEOUT=30
|
|
60
|
+
|
|
61
|
+
# 方法级模式覆盖(JSON 格式)
|
|
62
|
+
# query 走真实接口,refund 禁用
|
|
63
|
+
THIRD_PARTY_PAYMENT_METHOD_MODES={"query": "real", "refund": "disabled"}
|
|
64
|
+
|
|
65
|
+
# 挡板配置
|
|
66
|
+
THIRD_PARTY_PAYMENT_MOCK_DELAY=0.1 # 模拟网络延迟 100ms
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
# ========== 基础配置 ==========
|
|
70
|
+
enabled: bool = Field(
|
|
71
|
+
default=True,
|
|
72
|
+
description="是否启用此集成",
|
|
73
|
+
)
|
|
74
|
+
mode: AdapterMode = Field(
|
|
75
|
+
default="real",
|
|
76
|
+
description="当前模式:real(生产)/sandbox(沙箱)/mock(挡板)/disabled(禁用)",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# ========== 连接配置 ==========
|
|
80
|
+
base_url: str | None = Field(
|
|
81
|
+
default=None,
|
|
82
|
+
description="服务基础 URL(生产环境)",
|
|
83
|
+
)
|
|
84
|
+
sandbox_url: str | None = Field(
|
|
85
|
+
default=None,
|
|
86
|
+
description="沙箱环境 URL(sandbox 模式时优先使用)",
|
|
87
|
+
)
|
|
88
|
+
api_key: str | None = Field(
|
|
89
|
+
default=None,
|
|
90
|
+
description="API 密钥(如 Authorization header)",
|
|
91
|
+
)
|
|
92
|
+
api_secret: str | None = Field(
|
|
93
|
+
default=None,
|
|
94
|
+
description="API 密钥(用于签名等)",
|
|
95
|
+
)
|
|
96
|
+
timeout: int = Field(
|
|
97
|
+
default=30,
|
|
98
|
+
description="请求超时(秒)",
|
|
99
|
+
)
|
|
100
|
+
retry_times: int = Field(
|
|
101
|
+
default=3,
|
|
102
|
+
description="重试次数",
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# ========== 模式覆盖 ==========
|
|
106
|
+
method_modes: dict[str, AdapterMode] = Field(
|
|
107
|
+
default_factory=dict,
|
|
108
|
+
description="按方法覆盖模式,如 {'refund': 'disabled', 'query': 'real'}",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# ========== Mock 配置 ==========
|
|
112
|
+
mock_strategy: MockStrategy = Field(
|
|
113
|
+
default="success",
|
|
114
|
+
description="默认 mock 策略:success(成功)/failure(失败)/echo(回显)/noop(空操作)/custom(自定义)",
|
|
115
|
+
)
|
|
116
|
+
mock_default_response: dict[str, Any] | None = Field(
|
|
117
|
+
default=None,
|
|
118
|
+
description="默认 mock 响应(当没有自定义 mock handler 时使用)",
|
|
119
|
+
)
|
|
120
|
+
mock_delay: float = Field(
|
|
121
|
+
default=0.0,
|
|
122
|
+
description="mock 模拟延迟(秒),用于模拟网络延迟",
|
|
123
|
+
)
|
|
124
|
+
mock_method_responses: dict[str, dict[str, Any]] = Field(
|
|
125
|
+
default_factory=dict,
|
|
126
|
+
description="按方法的 mock 响应,如 {'create': {'success': True, 'id': 'mock-123'}}",
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# ========== 扩展配置 ==========
|
|
130
|
+
extra: dict[str, Any] = Field(
|
|
131
|
+
default_factory=dict,
|
|
132
|
+
description="额外配置(供子类或特定第三方使用)",
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def get_effective_url(self) -> str | None:
|
|
136
|
+
"""获取当前模式下的有效 URL。
|
|
137
|
+
|
|
138
|
+
sandbox 模式优先使用 sandbox_url,否则使用 base_url。
|
|
139
|
+
"""
|
|
140
|
+
if self.mode == "sandbox" and self.sandbox_url:
|
|
141
|
+
return self.sandbox_url
|
|
142
|
+
return self.base_url
|
|
143
|
+
|
|
144
|
+
def get_method_mode(self, method_name: str) -> AdapterMode:
|
|
145
|
+
"""获取指定方法的有效模式。
|
|
146
|
+
|
|
147
|
+
优先使用 method_modes 中的配置,否则使用全局 mode。
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
method_name: 方法名称
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
AdapterMode: 有效模式
|
|
154
|
+
"""
|
|
155
|
+
return self.method_modes.get(method_name, self.mode)
|
|
156
|
+
|
|
157
|
+
def is_method_enabled(self, method_name: str) -> bool:
|
|
158
|
+
"""检查指定方法是否启用。
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
method_name: 方法名称
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
bool: 是否启用
|
|
165
|
+
"""
|
|
166
|
+
if not self.enabled:
|
|
167
|
+
return False
|
|
168
|
+
return self.get_method_mode(method_name) != "disabled"
|
|
169
|
+
|
|
170
|
+
def get_mock_response(self, method_name: str) -> dict[str, Any] | None:
|
|
171
|
+
"""获取指定方法的 mock 响应。
|
|
172
|
+
|
|
173
|
+
优先使用 mock_method_responses,否则使用 mock_default_response。
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
method_name: 方法名称
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
dict | None: mock 响应
|
|
180
|
+
"""
|
|
181
|
+
return self.mock_method_responses.get(method_name, self.mock_default_response)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class ThirdPartySettings(BaseSettings):
|
|
185
|
+
"""第三方服务配置聚合。
|
|
186
|
+
|
|
187
|
+
将所有第三方服务的配置聚合到一个类中,便于统一管理。
|
|
188
|
+
项目可以继承此类添加更多第三方服务。
|
|
189
|
+
|
|
190
|
+
环境变量前缀: THIRD_PARTY_
|
|
191
|
+
|
|
192
|
+
使用示例:
|
|
193
|
+
class MyThirdPartySettings(ThirdPartySettings):
|
|
194
|
+
wechat: AdapterSettings = Field(default_factory=AdapterSettings)
|
|
195
|
+
alipay: AdapterSettings = Field(default_factory=AdapterSettings)
|
|
196
|
+
|
|
197
|
+
# 环境变量
|
|
198
|
+
THIRD_PARTY_WECHAT_MODE=sandbox
|
|
199
|
+
THIRD_PARTY_ALIPAY_MODE=mock
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
# 示例:常见第三方服务(项目可以继承并添加)
|
|
203
|
+
# payment: AdapterSettings = Field(default_factory=AdapterSettings)
|
|
204
|
+
# sms: AdapterSettings = Field(default_factory=AdapterSettings)
|
|
205
|
+
# oss: AdapterSettings = Field(default_factory=AdapterSettings)
|
|
206
|
+
# email: AdapterSettings = Field(default_factory=AdapterSettings)
|
|
207
|
+
|
|
208
|
+
model_config = SettingsConfigDict(
|
|
209
|
+
env_prefix="THIRD_PARTY_",
|
|
210
|
+
env_nested_delimiter="__",
|
|
211
|
+
case_sensitive=False,
|
|
212
|
+
extra="ignore",
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
def set_all_mode(self, mode: AdapterMode) -> None:
|
|
216
|
+
"""将所有第三方服务设置为指定模式。
|
|
217
|
+
|
|
218
|
+
便捷方法,用于测试环境一键切换所有服务到 mock 模式。
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
mode: 目标模式
|
|
222
|
+
"""
|
|
223
|
+
for field_name in self.model_fields:
|
|
224
|
+
field_value = getattr(self, field_name, None)
|
|
225
|
+
if isinstance(field_value, AdapterSettings):
|
|
226
|
+
field_value.mode = mode
|
|
227
|
+
|
|
228
|
+
def disable_all(self) -> None:
|
|
229
|
+
"""禁用所有第三方服务。"""
|
|
230
|
+
self.set_all_mode("disabled")
|
|
231
|
+
|
|
232
|
+
def mock_all(self) -> None:
|
|
233
|
+
"""将所有第三方服务设置为 mock 模式。"""
|
|
234
|
+
self.set_all_mode("mock")
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
__all__ = [
|
|
238
|
+
"AdapterMode",
|
|
239
|
+
"AdapterSettings",
|
|
240
|
+
"MockStrategy",
|
|
241
|
+
"ThirdPartySettings",
|
|
242
|
+
]
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""第三方接口适配器装饰器。
|
|
2
|
+
|
|
3
|
+
提供 @adapter_method 装饰器,用于声明式定义 Adapter 方法和挡板(Mock)逻辑。
|
|
4
|
+
|
|
5
|
+
核心特性:
|
|
6
|
+
- 声明方法名称,用于配置层按方法覆盖模式(method_modes)
|
|
7
|
+
- 支持链式 .mock 注册挡板处理函数
|
|
8
|
+
- 自动根据 settings.mode 路由到真实调用或挡板逻辑
|
|
9
|
+
- 记录调用历史,便于测试断言
|
|
10
|
+
|
|
11
|
+
挡板定义方式:
|
|
12
|
+
1. @method.mock 装饰器:定义带逻辑的挡板函数,可以根据参数返回不同结果
|
|
13
|
+
2. mock_response 参数:简单场景直接指定固定响应值
|
|
14
|
+
3. 配置 mock_method_responses:按方法名配置固定响应
|
|
15
|
+
4. 配置 mock_default_response:全局默认响应
|
|
16
|
+
|
|
17
|
+
使用示例:
|
|
18
|
+
class PaymentAdapter(HttpAdapter):
|
|
19
|
+
# 方法 1:使用 @method.mock 定义挡板逻辑
|
|
20
|
+
@adapter_method("create")
|
|
21
|
+
async def create_order(self, amount: int, order_id: str) -> dict:
|
|
22
|
+
# 真实调用逻辑
|
|
23
|
+
return await self._request("POST", "/charge", json={...})
|
|
24
|
+
|
|
25
|
+
@create_order.mock
|
|
26
|
+
async def create_order_mock(self, amount: int, order_id: str) -> dict:
|
|
27
|
+
# 挡板逻辑,可以模拟各种场景
|
|
28
|
+
if amount > 10000:
|
|
29
|
+
return {"success": False, "error": "金额超限"} # 模拟失败
|
|
30
|
+
return {"success": True, "mock": True} # 模拟成功
|
|
31
|
+
|
|
32
|
+
# 方法 2:简单场景直接指定固定响应
|
|
33
|
+
@adapter_method("query", mock_response={"status": "paid", "mock": True})
|
|
34
|
+
async def query_order(self, order_id: str) -> dict:
|
|
35
|
+
return await self._request("GET", f"/orders/{order_id}")
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
import asyncio
|
|
41
|
+
from dataclasses import dataclass, field
|
|
42
|
+
from functools import wraps
|
|
43
|
+
from typing import TYPE_CHECKING, Any, Awaitable, Callable, TypeVar
|
|
44
|
+
|
|
45
|
+
from aury.boot.common.logging import logger
|
|
46
|
+
|
|
47
|
+
from .config import AdapterMode
|
|
48
|
+
from .exceptions import AdapterDisabledError
|
|
49
|
+
|
|
50
|
+
if TYPE_CHECKING:
|
|
51
|
+
from .base import BaseAdapter
|
|
52
|
+
|
|
53
|
+
T = TypeVar("T")
|
|
54
|
+
F = TypeVar("F", bound=Callable[..., Awaitable[Any]])
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class AdapterMethodMeta:
|
|
59
|
+
"""适配器方法元信息。
|
|
60
|
+
|
|
61
|
+
存储 @adapter_method 装饰器声明的信息,包括:
|
|
62
|
+
- 方法名称
|
|
63
|
+
- mock 处理函数
|
|
64
|
+
- 固定 mock 响应
|
|
65
|
+
- 模式覆盖
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
name: str
|
|
69
|
+
mock_handler: Callable[..., Awaitable[Any]] | None = None
|
|
70
|
+
mock_response: Any = None
|
|
71
|
+
mode_override: AdapterMode | None = None
|
|
72
|
+
|
|
73
|
+
# 内部使用:原始真实方法
|
|
74
|
+
_real_method: Callable[..., Awaitable[Any]] | None = field(default=None, repr=False)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class AdapterMethodDescriptor:
|
|
78
|
+
"""适配器方法描述符。
|
|
79
|
+
|
|
80
|
+
实现类似 @property 的描述符协议,支持链式 .mock 注册。
|
|
81
|
+
|
|
82
|
+
使用示例:
|
|
83
|
+
class PaymentAdapter(HttpAdapter):
|
|
84
|
+
@adapter_method("create")
|
|
85
|
+
async def create_order(self, ...): ...
|
|
86
|
+
|
|
87
|
+
@create_order.mock
|
|
88
|
+
async def create_order_mock(self, ...): ...
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
def __init__(
|
|
92
|
+
self,
|
|
93
|
+
real_method: Callable[..., Awaitable[Any]],
|
|
94
|
+
meta: AdapterMethodMeta,
|
|
95
|
+
) -> None:
|
|
96
|
+
"""初始化描述符。
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
real_method: 真实调用方法
|
|
100
|
+
meta: 方法元信息
|
|
101
|
+
"""
|
|
102
|
+
self._real_method = real_method
|
|
103
|
+
self._meta = meta
|
|
104
|
+
self._meta._real_method = real_method
|
|
105
|
+
|
|
106
|
+
# 复制原方法的文档和名称
|
|
107
|
+
self.__name__ = real_method.__name__
|
|
108
|
+
self.__doc__ = real_method.__doc__
|
|
109
|
+
self.__module__ = real_method.__module__
|
|
110
|
+
self.__qualname__ = getattr(real_method, "__qualname__", real_method.__name__)
|
|
111
|
+
|
|
112
|
+
def __get__(self, obj: BaseAdapter | None, objtype: type | None = None) -> Any:
|
|
113
|
+
"""描述符协议:获取绑定方法。"""
|
|
114
|
+
if obj is None:
|
|
115
|
+
# 类访问,返回描述符本身(用于 .mock 链式调用)
|
|
116
|
+
return self
|
|
117
|
+
|
|
118
|
+
# 实例访问,返回包装后的绑定方法
|
|
119
|
+
return self._create_bound_method(obj)
|
|
120
|
+
|
|
121
|
+
def __set_name__(self, owner: type, name: str) -> None:
|
|
122
|
+
"""描述符协议:设置属性名。"""
|
|
123
|
+
self._attr_name = name
|
|
124
|
+
|
|
125
|
+
def _create_bound_method(self, adapter: BaseAdapter) -> Callable[..., Awaitable[Any]]:
|
|
126
|
+
"""创建绑定到实例的方法。"""
|
|
127
|
+
meta = self._meta
|
|
128
|
+
real_method = self._real_method
|
|
129
|
+
|
|
130
|
+
@wraps(real_method)
|
|
131
|
+
async def bound_method(*args: Any, **kwargs: Any) -> Any:
|
|
132
|
+
# 解析当前操作的有效模式
|
|
133
|
+
mode = adapter._resolve_mode(meta)
|
|
134
|
+
|
|
135
|
+
# 记录调用
|
|
136
|
+
adapter._record_call(
|
|
137
|
+
operation=meta.name,
|
|
138
|
+
args=args,
|
|
139
|
+
kwargs=kwargs,
|
|
140
|
+
mode=mode,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# 检查是否禁用
|
|
144
|
+
if not adapter.settings.enabled or mode == "disabled":
|
|
145
|
+
raise AdapterDisabledError(
|
|
146
|
+
f"{adapter.name}.{meta.name} is disabled in current environment",
|
|
147
|
+
adapter_name=adapter.name,
|
|
148
|
+
operation=meta.name,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# mock 模式
|
|
152
|
+
if mode == "mock":
|
|
153
|
+
return await adapter._invoke_mock(meta, args, kwargs)
|
|
154
|
+
|
|
155
|
+
# real / sandbox 模式:调用原始方法
|
|
156
|
+
# 触发钩子
|
|
157
|
+
await adapter._on_before_call(meta.name, args, kwargs)
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
result = await real_method(adapter, *args, **kwargs)
|
|
161
|
+
await adapter._on_after_call(meta.name, args, kwargs, result)
|
|
162
|
+
return result
|
|
163
|
+
except Exception as exc:
|
|
164
|
+
await adapter._on_call_error(meta.name, args, kwargs, exc)
|
|
165
|
+
raise
|
|
166
|
+
|
|
167
|
+
return bound_method
|
|
168
|
+
|
|
169
|
+
def mock(self, mock_method: F) -> F:
|
|
170
|
+
"""注册 mock 处理函数(链式装饰器)。
|
|
171
|
+
|
|
172
|
+
使用示例:
|
|
173
|
+
@adapter_method("create")
|
|
174
|
+
async def create_order(self, ...): ...
|
|
175
|
+
|
|
176
|
+
@create_order.mock
|
|
177
|
+
async def create_order_mock(self, ...): ...
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
mock_method: mock 处理函数
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
原始 mock 方法(保持可调用)
|
|
184
|
+
"""
|
|
185
|
+
self._meta.mock_handler = mock_method
|
|
186
|
+
logger.debug(f"Adapter 注册 mock handler: {self.__name__} -> {mock_method.__name__}")
|
|
187
|
+
|
|
188
|
+
# 返回原方法,使其可以作为独立方法调用(如果需要)
|
|
189
|
+
return mock_method
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def adapter_method(
|
|
193
|
+
name: str,
|
|
194
|
+
*,
|
|
195
|
+
mock_response: Any = None,
|
|
196
|
+
mode_override: AdapterMode | None = None,
|
|
197
|
+
) -> Callable[[F], AdapterMethodDescriptor]:
|
|
198
|
+
"""适配器方法装饰器。
|
|
199
|
+
|
|
200
|
+
声明一个 Adapter 方法,支持:
|
|
201
|
+
- 根据配置自动切换 real/mock 模式
|
|
202
|
+
- 链式 .mock 注册 mock 处理函数
|
|
203
|
+
- 调用历史记录
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
name: 方法名称,用于配置层按方法覆盖模式
|
|
207
|
+
mock_response: 简单 mock 场景的固定响应(可选)
|
|
208
|
+
mode_override: 强制覆盖模式,不走配置(可选,用于某些方法必须真实调用的场景)
|
|
209
|
+
|
|
210
|
+
使用示例:
|
|
211
|
+
# 基础用法
|
|
212
|
+
@adapter_method("create")
|
|
213
|
+
async def create_order(self, amount: int) -> dict:
|
|
214
|
+
return await self._request("POST", "/charge", json={"amount": amount})
|
|
215
|
+
|
|
216
|
+
# 带固定 mock 响应
|
|
217
|
+
@adapter_method("query", mock_response={"status": "paid", "mock": True})
|
|
218
|
+
async def query_order(self, order_id: str) -> dict:
|
|
219
|
+
return await self._request("GET", f"/orders/{order_id}")
|
|
220
|
+
|
|
221
|
+
# 强制走真实(无论配置如何)
|
|
222
|
+
@adapter_method("health", mode_override="real")
|
|
223
|
+
async def health_check(self) -> dict:
|
|
224
|
+
return await self._request("GET", "/health")
|
|
225
|
+
|
|
226
|
+
# 链式 mock
|
|
227
|
+
@adapter_method("create")
|
|
228
|
+
async def create_order(self, amount: int) -> dict:
|
|
229
|
+
return await self._request("POST", "/charge", json={"amount": amount})
|
|
230
|
+
|
|
231
|
+
@create_order.mock
|
|
232
|
+
async def create_order_mock(self, amount: int) -> dict:
|
|
233
|
+
if amount > 10000:
|
|
234
|
+
return {"success": False, "error": "超限"}
|
|
235
|
+
return {"success": True, "mock": True, "amount": amount}
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
装饰器函数
|
|
239
|
+
"""
|
|
240
|
+
def decorator(fn: F) -> AdapterMethodDescriptor:
|
|
241
|
+
meta = AdapterMethodMeta(
|
|
242
|
+
name=name,
|
|
243
|
+
mock_response=mock_response,
|
|
244
|
+
mode_override=mode_override,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
descriptor = AdapterMethodDescriptor(fn, meta)
|
|
248
|
+
logger.debug(f"Adapter 注册方法: {fn.__name__} -> {name}")
|
|
249
|
+
|
|
250
|
+
return descriptor
|
|
251
|
+
|
|
252
|
+
return decorator
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
__all__ = [
|
|
256
|
+
"AdapterMethodDescriptor",
|
|
257
|
+
"AdapterMethodMeta",
|
|
258
|
+
"adapter_method",
|
|
259
|
+
]
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""第三方接口适配器异常定义。
|
|
2
|
+
|
|
3
|
+
用于第三方接口调用过程中的各类异常场景,所有异常都继承自 FoundationError。
|
|
4
|
+
|
|
5
|
+
异常类型:
|
|
6
|
+
- AdapterError: 通用错误(调用失败、第三方返回错误码等)
|
|
7
|
+
- AdapterDisabledError: 接口被禁用(mode=disabled 或 enabled=False)
|
|
8
|
+
- AdapterTimeoutError: 调用超时
|
|
9
|
+
- AdapterValidationError: 响应数据格式验证失败
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from aury.boot.common.exceptions import FoundationError
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AdapterError(FoundationError):
|
|
20
|
+
"""第三方适配器通用错误。
|
|
21
|
+
|
|
22
|
+
当第三方调用失败、返回错误码、数据解析异常等情况时抛出。
|
|
23
|
+
|
|
24
|
+
Attributes:
|
|
25
|
+
adapter_name: 适配器名称
|
|
26
|
+
operation: 操作名称
|
|
27
|
+
third_party_code: 第三方返回的错误码(如有)
|
|
28
|
+
third_party_message: 第三方返回的错误消息(如有)
|
|
29
|
+
|
|
30
|
+
使用示例:
|
|
31
|
+
raise AdapterError(
|
|
32
|
+
"支付失败",
|
|
33
|
+
adapter_name="payment",
|
|
34
|
+
operation="charge",
|
|
35
|
+
third_party_code="INSUFFICIENT_FUNDS",
|
|
36
|
+
third_party_message="余额不足",
|
|
37
|
+
)
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
message: str,
|
|
43
|
+
*args: object,
|
|
44
|
+
adapter_name: str | None = None,
|
|
45
|
+
operation: str | None = None,
|
|
46
|
+
third_party_code: str | None = None,
|
|
47
|
+
third_party_message: str | None = None,
|
|
48
|
+
metadata: dict[str, Any] | None = None,
|
|
49
|
+
cause: Exception | None = None,
|
|
50
|
+
) -> None:
|
|
51
|
+
"""初始化集成异常。
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
message: 错误消息
|
|
55
|
+
adapter_name: 适配器名称
|
|
56
|
+
operation: 操作名称
|
|
57
|
+
third_party_code: 第三方错误码
|
|
58
|
+
third_party_message: 第三方错误消息
|
|
59
|
+
metadata: 额外元数据
|
|
60
|
+
cause: 原始异常
|
|
61
|
+
"""
|
|
62
|
+
self.adapter_name = adapter_name
|
|
63
|
+
self.operation = operation
|
|
64
|
+
self.third_party_code = third_party_code
|
|
65
|
+
self.third_party_message = third_party_message
|
|
66
|
+
|
|
67
|
+
# 构建完整的元数据
|
|
68
|
+
full_metadata = metadata or {}
|
|
69
|
+
if adapter_name:
|
|
70
|
+
full_metadata["adapter_name"] = adapter_name
|
|
71
|
+
if operation:
|
|
72
|
+
full_metadata["operation"] = operation
|
|
73
|
+
if third_party_code:
|
|
74
|
+
full_metadata["third_party_code"] = third_party_code
|
|
75
|
+
if third_party_message:
|
|
76
|
+
full_metadata["third_party_message"] = third_party_message
|
|
77
|
+
|
|
78
|
+
super().__init__(message, *args, metadata=full_metadata, cause=cause)
|
|
79
|
+
|
|
80
|
+
def __str__(self) -> str:
|
|
81
|
+
"""返回异常字符串表示。"""
|
|
82
|
+
parts = [self.message]
|
|
83
|
+
if self.adapter_name:
|
|
84
|
+
parts.append(f"adapter={self.adapter_name}")
|
|
85
|
+
if self.operation:
|
|
86
|
+
parts.append(f"operation={self.operation}")
|
|
87
|
+
if self.third_party_code:
|
|
88
|
+
parts.append(f"code={self.third_party_code}")
|
|
89
|
+
return " | ".join(parts)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class AdapterDisabledError(AdapterError):
|
|
93
|
+
"""当前环境禁用了某个第三方功能。
|
|
94
|
+
|
|
95
|
+
当 mode=disabled 或 enabled=False 时,调用任何操作都会抛出此异常。
|
|
96
|
+
|
|
97
|
+
使用示例:
|
|
98
|
+
# 在测试环境禁用支付功能
|
|
99
|
+
settings = AdapterSettings(mode="disabled")
|
|
100
|
+
adapter = PaymentAdapter("payment", settings)
|
|
101
|
+
|
|
102
|
+
await adapter.charge(100) # 抛出 AdapterDisabledError
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
def __init__(
|
|
106
|
+
self,
|
|
107
|
+
message: str,
|
|
108
|
+
*args: object,
|
|
109
|
+
adapter_name: str | None = None,
|
|
110
|
+
operation: str | None = None,
|
|
111
|
+
metadata: dict[str, Any] | None = None,
|
|
112
|
+
) -> None:
|
|
113
|
+
"""初始化禁用异常。"""
|
|
114
|
+
super().__init__(
|
|
115
|
+
message,
|
|
116
|
+
*args,
|
|
117
|
+
adapter_name=adapter_name,
|
|
118
|
+
operation=operation,
|
|
119
|
+
metadata=metadata,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class AdapterTimeoutError(AdapterError):
|
|
124
|
+
"""第三方调用超时。
|
|
125
|
+
|
|
126
|
+
当调用第三方接口超过配置的超时时间时抛出。
|
|
127
|
+
|
|
128
|
+
Attributes:
|
|
129
|
+
timeout_seconds: 超时时间(秒)
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
def __init__(
|
|
133
|
+
self,
|
|
134
|
+
message: str,
|
|
135
|
+
*args: object,
|
|
136
|
+
adapter_name: str | None = None,
|
|
137
|
+
operation: str | None = None,
|
|
138
|
+
timeout_seconds: float | None = None,
|
|
139
|
+
metadata: dict[str, Any] | None = None,
|
|
140
|
+
cause: Exception | None = None,
|
|
141
|
+
) -> None:
|
|
142
|
+
"""初始化超时异常。"""
|
|
143
|
+
self.timeout_seconds = timeout_seconds
|
|
144
|
+
|
|
145
|
+
full_metadata = metadata or {}
|
|
146
|
+
if timeout_seconds is not None:
|
|
147
|
+
full_metadata["timeout_seconds"] = timeout_seconds
|
|
148
|
+
|
|
149
|
+
super().__init__(
|
|
150
|
+
message,
|
|
151
|
+
*args,
|
|
152
|
+
adapter_name=adapter_name,
|
|
153
|
+
operation=operation,
|
|
154
|
+
metadata=full_metadata,
|
|
155
|
+
cause=cause,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class AdapterValidationError(AdapterError):
|
|
160
|
+
"""第三方响应数据验证失败。
|
|
161
|
+
|
|
162
|
+
当第三方返回的数据格式不符合预期时抛出。
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
def __init__(
|
|
166
|
+
self,
|
|
167
|
+
message: str,
|
|
168
|
+
*args: object,
|
|
169
|
+
adapter_name: str | None = None,
|
|
170
|
+
operation: str | None = None,
|
|
171
|
+
expected_format: str | None = None,
|
|
172
|
+
actual_data: Any | None = None,
|
|
173
|
+
metadata: dict[str, Any] | None = None,
|
|
174
|
+
cause: Exception | None = None,
|
|
175
|
+
) -> None:
|
|
176
|
+
"""初始化验证异常。"""
|
|
177
|
+
full_metadata = metadata or {}
|
|
178
|
+
if expected_format:
|
|
179
|
+
full_metadata["expected_format"] = expected_format
|
|
180
|
+
if actual_data is not None:
|
|
181
|
+
# 截断过长的数据
|
|
182
|
+
actual_str = str(actual_data)
|
|
183
|
+
if len(actual_str) > 200:
|
|
184
|
+
actual_str = actual_str[:200] + "..."
|
|
185
|
+
full_metadata["actual_data"] = actual_str
|
|
186
|
+
|
|
187
|
+
super().__init__(
|
|
188
|
+
message,
|
|
189
|
+
*args,
|
|
190
|
+
adapter_name=adapter_name,
|
|
191
|
+
operation=operation,
|
|
192
|
+
metadata=full_metadata,
|
|
193
|
+
cause=cause,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
__all__ = [
|
|
198
|
+
"AdapterError",
|
|
199
|
+
"AdapterDisabledError",
|
|
200
|
+
"AdapterTimeoutError",
|
|
201
|
+
"AdapterValidationError",
|
|
202
|
+
]
|