aury-boot 0.0.4__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 +27 -8
- 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.4.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.4.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.4.dist-info → aury_boot-0.0.5.dist-info}/WHEEL +0 -0
- {aury_boot-0.0.4.dist-info → aury_boot-0.0.5.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# 消息队列(MQ)
|
|
2
|
+
|
|
3
|
+
支持 `redis` 和 `rabbitmq` 后端的消息队列,用于异步任务解耦。
|
|
4
|
+
|
|
5
|
+
## 14.1 基本用法
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from aury.boot.infrastructure.mq import MQManager
|
|
9
|
+
|
|
10
|
+
# 获取实例
|
|
11
|
+
mq = MQManager.get_instance()
|
|
12
|
+
|
|
13
|
+
# Redis 后端
|
|
14
|
+
await mq.initialize(backend="redis", url="redis://localhost:6379/0")
|
|
15
|
+
|
|
16
|
+
# RabbitMQ 后端
|
|
17
|
+
await mq.initialize(backend="rabbitmq", url="amqp://guest:guest@localhost:5672/")
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## 14.2 生产者
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
# 发送消息
|
|
24
|
+
await mq.publish(
|
|
25
|
+
queue="orders",
|
|
26
|
+
message={{"order_id": "123", "action": "created"}}
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# 批量发送
|
|
30
|
+
await mq.publish_batch(
|
|
31
|
+
queue="orders",
|
|
32
|
+
messages=[
|
|
33
|
+
{{"order_id": "1", "action": "created"}},
|
|
34
|
+
{{"order_id": "2", "action": "updated"}},
|
|
35
|
+
]
|
|
36
|
+
)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## 14.3 消费者
|
|
40
|
+
|
|
41
|
+
**文件**: `{package_name}/workers/order_worker.py`
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from aury.boot.infrastructure.mq import MQManager
|
|
45
|
+
from aury.boot.common.logging import logger
|
|
46
|
+
|
|
47
|
+
mq = MQManager.get_instance()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
async def process_order(message: dict):
|
|
51
|
+
\"\"\"处理订单消息。\"\"\"
|
|
52
|
+
logger.info(f"处理订单: {{message['order_id']}}")
|
|
53
|
+
# 业务逻辑...
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def start_consumer():
|
|
57
|
+
\"\"\"启动消费者。\"\"\"
|
|
58
|
+
await mq.consume("orders", process_order)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# 带确认的消费
|
|
62
|
+
async def process_with_ack(message: dict, ack, nack):
|
|
63
|
+
try:
|
|
64
|
+
await process_order(message)
|
|
65
|
+
await ack()
|
|
66
|
+
except Exception:
|
|
67
|
+
await nack(requeue=True)
|
|
68
|
+
|
|
69
|
+
await mq.consume("orders", process_with_ack, auto_ack=False)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## 14.4 多实例
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
# 不同用途的 MQ 实例
|
|
76
|
+
orders_mq = MQManager.get_instance("orders")
|
|
77
|
+
notifications_mq = MQManager.get_instance("notifications")
|
|
78
|
+
|
|
79
|
+
# 分别初始化
|
|
80
|
+
await orders_mq.initialize(backend="rabbitmq", url="amqp://localhost:5672/orders")
|
|
81
|
+
await notifications_mq.initialize(backend="redis", url="redis://localhost:6379/5")
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## 14.5 环境变量
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# 默认实例
|
|
88
|
+
MQ_BACKEND=redis
|
|
89
|
+
MQ_URL=redis://localhost:6379/0
|
|
90
|
+
|
|
91
|
+
# 多实例(格式:MQ_{{INSTANCE}}_{{FIELD}})
|
|
92
|
+
MQ_DEFAULT_BACKEND=redis
|
|
93
|
+
MQ_DEFAULT_URL=redis://localhost:6379/4
|
|
94
|
+
MQ_ORDERS_BACKEND=rabbitmq
|
|
95
|
+
MQ_ORDERS_URL=amqp://guest:guest@localhost:5672/
|
|
96
|
+
MQ_ORDERS_PREFETCH_COUNT=10
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## 14.6 与异步任务(Dramatiq)的区别
|
|
100
|
+
|
|
101
|
+
- **MQ**:轻量级消息传递,适合简单的生产者-消费者模式
|
|
102
|
+
- **Dramatiq(TaskManager)**:功能更丰富,支持重试、延迟、优先级等
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# 事件总线(Events)
|
|
2
|
+
|
|
3
|
+
支持 `memory`、`redis`、`rabbitmq` 后端的事件发布订阅系统。
|
|
4
|
+
|
|
5
|
+
## 15.1 定义事件
|
|
6
|
+
|
|
7
|
+
**文件**: `{package_name}/events/__init__.py`
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from aury.boot.infrastructure.events import Event
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class OrderCreatedEvent(Event):
|
|
14
|
+
\"\"\"订单创建事件。\"\"\"
|
|
15
|
+
order_id: str
|
|
16
|
+
amount: float
|
|
17
|
+
user_id: str
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def event_name(self) -> str:
|
|
21
|
+
return "order.created"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class UserRegisteredEvent(Event):
|
|
25
|
+
\"\"\"用户注册事件。\"\"\"
|
|
26
|
+
user_id: str
|
|
27
|
+
email: str
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def event_name(self) -> str:
|
|
31
|
+
return "user.registered"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### 15.1.1 事件初始化
|
|
35
|
+
|
|
36
|
+
事件类基于 Pydantic BaseModel,支持以下初始化方式:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
# 方式 1:关键字参数(推荐)
|
|
40
|
+
event = OrderCreatedEvent(
|
|
41
|
+
order_id="order-123",
|
|
42
|
+
amount=99.99,
|
|
43
|
+
user_id="user-456",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# 方式 2:字典解包
|
|
47
|
+
event = OrderCreatedEvent(**{{
|
|
48
|
+
"order_id": "order-123",
|
|
49
|
+
"amount": 99.99,
|
|
50
|
+
"user_id": "user-456",
|
|
51
|
+
}})
|
|
52
|
+
|
|
53
|
+
# 方式 3:从实体创建
|
|
54
|
+
event = OrderCreatedEvent(
|
|
55
|
+
order_id=str(order.id),
|
|
56
|
+
amount=order.amount,
|
|
57
|
+
user_id=str(order.user_id),
|
|
58
|
+
)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## 15.2 订阅事件
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from aury.boot.infrastructure.events import EventBusManager
|
|
65
|
+
|
|
66
|
+
bus = EventBusManager.get_instance()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@bus.subscribe(OrderCreatedEvent)
|
|
70
|
+
async def on_order_created(event: OrderCreatedEvent):
|
|
71
|
+
\"\"\"处理订单创建事件。\"\"\"
|
|
72
|
+
logger.info(f"订单创建: {{event.order_id}}, 金额: {{event.amount}}")
|
|
73
|
+
# 发送通知、更新统计等...
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@bus.subscribe(UserRegisteredEvent)
|
|
77
|
+
async def send_welcome_email(event: UserRegisteredEvent):
|
|
78
|
+
\"\"\"发送欢迎邮件。\"\"\"
|
|
79
|
+
await email_service.send_welcome(event.email)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## 15.3 发布事件
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from {package_name}.events import OrderCreatedEvent
|
|
86
|
+
|
|
87
|
+
@router.post("/orders")
|
|
88
|
+
async def create_order(request: OrderCreateRequest):
|
|
89
|
+
# 创建订单
|
|
90
|
+
order = await order_service.create(request)
|
|
91
|
+
|
|
92
|
+
# 发布事件
|
|
93
|
+
await bus.publish(OrderCreatedEvent(
|
|
94
|
+
order_id=order.id,
|
|
95
|
+
amount=order.amount,
|
|
96
|
+
user_id=order.user_id
|
|
97
|
+
))
|
|
98
|
+
|
|
99
|
+
return BaseResponse(code=200, message="订单创建成功", data=order)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## 15.4 多实例(EventBusManager)
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from aury.boot.infrastructure.events import EventBusManager
|
|
106
|
+
|
|
107
|
+
# 获取实例
|
|
108
|
+
bus = EventBusManager.get_instance()
|
|
109
|
+
domain_bus = EventBusManager.get_instance("domain")
|
|
110
|
+
|
|
111
|
+
# Memory 后端(单进程)
|
|
112
|
+
await bus.initialize(backend="memory")
|
|
113
|
+
|
|
114
|
+
# Redis Pub/Sub 后端
|
|
115
|
+
await bus.initialize(
|
|
116
|
+
backend="redis",
|
|
117
|
+
url="redis://localhost:6379/0",
|
|
118
|
+
channel_prefix="events:",
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# RabbitMQ 后端
|
|
122
|
+
await bus.initialize(
|
|
123
|
+
backend="rabbitmq",
|
|
124
|
+
url="amqp://guest:guest@localhost:5672/",
|
|
125
|
+
exchange_name="app.events",
|
|
126
|
+
)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## 15.5 环境变量
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
# 默认实例
|
|
133
|
+
EVENT_BACKEND=memory
|
|
134
|
+
|
|
135
|
+
# 多实例(格式:EVENT_{{INSTANCE}}_{{FIELD}})
|
|
136
|
+
EVENT_DEFAULT_BACKEND=redis
|
|
137
|
+
EVENT_DEFAULT_URL=redis://localhost:6379/5
|
|
138
|
+
EVENT_DOMAIN_BACKEND=rabbitmq
|
|
139
|
+
EVENT_DOMAIN_URL=amqp://guest:guest@localhost:5672/
|
|
140
|
+
EVENT_DOMAIN_EXCHANGE_NAME=domain.events
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## 15.6 最佳实践
|
|
144
|
+
|
|
145
|
+
1. **事件应该是不可变的** - 使用 Pydantic 的 `frozen=True`
|
|
146
|
+
2. **订阅者应该快速完成** - 耗时操作使用任务队列
|
|
147
|
+
3. **避免循环事件** - 不要在订阅者中发布相同类型的事件
|
|
@@ -25,8 +25,10 @@ SERVICE_NAME={project_name_snake}
|
|
|
25
25
|
# =============================================================================
|
|
26
26
|
# 数据库配置 (DATABASE_)
|
|
27
27
|
# =============================================================================
|
|
28
|
-
#
|
|
28
|
+
# 支持多实例配置,格式: DATABASE_{{INSTANCE}}_{{FIELD}}
|
|
29
|
+
# 默认实例 (default):
|
|
29
30
|
# DATABASE_URL=sqlite+aiosqlite:///./dev.db
|
|
31
|
+
# DATABASE_DEFAULT_URL=sqlite+aiosqlite:///./dev.db
|
|
30
32
|
# PostgreSQL: postgresql+asyncpg://user:pass@localhost:5432/{project_name_snake}
|
|
31
33
|
# MySQL: mysql+aiomysql://user:pass@localhost:3306/{project_name_snake}
|
|
32
34
|
|
|
@@ -43,15 +45,25 @@ SERVICE_NAME={project_name_snake}
|
|
|
43
45
|
# 是否输出 SQL 语句(调试用)
|
|
44
46
|
# DATABASE_ECHO=false
|
|
45
47
|
|
|
48
|
+
# 多实例示例 (readonly):
|
|
49
|
+
# DATABASE_READONLY_URL=postgresql+asyncpg://user:pass@replica:5432/{project_name_snake}
|
|
50
|
+
# DATABASE_READONLY_POOL_SIZE=10
|
|
51
|
+
|
|
46
52
|
# =============================================================================
|
|
47
53
|
# 缓存配置 (CACHE_)
|
|
48
54
|
# =============================================================================
|
|
55
|
+
# 支持多实例配置,格式: CACHE_{{INSTANCE}}_{{FIELD}}
|
|
49
56
|
# 缓存类型: memory / redis / memcached
|
|
50
57
|
# CACHE_TYPE=memory
|
|
51
|
-
# Redis/Memcached URL
|
|
52
58
|
# CACHE_URL=redis://localhost:6379/0
|
|
53
59
|
# 内存缓存最大大小
|
|
54
60
|
# CACHE_MAX_SIZE=1000
|
|
61
|
+
# 默认 TTL(秒)
|
|
62
|
+
# CACHE_DEFAULT_TTL=300
|
|
63
|
+
#
|
|
64
|
+
# 多实例示例 (session):
|
|
65
|
+
# CACHE_SESSION_TYPE=redis
|
|
66
|
+
# CACHE_SESSION_URL=redis://localhost:6379/2
|
|
55
67
|
|
|
56
68
|
# =============================================================================
|
|
57
69
|
# 日志配置 (LOG_)
|
|
@@ -138,13 +150,69 @@ SERVICE_NAME={project_name_snake}
|
|
|
138
150
|
# 任务超时时间(秒)
|
|
139
151
|
# TASK_TIMEOUT=3600
|
|
140
152
|
|
|
153
|
+
# =============================================================================
|
|
154
|
+
# 流式通道配置 (CHANNEL_) - SSE/实时通信
|
|
155
|
+
# =============================================================================
|
|
156
|
+
# 支持多实例配置,格式: CHANNEL_{{INSTANCE}}_{{FIELD}}
|
|
157
|
+
# 后端类型: memory / redis
|
|
158
|
+
# CHANNEL_BACKEND=memory
|
|
159
|
+
# CHANNEL_DEFAULT_BACKEND=memory
|
|
160
|
+
#
|
|
161
|
+
# Redis 后端配置:
|
|
162
|
+
# CHANNEL_DEFAULT_BACKEND=redis
|
|
163
|
+
# CHANNEL_DEFAULT_URL=redis://localhost:6379/3
|
|
164
|
+
# CHANNEL_DEFAULT_KEY_PREFIX=channel:
|
|
165
|
+
# CHANNEL_DEFAULT_TTL=86400
|
|
166
|
+
#
|
|
167
|
+
# 多实例示例 (notifications):
|
|
168
|
+
# CHANNEL_NOTIFICATIONS_BACKEND=redis
|
|
169
|
+
# CHANNEL_NOTIFICATIONS_URL=redis://localhost:6379/3
|
|
170
|
+
|
|
171
|
+
# =============================================================================
|
|
172
|
+
# 消息队列配置 (MQ_)
|
|
173
|
+
# =============================================================================
|
|
174
|
+
# 支持多实例配置,格式: MQ_{{INSTANCE}}_{{FIELD}}
|
|
175
|
+
# 后端类型: redis / rabbitmq
|
|
176
|
+
# MQ_BACKEND=redis
|
|
177
|
+
# MQ_DEFAULT_BACKEND=redis
|
|
178
|
+
#
|
|
179
|
+
# Redis 后端配置:
|
|
180
|
+
# MQ_DEFAULT_URL=redis://localhost:6379/4
|
|
181
|
+
# MQ_DEFAULT_MAX_CONNECTIONS=10
|
|
182
|
+
#
|
|
183
|
+
# RabbitMQ 后端配置:
|
|
184
|
+
# MQ_DEFAULT_BACKEND=rabbitmq
|
|
185
|
+
# MQ_DEFAULT_URL=amqp://guest:guest@localhost:5672/
|
|
186
|
+
# MQ_DEFAULT_PREFETCH_COUNT=10
|
|
187
|
+
# MQ_DEFAULT_HEARTBEAT=60
|
|
188
|
+
#
|
|
189
|
+
# 多实例示例 (orders):
|
|
190
|
+
# MQ_ORDERS_BACKEND=rabbitmq
|
|
191
|
+
# MQ_ORDERS_URL=amqp://guest:guest@localhost:5672/orders
|
|
192
|
+
|
|
141
193
|
# =============================================================================
|
|
142
194
|
# 事件总线配置 (EVENT_)
|
|
143
195
|
# =============================================================================
|
|
144
|
-
#
|
|
145
|
-
#
|
|
146
|
-
#
|
|
196
|
+
# 支持多实例配置,格式: EVENT_{{INSTANCE}}_{{FIELD}}
|
|
197
|
+
# 后端类型: memory / redis / rabbitmq
|
|
198
|
+
# EVENT_BACKEND=memory
|
|
199
|
+
# EVENT_DEFAULT_BACKEND=memory
|
|
200
|
+
#
|
|
201
|
+
# Redis Pub/Sub 后端:
|
|
202
|
+
# EVENT_DEFAULT_BACKEND=redis
|
|
203
|
+
# EVENT_DEFAULT_URL=redis://localhost:6379/5
|
|
204
|
+
# EVENT_DEFAULT_KEY_PREFIX=events:
|
|
205
|
+
#
|
|
206
|
+
# RabbitMQ 后端:
|
|
207
|
+
# EVENT_DEFAULT_BACKEND=rabbitmq
|
|
208
|
+
# EVENT_DEFAULT_URL=amqp://guest:guest@localhost:5672/
|
|
147
209
|
# EVENT_EXCHANGE_NAME=aury.events
|
|
210
|
+
# EVENT_DEFAULT_EXCHANGE_TYPE=topic
|
|
211
|
+
#
|
|
212
|
+
# 多实例示例 (domain):
|
|
213
|
+
# EVENT_DOMAIN_BACKEND=rabbitmq
|
|
214
|
+
# EVENT_DOMAIN_URL=amqp://guest:guest@localhost:5672/
|
|
215
|
+
# EVENT_DOMAIN_EXCHANGE_NAME=domain.events
|
|
148
216
|
|
|
149
217
|
# =============================================================================
|
|
150
218
|
# 数据库迁移配置 (MIGRATION_)
|
|
@@ -5,7 +5,6 @@ from typing import Any
|
|
|
5
5
|
|
|
6
6
|
from starlette.requests import Request
|
|
7
7
|
|
|
8
|
-
|
|
9
8
|
try: # pragma: no cover
|
|
10
9
|
from sqladmin.authentication import AuthenticationBackend as _SQLAdminAuthenticationBackend
|
|
11
10
|
except Exception: # pragma: no cover
|
|
@@ -14,7 +13,7 @@ except Exception: # pragma: no cover
|
|
|
14
13
|
|
|
15
14
|
def _require_sqladmin():
|
|
16
15
|
try:
|
|
17
|
-
from sqladmin.authentication import AuthenticationBackend
|
|
16
|
+
from sqladmin.authentication import AuthenticationBackend
|
|
18
17
|
except Exception as exc: # pragma: no cover
|
|
19
18
|
raise ImportError(
|
|
20
19
|
"未安装 sqladmin。请先安装: uv add \"aury-boot[admin]\" 或 uv add sqladmin"
|
|
@@ -118,7 +117,7 @@ def wrap_authenticate(
|
|
|
118
117
|
from sqladmin.authentication import AuthenticationBackend
|
|
119
118
|
|
|
120
119
|
class _Wrapped(AuthenticationBackend):
|
|
121
|
-
async def login(self, request: Request) -> bool:
|
|
120
|
+
async def login(self, request: Request) -> bool:
|
|
122
121
|
# 默认不提供登录页逻辑;用户可自己实现更复杂版本
|
|
123
122
|
return False
|
|
124
123
|
|
|
@@ -14,7 +14,7 @@ from .utils import import_from_string
|
|
|
14
14
|
|
|
15
15
|
def _require_sqladmin():
|
|
16
16
|
try:
|
|
17
|
-
from sqladmin import Admin
|
|
17
|
+
from sqladmin import Admin
|
|
18
18
|
except Exception as exc: # pragma: no cover
|
|
19
19
|
raise ImportError(
|
|
20
20
|
"未安装 sqladmin。请先安装: uv add \"aury-boot[admin]\" 或 uv add sqladmin"
|
|
@@ -119,7 +119,54 @@ class AuditableStateMixin:
|
|
|
119
119
|
|
|
120
120
|
|
|
121
121
|
class VersionMixin:
|
|
122
|
-
"""乐观锁版本控制 Mixin
|
|
122
|
+
"""乐观锁版本控制 Mixin。
|
|
123
|
+
|
|
124
|
+
用于防止并发更新冲突。每次更新时会检查 version 是否一致,
|
|
125
|
+
如果不一致则抛出 VersionConflictError。
|
|
126
|
+
|
|
127
|
+
工作原理:
|
|
128
|
+
1. 读取记录时获取当前 version
|
|
129
|
+
2. 更新时检查 version 是否与读取时一致
|
|
130
|
+
3. 如果一致,version + 1 并完成更新
|
|
131
|
+
4. 如果不一致,抛出 VersionConflictError
|
|
132
|
+
|
|
133
|
+
使用场景:
|
|
134
|
+
- 库存管理(防止超卖)
|
|
135
|
+
- 订单状态更新(防止重复支付)
|
|
136
|
+
- 文档编辑(防止覆盖他人修改)
|
|
137
|
+
|
|
138
|
+
示例:
|
|
139
|
+
class Product(IDMixin, VersionMixin, Base):
|
|
140
|
+
__tablename__ = "products"
|
|
141
|
+
name: Mapped[str]
|
|
142
|
+
stock: Mapped[int]
|
|
143
|
+
|
|
144
|
+
# 更新时自动检查版本
|
|
145
|
+
product = await repo.get(1) # version=1
|
|
146
|
+
product.stock -= 1
|
|
147
|
+
await repo.update(product) # version 自动变为 2
|
|
148
|
+
|
|
149
|
+
# 并发更新时抛出异常
|
|
150
|
+
# 线程 A: product.version = 1, 准备更新
|
|
151
|
+
# 线程 B: 已经更新,version = 2
|
|
152
|
+
# 线程 A: await repo.update(product) -> VersionConflictError
|
|
153
|
+
|
|
154
|
+
异常处理:
|
|
155
|
+
from aury.boot.domain.exceptions import VersionConflictError
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
await repo.update(product)
|
|
159
|
+
except VersionConflictError as e:
|
|
160
|
+
# 重新加载数据并重试
|
|
161
|
+
product = await repo.get(product.id)
|
|
162
|
+
# ... 重新应用业务逻辑 ...
|
|
163
|
+
await repo.update(product)
|
|
164
|
+
|
|
165
|
+
注意:
|
|
166
|
+
- 乐观锁适用于读多写少的场景
|
|
167
|
+
- 高并发写入场景建议使用悲观锁 (QueryBuilder.for_update())
|
|
168
|
+
- BaseRepository.update() 已自动支持乐观锁检查
|
|
169
|
+
"""
|
|
123
170
|
|
|
124
171
|
version: Mapped[int] = mapped_column(
|
|
125
172
|
Integer,
|
|
@@ -75,10 +75,104 @@ class SortParams(BaseModel):
|
|
|
75
75
|
"""排序参数。
|
|
76
76
|
|
|
77
77
|
定义排序的字段和方向。
|
|
78
|
+
|
|
79
|
+
支持两种语法:
|
|
80
|
+
- 简洁语法: "-created_at" (前缀 - 表示降序)
|
|
81
|
+
- 完整语法: "created_at:desc"
|
|
82
|
+
|
|
83
|
+
示例:
|
|
84
|
+
# 从字符串解析
|
|
85
|
+
sort_params = SortParams.from_string("-created_at,priority")
|
|
86
|
+
sort_params = SortParams.from_string("created_at:desc,priority:asc")
|
|
87
|
+
|
|
88
|
+
# 带白名单验证
|
|
89
|
+
sort_params = SortParams.from_string(
|
|
90
|
+
"-created_at",
|
|
91
|
+
allowed_fields={"id", "created_at", "priority"}
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# 程序化构建
|
|
95
|
+
sort_params = SortParams(sorts=[("-created_at",), ("priority", "asc")])
|
|
78
96
|
"""
|
|
79
97
|
|
|
80
98
|
sorts: list[tuple[str, str]] = Field(default_factory=list, description="排序字段列表")
|
|
81
99
|
|
|
100
|
+
@classmethod
|
|
101
|
+
def from_string(
|
|
102
|
+
cls,
|
|
103
|
+
sort_str: str | None,
|
|
104
|
+
*,
|
|
105
|
+
allowed_fields: set[str] | None = None,
|
|
106
|
+
default_direction: str = "desc",
|
|
107
|
+
) -> SortParams:
|
|
108
|
+
"""从字符串解析排序参数。
|
|
109
|
+
|
|
110
|
+
支持两种语法:
|
|
111
|
+
- 简洁语法: "-created_at" (前缀 - 表示降序)
|
|
112
|
+
- 完整语法: "created_at:desc"
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
sort_str: 排序字符串,如 "-created_at,priority" 或 "created_at:desc,priority:asc"
|
|
116
|
+
allowed_fields: 允许的字段白名单(可选,用于安全校验)
|
|
117
|
+
default_direction: 默认排序方向(当不指定方向时使用)
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
SortParams: 排序参数对象
|
|
121
|
+
|
|
122
|
+
Raises:
|
|
123
|
+
ValueError: 字段不在白名单中 或 方向无效
|
|
124
|
+
|
|
125
|
+
示例:
|
|
126
|
+
>>> SortParams.from_string("-created_at,priority")
|
|
127
|
+
SortParams(sorts=[('created_at', 'desc'), ('priority', 'desc')])
|
|
128
|
+
|
|
129
|
+
>>> SortParams.from_string("created_at:desc,priority:asc")
|
|
130
|
+
SortParams(sorts=[('created_at', 'desc'), ('priority', 'asc')])
|
|
131
|
+
|
|
132
|
+
>>> SortParams.from_string("-id", allowed_fields={"id", "name"})
|
|
133
|
+
SortParams(sorts=[('id', 'desc')])
|
|
134
|
+
|
|
135
|
+
>>> SortParams.from_string("-invalid", allowed_fields={"id", "name"})
|
|
136
|
+
ValueError: 不允许的排序字段: invalid,允许的字段: id, name
|
|
137
|
+
"""
|
|
138
|
+
if not sort_str:
|
|
139
|
+
return cls(sorts=[])
|
|
140
|
+
|
|
141
|
+
sorts = []
|
|
142
|
+
for part in sort_str.split(","):
|
|
143
|
+
part = part.strip()
|
|
144
|
+
if not part:
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
# 解析字段和方向
|
|
148
|
+
if part.startswith("-"):
|
|
149
|
+
# 简洁语法: -created_at
|
|
150
|
+
field = part[1:]
|
|
151
|
+
direction = "desc"
|
|
152
|
+
elif ":" in part:
|
|
153
|
+
# 完整语法: created_at:desc
|
|
154
|
+
field, direction = part.split(":", 1)
|
|
155
|
+
else:
|
|
156
|
+
# 无方向指示,使用默认
|
|
157
|
+
field = part
|
|
158
|
+
direction = default_direction
|
|
159
|
+
|
|
160
|
+
field = field.strip()
|
|
161
|
+
direction = direction.strip().lower()
|
|
162
|
+
|
|
163
|
+
# 字段白名单校验
|
|
164
|
+
if allowed_fields and field not in allowed_fields:
|
|
165
|
+
allowed_list = ", ".join(sorted(allowed_fields))
|
|
166
|
+
raise ValueError(f"不允许的排序字段: {field},允许的字段: {allowed_list}")
|
|
167
|
+
|
|
168
|
+
# 方向校验
|
|
169
|
+
if direction not in ("asc", "desc"):
|
|
170
|
+
raise ValueError(f"排序方向必须是 'asc' 或 'desc',得到: {direction}")
|
|
171
|
+
|
|
172
|
+
sorts.append((field, direction))
|
|
173
|
+
|
|
174
|
+
return cls(sorts=sorts)
|
|
175
|
+
|
|
82
176
|
def add_sort(self, field: str, direction: str = "asc") -> None:
|
|
83
177
|
"""添加排序字段。
|
|
84
178
|
|
|
@@ -17,7 +17,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
17
17
|
from aury.boot.common.logging import logger
|
|
18
18
|
from aury.boot.domain.exceptions import VersionConflictError
|
|
19
19
|
from aury.boot.domain.models import GUID, Base
|
|
20
|
-
from aury.boot.domain.transaction import _transaction_depth
|
|
21
20
|
from aury.boot.domain.pagination import (
|
|
22
21
|
PaginationParams,
|
|
23
22
|
PaginationResult,
|
|
@@ -25,6 +24,7 @@ from aury.boot.domain.pagination import (
|
|
|
25
24
|
)
|
|
26
25
|
from aury.boot.domain.repository.interface import IRepository
|
|
27
26
|
from aury.boot.domain.repository.query_builder import QueryBuilder
|
|
27
|
+
from aury.boot.domain.transaction import _transaction_depth
|
|
28
28
|
|
|
29
29
|
if TYPE_CHECKING:
|
|
30
30
|
from typing import Self
|
|
@@ -8,7 +8,7 @@ from __future__ import annotations
|
|
|
8
8
|
from abc import ABC, abstractmethod
|
|
9
9
|
from typing import TYPE_CHECKING, Any
|
|
10
10
|
|
|
11
|
-
from aury.boot.domain.models import
|
|
11
|
+
from aury.boot.domain.models import GUID, Base
|
|
12
12
|
from aury.boot.domain.pagination import PaginationParams, PaginationResult, SortParams
|
|
13
13
|
|
|
14
14
|
if TYPE_CHECKING:
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
-
import contextvars
|
|
13
12
|
from collections.abc import AsyncGenerator, Awaitable, Callable
|
|
14
13
|
from contextlib import asynccontextmanager
|
|
14
|
+
import contextvars
|
|
15
15
|
from functools import wraps
|
|
16
16
|
from inspect import signature
|
|
17
17
|
from typing import Any
|
|
@@ -24,9 +24,9 @@ from aury.boot.domain.exceptions import TransactionRequiredError
|
|
|
24
24
|
# 用于跟踪嵌套事务的上下文变量
|
|
25
25
|
_transaction_depth: contextvars.ContextVar[int] = contextvars.ContextVar("transaction_depth", default=0)
|
|
26
26
|
|
|
27
|
-
# 用于存储 on_commit
|
|
28
|
-
_on_commit_callbacks: contextvars.ContextVar[list[Callable[[], Any] | Callable[[], Awaitable[Any]]]] = (
|
|
29
|
-
contextvars.ContextVar("on_commit_callbacks", default=
|
|
27
|
+
# 用于存储 on_commit 回调的上下文变量(不设置 default,避免可变对象问题)
|
|
28
|
+
_on_commit_callbacks: contextvars.ContextVar[list[Callable[[], Any] | Callable[[], Awaitable[Any]]] | None] = (
|
|
29
|
+
contextvars.ContextVar("on_commit_callbacks", default=None)
|
|
30
30
|
)
|
|
31
31
|
|
|
32
32
|
|
|
@@ -51,8 +51,7 @@ def on_commit(callback: Callable[[], Any] | Callable[[], Awaitable[Any]]) -> Non
|
|
|
51
51
|
- 嵌套事务中注册的回调,只在最外层事务提交后执行
|
|
52
52
|
"""
|
|
53
53
|
callbacks = _on_commit_callbacks.get()
|
|
54
|
-
|
|
55
|
-
if not callbacks:
|
|
54
|
+
if callbacks is None:
|
|
56
55
|
callbacks = []
|
|
57
56
|
_on_commit_callbacks.set(callbacks)
|
|
58
57
|
callbacks.append(callback)
|
|
@@ -65,7 +64,7 @@ async def _execute_on_commit_callbacks() -> None:
|
|
|
65
64
|
return
|
|
66
65
|
|
|
67
66
|
# 清空回调列表
|
|
68
|
-
_on_commit_callbacks.set(
|
|
67
|
+
_on_commit_callbacks.set(None)
|
|
69
68
|
|
|
70
69
|
for callback in callbacks:
|
|
71
70
|
try:
|
|
@@ -106,7 +105,7 @@ async def transactional_context(session: AsyncSession, auto_commit: bool = True)
|
|
|
106
105
|
|
|
107
106
|
# 最外层事务初始化回调列表
|
|
108
107
|
if is_outermost:
|
|
109
|
-
_on_commit_callbacks.set(
|
|
108
|
+
_on_commit_callbacks.set(None)
|
|
110
109
|
|
|
111
110
|
try:
|
|
112
111
|
yield session
|
|
@@ -121,7 +120,7 @@ async def transactional_context(session: AsyncSession, auto_commit: bool = True)
|
|
|
121
120
|
if is_outermost:
|
|
122
121
|
await session.rollback()
|
|
123
122
|
# 回滚时清空回调列表(不执行)
|
|
124
|
-
_on_commit_callbacks.set(
|
|
123
|
+
_on_commit_callbacks.set(None)
|
|
125
124
|
logger.error(f"事务回滚: {exc}")
|
|
126
125
|
raise
|
|
127
126
|
finally:
|