aury-boot 0.0.3__py3-none-any.whl → 0.0.5__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 +2 -2
- aury/boot/_version.py +2 -2
- aury/boot/application/__init__.py +45 -36
- aury/boot/application/app/__init__.py +12 -8
- aury/boot/application/app/base.py +12 -0
- aury/boot/application/app/components.py +137 -44
- aury/boot/application/app/middlewares.py +2 -0
- aury/boot/application/app/startup.py +249 -0
- aury/boot/application/config/__init__.py +36 -1
- aury/boot/application/config/multi_instance.py +200 -0
- aury/boot/application/config/settings.py +341 -12
- aury/boot/application/constants/components.py +6 -0
- aury/boot/application/errors/handlers.py +17 -3
- aury/boot/application/middleware/logging.py +8 -120
- aury/boot/application/rpc/__init__.py +2 -2
- aury/boot/commands/__init__.py +30 -10
- aury/boot/commands/app.py +131 -1
- aury/boot/commands/docs.py +104 -17
- aury/boot/commands/init.py +30 -9
- aury/boot/commands/server/app.py +2 -3
- aury/boot/commands/templates/project/AGENTS.md.tpl +217 -0
- aury/boot/commands/templates/project/README.md.tpl +2 -2
- aury/boot/commands/templates/project/aury_docs/00-overview.md.tpl +59 -0
- aury/boot/commands/templates/project/aury_docs/01-model.md.tpl +183 -0
- aury/boot/commands/templates/project/aury_docs/02-repository.md.tpl +206 -0
- aury/boot/commands/templates/project/aury_docs/03-service.md.tpl +398 -0
- aury/boot/commands/templates/project/aury_docs/04-schema.md.tpl +95 -0
- aury/boot/commands/templates/project/aury_docs/05-api.md.tpl +116 -0
- aury/boot/commands/templates/project/aury_docs/06-exception.md.tpl +118 -0
- aury/boot/commands/templates/project/aury_docs/07-cache.md.tpl +122 -0
- aury/boot/commands/templates/project/aury_docs/08-scheduler.md.tpl +32 -0
- aury/boot/commands/templates/project/aury_docs/09-tasks.md.tpl +38 -0
- aury/boot/commands/templates/project/aury_docs/10-storage.md.tpl +115 -0
- aury/boot/commands/templates/project/aury_docs/11-logging.md.tpl +92 -0
- aury/boot/commands/templates/project/aury_docs/12-admin.md.tpl +56 -0
- aury/boot/commands/templates/project/aury_docs/13-channel.md.tpl +92 -0
- aury/boot/commands/templates/project/aury_docs/14-mq.md.tpl +102 -0
- aury/boot/commands/templates/project/aury_docs/15-events.md.tpl +147 -0
- aury/boot/commands/templates/project/config.py.tpl +1 -1
- aury/boot/commands/templates/project/env.example.tpl +73 -5
- aury/boot/commands/templates/project/modules/tasks.py.tpl +1 -1
- aury/boot/contrib/admin_console/auth.py +2 -3
- aury/boot/contrib/admin_console/install.py +1 -1
- aury/boot/domain/models/mixins.py +48 -1
- aury/boot/domain/pagination/__init__.py +94 -0
- aury/boot/domain/repository/impl.py +1 -1
- aury/boot/domain/repository/interface.py +1 -1
- aury/boot/domain/transaction/__init__.py +8 -9
- aury/boot/infrastructure/__init__.py +86 -29
- aury/boot/infrastructure/cache/backends.py +102 -18
- aury/boot/infrastructure/cache/base.py +12 -0
- aury/boot/infrastructure/cache/manager.py +153 -91
- aury/boot/infrastructure/channel/__init__.py +24 -0
- aury/boot/infrastructure/channel/backends/__init__.py +9 -0
- aury/boot/infrastructure/channel/backends/memory.py +83 -0
- aury/boot/infrastructure/channel/backends/redis.py +88 -0
- aury/boot/infrastructure/channel/base.py +92 -0
- aury/boot/infrastructure/channel/manager.py +203 -0
- aury/boot/infrastructure/clients/__init__.py +22 -0
- aury/boot/infrastructure/clients/rabbitmq/__init__.py +9 -0
- aury/boot/infrastructure/clients/rabbitmq/config.py +46 -0
- aury/boot/infrastructure/clients/rabbitmq/manager.py +288 -0
- aury/boot/infrastructure/clients/redis/__init__.py +28 -0
- aury/boot/infrastructure/clients/redis/config.py +51 -0
- aury/boot/infrastructure/clients/redis/manager.py +264 -0
- aury/boot/infrastructure/database/config.py +1 -2
- aury/boot/infrastructure/database/manager.py +16 -38
- aury/boot/infrastructure/events/__init__.py +18 -21
- aury/boot/infrastructure/events/backends/__init__.py +11 -0
- aury/boot/infrastructure/events/backends/memory.py +86 -0
- aury/boot/infrastructure/events/backends/rabbitmq.py +193 -0
- aury/boot/infrastructure/events/backends/redis.py +162 -0
- aury/boot/infrastructure/events/base.py +127 -0
- aury/boot/infrastructure/events/manager.py +224 -0
- aury/boot/infrastructure/mq/__init__.py +24 -0
- aury/boot/infrastructure/mq/backends/__init__.py +9 -0
- aury/boot/infrastructure/mq/backends/rabbitmq.py +179 -0
- aury/boot/infrastructure/mq/backends/redis.py +167 -0
- aury/boot/infrastructure/mq/base.py +143 -0
- aury/boot/infrastructure/mq/manager.py +239 -0
- aury/boot/infrastructure/scheduler/manager.py +7 -3
- aury/boot/infrastructure/storage/__init__.py +9 -9
- aury/boot/infrastructure/storage/base.py +17 -5
- aury/boot/infrastructure/storage/factory.py +0 -1
- aury/boot/infrastructure/tasks/__init__.py +2 -2
- aury/boot/infrastructure/tasks/manager.py +47 -29
- aury/boot/testing/base.py +2 -2
- {aury_boot-0.0.3.dist-info → aury_boot-0.0.5.dist-info}/METADATA +19 -2
- aury_boot-0.0.5.dist-info/RECORD +176 -0
- aury/boot/commands/templates/project/DEVELOPMENT.md.tpl +0 -1397
- aury/boot/infrastructure/events/bus.py +0 -362
- aury/boot/infrastructure/events/config.py +0 -52
- aury/boot/infrastructure/events/consumer.py +0 -134
- aury/boot/infrastructure/events/models.py +0 -63
- aury_boot-0.0.3.dist-info/RECORD +0 -137
- /aury/boot/commands/templates/project/{CLI.md.tpl → aury_docs/99-cli.md.tpl} +0 -0
- {aury_boot-0.0.3.dist-info → aury_boot-0.0.5.dist-info}/WHEEL +0 -0
- {aury_boot-0.0.3.dist-info → aury_boot-0.0.5.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""启动日志工具。
|
|
2
|
+
|
|
3
|
+
提供应用启动时的组件状态打印功能,包括 URL 脱敏。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
import re
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from aury.boot.common.logging import logger
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class ComponentStatus:
|
|
17
|
+
"""组件状态。"""
|
|
18
|
+
|
|
19
|
+
name: str
|
|
20
|
+
status: str # "ok", "error", "disabled"
|
|
21
|
+
backend: str | None = None
|
|
22
|
+
url: str | None = None
|
|
23
|
+
details: dict[str, Any] = field(default_factory=dict)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def mask_url(url: str | None) -> str:
|
|
27
|
+
"""URL 脱敏(隐藏密码和敏感信息)。
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
url: 原始 URL
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
脱敏后的 URL
|
|
34
|
+
"""
|
|
35
|
+
if not url:
|
|
36
|
+
return "N/A"
|
|
37
|
+
|
|
38
|
+
# 匹配常见 URL 格式中的密码部分
|
|
39
|
+
# redis://:password@host:port/db
|
|
40
|
+
# amqp://user:password@host:port/vhost
|
|
41
|
+
# postgresql://user:password@host:port/db
|
|
42
|
+
patterns = [
|
|
43
|
+
# user:password@ 格式
|
|
44
|
+
(r"(://[^:]+:)([^@]+)(@)", r"\1***\3"),
|
|
45
|
+
# :password@ 格式 (无用户名)
|
|
46
|
+
(r"(://:)([^@]+)(@)", r"\1***\3"),
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
masked = url
|
|
50
|
+
for pattern, replacement in patterns:
|
|
51
|
+
masked = re.sub(pattern, replacement, masked)
|
|
52
|
+
|
|
53
|
+
return masked
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def print_startup_banner(
|
|
57
|
+
app_name: str = "Aury Boot",
|
|
58
|
+
version: str = "",
|
|
59
|
+
components: list[ComponentStatus] | None = None,
|
|
60
|
+
) -> None:
|
|
61
|
+
"""打印启动横幅和组件状态。
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
app_name: 应用名称
|
|
65
|
+
version: 版本号
|
|
66
|
+
components: 组件状态列表
|
|
67
|
+
"""
|
|
68
|
+
# 打印横幅
|
|
69
|
+
banner = f"""
|
|
70
|
+
╔══════════════════════════════════════════════════════════════╗
|
|
71
|
+
║ {app_name:^58} ║
|
|
72
|
+
║ {f'v{version}' if version else '':^58} ║
|
|
73
|
+
╚══════════════════════════════════════════════════════════════╝
|
|
74
|
+
"""
|
|
75
|
+
logger.info(banner)
|
|
76
|
+
|
|
77
|
+
# 打印组件状态
|
|
78
|
+
if components:
|
|
79
|
+
logger.info("组件状态:")
|
|
80
|
+
logger.info("-" * 60)
|
|
81
|
+
|
|
82
|
+
for comp in components:
|
|
83
|
+
status_icon = "✓" if comp.status == "ok" else "✗" if comp.status == "error" else "○"
|
|
84
|
+
status_text = f"[{status_icon}] {comp.name}"
|
|
85
|
+
|
|
86
|
+
if comp.backend:
|
|
87
|
+
status_text += f" ({comp.backend})"
|
|
88
|
+
|
|
89
|
+
if comp.url:
|
|
90
|
+
status_text += f" -> {mask_url(comp.url)}"
|
|
91
|
+
|
|
92
|
+
if comp.status == "error" and comp.details.get("error"):
|
|
93
|
+
status_text += f" | Error: {comp.details['error']}"
|
|
94
|
+
|
|
95
|
+
logger.info(f" {status_text}")
|
|
96
|
+
|
|
97
|
+
logger.info("-" * 60)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def collect_component_status() -> list[ComponentStatus]:
|
|
101
|
+
"""收集所有组件状态。
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
组件状态列表
|
|
105
|
+
"""
|
|
106
|
+
from aury.boot.infrastructure.cache import CacheManager
|
|
107
|
+
from aury.boot.infrastructure.database import DatabaseManager
|
|
108
|
+
from aury.boot.infrastructure.scheduler import SchedulerManager
|
|
109
|
+
from aury.boot.infrastructure.storage import StorageManager
|
|
110
|
+
|
|
111
|
+
# 延迟导入新模块(可能不存在)
|
|
112
|
+
try:
|
|
113
|
+
from aury.boot.infrastructure.clients.redis import RedisClient
|
|
114
|
+
except ImportError:
|
|
115
|
+
RedisClient = None
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
from aury.boot.infrastructure.channel import ChannelManager
|
|
119
|
+
except ImportError:
|
|
120
|
+
ChannelManager = None
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
from aury.boot.infrastructure.mq import MQManager
|
|
124
|
+
except ImportError:
|
|
125
|
+
MQManager = None
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
from aury.boot.infrastructure.events import EventBusManager
|
|
129
|
+
except ImportError:
|
|
130
|
+
EventBusManager = None
|
|
131
|
+
|
|
132
|
+
statuses = []
|
|
133
|
+
|
|
134
|
+
# Database - 收集所有实例
|
|
135
|
+
for name, instance in DatabaseManager._instances.items():
|
|
136
|
+
if instance.is_initialized:
|
|
137
|
+
url = str(instance._engine.url) if instance._engine else None
|
|
138
|
+
statuses.append(
|
|
139
|
+
ComponentStatus(
|
|
140
|
+
name="Database" if name == "default" else f"Database [{name}]",
|
|
141
|
+
status="ok",
|
|
142
|
+
backend="SQLAlchemy",
|
|
143
|
+
url=url,
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Cache - 收集所有实例
|
|
148
|
+
for name, instance in CacheManager._instances.items():
|
|
149
|
+
if instance._backend:
|
|
150
|
+
statuses.append(
|
|
151
|
+
ComponentStatus(
|
|
152
|
+
name="Cache" if name == "default" else f"Cache [{name}]",
|
|
153
|
+
status="ok",
|
|
154
|
+
backend=instance.backend_type,
|
|
155
|
+
url=instance._config.get("CACHE_URL") if instance._config else None,
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Storage - 收集所有实例
|
|
160
|
+
for name, instance in StorageManager._instances.items():
|
|
161
|
+
if instance._backend:
|
|
162
|
+
backend_type = instance._config.backend.value if instance._config else "unknown"
|
|
163
|
+
url = None
|
|
164
|
+
if instance._config:
|
|
165
|
+
if backend_type == "local":
|
|
166
|
+
url = instance._config.base_path
|
|
167
|
+
elif instance._config.endpoint:
|
|
168
|
+
url = instance._config.endpoint
|
|
169
|
+
statuses.append(
|
|
170
|
+
ComponentStatus(
|
|
171
|
+
name="Storage" if name == "default" else f"Storage [{name}]",
|
|
172
|
+
status="ok",
|
|
173
|
+
backend=backend_type,
|
|
174
|
+
url=url,
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Scheduler
|
|
179
|
+
scheduler = SchedulerManager.get_instance()
|
|
180
|
+
if scheduler._initialized:
|
|
181
|
+
statuses.append(
|
|
182
|
+
ComponentStatus(
|
|
183
|
+
name="Scheduler",
|
|
184
|
+
status="ok",
|
|
185
|
+
backend="APScheduler",
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Redis Clients
|
|
190
|
+
if RedisClient:
|
|
191
|
+
for name, instance in RedisClient._instances.items():
|
|
192
|
+
if instance.is_initialized:
|
|
193
|
+
statuses.append(
|
|
194
|
+
ComponentStatus(
|
|
195
|
+
name="Redis" if name == "default" else f"Redis [{name}]",
|
|
196
|
+
status="ok",
|
|
197
|
+
backend="redis",
|
|
198
|
+
url=instance._config.url if instance._config else None,
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# Channel
|
|
203
|
+
if ChannelManager:
|
|
204
|
+
for name, instance in ChannelManager._instances.items():
|
|
205
|
+
if instance.is_initialized:
|
|
206
|
+
statuses.append(
|
|
207
|
+
ComponentStatus(
|
|
208
|
+
name="Channel" if name == "default" else f"Channel [{name}]",
|
|
209
|
+
status="ok",
|
|
210
|
+
backend=instance.backend_type,
|
|
211
|
+
url=getattr(instance._backend, "_url", None) if instance._backend else None,
|
|
212
|
+
)
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# MQ
|
|
216
|
+
if MQManager:
|
|
217
|
+
for name, instance in MQManager._instances.items():
|
|
218
|
+
if instance.is_initialized:
|
|
219
|
+
statuses.append(
|
|
220
|
+
ComponentStatus(
|
|
221
|
+
name="MQ" if name == "default" else f"MQ [{name}]",
|
|
222
|
+
status="ok",
|
|
223
|
+
backend=instance.backend_type,
|
|
224
|
+
url=getattr(instance._backend, "_url", None) if instance._backend else None,
|
|
225
|
+
)
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
# Events
|
|
229
|
+
if EventBusManager:
|
|
230
|
+
for name, instance in EventBusManager._instances.items():
|
|
231
|
+
if instance.is_initialized:
|
|
232
|
+
statuses.append(
|
|
233
|
+
ComponentStatus(
|
|
234
|
+
name="Events" if name == "default" else f"Events [{name}]",
|
|
235
|
+
status="ok",
|
|
236
|
+
backend=instance.backend_type,
|
|
237
|
+
url=getattr(instance._backend, "_url", None) if instance._backend else None,
|
|
238
|
+
)
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
return statuses
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
__all__ = [
|
|
245
|
+
"ComponentStatus",
|
|
246
|
+
"collect_component_status",
|
|
247
|
+
"mask_url",
|
|
248
|
+
"print_startup_banner",
|
|
249
|
+
]
|
|
@@ -6,39 +6,74 @@
|
|
|
6
6
|
设计原则:
|
|
7
7
|
- Application 层配置完全独立,不依赖 Infrastructure 层
|
|
8
8
|
- 配置是纯粹的数据模型定义
|
|
9
|
+
|
|
10
|
+
多实例配置:
|
|
11
|
+
框架支持多种组件的多实例配置,使用统一的环境变量格式:
|
|
12
|
+
{PREFIX}_{INSTANCE}_{FIELD}=value
|
|
13
|
+
|
|
14
|
+
示例:
|
|
15
|
+
DATABASE_DEFAULT_URL=postgresql://main...
|
|
16
|
+
DATABASE_ANALYTICS_URL=postgresql://analytics...
|
|
17
|
+
CACHE_DEFAULT_URL=redis://localhost:6379/1
|
|
18
|
+
MQ_DEFAULT_URL=redis://localhost:6379/2
|
|
9
19
|
"""
|
|
10
20
|
|
|
21
|
+
from .multi_instance import (
|
|
22
|
+
MultiInstanceConfigLoader,
|
|
23
|
+
MultiInstanceSettings,
|
|
24
|
+
parse_multi_instance_env,
|
|
25
|
+
)
|
|
11
26
|
from .settings import (
|
|
12
27
|
BaseConfig,
|
|
28
|
+
CacheInstanceConfig,
|
|
13
29
|
CacheSettings,
|
|
30
|
+
ChannelInstanceConfig,
|
|
14
31
|
CORSSettings,
|
|
32
|
+
DatabaseInstanceConfig,
|
|
15
33
|
DatabaseSettings,
|
|
34
|
+
EventInstanceConfig,
|
|
16
35
|
EventSettings,
|
|
17
36
|
HealthCheckSettings,
|
|
18
37
|
LogSettings,
|
|
38
|
+
MessageQueueSettings,
|
|
19
39
|
MigrationSettings,
|
|
40
|
+
MQInstanceConfig,
|
|
20
41
|
RPCClientSettings,
|
|
21
42
|
RPCServiceSettings,
|
|
22
43
|
SchedulerSettings,
|
|
23
44
|
ServerSettings,
|
|
24
45
|
ServiceSettings,
|
|
46
|
+
StorageInstanceConfig,
|
|
25
47
|
TaskSettings,
|
|
26
48
|
)
|
|
27
49
|
|
|
28
50
|
__all__ = [
|
|
51
|
+
# 配置类
|
|
29
52
|
"BaseConfig",
|
|
30
|
-
"CacheSettings",
|
|
31
53
|
"CORSSettings",
|
|
54
|
+
# 多实例配置类
|
|
55
|
+
"CacheInstanceConfig",
|
|
56
|
+
"CacheSettings",
|
|
57
|
+
"ChannelInstanceConfig",
|
|
58
|
+
"DatabaseInstanceConfig",
|
|
32
59
|
"DatabaseSettings",
|
|
60
|
+
"EventInstanceConfig",
|
|
33
61
|
"EventSettings",
|
|
34
62
|
"HealthCheckSettings",
|
|
35
63
|
"LogSettings",
|
|
64
|
+
"MQInstanceConfig",
|
|
65
|
+
"MessageQueueSettings",
|
|
36
66
|
"MigrationSettings",
|
|
67
|
+
# 多实例配置工具
|
|
68
|
+
"MultiInstanceConfigLoader",
|
|
69
|
+
"MultiInstanceSettings",
|
|
37
70
|
"RPCClientSettings",
|
|
38
71
|
"RPCServiceSettings",
|
|
39
72
|
"SchedulerSettings",
|
|
40
73
|
"ServerSettings",
|
|
41
74
|
"ServiceSettings",
|
|
75
|
+
"StorageInstanceConfig",
|
|
42
76
|
"TaskSettings",
|
|
77
|
+
"parse_multi_instance_env",
|
|
43
78
|
]
|
|
44
79
|
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""多实例配置解析工具。
|
|
2
|
+
|
|
3
|
+
支持从环境变量解析 {PREFIX}_{INSTANCE}_{FIELD} 格式的多实例配置。
|
|
4
|
+
|
|
5
|
+
示例:
|
|
6
|
+
DATABASE_DEFAULT_URL=postgresql://main...
|
|
7
|
+
DATABASE_DEFAULT_POOL_SIZE=10
|
|
8
|
+
DATABASE_ANALYTICS_URL=postgresql://analytics...
|
|
9
|
+
|
|
10
|
+
解析后:
|
|
11
|
+
{
|
|
12
|
+
"default": {"url": "postgresql://main...", "pool_size": 10},
|
|
13
|
+
"analytics": {"url": "postgresql://analytics..."}
|
|
14
|
+
}
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import os
|
|
20
|
+
import re
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from pydantic import BaseModel
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def parse_multi_instance_env(
|
|
27
|
+
prefix: str,
|
|
28
|
+
fields: list[str],
|
|
29
|
+
*,
|
|
30
|
+
type_hints: dict[str, type] | None = None,
|
|
31
|
+
) -> dict[str, dict[str, Any]]:
|
|
32
|
+
"""从环境变量解析多实例配置。
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
prefix: 环境变量前缀,如 "DATABASE"
|
|
36
|
+
fields: 支持的字段列表,如 ["url", "pool_size", "echo"]
|
|
37
|
+
type_hints: 字段类型提示,用于类型转换
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
dict[str, dict[str, Any]]: 实例名 -> 配置字典
|
|
41
|
+
|
|
42
|
+
示例:
|
|
43
|
+
>>> parse_multi_instance_env("DATABASE", ["url", "pool_size"])
|
|
44
|
+
{
|
|
45
|
+
"default": {"url": "postgresql://...", "pool_size": "10"},
|
|
46
|
+
"analytics": {"url": "postgresql://..."}
|
|
47
|
+
}
|
|
48
|
+
"""
|
|
49
|
+
instances: dict[str, dict[str, Any]] = {}
|
|
50
|
+
type_hints = type_hints or {}
|
|
51
|
+
|
|
52
|
+
# 构建正则:{PREFIX}_{INSTANCE}_{FIELD}
|
|
53
|
+
# INSTANCE 和 FIELD 都是大写字母和下划线
|
|
54
|
+
fields_pattern = "|".join(re.escape(f.upper()) for f in fields)
|
|
55
|
+
pattern = re.compile(
|
|
56
|
+
rf"^{prefix}_([A-Z][A-Z0-9_]*)_({fields_pattern})$",
|
|
57
|
+
re.IGNORECASE
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
for key, value in os.environ.items():
|
|
61
|
+
match = pattern.match(key)
|
|
62
|
+
if match:
|
|
63
|
+
instance_name = match.group(1).lower()
|
|
64
|
+
field_name = match.group(2).lower()
|
|
65
|
+
|
|
66
|
+
# 类型转换
|
|
67
|
+
converted_value = _convert_value(value, type_hints.get(field_name))
|
|
68
|
+
|
|
69
|
+
if instance_name not in instances:
|
|
70
|
+
instances[instance_name] = {}
|
|
71
|
+
instances[instance_name][field_name] = converted_value
|
|
72
|
+
|
|
73
|
+
return instances
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _convert_value(value: str, target_type: type | None) -> Any:
|
|
77
|
+
"""转换环境变量值到目标类型。"""
|
|
78
|
+
if target_type is None:
|
|
79
|
+
return value
|
|
80
|
+
|
|
81
|
+
if target_type is bool:
|
|
82
|
+
return value.lower() in ("true", "1", "yes", "on")
|
|
83
|
+
elif target_type is int:
|
|
84
|
+
return int(value)
|
|
85
|
+
elif target_type is float:
|
|
86
|
+
return float(value)
|
|
87
|
+
elif target_type is list:
|
|
88
|
+
# 简单的逗号分隔
|
|
89
|
+
return [v.strip() for v in value.split(",") if v.strip()]
|
|
90
|
+
else:
|
|
91
|
+
return value
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class MultiInstanceSettings(BaseModel):
|
|
95
|
+
"""多实例配置基类。
|
|
96
|
+
|
|
97
|
+
子类需要定义各实例共享的配置字段。
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def get_field_names(cls) -> list[str]:
|
|
102
|
+
"""获取所有字段名。"""
|
|
103
|
+
return list(cls.model_fields.keys())
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def get_type_hints(cls) -> dict[str, type]:
|
|
107
|
+
"""获取字段类型提示。"""
|
|
108
|
+
hints = {}
|
|
109
|
+
for name, field_info in cls.model_fields.items():
|
|
110
|
+
annotation = field_info.annotation
|
|
111
|
+
# 处理 Optional 类型
|
|
112
|
+
if hasattr(annotation, "__origin__"):
|
|
113
|
+
# 如 str | None -> str
|
|
114
|
+
args = getattr(annotation, "__args__", ())
|
|
115
|
+
for arg in args:
|
|
116
|
+
if arg is not type(None):
|
|
117
|
+
hints[name] = arg
|
|
118
|
+
break
|
|
119
|
+
else:
|
|
120
|
+
hints[name] = annotation
|
|
121
|
+
return hints
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class MultiInstanceConfigLoader:
|
|
125
|
+
"""多实例配置加载器。
|
|
126
|
+
|
|
127
|
+
使用示例:
|
|
128
|
+
loader = MultiInstanceConfigLoader("DATABASE", DatabaseInstanceConfig)
|
|
129
|
+
instances = loader.load()
|
|
130
|
+
# {"default": DatabaseInstanceConfig(...), "analytics": DatabaseInstanceConfig(...)}
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
def __init__(
|
|
134
|
+
self,
|
|
135
|
+
prefix: str,
|
|
136
|
+
config_class: type[MultiInstanceSettings],
|
|
137
|
+
):
|
|
138
|
+
"""初始化加载器。
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
prefix: 环境变量前缀
|
|
142
|
+
config_class: 配置类(继承自 MultiInstanceSettings)
|
|
143
|
+
"""
|
|
144
|
+
self.prefix = prefix.upper()
|
|
145
|
+
self.config_class = config_class
|
|
146
|
+
|
|
147
|
+
def load(self) -> dict[str, MultiInstanceSettings]:
|
|
148
|
+
"""加载所有实例配置。
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
dict[str, config_class]: 实例名 -> 配置对象
|
|
152
|
+
"""
|
|
153
|
+
fields = self.config_class.get_field_names()
|
|
154
|
+
type_hints = self.config_class.get_type_hints()
|
|
155
|
+
|
|
156
|
+
raw_instances = parse_multi_instance_env(
|
|
157
|
+
self.prefix,
|
|
158
|
+
fields,
|
|
159
|
+
type_hints=type_hints,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# 转换为配置对象
|
|
163
|
+
instances = {}
|
|
164
|
+
for name, config_dict in raw_instances.items():
|
|
165
|
+
try:
|
|
166
|
+
instances[name] = self.config_class(**config_dict)
|
|
167
|
+
except Exception as e:
|
|
168
|
+
# 配置不完整时跳过,让 Pydantic 验证报错
|
|
169
|
+
raise ValueError(
|
|
170
|
+
f"配置实例 [{self.prefix}_{name.upper()}] 无效: {e}"
|
|
171
|
+
) from e
|
|
172
|
+
|
|
173
|
+
return instances
|
|
174
|
+
|
|
175
|
+
def load_or_default(
|
|
176
|
+
self,
|
|
177
|
+
default_instance: str = "default",
|
|
178
|
+
) -> dict[str, MultiInstanceSettings]:
|
|
179
|
+
"""加载配置,如果没有任何实例则返回包含默认实例的字典。
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
default_instance: 默认实例名
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
dict[str, config_class]: 实例名 -> 配置对象
|
|
186
|
+
"""
|
|
187
|
+
instances = self.load()
|
|
188
|
+
|
|
189
|
+
if not instances:
|
|
190
|
+
# 没有配置任何实例,创建一个默认的
|
|
191
|
+
instances[default_instance] = self.config_class()
|
|
192
|
+
|
|
193
|
+
return instances
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
__all__ = [
|
|
197
|
+
"MultiInstanceConfigLoader",
|
|
198
|
+
"MultiInstanceSettings",
|
|
199
|
+
"parse_multi_instance_env",
|
|
200
|
+
]
|