Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

添加基本架构代码 #2

Merged
merged 4 commits into from
Dec 7, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
__pycache__/
.idea/
.env
venv/
backend/app/log/
.ruff_cache/
19 changes: 19 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: check-added-large-files
- id: end-of-file-fixer
- id: requirements-txt-fixer
- id: check-yaml

- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.1.6
hooks:
- id: ruff
args:
- '--config'
- '.ruff.toml'
- '--fix'
- '--unsafe-fixes'
- id: ruff-format
30 changes: 30 additions & 0 deletions .ruff.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
line-length = 120
target-version = "py310"
cache-dir = "./.ruff_cache"

[lint]
select = [
"E",
"F",
"I",
"W505",
"SIM101",
"SIM114",
"PGH004",
"PLE1142",
"RUF100",
"F404",
"TCH",
"UP007"
]

[lint.isort]
lines-between-types = 1

[lint.per-file-ignores]
"backend/app/api/v1/*.py" = ["TCH"]
"backend/app/models/*.py" = ["TCH003"]
"backend/app/**/__init__.py" = ["F401"]

[format]
quote-style = "single"
114 changes: 114 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# FastAPI SQLModel Architecture

作为 FastAPI 框架的一个基础项目,基于 python3.10 开发

## 特征

- [x] FastAPI > 0.100.0
- [x] Async design
- [x] Restful API
- [x] SQLAlchemy 2.0
- [x] Pydantic 2.0
- [ ] ......

## TODO

- [ ] Docker

## 使用

> [!WARNING]
> 此过程请格外注意端口占用情况, 特别是 8000, 3306, 6379...

### 1: 传统

1. 安装依赖项

```shell
pip install -r requirements.txt
```

2. 创建一个数据库 `fsm`, 选择 utf8mb4 编码
3. 安装启动 redis
4. 在 `backend/app/` 目录下创建一个 `.env` 文件

```shell
cd backend/app/
touch .env
```

5. 复制 `.env.example` 到 `.env`

```shell
cp .env.example .env
```

