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.
Files changed (138) hide show
  1. aury/boot/__init__.py +66 -0
  2. aury/boot/_version.py +2 -2
  3. aury/boot/application/__init__.py +120 -0
  4. aury/boot/application/app/__init__.py +39 -0
  5. aury/boot/application/app/base.py +511 -0
  6. aury/boot/application/app/components.py +434 -0
  7. aury/boot/application/app/middlewares.py +101 -0
  8. aury/boot/application/config/__init__.py +44 -0
  9. aury/boot/application/config/settings.py +663 -0
  10. aury/boot/application/constants/__init__.py +19 -0
  11. aury/boot/application/constants/components.py +50 -0
  12. aury/boot/application/constants/scheduler.py +28 -0
  13. aury/boot/application/constants/service.py +29 -0
  14. aury/boot/application/errors/__init__.py +55 -0
  15. aury/boot/application/errors/chain.py +80 -0
  16. aury/boot/application/errors/codes.py +67 -0
  17. aury/boot/application/errors/exceptions.py +238 -0
  18. aury/boot/application/errors/handlers.py +320 -0
  19. aury/boot/application/errors/response.py +120 -0
  20. aury/boot/application/interfaces/__init__.py +76 -0
  21. aury/boot/application/interfaces/egress.py +224 -0
  22. aury/boot/application/interfaces/ingress.py +98 -0
  23. aury/boot/application/middleware/__init__.py +22 -0
  24. aury/boot/application/middleware/logging.py +451 -0
  25. aury/boot/application/migrations/__init__.py +13 -0
  26. aury/boot/application/migrations/manager.py +685 -0
  27. aury/boot/application/migrations/setup.py +237 -0
  28. aury/boot/application/rpc/__init__.py +63 -0
  29. aury/boot/application/rpc/base.py +108 -0
  30. aury/boot/application/rpc/client.py +294 -0
  31. aury/boot/application/rpc/discovery.py +218 -0
  32. aury/boot/application/scheduler/__init__.py +13 -0
  33. aury/boot/application/scheduler/runner.py +123 -0
  34. aury/boot/application/server/__init__.py +296 -0
  35. aury/boot/commands/__init__.py +30 -0
  36. aury/boot/commands/add.py +76 -0
  37. aury/boot/commands/app.py +105 -0
  38. aury/boot/commands/config.py +177 -0
  39. aury/boot/commands/docker.py +367 -0
  40. aury/boot/commands/docs.py +284 -0
  41. aury/boot/commands/generate.py +1277 -0
  42. aury/boot/commands/init.py +892 -0
  43. aury/boot/commands/migrate/__init__.py +37 -0
  44. aury/boot/commands/migrate/app.py +54 -0
  45. aury/boot/commands/migrate/commands.py +303 -0
  46. aury/boot/commands/scheduler.py +124 -0
  47. aury/boot/commands/server/__init__.py +21 -0
  48. aury/boot/commands/server/app.py +541 -0
  49. aury/boot/commands/templates/generate/api.py.tpl +105 -0
  50. aury/boot/commands/templates/generate/model.py.tpl +17 -0
  51. aury/boot/commands/templates/generate/repository.py.tpl +19 -0
  52. aury/boot/commands/templates/generate/schema.py.tpl +29 -0
  53. aury/boot/commands/templates/generate/service.py.tpl +48 -0
  54. aury/boot/commands/templates/project/CLI.md.tpl +92 -0
  55. aury/boot/commands/templates/project/DEVELOPMENT.md.tpl +1397 -0
  56. aury/boot/commands/templates/project/README.md.tpl +111 -0
  57. aury/boot/commands/templates/project/admin_console_init.py.tpl +50 -0
  58. aury/boot/commands/templates/project/config.py.tpl +30 -0
  59. aury/boot/commands/templates/project/conftest.py.tpl +26 -0
  60. aury/boot/commands/templates/project/env.example.tpl +213 -0
  61. aury/boot/commands/templates/project/gitignore.tpl +128 -0
  62. aury/boot/commands/templates/project/main.py.tpl +41 -0
  63. aury/boot/commands/templates/project/modules/api.py.tpl +19 -0
  64. aury/boot/commands/templates/project/modules/exceptions.py.tpl +84 -0
  65. aury/boot/commands/templates/project/modules/schedules.py.tpl +18 -0
  66. aury/boot/commands/templates/project/modules/tasks.py.tpl +20 -0
  67. aury/boot/commands/worker.py +143 -0
  68. aury/boot/common/__init__.py +35 -0
  69. aury/boot/common/exceptions/__init__.py +114 -0
  70. aury/boot/common/i18n/__init__.py +16 -0
  71. aury/boot/common/i18n/translator.py +272 -0
  72. aury/boot/common/logging/__init__.py +716 -0
  73. aury/boot/contrib/__init__.py +10 -0
  74. aury/boot/contrib/admin_console/__init__.py +18 -0
  75. aury/boot/contrib/admin_console/auth.py +137 -0
  76. aury/boot/contrib/admin_console/discovery.py +69 -0
  77. aury/boot/contrib/admin_console/install.py +172 -0
  78. aury/boot/contrib/admin_console/utils.py +44 -0
  79. aury/boot/domain/__init__.py +79 -0
  80. aury/boot/domain/exceptions/__init__.py +132 -0
  81. aury/boot/domain/models/__init__.py +51 -0
  82. aury/boot/domain/models/base.py +69 -0
  83. aury/boot/domain/models/mixins.py +135 -0
  84. aury/boot/domain/models/models.py +96 -0
  85. aury/boot/domain/pagination/__init__.py +279 -0
  86. aury/boot/domain/repository/__init__.py +23 -0
  87. aury/boot/domain/repository/impl.py +423 -0
  88. aury/boot/domain/repository/interceptors.py +47 -0
  89. aury/boot/domain/repository/interface.py +106 -0
  90. aury/boot/domain/repository/query_builder.py +348 -0
  91. aury/boot/domain/service/__init__.py +11 -0
  92. aury/boot/domain/service/base.py +73 -0
  93. aury/boot/domain/transaction/__init__.py +404 -0
  94. aury/boot/infrastructure/__init__.py +104 -0
  95. aury/boot/infrastructure/cache/__init__.py +31 -0
  96. aury/boot/infrastructure/cache/backends.py +348 -0
  97. aury/boot/infrastructure/cache/base.py +68 -0
  98. aury/boot/infrastructure/cache/exceptions.py +37 -0
  99. aury/boot/infrastructure/cache/factory.py +94 -0
  100. aury/boot/infrastructure/cache/manager.py +274 -0
  101. aury/boot/infrastructure/database/__init__.py +39 -0
  102. aury/boot/infrastructure/database/config.py +71 -0
  103. aury/boot/infrastructure/database/exceptions.py +44 -0
  104. aury/boot/infrastructure/database/manager.py +317 -0
  105. aury/boot/infrastructure/database/query_tools/__init__.py +164 -0
  106. aury/boot/infrastructure/database/strategies/__init__.py +198 -0
  107. aury/boot/infrastructure/di/__init__.py +15 -0
  108. aury/boot/infrastructure/di/container.py +393 -0
  109. aury/boot/infrastructure/events/__init__.py +33 -0
  110. aury/boot/infrastructure/events/bus.py +362 -0
  111. aury/boot/infrastructure/events/config.py +52 -0
  112. aury/boot/infrastructure/events/consumer.py +134 -0
  113. aury/boot/infrastructure/events/middleware.py +51 -0
  114. aury/boot/infrastructure/events/models.py +63 -0
  115. aury/boot/infrastructure/monitoring/__init__.py +529 -0
  116. aury/boot/infrastructure/scheduler/__init__.py +19 -0
  117. aury/boot/infrastructure/scheduler/exceptions.py +37 -0
  118. aury/boot/infrastructure/scheduler/manager.py +478 -0
  119. aury/boot/infrastructure/storage/__init__.py +38 -0
  120. aury/boot/infrastructure/storage/base.py +164 -0
  121. aury/boot/infrastructure/storage/exceptions.py +37 -0
  122. aury/boot/infrastructure/storage/factory.py +88 -0
  123. aury/boot/infrastructure/tasks/__init__.py +24 -0
  124. aury/boot/infrastructure/tasks/config.py +45 -0
  125. aury/boot/infrastructure/tasks/constants.py +37 -0
  126. aury/boot/infrastructure/tasks/exceptions.py +37 -0
  127. aury/boot/infrastructure/tasks/manager.py +490 -0
  128. aury/boot/testing/__init__.py +24 -0
  129. aury/boot/testing/base.py +122 -0
  130. aury/boot/testing/client.py +163 -0
  131. aury/boot/testing/factory.py +154 -0
  132. aury/boot/toolkit/__init__.py +21 -0
  133. aury/boot/toolkit/http/__init__.py +367 -0
  134. {aury_boot-0.0.2.dist-info → aury_boot-0.0.4.dist-info}/METADATA +3 -2
  135. aury_boot-0.0.4.dist-info/RECORD +137 -0
  136. aury_boot-0.0.2.dist-info/RECORD +0 -5
  137. {aury_boot-0.0.2.dist-info → aury_boot-0.0.4.dist-info}/WHEEL +0 -0
  138. {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
+