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
|
@@ -9,23 +9,183 @@
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
from pathlib import Path
|
|
12
|
-
from typing import Literal
|
|
12
|
+
from typing import Any, Literal
|
|
13
13
|
|
|
14
14
|
from dotenv import load_dotenv
|
|
15
15
|
from pydantic import Field
|
|
16
16
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
17
17
|
|
|
18
|
+
from .multi_instance import MultiInstanceConfigLoader, MultiInstanceSettings
|
|
19
|
+
|
|
18
20
|
|
|
19
21
|
def _load_env_file(env_file: str | Path) -> bool:
|
|
20
22
|
"""加载 .env 文件到环境变量。"""
|
|
21
23
|
return load_dotenv(env_file, override=True)
|
|
22
24
|
|
|
23
25
|
|
|
26
|
+
# =============================================================================
|
|
27
|
+
# 多实例配置基类
|
|
28
|
+
# =============================================================================
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class DatabaseInstanceConfig(MultiInstanceSettings):
|
|
32
|
+
"""数据库实例配置。
|
|
33
|
+
|
|
34
|
+
环境变量格式: DATABASE_{INSTANCE}_{FIELD}
|
|
35
|
+
示例:
|
|
36
|
+
DATABASE_DEFAULT_URL=postgresql://main...
|
|
37
|
+
DATABASE_DEFAULT_POOL_SIZE=10
|
|
38
|
+
DATABASE_ANALYTICS_URL=postgresql://analytics...
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
url: str = Field(
|
|
42
|
+
default="sqlite+aiosqlite:///./app.db",
|
|
43
|
+
description="数据库连接字符串"
|
|
44
|
+
)
|
|
45
|
+
echo: bool = Field(
|
|
46
|
+
default=False,
|
|
47
|
+
description="是否输出 SQL 语句"
|
|
48
|
+
)
|
|
49
|
+
pool_size: int = Field(
|
|
50
|
+
default=5,
|
|
51
|
+
description="数据库连接池大小"
|
|
52
|
+
)
|
|
53
|
+
max_overflow: int = Field(
|
|
54
|
+
default=10,
|
|
55
|
+
description="连接池最大溢出连接数"
|
|
56
|
+
)
|
|
57
|
+
pool_recycle: int = Field(
|
|
58
|
+
default=3600,
|
|
59
|
+
description="连接回收时间(秒)"
|
|
60
|
+
)
|
|
61
|
+
pool_timeout: int = Field(
|
|
62
|
+
default=30,
|
|
63
|
+
description="获取连接超时时间(秒)"
|
|
64
|
+
)
|
|
65
|
+
pool_pre_ping: bool = Field(
|
|
66
|
+
default=True,
|
|
67
|
+
description="是否在获取连接前进行 PING"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class CacheInstanceConfig(MultiInstanceSettings):
|
|
72
|
+
"""缓存实例配置。
|
|
73
|
+
|
|
74
|
+
环境变量格式: CACHE_{INSTANCE}_{FIELD}
|
|
75
|
+
示例:
|
|
76
|
+
CACHE_DEFAULT_BACKEND=redis
|
|
77
|
+
CACHE_DEFAULT_URL=redis://localhost:6379/0
|
|
78
|
+
CACHE_LOCAL_BACKEND=memory
|
|
79
|
+
CACHE_LOCAL_MAX_SIZE=5000
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
backend: str = Field(
|
|
83
|
+
default="memory",
|
|
84
|
+
description="缓存后端 (memory/redis/memcached)"
|
|
85
|
+
)
|
|
86
|
+
url: str | None = Field(
|
|
87
|
+
default=None,
|
|
88
|
+
description="缓存服务 URL"
|
|
89
|
+
)
|
|
90
|
+
max_size: int = Field(
|
|
91
|
+
default=1000,
|
|
92
|
+
description="内存缓存最大大小"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class StorageInstanceConfig(MultiInstanceSettings):
|
|
97
|
+
"""对象存储实例配置。
|
|
98
|
+
|
|
99
|
+
环境变量格式: STORAGE_{INSTANCE}_{FIELD}
|
|
100
|
+
示例:
|
|
101
|
+
STORAGE_DEFAULT_BACKEND=s3
|
|
102
|
+
STORAGE_DEFAULT_BUCKET=main-bucket
|
|
103
|
+
STORAGE_BACKUP_BACKEND=local
|
|
104
|
+
STORAGE_BACKUP_BASE_PATH=/backup
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
backend: Literal["local", "s3", "oss", "cos"] = Field(
|
|
108
|
+
default="local",
|
|
109
|
+
description="存储后端"
|
|
110
|
+
)
|
|
111
|
+
# S3 配置
|
|
112
|
+
access_key_id: str | None = Field(default=None)
|
|
113
|
+
access_key_secret: str | None = Field(default=None)
|
|
114
|
+
endpoint: str | None = Field(default=None)
|
|
115
|
+
region: str | None = Field(default=None)
|
|
116
|
+
bucket_name: str | None = Field(default=None)
|
|
117
|
+
# 本地存储
|
|
118
|
+
base_path: str = Field(default="./storage")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class ChannelInstanceConfig(MultiInstanceSettings):
|
|
122
|
+
"""通道实例配置。
|
|
123
|
+
|
|
124
|
+
环境变量格式: CHANNEL_{INSTANCE}_{FIELD}
|
|
125
|
+
示例:
|
|
126
|
+
CHANNEL_DEFAULT_BACKEND=memory
|
|
127
|
+
CHANNEL_SHARED_BACKEND=redis
|
|
128
|
+
CHANNEL_SHARED_URL=redis://localhost:6379/3
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
backend: str = Field(
|
|
132
|
+
default="memory",
|
|
133
|
+
description="通道后端 (memory/redis)"
|
|
134
|
+
)
|
|
135
|
+
url: str | None = Field(
|
|
136
|
+
default=None,
|
|
137
|
+
description="Redis URL(当 backend=redis 时需要)"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class MQInstanceConfig(MultiInstanceSettings):
|
|
142
|
+
"""消息队列实例配置。
|
|
143
|
+
|
|
144
|
+
环境变量格式: MQ_{INSTANCE}_{FIELD}
|
|
145
|
+
示例:
|
|
146
|
+
MQ_DEFAULT_BACKEND=redis
|
|
147
|
+
MQ_DEFAULT_URL=redis://localhost:6379/4
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
backend: str = Field(
|
|
151
|
+
default="redis",
|
|
152
|
+
description="消息队列后端 (redis/rabbitmq)"
|
|
153
|
+
)
|
|
154
|
+
url: str | None = Field(
|
|
155
|
+
default=None,
|
|
156
|
+
description="连接 URL"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class EventInstanceConfig(MultiInstanceSettings):
|
|
161
|
+
"""事件总线实例配置。
|
|
162
|
+
|
|
163
|
+
环境变量格式: EVENT_{INSTANCE}_{FIELD}
|
|
164
|
+
示例:
|
|
165
|
+
EVENT_DEFAULT_BACKEND=memory
|
|
166
|
+
EVENT_DISTRIBUTED_BACKEND=redis
|
|
167
|
+
EVENT_DISTRIBUTED_URL=redis://localhost:6379/5
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
backend: str = Field(
|
|
171
|
+
default="memory",
|
|
172
|
+
description="事件后端 (memory/redis/rabbitmq)"
|
|
173
|
+
)
|
|
174
|
+
url: str | None = Field(
|
|
175
|
+
default=None,
|
|
176
|
+
description="连接 URL"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# =============================================================================
|
|
181
|
+
# 单实例配置
|
|
182
|
+
# =============================================================================
|
|
183
|
+
|
|
184
|
+
|
|
24
185
|
class DatabaseSettings(BaseSettings):
|
|
25
|
-
"""
|
|
186
|
+
"""数据库配置(单实例)。
|
|
26
187
|
|
|
27
|
-
|
|
28
|
-
示例: DATABASE_URL, DATABASE_ECHO, DATABASE_POOL_SIZE
|
|
188
|
+
推荐使用多实例配置: DATABASE_{INSTANCE}_{FIELD}
|
|
29
189
|
"""
|
|
30
190
|
|
|
31
191
|
url: str = Field(
|
|
@@ -350,6 +510,49 @@ class EventSettings(BaseSettings):
|
|
|
350
510
|
)
|
|
351
511
|
|
|
352
512
|
|
|
513
|
+
class MessageQueueSettings(BaseSettings):
|
|
514
|
+
"""消息队列配置。
|
|
515
|
+
|
|
516
|
+
环境变量前缀: MQ_
|
|
517
|
+
示例: MQ_BROKER_URL, MQ_DEFAULT_QUEUE, MQ_SERIALIZER
|
|
518
|
+
|
|
519
|
+
与 Task(任务队列)的区别:
|
|
520
|
+
- Task: 基于 Dramatiq,用于异步任务处理(API + Worker 模式)
|
|
521
|
+
- MQ: 通用消息队列,用于服务间通信、事件驱动架构
|
|
522
|
+
|
|
523
|
+
支持的后端(通过 Kombu):
|
|
524
|
+
- Redis: redis://localhost:6379/0
|
|
525
|
+
- RabbitMQ: amqp://guest:guest@localhost:5672//
|
|
526
|
+
- Amazon SQS: sqs://
|
|
527
|
+
"""
|
|
528
|
+
|
|
529
|
+
enabled: bool = Field(
|
|
530
|
+
default=False,
|
|
531
|
+
description="是否启用消息队列组件"
|
|
532
|
+
)
|
|
533
|
+
broker_url: str | None = Field(
|
|
534
|
+
default=None,
|
|
535
|
+
description="消息队列代理 URL"
|
|
536
|
+
)
|
|
537
|
+
default_queue: str = Field(
|
|
538
|
+
default="default",
|
|
539
|
+
description="默认队列名称"
|
|
540
|
+
)
|
|
541
|
+
serializer: str = Field(
|
|
542
|
+
default="json",
|
|
543
|
+
description="序列化方式(json/pickle/msgpack)"
|
|
544
|
+
)
|
|
545
|
+
prefetch_count: int = Field(
|
|
546
|
+
default=1,
|
|
547
|
+
description="预取消息数量"
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
model_config = SettingsConfigDict(
|
|
551
|
+
env_prefix="MQ_",
|
|
552
|
+
case_sensitive=False,
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
|
|
353
556
|
class MigrationSettings(BaseSettings):
|
|
354
557
|
"""数据库迁移配置。
|
|
355
558
|
|
|
@@ -566,9 +769,29 @@ class BaseConfig(BaseSettings):
|
|
|
566
769
|
所有应用配置的基类,提供通用配置项。
|
|
567
770
|
初始化时自动从 .env 文件加载环境变量,然后由 pydantic-settings 读取环境变量。
|
|
568
771
|
|
|
772
|
+
多实例配置:
|
|
773
|
+
框架支持多种组件的多实例配置,使用统一的环境变量格式:
|
|
774
|
+
{PREFIX}_{INSTANCE}_{FIELD}=value
|
|
775
|
+
|
|
776
|
+
示例:
|
|
777
|
+
DATABASE_DEFAULT_URL=postgresql://main...
|
|
778
|
+
DATABASE_ANALYTICS_URL=postgresql://analytics...
|
|
779
|
+
CACHE_DEFAULT_BACKEND=redis
|
|
780
|
+
CACHE_DEFAULT_URL=redis://localhost:6379/1
|
|
781
|
+
MQ_DEFAULT_URL=redis://localhost:6379/2
|
|
782
|
+
EVENT_DEFAULT_BACKEND=memory
|
|
783
|
+
|
|
569
784
|
注意:Application 层配置完全独立,不依赖 Infrastructure 层。
|
|
570
785
|
"""
|
|
571
786
|
|
|
787
|
+
# 多实例配置缓存
|
|
788
|
+
_databases: dict[str, DatabaseInstanceConfig] | None = None
|
|
789
|
+
_caches: dict[str, CacheInstanceConfig] | None = None
|
|
790
|
+
_storages: dict[str, StorageInstanceConfig] | None = None
|
|
791
|
+
_channels: dict[str, ChannelInstanceConfig] | None = None
|
|
792
|
+
_mqs: dict[str, MQInstanceConfig] | None = None
|
|
793
|
+
_events: dict[str, EventInstanceConfig] | None = None
|
|
794
|
+
|
|
572
795
|
def __init__(self, _env_file: str | Path = ".env", **kwargs) -> None:
|
|
573
796
|
"""初始化配置。
|
|
574
797
|
|
|
@@ -597,13 +820,8 @@ class BaseConfig(BaseSettings):
|
|
|
597
820
|
admin: AdminConsoleSettings = Field(default_factory=AdminConsoleSettings)
|
|
598
821
|
|
|
599
822
|
# ========== 数据与缓存 ==========
|
|
600
|
-
# 数据库配置
|
|
601
823
|
database: DatabaseSettings = Field(default_factory=DatabaseSettings)
|
|
602
|
-
|
|
603
|
-
# 缓存配置
|
|
604
824
|
cache: CacheSettings = Field(default_factory=CacheSettings)
|
|
605
|
-
|
|
606
|
-
# 对象存储配置(接入用;storage SDK 本身不读取 env)
|
|
607
825
|
storage: StorageSettings = Field(default_factory=StorageSettings)
|
|
608
826
|
|
|
609
827
|
# 迁移配置
|
|
@@ -617,10 +835,7 @@ class BaseConfig(BaseSettings):
|
|
|
617
835
|
scheduler: SchedulerSettings = Field(default_factory=SchedulerSettings)
|
|
618
836
|
|
|
619
837
|
# ========== 异步与事件 ==========
|
|
620
|
-
# 任务队列配置
|
|
621
838
|
task: TaskSettings = Field(default_factory=TaskSettings)
|
|
622
|
-
|
|
623
|
-
# 事件总线配置
|
|
624
839
|
event: EventSettings = Field(default_factory=EventSettings)
|
|
625
840
|
|
|
626
841
|
# ========== 微服务通信 ==========
|
|
@@ -635,6 +850,111 @@ class BaseConfig(BaseSettings):
|
|
|
635
850
|
extra="ignore",
|
|
636
851
|
)
|
|
637
852
|
|
|
853
|
+
# ========== 多实例配置访问方法 ==========
|
|
854
|
+
|
|
855
|
+
def get_databases(self) -> dict[str, DatabaseInstanceConfig]:
|
|
856
|
+
"""获取所有数据库实例配置。
|
|
857
|
+
|
|
858
|
+
从环境变量解析 DATABASE_{INSTANCE}_{FIELD} 格式的配置。
|
|
859
|
+
如果没有配置多实例,返回从单实例配置转换的 default 实例。
|
|
860
|
+
"""
|
|
861
|
+
if self._databases is None:
|
|
862
|
+
loader = MultiInstanceConfigLoader("DATABASE", DatabaseInstanceConfig)
|
|
863
|
+
self._databases = loader.load()
|
|
864
|
+
if not self._databases:
|
|
865
|
+
self._databases = {
|
|
866
|
+
"default": DatabaseInstanceConfig(
|
|
867
|
+
url=self.database.url,
|
|
868
|
+
echo=self.database.echo,
|
|
869
|
+
pool_size=self.database.pool_size,
|
|
870
|
+
max_overflow=self.database.max_overflow,
|
|
871
|
+
pool_recycle=self.database.pool_recycle,
|
|
872
|
+
pool_timeout=self.database.pool_timeout,
|
|
873
|
+
pool_pre_ping=self.database.pool_pre_ping,
|
|
874
|
+
)
|
|
875
|
+
}
|
|
876
|
+
return self._databases
|
|
877
|
+
|
|
878
|
+
def get_caches(self) -> dict[str, CacheInstanceConfig]:
|
|
879
|
+
"""获取所有缓存实例配置。
|
|
880
|
+
|
|
881
|
+
从环境变量解析 CACHE_{INSTANCE}_{FIELD} 格式的配置。
|
|
882
|
+
如果没有配置多实例,返回从单实例配置转换的 default 实例。
|
|
883
|
+
"""
|
|
884
|
+
if self._caches is None:
|
|
885
|
+
loader = MultiInstanceConfigLoader("CACHE", CacheInstanceConfig)
|
|
886
|
+
self._caches = loader.load()
|
|
887
|
+
if not self._caches:
|
|
888
|
+
self._caches = {
|
|
889
|
+
"default": CacheInstanceConfig(
|
|
890
|
+
backend=self.cache.cache_type,
|
|
891
|
+
url=self.cache.url,
|
|
892
|
+
max_size=self.cache.max_size,
|
|
893
|
+
)
|
|
894
|
+
}
|
|
895
|
+
return self._caches
|
|
896
|
+
|
|
897
|
+
def get_storages(self) -> dict[str, StorageInstanceConfig]:
|
|
898
|
+
"""获取所有存储实例配置。
|
|
899
|
+
|
|
900
|
+
从环境变量解析 STORAGE_{INSTANCE}_{FIELD} 格式的配置。
|
|
901
|
+
如果没有配置多实例,返回从单实例配置转换的 default 实例。
|
|
902
|
+
"""
|
|
903
|
+
if self._storages is None:
|
|
904
|
+
loader = MultiInstanceConfigLoader("STORAGE", StorageInstanceConfig)
|
|
905
|
+
self._storages = loader.load()
|
|
906
|
+
if not self._storages:
|
|
907
|
+
self._storages = {
|
|
908
|
+
"default": StorageInstanceConfig(
|
|
909
|
+
backend=self.storage.type,
|
|
910
|
+
access_key_id=self.storage.access_key_id,
|
|
911
|
+
access_key_secret=self.storage.access_key_secret,
|
|
912
|
+
endpoint=self.storage.endpoint,
|
|
913
|
+
region=self.storage.region,
|
|
914
|
+
bucket_name=self.storage.bucket_name,
|
|
915
|
+
base_path=self.storage.base_path,
|
|
916
|
+
)
|
|
917
|
+
}
|
|
918
|
+
return self._storages
|
|
919
|
+
|
|
920
|
+
def get_channels(self) -> dict[str, ChannelInstanceConfig]:
|
|
921
|
+
"""获取所有通道实例配置。
|
|
922
|
+
|
|
923
|
+
从环境变量解析 CHANNEL_{INSTANCE}_{FIELD} 格式的配置。
|
|
924
|
+
"""
|
|
925
|
+
if self._channels is None:
|
|
926
|
+
loader = MultiInstanceConfigLoader("CHANNEL", ChannelInstanceConfig)
|
|
927
|
+
self._channels = loader.load()
|
|
928
|
+
return self._channels
|
|
929
|
+
|
|
930
|
+
def get_mqs(self) -> dict[str, MQInstanceConfig]:
|
|
931
|
+
"""获取所有消息队列实例配置。
|
|
932
|
+
|
|
933
|
+
从环境变量解析 MQ_{INSTANCE}_{FIELD} 格式的配置。
|
|
934
|
+
"""
|
|
935
|
+
if self._mqs is None:
|
|
936
|
+
loader = MultiInstanceConfigLoader("MQ", MQInstanceConfig)
|
|
937
|
+
self._mqs = loader.load()
|
|
938
|
+
return self._mqs
|
|
939
|
+
|
|
940
|
+
def get_events(self) -> dict[str, EventInstanceConfig]:
|
|
941
|
+
"""获取所有事件总线实例配置。
|
|
942
|
+
|
|
943
|
+
从环境变量解析 EVENT_{INSTANCE}_{FIELD} 格式的配置。
|
|
944
|
+
如果没有配置多实例,返回从单实例配置转换的 default 实例。
|
|
945
|
+
"""
|
|
946
|
+
if self._events is None:
|
|
947
|
+
loader = MultiInstanceConfigLoader("EVENT", EventInstanceConfig)
|
|
948
|
+
self._events = loader.load()
|
|
949
|
+
if not self._events and self.event.broker_url:
|
|
950
|
+
self._events = {
|
|
951
|
+
"default": EventInstanceConfig(
|
|
952
|
+
backend="redis" if "redis" in (self.event.broker_url or "") else "rabbitmq",
|
|
953
|
+
url=self.event.broker_url,
|
|
954
|
+
)
|
|
955
|
+
}
|
|
956
|
+
return self._events
|
|
957
|
+
|
|
638
958
|
@property
|
|
639
959
|
def is_production(self) -> bool:
|
|
640
960
|
"""是否为生产环境。"""
|
|
@@ -642,21 +962,30 @@ class BaseConfig(BaseSettings):
|
|
|
642
962
|
|
|
643
963
|
|
|
644
964
|
__all__ = [
|
|
965
|
+
# 配置类
|
|
645
966
|
"AdminAuthSettings",
|
|
646
967
|
"AdminConsoleSettings",
|
|
647
968
|
"BaseConfig",
|
|
648
969
|
"CORSSettings",
|
|
970
|
+
# 多实例配置类
|
|
971
|
+
"CacheInstanceConfig",
|
|
649
972
|
"CacheSettings",
|
|
973
|
+
"ChannelInstanceConfig",
|
|
974
|
+
"DatabaseInstanceConfig",
|
|
650
975
|
"DatabaseSettings",
|
|
976
|
+
"EventInstanceConfig",
|
|
651
977
|
"EventSettings",
|
|
652
978
|
"HealthCheckSettings",
|
|
653
979
|
"LogSettings",
|
|
980
|
+
"MQInstanceConfig",
|
|
981
|
+
"MessageQueueSettings",
|
|
654
982
|
"MigrationSettings",
|
|
655
983
|
"RPCClientSettings",
|
|
656
984
|
"RPCServiceSettings",
|
|
657
985
|
"SchedulerSettings",
|
|
658
986
|
"ServerSettings",
|
|
659
987
|
"ServiceSettings",
|
|
988
|
+
"StorageInstanceConfig",
|
|
660
989
|
"StorageSettings",
|
|
661
990
|
"TaskSettings",
|
|
662
991
|
]
|
|
@@ -9,6 +9,7 @@ from abc import ABC, abstractmethod
|
|
|
9
9
|
from typing import TYPE_CHECKING
|
|
10
10
|
|
|
11
11
|
from fastapi import HTTPException, Request, status
|
|
12
|
+
from fastapi.exceptions import RequestValidationError
|
|
12
13
|
from fastapi.responses import JSONResponse
|
|
13
14
|
from pydantic import ValidationError
|
|
14
15
|
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
|
|
@@ -121,9 +122,19 @@ class BaseErrorHandler(ErrorHandler):
|
|
|
121
122
|
|
|
122
123
|
errors = [detail.model_dump() for detail in exception.details] if exception.details else None
|
|
123
124
|
|
|
125
|
+
# 兼容 ErrorCode 枚举和字符串
|
|
126
|
+
code_value = exception.code.value if hasattr(exception.code, "value") else exception.code
|
|
127
|
+
|
|
128
|
+
# 尝试转换为 int,如果失败则使用 status_code
|
|
129
|
+
try:
|
|
130
|
+
code_int = int(code_value)
|
|
131
|
+
except (ValueError, TypeError):
|
|
132
|
+
# 非数字字符串(如 "TODO_ATTACHMENT_ERROR"),使用 HTTP 状态码作为 code
|
|
133
|
+
code_int = exception.status_code
|
|
134
|
+
|
|
124
135
|
response = ResponseBuilder.fail(
|
|
125
136
|
message=exception.message,
|
|
126
|
-
code=
|
|
137
|
+
code=code_int,
|
|
127
138
|
errors=errors,
|
|
128
139
|
)
|
|
129
140
|
|
|
@@ -160,11 +171,14 @@ class HTTPExceptionHandler(ErrorHandler):
|
|
|
160
171
|
|
|
161
172
|
|
|
162
173
|
class ValidationErrorHandler(ErrorHandler):
|
|
163
|
-
"""
|
|
174
|
+
"""验证异常处理器。
|
|
175
|
+
|
|
176
|
+
处理 Pydantic ValidationError 和 FastAPI RequestValidationError。
|
|
177
|
+
"""
|
|
164
178
|
|
|
165
179
|
def can_handle(self, exception: Exception) -> bool:
|
|
166
180
|
"""判断是否为验证异常。"""
|
|
167
|
-
return isinstance(exception, ValidationError)
|
|
181
|
+
return isinstance(exception, ValidationError | RequestValidationError)
|
|
168
182
|
|
|
169
183
|
async def handle(self, exception: Exception, request: Request) -> JSONResponse:
|
|
170
184
|
"""处理验证异常。"""
|
|
@@ -16,7 +16,8 @@ from starlette.middleware.base import BaseHTTPMiddleware
|
|
|
16
16
|
from starlette.requests import Request
|
|
17
17
|
from starlette.responses import Response
|
|
18
18
|
|
|
19
|
-
from aury.boot.
|
|
19
|
+
from aury.boot.application.errors import global_exception_handler
|
|
20
|
+
from aury.boot.common.logging import logger, set_trace_id
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
def log_request[T](func: Callable[..., T]) -> Callable[..., T]:
|
|
@@ -107,10 +108,7 @@ def _should_log_body(content_type: str | None) -> bool:
|
|
|
107
108
|
if not content_type:
|
|
108
109
|
return True
|
|
109
110
|
content_type = content_type.lower()
|
|
110
|
-
for skip_type in SKIP_BODY_CONTENT_TYPES
|
|
111
|
-
if skip_type in content_type:
|
|
112
|
-
return False
|
|
113
|
-
return True
|
|
111
|
+
return all(skip_type not in content_type for skip_type in SKIP_BODY_CONTENT_TYPES)
|
|
114
112
|
|
|
115
113
|
|
|
116
114
|
class RequestLoggingMiddleware(BaseHTTPMiddleware):
|
|
@@ -211,7 +209,11 @@ class RequestLoggingMiddleware(BaseHTTPMiddleware):
|
|
|
211
209
|
f"请求处理失败: {request.method} {request.url.path} | "
|
|
212
210
|
f"耗时: {duration:.3f}s | Trace-ID: {trace_id}"
|
|
213
211
|
)
|
|
214
|
-
|
|
212
|
+
# 使用全局异常处理器生成响应,而不是直接抛出异常
|
|
213
|
+
# BaseHTTPMiddleware 中直接 raise 会绕过 FastAPI 的异常处理器
|
|
214
|
+
response = await global_exception_handler(request, exc)
|
|
215
|
+
response.headers["x-trace-id"] = trace_id
|
|
216
|
+
return response
|
|
215
217
|
|
|
216
218
|
|
|
217
219
|
class WebSocketLoggingMiddleware:
|
|
@@ -328,120 +330,6 @@ class WebSocketLoggingMiddleware:
|
|
|
328
330
|
raise
|
|
329
331
|
|
|
330
332
|
|
|
331
|
-
class WebSocketLoggingMiddleware:
|
|
332
|
-
"""WebSocket 日志中间件。
|
|
333
|
-
|
|
334
|
-
记录 WebSocket 连接生命周期和消息收发(可选)。
|
|
335
|
-
|
|
336
|
-
使用示例:
|
|
337
|
-
from aury.boot.application.middleware.logging import WebSocketLoggingMiddleware
|
|
338
|
-
|
|
339
|
-
app.add_middleware(WebSocketLoggingMiddleware, log_messages=True)
|
|
340
|
-
"""
|
|
341
|
-
|
|
342
|
-
def __init__(
|
|
343
|
-
self,
|
|
344
|
-
app,
|
|
345
|
-
*,
|
|
346
|
-
log_messages: bool = False,
|
|
347
|
-
max_message_length: int = 500,
|
|
348
|
-
) -> None:
|
|
349
|
-
"""初始化 WebSocket 日志中间件。
|
|
350
|
-
|
|
351
|
-
Args:
|
|
352
|
-
app: ASGI 应用
|
|
353
|
-
log_messages: 是否记录消息内容(默认 False,注意性能和敏感数据)
|
|
354
|
-
max_message_length: 消息内容最大记录长度
|
|
355
|
-
"""
|
|
356
|
-
self.app = app
|
|
357
|
-
self.log_messages = log_messages
|
|
358
|
-
self.max_message_length = max_message_length
|
|
359
|
-
|
|
360
|
-
async def __call__(self, scope, receive, send) -> None:
|
|
361
|
-
if scope["type"] != "websocket":
|
|
362
|
-
await self.app(scope, receive, send)
|
|
363
|
-
return
|
|
364
|
-
|
|
365
|
-
# 获取或生成 trace_id
|
|
366
|
-
headers = dict(scope.get("headers", []))
|
|
367
|
-
trace_id = (
|
|
368
|
-
headers.get(b"x-trace-id", b"").decode() or
|
|
369
|
-
headers.get(b"x-request-id", b"").decode() or
|
|
370
|
-
str(uuid.uuid4())
|
|
371
|
-
)
|
|
372
|
-
set_trace_id(trace_id)
|
|
373
|
-
|
|
374
|
-
path = scope.get("path", "/")
|
|
375
|
-
client = scope.get("client")
|
|
376
|
-
client_host = f"{client[0]}:{client[1]}" if client else "unknown"
|
|
377
|
-
|
|
378
|
-
start_time = time.time()
|
|
379
|
-
message_count = {"sent": 0, "received": 0}
|
|
380
|
-
|
|
381
|
-
async def logging_receive():
|
|
382
|
-
message = await receive()
|
|
383
|
-
msg_type = message.get("type", "")
|
|
384
|
-
|
|
385
|
-
if msg_type == "websocket.connect":
|
|
386
|
-
logger.info(
|
|
387
|
-
f"WS → 连接: {path} | "
|
|
388
|
-
f"客户端: {client_host} | Trace-ID: {trace_id}"
|
|
389
|
-
)
|
|
390
|
-
elif msg_type == "websocket.disconnect":
|
|
391
|
-
duration = time.time() - start_time
|
|
392
|
-
logger.info(
|
|
393
|
-
f"WS ← 断开: {path} | "
|
|
394
|
-
f"时长: {duration:.1f}s | "
|
|
395
|
-
f"收/发: {message_count['received']}/{message_count['sent']} | "
|
|
396
|
-
f"Trace-ID: {trace_id}"
|
|
397
|
-
)
|
|
398
|
-
elif msg_type == "websocket.receive":
|
|
399
|
-
message_count["received"] += 1
|
|
400
|
-
if self.log_messages:
|
|
401
|
-
text = message.get("text") or message.get("bytes", b"").decode("utf-8", errors="replace")
|
|
402
|
-
if len(text) > self.max_message_length:
|
|
403
|
-
text = text[:self.max_message_length] + "..."
|
|
404
|
-
logger.debug(f"WS → 收: {path} | {text}")
|
|
405
|
-
|
|
406
|
-
return message
|
|
407
|
-
|
|
408
|
-
async def logging_send(message):
|
|
409
|
-
msg_type = message.get("type", "")
|
|
410
|
-
|
|
411
|
-
if msg_type == "websocket.send":
|
|
412
|
-
message_count["sent"] += 1
|
|
413
|
-
if self.log_messages:
|
|
414
|
-
text = message.get("text") or message.get("bytes", b"").decode("utf-8", errors="replace")
|
|
415
|
-
if len(text) > self.max_message_length:
|
|
416
|
-
text = text[:self.max_message_length] + "..."
|
|
417
|
-
logger.debug(f"WS ← 发: {path} | {text}")
|
|
418
|
-
elif msg_type == "websocket.close":
|
|
419
|
-
code = message.get("code", 1000)
|
|
420
|
-
reason = message.get("reason", "")
|
|
421
|
-
duration = time.time() - start_time
|
|
422
|
-
log_level = "warning" if code != 1000 else "info"
|
|
423
|
-
logger.log(
|
|
424
|
-
log_level.upper(),
|
|
425
|
-
f"WS × 关闭: {path} | "
|
|
426
|
-
f"Code: {code}{' | 原因: ' + reason if reason else ''} | "
|
|
427
|
-
f"时长: {duration:.1f}s | Trace-ID: {trace_id}"
|
|
428
|
-
)
|
|
429
|
-
|
|
430
|
-
await send(message)
|
|
431
|
-
|
|
432
|
-
try:
|
|
433
|
-
await self.app(scope, logging_receive, logging_send)
|
|
434
|
-
except Exception as exc:
|
|
435
|
-
duration = time.time() - start_time
|
|
436
|
-
logger.exception(
|
|
437
|
-
f"WS ✖ 异常: {path} | "
|
|
438
|
-
f"时长: {duration:.1f}s | "
|
|
439
|
-
f"收/发: {message_count['received']}/{message_count['sent']} | "
|
|
440
|
-
f"Trace-ID: {trace_id}"
|
|
441
|
-
)
|
|
442
|
-
raise
|
|
443
|
-
|
|
444
|
-
|
|
445
333
|
__all__ = [
|
|
446
334
|
"RequestLoggingMiddleware",
|
|
447
335
|
"WebSocketLoggingMiddleware",
|
|
@@ -51,13 +51,13 @@ __all__ = [
|
|
|
51
51
|
"BaseRPCClient",
|
|
52
52
|
"CompositeServiceDiscovery",
|
|
53
53
|
"ConfigServiceDiscovery",
|
|
54
|
-
"create_rpc_client",
|
|
55
54
|
"DNSServiceDiscovery",
|
|
56
|
-
"get_service_discovery",
|
|
57
55
|
"RPCClient",
|
|
58
56
|
"RPCError",
|
|
59
57
|
"RPCResponse",
|
|
60
58
|
"ServiceDiscovery",
|
|
59
|
+
"create_rpc_client",
|
|
60
|
+
"get_service_discovery",
|
|
61
61
|
"set_service_discovery",
|
|
62
62
|
]
|
|
63
63
|
|