aury-boot 0.0.2__py3-none-any.whl → 0.0.4__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 +66 -0
- aury/boot/_version.py +2 -2
- aury/boot/application/__init__.py +120 -0
- aury/boot/application/app/__init__.py +39 -0
- aury/boot/application/app/base.py +511 -0
- aury/boot/application/app/components.py +434 -0
- aury/boot/application/app/middlewares.py +101 -0
- aury/boot/application/config/__init__.py +44 -0
- aury/boot/application/config/settings.py +663 -0
- aury/boot/application/constants/__init__.py +19 -0
- aury/boot/application/constants/components.py +50 -0
- aury/boot/application/constants/scheduler.py +28 -0
- aury/boot/application/constants/service.py +29 -0
- aury/boot/application/errors/__init__.py +55 -0
- aury/boot/application/errors/chain.py +80 -0
- aury/boot/application/errors/codes.py +67 -0
- aury/boot/application/errors/exceptions.py +238 -0
- aury/boot/application/errors/handlers.py +320 -0
- aury/boot/application/errors/response.py +120 -0
- aury/boot/application/interfaces/__init__.py +76 -0
- aury/boot/application/interfaces/egress.py +224 -0
- aury/boot/application/interfaces/ingress.py +98 -0
- aury/boot/application/middleware/__init__.py +22 -0
- aury/boot/application/middleware/logging.py +451 -0
- aury/boot/application/migrations/__init__.py +13 -0
- aury/boot/application/migrations/manager.py +685 -0
- aury/boot/application/migrations/setup.py +237 -0
- aury/boot/application/rpc/__init__.py +63 -0
- aury/boot/application/rpc/base.py +108 -0
- aury/boot/application/rpc/client.py +294 -0
- aury/boot/application/rpc/discovery.py +218 -0
- aury/boot/application/scheduler/__init__.py +13 -0
- aury/boot/application/scheduler/runner.py +123 -0
- aury/boot/application/server/__init__.py +296 -0
- aury/boot/commands/__init__.py +30 -0
- aury/boot/commands/add.py +76 -0
- aury/boot/commands/app.py +105 -0
- aury/boot/commands/config.py +177 -0
- aury/boot/commands/docker.py +367 -0
- aury/boot/commands/docs.py +284 -0
- aury/boot/commands/generate.py +1277 -0
- aury/boot/commands/init.py +892 -0
- aury/boot/commands/migrate/__init__.py +37 -0
- aury/boot/commands/migrate/app.py +54 -0
- aury/boot/commands/migrate/commands.py +303 -0
- aury/boot/commands/scheduler.py +124 -0
- aury/boot/commands/server/__init__.py +21 -0
- aury/boot/commands/server/app.py +541 -0
- aury/boot/commands/templates/generate/api.py.tpl +105 -0
- aury/boot/commands/templates/generate/model.py.tpl +17 -0
- aury/boot/commands/templates/generate/repository.py.tpl +19 -0
- aury/boot/commands/templates/generate/schema.py.tpl +29 -0
- aury/boot/commands/templates/generate/service.py.tpl +48 -0
- aury/boot/commands/templates/project/CLI.md.tpl +92 -0
- aury/boot/commands/templates/project/DEVELOPMENT.md.tpl +1397 -0
- aury/boot/commands/templates/project/README.md.tpl +111 -0
- aury/boot/commands/templates/project/admin_console_init.py.tpl +50 -0
- aury/boot/commands/templates/project/config.py.tpl +30 -0
- aury/boot/commands/templates/project/conftest.py.tpl +26 -0
- aury/boot/commands/templates/project/env.example.tpl +213 -0
- aury/boot/commands/templates/project/gitignore.tpl +128 -0
- aury/boot/commands/templates/project/main.py.tpl +41 -0
- aury/boot/commands/templates/project/modules/api.py.tpl +19 -0
- aury/boot/commands/templates/project/modules/exceptions.py.tpl +84 -0
- aury/boot/commands/templates/project/modules/schedules.py.tpl +18 -0
- aury/boot/commands/templates/project/modules/tasks.py.tpl +20 -0
- aury/boot/commands/worker.py +143 -0
- aury/boot/common/__init__.py +35 -0
- aury/boot/common/exceptions/__init__.py +114 -0
- aury/boot/common/i18n/__init__.py +16 -0
- aury/boot/common/i18n/translator.py +272 -0
- aury/boot/common/logging/__init__.py +716 -0
- aury/boot/contrib/__init__.py +10 -0
- aury/boot/contrib/admin_console/__init__.py +18 -0
- aury/boot/contrib/admin_console/auth.py +137 -0
- aury/boot/contrib/admin_console/discovery.py +69 -0
- aury/boot/contrib/admin_console/install.py +172 -0
- aury/boot/contrib/admin_console/utils.py +44 -0
- aury/boot/domain/__init__.py +79 -0
- aury/boot/domain/exceptions/__init__.py +132 -0
- aury/boot/domain/models/__init__.py +51 -0
- aury/boot/domain/models/base.py +69 -0
- aury/boot/domain/models/mixins.py +135 -0
- aury/boot/domain/models/models.py +96 -0
- aury/boot/domain/pagination/__init__.py +279 -0
- aury/boot/domain/repository/__init__.py +23 -0
- aury/boot/domain/repository/impl.py +423 -0
- aury/boot/domain/repository/interceptors.py +47 -0
- aury/boot/domain/repository/interface.py +106 -0
- aury/boot/domain/repository/query_builder.py +348 -0
- aury/boot/domain/service/__init__.py +11 -0
- aury/boot/domain/service/base.py +73 -0
- aury/boot/domain/transaction/__init__.py +404 -0
- aury/boot/infrastructure/__init__.py +104 -0
- aury/boot/infrastructure/cache/__init__.py +31 -0
- aury/boot/infrastructure/cache/backends.py +348 -0
- aury/boot/infrastructure/cache/base.py +68 -0
- aury/boot/infrastructure/cache/exceptions.py +37 -0
- aury/boot/infrastructure/cache/factory.py +94 -0
- aury/boot/infrastructure/cache/manager.py +274 -0
- aury/boot/infrastructure/database/__init__.py +39 -0
- aury/boot/infrastructure/database/config.py +71 -0
- aury/boot/infrastructure/database/exceptions.py +44 -0
- aury/boot/infrastructure/database/manager.py +317 -0
- aury/boot/infrastructure/database/query_tools/__init__.py +164 -0
- aury/boot/infrastructure/database/strategies/__init__.py +198 -0
- aury/boot/infrastructure/di/__init__.py +15 -0
- aury/boot/infrastructure/di/container.py +393 -0
- aury/boot/infrastructure/events/__init__.py +33 -0
- aury/boot/infrastructure/events/bus.py +362 -0
- aury/boot/infrastructure/events/config.py +52 -0
- aury/boot/infrastructure/events/consumer.py +134 -0
- aury/boot/infrastructure/events/middleware.py +51 -0
- aury/boot/infrastructure/events/models.py +63 -0
- aury/boot/infrastructure/monitoring/__init__.py +529 -0
- aury/boot/infrastructure/scheduler/__init__.py +19 -0
- aury/boot/infrastructure/scheduler/exceptions.py +37 -0
- aury/boot/infrastructure/scheduler/manager.py +478 -0
- aury/boot/infrastructure/storage/__init__.py +38 -0
- aury/boot/infrastructure/storage/base.py +164 -0
- aury/boot/infrastructure/storage/exceptions.py +37 -0
- aury/boot/infrastructure/storage/factory.py +88 -0
- aury/boot/infrastructure/tasks/__init__.py +24 -0
- aury/boot/infrastructure/tasks/config.py +45 -0
- aury/boot/infrastructure/tasks/constants.py +37 -0
- aury/boot/infrastructure/tasks/exceptions.py +37 -0
- aury/boot/infrastructure/tasks/manager.py +490 -0
- aury/boot/testing/__init__.py +24 -0
- aury/boot/testing/base.py +122 -0
- aury/boot/testing/client.py +163 -0
- aury/boot/testing/factory.py +154 -0
- aury/boot/toolkit/__init__.py +21 -0
- aury/boot/toolkit/http/__init__.py +367 -0
- {aury_boot-0.0.2.dist-info → aury_boot-0.0.4.dist-info}/METADATA +3 -2
- aury_boot-0.0.4.dist-info/RECORD +137 -0
- aury_boot-0.0.2.dist-info/RECORD +0 -5
- {aury_boot-0.0.2.dist-info → aury_boot-0.0.4.dist-info}/WHEEL +0 -0
- {aury_boot-0.0.2.dist-info → aury_boot-0.0.4.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,1277 @@
|
|
|
1
|
+
"""代码生成器命令。
|
|
2
|
+
|
|
3
|
+
生成符合 Aury 规范的代码文件:
|
|
4
|
+
- model: SQLAlchemy 模型
|
|
5
|
+
- repo: Repository 数据访问层
|
|
6
|
+
- service: Service 业务逻辑层
|
|
7
|
+
- api: FastAPI 路由
|
|
8
|
+
- schema: Pydantic 模型
|
|
9
|
+
- crud: 一键生成以上所有
|
|
10
|
+
|
|
11
|
+
支持两种字段定义模式:
|
|
12
|
+
1. 命令行参数(AI 友好):
|
|
13
|
+
aury generate model user email:str:unique age:int? status:str=active
|
|
14
|
+
|
|
15
|
+
2. 交互式(人类友好):
|
|
16
|
+
aury generate model user -i
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
import re
|
|
24
|
+
from typing import Annotated
|
|
25
|
+
|
|
26
|
+
from rich.console import Console
|
|
27
|
+
from rich.panel import Panel
|
|
28
|
+
from rich.prompt import Confirm, Prompt
|
|
29
|
+
from rich.table import Table
|
|
30
|
+
import typer
|
|
31
|
+
|
|
32
|
+
from .config import get_project_config
|
|
33
|
+
|
|
34
|
+
console = Console()
|
|
35
|
+
|
|
36
|
+
# 模板目录
|
|
37
|
+
GENERATE_TEMPLATES_DIR = Path(__file__).parent / "templates" / "generate"
|
|
38
|
+
|
|
39
|
+
# 创建代码生成器子应用
|
|
40
|
+
app = typer.Typer(
|
|
41
|
+
name="generate",
|
|
42
|
+
help="代码生成器 - 生成 model/repo/service/api/schema",
|
|
43
|
+
no_args_is_help=True,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ============================================================
|
|
48
|
+
# 字段解析
|
|
49
|
+
# ============================================================
|
|
50
|
+
|
|
51
|
+
# 类型映射:简写 -> (SQLAlchemy 类型, Pydantic 类型, 需要的导入)
|
|
52
|
+
TYPE_MAPPING: dict[str, tuple[str, str, list[str]]] = {
|
|
53
|
+
# 字符串
|
|
54
|
+
"str": ("String(255)", "str", ["String"]),
|
|
55
|
+
"string": ("String(255)", "str", ["String"]),
|
|
56
|
+
"text": ("Text", "str", ["Text"]),
|
|
57
|
+
# 数字
|
|
58
|
+
"int": ("Integer", "int", ["Integer"]),
|
|
59
|
+
"integer": ("Integer", "int", ["Integer"]),
|
|
60
|
+
"bigint": ("BigInteger", "int", ["BigInteger"]),
|
|
61
|
+
"float": ("Float", "float", ["Float"]),
|
|
62
|
+
"decimal": ("Numeric(10, 2)", "Decimal", ["Numeric"]),
|
|
63
|
+
# 布尔
|
|
64
|
+
"bool": ("Boolean", "bool", ["Boolean"]),
|
|
65
|
+
"boolean": ("Boolean", "bool", ["Boolean"]),
|
|
66
|
+
# 日期时间
|
|
67
|
+
"datetime": ("DateTime", "datetime", ["DateTime"]),
|
|
68
|
+
"date": ("Date", "date", ["Date"]),
|
|
69
|
+
"time": ("Time", "time", ["Time"]),
|
|
70
|
+
# JSON
|
|
71
|
+
"json": ("JSON", "dict", ["JSON"]),
|
|
72
|
+
"dict": ("JSON", "dict", ["JSON"]),
|
|
73
|
+
# UUID
|
|
74
|
+
"uuid": ("GUID", "str", []), # 使用框架内置 GUID
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class FieldDefinition:
|
|
80
|
+
"""字段定义。"""
|
|
81
|
+
|
|
82
|
+
name: str
|
|
83
|
+
type_name: str = "str"
|
|
84
|
+
nullable: bool = False
|
|
85
|
+
unique: bool = False
|
|
86
|
+
index: bool = False
|
|
87
|
+
default: str | None = None
|
|
88
|
+
max_length: int | None = None # 用于 str 类型
|
|
89
|
+
comment: str | None = None
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def parse(cls, spec: str) -> "FieldDefinition":
|
|
93
|
+
"""解析字段定义字符串。
|
|
94
|
+
|
|
95
|
+
格式: name:type:modifiers
|
|
96
|
+
修饰符:
|
|
97
|
+
- ? 或 nullable: 可空
|
|
98
|
+
- unique: 唯一
|
|
99
|
+
- index: 索引
|
|
100
|
+
- =value: 默认值
|
|
101
|
+
- (length): 长度限制
|
|
102
|
+
|
|
103
|
+
示例:
|
|
104
|
+
- email:str:unique
|
|
105
|
+
- age:int?
|
|
106
|
+
- status:str=active
|
|
107
|
+
- name:str(100)
|
|
108
|
+
- bio:text?
|
|
109
|
+
- price:decimal:index
|
|
110
|
+
"""
|
|
111
|
+
parts = spec.split(":")
|
|
112
|
+
name = parts[0]
|
|
113
|
+
|
|
114
|
+
# 默认类型为 str
|
|
115
|
+
type_name = "str"
|
|
116
|
+
nullable = False
|
|
117
|
+
unique = False
|
|
118
|
+
index = False
|
|
119
|
+
default = None
|
|
120
|
+
max_length = None
|
|
121
|
+
|
|
122
|
+
for part in parts[1:]:
|
|
123
|
+
# 检查是否是类型定义
|
|
124
|
+
type_match = re.match(r"^([a-z]+)(\((\d+)\))?$", part.lower())
|
|
125
|
+
if type_match and type_match.group(1) in TYPE_MAPPING:
|
|
126
|
+
type_name = type_match.group(1)
|
|
127
|
+
if type_match.group(3):
|
|
128
|
+
max_length = int(type_match.group(3))
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
# 检查修饰符
|
|
132
|
+
part_lower = part.lower()
|
|
133
|
+
if part_lower in ("?", "nullable"):
|
|
134
|
+
nullable = True
|
|
135
|
+
elif part_lower == "unique":
|
|
136
|
+
unique = True
|
|
137
|
+
elif part_lower == "index":
|
|
138
|
+
index = True
|
|
139
|
+
elif part.startswith("="):
|
|
140
|
+
default = part[1:]
|
|
141
|
+
elif part.endswith("?"):
|
|
142
|
+
# 处理 age:int? 这种格式
|
|
143
|
+
type_check = part[:-1].lower()
|
|
144
|
+
if type_check in TYPE_MAPPING:
|
|
145
|
+
type_name = type_check
|
|
146
|
+
nullable = True
|
|
147
|
+
|
|
148
|
+
# 处理名字后面直接跟 ? 的情况,如 "age?"
|
|
149
|
+
if name.endswith("?"):
|
|
150
|
+
name = name[:-1]
|
|
151
|
+
nullable = True
|
|
152
|
+
|
|
153
|
+
return cls(
|
|
154
|
+
name=name,
|
|
155
|
+
type_name=type_name,
|
|
156
|
+
nullable=nullable,
|
|
157
|
+
unique=unique,
|
|
158
|
+
index=index,
|
|
159
|
+
default=default,
|
|
160
|
+
max_length=max_length,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# 可用的模型基类
|
|
165
|
+
# id_type: "int" | "uuid" 用于生成正确的 Schema/Service/API 类型
|
|
166
|
+
MODEL_BASE_CLASSES = {
|
|
167
|
+
"IDOnlyModel": {
|
|
168
|
+
"desc": "纯 int 主键(无时间戳,适合关系表)",
|
|
169
|
+
"features": ["id: int"],
|
|
170
|
+
"id_type": "int",
|
|
171
|
+
"has_timestamps": False,
|
|
172
|
+
},
|
|
173
|
+
"UUIDOnlyModel": {
|
|
174
|
+
"desc": "纯 UUID 主键(无时间戳,适合关系表)",
|
|
175
|
+
"features": ["id: UUID"],
|
|
176
|
+
"id_type": "uuid",
|
|
177
|
+
"has_timestamps": False,
|
|
178
|
+
},
|
|
179
|
+
"Model": {
|
|
180
|
+
"desc": "标准模型(int主键 + 时间戳)",
|
|
181
|
+
"features": ["id: int", "created_at", "updated_at"],
|
|
182
|
+
"id_type": "int",
|
|
183
|
+
"has_timestamps": True,
|
|
184
|
+
},
|
|
185
|
+
"AuditableStateModel": {
|
|
186
|
+
"desc": "标准模型 + 软删除",
|
|
187
|
+
"features": ["id: int", "created_at", "updated_at", "deleted_at"],
|
|
188
|
+
"id_type": "int",
|
|
189
|
+
"has_timestamps": True,
|
|
190
|
+
},
|
|
191
|
+
"UUIDModel": {
|
|
192
|
+
"desc": "UUID 主键模型",
|
|
193
|
+
"features": ["id: UUID", "created_at", "updated_at"],
|
|
194
|
+
"id_type": "uuid",
|
|
195
|
+
"has_timestamps": True,
|
|
196
|
+
},
|
|
197
|
+
"UUIDAuditableStateModel": {
|
|
198
|
+
"desc": "UUID 主键 + 软删除(推荐)",
|
|
199
|
+
"features": ["id: UUID", "created_at", "updated_at", "deleted_at"],
|
|
200
|
+
"id_type": "uuid",
|
|
201
|
+
"has_timestamps": True,
|
|
202
|
+
},
|
|
203
|
+
"VersionedModel": {
|
|
204
|
+
"desc": "乐观锁模型(int主键 + version)",
|
|
205
|
+
"features": ["id: int", "version"],
|
|
206
|
+
"id_type": "int",
|
|
207
|
+
"has_timestamps": False,
|
|
208
|
+
},
|
|
209
|
+
"VersionedTimestampedModel": {
|
|
210
|
+
"desc": "乐观锁 + 时间戳",
|
|
211
|
+
"features": ["id: int", "created_at", "updated_at", "version"],
|
|
212
|
+
"id_type": "int",
|
|
213
|
+
"has_timestamps": True,
|
|
214
|
+
},
|
|
215
|
+
"VersionedUUIDModel": {
|
|
216
|
+
"desc": "UUID + 乐观锁 + 时间戳",
|
|
217
|
+
"features": ["id: UUID", "created_at", "updated_at", "version"],
|
|
218
|
+
"id_type": "uuid",
|
|
219
|
+
"has_timestamps": True,
|
|
220
|
+
},
|
|
221
|
+
"FullFeaturedModel": {
|
|
222
|
+
"desc": "完整功能 int版",
|
|
223
|
+
"features": ["id: int", "created_at", "updated_at", "deleted_at", "version"],
|
|
224
|
+
"id_type": "int",
|
|
225
|
+
"has_timestamps": True,
|
|
226
|
+
},
|
|
227
|
+
"FullFeaturedUUIDModel": {
|
|
228
|
+
"desc": "完整功能 UUID版",
|
|
229
|
+
"features": ["id: UUID", "created_at", "updated_at", "deleted_at", "version"],
|
|
230
|
+
"id_type": "uuid",
|
|
231
|
+
"has_timestamps": True,
|
|
232
|
+
},
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
# UUID 类型的基类名称
|
|
236
|
+
UUID_BASE_CLASSES = {k for k, v in MODEL_BASE_CLASSES.items() if v.get("id_type") == "uuid"}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@dataclass
|
|
240
|
+
class ModelDefinition:
|
|
241
|
+
"""模型定义。"""
|
|
242
|
+
|
|
243
|
+
name: str
|
|
244
|
+
fields: list[FieldDefinition] = field(default_factory=list)
|
|
245
|
+
soft_delete: bool = True
|
|
246
|
+
timestamps: bool = True
|
|
247
|
+
base_class: str | None = None # 用户指定的基类
|
|
248
|
+
|
|
249
|
+
@property
|
|
250
|
+
def id_type(self) -> str:
|
|
251
|
+
"""获取 id 类型:'int' 或 'uuid'。"""
|
|
252
|
+
if self.base_class:
|
|
253
|
+
return MODEL_BASE_CLASSES.get(self.base_class, {}).get("id_type", "uuid")
|
|
254
|
+
# 默认通过 soft_delete/timestamps 推断时使用 UUID
|
|
255
|
+
return "uuid"
|
|
256
|
+
|
|
257
|
+
@property
|
|
258
|
+
def id_py_type(self) -> str:
|
|
259
|
+
"""获取 Python/Pydantic 的 id 类型。"""
|
|
260
|
+
return "int" if self.id_type == "int" else "UUID"
|
|
261
|
+
|
|
262
|
+
@property
|
|
263
|
+
def has_timestamps(self) -> bool:
|
|
264
|
+
"""是否有时间戳字段。"""
|
|
265
|
+
if self.base_class:
|
|
266
|
+
return MODEL_BASE_CLASSES.get(self.base_class, {}).get("has_timestamps", True)
|
|
267
|
+
return self.timestamps
|
|
268
|
+
|
|
269
|
+
@property
|
|
270
|
+
def class_name(self) -> str:
|
|
271
|
+
"""PascalCase 类名。"""
|
|
272
|
+
return _to_pascal_case(self.name)
|
|
273
|
+
|
|
274
|
+
@property
|
|
275
|
+
def file_name(self) -> str:
|
|
276
|
+
"""snake_case 文件名。"""
|
|
277
|
+
return _to_snake_case(self.name)
|
|
278
|
+
|
|
279
|
+
@property
|
|
280
|
+
def table_name(self) -> str:
|
|
281
|
+
"""snake_case 复数表名。"""
|
|
282
|
+
return _to_plural(self.file_name)
|
|
283
|
+
|
|
284
|
+
@property
|
|
285
|
+
def var_name(self) -> str:
|
|
286
|
+
"""变量名。"""
|
|
287
|
+
return self.file_name
|
|
288
|
+
|
|
289
|
+
@property
|
|
290
|
+
def var_name_plural(self) -> str:
|
|
291
|
+
"""复数变量名。"""
|
|
292
|
+
return _to_plural(self.file_name)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
# ============================================================
|
|
296
|
+
# 工具函数
|
|
297
|
+
# ============================================================
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _to_snake_case(name: str) -> str:
|
|
301
|
+
"""转换为 snake_case。"""
|
|
302
|
+
s1 = re.sub(r"(.)([A-Z][a-z]+)", r"\1_\2", name)
|
|
303
|
+
return re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _to_pascal_case(name: str) -> str:
|
|
307
|
+
"""转换为 PascalCase。"""
|
|
308
|
+
snake = _to_snake_case(name)
|
|
309
|
+
return "".join(word.capitalize() for word in snake.split("_"))
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _to_plural(name: str) -> str:
|
|
313
|
+
"""简单的复数转换。"""
|
|
314
|
+
if name.endswith("y"):
|
|
315
|
+
return name[:-1] + "ies"
|
|
316
|
+
if name.endswith(("s", "x", "ch", "sh")):
|
|
317
|
+
return name + "es"
|
|
318
|
+
return name + "s"
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _create_file(path: Path, content: str, force: bool = False) -> bool:
|
|
322
|
+
"""创建文件。"""
|
|
323
|
+
if path.exists() and not force:
|
|
324
|
+
return False
|
|
325
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
326
|
+
path.write_text(content, encoding="utf-8")
|
|
327
|
+
return True
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _update_init_file(init_path: Path, import_line: str, export_name: str) -> None:
|
|
331
|
+
"""更新 __init__.py 文件。"""
|
|
332
|
+
if not init_path.exists():
|
|
333
|
+
init_path.write_text(
|
|
334
|
+
f'{import_line}\n\n__all__ = ["{export_name}"]\n', encoding="utf-8"
|
|
335
|
+
)
|
|
336
|
+
return
|
|
337
|
+
|
|
338
|
+
content = init_path.read_text(encoding="utf-8")
|
|
339
|
+
|
|
340
|
+
# 检查是否已导入
|
|
341
|
+
if import_line in content:
|
|
342
|
+
return
|
|
343
|
+
|
|
344
|
+
# 添加导入
|
|
345
|
+
if "__all__" in content:
|
|
346
|
+
content = content.replace("__all__", f"{import_line}\n\n__all__")
|
|
347
|
+
if f'"{export_name}"' not in content:
|
|
348
|
+
content = content.replace("__all__ = [", f'__all__ = [\n "{export_name}",')
|
|
349
|
+
else:
|
|
350
|
+
content += f'\n{import_line}\n\n__all__ = ["{export_name}"]\n'
|
|
351
|
+
|
|
352
|
+
init_path.write_text(content, encoding="utf-8")
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
# ============================================================
|
|
356
|
+
# 交互式字段收集
|
|
357
|
+
# ============================================================
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _collect_base_class_interactive() -> str:
|
|
361
|
+
"""交互式选择模型基类。"""
|
|
362
|
+
console.print("\n[bold cyan]📚 选择模型基类[/bold cyan]\n")
|
|
363
|
+
|
|
364
|
+
# 显示可用基类
|
|
365
|
+
table = Table(title="可用模型基类", show_header=True, header_style="bold magenta")
|
|
366
|
+
table.add_column("序号", style="dim", width=4)
|
|
367
|
+
table.add_column("基类名", style="cyan")
|
|
368
|
+
table.add_column("说明")
|
|
369
|
+
table.add_column("自动继承的字段")
|
|
370
|
+
|
|
371
|
+
base_names = list(MODEL_BASE_CLASSES.keys())
|
|
372
|
+
for i, name in enumerate(base_names, 1):
|
|
373
|
+
info = MODEL_BASE_CLASSES[name]
|
|
374
|
+
# 推荐的加标记
|
|
375
|
+
desc = info["desc"]
|
|
376
|
+
if name == "UUIDAuditableStateModel":
|
|
377
|
+
desc = f"[bold green]★ {desc}[/bold green]"
|
|
378
|
+
table.add_row(str(i), name, desc, ", ".join(info["features"]))
|
|
379
|
+
|
|
380
|
+
console.print(table)
|
|
381
|
+
console.print()
|
|
382
|
+
|
|
383
|
+
# 默认选择 UUIDAuditableStateModel(第 4 个)
|
|
384
|
+
choice = Prompt.ask(
|
|
385
|
+
"请选择基类序号",
|
|
386
|
+
default="4",
|
|
387
|
+
choices=[str(i) for i in range(1, len(base_names) + 1)],
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
selected = base_names[int(choice) - 1]
|
|
391
|
+
console.print(f" [green]✓ 已选择: {selected}[/green]\n")
|
|
392
|
+
return selected
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _collect_fields_interactive() -> list[FieldDefinition]:
|
|
396
|
+
"""交互式收集字段定义。"""
|
|
397
|
+
fields: list[FieldDefinition] = []
|
|
398
|
+
|
|
399
|
+
console.print("\n[bold cyan]📝 添加字段[/bold cyan] (输入空名称结束)\n")
|
|
400
|
+
|
|
401
|
+
# 显示类型帮助
|
|
402
|
+
table = Table(title="支持的类型", show_header=True, header_style="bold magenta")
|
|
403
|
+
table.add_column("类型", style="cyan")
|
|
404
|
+
table.add_column("说明")
|
|
405
|
+
table.add_row("str, string", "字符串 (默认)")
|
|
406
|
+
table.add_row("text", "长文本")
|
|
407
|
+
table.add_row("int, integer", "整数")
|
|
408
|
+
table.add_row("bigint", "大整数")
|
|
409
|
+
table.add_row("float", "浮点数")
|
|
410
|
+
table.add_row("decimal", "精确小数")
|
|
411
|
+
table.add_row("bool, boolean", "布尔值")
|
|
412
|
+
table.add_row("datetime", "日期时间")
|
|
413
|
+
table.add_row("date", "日期")
|
|
414
|
+
table.add_row("json, dict", "JSON 对象")
|
|
415
|
+
console.print(table)
|
|
416
|
+
console.print()
|
|
417
|
+
|
|
418
|
+
while True:
|
|
419
|
+
name = Prompt.ask("[bold]字段名[/bold]", default="")
|
|
420
|
+
if not name:
|
|
421
|
+
break
|
|
422
|
+
|
|
423
|
+
type_name = Prompt.ask(
|
|
424
|
+
" 类型",
|
|
425
|
+
default="str",
|
|
426
|
+
choices=list(TYPE_MAPPING.keys()),
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
nullable = Confirm.ask(" 可空?", default=False)
|
|
430
|
+
unique = Confirm.ask(" 唯一?", default=False)
|
|
431
|
+
index = Confirm.ask(" 索引?", default=False)
|
|
432
|
+
default = Prompt.ask(" 默认值 (留空无默认)", default="")
|
|
433
|
+
|
|
434
|
+
max_length = None
|
|
435
|
+
if type_name in ("str", "string"):
|
|
436
|
+
length_str = Prompt.ask(" 最大长度", default="255")
|
|
437
|
+
max_length = int(length_str) if length_str.isdigit() else 255
|
|
438
|
+
|
|
439
|
+
fields.append(
|
|
440
|
+
FieldDefinition(
|
|
441
|
+
name=name,
|
|
442
|
+
type_name=type_name,
|
|
443
|
+
nullable=nullable,
|
|
444
|
+
unique=unique,
|
|
445
|
+
index=index,
|
|
446
|
+
default=default if default else None,
|
|
447
|
+
max_length=max_length,
|
|
448
|
+
)
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
console.print(f" [green]✓ 已添加: {name}:{type_name}[/green]\n")
|
|
452
|
+
|
|
453
|
+
return fields
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
# ============================================================
|
|
457
|
+
# 模板读取
|
|
458
|
+
# ============================================================
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _read_generate_template(name: str) -> str:
|
|
462
|
+
"""读取代码生成模板文件。"""
|
|
463
|
+
template_path = GENERATE_TEMPLATES_DIR / name
|
|
464
|
+
if not template_path.exists():
|
|
465
|
+
raise FileNotFoundError(f"模板文件不存在: {name} (查找路径: {GENERATE_TEMPLATES_DIR})")
|
|
466
|
+
return template_path.read_text(encoding="utf-8")
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _get_base_class_from_model_file(code_root: Path, model_name: str) -> str | None:
|
|
470
|
+
"""从已生成的模型文件中读取基类名称。
|
|
471
|
+
|
|
472
|
+
Args:
|
|
473
|
+
code_root: 代码根目录
|
|
474
|
+
model_name: 模型名称(snake_case)
|
|
475
|
+
|
|
476
|
+
Returns:
|
|
477
|
+
基类名称,如果无法读取则返回 None
|
|
478
|
+
"""
|
|
479
|
+
model_file = code_root / "models" / f"{model_name}.py"
|
|
480
|
+
if not model_file.exists():
|
|
481
|
+
return None
|
|
482
|
+
|
|
483
|
+
try:
|
|
484
|
+
content = model_file.read_text(encoding="utf-8")
|
|
485
|
+
# 查找继承的基类,例如: class User(UUIDAuditableStateModel):
|
|
486
|
+
pattern = r"class\s+\w+\s*\((\w+)\)"
|
|
487
|
+
match = re.search(pattern, content)
|
|
488
|
+
if match:
|
|
489
|
+
base_class = match.group(1)
|
|
490
|
+
# 验证是否是有效的基类
|
|
491
|
+
if base_class in MODEL_BASE_CLASSES:
|
|
492
|
+
return base_class
|
|
493
|
+
except Exception:
|
|
494
|
+
pass
|
|
495
|
+
|
|
496
|
+
return None
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
# ============================================================
|
|
500
|
+
# 模板生成
|
|
501
|
+
# ============================================================
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _generate_model_content(model: ModelDefinition) -> str:
|
|
505
|
+
"""生成 Model 内容。"""
|
|
506
|
+
# 收集需要的导入
|
|
507
|
+
imports: set[str] = {"String"} # 默认总是有 String
|
|
508
|
+
for f in model.fields:
|
|
509
|
+
type_info = TYPE_MAPPING.get(f.type_name, ("String(255)", "str", ["String"]))
|
|
510
|
+
imports.update(type_info[2])
|
|
511
|
+
|
|
512
|
+
imports_str = ", ".join(sorted(imports))
|
|
513
|
+
|
|
514
|
+
# 生成字段定义
|
|
515
|
+
field_lines = []
|
|
516
|
+
for f in model.fields:
|
|
517
|
+
type_info = TYPE_MAPPING.get(f.type_name, ("String(255)", "str", ["String"]))
|
|
518
|
+
sa_type = type_info[0]
|
|
519
|
+
py_type = type_info[1]
|
|
520
|
+
|
|
521
|
+
# 处理字符串长度
|
|
522
|
+
if f.type_name in ("str", "string") and f.max_length:
|
|
523
|
+
sa_type = f"String({f.max_length})"
|
|
524
|
+
|
|
525
|
+
# 构建 Mapped 类型
|
|
526
|
+
if f.nullable:
|
|
527
|
+
mapped_type = f"Mapped[{py_type} | None]"
|
|
528
|
+
else:
|
|
529
|
+
mapped_type = f"Mapped[{py_type}]"
|
|
530
|
+
|
|
531
|
+
# 构建 mapped_column 参数
|
|
532
|
+
col_args = [sa_type]
|
|
533
|
+
if f.unique:
|
|
534
|
+
col_args.append("unique=True")
|
|
535
|
+
if f.index:
|
|
536
|
+
col_args.append("index=True")
|
|
537
|
+
if f.nullable:
|
|
538
|
+
col_args.append("nullable=True")
|
|
539
|
+
if f.default is not None:
|
|
540
|
+
if f.type_name in ("str", "string", "text"):
|
|
541
|
+
col_args.append(f'default="{f.default}"')
|
|
542
|
+
elif f.type_name in ("bool", "boolean"):
|
|
543
|
+
col_args.append(f"default={f.default.capitalize()}")
|
|
544
|
+
else:
|
|
545
|
+
col_args.append(f"default={f.default}")
|
|
546
|
+
|
|
547
|
+
col_args_str = ", ".join(col_args)
|
|
548
|
+
field_lines.append(f" {f.name}: {mapped_type} = mapped_column({col_args_str})")
|
|
549
|
+
|
|
550
|
+
fields_str = "\n".join(field_lines) if field_lines else " # 添加字段"
|
|
551
|
+
|
|
552
|
+
# 选择基类:优先使用用户指定的,否则根据选项推断
|
|
553
|
+
if model.base_class:
|
|
554
|
+
base_class = model.base_class
|
|
555
|
+
base_info = MODEL_BASE_CLASSES.get(base_class, {})
|
|
556
|
+
features = base_info.get("features", [])
|
|
557
|
+
base_doc = f"继承 {base_class} 自动获得:\n - " + "\n - ".join(features) if features else f"继承 {base_class} 基类。"
|
|
558
|
+
elif model.soft_delete and model.timestamps:
|
|
559
|
+
base_class = "UUIDAuditableStateModel"
|
|
560
|
+
base_doc = """继承 UUIDAuditableStateModel 自动获得:
|
|
561
|
+
- id: UUID 主键
|
|
562
|
+
- created_at: 创建时间
|
|
563
|
+
- updated_at: 更新时间
|
|
564
|
+
- deleted_at: 软删除时间戳"""
|
|
565
|
+
elif model.timestamps:
|
|
566
|
+
base_class = "UUIDModel"
|
|
567
|
+
base_doc = """继承 UUIDModel 自动获得:
|
|
568
|
+
- id: UUID 主键
|
|
569
|
+
- created_at: 创建时间
|
|
570
|
+
- updated_at: 更新时间"""
|
|
571
|
+
else:
|
|
572
|
+
base_class = "Model"
|
|
573
|
+
base_doc = "继承 Model 基类。"
|
|
574
|
+
|
|
575
|
+
template = _read_generate_template("model.py.tpl")
|
|
576
|
+
return template.format(
|
|
577
|
+
class_name=model.class_name,
|
|
578
|
+
imports_str=imports_str,
|
|
579
|
+
base_class=base_class,
|
|
580
|
+
base_doc=base_doc,
|
|
581
|
+
table_name=model.table_name,
|
|
582
|
+
fields_str=fields_str,
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def _generate_schema_content(model: ModelDefinition) -> str:
|
|
587
|
+
"""生成 Schema 内容。"""
|
|
588
|
+
# 基础字段
|
|
589
|
+
base_fields = []
|
|
590
|
+
update_fields = []
|
|
591
|
+
|
|
592
|
+
for f in model.fields:
|
|
593
|
+
type_info = TYPE_MAPPING.get(f.type_name, ("String(255)", "str", ["String"]))
|
|
594
|
+
py_type = type_info[1]
|
|
595
|
+
|
|
596
|
+
# Base 字段(Create 继承)
|
|
597
|
+
if f.nullable:
|
|
598
|
+
field_type = f"{py_type} | None"
|
|
599
|
+
default = "None"
|
|
600
|
+
elif f.default is not None:
|
|
601
|
+
field_type = py_type
|
|
602
|
+
if f.type_name in ("str", "string", "text"):
|
|
603
|
+
default = f'"{f.default}"'
|
|
604
|
+
elif f.type_name in ("bool", "boolean"):
|
|
605
|
+
default = f.default.capitalize()
|
|
606
|
+
else:
|
|
607
|
+
default = f.default
|
|
608
|
+
else:
|
|
609
|
+
field_type = py_type
|
|
610
|
+
default = "..."
|
|
611
|
+
|
|
612
|
+
# 构建 Field 参数
|
|
613
|
+
field_args = [default]
|
|
614
|
+
if f.type_name in ("str", "string") and f.max_length:
|
|
615
|
+
field_args.append(f"max_length={f.max_length}")
|
|
616
|
+
field_args.append(f'description="{f.name}"')
|
|
617
|
+
|
|
618
|
+
field_args_str = ", ".join(field_args)
|
|
619
|
+
base_fields.append(f" {f.name}: {field_type} = Field({field_args_str})")
|
|
620
|
+
|
|
621
|
+
# Update 字段(全部可选)
|
|
622
|
+
update_field_args = ["None"]
|
|
623
|
+
if f.type_name in ("str", "string") and f.max_length:
|
|
624
|
+
update_field_args.append(f"max_length={f.max_length}")
|
|
625
|
+
update_field_args.append(f'description="{f.name}"')
|
|
626
|
+
update_field_args_str = ", ".join(update_field_args)
|
|
627
|
+
update_fields.append(
|
|
628
|
+
f" {f.name}: {py_type} | None = Field({update_field_args_str})"
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
base_fields_str = "\n".join(base_fields) if base_fields else " pass"
|
|
632
|
+
update_fields_str = "\n".join(update_fields) if update_fields else " pass"
|
|
633
|
+
|
|
634
|
+
# Response 字段(继承 Base,添加 id 和时间戳)
|
|
635
|
+
id_type = model.id_py_type # "int" 或 "UUID"
|
|
636
|
+
|
|
637
|
+
# 根据是否有时间戳生成不同的 response 字段
|
|
638
|
+
if model.has_timestamps:
|
|
639
|
+
response_extra = f''' id: {id_type} = Field(..., description="ID")
|
|
640
|
+
created_at: datetime = Field(..., description="创建时间")
|
|
641
|
+
updated_at: datetime = Field(..., description="更新时间")'''
|
|
642
|
+
else:
|
|
643
|
+
response_extra = f' id: {id_type} = Field(..., description="ID")'
|
|
644
|
+
|
|
645
|
+
# 导入语句
|
|
646
|
+
imports = ["from datetime import datetime"] if model.has_timestamps else []
|
|
647
|
+
if id_type == "UUID":
|
|
648
|
+
imports.append("from uuid import UUID")
|
|
649
|
+
imports_str = "\n".join(imports)
|
|
650
|
+
if imports_str:
|
|
651
|
+
imports_str += "\n"
|
|
652
|
+
|
|
653
|
+
template = _read_generate_template("schema.py.tpl")
|
|
654
|
+
return template.format(
|
|
655
|
+
class_name=model.class_name,
|
|
656
|
+
imports_str=imports_str,
|
|
657
|
+
base_fields_str=base_fields_str,
|
|
658
|
+
update_fields_str=update_fields_str,
|
|
659
|
+
response_extra=response_extra,
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def _generate_repository_content(model: ModelDefinition, import_prefix: str = "") -> str:
|
|
664
|
+
"""生成 Repository 内容。
|
|
665
|
+
|
|
666
|
+
Args:
|
|
667
|
+
model: 模型定义
|
|
668
|
+
import_prefix: import 前缀,如 "mypackage." 或 ""
|
|
669
|
+
"""
|
|
670
|
+
# 查找唯一字段生成 get_by_xxx 方法
|
|
671
|
+
get_by_methods = []
|
|
672
|
+
for f in model.fields:
|
|
673
|
+
if f.unique:
|
|
674
|
+
type_info = TYPE_MAPPING.get(f.type_name, ("String(255)", "str", ["String"]))
|
|
675
|
+
py_type = type_info[1]
|
|
676
|
+
get_by_methods.append(f'''
|
|
677
|
+
async def get_by_{f.name}(self, {f.name}: {py_type}) -> {model.class_name} | None:
|
|
678
|
+
"""按 {f.name} 获取。"""
|
|
679
|
+
return await self.get_by({f.name}={f.name})''')
|
|
680
|
+
|
|
681
|
+
methods_str = "\n".join(get_by_methods) if get_by_methods else ""
|
|
682
|
+
|
|
683
|
+
template = _read_generate_template("repository.py.tpl")
|
|
684
|
+
return template.format(
|
|
685
|
+
class_name=model.class_name,
|
|
686
|
+
import_prefix=import_prefix,
|
|
687
|
+
file_name=model.file_name,
|
|
688
|
+
methods_str=methods_str,
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def _generate_service_content(model: ModelDefinition, import_prefix: str = "") -> str:
|
|
693
|
+
"""生成 Service 内容。
|
|
694
|
+
|
|
695
|
+
Args:
|
|
696
|
+
model: 模型定义
|
|
697
|
+
import_prefix: import 前缀,如 "mypackage." 或 ""
|
|
698
|
+
"""
|
|
699
|
+
# 检查唯一字段,生成重复检测
|
|
700
|
+
unique_checks = []
|
|
701
|
+
for f in model.fields:
|
|
702
|
+
if f.unique:
|
|
703
|
+
unique_checks.append(
|
|
704
|
+
f''' # 检查 {f.name} 是否已存在
|
|
705
|
+
existing = await self.repo.get_by_{f.name}(data.{f.name})
|
|
706
|
+
if existing:
|
|
707
|
+
raise AlreadyExistsError(f"{model.class_name} 已存在: {{data.{f.name}}}")
|
|
708
|
+
'''
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
unique_check_str = "\n".join(unique_checks) if unique_checks else ""
|
|
712
|
+
|
|
713
|
+
# UUID 类型需要导入
|
|
714
|
+
id_type = model.id_py_type
|
|
715
|
+
uuid_import = "from uuid import UUID\n\n" if id_type == "UUID" else ""
|
|
716
|
+
|
|
717
|
+
template = _read_generate_template("service.py.tpl")
|
|
718
|
+
return template.format(
|
|
719
|
+
class_name=model.class_name,
|
|
720
|
+
uuid_import=uuid_import,
|
|
721
|
+
import_prefix=import_prefix,
|
|
722
|
+
file_name=model.file_name,
|
|
723
|
+
id_py_type=model.id_py_type,
|
|
724
|
+
unique_check_str=unique_check_str,
|
|
725
|
+
)
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
def _generate_api_content(model: ModelDefinition, import_prefix: str = "") -> str:
|
|
729
|
+
"""生成 API 内容。
|
|
730
|
+
|
|
731
|
+
Args:
|
|
732
|
+
model: 模型定义
|
|
733
|
+
import_prefix: import 前缀,如 "mypackage." 或 ""
|
|
734
|
+
"""
|
|
735
|
+
id_type = model.id_py_type # "int" 或 "UUID"
|
|
736
|
+
|
|
737
|
+
# 导入语句
|
|
738
|
+
imports = []
|
|
739
|
+
if id_type == "UUID":
|
|
740
|
+
imports.append("from uuid import UUID")
|
|
741
|
+
imports_str = "\n".join(imports)
|
|
742
|
+
if imports_str:
|
|
743
|
+
imports_str += "\n"
|
|
744
|
+
|
|
745
|
+
template = _read_generate_template("api.py.tpl")
|
|
746
|
+
return template.format(
|
|
747
|
+
class_name=model.class_name,
|
|
748
|
+
uuid_import=imports_str, # 模板中使用 uuid_import 占位符,但实际传入的是 imports_str(可能为空)
|
|
749
|
+
import_prefix=import_prefix,
|
|
750
|
+
file_name=model.file_name,
|
|
751
|
+
var_name_plural=model.var_name_plural,
|
|
752
|
+
var_name=model.var_name,
|
|
753
|
+
id_type=id_type,
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
# ============================================================
|
|
758
|
+
# 命令
|
|
759
|
+
# ============================================================
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
@app.command(name="model")
|
|
763
|
+
def generate_model(
|
|
764
|
+
name: str = typer.Argument(..., help="模型名称(如 user, UserProfile)"),
|
|
765
|
+
fields: Annotated[
|
|
766
|
+
list[str] | None,
|
|
767
|
+
typer.Argument(
|
|
768
|
+
help="字段定义,格式: name:type:modifiers(如 email:str:unique age:int?)"
|
|
769
|
+
),
|
|
770
|
+
] = None,
|
|
771
|
+
interactive: bool = typer.Option(
|
|
772
|
+
False, "--interactive", "-i", help="交互式添加字段"
|
|
773
|
+
),
|
|
774
|
+
base: str | None = typer.Option(
|
|
775
|
+
None, "--base", "-b",
|
|
776
|
+
help="模型基类(Model/UUIDModel/UUIDAuditableStateModel/VersionedModel 等)"
|
|
777
|
+
),
|
|
778
|
+
force: bool = typer.Option(False, "--force", "-f", help="强制覆盖"),
|
|
779
|
+
no_soft_delete: bool = typer.Option(False, "--no-soft-delete", help="禁用软删除"),
|
|
780
|
+
no_timestamps: bool = typer.Option(False, "--no-timestamps", help="禁用时间戳"),
|
|
781
|
+
) -> None:
|
|
782
|
+
"""生成 SQLAlchemy 模型。
|
|
783
|
+
|
|
784
|
+
支持两种模式:
|
|
785
|
+
|
|
786
|
+
1. 命令行参数(AI 友好):
|
|
787
|
+
aury generate model user email:str:unique age:int? status:str=active
|
|
788
|
+
|
|
789
|
+
2. 交互式(人类友好):
|
|
790
|
+
aury generate model user -i
|
|
791
|
+
|
|
792
|
+
字段语法:
|
|
793
|
+
name:type:modifiers
|
|
794
|
+
|
|
795
|
+
支持的类型:
|
|
796
|
+
str, text, int, bigint, float, decimal, bool, datetime, date, json
|
|
797
|
+
|
|
798
|
+
修饰符:
|
|
799
|
+
? 或 nullable - 可空
|
|
800
|
+
unique - 唯一约束
|
|
801
|
+
index - 索引
|
|
802
|
+
=value - 默认值
|
|
803
|
+
(length) - 字符串长度,如 str(100)
|
|
804
|
+
|
|
805
|
+
可用基类:
|
|
806
|
+
Model, AuditableStateModel, UUIDModel, UUIDAuditableStateModel,
|
|
807
|
+
VersionedModel, VersionedTimestampedModel, VersionedUUIDModel,
|
|
808
|
+
FullFeaturedModel, FullFeaturedUUIDModel
|
|
809
|
+
|
|
810
|
+
示例:
|
|
811
|
+
aury generate model user
|
|
812
|
+
aury generate model user -b VersionedUUIDModel
|
|
813
|
+
aury generate model user email:str:unique age:int?
|
|
814
|
+
aury generate model article title:str(200) content:text status:str=draft
|
|
815
|
+
"""
|
|
816
|
+
base_path = Path.cwd()
|
|
817
|
+
|
|
818
|
+
# 读取项目配置
|
|
819
|
+
config = get_project_config(base_path)
|
|
820
|
+
code_root = config.get_package_dir(base_path)
|
|
821
|
+
|
|
822
|
+
# 解析字段
|
|
823
|
+
field_defs: list[FieldDefinition] = []
|
|
824
|
+
selected_base_class: str | None = base
|
|
825
|
+
|
|
826
|
+
if interactive:
|
|
827
|
+
# 交互式模式:先选择基类,再添加字段
|
|
828
|
+
selected_base_class = _collect_base_class_interactive()
|
|
829
|
+
field_defs = _collect_fields_interactive()
|
|
830
|
+
elif fields:
|
|
831
|
+
for spec in fields:
|
|
832
|
+
try:
|
|
833
|
+
field_defs.append(FieldDefinition.parse(spec))
|
|
834
|
+
except Exception as e:
|
|
835
|
+
console.print(f"[red]❌ 解析字段失败: {spec} - {e}[/red]")
|
|
836
|
+
raise typer.Exit(1) from e
|
|
837
|
+
|
|
838
|
+
# 验证基类名称
|
|
839
|
+
if selected_base_class and selected_base_class not in MODEL_BASE_CLASSES:
|
|
840
|
+
console.print(f"[red]❌ 无效的基类: {selected_base_class}[/red]")
|
|
841
|
+
console.print(f"[dim]可用基类: {', '.join(MODEL_BASE_CLASSES.keys())}[/dim]")
|
|
842
|
+
raise typer.Exit(1)
|
|
843
|
+
|
|
844
|
+
model = ModelDefinition(
|
|
845
|
+
name=name,
|
|
846
|
+
fields=field_defs,
|
|
847
|
+
soft_delete=not no_soft_delete,
|
|
848
|
+
timestamps=not no_timestamps,
|
|
849
|
+
base_class=selected_base_class,
|
|
850
|
+
)
|
|
851
|
+
|
|
852
|
+
content = _generate_model_content(model)
|
|
853
|
+
file_path = code_root / "models" / f"{model.file_name}.py"
|
|
854
|
+
rel_path = file_path.relative_to(base_path)
|
|
855
|
+
|
|
856
|
+
if _create_file(file_path, content, force):
|
|
857
|
+
console.print(f"[green]✅ 创建模型: {rel_path}[/green]")
|
|
858
|
+
_update_init_file(
|
|
859
|
+
code_root / "models" / "__init__.py",
|
|
860
|
+
f"from .{model.file_name} import {model.class_name}",
|
|
861
|
+
model.class_name,
|
|
862
|
+
)
|
|
863
|
+
else:
|
|
864
|
+
console.print(
|
|
865
|
+
f"[yellow]⚠️ 文件已存在: {rel_path}(使用 --force 覆盖)[/yellow]"
|
|
866
|
+
)
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
@app.command(name="repo")
|
|
870
|
+
def generate_repo(
|
|
871
|
+
name: str = typer.Argument(..., help="模型名称"),
|
|
872
|
+
fields: Annotated[
|
|
873
|
+
list[str] | None,
|
|
874
|
+
typer.Argument(help="字段定义(用于生成 get_by_xxx 方法)"),
|
|
875
|
+
] = None,
|
|
876
|
+
force: bool = typer.Option(False, "--force", "-f", help="强制覆盖"),
|
|
877
|
+
) -> None:
|
|
878
|
+
"""生成 Repository 数据访问层。
|
|
879
|
+
|
|
880
|
+
示例:
|
|
881
|
+
aury generate repo user
|
|
882
|
+
aury generate repo user email:str:unique # 生成 get_by_email 方法
|
|
883
|
+
"""
|
|
884
|
+
base_path = Path.cwd()
|
|
885
|
+
|
|
886
|
+
# 读取项目配置
|
|
887
|
+
config = get_project_config(base_path)
|
|
888
|
+
code_root = config.get_package_dir(base_path)
|
|
889
|
+
import_prefix = config.get_import_prefix()
|
|
890
|
+
|
|
891
|
+
field_defs = [FieldDefinition.parse(spec) for spec in (fields or [])]
|
|
892
|
+
# 尝试从已生成的模型文件中读取基类信息
|
|
893
|
+
base_class = _get_base_class_from_model_file(code_root, _to_snake_case(name))
|
|
894
|
+
model = ModelDefinition(name=name, fields=field_defs, base_class=base_class)
|
|
895
|
+
|
|
896
|
+
content = _generate_repository_content(model, import_prefix)
|
|
897
|
+
file_path = code_root / "repositories" / f"{model.file_name}_repository.py"
|
|
898
|
+
rel_path = file_path.relative_to(base_path)
|
|
899
|
+
|
|
900
|
+
if _create_file(file_path, content, force):
|
|
901
|
+
console.print(f"[green]✅ 创建仓储: {rel_path}[/green]")
|
|
902
|
+
_update_init_file(
|
|
903
|
+
code_root / "repositories" / "__init__.py",
|
|
904
|
+
f"from .{model.file_name}_repository import {model.class_name}Repository",
|
|
905
|
+
f"{model.class_name}Repository",
|
|
906
|
+
)
|
|
907
|
+
else:
|
|
908
|
+
console.print("[yellow]⚠️ 文件已存在(使用 --force 覆盖)[/yellow]")
|
|
909
|
+
|
|
910
|
+
|
|
911
|
+
@app.command(name="service")
|
|
912
|
+
def generate_service(
|
|
913
|
+
name: str = typer.Argument(..., help="模型名称"),
|
|
914
|
+
fields: Annotated[
|
|
915
|
+
list[str] | None,
|
|
916
|
+
typer.Argument(help="字段定义(用于生成重复检测)"),
|
|
917
|
+
] = None,
|
|
918
|
+
force: bool = typer.Option(False, "--force", "-f", help="强制覆盖"),
|
|
919
|
+
) -> None:
|
|
920
|
+
"""生成 Service 业务逻辑层。
|
|
921
|
+
|
|
922
|
+
示例:
|
|
923
|
+
aury generate service user
|
|
924
|
+
aury generate service user email:str:unique # 创建时检查 email 重复
|
|
925
|
+
"""
|
|
926
|
+
base_path = Path.cwd()
|
|
927
|
+
|
|
928
|
+
# 读取项目配置
|
|
929
|
+
config = get_project_config(base_path)
|
|
930
|
+
code_root = config.get_package_dir(base_path)
|
|
931
|
+
import_prefix = config.get_import_prefix()
|
|
932
|
+
|
|
933
|
+
field_defs = [FieldDefinition.parse(spec) for spec in (fields or [])]
|
|
934
|
+
# 尝试从已生成的模型文件中读取基类信息
|
|
935
|
+
base_class = _get_base_class_from_model_file(code_root, _to_snake_case(name))
|
|
936
|
+
model = ModelDefinition(name=name, fields=field_defs, base_class=base_class)
|
|
937
|
+
|
|
938
|
+
content = _generate_service_content(model, import_prefix)
|
|
939
|
+
file_path = code_root / "services" / f"{model.file_name}_service.py"
|
|
940
|
+
rel_path = file_path.relative_to(base_path)
|
|
941
|
+
|
|
942
|
+
if _create_file(file_path, content, force):
|
|
943
|
+
console.print(f"[green]✅ 创建服务: {rel_path}[/green]")
|
|
944
|
+
_update_init_file(
|
|
945
|
+
code_root / "services" / "__init__.py",
|
|
946
|
+
f"from .{model.file_name}_service import {model.class_name}Service",
|
|
947
|
+
f"{model.class_name}Service",
|
|
948
|
+
)
|
|
949
|
+
else:
|
|
950
|
+
console.print("[yellow]⚠️ 文件已存在(使用 --force 覆盖)[/yellow]")
|
|
951
|
+
|
|
952
|
+
|
|
953
|
+
def _register_router_in_api_init(code_root: Path, model: ModelDefinition) -> bool:
|
|
954
|
+
"""自动在 api/__init__.py 中注册路由。
|
|
955
|
+
|
|
956
|
+
Args:
|
|
957
|
+
code_root: 代码根目录(包含 api/ 的目录)
|
|
958
|
+
model: 模型定义
|
|
959
|
+
|
|
960
|
+
Returns:
|
|
961
|
+
是否成功注册
|
|
962
|
+
"""
|
|
963
|
+
api_init_path = code_root / "api" / "__init__.py"
|
|
964
|
+
if not api_init_path.exists():
|
|
965
|
+
return False
|
|
966
|
+
|
|
967
|
+
content = api_init_path.read_text(encoding="utf-8")
|
|
968
|
+
|
|
969
|
+
# 检查是否已经注册
|
|
970
|
+
import_line = f"from . import {model.file_name}"
|
|
971
|
+
router_line = f"router.include_router({model.file_name}.router)"
|
|
972
|
+
|
|
973
|
+
if import_line in content or f"{model.file_name}.router" in content:
|
|
974
|
+
return False # 已经注册
|
|
975
|
+
|
|
976
|
+
# 查找插入位置:在 "# 注册子路由" 标记之后
|
|
977
|
+
marker = "# 注册子路由"
|
|
978
|
+
|
|
979
|
+
if marker not in content:
|
|
980
|
+
return False
|
|
981
|
+
|
|
982
|
+
try:
|
|
983
|
+
# 在标记之后插入
|
|
984
|
+
lines = content.split("\n")
|
|
985
|
+
new_lines = []
|
|
986
|
+
inserted = False
|
|
987
|
+
|
|
988
|
+
for line in lines:
|
|
989
|
+
new_lines.append(line)
|
|
990
|
+
if marker in line and not inserted:
|
|
991
|
+
new_lines.append(import_line)
|
|
992
|
+
new_lines.append(router_line)
|
|
993
|
+
inserted = True
|
|
994
|
+
|
|
995
|
+
if inserted:
|
|
996
|
+
api_init_path.write_text("\n".join(new_lines), encoding="utf-8")
|
|
997
|
+
return True
|
|
998
|
+
except Exception:
|
|
999
|
+
pass # 插入失败不影响主流程
|
|
1000
|
+
|
|
1001
|
+
return False
|
|
1002
|
+
|
|
1003
|
+
|
|
1004
|
+
@app.command(name="api")
|
|
1005
|
+
def generate_api(
|
|
1006
|
+
name: str = typer.Argument(..., help="模型名称"),
|
|
1007
|
+
force: bool = typer.Option(False, "--force", "-f", help="强制覆盖"),
|
|
1008
|
+
no_register: bool = typer.Option(False, "--no-register", help="不自动注册到 api/__init__.py"),
|
|
1009
|
+
) -> None:
|
|
1010
|
+
"""生成 FastAPI 路由。
|
|
1011
|
+
|
|
1012
|
+
示例:
|
|
1013
|
+
aury generate api user
|
|
1014
|
+
aury generate api user --no-register # 不自动注册到 api/__init__.py
|
|
1015
|
+
"""
|
|
1016
|
+
base_path = Path.cwd()
|
|
1017
|
+
|
|
1018
|
+
# 读取项目配置
|
|
1019
|
+
config = get_project_config(base_path)
|
|
1020
|
+
code_root = config.get_package_dir(base_path)
|
|
1021
|
+
import_prefix = config.get_import_prefix()
|
|
1022
|
+
|
|
1023
|
+
# 尝试从已生成的模型文件中读取基类信息
|
|
1024
|
+
base_class = _get_base_class_from_model_file(code_root, _to_snake_case(name))
|
|
1025
|
+
model = ModelDefinition(name=name, base_class=base_class)
|
|
1026
|
+
|
|
1027
|
+
content = _generate_api_content(model, import_prefix)
|
|
1028
|
+
file_path = code_root / "api" / f"{model.file_name}.py"
|
|
1029
|
+
rel_path = file_path.relative_to(base_path)
|
|
1030
|
+
|
|
1031
|
+
if _create_file(file_path, content, force):
|
|
1032
|
+
console.print(f"[green]✅ 创建 API: {rel_path}[/green]")
|
|
1033
|
+
|
|
1034
|
+
# 自动注册到 api/__init__.py
|
|
1035
|
+
if not no_register:
|
|
1036
|
+
if _register_router_in_api_init(code_root, model):
|
|
1037
|
+
console.print("[green]✅ 已自动注册到 api/__init__.py[/green]")
|
|
1038
|
+
else:
|
|
1039
|
+
console.print("[dim]提示: 请在 api/__init__.py 中注册路由:[/dim]")
|
|
1040
|
+
console.print(f"[dim] from . import {model.file_name}[/dim]")
|
|
1041
|
+
console.print(f"[dim] router.include_router({model.file_name}.router)[/dim]")
|
|
1042
|
+
else:
|
|
1043
|
+
console.print("[yellow]⚠️ 文件已存在(使用 --force 覆盖)[/yellow]")
|
|
1044
|
+
|
|
1045
|
+
|
|
1046
|
+
@app.command(name="schema")
|
|
1047
|
+
def generate_schema(
|
|
1048
|
+
name: str = typer.Argument(..., help="模型名称"),
|
|
1049
|
+
fields: Annotated[
|
|
1050
|
+
list[str] | None,
|
|
1051
|
+
typer.Argument(help="字段定义"),
|
|
1052
|
+
] = None,
|
|
1053
|
+
interactive: bool = typer.Option(
|
|
1054
|
+
False, "--interactive", "-i", help="交互式添加字段"
|
|
1055
|
+
),
|
|
1056
|
+
force: bool = typer.Option(False, "--force", "-f", help="强制覆盖"),
|
|
1057
|
+
) -> None:
|
|
1058
|
+
"""生成 Pydantic Schema。
|
|
1059
|
+
|
|
1060
|
+
示例:
|
|
1061
|
+
aury generate schema user
|
|
1062
|
+
aury generate schema user email:str:unique age:int?
|
|
1063
|
+
"""
|
|
1064
|
+
base_path = Path.cwd()
|
|
1065
|
+
|
|
1066
|
+
# 读取项目配置
|
|
1067
|
+
config = get_project_config(base_path)
|
|
1068
|
+
code_root = config.get_package_dir(base_path)
|
|
1069
|
+
|
|
1070
|
+
field_defs: list[FieldDefinition] = []
|
|
1071
|
+
if interactive:
|
|
1072
|
+
field_defs = _collect_fields_interactive()
|
|
1073
|
+
elif fields:
|
|
1074
|
+
field_defs = [FieldDefinition.parse(spec) for spec in fields]
|
|
1075
|
+
|
|
1076
|
+
# 尝试从已生成的模型文件中读取基类信息
|
|
1077
|
+
base_class = _get_base_class_from_model_file(code_root, _to_snake_case(name))
|
|
1078
|
+
model = ModelDefinition(name=name, fields=field_defs, base_class=base_class)
|
|
1079
|
+
|
|
1080
|
+
content = _generate_schema_content(model)
|
|
1081
|
+
file_path = code_root / "schemas" / f"{model.file_name}.py"
|
|
1082
|
+
rel_path = file_path.relative_to(base_path)
|
|
1083
|
+
|
|
1084
|
+
if _create_file(file_path, content, force):
|
|
1085
|
+
console.print(f"[green]✅ 创建 Schema: {rel_path}[/green]")
|
|
1086
|
+
_update_init_file(
|
|
1087
|
+
code_root / "schemas" / "__init__.py",
|
|
1088
|
+
f"from .{model.file_name} import {model.class_name}Create, {model.class_name}Response, {model.class_name}Update",
|
|
1089
|
+
f"{model.class_name}Create",
|
|
1090
|
+
)
|
|
1091
|
+
else:
|
|
1092
|
+
console.print("[yellow]⚠️ 文件已存在(使用 --force 覆盖)[/yellow]")
|
|
1093
|
+
|
|
1094
|
+
|
|
1095
|
+
@app.command(name="crud")
|
|
1096
|
+
def generate_crud(
|
|
1097
|
+
name: str = typer.Argument(..., help="模型名称"),
|
|
1098
|
+
fields: Annotated[
|
|
1099
|
+
list[str] | None,
|
|
1100
|
+
typer.Argument(help="字段定义"),
|
|
1101
|
+
] = None,
|
|
1102
|
+
interactive: bool = typer.Option(
|
|
1103
|
+
False, "--interactive", "-i", help="交互式添加字段"
|
|
1104
|
+
),
|
|
1105
|
+
base: str | None = typer.Option(
|
|
1106
|
+
None, "--base", "-b",
|
|
1107
|
+
help="模型基类(Model/UUIDModel/UUIDAuditableStateModel/VersionedModel 等)"
|
|
1108
|
+
),
|
|
1109
|
+
force: bool = typer.Option(False, "--force", "-f", help="强制覆盖"),
|
|
1110
|
+
no_soft_delete: bool = typer.Option(False, "--no-soft-delete", help="禁用软删除"),
|
|
1111
|
+
no_timestamps: bool = typer.Option(False, "--no-timestamps", help="禁用时间戳"),
|
|
1112
|
+
) -> None:
|
|
1113
|
+
"""一键生成完整 CRUD(model + repo + service + api + schema)。
|
|
1114
|
+
|
|
1115
|
+
支持两种模式:
|
|
1116
|
+
|
|
1117
|
+
1. 命令行参数(AI 友好):
|
|
1118
|
+
aury generate crud user email:str:unique age:int? status:str=active
|
|
1119
|
+
|
|
1120
|
+
2. 交互式(人类友好):
|
|
1121
|
+
aury generate crud user -i
|
|
1122
|
+
|
|
1123
|
+
示例:
|
|
1124
|
+
aury generate crud user
|
|
1125
|
+
aury generate crud user --base Model # 使用 int 主键
|
|
1126
|
+
aury generate crud user --base UUIDAuditableStateModel # 使用 UUID 主键(推荐)
|
|
1127
|
+
aury generate crud user email:str:unique age:int? --force
|
|
1128
|
+
aury generate crud article title:str(200) content:text status:str=draft
|
|
1129
|
+
"""
|
|
1130
|
+
base_path = Path.cwd()
|
|
1131
|
+
|
|
1132
|
+
# 读取项目配置
|
|
1133
|
+
config = get_project_config(base_path)
|
|
1134
|
+
code_root = config.get_package_dir(base_path)
|
|
1135
|
+
|
|
1136
|
+
# 解析字段和基类
|
|
1137
|
+
field_defs: list[FieldDefinition] = []
|
|
1138
|
+
selected_base_class: str | None = base
|
|
1139
|
+
|
|
1140
|
+
if interactive:
|
|
1141
|
+
# 交互式模式:先选择基类,再添加字段
|
|
1142
|
+
selected_base_class = _collect_base_class_interactive()
|
|
1143
|
+
field_defs = _collect_fields_interactive()
|
|
1144
|
+
elif fields:
|
|
1145
|
+
for spec in fields:
|
|
1146
|
+
try:
|
|
1147
|
+
field_defs.append(FieldDefinition.parse(spec))
|
|
1148
|
+
except Exception as e:
|
|
1149
|
+
console.print(f"[red]❌ 解析字段失败: {spec} - {e}[/red]")
|
|
1150
|
+
raise typer.Exit(1) from e
|
|
1151
|
+
|
|
1152
|
+
# 验证基类名称
|
|
1153
|
+
if selected_base_class and selected_base_class not in MODEL_BASE_CLASSES:
|
|
1154
|
+
console.print(f"[red]❌ 无效的基类: {selected_base_class}[/red]")
|
|
1155
|
+
console.print(f"[dim]可用基类: {', '.join(MODEL_BASE_CLASSES.keys())}[/dim]")
|
|
1156
|
+
raise typer.Exit(1)
|
|
1157
|
+
|
|
1158
|
+
model = ModelDefinition(
|
|
1159
|
+
name=name,
|
|
1160
|
+
fields=field_defs,
|
|
1161
|
+
soft_delete=not no_soft_delete,
|
|
1162
|
+
timestamps=not no_timestamps,
|
|
1163
|
+
base_class=selected_base_class,
|
|
1164
|
+
)
|
|
1165
|
+
|
|
1166
|
+
console.print(
|
|
1167
|
+
Panel.fit(
|
|
1168
|
+
f"[bold cyan]⚡ 生成 CRUD: {model.class_name}[/bold cyan]",
|
|
1169
|
+
border_style="cyan",
|
|
1170
|
+
)
|
|
1171
|
+
)
|
|
1172
|
+
|
|
1173
|
+
# 显示字段信息
|
|
1174
|
+
if model.fields:
|
|
1175
|
+
console.print("\n[bold]字段列表:[/bold]")
|
|
1176
|
+
for f in model.fields:
|
|
1177
|
+
modifiers = []
|
|
1178
|
+
if f.nullable:
|
|
1179
|
+
modifiers.append("nullable")
|
|
1180
|
+
if f.unique:
|
|
1181
|
+
modifiers.append("unique")
|
|
1182
|
+
if f.index:
|
|
1183
|
+
modifiers.append("index")
|
|
1184
|
+
if f.default:
|
|
1185
|
+
modifiers.append(f"default={f.default}")
|
|
1186
|
+
mod_str = f" [{', '.join(modifiers)}]" if modifiers else ""
|
|
1187
|
+
console.print(f" • {f.name}: {f.type_name}{mod_str}")
|
|
1188
|
+
|
|
1189
|
+
console.print()
|
|
1190
|
+
|
|
1191
|
+
# 获取 import_prefix(项目配置已在前面读取)
|
|
1192
|
+
import_prefix = config.get_import_prefix()
|
|
1193
|
+
|
|
1194
|
+
# 生成所有文件,直接使用已创建的 ModelDefinition 对象,确保基类信息一致
|
|
1195
|
+
# 1. 生成 Model
|
|
1196
|
+
model_content = _generate_model_content(model)
|
|
1197
|
+
model_file_path = code_root / "models" / f"{model.file_name}.py"
|
|
1198
|
+
model_rel_path = model_file_path.relative_to(base_path)
|
|
1199
|
+
if _create_file(model_file_path, model_content, force):
|
|
1200
|
+
console.print(f"[green]✅ 创建模型: {model_rel_path}[/green]")
|
|
1201
|
+
_update_init_file(
|
|
1202
|
+
code_root / "models" / "__init__.py",
|
|
1203
|
+
f"from .{model.file_name} import {model.class_name}",
|
|
1204
|
+
model.class_name,
|
|
1205
|
+
)
|
|
1206
|
+
else:
|
|
1207
|
+
console.print(f"[yellow]⚠️ 文件已存在: {model_rel_path}(使用 --force 覆盖)[/yellow]")
|
|
1208
|
+
|
|
1209
|
+
# 2. 生成 Repository
|
|
1210
|
+
repo_content = _generate_repository_content(model, import_prefix)
|
|
1211
|
+
repo_file_path = code_root / "repositories" / f"{model.file_name}_repository.py"
|
|
1212
|
+
repo_rel_path = repo_file_path.relative_to(base_path)
|
|
1213
|
+
if _create_file(repo_file_path, repo_content, force):
|
|
1214
|
+
console.print(f"[green]✅ 创建仓储: {repo_rel_path}[/green]")
|
|
1215
|
+
_update_init_file(
|
|
1216
|
+
code_root / "repositories" / "__init__.py",
|
|
1217
|
+
f"from .{model.file_name}_repository import {model.class_name}Repository",
|
|
1218
|
+
f"{model.class_name}Repository",
|
|
1219
|
+
)
|
|
1220
|
+
else:
|
|
1221
|
+
console.print(f"[yellow]⚠️ 文件已存在: {repo_rel_path}(使用 --force 覆盖)[/yellow]")
|
|
1222
|
+
|
|
1223
|
+
# 3. 生成 Service
|
|
1224
|
+
service_content = _generate_service_content(model, import_prefix)
|
|
1225
|
+
service_file_path = code_root / "services" / f"{model.file_name}_service.py"
|
|
1226
|
+
service_rel_path = service_file_path.relative_to(base_path)
|
|
1227
|
+
if _create_file(service_file_path, service_content, force):
|
|
1228
|
+
console.print(f"[green]✅ 创建服务: {service_rel_path}[/green]")
|
|
1229
|
+
_update_init_file(
|
|
1230
|
+
code_root / "services" / "__init__.py",
|
|
1231
|
+
f"from .{model.file_name}_service import {model.class_name}Service",
|
|
1232
|
+
f"{model.class_name}Service",
|
|
1233
|
+
)
|
|
1234
|
+
else:
|
|
1235
|
+
console.print(f"[yellow]⚠️ 文件已存在: {service_rel_path}(使用 --force 覆盖)[/yellow]")
|
|
1236
|
+
|
|
1237
|
+
# 4. 生成 Schema
|
|
1238
|
+
schema_content = _generate_schema_content(model)
|
|
1239
|
+
schema_file_path = code_root / "schemas" / f"{model.file_name}.py"
|
|
1240
|
+
schema_rel_path = schema_file_path.relative_to(base_path)
|
|
1241
|
+
if _create_file(schema_file_path, schema_content, force):
|
|
1242
|
+
console.print(f"[green]✅ 创建 Schema: {schema_rel_path}[/green]")
|
|
1243
|
+
_update_init_file(
|
|
1244
|
+
code_root / "schemas" / "__init__.py",
|
|
1245
|
+
f"from .{model.file_name} import {model.class_name}Create, {model.class_name}Response, {model.class_name}Update",
|
|
1246
|
+
f"{model.class_name}Create",
|
|
1247
|
+
)
|
|
1248
|
+
else:
|
|
1249
|
+
console.print(f"[yellow]⚠️ 文件已存在: {schema_rel_path}(使用 --force 覆盖)[/yellow]")
|
|
1250
|
+
|
|
1251
|
+
# 5. 生成 API
|
|
1252
|
+
api_content = _generate_api_content(model, import_prefix)
|
|
1253
|
+
api_file_path = code_root / "api" / f"{model.file_name}.py"
|
|
1254
|
+
api_rel_path = api_file_path.relative_to(base_path)
|
|
1255
|
+
if _create_file(api_file_path, api_content, force):
|
|
1256
|
+
console.print(f"[green]✅ 创建 API: {api_rel_path}[/green]")
|
|
1257
|
+
# 自动注册到 api/__init__.py
|
|
1258
|
+
if _register_router_in_api_init(code_root, model):
|
|
1259
|
+
console.print("[green]✅ 已自动注册到 api/__init__.py[/green]")
|
|
1260
|
+
else:
|
|
1261
|
+
console.print("[dim]提示: 请在 api/__init__.py 中注册路由:[/dim]")
|
|
1262
|
+
console.print(f"[dim] from . import {model.file_name}[/dim]")
|
|
1263
|
+
console.print(f"[dim] router.include_router({model.file_name}.router)[/dim]")
|
|
1264
|
+
else:
|
|
1265
|
+
console.print(f"[yellow]⚠️ 文件已存在: {api_rel_path}(使用 --force 覆盖)[/yellow]")
|
|
1266
|
+
|
|
1267
|
+
console.print()
|
|
1268
|
+
console.print("[bold green]✨ CRUD 生成完成![/bold green]")
|
|
1269
|
+
console.print()
|
|
1270
|
+
console.print("[bold]下一步:[/bold]")
|
|
1271
|
+
console.print(" 1. 生成数据库迁移:")
|
|
1272
|
+
console.print(f' [cyan]aury migrate make -m "add {model.file_name} table"[/cyan]')
|
|
1273
|
+
console.print(" 2. 执行迁移:")
|
|
1274
|
+
console.print(" [cyan]aury migrate up[/cyan]")
|
|
1275
|
+
|
|
1276
|
+
|
|
1277
|
+
__all__ = ["app"]
|