toms-fast 0.2.1__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.
- toms_fast-0.2.1.dist-info/METADATA +467 -0
- toms_fast-0.2.1.dist-info/RECORD +60 -0
- toms_fast-0.2.1.dist-info/WHEEL +4 -0
- toms_fast-0.2.1.dist-info/entry_points.txt +2 -0
- tomskit/__init__.py +0 -0
- tomskit/celery/README.md +693 -0
- tomskit/celery/__init__.py +4 -0
- tomskit/celery/celery.py +306 -0
- tomskit/celery/config.py +377 -0
- tomskit/cli/__init__.py +207 -0
- tomskit/cli/__main__.py +8 -0
- tomskit/cli/scaffold.py +123 -0
- tomskit/cli/templates/__init__.py +42 -0
- tomskit/cli/templates/base.py +348 -0
- tomskit/cli/templates/celery.py +101 -0
- tomskit/cli/templates/extensions.py +213 -0
- tomskit/cli/templates/fastapi.py +400 -0
- tomskit/cli/templates/migrations.py +281 -0
- tomskit/cli/templates_config.py +122 -0
- tomskit/logger/README.md +466 -0
- tomskit/logger/__init__.py +4 -0
- tomskit/logger/config.py +106 -0
- tomskit/logger/logger.py +290 -0
- tomskit/py.typed +0 -0
- tomskit/redis/README.md +462 -0
- tomskit/redis/__init__.py +6 -0
- tomskit/redis/config.py +85 -0
- tomskit/redis/redis_pool.py +87 -0
- tomskit/redis/redis_sync.py +66 -0
- tomskit/server/__init__.py +47 -0
- tomskit/server/config.py +117 -0
- tomskit/server/exceptions.py +412 -0
- tomskit/server/middleware.py +371 -0
- tomskit/server/parser.py +312 -0
- tomskit/server/resource.py +464 -0
- tomskit/server/server.py +276 -0
- tomskit/server/type.py +263 -0
- tomskit/sqlalchemy/README.md +590 -0
- tomskit/sqlalchemy/__init__.py +20 -0
- tomskit/sqlalchemy/config.py +125 -0
- tomskit/sqlalchemy/database.py +125 -0
- tomskit/sqlalchemy/pagination.py +359 -0
- tomskit/sqlalchemy/property.py +19 -0
- tomskit/sqlalchemy/sqlalchemy.py +131 -0
- tomskit/sqlalchemy/types.py +32 -0
- tomskit/task/README.md +67 -0
- tomskit/task/__init__.py +4 -0
- tomskit/task/task_manager.py +124 -0
- tomskit/tools/README.md +63 -0
- tomskit/tools/__init__.py +18 -0
- tomskit/tools/config.py +70 -0
- tomskit/tools/warnings.py +37 -0
- tomskit/tools/woker.py +81 -0
- tomskit/utils/README.md +666 -0
- tomskit/utils/README_SERIALIZER.md +644 -0
- tomskit/utils/__init__.py +35 -0
- tomskit/utils/fields.py +434 -0
- tomskit/utils/marshal_utils.py +137 -0
- tomskit/utils/response_utils.py +13 -0
- tomskit/utils/serializers.py +447 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FastAPI 模板模块
|
|
3
|
+
包含所有 FastAPI 相关的文件模板
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Callable
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_fastapi_templates(project_name: str) -> dict[str, Callable[[], str]]:
|
|
10
|
+
"""获取 FastAPI 模板"""
|
|
11
|
+
return {
|
|
12
|
+
"main_py": lambda: f'''"""
|
|
13
|
+
{project_name} 应用入口
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from dotenv import load_dotenv
|
|
19
|
+
|
|
20
|
+
from tomskit.server import FastApp
|
|
21
|
+
from app.config import setup_config
|
|
22
|
+
from app.middleware import setup_middleware
|
|
23
|
+
from app.controllers.users.module import init_user_module
|
|
24
|
+
from extensions import init_all_extensions
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# 加载环境变量
|
|
28
|
+
env_path = Path(__file__).parent / ".env"
|
|
29
|
+
if env_path.exists():
|
|
30
|
+
load_dotenv(env_path)
|
|
31
|
+
else:
|
|
32
|
+
print("⚠️ 警告: .env 文件不存在,请从 .env.example 复制并配置")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# 创建应用实例
|
|
36
|
+
app = FastApp(
|
|
37
|
+
title="{project_name}",
|
|
38
|
+
description="基于 toms-fast 的 FastAPI 应用",
|
|
39
|
+
version="0.1.0"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# 设置应用根路径
|
|
43
|
+
app.set_app_root_path(__file__)
|
|
44
|
+
|
|
45
|
+
# 初始化配置
|
|
46
|
+
setup_config(app)
|
|
47
|
+
|
|
48
|
+
# 配置中间件
|
|
49
|
+
setup_middleware(app)
|
|
50
|
+
|
|
51
|
+
# 统一初始化所有扩展(logger, database, redis 等)
|
|
52
|
+
init_all_extensions(app)
|
|
53
|
+
|
|
54
|
+
# 注册控制器
|
|
55
|
+
init_user_module(app)
|
|
56
|
+
|
|
57
|
+
# 健康检查端点
|
|
58
|
+
@app.get("/health")
|
|
59
|
+
async def health_check():
|
|
60
|
+
"""健康检查"""
|
|
61
|
+
return {{"status": "ok", "service": "{project_name}"}}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
if __name__ == "__main__":
|
|
65
|
+
import uvicorn
|
|
66
|
+
uvicorn.run(
|
|
67
|
+
"main:app",
|
|
68
|
+
host="0.0.0.0",
|
|
69
|
+
port=8000,
|
|
70
|
+
reload=True
|
|
71
|
+
)
|
|
72
|
+
''',
|
|
73
|
+
|
|
74
|
+
"app_init_py": lambda: "",
|
|
75
|
+
|
|
76
|
+
"config_py": lambda: '''"""
|
|
77
|
+
应用配置管理
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
from tomskit.server import FastApp
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def setup_config(app: FastApp):
|
|
84
|
+
"""设置应用配置"""
|
|
85
|
+
# 从环境变量加载配置
|
|
86
|
+
# 配置会自动从环境变量读取(通过 pydantic-settings)
|
|
87
|
+
pass
|
|
88
|
+
''',
|
|
89
|
+
|
|
90
|
+
"middleware_init_py": lambda: '''"""
|
|
91
|
+
中间件统一注册
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
from tomskit.server import FastApp
|
|
95
|
+
|
|
96
|
+
from . import request_id, resource_cleanup
|
|
97
|
+
# 在这里导入更多中间件模块
|
|
98
|
+
# from . import cors, auth, rate_limit
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def setup_middleware(app: FastApp):
|
|
102
|
+
"""
|
|
103
|
+
统一注册所有中间件
|
|
104
|
+
|
|
105
|
+
中间件执行顺序(从外到内,按注册顺序):
|
|
106
|
+
1. request_id - 请求 ID 追踪(最外层)
|
|
107
|
+
2. resource_cleanup - 资源清理(最内层)
|
|
108
|
+
|
|
109
|
+
注意:中间件的注册顺序很重要,后注册的中间件会更靠近应用核心
|
|
110
|
+
"""
|
|
111
|
+
# 按顺序注册中间件
|
|
112
|
+
request_id.setup(app)
|
|
113
|
+
resource_cleanup.setup(app)
|
|
114
|
+
|
|
115
|
+
# 注册更多中间件
|
|
116
|
+
# cors.setup(app)
|
|
117
|
+
# auth.setup(app)
|
|
118
|
+
# rate_limit.setup(app)
|
|
119
|
+
''',
|
|
120
|
+
|
|
121
|
+
"middleware_request_id_py": lambda: '''"""
|
|
122
|
+
请求 ID 追踪中间件
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
from tomskit.server import FastApp, RequestIDMiddleware
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def setup(app: FastApp):
|
|
129
|
+
"""
|
|
130
|
+
配置请求 ID 追踪中间件
|
|
131
|
+
|
|
132
|
+
功能:
|
|
133
|
+
- 自动处理 X-Request-ID 请求头
|
|
134
|
+
- 如果请求中没有 X-Request-ID,自动生成 UUID
|
|
135
|
+
- 将请求 ID 设置到日志上下文中,用于分布式追踪
|
|
136
|
+
- 在响应头中添加 X-Request-ID
|
|
137
|
+
"""
|
|
138
|
+
app.add_middleware(RequestIDMiddleware)
|
|
139
|
+
''',
|
|
140
|
+
|
|
141
|
+
"middleware_resource_cleanup_py": lambda: '''"""
|
|
142
|
+
资源清理中间件
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
from tomskit.server import FastApp, ResourceCleanupMiddleware
|
|
146
|
+
from tomskit.sqlalchemy.database import DatabaseCleanupStrategy
|
|
147
|
+
from tomskit.redis.redis_pool import RedisCleanupStrategy
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def setup(app: FastApp):
|
|
151
|
+
"""
|
|
152
|
+
配置资源清理中间件
|
|
153
|
+
|
|
154
|
+
功能:
|
|
155
|
+
- 自动清理数据库会话,防止资源泄漏
|
|
156
|
+
- 自动清理 Redis 连接,防止资源泄漏
|
|
157
|
+
- 在请求完成后自动执行清理,即使发生异常也会清理
|
|
158
|
+
"""
|
|
159
|
+
app.add_middleware(
|
|
160
|
+
ResourceCleanupMiddleware,
|
|
161
|
+
strategies=[
|
|
162
|
+
DatabaseCleanupStrategy(),
|
|
163
|
+
RedisCleanupStrategy(),
|
|
164
|
+
]
|
|
165
|
+
)
|
|
166
|
+
''',
|
|
167
|
+
|
|
168
|
+
"controllers_init_py": lambda: '''"""
|
|
169
|
+
Controllers 目录
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
''',
|
|
173
|
+
|
|
174
|
+
"users_init_py": lambda: '''"""
|
|
175
|
+
用户控制器模块
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
from .module import init_user_module
|
|
179
|
+
|
|
180
|
+
__all__ = ["init_user_module"]
|
|
181
|
+
''',
|
|
182
|
+
|
|
183
|
+
"users_resources_py": lambda: '''"""
|
|
184
|
+
用户资源 API
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
from fastapi import Request, HTTPException
|
|
188
|
+
|
|
189
|
+
from tomskit.server import Resource, api_doc, register_resource
|
|
190
|
+
from .schemas import UserResponse, UserCreate, UserUpdate
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@register_resource(module="users", path="/users", tags=["用户管理"])
|
|
194
|
+
class UserResource(Resource):
|
|
195
|
+
"""用户资源"""
|
|
196
|
+
|
|
197
|
+
@api_doc(
|
|
198
|
+
summary="获取用户列表",
|
|
199
|
+
description="获取所有用户列表,支持分页",
|
|
200
|
+
response_model=list[UserResponse],
|
|
201
|
+
responses={{
|
|
202
|
+
200: "成功",
|
|
203
|
+
500: "服务器错误"
|
|
204
|
+
}}
|
|
205
|
+
)
|
|
206
|
+
async def get(self, request: Request):
|
|
207
|
+
"""获取用户列表"""
|
|
208
|
+
# TODO: 从数据库获取用户列表
|
|
209
|
+
return [
|
|
210
|
+
{{"id": 1, "name": "Alice", "email": "alice@example.com"}},
|
|
211
|
+
{{"id": 2, "name": "Bob", "email": "bob@example.com"}}
|
|
212
|
+
]
|
|
213
|
+
|
|
214
|
+
@api_doc(
|
|
215
|
+
summary="创建用户",
|
|
216
|
+
description="创建新用户",
|
|
217
|
+
response_model=UserResponse,
|
|
218
|
+
status_code=201,
|
|
219
|
+
responses={{
|
|
220
|
+
201: "用户创建成功",
|
|
221
|
+
400: "请求参数错误",
|
|
222
|
+
409: "用户已存在"
|
|
223
|
+
}}
|
|
224
|
+
)
|
|
225
|
+
async def post(self, request: Request):
|
|
226
|
+
"""创建用户"""
|
|
227
|
+
data = await request.json()
|
|
228
|
+
# TODO: 验证数据并创建用户
|
|
229
|
+
return {{
|
|
230
|
+
"id": 3,
|
|
231
|
+
"name": data.get("name", ""),
|
|
232
|
+
"email": data.get("email", "")
|
|
233
|
+
}}
|
|
234
|
+
|
|
235
|
+
@api_doc(
|
|
236
|
+
summary="获取用户详情",
|
|
237
|
+
description="根据用户 ID 获取用户详情",
|
|
238
|
+
response_model=UserResponse,
|
|
239
|
+
path="/users/{{user_id}}",
|
|
240
|
+
responses={{
|
|
241
|
+
200: "成功",
|
|
242
|
+
404: "用户不存在"
|
|
243
|
+
}}
|
|
244
|
+
)
|
|
245
|
+
async def get(self, request: Request):
|
|
246
|
+
"""获取用户详情"""
|
|
247
|
+
user_id = request.path_params.get("user_id")
|
|
248
|
+
if not user_id:
|
|
249
|
+
raise HTTPException(status_code=404, detail="用户不存在")
|
|
250
|
+
# TODO: 从数据库获取用户
|
|
251
|
+
return {{"id": int(user_id), "name": "Alice", "email": "alice@example.com"}}
|
|
252
|
+
|
|
253
|
+
@api_doc(
|
|
254
|
+
summary="更新用户",
|
|
255
|
+
description="更新用户信息",
|
|
256
|
+
response_model=UserResponse,
|
|
257
|
+
path="/users/{{user_id}}",
|
|
258
|
+
responses={{
|
|
259
|
+
200: "更新成功",
|
|
260
|
+
404: "用户不存在",
|
|
261
|
+
400: "请求参数错误"
|
|
262
|
+
}}
|
|
263
|
+
)
|
|
264
|
+
async def put(self, request: Request):
|
|
265
|
+
"""更新用户"""
|
|
266
|
+
user_id = request.path_params.get("user_id")
|
|
267
|
+
data = await request.json()
|
|
268
|
+
# TODO: 更新用户信息
|
|
269
|
+
return {{"id": int(user_id), "name": data.get("name", ""), "email": data.get("email", "")}}
|
|
270
|
+
|
|
271
|
+
@api_doc(
|
|
272
|
+
summary="删除用户",
|
|
273
|
+
description="删除指定用户",
|
|
274
|
+
path="/users/{{user_id}}",
|
|
275
|
+
responses={{
|
|
276
|
+
204: "删除成功",
|
|
277
|
+
404: "用户不存在"
|
|
278
|
+
}}
|
|
279
|
+
)
|
|
280
|
+
async def delete(self, request: Request):
|
|
281
|
+
"""删除用户"""
|
|
282
|
+
user_id = request.path_params.get("user_id")
|
|
283
|
+
# TODO: 删除用户
|
|
284
|
+
return None
|
|
285
|
+
''',
|
|
286
|
+
|
|
287
|
+
"users_schemas_py": lambda: '''"""
|
|
288
|
+
用户数据模型
|
|
289
|
+
"""
|
|
290
|
+
|
|
291
|
+
from pydantic import BaseModel, EmailStr
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
class UserBase(BaseModel):
|
|
295
|
+
"""用户基础模型"""
|
|
296
|
+
name: str
|
|
297
|
+
email: EmailStr
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
class UserCreate(UserBase):
|
|
301
|
+
"""创建用户请求模型"""
|
|
302
|
+
pass
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
class UserUpdate(BaseModel):
|
|
306
|
+
"""更新用户请求模型"""
|
|
307
|
+
name: str | None = None
|
|
308
|
+
email: EmailStr | None = None
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
class UserResponse(UserBase):
|
|
312
|
+
"""用户响应模型"""
|
|
313
|
+
id: int
|
|
314
|
+
|
|
315
|
+
class Config:
|
|
316
|
+
from_attributes = True
|
|
317
|
+
''',
|
|
318
|
+
|
|
319
|
+
"users_module_py": lambda: '''"""
|
|
320
|
+
用户控制器初始化
|
|
321
|
+
"""
|
|
322
|
+
|
|
323
|
+
from tomskit.server import FastApp, FastModule
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def init_user_module(app: FastApp):
|
|
327
|
+
"""初始化用户控制器"""
|
|
328
|
+
# 创建用户控制器模块
|
|
329
|
+
user_module = FastModule(name="users")
|
|
330
|
+
|
|
331
|
+
# 创建路由(前缀会自动添加到所有资源路径)
|
|
332
|
+
user_module.create_router(prefix="/api/v1")
|
|
333
|
+
|
|
334
|
+
# 自动注册所有标记为 "users" 模块的资源
|
|
335
|
+
user_module.auto_register_resources()
|
|
336
|
+
|
|
337
|
+
# 配置 CORS(如果需要)
|
|
338
|
+
# user_module.setup_cors(
|
|
339
|
+
# allow_origins=["http://localhost:3000"],
|
|
340
|
+
# allow_credentials=True
|
|
341
|
+
# )
|
|
342
|
+
|
|
343
|
+
# 挂载控制器到主应用
|
|
344
|
+
app.mount("/", user_module)
|
|
345
|
+
|
|
346
|
+
print("✅ 用户控制器初始化成功")
|
|
347
|
+
''',
|
|
348
|
+
|
|
349
|
+
"models_init_py": lambda: '''"""
|
|
350
|
+
数据库模型模块
|
|
351
|
+
导出所有模型和 Base
|
|
352
|
+
"""
|
|
353
|
+
|
|
354
|
+
from tomskit.sqlalchemy import SQLAlchemy
|
|
355
|
+
|
|
356
|
+
# 导入所有模型,确保它们被注册到 Base.metadata
|
|
357
|
+
from .user import User # noqa: F401
|
|
358
|
+
|
|
359
|
+
# 导出 Base 供 Alembic 使用
|
|
360
|
+
Base = SQLAlchemy.Model
|
|
361
|
+
''',
|
|
362
|
+
|
|
363
|
+
"user_model_py": lambda: '''"""
|
|
364
|
+
用户数据库模型
|
|
365
|
+
"""
|
|
366
|
+
|
|
367
|
+
from sqlalchemy import Column, Integer, String
|
|
368
|
+
from tomskit.sqlalchemy import SQLAlchemy
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
class User(SQLAlchemy.Model):
|
|
372
|
+
"""用户模型"""
|
|
373
|
+
__tablename__ = "users"
|
|
374
|
+
|
|
375
|
+
id = Column(Integer, primary_key=True, index=True)
|
|
376
|
+
name = Column(String(100), nullable=False)
|
|
377
|
+
email = Column(String(255), unique=True, nullable=False, index=True)
|
|
378
|
+
|
|
379
|
+
def __repr__(self):
|
|
380
|
+
return f"<User(id={self.id}, name={self.name}, email={self.email})>"
|
|
381
|
+
''',
|
|
382
|
+
|
|
383
|
+
"test_users_py": lambda: '''"""
|
|
384
|
+
用户模块测试
|
|
385
|
+
"""
|
|
386
|
+
|
|
387
|
+
import pytest
|
|
388
|
+
from httpx import AsyncClient
|
|
389
|
+
from main import app
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
@pytest.mark.asyncio
|
|
393
|
+
async def test_get_users():
|
|
394
|
+
"""测试获取用户列表"""
|
|
395
|
+
async with AsyncClient(app=app, base_url="http://test") as client:
|
|
396
|
+
response = await client.get("/api/v1/users")
|
|
397
|
+
assert response.status_code == 200
|
|
398
|
+
assert isinstance(response.json(), list)
|
|
399
|
+
''',
|
|
400
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""
|
|
2
|
+
数据库迁移模板模块
|
|
3
|
+
包含 Alembic 相关的文件模板
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Callable
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_migrations_templates(project_name: str) -> dict[str, Callable[[], str]]:
|
|
10
|
+
"""获取数据库迁移模板"""
|
|
11
|
+
return {
|
|
12
|
+
"alembic_ini": lambda: f'''# A generic, {project_name} database configuration.
|
|
13
|
+
|
|
14
|
+
[alembic]
|
|
15
|
+
# path to migration scripts
|
|
16
|
+
script_location = .
|
|
17
|
+
|
|
18
|
+
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
|
19
|
+
# Uncomment the line below if you want the files to be prepended with date and time
|
|
20
|
+
# file_template = %%%%Y%%%%m%%%%d_%%%%H%%%%M%%%%S_%%%(rev)s_%%%(slug)s
|
|
21
|
+
|
|
22
|
+
# sys.path path, will be prepended to sys.path if present.
|
|
23
|
+
# defaults to the current working directory.
|
|
24
|
+
prepend_sys_path = .
|
|
25
|
+
|
|
26
|
+
# timezone to use when rendering the date within the migration file
|
|
27
|
+
# as well as the filename.
|
|
28
|
+
# If specified, requires the python-dateutil library that can be
|
|
29
|
+
# installed by adding `alembic[tz]` to the pip requirements
|
|
30
|
+
# string value is passed to dateutil.tz.gettz()
|
|
31
|
+
# leave blank for localtime
|
|
32
|
+
# timezone =
|
|
33
|
+
|
|
34
|
+
# max length of characters to apply to the
|
|
35
|
+
# "slug" field
|
|
36
|
+
# truncate_slug_length = 40
|
|
37
|
+
|
|
38
|
+
# set to 'true' to run the environment during
|
|
39
|
+
# the 'revision' command, regardless of autogenerate
|
|
40
|
+
# revision_environment = false
|
|
41
|
+
|
|
42
|
+
# set to 'true' to allow .pyc and .pyo files without
|
|
43
|
+
# a source .py file to be detected as revisions in the
|
|
44
|
+
# versions/ directory
|
|
45
|
+
# sourceless = false
|
|
46
|
+
|
|
47
|
+
# version location specification; This defaults
|
|
48
|
+
# to migrations/versions. When using multiple version
|
|
49
|
+
# directories, initial revisions must be specified with --version-path.
|
|
50
|
+
# The path separator used here should be the separator specified by "version_path_separator" below.
|
|
51
|
+
# version_locations = %%(here)s/bar:%%(here)s/bat:migrations/versions
|
|
52
|
+
|
|
53
|
+
# version path separator; As mentioned above, this is the character used to split
|
|
54
|
+
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
|
|
55
|
+
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
|
|
56
|
+
# Valid values for version_path_separator are:
|
|
57
|
+
#
|
|
58
|
+
# version_path_separator = :
|
|
59
|
+
# version_path_separator = ;
|
|
60
|
+
# version_path_separator = space
|
|
61
|
+
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
|
|
62
|
+
|
|
63
|
+
# set to 'true' to search source files recursively
|
|
64
|
+
# in each "version_locations" directory
|
|
65
|
+
# new in Alembic version 1.10
|
|
66
|
+
# recursive_version_locations = false
|
|
67
|
+
|
|
68
|
+
# the output encoding used when revision files
|
|
69
|
+
# are written from script.py.mako
|
|
70
|
+
# output_encoding = utf-8
|
|
71
|
+
|
|
72
|
+
sqlalchemy.url = driver://user:pass@localhost/dbname
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
[post_write_hooks]
|
|
76
|
+
# post_write_hooks defines scripts or Python functions that are run
|
|
77
|
+
# on newly generated revision scripts. See the documentation for further
|
|
78
|
+
# detail and examples
|
|
79
|
+
|
|
80
|
+
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
|
81
|
+
# hooks = black
|
|
82
|
+
# black.type = console_scripts
|
|
83
|
+
# black.entrypoint = black
|
|
84
|
+
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
|
85
|
+
|
|
86
|
+
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
|
|
87
|
+
# hooks = ruff
|
|
88
|
+
# ruff.type = exec
|
|
89
|
+
# ruff.executable = %%(here)s/.venv/bin/ruff
|
|
90
|
+
# ruff.options = --fix REVISION_SCRIPT_FILENAME
|
|
91
|
+
|
|
92
|
+
# Logging configuration
|
|
93
|
+
[loggers]
|
|
94
|
+
keys = root,sqlalchemy,alembic
|
|
95
|
+
|
|
96
|
+
[handlers]
|
|
97
|
+
keys = console
|
|
98
|
+
|
|
99
|
+
[formatters]
|
|
100
|
+
keys = generic
|
|
101
|
+
|
|
102
|
+
[logger_root]
|
|
103
|
+
level = WARN
|
|
104
|
+
handlers = console
|
|
105
|
+
qualname =
|
|
106
|
+
|
|
107
|
+
[logger_sqlalchemy]
|
|
108
|
+
level = WARN
|
|
109
|
+
handlers =
|
|
110
|
+
qualname = sqlalchemy.engine
|
|
111
|
+
|
|
112
|
+
[logger_alembic]
|
|
113
|
+
level = INFO
|
|
114
|
+
handlers =
|
|
115
|
+
qualname = alembic
|
|
116
|
+
|
|
117
|
+
[handler_console]
|
|
118
|
+
class = StreamHandler
|
|
119
|
+
args = (sys.stderr,)
|
|
120
|
+
level = NOTSET
|
|
121
|
+
formatter = generic
|
|
122
|
+
|
|
123
|
+
[formatter_generic]
|
|
124
|
+
format = %%(levelname)-5.5s [%%(name)s] %%(message)s
|
|
125
|
+
datefmt = %%%%H:%%%%M:%%%%S
|
|
126
|
+
''',
|
|
127
|
+
|
|
128
|
+
"migrations_env_py": lambda: f'''"""
|
|
129
|
+
Alembic {project_name} 环境配置文件
|
|
130
|
+
用于数据库迁移
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
import sys
|
|
134
|
+
from logging.config import fileConfig
|
|
135
|
+
from pathlib import Path
|
|
136
|
+
|
|
137
|
+
from sqlalchemy import pool
|
|
138
|
+
from sqlalchemy.engine import Connection
|
|
139
|
+
from sqlalchemy import create_engine
|
|
140
|
+
|
|
141
|
+
from alembic import context
|
|
142
|
+
|
|
143
|
+
# 添加项目根目录到 Python 路径
|
|
144
|
+
# migrations 在 backend/migrations,backend 在项目根目录下
|
|
145
|
+
project_root = Path(__file__).parent.parent # backend 目录
|
|
146
|
+
sys.path.insert(0, str(project_root))
|
|
147
|
+
|
|
148
|
+
# 导入数据库配置和模型
|
|
149
|
+
try:
|
|
150
|
+
from app.models import Base # 导入所有模型
|
|
151
|
+
target_metadata = Base.metadata
|
|
152
|
+
except ImportError:
|
|
153
|
+
# 如果模型还没有定义,创建一个空的 metadata
|
|
154
|
+
from sqlalchemy import MetaData
|
|
155
|
+
target_metadata = MetaData()
|
|
156
|
+
print("⚠️ 警告: 未找到 app.models,使用空的 metadata")
|
|
157
|
+
|
|
158
|
+
# this is the Alembic Config object, which provides
|
|
159
|
+
# access to the values within the .ini file in use.
|
|
160
|
+
config = context.config
|
|
161
|
+
|
|
162
|
+
# Interpret the config file for Python logging.
|
|
163
|
+
# This line sets up loggers basically.
|
|
164
|
+
if config.config_file_name is not None:
|
|
165
|
+
fileConfig(config.config_file_name)
|
|
166
|
+
|
|
167
|
+
# other values from the config, defined by the needs of env.py,
|
|
168
|
+
# can be acquired:
|
|
169
|
+
# my_important_option = config.get_main_option("my_important_option")
|
|
170
|
+
# ... etc.
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def get_url():
|
|
174
|
+
"""从环境变量获取数据库 URL"""
|
|
175
|
+
from dotenv import load_dotenv
|
|
176
|
+
from tomskit.sqlalchemy import DatabaseConfig
|
|
177
|
+
|
|
178
|
+
# 加载环境变量(.env 在 backend 目录下)
|
|
179
|
+
env_path = Path(__file__).parent.parent / ".env"
|
|
180
|
+
if env_path.exists():
|
|
181
|
+
load_dotenv(env_path)
|
|
182
|
+
|
|
183
|
+
# 获取数据库配置
|
|
184
|
+
db_config = DatabaseConfig()
|
|
185
|
+
# 使用同步 URI 用于 Alembic(因为 Alembic 需要同步连接)
|
|
186
|
+
return db_config.SQLALCHEMY_DATABASE_SYNC_URI
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def run_migrations_offline() -> None:
|
|
190
|
+
"""Run migrations in 'offline' mode.
|
|
191
|
+
|
|
192
|
+
This configures the context with just a URL
|
|
193
|
+
and not an Engine, though an Engine is acceptable
|
|
194
|
+
here as well. By skipping the Engine creation
|
|
195
|
+
we don't even need a DBAPI to be available.
|
|
196
|
+
|
|
197
|
+
Calls to context.execute() here emit the given string to the
|
|
198
|
+
script output.
|
|
199
|
+
|
|
200
|
+
"""
|
|
201
|
+
url = get_url()
|
|
202
|
+
context.configure(
|
|
203
|
+
url=url,
|
|
204
|
+
target_metadata=target_metadata,
|
|
205
|
+
literal_binds=True,
|
|
206
|
+
dialect_opts={{"paramstyle": "named"}},
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
with context.begin_transaction():
|
|
210
|
+
context.run_migrations()
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def do_run_migrations(connection: Connection) -> None:
|
|
214
|
+
context.configure(connection=connection, target_metadata=target_metadata)
|
|
215
|
+
|
|
216
|
+
with context.begin_transaction():
|
|
217
|
+
context.run_migrations()
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def run_migrations_online() -> None:
|
|
221
|
+
"""Run migrations in 'online' mode."""
|
|
222
|
+
|
|
223
|
+
configuration = config.get_section(config.config_ini_section)
|
|
224
|
+
configuration["sqlalchemy.url"] = get_url()
|
|
225
|
+
|
|
226
|
+
# 使用同步引擎(Alembic 需要同步连接)
|
|
227
|
+
connectable = create_engine(
|
|
228
|
+
configuration["sqlalchemy.url"],
|
|
229
|
+
poolclass=pool.NullPool,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
with connectable.connect() as connection:
|
|
233
|
+
do_run_migrations(connection)
|
|
234
|
+
|
|
235
|
+
connectable.dispose()
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
if context.is_offline_mode():
|
|
239
|
+
run_migrations_offline()
|
|
240
|
+
else:
|
|
241
|
+
run_migrations_online()
|
|
242
|
+
''',
|
|
243
|
+
|
|
244
|
+
"migrations_script_py_mako": lambda: '''"""${message}
|
|
245
|
+
|
|
246
|
+
Revision ID: ${up_revision}
|
|
247
|
+
Revises: ${down_revision | comma,n}
|
|
248
|
+
Create Date: ${create_date}
|
|
249
|
+
|
|
250
|
+
"""
|
|
251
|
+
from typing import Sequence, Union
|
|
252
|
+
|
|
253
|
+
from alembic import op
|
|
254
|
+
import sqlalchemy as sa
|
|
255
|
+
${imports if imports else ""}
|
|
256
|
+
|
|
257
|
+
# revision identifiers, used by Alembic.
|
|
258
|
+
revision: str = ${repr(up_revision)}
|
|
259
|
+
down_revision: Union[str, None] = ${repr(down_revision)}
|
|
260
|
+
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
|
261
|
+
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def upgrade() -> None:
|
|
265
|
+
${upgrades if upgrades else "pass"}
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def downgrade() -> None:
|
|
269
|
+
${downgrades if downgrades else "pass"}
|
|
270
|
+
''',
|
|
271
|
+
|
|
272
|
+
"migrations_versions_init_py": lambda: '''"""
|
|
273
|
+
数据库迁移版本目录
|
|
274
|
+
"""
|
|
275
|
+
''',
|
|
276
|
+
|
|
277
|
+
"migrations_init_py": lambda: '''"""
|
|
278
|
+
数据库迁移目录
|
|
279
|
+
"""
|
|
280
|
+
''',
|
|
281
|
+
}
|