fastapp-cli 0.1.0__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.
- fastapp_cli/__init__.py +3 -0
- fastapp_cli/create.py +157 -0
- fastapp_cli/main.py +33 -0
- fastapp_cli/naming.py +39 -0
- fastapp_cli/prompts.py +35 -0
- fastapp_cli/render.py +130 -0
- fastapp_cli/templates/__init__.py +1 -0
- fastapp_cli/templates/project/.env.development.example.j2 +21 -0
- fastapp_cli/templates/project/.env.example.j2 +29 -0
- fastapp_cli/templates/project/.env.j2 +27 -0
- fastapp_cli/templates/project/.gitignore +178 -0
- fastapp_cli/templates/project/.pre-commit-config.yaml.j2 +80 -0
- fastapp_cli/templates/project/.python-version.j2 +1 -0
- fastapp_cli/templates/project/Dockerfile.j2 +17 -0
- fastapp_cli/templates/project/Makefile.j2 +31 -0
- fastapp_cli/templates/project/README.md.j2 +68 -0
- fastapp_cli/templates/project/alembic/env.py.j2 +84 -0
- fastapp_cli/templates/project/alembic/script.py.mako +28 -0
- fastapp_cli/templates/project/alembic/versions/.gitkeep +0 -0
- fastapp_cli/templates/project/alembic.ini.j2 +50 -0
- fastapp_cli/templates/project/app/__init__.py.j2 +1 -0
- fastapp_cli/templates/project/app/api/__init__.py.j2 +1 -0
- fastapp_cli/templates/project/app/api/deps.py.j2 +33 -0
- fastapp_cli/templates/project/app/api/v1/__init__.py.j2 +1 -0
- fastapp_cli/templates/project/app/api/v1/endpoints/__init__.py.j2 +1 -0
- fastapp_cli/templates/project/app/api/v1/endpoints/health.py.j2 +33 -0
- fastapp_cli/templates/project/app/api/v1/endpoints/items.py.j2 +90 -0
- fastapp_cli/templates/project/app/api/v1/router.py.j2 +9 -0
- fastapp_cli/templates/project/app/core/__init__.py.j2 +1 -0
- fastapp_cli/templates/project/app/core/celery_app.py.j2 +62 -0
- fastapp_cli/templates/project/app/core/config.py.j2 +105 -0
- fastapp_cli/templates/project/app/core/context_var.py.j2 +13 -0
- fastapp_cli/templates/project/app/core/database.py.j2 +50 -0
- fastapp_cli/templates/project/app/core/exceptions.py.j2 +175 -0
- fastapp_cli/templates/project/app/core/logging.py.j2 +125 -0
- fastapp_cli/templates/project/app/core/middleware.py.j2 +39 -0
- fastapp_cli/templates/project/app/crud/__init__.py.j2 +1 -0
- fastapp_cli/templates/project/app/crud/base.py.j2 +229 -0
- fastapp_cli/templates/project/app/crud/item.py.j2 +10 -0
- fastapp_cli/templates/project/app/main.py.j2 +118 -0
- fastapp_cli/templates/project/app/models/__init__.py.j2 +10 -0
- fastapp_cli/templates/project/app/models/base.py.j2 +59 -0
- fastapp_cli/templates/project/app/models/item.py.j2 +22 -0
- fastapp_cli/templates/project/app/schemas/__init__.py.j2 +1 -0
- fastapp_cli/templates/project/app/schemas/common.py.j2 +81 -0
- fastapp_cli/templates/project/app/schemas/item.py.j2 +35 -0
- fastapp_cli/templates/project/app/services/__init__.py.j2 +1 -0
- fastapp_cli/templates/project/app/services/base.py.j2 +79 -0
- fastapp_cli/templates/project/app/services/item_service.py.j2 +10 -0
- fastapp_cli/templates/project/app/tasks/__init__.py.j2 +1 -0
- fastapp_cli/templates/project/app/tasks/sample_tasks.py.j2 +28 -0
- fastapp_cli/templates/project/app/utils/__init__.py.j2 +1 -0
- fastapp_cli/templates/project/docs/SQLModel/345/256/232/344/271/211/347/244/272/344/276/213.md +400 -0
- fastapp_cli/templates/project/pm2.config.json.j2 +47 -0
- fastapp_cli/templates/project/pyproject.toml.j2 +195 -0
- fastapp_cli/templates/project/scripts/celery_beat.sh.j2 +9 -0
- fastapp_cli/templates/project/scripts/celery_flower.sh.j2 +22 -0
- fastapp_cli/templates/project/scripts/celery_worker.sh.j2 +15 -0
- fastapp_cli/templates/project/scripts/start.sh.j2 +17 -0
- fastapp_cli/templates/project/tests/api/test_health.py.j2 +15 -0
- fastapp_cli/templates/project/tests/api/test_items.py.j2 +61 -0
- fastapp_cli/templates/project/tests/conftest.py.j2 +61 -0
- fastapp_cli/templates/project/tests/services/test_item_service.py.j2 +44 -0
- fastapp_cli-0.1.0.dist-info/METADATA +102 -0
- fastapp_cli-0.1.0.dist-info/RECORD +67 -0
- fastapp_cli-0.1.0.dist-info/WHEEL +4 -0
- fastapp_cli-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Celery 示例任务."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
from loguru import logger
|
|
8
|
+
|
|
9
|
+
from app.core.celery_app import celery_app
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@celery_app.task(
|
|
13
|
+
name="app.tasks.sample_tasks.sample_task",
|
|
14
|
+
bind=True,
|
|
15
|
+
autoretry_for=(Exception,),
|
|
16
|
+
retry_kwargs={"max_retries": 3, "countdown": 10},
|
|
17
|
+
)
|
|
18
|
+
def sample_task(self, name: str = "world") -> dict:
|
|
19
|
+
"""示例任务:模拟耗时操作并返回结果.
|
|
20
|
+
|
|
21
|
+
测试方式(需 redis 可达)::
|
|
22
|
+
|
|
23
|
+
uv run celery -A app.core.celery_app.celery_app worker -l INFO
|
|
24
|
+
uv run python -c "from app.tasks.sample_tasks import sample_task; print(sample_task.delay('fastapp').id)"
|
|
25
|
+
"""
|
|
26
|
+
logger.info("[task={}] sample_task running, name={}", self.request.id, name)
|
|
27
|
+
time.sleep(1)
|
|
28
|
+
return {"message": f"hello, {name}"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""app · utils"""
|
fastapp_cli/templates/project/docs/SQLModel/345/256/232/344/271/211/347/244/272/344/276/213.md
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
# SQLModel 定义模型的经典示例
|
|
2
|
+
|
|
3
|
+
## 1. 基础模型
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
from sqlmodel import SQLModel, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Hero(SQLModel, table=True):
|
|
10
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
11
|
+
name: str = Field(index=True)
|
|
12
|
+
secret_name: str
|
|
13
|
+
age: int | None = Field(default=None, index=True)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
**要点**:
|
|
17
|
+
|
|
18
|
+
- `table=True` 才会建表;没有则只是 Pydantic 模型
|
|
19
|
+
- `id: int | None` + `default=None` 是自增主键的标准写法
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 2. 字段约束与索引
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from sqlmodel import SQLModel, Field
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class User(SQLModel, table=True):
|
|
30
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
31
|
+
username: str = Field(min_length=3, max_length=32, unique=True, index=True)
|
|
32
|
+
email: str = Field(unique=True, index=True)
|
|
33
|
+
age: int = Field(ge=0, le=150)
|
|
34
|
+
bio: str | None = Field(default=None, max_length=500)
|
|
35
|
+
is_active: bool = Field(default=True)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 3. 时间戳与默认值
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from datetime import datetime, timezone
|
|
44
|
+
from sqlalchemy import DateTime, func
|
|
45
|
+
from sqlmodel import SQLModel, Field
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def utcnow():
|
|
49
|
+
return datetime.now(tz=timezone.utc)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Article(SQLModel, table=True):
|
|
53
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
54
|
+
title: str
|
|
55
|
+
# Python 端默认值
|
|
56
|
+
created_at: datetime = Field(default_factory=utcnow)
|
|
57
|
+
# 数据库端默认值 + 自动更新
|
|
58
|
+
updated_at: datetime = Field(
|
|
59
|
+
default_factory=utcnow,
|
|
60
|
+
sa_type=DateTime(timezone=True),
|
|
61
|
+
sa_column_kwargs={"onupdate": func.now()},
|
|
62
|
+
)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 4. 枚举字段
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from enum import Enum
|
|
71
|
+
from sqlmodel import SQLModel, Field
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class Status(str, Enum):
|
|
75
|
+
DRAFT = "draft"
|
|
76
|
+
PUBLISHED = "published"
|
|
77
|
+
ARCHIVED = "archived"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class Post(SQLModel, table=True):
|
|
81
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
82
|
+
title: str
|
|
83
|
+
status: Status = Field(default=Status.DRAFT, index=True)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## 5. 一对多关系
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from sqlmodel import SQLModel, Field, Relationship
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class Team(SQLModel, table=True):
|
|
95
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
96
|
+
name: str = Field(index=True, unique=True)
|
|
97
|
+
|
|
98
|
+
heroes: list["Hero"] = Relationship(back_populates="team")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class Hero(SQLModel, table=True):
|
|
102
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
103
|
+
name: str
|
|
104
|
+
team_id: int | None = Field(default=None, foreign_key="team.id", index=True)
|
|
105
|
+
|
|
106
|
+
team: Team | None = Relationship(back_populates="heroes")
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**使用**:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
team = Team(name="Avengers", heroes=[Hero(name="Iron Man")])
|
|
113
|
+
session.add(team)
|
|
114
|
+
session.commit()
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## 6. 多对多关系(需关联表)
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from sqlmodel import SQLModel, Field, Relationship
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class HeroTeamLink(SQLModel, table=True):
|
|
126
|
+
hero_id: int | None = Field(default=None, foreign_key="hero.id", primary_key=True)
|
|
127
|
+
team_id: int | None = Field(default=None, foreign_key="team.id", primary_key=True)
|
|
128
|
+
# 可加额外字段
|
|
129
|
+
joined_at: datetime = Field(default_factory=utcnow)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class Team(SQLModel, table=True):
|
|
133
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
134
|
+
name: str
|
|
135
|
+
heroes: list["Hero"] = Relationship(back_populates="teams", link_model=HeroTeamLink)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class Hero(SQLModel, table=True):
|
|
139
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
140
|
+
name: str
|
|
141
|
+
teams: list[Team] = Relationship(back_populates="heroes", link_model=HeroTeamLink)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## 7. Mixin 组合(推荐项目结构)
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
from datetime import datetime
|
|
150
|
+
from sqlalchemy import DateTime, func
|
|
151
|
+
from sqlmodel import SQLModel, Field
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class IDMixin(SQLModel):
|
|
155
|
+
id: int | None = Field(default=None, primary_key=True, index=True)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class TimestampMixin(SQLModel):
|
|
159
|
+
created_at: datetime = Field(
|
|
160
|
+
default_factory=utcnow,
|
|
161
|
+
sa_type=DateTime(timezone=True),
|
|
162
|
+
sa_column_kwargs={"server_default": func.now(), "nullable": False},
|
|
163
|
+
)
|
|
164
|
+
updated_at: datetime = Field(
|
|
165
|
+
default_factory=utcnow,
|
|
166
|
+
sa_type=DateTime(timezone=True),
|
|
167
|
+
sa_column_kwargs={
|
|
168
|
+
"server_default": func.now(),
|
|
169
|
+
"onupdate": func.now(),
|
|
170
|
+
"nullable": False,
|
|
171
|
+
},
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class SoftDeleteMixin(SQLModel):
|
|
176
|
+
deleted_at: datetime | None = Field(default=None, index=True)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# 组合使用
|
|
180
|
+
class User(IDMixin, TimestampMixin, SoftDeleteMixin, table=True):
|
|
181
|
+
username: str = Field(unique=True, index=True)
|
|
182
|
+
email: str
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## 8. 读写模型分离(API 最佳实践)
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
# 共享基类(不建表)
|
|
191
|
+
class UserBase(SQLModel):
|
|
192
|
+
username: str = Field(index=True)
|
|
193
|
+
email: str
|
|
194
|
+
age: int | None = None
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# 数据库表
|
|
198
|
+
class User(UserBase, table=True):
|
|
199
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
200
|
+
hashed_password: str # 数据库独有
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# 创建请求(API 输入)
|
|
204
|
+
class UserCreate(UserBase):
|
|
205
|
+
password: str # 明文密码,仅创建时
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# 响应(API 输出,不含敏感字段)
|
|
209
|
+
class UserRead(UserBase):
|
|
210
|
+
id: int
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# 更新(字段全部可选)
|
|
214
|
+
class UserUpdate(SQLModel):
|
|
215
|
+
username: str | None = None
|
|
216
|
+
email: str | None = None
|
|
217
|
+
age: int | None = None
|
|
218
|
+
password: str | None = None
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**FastAPI 中使用**:
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
@app.post("/users/", response_model=UserRead)
|
|
225
|
+
def create_user(user: UserCreate, session: Session = Depends(get_session)):
|
|
226
|
+
db_user = User.model_validate(user, update={"hashed_password": hash(user.password)})
|
|
227
|
+
session.add(db_user)
|
|
228
|
+
session.commit()
|
|
229
|
+
session.refresh(db_user)
|
|
230
|
+
return db_user
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## 9. 复合索引与表参数
|
|
236
|
+
|
|
237
|
+
```python
|
|
238
|
+
from sqlalchemy import Index, UniqueConstraint
|
|
239
|
+
from sqlmodel import SQLModel, Field
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class OrderItem(SQLModel, table=True):
|
|
243
|
+
__tablename__ = "order_items"
|
|
244
|
+
__table_args__ = (
|
|
245
|
+
UniqueConstraint("order_id", "product_id", name="uq_order_product"),
|
|
246
|
+
Index("ix_order_product", "order_id", "product_id"),
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
250
|
+
order_id: int = Field(foreign_key="orders.id")
|
|
251
|
+
product_id: int = Field(foreign_key="products.id")
|
|
252
|
+
quantity: int
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## 10. JSON 字段
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
from sqlalchemy import JSON
|
|
261
|
+
from sqlmodel import SQLModel, Field
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class Config(SQLModel, table=True):
|
|
265
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
266
|
+
name: str
|
|
267
|
+
# 存储任意 JSON 数据
|
|
268
|
+
data: dict = Field(default_factory=dict, sa_type=JSON)
|
|
269
|
+
tags: list[str] = Field(default_factory=list, sa_type=JSON)
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## 11. UUID 主键
|
|
275
|
+
|
|
276
|
+
```python
|
|
277
|
+
from uuid import UUID, uuid4
|
|
278
|
+
from sqlmodel import SQLModel, Field
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
class Session(SQLModel, table=True):
|
|
282
|
+
id: UUID = Field(default_factory=uuid4, primary_key=True)
|
|
283
|
+
user_id: int = Field(foreign_key="user.id")
|
|
284
|
+
token: str = Field(unique=True, index=True)
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## 12. 自引用关系(树形结构)
|
|
290
|
+
|
|
291
|
+
```python
|
|
292
|
+
from sqlmodel import SQLModel, Field, Relationship
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
class Category(SQLModel, table=True):
|
|
296
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
297
|
+
name: str
|
|
298
|
+
parent_id: int | None = Field(default=None, foreign_key="category.id")
|
|
299
|
+
|
|
300
|
+
parent: "Category | None" = Relationship(
|
|
301
|
+
back_populates="children",
|
|
302
|
+
sa_relationship_kwargs={"remote_side": "Category.id"},
|
|
303
|
+
)
|
|
304
|
+
children: list["Category"] = Relationship(back_populates="parent")
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## 全局配置 + 自定义类型(项目统一风格)
|
|
308
|
+
|
|
309
|
+
```python
|
|
310
|
+
from datetime import datetime
|
|
311
|
+
from typing import Annotated
|
|
312
|
+
from pydantic import PlainSerializer
|
|
313
|
+
from sqlalchemy import DateTime, func
|
|
314
|
+
from sqlmodel import SQLModel, Field
|
|
315
|
+
|
|
316
|
+
# 定义一个可复用的序列化类型
|
|
317
|
+
FormattedDatetime = Annotated[
|
|
318
|
+
datetime,
|
|
319
|
+
PlainSerializer(
|
|
320
|
+
lambda v: v.strftime("%Y-%m-%d %H:%M:%S") if v else None,
|
|
321
|
+
return_type=str,
|
|
322
|
+
when_used="json", # 仅 JSON 序列化时生效;数据库仍存 datetime
|
|
323
|
+
),
|
|
324
|
+
]
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
class TimestampMixin(SQLModel):
|
|
328
|
+
created_at: FormattedDatetime = Field(
|
|
329
|
+
default_factory=utcnow,
|
|
330
|
+
sa_type=DateTime(timezone=True),
|
|
331
|
+
sa_column_kwargs={"server_default": func.now(), "nullable": False},
|
|
332
|
+
)
|
|
333
|
+
updated_at: FormattedDatetime = Field(
|
|
334
|
+
default_factory=utcnow,
|
|
335
|
+
sa_type=DateTime(timezone=True),
|
|
336
|
+
sa_column_kwargs={
|
|
337
|
+
"server_default": func.now(),
|
|
338
|
+
"onupdate": func.now(),
|
|
339
|
+
"nullable": False,
|
|
340
|
+
},
|
|
341
|
+
)
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
**优点**:
|
|
345
|
+
|
|
346
|
+
- 类型可复用,多个字段共用一条规则
|
|
347
|
+
- `when_used="json"` 保证只影响 JSON 输出,不影响数据库存取
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
## 使用 Pydantic 模型做 JSON 结构校验
|
|
351
|
+
|
|
352
|
+
```python
|
|
353
|
+
from pydantic import BaseModel
|
|
354
|
+
from sqlalchemy import JSON
|
|
355
|
+
from sqlmodel import SQLModel, Field
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
class Author(BaseModel):
|
|
359
|
+
name: str
|
|
360
|
+
email: str
|
|
361
|
+
role: str = "writer"
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
class ArticleMeta(BaseModel):
|
|
365
|
+
seo_title: str | None = None
|
|
366
|
+
cover: str | None = None
|
|
367
|
+
reading_time: int = 0
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
class Article(SQLModel, table=True):
|
|
371
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
372
|
+
title: str
|
|
373
|
+
|
|
374
|
+
# 用 Pydantic 模型作为 JSON 字段类型,自动校验结构
|
|
375
|
+
meta: ArticleMeta = Field(
|
|
376
|
+
default_factory=ArticleMeta,
|
|
377
|
+
sa_type=JSON,
|
|
378
|
+
)
|
|
379
|
+
authors: list[Author] = Field(
|
|
380
|
+
default_factory=list,
|
|
381
|
+
sa_type=JSON,
|
|
382
|
+
)
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
## 常用小结
|
|
388
|
+
|
|
389
|
+
| 场景 | 关键参数 |
|
|
390
|
+
|------------|----------------------------------------------------|
|
|
391
|
+
| 自增主键 | `Field(default=None, primary_key=True)` |
|
|
392
|
+
| 索引 | `Field(index=True)` |
|
|
393
|
+
| 唯一 | `Field(unique=True)` |
|
|
394
|
+
| 外键 | `Field(foreign_key="table.id")` |
|
|
395
|
+
| Python 默认值 | `default=` / `default_factory=` |
|
|
396
|
+
| 数据库默认值 | `sa_column_kwargs={"server_default": ...}` |
|
|
397
|
+
| 自动更新 | `sa_column_kwargs={"onupdate": func.now()}` |
|
|
398
|
+
| 自定义列类型 | `sa_type=...` |
|
|
399
|
+
| 完整列控制 | `sa_column=Column(...)` |
|
|
400
|
+
| 关系 | `Relationship(back_populates=..., link_model=...)` |
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{% if use_pm2 %}
|
|
2
|
+
{
|
|
3
|
+
"apps": [
|
|
4
|
+
{
|
|
5
|
+
"name": "{{ project_name }}-server",
|
|
6
|
+
"script": "bash scripts/start.sh",
|
|
7
|
+
"output": "./logs/server.log",
|
|
8
|
+
"error": "./logs/server-error.log",
|
|
9
|
+
"env": {
|
|
10
|
+
"HOST": "0.0.0.0",
|
|
11
|
+
"PORT": "8000",
|
|
12
|
+
"APP_ENV": "production",
|
|
13
|
+
"WORKERS": "1"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"name": "{{ project_name }}-worker",
|
|
18
|
+
"script": "bash scripts/celery_worker.sh",
|
|
19
|
+
"output": "./logs/celery.log",
|
|
20
|
+
"error": "./logs/celery-error.log",
|
|
21
|
+
"env": {
|
|
22
|
+
"CONCURRENCY": "4",
|
|
23
|
+
"QUEUE": "default",
|
|
24
|
+
"LOG_LEVEL": "INFO"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"name": "{{ project_name }}-beat",
|
|
29
|
+
"script": "bash scripts/celery_beat.sh",
|
|
30
|
+
"log_file": "./logs/celery_beat.log",
|
|
31
|
+
"env": {
|
|
32
|
+
"LOG_LEVEL": "INFO"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"name": "{{ project_name }}-flower",
|
|
37
|
+
"script": "bash scripts/celery_flower.sh",
|
|
38
|
+
"log_file": "./logs/flower.log",
|
|
39
|
+
"env": {
|
|
40
|
+
"FLOWER_HOST": "0.0.0.0",
|
|
41
|
+
"FLOWER_PORT": "5555",
|
|
42
|
+
"LOG_LEVEL": "INFO"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
{% endif %}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "{{ project_name }}"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "{{ project_description }}"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
"alembic>=1.18.4",
|
|
10
|
+
"celery[redis]>=5.6.3",
|
|
11
|
+
"fastapi>=0.136.1",
|
|
12
|
+
"fastapi-filter[sqlalchemy]>=2.0.1",
|
|
13
|
+
"fastapi-pagination>=0.15.13",
|
|
14
|
+
"flower>=2.0.1",
|
|
15
|
+
"httpx>=0.28.1",
|
|
16
|
+
"loguru>=0.7.3",
|
|
17
|
+
"pydantic>=2.13.3",
|
|
18
|
+
"pydantic-settings>=2.14.0",
|
|
19
|
+
"pymysql>=1.1.2",
|
|
20
|
+
"sqlalchemy>=2.0.49",
|
|
21
|
+
"sqlmodel>=0.0.38",
|
|
22
|
+
"tenacity>=9.1.4", # retrying library
|
|
23
|
+
"uvicorn[standard]>=0.46.0",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[dependency-groups]
|
|
27
|
+
dev = [
|
|
28
|
+
"mypy>=1.11.0",
|
|
29
|
+
"pre-commit>=4.6.0",
|
|
30
|
+
"pytest>=9.0.3",
|
|
31
|
+
"pytest-asyncio>=1.3.0",
|
|
32
|
+
"pytest-cov>=7.1.0",
|
|
33
|
+
"pytest-mock>=3.15.1",
|
|
34
|
+
"ruff>=0.6.0",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
# -----------------------------------------------------------------------------
|
|
38
|
+
# pytest 测试配置
|
|
39
|
+
# -----------------------------------------------------------------------------
|
|
40
|
+
[tool.pytest.ini_options]
|
|
41
|
+
minversion = "7.0"
|
|
42
|
+
testpaths = ["tests"]
|
|
43
|
+
python_files = ["test_*.py", "*_test.py"]
|
|
44
|
+
# 使测试可从项目根导入 app 包(项目未作为发行包安装时)
|
|
45
|
+
pythonpath = ["."]
|
|
46
|
+
asyncio_mode = "auto"
|
|
47
|
+
addopts = [
|
|
48
|
+
"-ra",
|
|
49
|
+
"--strict-markers",
|
|
50
|
+
"--strict-config",
|
|
51
|
+
"--tb=short",
|
|
52
|
+
]
|
|
53
|
+
markers = [
|
|
54
|
+
"unit: unit tests",
|
|
55
|
+
"integration: integration tests",
|
|
56
|
+
"slow: slow tests",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
# -----------------------------------------------------------------------------
|
|
60
|
+
# uv 镜像源:默认使用 pypi.org;内网用户取消下方注释即可启用腾讯镜像
|
|
61
|
+
# -----------------------------------------------------------------------------
|
|
62
|
+
# [[tool.uv.index]]
|
|
63
|
+
# # 腾讯镜像源,同步自pypi
|
|
64
|
+
# url = "https://mirrors.tencent.com/pypi/simple"
|
|
65
|
+
# name = "mirrors_pypi"
|
|
66
|
+
# default = true
|
|
67
|
+
#
|
|
68
|
+
# [[tool.uv.index]]
|
|
69
|
+
# # 腾讯镜像源,同步自pypi
|
|
70
|
+
# url = "https://mirrors.tencent.com/repository/pypi/tencent_pypi/simple"
|
|
71
|
+
# name = "tencent_pypi"
|
|
72
|
+
|
|
73
|
+
# -----------------------------------------------------------------------------
|
|
74
|
+
# Ruff 配置(linter + formatter,替代 flake8 / isort / black)
|
|
75
|
+
# 参考:https://docs.astral.sh/ruff/
|
|
76
|
+
# -----------------------------------------------------------------------------
|
|
77
|
+
[tool.ruff]
|
|
78
|
+
# 每行最多 120 字符
|
|
79
|
+
line-length = 120
|
|
80
|
+
# 目标 Python 版本(与 requires-python 对齐)
|
|
81
|
+
target-version = "py312"
|
|
82
|
+
# 自动遵循 .gitignore 配置,无需重复配置排除项
|
|
83
|
+
respect-gitignore = true
|
|
84
|
+
extend-exclude = [
|
|
85
|
+
".venv",
|
|
86
|
+
"venv",
|
|
87
|
+
"build",
|
|
88
|
+
"dist",
|
|
89
|
+
"alembic/versions",
|
|
90
|
+
"__pycache__",
|
|
91
|
+
".git",
|
|
92
|
+
".mypy_cache",
|
|
93
|
+
".pytest_cache",
|
|
94
|
+
".ruff_cache",
|
|
95
|
+
]
|
|
96
|
+
# 每个文件都应该可被修复
|
|
97
|
+
fix = true
|
|
98
|
+
show-fixes = true
|
|
99
|
+
|
|
100
|
+
[tool.ruff.lint]
|
|
101
|
+
select = [
|
|
102
|
+
# ========== 必选核心规则(所有项目必须开启,零容忍错误检测) ==========
|
|
103
|
+
"F", # Pyflakes:检测未定义变量、未使用导入、无效语法等致命运行时错误
|
|
104
|
+
"E", # pycodestyle(Error):PEP8语法错误检测
|
|
105
|
+
"W", # pycodestyle(Warning):PEP8规范警告
|
|
106
|
+
"I", # isort:导入排序与分组规范,替代独立isort工具
|
|
107
|
+
# ========== 推荐质量规则(强烈建议开启,提升代码健壮性) ==========
|
|
108
|
+
"B", # flake8-bugbear:捕获基础规则遗漏的常见bug、反模式和陷阱
|
|
109
|
+
"UP", # pyupgrade:自动升级Python语法到目标版本,实现代码现代化
|
|
110
|
+
"SIM", # 代码简化:检测复杂逻辑、冗余判断,提升可读性
|
|
111
|
+
# ========== 可选增强规则(按需开启,团队规范对齐) ==========
|
|
112
|
+
"N", # pep8-naming:强制PEP8命名规范,类/函数/变量命名约束
|
|
113
|
+
"TID", # 导入整洁度:禁止循环导入、无用导入别名等
|
|
114
|
+
"FAST", # FastAPI特定规则
|
|
115
|
+
]
|
|
116
|
+
ignore = [
|
|
117
|
+
"E501", # 行长度超限:完全由Ruff Format控制,lint无需重复检测
|
|
118
|
+
]
|
|
119
|
+
# 允许自动修复的规则
|
|
120
|
+
fixable = ["ALL"]
|
|
121
|
+
unfixable = []
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
[tool.ruff.lint.per-file-ignores]
|
|
125
|
+
# 测试目录放宽规则
|
|
126
|
+
"tests/**/*.py" = ["E", "F", "I", "N", "UP"]
|
|
127
|
+
# alembic 迁移脚本放宽规则
|
|
128
|
+
"alembic/**/*.py" = ["E", "F", "I", "N", "UP"]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
[tool.ruff.format]
|
|
132
|
+
quote-style = "double" # 使用双引号(与 black 行为一致)
|
|
133
|
+
indent-style = "space" # 使用空格缩进
|
|
134
|
+
skip-magic-trailing-comma = false # 保留末尾逗号
|
|
135
|
+
|
|
136
|
+
# -----------------------------------------------------------------------------
|
|
137
|
+
# mypy 配置(仅开启最核心必要的类型检查规则)
|
|
138
|
+
# 参考:https://mypy.readthedocs.io/en/stable/config_file.html
|
|
139
|
+
# -----------------------------------------------------------------------------
|
|
140
|
+
[tool.mypy]
|
|
141
|
+
python_version = "3.12"
|
|
142
|
+
|
|
143
|
+
# ---------- 核心必要检查(捕获真实 bug,不做强约束) ----------
|
|
144
|
+
# 警告未使用的 # type: ignore 注释,保持注释整洁
|
|
145
|
+
warn_unused_ignores = true
|
|
146
|
+
# 警告函数返回了 Any(可能意味着类型推断失效)
|
|
147
|
+
warn_return_any = false # 宽松:第三方库返回 Any 很常见,避免噪音
|
|
148
|
+
# 警告永远不会执行到的代码
|
|
149
|
+
warn_unreachable = true
|
|
150
|
+
# 警告多余的类型转换
|
|
151
|
+
warn_redundant_casts = true
|
|
152
|
+
# 禁止隐式的 Optional(如 def f(x: int = None) 应显式写 Optional[int])
|
|
153
|
+
no_implicit_optional = true
|
|
154
|
+
# 类型不严格,但保证基础一致性
|
|
155
|
+
strict_equality = true
|
|
156
|
+
|
|
157
|
+
# ---------- 宽松策略:不要求所有函数都必须标注类型 ----------
|
|
158
|
+
# 不强制要求所有函数都有类型注解(核心:避免过度约束)
|
|
159
|
+
disallow_untyped_defs = false
|
|
160
|
+
disallow_incomplete_defs = false
|
|
161
|
+
check_untyped_defs = true # 但对无注解的函数体也做一次检查,不放过隐藏 bug
|
|
162
|
+
disallow_untyped_decorators = false
|
|
163
|
+
|
|
164
|
+
# ---------- 报错展示优化 ----------
|
|
165
|
+
show_error_codes = true
|
|
166
|
+
show_column_numbers = true
|
|
167
|
+
pretty = true
|
|
168
|
+
|
|
169
|
+
# ---------- 排除目录 ----------
|
|
170
|
+
exclude = [
|
|
171
|
+
"^\\.venv/",
|
|
172
|
+
"^venv/",
|
|
173
|
+
"^build/",
|
|
174
|
+
"^dist/",
|
|
175
|
+
"^alembic/versions/",
|
|
176
|
+
"^__pycache__/",
|
|
177
|
+
]
|
|
178
|
+
|
|
179
|
+
# ---------- 针对缺少类型存根的第三方库,忽略导入错误 ----------
|
|
180
|
+
# 说明:这些库通常没有官方 type stubs,忽略可以避免 "Cannot find implementation" 报错
|
|
181
|
+
[[tool.mypy.overrides]]
|
|
182
|
+
module = [
|
|
183
|
+
"celery.*",
|
|
184
|
+
"flower.*",
|
|
185
|
+
"sqlmodel.*", # SQLModel 的类型推断对 mypy 不友好,建议忽略
|
|
186
|
+
]
|
|
187
|
+
ignore_missing_imports = true
|
|
188
|
+
|
|
189
|
+
# ---------- 测试和迁移脚本:进一步放宽 ----------
|
|
190
|
+
[[tool.mypy.overrides]]
|
|
191
|
+
module = [
|
|
192
|
+
"tests.*",
|
|
193
|
+
"alembic.*",
|
|
194
|
+
]
|
|
195
|
+
ignore_errors = true
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# 启动 Flower(Celery 任务监控面板)
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
cd "$(dirname "$0")/.."
|
|
6
|
+
|
|
7
|
+
HOST="${FLOWER_HOST:-0.0.0.0}"
|
|
8
|
+
PORT="${FLOWER_PORT:-5555}"
|
|
9
|
+
LOG_LEVEL="${LOG_LEVEL:-INFO}"
|
|
10
|
+
|
|
11
|
+
# 基本认证,格式:user:password,多个用逗号分隔
|
|
12
|
+
# 可在 .env 中设置 FLOWER_BASIC_AUTH=admin:admin
|
|
13
|
+
BASIC_AUTH_ARG=""
|
|
14
|
+
if [ -n "${FLOWER_BASIC_AUTH:-}" ]; then
|
|
15
|
+
BASIC_AUTH_ARG="--basic-auth=${FLOWER_BASIC_AUTH}"
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
exec uv run celery -A app.core.celery_app.celery_app flower \
|
|
19
|
+
--address="${HOST}" \
|
|
20
|
+
--port="${PORT}" \
|
|
21
|
+
--loglevel="${LOG_LEVEL}" \
|
|
22
|
+
${BASIC_AUTH_ARG}
|