6. 数据库迁移 [alembic](https://alembic.sqlalchemy.org/en/latest/tutorial.html)

```shell
cd backend/app/

# 生成迁移文件
alembic revision --autogenerate

# 执行迁移
alembic upgrade head
```

7. 执行 backend/app/main.py 文件启动服务
8. 浏览器访问: http://127.0.0.1:8000/api/v1/docs

---

### 2: docker

[TODO](#TODO)

[//]: # (1. 进入 `docker-compose.yml` 文件所在目录,创建环境变量文件 `.env`)

[//]: # ()
[//]: # ( ```shell)

[//]: # ( dcd deploy/docker-compose/)

[//]: # ( )
[//]: # ( cp .env.server ../../backend/app/.env)

[//]: # ( ```)

[//]: # ()
[//]: # (2. 执行一键启动命令)

[//]: # ()
[//]: # ( ```shell)

[//]: # ( docker-compose up -d --build)

[//]: # ( ```)

[//]: # ()
[//]: # (3. 等待命令自动完成)

[//]: # (4. 浏览器访问:http://127.0.0.1:8000/api/v1/docs)

## 赞助

> 如果此项目能够帮助到你,你可以赞助作者一些咖啡豆表示鼓励 :coffee:

<table>
<tr>
<td><img src="https://github.com/wu-clan/image/blob/master/pay/weixin.jpg?raw=true" width="180px" alt="Wechat"/>
<td><img src="https://github.com/wu-clan/image/blob/master/pay/zfb.jpg?raw=true" width="180px" alt="Alipay"/>
<td><img src="https://github.com/wu-clan/image/blob/master/pay/ERC20.jpg?raw=true" width="180px" alt="0x40D5e2304b452256afD9CE2d3d5531dc8d293138"/>
</tr>
<tr>
<td align="center">微信</td>
<td align="center">支付宝</td>
<td align="center">ERC20</td>
</tr>
</table>

## 许可证

本项目根据 MIT 许可证的条款进行许可

Empty file added backend/app/.env.example
Empty file.
2 changes: 2 additions & 0 deletions backend/app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
2 changes: 2 additions & 0 deletions backend/app/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
13 changes: 13 additions & 0 deletions backend/app/api/routers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from fastapi import APIRouter

from backend.app.api.v1.auth import router as auth_router
from backend.app.api.v1.user import router as user_router
from backend.app.core.conf import settings

v1 = APIRouter(prefix=settings.API_V1_STR)

v1.include_router(auth_router, prefix='/auth', tags=['认证'])

v1.include_router(user_router, prefix='/users', tags=['用户'])
2 changes: 2 additions & 0 deletions backend/app/api/v1/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
11 changes: 11 additions & 0 deletions backend/app/api/v1/auth/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from fastapi import APIRouter

from backend.app.api.v1.auth.auth import router as auth_router
from backend.app.api.v1.auth.captcha import router as captcha_router

router = APIRouter()

router.include_router(auth_router)
router.include_router(captcha_router)
29 changes: 29 additions & 0 deletions backend/app/api/v1/auth/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from fastapi import APIRouter, Depends, Request
from fastapi.security import OAuth2PasswordRequestForm

from backend.app.common.jwt import DependsJwtUser
from backend.app.common.response.response_schema import response_base
from backend.app.schemas.token import Token
from backend.app.schemas.user import Auth2
from backend.app.services.user_service import UserService

router = APIRouter()


@router.post('/swagger_login', summary='swagger 表单登录', description='form 格式登录,仅用于 swagger 文档调试接口')
async def swagger_user_login(form_data: OAuth2PasswordRequestForm = Depends()) -> Token:
token, user = await UserService.login_swagger(form_data=form_data)
return Token(access_token=token, user=user) # type: ignore


@router.post('/login', summary='验证码登录')
async def user_login(request: Request, obj: Auth2) -> Token:
token, user = await UserService.login_captcha(obj=obj, request=request)
return Token(access_token=token, user=user) # type: ignore


@router.post('/logout', summary='登出', dependencies=[DependsJwtUser])
async def user_logout():
return await response_base.success()
22 changes: 22 additions & 0 deletions backend/app/api/v1/auth/captcha.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from fast_captcha import img_captcha
from fastapi import APIRouter, Depends, Request
from fastapi_limiter.depends import RateLimiter
from starlette.concurrency import run_in_threadpool
from starlette.responses import StreamingResponse

from backend.app.common.redis import redis_client
from backend.app.core.conf import settings
from backend.app.utils.generate_string import get_uuid4_str

router = APIRouter()


@router.get('/captcha', summary='获取验证码', dependencies=[Depends(RateLimiter(times=5, seconds=10))])
async def get_captcha(request: Request):
img, code = await run_in_threadpool(img_captcha)
uuid = get_uuid4_str()
request.app.state.captcha_uuid = uuid
await redis_client.set(uuid, code, settings.CAPTCHA_EXPIRATION_TIME)
return StreamingResponse(content=img, media_type='image/jpeg')
15 changes: 15 additions & 0 deletions backend/app/api/v1/auth/response.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
## A note about the response

由于 SQLModel 在 exec() 方法执行时,返回类型为

`TupleResult[_TSelectParam], ScalarResult[_TSelectParam]`

,但是实际返回类型应为 `_RT = TypeVar("_RT", bound="Result[Any]")`

不能确定当前返回类型是否包含 `rowcount` 属性,可以肯定的是 `Result[Any]` 包含此属性

这里并没有对接口进行测试,感兴趣的话可以尝试一下,如果可行,可以统一接口返回结构,

请参考:[fastapi-practices](https://github.com/fastapi-practices) 中其他相关脚手架

如果可行,请注意 crud 中的方法返回类型及返回值
80 changes: 80 additions & 0 deletions backend/app/api/v1/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Annotated

from fastapi import APIRouter, Query
from sqlmodel.ext.asyncio.session import AsyncSession

from backend.app.common.jwt import CurrentUser, DependsJwtUser
from backend.app.common.pagination import PageDepends, paging_data
from backend.app.common.response.response_schema import response_base
from backend.app.database.db_mysql import async_engine
from backend.app.schemas.user import Avatar, CreateUser, GetUserInfo, ResetPassword, UpdateUser
from backend.app.services.user_service import UserService

router = APIRouter()


@router.post('/register', summary='用户注册')
async def user_register(obj: CreateUser):
await UserService.register(obj=obj)
return await response_base.success()


@router.post('/password/reset', summary='密码重置', dependencies=[DependsJwtUser])
async def password_reset(obj: ResetPassword):
await UserService.pwd_reset(obj=obj)
return await response_base.success()


@router.get('/{username}', summary='查看用户信息', dependencies=[DependsJwtUser])
async def get_user(username: str):
data = await UserService.get_userinfo(username=username)
return await response_base.success(data=data)


@router.put('/{username}', summary='更新用户信息', dependencies=[DependsJwtUser])
async def update_userinfo(username: str, obj: UpdateUser):
await UserService.update(username=username, obj=obj)
return await response_base.success()


@router.put('/{username}/avatar', summary='更新头像', dependencies=[DependsJwtUser])
async def update_avatar(username: str, avatar: Avatar):
await UserService.update_avatar(username=username, avatar=avatar)
return await response_base.success()


@router.get('', summary='(模糊条件)分页获取所有用户', dependencies=[DependsJwtUser, PageDepends])
async def get_all_users(
username: Annotated[str | None, Query()] = None,
phone: Annotated[str | None, Query()] = None,
status: Annotated[int | None, Query()] = None,
):
async with AsyncSession(async_engine) as db:
user_select = await UserService.get_select(username=username, phone=phone, status=status)
page_data = await paging_data(db, user_select, GetUserInfo)
return await response_base.success(data=page_data)


@router.put('/{pk}/super', summary='修改用户超级权限', dependencies=[DependsJwtUser])
async def super_set(current_user: CurrentUser, pk: int):
await UserService.update_permission(current_user=current_user, pk=pk)
return await response_base.success()


@router.put('/{pk}/status', summary='修改用户状态', dependencies=[DependsJwtUser])
async def status_set(current_user: CurrentUser, pk: int):
await UserService.update_status(current_user=current_user, pk=pk)
return await response_base.success()


@router.delete(
path='/{username}',
summary='用户注销',
description='用户注销 != 用户登出,注销之后用户将从数据库删除',
dependencies=[DependsJwtUser],
)
async def delete_user(current_user: CurrentUser, username: str):
await UserService.delete(current_user=current_user, username=username)
return await response_base.success()
2 changes: 2 additions & 0 deletions backend/app/common/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
27 changes: 27 additions & 0 deletions backend/app/common/enums.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from enum import Enum
from enum import IntEnum as SourceIntEnum
from typing import Type


class _EnumBase:
@classmethod
def get_member_keys(cls: Type[Enum]) -> list[str]:
return [name for name in cls.__members__.keys()]

@classmethod
def get_member_values(cls: Type[Enum]) -> list:
return [item.value for item in cls.__members__.values()]


class IntEnum(_EnumBase, SourceIntEnum):
"""整型枚举"""

pass


class StrEnum(_EnumBase, str, Enum):
"""字符串枚举"""

pass
2 changes: 2 additions & 0 deletions backend/app/common/exception/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
Loading
Oops, something went wrong.