aury-boot 0.0.4__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.
Files changed (122) hide show
  1. aury/boot/__init__.py +2 -2
  2. aury/boot/_version.py +2 -2
  3. aury/boot/application/__init__.py +60 -36
  4. aury/boot/application/adapter/__init__.py +112 -0
  5. aury/boot/application/adapter/base.py +511 -0
  6. aury/boot/application/adapter/config.py +242 -0
  7. aury/boot/application/adapter/decorators.py +259 -0
  8. aury/boot/application/adapter/exceptions.py +202 -0
  9. aury/boot/application/adapter/http.py +325 -0
  10. aury/boot/application/app/__init__.py +12 -8
  11. aury/boot/application/app/base.py +12 -0
  12. aury/boot/application/app/components.py +137 -44
  13. aury/boot/application/app/middlewares.py +9 -4
  14. aury/boot/application/app/startup.py +249 -0
  15. aury/boot/application/config/__init__.py +36 -1
  16. aury/boot/application/config/multi_instance.py +216 -0
  17. aury/boot/application/config/settings.py +398 -149
  18. aury/boot/application/constants/components.py +6 -0
  19. aury/boot/application/errors/handlers.py +17 -3
  20. aury/boot/application/middleware/logging.py +21 -120
  21. aury/boot/application/rpc/__init__.py +2 -2
  22. aury/boot/commands/__init__.py +30 -10
  23. aury/boot/commands/app.py +131 -1
  24. aury/boot/commands/docs.py +104 -17
  25. aury/boot/commands/generate.py +22 -22
  26. aury/boot/commands/init.py +68 -17
  27. aury/boot/commands/server/app.py +2 -3
  28. aury/boot/commands/templates/project/AGENTS.md.tpl +221 -0
  29. aury/boot/commands/templates/project/README.md.tpl +2 -2
  30. aury/boot/commands/templates/project/aury_docs/00-overview.md.tpl +59 -0
  31. aury/boot/commands/templates/project/aury_docs/01-model.md.tpl +184 -0
  32. aury/boot/commands/templates/project/aury_docs/02-repository.md.tpl +206 -0
  33. aury/boot/commands/templates/project/aury_docs/03-service.md.tpl +398 -0
  34. aury/boot/commands/templates/project/aury_docs/04-schema.md.tpl +95 -0
  35. aury/boot/commands/templates/project/aury_docs/05-api.md.tpl +116 -0
  36. aury/boot/commands/templates/project/aury_docs/06-exception.md.tpl +118 -0
  37. aury/boot/commands/templates/project/aury_docs/07-cache.md.tpl +122 -0
  38. aury/boot/commands/templates/project/aury_docs/08-scheduler.md.tpl +32 -0
  39. aury/boot/commands/templates/project/aury_docs/09-tasks.md.tpl +38 -0
  40. aury/boot/commands/templates/project/aury_docs/10-storage.md.tpl +115 -0
  41. aury/boot/commands/templates/project/aury_docs/11-logging.md.tpl +131 -0
  42. aury/boot/commands/templates/project/aury_docs/12-admin.md.tpl +56 -0
  43. aury/boot/commands/templates/project/aury_docs/13-channel.md.tpl +104 -0
  44. aury/boot/commands/templates/project/aury_docs/14-mq.md.tpl +102 -0
  45. aury/boot/commands/templates/project/aury_docs/15-events.md.tpl +147 -0
  46. aury/boot/commands/templates/project/aury_docs/16-adapter.md.tpl +403 -0
  47. aury/boot/commands/templates/project/{CLI.md.tpl → aury_docs/99-cli.md.tpl} +19 -19
  48. aury/boot/commands/templates/project/config.py.tpl +10 -10
  49. aury/boot/commands/templates/project/env_templates/_header.tpl +10 -0
  50. aury/boot/commands/templates/project/env_templates/admin.tpl +49 -0
  51. aury/boot/commands/templates/project/env_templates/cache.tpl +14 -0
  52. aury/boot/commands/templates/project/env_templates/database.tpl +22 -0
  53. aury/boot/commands/templates/project/env_templates/log.tpl +18 -0
  54. aury/boot/commands/templates/project/env_templates/messaging.tpl +46 -0
  55. aury/boot/commands/templates/project/env_templates/rpc.tpl +28 -0
  56. aury/boot/commands/templates/project/env_templates/scheduler.tpl +18 -0
  57. aury/boot/commands/templates/project/env_templates/service.tpl +18 -0
  58. aury/boot/commands/templates/project/env_templates/storage.tpl +38 -0
  59. aury/boot/commands/templates/project/env_templates/third_party.tpl +43 -0
  60. aury/boot/commands/templates/project/modules/tasks.py.tpl +1 -1
  61. aury/boot/common/logging/__init__.py +26 -674
  62. aury/boot/common/logging/context.py +132 -0
  63. aury/boot/common/logging/decorators.py +118 -0
  64. aury/boot/common/logging/format.py +315 -0
  65. aury/boot/common/logging/setup.py +214 -0
  66. aury/boot/contrib/admin_console/auth.py +2 -3
  67. aury/boot/contrib/admin_console/install.py +1 -1
  68. aury/boot/domain/models/mixins.py +48 -1
  69. aury/boot/domain/pagination/__init__.py +94 -0
  70. aury/boot/domain/repository/impl.py +1 -1
  71. aury/boot/domain/repository/interface.py +1 -1
  72. aury/boot/domain/transaction/__init__.py +8 -9
  73. aury/boot/infrastructure/__init__.py +86 -29
  74. aury/boot/infrastructure/cache/backends.py +102 -18
  75. aury/boot/infrastructure/cache/base.py +12 -0
  76. aury/boot/infrastructure/cache/manager.py +153 -91
  77. aury/boot/infrastructure/channel/__init__.py +24 -0
  78. aury/boot/infrastructure/channel/backends/__init__.py +9 -0
  79. aury/boot/infrastructure/channel/backends/memory.py +83 -0
  80. aury/boot/infrastructure/channel/backends/redis.py +88 -0
  81. aury/boot/infrastructure/channel/base.py +92 -0
  82. aury/boot/infrastructure/channel/manager.py +203 -0
  83. aury/boot/infrastructure/clients/__init__.py +22 -0
  84. aury/boot/infrastructure/clients/rabbitmq/__init__.py +9 -0
  85. aury/boot/infrastructure/clients/rabbitmq/config.py +46 -0
  86. aury/boot/infrastructure/clients/rabbitmq/manager.py +288 -0
  87. aury/boot/infrastructure/clients/redis/__init__.py +28 -0
  88. aury/boot/infrastructure/clients/redis/config.py +51 -0
  89. aury/boot/infrastructure/clients/redis/manager.py +264 -0
  90. aury/boot/infrastructure/database/config.py +7 -16
  91. aury/boot/infrastructure/database/manager.py +16 -38
  92. aury/boot/infrastructure/events/__init__.py +18 -21
  93. aury/boot/infrastructure/events/backends/__init__.py +11 -0
  94. aury/boot/infrastructure/events/backends/memory.py +86 -0
  95. aury/boot/infrastructure/events/backends/rabbitmq.py +193 -0
  96. aury/boot/infrastructure/events/backends/redis.py +162 -0
  97. aury/boot/infrastructure/events/base.py +127 -0
  98. aury/boot/infrastructure/events/manager.py +224 -0
  99. aury/boot/infrastructure/mq/__init__.py +24 -0
  100. aury/boot/infrastructure/mq/backends/__init__.py +9 -0
  101. aury/boot/infrastructure/mq/backends/rabbitmq.py +179 -0
  102. aury/boot/infrastructure/mq/backends/redis.py +167 -0
  103. aury/boot/infrastructure/mq/base.py +143 -0
  104. aury/boot/infrastructure/mq/manager.py +239 -0
  105. aury/boot/infrastructure/scheduler/manager.py +7 -3
  106. aury/boot/infrastructure/storage/__init__.py +9 -9
  107. aury/boot/infrastructure/storage/base.py +17 -5
  108. aury/boot/infrastructure/storage/factory.py +0 -1
  109. aury/boot/infrastructure/tasks/__init__.py +2 -2
  110. aury/boot/infrastructure/tasks/config.py +5 -13
  111. aury/boot/infrastructure/tasks/manager.py +55 -33
  112. {aury_boot-0.0.4.dist-info → aury_boot-0.0.7.dist-info}/METADATA +20 -2
  113. aury_boot-0.0.7.dist-info/RECORD +197 -0
  114. aury/boot/commands/templates/project/DEVELOPMENT.md.tpl +0 -1397
  115. aury/boot/commands/templates/project/env.example.tpl +0 -213
  116. aury/boot/infrastructure/events/bus.py +0 -362
  117. aury/boot/infrastructure/events/config.py +0 -52
  118. aury/boot/infrastructure/events/consumer.py +0 -134
  119. aury/boot/infrastructure/events/models.py +0 -63
  120. aury_boot-0.0.4.dist-info/RECORD +0 -137
  121. {aury_boot-0.0.4.dist-info → aury_boot-0.0.7.dist-info}/WHEEL +0 -0
  122. {aury_boot-0.0.4.dist-info → aury_boot-0.0.7.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,216 @@
1
+ """多实例配置解析工具。
2
+
3
+ 支持从环境变量解析 {PREFIX}__{INSTANCE}__{FIELD} 格式的多实例配置。
4
+ 使用双下划线 (__) 作为层级分隔符,符合行业标准。
5
+
6
+ 示例:
7
+ DATABASE__DEFAULT__URL=postgresql://main...
8
+ DATABASE__DEFAULT__POOL_SIZE=10
9
+ DATABASE__ANALYTICS__URL=postgresql://analytics...
10
+
11
+ 解析后:
12
+ {
13
+ "default": {"url": "postgresql://main...", "pool_size": 10},
14
+ "analytics": {"url": "postgresql://analytics..."}
15
+ }
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import os
21
+ import re
22
+ from typing import Any
23
+
24
+ from pydantic import BaseModel
25
+
26
+
27
+ def parse_multi_instance_env(
28
+ prefix: str,
29
+ fields: list[str] | None = None,
30
+ *,
31
+ type_hints: dict[str, type] | None = None,
32
+ ) -> dict[str, dict[str, Any]]:
33
+ """从环境变量解析多实例配置。
34
+
35
+ 使用双下划线 (__) 作为层级分隔符:
36
+ - {PREFIX}__{INSTANCE}__{FIELD}=value
37
+
38
+ Args:
39
+ prefix: 环境变量前缀,如 "DATABASE"
40
+ fields: 支持的字段列表(可选,用于过滤)
41
+ type_hints: 字段类型提示,用于类型转换
42
+
43
+ Returns:
44
+ dict[str, dict[str, Any]]: 实例名 -> 配置字典
45
+
46
+ 示例:
47
+ >>> parse_multi_instance_env("DATABASE")
48
+ {
49
+ "default": {"url": "postgresql://...", "pool_size": 10},
50
+ "analytics": {"url": "postgresql://..."}
51
+ }
52
+ """
53
+ instances: dict[str, dict[str, Any]] = {}
54
+ type_hints = type_hints or {}
55
+ prefix_with_sep = f"{prefix}__"
56
+
57
+ # 将 fields 转为大写集合用于过滤
58
+ valid_fields: set[str] | None = None
59
+ if fields:
60
+ valid_fields = {f.upper() for f in fields}
61
+
62
+ for key, value in os.environ.items():
63
+ # 检查前缀
64
+ if not key.upper().startswith(prefix_with_sep):
65
+ continue
66
+
67
+ # 移除前缀后分割
68
+ remainder = key[len(prefix_with_sep):]
69
+ parts = remainder.split("__", 1) # 只分割一次:INSTANCE__FIELD
70
+
71
+ if len(parts) != 2:
72
+ continue
73
+
74
+ instance_name = parts[0].lower()
75
+ field_name = parts[1].lower()
76
+ field_name_upper = parts[1].upper()
77
+
78
+ # 如果指定了字段列表,进行过滤
79
+ if valid_fields and field_name_upper not in valid_fields:
80
+ continue
81
+
82
+ # 类型转换
83
+ converted_value = _convert_value(value, type_hints.get(field_name))
84
+
85
+ if instance_name not in instances:
86
+ instances[instance_name] = {}
87
+ instances[instance_name][field_name] = converted_value
88
+
89
+ return instances
90
+
91
+
92
+ def _convert_value(value: str, target_type: type | None) -> Any:
93
+ """转换环境变量值到目标类型。"""
94
+ if target_type is None:
95
+ return value
96
+
97
+ if target_type is bool:
98
+ return value.lower() in ("true", "1", "yes", "on")
99
+ elif target_type is int:
100
+ return int(value)
101
+ elif target_type is float:
102
+ return float(value)
103
+ elif target_type is list:
104
+ # 简单的逗号分隔
105
+ return [v.strip() for v in value.split(",") if v.strip()]
106
+ else:
107
+ return value
108
+
109
+
110
+ class MultiInstanceSettings(BaseModel):
111
+ """多实例配置基类。
112
+
113
+ 子类需要定义各实例共享的配置字段。
114
+ """
115
+
116
+ @classmethod
117
+ def get_field_names(cls) -> list[str]:
118
+ """获取所有字段名。"""
119
+ return list(cls.model_fields.keys())
120
+
121
+ @classmethod
122
+ def get_type_hints(cls) -> dict[str, type]:
123
+ """获取字段类型提示。"""
124
+ hints = {}
125
+ for name, field_info in cls.model_fields.items():
126
+ annotation = field_info.annotation
127
+ # 处理 Optional 类型
128
+ if hasattr(annotation, "__origin__"):
129
+ # 如 str | None -> str
130
+ args = getattr(annotation, "__args__", ())
131
+ for arg in args:
132
+ if arg is not type(None):
133
+ hints[name] = arg
134
+ break
135
+ else:
136
+ hints[name] = annotation
137
+ return hints
138
+
139
+
140
+ class MultiInstanceConfigLoader:
141
+ """多实例配置加载器。
142
+
143
+ 使用示例:
144
+ loader = MultiInstanceConfigLoader("DATABASE", DatabaseInstanceConfig)
145
+ instances = loader.load()
146
+ # {"default": DatabaseInstanceConfig(...), "analytics": DatabaseInstanceConfig(...)}
147
+ """
148
+
149
+ def __init__(
150
+ self,
151
+ prefix: str,
152
+ config_class: type[MultiInstanceSettings],
153
+ ):
154
+ """初始化加载器。
155
+
156
+ Args:
157
+ prefix: 环境变量前缀
158
+ config_class: 配置类(继承自 MultiInstanceSettings)
159
+ """
160
+ self.prefix = prefix.upper()
161
+ self.config_class = config_class
162
+
163
+ def load(self) -> dict[str, MultiInstanceSettings]:
164
+ """加载所有实例配置。
165
+
166
+ Returns:
167
+ dict[str, config_class]: 实例名 -> 配置对象
168
+ """
169
+ fields = self.config_class.get_field_names()
170
+ type_hints = self.config_class.get_type_hints()
171
+
172
+ raw_instances = parse_multi_instance_env(
173
+ self.prefix,
174
+ fields,
175
+ type_hints=type_hints,
176
+ )
177
+
178
+ # 转换为配置对象
179
+ instances = {}
180
+ for name, config_dict in raw_instances.items():
181
+ try:
182
+ instances[name] = self.config_class(**config_dict)
183
+ except Exception as e:
184
+ # 配置不完整时跳过,让 Pydantic 验证报错
185
+ raise ValueError(
186
+ f"配置实例 [{self.prefix}_{name.upper()}] 无效: {e}"
187
+ ) from e
188
+
189
+ return instances
190
+
191
+ def load_or_default(
192
+ self,
193
+ default_instance: str = "default",
194
+ ) -> dict[str, MultiInstanceSettings]:
195
+ """加载配置,如果没有任何实例则返回包含默认实例的字典。
196
+
197
+ Args:
198
+ default_instance: 默认实例名
199
+
200
+ Returns:
201
+ dict[str, config_class]: 实例名 -> 配置对象
202
+ """
203
+ instances = self.load()
204
+
205
+ if not instances:
206
+ # 没有配置任何实例,创建一个默认的
207
+ instances[default_instance] = self.config_class()
208
+
209
+ return instances
210
+
211
+
212
+ __all__ = [
213
+ "MultiInstanceConfigLoader",
214
+ "MultiInstanceSettings",
215
+ "parse_multi_instance_env",
216
+ ]