aury-boot 0.0.2__py3-none-any.whl → 0.0.4__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 +66 -0
- aury/boot/_version.py +2 -2
- aury/boot/application/__init__.py +120 -0
- aury/boot/application/app/__init__.py +39 -0
- aury/boot/application/app/base.py +511 -0
- aury/boot/application/app/components.py +434 -0
- aury/boot/application/app/middlewares.py +101 -0
- aury/boot/application/config/__init__.py +44 -0
- aury/boot/application/config/settings.py +663 -0
- aury/boot/application/constants/__init__.py +19 -0
- aury/boot/application/constants/components.py +50 -0
- aury/boot/application/constants/scheduler.py +28 -0
- aury/boot/application/constants/service.py +29 -0
- aury/boot/application/errors/__init__.py +55 -0
- aury/boot/application/errors/chain.py +80 -0
- aury/boot/application/errors/codes.py +67 -0
- aury/boot/application/errors/exceptions.py +238 -0
- aury/boot/application/errors/handlers.py +320 -0
- aury/boot/application/errors/response.py +120 -0
- aury/boot/application/interfaces/__init__.py +76 -0
- aury/boot/application/interfaces/egress.py +224 -0
- aury/boot/application/interfaces/ingress.py +98 -0
- aury/boot/application/middleware/__init__.py +22 -0
- aury/boot/application/middleware/logging.py +451 -0
- aury/boot/application/migrations/__init__.py +13 -0
- aury/boot/application/migrations/manager.py +685 -0
- aury/boot/application/migrations/setup.py +237 -0
- aury/boot/application/rpc/__init__.py +63 -0
- aury/boot/application/rpc/base.py +108 -0
- aury/boot/application/rpc/client.py +294 -0
- aury/boot/application/rpc/discovery.py +218 -0
- aury/boot/application/scheduler/__init__.py +13 -0
- aury/boot/application/scheduler/runner.py +123 -0
- aury/boot/application/server/__init__.py +296 -0
- aury/boot/commands/__init__.py +30 -0
- aury/boot/commands/add.py +76 -0
- aury/boot/commands/app.py +105 -0
- aury/boot/commands/config.py +177 -0
- aury/boot/commands/docker.py +367 -0
- aury/boot/commands/docs.py +284 -0
- aury/boot/commands/generate.py +1277 -0
- aury/boot/commands/init.py +892 -0
- aury/boot/commands/migrate/__init__.py +37 -0
- aury/boot/commands/migrate/app.py +54 -0
- aury/boot/commands/migrate/commands.py +303 -0
- aury/boot/commands/scheduler.py +124 -0
- aury/boot/commands/server/__init__.py +21 -0
- aury/boot/commands/server/app.py +541 -0
- aury/boot/commands/templates/generate/api.py.tpl +105 -0
- aury/boot/commands/templates/generate/model.py.tpl +17 -0
- aury/boot/commands/templates/generate/repository.py.tpl +19 -0
- aury/boot/commands/templates/generate/schema.py.tpl +29 -0
- aury/boot/commands/templates/generate/service.py.tpl +48 -0
- aury/boot/commands/templates/project/CLI.md.tpl +92 -0
- aury/boot/commands/templates/project/DEVELOPMENT.md.tpl +1397 -0
- aury/boot/commands/templates/project/README.md.tpl +111 -0
- aury/boot/commands/templates/project/admin_console_init.py.tpl +50 -0
- aury/boot/commands/templates/project/config.py.tpl +30 -0
- aury/boot/commands/templates/project/conftest.py.tpl +26 -0
- aury/boot/commands/templates/project/env.example.tpl +213 -0
- aury/boot/commands/templates/project/gitignore.tpl +128 -0
- aury/boot/commands/templates/project/main.py.tpl +41 -0
- aury/boot/commands/templates/project/modules/api.py.tpl +19 -0
- aury/boot/commands/templates/project/modules/exceptions.py.tpl +84 -0
- aury/boot/commands/templates/project/modules/schedules.py.tpl +18 -0
- aury/boot/commands/templates/project/modules/tasks.py.tpl +20 -0
- aury/boot/commands/worker.py +143 -0
- aury/boot/common/__init__.py +35 -0
- aury/boot/common/exceptions/__init__.py +114 -0
- aury/boot/common/i18n/__init__.py +16 -0
- aury/boot/common/i18n/translator.py +272 -0
- aury/boot/common/logging/__init__.py +716 -0
- aury/boot/contrib/__init__.py +10 -0
- aury/boot/contrib/admin_console/__init__.py +18 -0
- aury/boot/contrib/admin_console/auth.py +137 -0
- aury/boot/contrib/admin_console/discovery.py +69 -0
- aury/boot/contrib/admin_console/install.py +172 -0
- aury/boot/contrib/admin_console/utils.py +44 -0
- aury/boot/domain/__init__.py +79 -0
- aury/boot/domain/exceptions/__init__.py +132 -0
- aury/boot/domain/models/__init__.py +51 -0
- aury/boot/domain/models/base.py +69 -0
- aury/boot/domain/models/mixins.py +135 -0
- aury/boot/domain/models/models.py +96 -0
- aury/boot/domain/pagination/__init__.py +279 -0
- aury/boot/domain/repository/__init__.py +23 -0
- aury/boot/domain/repository/impl.py +423 -0
- aury/boot/domain/repository/interceptors.py +47 -0
- aury/boot/domain/repository/interface.py +106 -0
- aury/boot/domain/repository/query_builder.py +348 -0
- aury/boot/domain/service/__init__.py +11 -0
- aury/boot/domain/service/base.py +73 -0
- aury/boot/domain/transaction/__init__.py +404 -0
- aury/boot/infrastructure/__init__.py +104 -0
- aury/boot/infrastructure/cache/__init__.py +31 -0
- aury/boot/infrastructure/cache/backends.py +348 -0
- aury/boot/infrastructure/cache/base.py +68 -0
- aury/boot/infrastructure/cache/exceptions.py +37 -0
- aury/boot/infrastructure/cache/factory.py +94 -0
- aury/boot/infrastructure/cache/manager.py +274 -0
- aury/boot/infrastructure/database/__init__.py +39 -0
- aury/boot/infrastructure/database/config.py +71 -0
- aury/boot/infrastructure/database/exceptions.py +44 -0
- aury/boot/infrastructure/database/manager.py +317 -0
- aury/boot/infrastructure/database/query_tools/__init__.py +164 -0
- aury/boot/infrastructure/database/strategies/__init__.py +198 -0
- aury/boot/infrastructure/di/__init__.py +15 -0
- aury/boot/infrastructure/di/container.py +393 -0
- aury/boot/infrastructure/events/__init__.py +33 -0
- aury/boot/infrastructure/events/bus.py +362 -0
- aury/boot/infrastructure/events/config.py +52 -0
- aury/boot/infrastructure/events/consumer.py +134 -0
- aury/boot/infrastructure/events/middleware.py +51 -0
- aury/boot/infrastructure/events/models.py +63 -0
- aury/boot/infrastructure/monitoring/__init__.py +529 -0
- aury/boot/infrastructure/scheduler/__init__.py +19 -0
- aury/boot/infrastructure/scheduler/exceptions.py +37 -0
- aury/boot/infrastructure/scheduler/manager.py +478 -0
- aury/boot/infrastructure/storage/__init__.py +38 -0
- aury/boot/infrastructure/storage/base.py +164 -0
- aury/boot/infrastructure/storage/exceptions.py +37 -0
- aury/boot/infrastructure/storage/factory.py +88 -0
- aury/boot/infrastructure/tasks/__init__.py +24 -0
- aury/boot/infrastructure/tasks/config.py +45 -0
- aury/boot/infrastructure/tasks/constants.py +37 -0
- aury/boot/infrastructure/tasks/exceptions.py +37 -0
- aury/boot/infrastructure/tasks/manager.py +490 -0
- aury/boot/testing/__init__.py +24 -0
- aury/boot/testing/base.py +122 -0
- aury/boot/testing/client.py +163 -0
- aury/boot/testing/factory.py +154 -0
- aury/boot/toolkit/__init__.py +21 -0
- aury/boot/toolkit/http/__init__.py +367 -0
- {aury_boot-0.0.2.dist-info → aury_boot-0.0.4.dist-info}/METADATA +3 -2
- aury_boot-0.0.4.dist-info/RECORD +137 -0
- aury_boot-0.0.2.dist-info/RECORD +0 -5
- {aury_boot-0.0.2.dist-info → aury_boot-0.0.4.dist-info}/WHEEL +0 -0
- {aury_boot-0.0.2.dist-info → aury_boot-0.0.4.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""统一的迁移配置初始化模块。
|
|
2
|
+
|
|
3
|
+
这个模块提供单一的数据源来创建迁移配置,被 init 命令和 MigrationManager 共同使用。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def ensure_migration_setup(
|
|
10
|
+
base_path: Path,
|
|
11
|
+
config_path: str,
|
|
12
|
+
script_location: str,
|
|
13
|
+
model_modules: list[str],
|
|
14
|
+
) -> None:
|
|
15
|
+
"""确保迁移配置和目录存在,不存在则自动创建。
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
base_path: 项目根目录
|
|
19
|
+
config_path: alembic.ini 路径
|
|
20
|
+
script_location: 迁移脚本目录名
|
|
21
|
+
model_modules: 模型模块列表
|
|
22
|
+
"""
|
|
23
|
+
script_dir = base_path / script_location
|
|
24
|
+
config_file = base_path / config_path
|
|
25
|
+
|
|
26
|
+
# 创建迁移脚本目录
|
|
27
|
+
if not script_dir.exists():
|
|
28
|
+
script_dir.mkdir(parents=True, exist_ok=True)
|
|
29
|
+
|
|
30
|
+
# 创建 versions 目录
|
|
31
|
+
versions_dir = script_dir / "versions"
|
|
32
|
+
versions_dir.mkdir(exist_ok=True)
|
|
33
|
+
|
|
34
|
+
# 创建 env.py
|
|
35
|
+
env_py = script_dir / "env.py"
|
|
36
|
+
if not env_py.exists():
|
|
37
|
+
env_content = _get_env_py_template(model_modules)
|
|
38
|
+
env_py.write_text(env_content, encoding="utf-8")
|
|
39
|
+
|
|
40
|
+
# 创建 script.py.mako
|
|
41
|
+
mako_file = script_dir / "script.py.mako"
|
|
42
|
+
if not mako_file.exists():
|
|
43
|
+
mako_content = _get_script_mako_template()
|
|
44
|
+
mako_file.write_text(mako_content, encoding="utf-8")
|
|
45
|
+
|
|
46
|
+
# 创建 alembic.ini
|
|
47
|
+
if not config_file.exists():
|
|
48
|
+
ini_content = _get_alembic_ini_template(script_location)
|
|
49
|
+
config_file.write_text(ini_content, encoding="utf-8")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _get_env_py_template(model_modules: list[str]) -> str:
|
|
53
|
+
"""获取 env.py 模板(异步版本)。"""
|
|
54
|
+
model_modules_str = repr(model_modules)
|
|
55
|
+
|
|
56
|
+
return f'''"""Alembic 环境配置(异步)。
|
|
57
|
+
|
|
58
|
+
由 Aury Boot 自动生成,并改造为全异步模式,
|
|
59
|
+
适配 sqlite+aiosqlite / postgresql+asyncpg / mysql+asyncmy 等异步驱动。
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
from logging.config import fileConfig
|
|
63
|
+
from pathlib import Path
|
|
64
|
+
import os
|
|
65
|
+
import sys
|
|
66
|
+
|
|
67
|
+
from alembic import context
|
|
68
|
+
from sqlalchemy import pool
|
|
69
|
+
from sqlalchemy.ext.asyncio import async_engine_from_config
|
|
70
|
+
|
|
71
|
+
# 导入模型基类
|
|
72
|
+
from aury.boot.domain.models import Base
|
|
73
|
+
|
|
74
|
+
# Alembic Config 对象
|
|
75
|
+
config = context.config
|
|
76
|
+
|
|
77
|
+
# 解析日志配置
|
|
78
|
+
if config.config_file_name is not None:
|
|
79
|
+
fileConfig(config.config_file_name)
|
|
80
|
+
|
|
81
|
+
# === 模型加载(基于项目包名自动发现) ===
|
|
82
|
+
# 确保项目根目录在 sys.path 中
|
|
83
|
+
project_root = Path(__file__).resolve().parents[1]
|
|
84
|
+
if str(project_root) not in sys.path:
|
|
85
|
+
sys.path.insert(0, str(project_root))
|
|
86
|
+
|
|
87
|
+
from aury.boot.application.migrations import load_all_models
|
|
88
|
+
try:
|
|
89
|
+
from aury.boot.commands.config import get_project_config
|
|
90
|
+
_cfg = get_project_config()
|
|
91
|
+
if _cfg.has_package:
|
|
92
|
+
_model_modules = [f"{{_cfg.package}}.models", f"{{_cfg.package}}.**.models"]
|
|
93
|
+
else:
|
|
94
|
+
_model_modules = ["models"]
|
|
95
|
+
except Exception:
|
|
96
|
+
_model_modules = {model_modules_str}
|
|
97
|
+
|
|
98
|
+
# 加载模型,确保 Base.metadata 完整
|
|
99
|
+
load_all_models(_model_modules)
|
|
100
|
+
|
|
101
|
+
# 目标元数据
|
|
102
|
+
target_metadata = Base.metadata
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def get_url() -> str:
|
|
106
|
+
"""获取数据库 URL(优先环境变量,其次 alembic.ini)。"""
|
|
107
|
+
return os.environ.get("DATABASE_URL") or config.get_main_option("sqlalchemy.url", "")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def run_migrations_offline() -> None:
|
|
111
|
+
"""离线模式运行迁移(不建立实际连接)。"""
|
|
112
|
+
url = get_url()
|
|
113
|
+
context.configure(
|
|
114
|
+
url=url,
|
|
115
|
+
target_metadata=target_metadata,
|
|
116
|
+
literal_binds=True,
|
|
117
|
+
dialect_opts={{"paramstyle": "named"}},
|
|
118
|
+
compare_type=True,
|
|
119
|
+
compare_server_default=True,
|
|
120
|
+
)
|
|
121
|
+
with context.begin_transaction():
|
|
122
|
+
context.run_migrations()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _do_run_migrations(connection) -> None:
|
|
126
|
+
"""在同步上下文里执行迁移(由 AsyncConnection.run_sync 调用)。"""
|
|
127
|
+
context.configure(
|
|
128
|
+
connection=connection,
|
|
129
|
+
target_metadata=target_metadata,
|
|
130
|
+
compare_type=True,
|
|
131
|
+
compare_server_default=True,
|
|
132
|
+
)
|
|
133
|
+
with context.begin_transaction():
|
|
134
|
+
context.run_migrations()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
async def _run_async_migrations() -> None:
|
|
138
|
+
configuration = config.get_section(config.config_ini_section, {{}})
|
|
139
|
+
configuration["sqlalchemy.url"] = get_url()
|
|
140
|
+
|
|
141
|
+
connectable = async_engine_from_config(
|
|
142
|
+
configuration,
|
|
143
|
+
prefix="sqlalchemy.",
|
|
144
|
+
poolclass=pool.NullPool,
|
|
145
|
+
)
|
|
146
|
+
async with connectable.connect() as connection:
|
|
147
|
+
await connection.run_sync(_do_run_migrations)
|
|
148
|
+
await connectable.dispose()
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def run_migrations_online() -> None:
|
|
152
|
+
import asyncio
|
|
153
|
+
asyncio.run(_run_async_migrations())
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
if context.is_offline_mode():
|
|
157
|
+
run_migrations_offline()
|
|
158
|
+
else:
|
|
159
|
+
run_migrations_online()
|
|
160
|
+
'''
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _get_script_mako_template() -> str:
|
|
164
|
+
"""获取 script.py.mako 模板。"""
|
|
165
|
+
return '''"""${message}
|
|
166
|
+
|
|
167
|
+
Revision ID: ${up_revision}
|
|
168
|
+
Revises: ${down_revision | comma,n}
|
|
169
|
+
Create Date: ${create_date}
|
|
170
|
+
"""
|
|
171
|
+
from typing import Sequence, Union
|
|
172
|
+
|
|
173
|
+
from alembic import op
|
|
174
|
+
import sqlalchemy as sa
|
|
175
|
+
${imports if imports else ""}
|
|
176
|
+
|
|
177
|
+
revision: str = ${repr(up_revision)}
|
|
178
|
+
down_revision: Union[str, None] = ${repr(down_revision)}
|
|
179
|
+
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
|
180
|
+
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def upgrade() -> None:
|
|
184
|
+
${upgrades if upgrades else "pass"}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def downgrade() -> None:
|
|
188
|
+
${downgrades if downgrades else "pass"}
|
|
189
|
+
'''
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _get_alembic_ini_template(script_location: str) -> str:
|
|
193
|
+
"""获取 alembic.ini 模板。"""
|
|
194
|
+
return f'''[alembic]
|
|
195
|
+
script_location = {script_location}
|
|
196
|
+
file_template = %%(year)d%%(month).2d%%(day).2d_%%(hour).2d%%(minute).2d_%%(rev)s_%%(slug)s
|
|
197
|
+
timezone = UTC
|
|
198
|
+
truncate_slug_length = 40
|
|
199
|
+
version_path_separator = os
|
|
200
|
+
|
|
201
|
+
[loggers]
|
|
202
|
+
keys = root,sqlalchemy,alembic
|
|
203
|
+
|
|
204
|
+
[handlers]
|
|
205
|
+
keys = console
|
|
206
|
+
|
|
207
|
+
[formatters]
|
|
208
|
+
keys = generic
|
|
209
|
+
|
|
210
|
+
[logger_root]
|
|
211
|
+
level = WARN
|
|
212
|
+
handlers = console
|
|
213
|
+
qualname =
|
|
214
|
+
|
|
215
|
+
[logger_sqlalchemy]
|
|
216
|
+
level = WARN
|
|
217
|
+
handlers =
|
|
218
|
+
qualname = sqlalchemy.engine
|
|
219
|
+
|
|
220
|
+
[logger_alembic]
|
|
221
|
+
level = INFO
|
|
222
|
+
handlers =
|
|
223
|
+
qualname = alembic
|
|
224
|
+
|
|
225
|
+
[handler_console]
|
|
226
|
+
class = StreamHandler
|
|
227
|
+
args = (sys.stderr,)
|
|
228
|
+
level = NOTSET
|
|
229
|
+
formatter = generic
|
|
230
|
+
|
|
231
|
+
[formatter_generic]
|
|
232
|
+
format = %(levelname)-5.5s [%(name)s] %(message)s
|
|
233
|
+
datefmt = %H:%M:%S
|
|
234
|
+
'''
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
__all__ = ["ensure_migration_setup"]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""RPC调用框架。
|
|
2
|
+
|
|
3
|
+
提供统一的微服务RPC调用接口。
|
|
4
|
+
|
|
5
|
+
基于 toolkit/http 的 HttpClient,提供 RPC 调用封装。
|
|
6
|
+
支持多种服务发现方式(通过 BaseConfig.rpc_client 配置):
|
|
7
|
+
- 配置文件(BaseConfig.rpc_client.services,优先级最高)
|
|
8
|
+
- DNS 解析(统一处理 K8s/Docker Compose,自动使用服务名)
|
|
9
|
+
|
|
10
|
+
注意:负载均衡由基础设施层(K8s Service、Docker Compose)自动处理,
|
|
11
|
+
应用层仅负责服务发现和调用,不实现负载均衡策略。
|
|
12
|
+
|
|
13
|
+
使用示例:
|
|
14
|
+
# 方式1:使用服务发现(推荐)
|
|
15
|
+
from aury.boot.application.config import BaseConfig
|
|
16
|
+
from aury.boot.application.rpc import create_rpc_client
|
|
17
|
+
|
|
18
|
+
config = BaseConfig() # 从环境变量和 .env 文件加载配置
|
|
19
|
+
client = create_rpc_client(service_name="user-service", config=config)
|
|
20
|
+
response = await client.get("/api/v1/users/1")
|
|
21
|
+
|
|
22
|
+
# 方式2:直接指定 URL(不使用服务发现)
|
|
23
|
+
from aury.boot.application.rpc import RPCClient
|
|
24
|
+
|
|
25
|
+
client = RPCClient(base_url="http://user-service:8000")
|
|
26
|
+
response = await client.get("/api/v1/users/1")
|
|
27
|
+
|
|
28
|
+
配置方式(通过环境变量或 .env 文件):
|
|
29
|
+
# 调用配置(RPC_CLIENT_ 前缀)
|
|
30
|
+
RPC_CLIENT_SERVICES={"user-service": "http://user-service:8000"}
|
|
31
|
+
RPC_CLIENT_DNS_SCHEME=http
|
|
32
|
+
RPC_CLIENT_DNS_PORT=80
|
|
33
|
+
|
|
34
|
+
# 注册配置(RPC_SERVICE_ 前缀)
|
|
35
|
+
RPC_SERVICE_NAME=my-service
|
|
36
|
+
RPC_SERVICE_URL=http://my-service:8000
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from .base import BaseRPCClient, RPCError, RPCResponse
|
|
40
|
+
from .client import RPCClient, create_rpc_client
|
|
41
|
+
from .discovery import (
|
|
42
|
+
CompositeServiceDiscovery,
|
|
43
|
+
ConfigServiceDiscovery,
|
|
44
|
+
DNSServiceDiscovery,
|
|
45
|
+
ServiceDiscovery,
|
|
46
|
+
get_service_discovery,
|
|
47
|
+
set_service_discovery,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
__all__ = [
|
|
51
|
+
"BaseRPCClient",
|
|
52
|
+
"CompositeServiceDiscovery",
|
|
53
|
+
"ConfigServiceDiscovery",
|
|
54
|
+
"create_rpc_client",
|
|
55
|
+
"DNSServiceDiscovery",
|
|
56
|
+
"get_service_discovery",
|
|
57
|
+
"RPCClient",
|
|
58
|
+
"RPCError",
|
|
59
|
+
"RPCResponse",
|
|
60
|
+
"ServiceDiscovery",
|
|
61
|
+
"set_service_discovery",
|
|
62
|
+
]
|
|
63
|
+
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""RPC基类和异常定义。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RPCError(Exception):
|
|
11
|
+
"""RPC调用异常。"""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
message: str,
|
|
16
|
+
code: str = "RPC_ERROR",
|
|
17
|
+
status_code: int = 500,
|
|
18
|
+
details: dict[str, Any] | None = None,
|
|
19
|
+
) -> None:
|
|
20
|
+
"""初始化RPC异常。
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
message: 错误消息
|
|
24
|
+
code: 错误代码
|
|
25
|
+
status_code: HTTP状态码
|
|
26
|
+
details: 错误详情
|
|
27
|
+
"""
|
|
28
|
+
super().__init__(message)
|
|
29
|
+
self.message = message
|
|
30
|
+
self.code = code
|
|
31
|
+
self.status_code = status_code
|
|
32
|
+
self.details = details or {}
|
|
33
|
+
|
|
34
|
+
def __str__(self) -> str:
|
|
35
|
+
return f"[{self.code}] {self.message} (status: {self.status_code})"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class RPCResponse(BaseModel):
|
|
39
|
+
"""RPC响应模型。"""
|
|
40
|
+
|
|
41
|
+
success: bool = True
|
|
42
|
+
data: Any | None = None
|
|
43
|
+
message: str = ""
|
|
44
|
+
code: str = "0000"
|
|
45
|
+
status_code: int = 200
|
|
46
|
+
|
|
47
|
+
def raise_for_status(self) -> None:
|
|
48
|
+
"""如果响应失败,抛出异常。"""
|
|
49
|
+
if not self.success:
|
|
50
|
+
raise RPCError(
|
|
51
|
+
message=self.message,
|
|
52
|
+
code=self.code,
|
|
53
|
+
status_code=self.status_code,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class BaseRPCClient:
|
|
58
|
+
"""RPC客户端基类。
|
|
59
|
+
|
|
60
|
+
提供统一的RPC调用接口和错误处理。
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
base_url: str,
|
|
66
|
+
timeout: int = 30,
|
|
67
|
+
retry_times: int = 3,
|
|
68
|
+
headers: dict[str, str] | None = None,
|
|
69
|
+
) -> None:
|
|
70
|
+
"""初始化RPC客户端。
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
base_url: 服务基础URL
|
|
74
|
+
timeout: 超时时间(秒)
|
|
75
|
+
retry_times: 重试次数
|
|
76
|
+
headers: 默认请求头
|
|
77
|
+
"""
|
|
78
|
+
self.base_url = base_url.rstrip("/")
|
|
79
|
+
self.timeout = timeout
|
|
80
|
+
self.retry_times = retry_times
|
|
81
|
+
self.headers = headers or {}
|
|
82
|
+
|
|
83
|
+
def _build_url(self, path: str) -> str:
|
|
84
|
+
"""构建完整URL。
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
path: API路径
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
str: 完整URL
|
|
91
|
+
"""
|
|
92
|
+
path = path.lstrip("/")
|
|
93
|
+
return f"{self.base_url}/{path}"
|
|
94
|
+
|
|
95
|
+
def _prepare_headers(self, extra_headers: dict[str, str] | None = None) -> dict[str, str]:
|
|
96
|
+
"""准备请求头。
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
extra_headers: 额外的请求头
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
dict[str, str]: 合并后的请求头
|
|
103
|
+
"""
|
|
104
|
+
headers = self.headers.copy()
|
|
105
|
+
if extra_headers:
|
|
106
|
+
headers.update(extra_headers)
|
|
107
|
+
return headers
|
|
108
|
+
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"""RPC客户端实现。
|
|
2
|
+
|
|
3
|
+
基于 toolkit/http 的 HttpClient,提供 RPC 调用封装。
|
|
4
|
+
支持链路追踪(Distributed Tracing)。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
|
|
11
|
+
from aury.boot.application.rpc.base import BaseRPCClient, RPCError, RPCResponse
|
|
12
|
+
from aury.boot.application.rpc.discovery import get_service_discovery
|
|
13
|
+
from aury.boot.common.logging import get_trace_id, logger
|
|
14
|
+
from aury.boot.toolkit.http import HttpClient
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from aury.boot.application.config import BaseConfig
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RPCClient(BaseRPCClient):
|
|
21
|
+
"""RPC客户端实现(支持链路追踪)。
|
|
22
|
+
|
|
23
|
+
基于 toolkit/http 的 HttpClient,提供 RPC 调用封装。
|
|
24
|
+
支持自动重试、错误处理和链路追踪(自动传递追踪ID)。
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
base_url: str,
|
|
30
|
+
timeout: int = 30,
|
|
31
|
+
retry_times: int = 3,
|
|
32
|
+
headers: dict[str, str] | None = None,
|
|
33
|
+
) -> None:
|
|
34
|
+
"""初始化RPC客户端。
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
base_url: 服务基础URL
|
|
38
|
+
timeout: 超时时间(秒)
|
|
39
|
+
retry_times: 重试次数
|
|
40
|
+
headers: 默认请求头
|
|
41
|
+
"""
|
|
42
|
+
super().__init__(base_url, timeout, retry_times, headers)
|
|
43
|
+
# 使用 toolkit/http 的 HttpClient
|
|
44
|
+
from aury.boot.toolkit.http import RetryConfig
|
|
45
|
+
|
|
46
|
+
retry_config = RetryConfig(max_retries=retry_times)
|
|
47
|
+
self._http_client = HttpClient(
|
|
48
|
+
base_url=base_url,
|
|
49
|
+
timeout=float(timeout),
|
|
50
|
+
retry_config=retry_config,
|
|
51
|
+
)
|
|
52
|
+
# 添加默认请求头
|
|
53
|
+
if headers:
|
|
54
|
+
# HttpClient 不支持直接设置默认 headers,需要在每次请求时传递
|
|
55
|
+
self._default_headers = headers
|
|
56
|
+
else:
|
|
57
|
+
self._default_headers = {}
|
|
58
|
+
|
|
59
|
+
async def close(self) -> None:
|
|
60
|
+
"""关闭HTTP客户端。"""
|
|
61
|
+
await self._http_client.close()
|
|
62
|
+
|
|
63
|
+
async def _call(
|
|
64
|
+
self,
|
|
65
|
+
method: str,
|
|
66
|
+
path: str,
|
|
67
|
+
data: dict[str, Any] | None = None,
|
|
68
|
+
params: dict[str, Any] | None = None,
|
|
69
|
+
headers: dict[str, str] | None = None,
|
|
70
|
+
) -> RPCResponse:
|
|
71
|
+
"""执行RPC调用。
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
method: HTTP方法(GET, POST, PUT, DELETE)
|
|
75
|
+
path: API路径
|
|
76
|
+
data: 请求体数据
|
|
77
|
+
params: URL参数
|
|
78
|
+
headers: 请求头
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
RPCResponse: RPC响应
|
|
82
|
+
|
|
83
|
+
Raises:
|
|
84
|
+
RPCError: RPC调用失败
|
|
85
|
+
"""
|
|
86
|
+
# 合并请求头
|
|
87
|
+
request_headers = self._prepare_headers(headers)
|
|
88
|
+
|
|
89
|
+
# 添加链路追踪 ID
|
|
90
|
+
trace_id = get_trace_id()
|
|
91
|
+
request_headers["x-trace-id"] = trace_id
|
|
92
|
+
request_headers["x-request-id"] = trace_id
|
|
93
|
+
|
|
94
|
+
logger.debug(
|
|
95
|
+
f"RPC调用开始: {method} {path} | "
|
|
96
|
+
f"Trace-ID: {trace_id}"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
# 使用 toolkit/http 的 HttpClient
|
|
101
|
+
response = await self._http_client.request(
|
|
102
|
+
method=method,
|
|
103
|
+
url=path,
|
|
104
|
+
json=data,
|
|
105
|
+
params=params,
|
|
106
|
+
headers=request_headers,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# 解析响应
|
|
110
|
+
result = response.json()
|
|
111
|
+
rpc_response = RPCResponse(
|
|
112
|
+
success=result.get("success", True),
|
|
113
|
+
data=result.get("data"),
|
|
114
|
+
message=result.get("message", ""),
|
|
115
|
+
code=result.get("code", "0000"),
|
|
116
|
+
status_code=response.status_code,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
rpc_response.raise_for_status()
|
|
120
|
+
|
|
121
|
+
logger.debug(
|
|
122
|
+
f"RPC调用成功: {method} {path} | "
|
|
123
|
+
f"状态: {response.status_code} | "
|
|
124
|
+
f"Trace-ID: {trace_id}"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
return rpc_response
|
|
128
|
+
|
|
129
|
+
except Exception as e:
|
|
130
|
+
# HttpClient 已经处理了 HTTP 错误,这里只需要转换为 RPCError
|
|
131
|
+
status_code = getattr(e, "response", None)
|
|
132
|
+
if status_code and hasattr(status_code, "status_code"):
|
|
133
|
+
status_code = status_code.status_code
|
|
134
|
+
else:
|
|
135
|
+
status_code = 500
|
|
136
|
+
|
|
137
|
+
logger.error(
|
|
138
|
+
f"RPC调用失败: {method} {path} | "
|
|
139
|
+
f"错误: {e!s} | "
|
|
140
|
+
f"Trace-ID: {trace_id}"
|
|
141
|
+
)
|
|
142
|
+
raise RPCError(
|
|
143
|
+
message=f"RPC调用失败: {e!s}",
|
|
144
|
+
code="RPC_ERROR",
|
|
145
|
+
status_code=status_code,
|
|
146
|
+
) from e
|
|
147
|
+
|
|
148
|
+
async def get(
|
|
149
|
+
self,
|
|
150
|
+
path: str,
|
|
151
|
+
params: dict[str, Any] | None = None,
|
|
152
|
+
headers: dict[str, str] | None = None,
|
|
153
|
+
) -> RPCResponse:
|
|
154
|
+
"""GET请求。
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
path: API路径
|
|
158
|
+
params: URL参数
|
|
159
|
+
headers: 请求头
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
RPCResponse: RPC响应
|
|
163
|
+
"""
|
|
164
|
+
return await self._call("GET", path, params=params, headers=headers)
|
|
165
|
+
|
|
166
|
+
async def post(
|
|
167
|
+
self,
|
|
168
|
+
path: str,
|
|
169
|
+
data: dict[str, Any] | None = None,
|
|
170
|
+
headers: dict[str, str] | None = None,
|
|
171
|
+
) -> RPCResponse:
|
|
172
|
+
"""POST请求。
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
path: API路径
|
|
176
|
+
data: 请求体数据
|
|
177
|
+
headers: 请求头
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
RPCResponse: RPC响应
|
|
181
|
+
"""
|
|
182
|
+
return await self._call("POST", path, data=data, headers=headers)
|
|
183
|
+
|
|
184
|
+
async def put(
|
|
185
|
+
self,
|
|
186
|
+
path: str,
|
|
187
|
+
data: dict[str, Any] | None = None,
|
|
188
|
+
headers: dict[str, str] | None = None,
|
|
189
|
+
) -> RPCResponse:
|
|
190
|
+
"""PUT请求。
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
path: API路径
|
|
194
|
+
data: 请求体数据
|
|
195
|
+
headers: 请求头
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
RPCResponse: RPC响应
|
|
199
|
+
"""
|
|
200
|
+
return await self._call("PUT", path, data=data, headers=headers)
|
|
201
|
+
|
|
202
|
+
async def delete(
|
|
203
|
+
self,
|
|
204
|
+
path: str,
|
|
205
|
+
headers: dict[str, str] | None = None,
|
|
206
|
+
) -> RPCResponse:
|
|
207
|
+
"""DELETE请求。
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
path: API路径
|
|
211
|
+
headers: 请求头
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
RPCResponse: RPC响应
|
|
215
|
+
"""
|
|
216
|
+
return await self._call("DELETE", path, headers=headers)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def create_rpc_client(
|
|
220
|
+
service_name: str | None = None,
|
|
221
|
+
base_url: str | None = None,
|
|
222
|
+
timeout: int | None = None,
|
|
223
|
+
retry_times: int | None = None,
|
|
224
|
+
headers: dict[str, str] | None = None,
|
|
225
|
+
config: "BaseConfig | None" = None,
|
|
226
|
+
) -> RPCClient:
|
|
227
|
+
"""创建 RPC 客户端(支持服务发现)。
|
|
228
|
+
|
|
229
|
+
优先使用服务发现解析服务地址,如果未提供 service_name 或 base_url,则使用 base_url。
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
service_name: 服务名称(用于服务发现)
|
|
233
|
+
base_url: 服务基础URL(如果提供,直接使用,不进行服务发现)
|
|
234
|
+
timeout: 超时时间(秒),如果为 None 则使用配置中的默认值
|
|
235
|
+
retry_times: 重试次数,如果为 None 则使用配置中的默认值
|
|
236
|
+
headers: 默认请求头
|
|
237
|
+
config: 应用配置(可选),用于服务发现和获取默认配置
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
RPCClient: RPC 客户端实例
|
|
241
|
+
|
|
242
|
+
Raises:
|
|
243
|
+
ValueError: 如果既未提供 service_name 也未提供 base_url
|
|
244
|
+
|
|
245
|
+
示例:
|
|
246
|
+
# 使用服务发现(自动从配置/DNS 解析)
|
|
247
|
+
from aury.boot.application.config import BaseConfig
|
|
248
|
+
|
|
249
|
+
config = BaseConfig()
|
|
250
|
+
client = create_rpc_client(service_name="user-service", config=config)
|
|
251
|
+
response = await client.get("/api/v1/users/1")
|
|
252
|
+
|
|
253
|
+
# 直接指定 URL(不使用服务发现)
|
|
254
|
+
client = create_rpc_client(base_url="http://user-service:8000")
|
|
255
|
+
response = await client.get("/api/v1/users/1")
|
|
256
|
+
"""
|
|
257
|
+
# 从配置中获取默认值
|
|
258
|
+
if config:
|
|
259
|
+
rpc_client_settings = config.rpc_client
|
|
260
|
+
default_timeout = timeout if timeout is not None else rpc_client_settings.default_timeout
|
|
261
|
+
default_retry_times = retry_times if retry_times is not None else rpc_client_settings.default_retry_times
|
|
262
|
+
else:
|
|
263
|
+
default_timeout = timeout if timeout is not None else 30
|
|
264
|
+
default_retry_times = retry_times if retry_times is not None else 3
|
|
265
|
+
|
|
266
|
+
if base_url:
|
|
267
|
+
# 直接使用提供的 URL
|
|
268
|
+
return RPCClient(
|
|
269
|
+
base_url=base_url,
|
|
270
|
+
timeout=default_timeout,
|
|
271
|
+
retry_times=default_retry_times,
|
|
272
|
+
headers=headers,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
if service_name:
|
|
276
|
+
# 使用服务发现解析
|
|
277
|
+
discovery = get_service_discovery(config)
|
|
278
|
+
resolved_url = discovery.resolve(service_name)
|
|
279
|
+
|
|
280
|
+
if not resolved_url:
|
|
281
|
+
raise ValueError(
|
|
282
|
+
f"无法解析服务地址: {service_name}。"
|
|
283
|
+
"请检查配置(BaseConfig.rpc_client.services)或确保 DNS 服务发现已启用。"
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
return RPCClient(
|
|
287
|
+
base_url=resolved_url,
|
|
288
|
+
timeout=default_timeout,
|
|
289
|
+
retry_times=default_retry_times,
|
|
290
|
+
headers=headers,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
raise ValueError("必须提供 service_name 或 base_url")
|
|
294
|
+
|