Compare commits

..

23 Commits

Author SHA1 Message Date
chenwj113 fc5d6b8953 fix: 更新Dockerfile文件
Gitea Action for docker build runtime / build-runtime (push) Successful in 5m53s Details
2024-09-02 10:11:46 +08:00
chenwj113 6d69a18b03 fix: 运行时环境替换为alpine
Gitea Action for docker build / build-app (push) Successful in 1m23s Details
Gitea Action for docker build runtime / build-runtime (push) Successful in 4m18s Details
2024-08-16 18:37:51 +08:00
chenwj113 61f91fa932 fix: 去除gitea action的pyproject.toml触发path
Gitea Action for docker build / build-app (push) Successful in 1m1s Details
2024-08-15 18:35:00 +08:00
chenweijia ee14c5591a bugfix: 修复gitea action构建脚本的bug
Gitea Action for docker build / build-app (push) Successful in 25s Details
Gitea Action for docker build runtime / build-runtime (push) Successful in 4m17s Details
2024-08-15 17:11:05 +08:00
chenweijia dce678ca95 fix: 更改gitea action获取版本号的方式
Gitea Action for docker build runtime / build-runtime (push) Successful in 4m27s Details
Gitea Action for docker build / build-app (push) Successful in 33s Details
2024-08-15 17:03:16 +08:00
chenweijia 8982dd82d8 fix: 添加用户验证功能
Gitea Action for docker build / build-app (push) Successful in 47s Details
Gitea Action for docker build runtime / build-runtime (push) Successful in 3m46s Details
2024-08-15 13:33:43 +08:00
chenweijia 8459e0c447 fix: 增加一个请求限制功能
Gitea Action for docker build / build-app (push) Successful in 34s Details
Gitea Action for docker build runtime / build-runtime (push) Successful in 5m20s Details
2024-08-14 14:20:19 +08:00
chenweijia bd9dac23be fix: api示例增加数据库操作示例
Gitea Action for docker build / build-app (push) Successful in 34s Details
2024-08-13 14:57:18 +08:00
chenweijia c6767778e2 fix: 更新依赖包信息
Gitea Action for docker build / build-app (push) Successful in 52s Details
Gitea Action for docker build runtime / build-runtime (push) Successful in 6m20s Details
2024-08-13 11:33:39 +08:00
chenwj113 088bee0199 fix:
Gitea Action for docker build runtime / build-runtime (push) Successful in 1m2s Details
2024-06-12 10:13:11 +08:00
chenwj113 b23786319e fix:
Gitea Action for docker build runtime / build-runtime (push) Failing after 3h4m10s Details
2024-06-11 17:51:50 +08:00
chenwj113 2d4e8397ef fix:更新Dockerfile
Gitea Action for docker build runtime / build-runtime (push) Successful in 1m56s Details
2024-06-11 17:36:34 +08:00
chenwj113 9bf965587e fix: 更改Dockerfile安装命令
Gitea Action for docker build runtime / build-runtime (push) Successful in 34s Details
2024-06-11 17:19:24 +08:00
chenwj113 598bb654c7 bugfix: 修复Dockerfile错误
Gitea Action for docker build / build-app (push) Successful in 37s Details
Gitea Action for docker build runtime / build-runtime (push) Successful in 2m40s Details
2024-06-11 17:06:19 +08:00
chenwj113 2ee8beb276 fix: 更新依赖包安装方式
Gitea Action for docker build / build-app (push) Successful in 1m20s Details
Gitea Action for docker build runtime / build-runtime (push) Failing after 1m7s Details
2024-06-11 16:59:21 +08:00
chenwj113 59d4f098ea bugfix:
Gitea Action for docker build / build-app (push) Successful in 43s Details
Gitea Action for docker build runtime / build-runtime (push) Successful in 4m55s Details
2024-06-02 14:35:50 +08:00
chenwj113 d93ec2b0f3 fix: 修复依赖包错误
Gitea Action for docker build runtime / build-runtime (push) Failing after 1m32s Details
2024-06-02 13:54:15 +08:00
chenwj113 6e60009711 fix: 修改RuntimeDockerfile
Gitea Action for docker build runtime / build-runtime (push) Failing after 2m12s Details
2024-06-02 13:49:39 +08:00
chenwj113 0ef5ce6fd8 bugfix:
Gitea Action for docker build runtime / build-runtime (push) Has been cancelled Details
2024-06-02 13:44:26 +08:00
chenwj113 3f0827e64f fix:
Gitea Action for docker build / build-app (push) Successful in 52s Details
Gitea Action for docker build runtime / build-runtime (push) Failing after 48s Details
2024-06-02 13:26:34 +08:00
chenwj113 7dfc9ff205 fix: 修改目录结构
Gitea Action for docker build runtime / build-runtime (push) Waiting to run Details
Gitea Action for docker build / build-app (push) Has been cancelled Details
2024-06-02 12:46:05 +08:00
chenwj113 dad5d2aaa5 fix: cicd文件增加判断null命令 2024-06-02 12:10:55 +08:00
chenwj113 c3fda4c8b5 feat: 新增gitea action功能
Gitea Action for docker build / build-app (push) Successful in 4m48s Details
Gitea Action for docker build runtime / build-runtime (push) Has been cancelled Details
2024-06-02 12:01:40 +08:00
25 changed files with 2924 additions and 153 deletions

View File

@ -0,0 +1,36 @@
name: Gitea Action for docker build
run-name: ${{ gitea.actor }} is testing out Gitea Actions 🚀
on:
push:
branches: [master]
paths-ignore:
- 'requirements.txt'
- '*RuntimeDockerfile'
- '.gitignore'
- 'README.md'
- 'pyproject.toml'
- '.gitea/workflows/**'
jobs:
build-app:
runs-on: ubuntu-latest
steps:
- run: echo "This job is now running on a ${{ runner.os }} server hosted by Gitea!"
- name: Check out repository code
uses: https://gitea.com/actions/checkout@v4
- name: Login to hub.airpig.cn
uses: https://gitea.com/docker/login-action@v3
with:
registry: hub.airpig.cn
username: admin
password: Chenweijia113!
- run: |
var=${{ gitea.repository }}
repo=${var##*/}
version=$(grep -oP '(?<=version = ")(.*)(?=")' "pyproject.toml")
new_version="v${version}"
docker build -t hub.airpig.cn/library/$repo:$new_version .
docker push hub.airpig.cn/library/$repo:$new_version
docker rmi hub.airpig.cn/library/$repo:$new_version
- run: echo "This job's status is ${{ job.status }}."

View File

@ -0,0 +1,32 @@
name: Gitea Action for docker build runtime
run-name: ${{ gitea.actor }} is testing out Gitea Actions 🚀
on:
push:
branches: [master]
paths:
- 'requirements.txt'
- 'poetry.lock'
- '*RuntimeDockerfile'
jobs:
build-runtime:
runs-on: ubuntu-latest
steps:
- run: echo "This job is now running on a ${{ runner.os }} server hosted by Gitea!"
- name: Check out repository code
uses: https://gitea.com/actions/checkout@v4
- name: Login to hub.airpig.cn
uses: https://gitea.com/docker/login-action@v3
with:
registry: hub.airpig.cn
username: admin
password: Chenweijia113!
- run: |
var=${{ gitea.repository }}
repo=${var##*/}"-runtime"
version=$(grep -oP '(?<=version = ")(.*)(?=")' "pyproject.toml")
new_version="v${version}"
docker build -t hub.airpig.cn/library/$repo:$new_version -f AlpineRuntimeDockerfile .
docker push hub.airpig.cn/library/$repo:$new_version
docker rmi hub.airpig.cn/library/$repo:$new_version
- run: echo "This job's status is ${{ job.status }}."

View File

@ -1,6 +1,6 @@
variables: variables:
PROJECT_NAME: fastapi_app_template PROJECT_NAME: fastapi_app_template
DOCKER_IMAGE_DOMAIN: 192.168.2.237:8088 DOCKER_IMAGE_DOMAIN: hub.airpig.cn
LATEST_VERSION: latest LATEST_VERSION: latest
K8S_NS: default K8S_NS: default
DEPLOYMENT_NAME: fastapi-app-template DEPLOYMENT_NAME: fastapi-app-template

30
AlpineRuntimeDockerfile Normal file
View File

@ -0,0 +1,30 @@
FROM python:3.11-alpine as builder
RUN pip install poetry -i https://mirrors.aliyun.com/pypi/simple/
WORKDIR /venv
COPY poetry.lock pyproject.toml /venv/
RUN poetry config virtualenvs.options.no-pip true \
&& poetry config virtualenvs.options.no-setuptools true \
&& poetry config virtualenvs.in-project true \
&& poetry install
FROM python:3.11-alpine as release
COPY --from=builder /venv /venv
ENV PATH="/venv/.venv/bin:${PATH}"
RUN chmod a+x /venv/.venv/bin/activate \
&& source /venv/.venv/bin/activate
WORKDIR /app
COPY . /app
ENV FAST_API_ENV=prod
CMD ["/usr/local/bin/uvicorn", "main:fast_api_app", "--reload", "--host", "0.0.0.0", "--port", "80"]

View File

@ -1,29 +1,10 @@
# 安装依赖阶段 FROM busybox
FROM python:3.8.6-slim as build
RUN mkdir /install RUN mkdir -p /data /app
WORKDIR /install WORKDIR /data
COPY requirements.txt . COPY . /data
RUN sed -i s@/deb.debian.org/@/mirrors.aliyun.com/@g /etc/apt/sources.list ENTRYPOINT [ "/bin/cp", "-r", "/data/*", "/app"]
RUN apt-get update \
&& apt-get install gcc -y \
&& apt-get clean
RUN pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/ --prefix=/install
# 应用启动
FROM python:3.8.6-slim
COPY --from=build /install /usr/local
COPY . /app
WORKDIR /app
ENV FAST_API_ENV=dev
CMD ["/usr/local/bin/uvicorn", "main:fast_api_app", "--reload", "--host", "0.0.0.0", "--port", "21021"]

35
Pipfile Normal file
View File

@ -0,0 +1,35 @@
[[source]]
url = "http://mirrors.cloud.aliyuncs.com/pypi/simple/"
verify_ssl = false
name = "pip_conf_index_global"
[packages]
aiofiles = "==22.1.0"
aioredis = "==2.0.1"
aiomysql = "==0.1.1"
bcrypt = "==4.0.1"
email-validator = ">=2.0.0"
fastapi = "==0.111.0"
fastapi-plugins = "==0.13.0"
fastapi-sqlalchemy = "==0.2.1"
pydantic = ">=2.0.0"
pydantic-settings = "==2.2.1"
python-multipart = ">=0.0.7"
pytest = "==7.2.1"
requests = "==2.28.2"
sqlacodegen = "==2.3.0"
sqlalchemy = "==2.0.1"
uvicorn = "==0.20.0"
pyjwt = "==2.6.0"
passlib = "==1.7.4"
pillow = "==9.4.0"
captcha = "==0.4"
jinja2 = "==3.1.2"
pycryptodome = "==3.17"
qiniu = "==7.10.0"
pytz = "==2022.7.1"
[dev-packages]
[requires]
python_version = "3.10"

View File

@ -1,7 +1,7 @@
# FastAPI App 文档 # FastAPI App 文档
## 0 开发说明 ## 0 开发说明
Python(3.8.6+) and pip(20.2.4+) Python(3.10+) and pip(20.2.4+)
### 开发命名规范 ### 开发命名规范
> * 避免采用的名字 > * 避免采用的名字
> 不要使用字符l小写字母elO大写字母ohI大写字母eye作为单字符变量名。 > 不要使用字符l小写字母elO大写字母ohI大写字母eye作为单字符变量名。
@ -32,9 +32,12 @@ database=test
## 2 开发环境下安装依赖和运行项目 ## 2 开发环境下安装依赖和运行项目
``` bash ``` bash
pip install -r requirements.txt pip install poetry -i https://pypi.tuna.tsinghua.edu.cn/simple/ \
&& poetry source add --priority=primary mirrors https://pypi.tuna.tsinghua.edu.cn/simple/ \
&& poetry config virtualenvs.path /install \
&& poetry install
./start.bat (windows) ./start.bat (windows)
./start.sh (mac) ./start.sh (mac/linux)
``` ```
@ -55,22 +58,31 @@ sqlacodegen.exe --tables permission_info --outfile .\Desktop\fastapi_app\models\
logs *日志文件目录 logs *日志文件目录
src *源码目录 src *源码目录
|--- api *接口目录 |--- api *接口目录
|--- biz *逻辑目录 |--- service *逻辑目录
|--- dtos *接口参数和返回值目录 |--- dtos *接口参数和返回值目录
|--- models *数据model目录 |--- models *数据model目录
|--- middleware *中间件目录
|--- router *路由目录 ----- TODO
|--- utils *工具类目录 |--- utils *工具类目录
|--- captcha_tools *验证码 |--- captcha *验证码
|--- common_tools *常规 |--- common *常规
|--- exception_tools *异常处理 |--- exception *异常处理
|--- file_upload_tools *文件上传 |--- file_upload *文件上传
|--- qiniu_tools *七牛 |--- qiniu_tools *七牛
|--- sms_tools *短信 |--- sms *短信
static *静态文件目录 static *静态文件目录
test *测试目录 test *测试目录
.gitignore *git忽略文件
.gitlab-ci.yml * CI/CD文件 .gitlab-ci.yml * CI/CD文件
main.py *入口文件 main.py *入口文件
config.py *配置入口文件 config.py *配置入口文件
Dockerfile *dockerfile容器文件 Dockerfile *dockerfile容器代码
requirements.txt *依赖包安装文件 RuntimeDockerfile *dockerfile容器运行环境
requirements.txt * pip依赖包安装文件
pyproject.toml * poetry依赖包安装文件
poetry.lock * poetry依赖包安装文件
Pipfile * pipenv安装文件
Pipfile.lock * pipenv安装文件
README.md * 项目说明文件
start.bat *Windows开发环境下启动文件 start.bat *Windows开发环境下启动文件
start.sh *Unix开发环境下启动文件 start.sh *Unix开发环境下启动文件

42
RuntimeDockerfile Normal file
View File

@ -0,0 +1,42 @@
# 安装依赖阶段
FROM python:3.11-slim as build
RUN mkdir /install
WORKDIR /install
# RUN sed -i s@/deb.debian.org/@/mirrors.aliyun.com/@g /etc/apt/sources.list.d/debian.sources
# RUN apt-get update \
# && apt-get install gcc -y \
# && apt-get clean
# COPY requirements.txt .
# RUN pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/ --prefix=/install
COPY poetry.lock .
COPY pyproject.toml .
RUN pip install poetry -i https://mirrors.aliyun.com/pypi/simple/ \
&& poetry source add --priority=primary mirrors https://pypi.tuna.tsinghua.edu.cn/simple/ \
&& poetry config virtualenvs.path /install \
&& poetry install
# 应用启动
FROM python:3.11-slim
# COPY --from=build /install /usr/local/
COPY --from=build /install/*/lib/python3.11/ /usr/local/lib/python3.11/
COPY --from=build /install/*/bin/ /usr/local/bin/
# 删除不需要的文件和工具,精简镜像
RUN apt-get purge -y --auto-remove \
&& rm -rf /var/lib/apt/lists/* \
&& rm -rf /usr/share/doc \
&& rm -rf /usr/share/man \
&& rm -rf /usr/local/bin/pip
RUN mkdir -p /app
WORKDIR /app
# COPY . /app
ENV FAST_API_ENV=prod
CMD ["/usr/local/bin/uvicorn", "main:fast_api_app", "--reload", "--host", "0.0.0.0", "--port", "80"]

View File

@ -5,16 +5,17 @@ lifetime_seconds=3600
[mysql] [mysql]
username=root username=root
password=123456 password=123456
host=192.168.2.94 host=127.0.0.1
port=3306 port=3306
database=admgs_v2 database=test
[redis] [redis]
redis_host=192.168.2.94 redis_host=127.0.0.1
redis_port=6379 redis_port=6379
redis_password=
[rabbitmq] [rabbitmq]
rabbitmq_host=192.168.2.94 rabbitmq_host=rabbitmq
rabbitmq_user=root rabbitmq_user=root
rabbitmq_password=123123 rabbitmq_password=123123

View File

@ -4,17 +4,18 @@ lifetime_seconds=3600
[mysql] [mysql]
username=root username=root
password=123456 password=Chenweijia113!
host=192.168.2.94 host=mysql
port=3306 port=3306
database=admgs_v2 database=test
[redis] [redis]
redis_host=192.168.2.94 redis_host=redis
redis_port=6379 redis_port=6379
redis_password=Chenweijia113!
[rabbitmq] [rabbitmq]
rabbitmq_host=192.168.2.94 rabbitmq_host=rabbitmq
rabbitmq_user=root rabbitmq_user=root
rabbitmq_password=123123 rabbitmq_password=123123

View File

@ -3,7 +3,7 @@ from configparser import ConfigParser
from typing import Optional from typing import Optional
from fastapi_plugins import RedisSettings from fastapi_plugins import RedisSettings
from pydantic import BaseSettings from pydantic_settings import BaseSettings
class ReConfigParser(ConfigParser): class ReConfigParser(ConfigParser):
@ -34,7 +34,6 @@ class MySQLConfig(BaseSettings):
class RedisConfig(RedisSettings): class RedisConfig(RedisSettings):
redis_url: str = None
redis_host: Optional[str] = 'localhost' redis_host: Optional[str] = 'localhost'
redis_port: Optional[int] = 6379 redis_port: Optional[int] = 6379
redis_password: str = None redis_password: str = None
@ -60,7 +59,7 @@ def init_config():
# common_config = CommonConfig(**dict(config.items('common'))) # common_config = CommonConfig(**dict(config.items('common')))
mysql_config = MySQLConfig(**dict(config.items('mysql'))) mysql_config = MySQLConfig(**dict(config.items('mysql')))
redis_config = RedisConfig(**dict(config.items('redis'))) redis_config = RedisConfig(**dict(config.items('redis')))
rabbitmq_config = RabbitmqConfig(**dict(config.items("rabbitmq"))) # rabbitmq_config = RabbitmqConfig(**dict(config.items("rabbitmq")))
# return common_config, mysql_config, redis_config, rabbitmq_config # return common_config, mysql_config, redis_config, rabbitmq_config
return mysql_config, redis_config return mysql_config, redis_config
except Exception as e: except Exception as e:

48
main.py
View File

@ -1,23 +1,26 @@
import logging.config as logging_config import logging.config as logging_config
import os import os
from contextlib import asynccontextmanager
import fastapi_plugins import fastapi_plugins
from fastapi import FastAPI, Request from fastapi import FastAPI, Request
from fastapi.exceptions import HTTPException, RequestValidationError from fastapi.exceptions import HTTPException, RequestValidationError
from fastapi.middleware.wsgi import WSGIMiddleware # from fastapi.middleware.wsgi import WSGIMiddleware
from fastapi_sqlalchemy import DBSessionMiddleware from fastapi_sqlalchemy import DBSessionMiddleware
from authx.exceptions import AuthXException
from config import init_config from config import init_config
# from src.middleware.flask import flask_app # from src.middleware.flask import flask_app
from src.utils.exception import (http_exception_handler,
request_validation_error_handler) from src.utils.exception import http_exception_handler, request_validation_error_handler, authx_exception_handler
# 请求限制
import redis.asyncio as aio_redis
from fastapi_limiter import FastAPILimiter
def create_app(): def create_app():
app = FastAPI() mysql_config, redis_config = init_config()
@app.on_event("startup") @asynccontextmanager
async def startup_event(): async def lifespan(app: FastAPI):
# 创建日志文件夹和临时文件上传文件夹 # 创建日志文件夹和临时文件上传文件夹
if not os.path.exists("files"): if not os.path.exists("files"):
os.mkdir("files") os.mkdir("files")
@ -27,31 +30,34 @@ def create_app():
os.mkdir("logs") os.mkdir("logs")
logging_config.fileConfig('conf/log.ini') logging_config.fileConfig('conf/log.ini')
# 初始化配置文件 # 初始化配置文件
mysql_config, redis_config = init_config()
# 添加sqlalchemy数据库中间件
# once the middleware is applied, any route can then access the database session from the global ``db``
app.add_middleware(DBSessionMiddleware, db_url=mysql_config.sqlalchemy_db_uri)
# Redis 缓存初始化 # Redis 缓存初始化
await fastapi_plugins.redis_plugin.init_app(app, redis_config) await fastapi_plugins.redis_plugin.init_app(app, redis_config)
await fastapi_plugins.redis_plugin.init() await fastapi_plugins.redis_plugin.init()
# 请求限制
@app.on_event("shutdown") await FastAPILimiter.init(redis=aio_redis.from_url("redis://localhost:6379", encoding="utf8"))
async def shutdown_event(): yield
# 应用关闭时关闭redis连接
await fastapi_plugins.redis_plugin.terminate() await fastapi_plugins.redis_plugin.terminate()
await FastAPILimiter.close()
app = FastAPI(lifespan=lifespan)
# 添加sqlalchemy数据库中间件
# once the middleware is applied, any route can then access the database session from the global ``db``
app.add_middleware(DBSessionMiddleware, db_url=mysql_config.sqlalchemy_db_uri)
# 添加异常处理 # 添加异常处理
app.add_exception_handler(HTTPException, http_exception_handler) app.add_exception_handler(HTTPException, http_exception_handler)
app.add_exception_handler(RequestValidationError, request_validation_error_handler) app.add_exception_handler(RequestValidationError, request_validation_error_handler)
# AuthX异常处理
app.add_exception_handler(AuthXException, authx_exception_handler)
# 可以在这里挂载Flask的应用复用之前项目的相关代码 # 可以在这里挂载Flask的应用复用之前项目的相关代码
# app.mount("/v1", WSGIMiddleware(flask_app)) # app.mount("/v1", WSGIMiddleware(flask_app))
# 在这里添加API route # 在这里添加API route
from src.api import example from src.api import example,auth_example
app.include_router(example.router, tags=["API示例"], prefix="/example") app.include_router(example.router, tags=["API示例"], prefix="/v1/example")
app.include_router(auth_example.router, tags=["认证示例"], prefix="/v1/auth_example")
return app return app

2478
poetry.lock generated Normal file

File diff suppressed because it is too large Load Diff

34
pyproject.toml Normal file
View File

@ -0,0 +1,34 @@
[tool.poetry]
name = "fastapi-template"
version = "0.1.5"
description = ""
authors = ["chenwj113 <chenwj113@gmail.com>"]
license = "MIT"
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.10"
fastapi = "0.111.0"
sqlalchemy = "2.0.0"
aioredis = "2.0.1"
aiomysql = "0.1.1"
fastapi-plugins = "0.13.0"
fastapi-sqlalchemy = "^0.2.1"
passlib = "^1.7.4"
pytz = "^2024.1"
qiniu = "^7.13.2"
pillow = "^10.4.0"
captcha = "^0.6.0"
fastapi-limiter = "^0.1.6"
authx = "^1.3.0"
itsdangerous = "^2.2.0"
[[tool.poetry.source]]
name = "mirrors"
url = "https://pypi.tuna.tsinghua.edu.cn/simple/"
priority = "primary"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

View File

@ -1,24 +1,75 @@
aiofiles==22.1.0 --index-url https://pypi.tuna.tsinghua.edu.cn/simple
aioredis==2.0.1
aiomysql==0.1.1 aiojobs==1.2.1 ; python_version >= "3.10" and python_version < "4.0"
bcrypt==4.0.1 aiomysql==0.1.1 ; python_version >= "3.10" and python_version < "4.0"
email-validator==1.3.1 aioredis==2.0.1 ; python_version >= "3.10" and python_version < "4.0"
fastapi==0.89.1 annotated-types==0.7.0 ; python_version >= "3.10" and python_version < "4.0"
fastapi-plugins==0.11.0 anyio==4.4.0 ; python_version >= "3.10" and python_version < "4.0"
FastAPI-SQLAlchemy==0.2.1 async-timeout==4.0.3 ; python_version >= "3.10" and python_version < "4.0"
pydantic==1.10.4 authx==1.3.0 ; python_version >= "3.10" and python_version < "4.0"
# pydantic-sqlalchemy==0.0.9 captcha==0.6.0 ; python_version >= "3.10" and python_version < "4.0"
python-multipart==0.0.5 certifi==2024.6.2 ; python_version >= "3.10" and python_version < "4.0"
pytest==7.2.1 cffi==1.17.0 ; python_version >= "3.10" and python_version < "4.0" and platform_python_implementation != "PyPy"
requests==2.28.2 charset-normalizer==3.3.2 ; python_version >= "3.10" and python_version < "4.0"
sqlacodegen==2.3.0 click==8.1.7 ; python_version >= "3.10" and python_version < "4.0"
SQLAlchemy==2.0.1 colorama==0.4.6 ; python_version >= "3.10" and python_version < "4.0" and (sys_platform == "win32" or platform_system == "Windows")
uvicorn==0.20.0 cryptography==43.0.0 ; python_version >= "3.10" and python_version < "4.0"
PyJWT==2.6.0 dnspython==2.6.1 ; python_version >= "3.10" and python_version < "4.0"
passlib==1.7.4 ecdsa==0.19.0 ; python_version >= "3.10" and python_version < "4.0"
Pillow==9.4.0 email-validator==2.1.1 ; python_version >= "3.10" and python_version < "4.0"
captcha==0.4 exceptiongroup==1.2.1 ; python_version >= "3.10" and python_version < "3.11"
jinja2==3.1.2 fastapi-cli==0.0.4 ; python_version >= "3.10" and python_version < "4.0"
pycryptodome==3.17 fastapi-limiter==0.1.6 ; python_version >= "3.10" and python_version < "4.0"
qiniu==7.10.0 fastapi-plugins==0.13.0 ; python_version >= "3.10" and python_version < "4.0"
pytz==2022.7.1 fastapi-sqlalchemy==0.2.1 ; python_version >= "3.10" and python_version < "4.0"
fastapi==0.111.0 ; python_version >= "3.10" and python_version < "4.0"
greenlet==3.0.3 ; python_version >= "3.10" and python_version < "4.0" and (platform_machine == "aarch64" or platform_machine == "ppc64le" or platform_machine == "x86_64" or platform_machine == "amd64" or platform_machine == "AMD64" or platform_machine == "win32" or platform_machine == "WIN32")
h11==0.14.0 ; python_version >= "3.10" and python_version < "4.0"
hiredis==2.3.2 ; python_version >= "3.10" and python_version < "4.0"
httpcore==1.0.5 ; python_version >= "3.10" and python_version < "4.0"
httptools==0.6.1 ; python_version >= "3.10" and python_version < "4.0"
httpx==0.27.0 ; python_version >= "3.10" and python_version < "4.0"
idna==3.7 ; python_version >= "3.10" and python_version < "4.0"
itsdangerous==2.2.0 ; python_version >= "3.10" and python_version < "4.0"
jinja2==3.1.4 ; python_version >= "3.10" and python_version < "4.0"
markdown-it-py==3.0.0 ; python_version >= "3.10" and python_version < "4.0"
markupsafe==2.1.5 ; python_version >= "3.10" and python_version < "4.0"
mdurl==0.1.2 ; python_version >= "3.10" and python_version < "4.0"
orjson==3.10.4 ; python_version >= "3.10" and python_version < "4.0"
passlib==1.7.4 ; python_version >= "3.10" and python_version < "4.0"
pillow==10.4.0 ; python_version >= "3.10" and python_version < "4.0"
pyasn1==0.6.0 ; python_version >= "3.10" and python_version < "4.0"
pycparser==2.22 ; python_version >= "3.10" and python_version < "4.0" and platform_python_implementation != "PyPy"
pydantic-core==2.18.4 ; python_version >= "3.10" and python_version < "4.0"
pydantic-settings==2.3.1 ; python_version >= "3.10" and python_version < "4.0"
pydantic==2.7.3 ; python_version >= "3.10" and python_version < "4.0"
pygments==2.18.0 ; python_version >= "3.10" and python_version < "4.0"
pyjwt[crypto]==2.9.0 ; python_version >= "3.10" and python_version < "4.0"
pymysql==1.1.1 ; python_version >= "3.10" and python_version < "4.0"
python-dateutil==2.9.0.post0 ; python_version >= "3.10" and python_version < "4.0"
python-dotenv==1.0.1 ; python_version >= "3.10" and python_version < "4.0"
python-jose==3.3.0 ; python_version >= "3.10" and python_version < "4.0"
python-json-logger==2.0.7 ; python_version >= "3.10" and python_version < "4.0"
python-multipart==0.0.9 ; python_version >= "3.10" and python_version < "4.0"
pytz==2024.1 ; python_version >= "3.10" and python_version < "4.0"
pyyaml==6.0.1 ; python_version >= "3.10" and python_version < "4.0"
qiniu==7.13.2 ; python_version >= "3.10" and python_version < "4.0"
redis==5.0.5 ; python_version >= "3.10" and python_version < "4.0"
redis[hiredis]==5.0.5 ; python_version >= "3.10" and python_version < "4.0"
requests==2.32.3 ; python_version >= "3.10" and python_version < "4.0"
rich==13.7.1 ; python_version >= "3.10" and python_version < "4.0"
rsa==4.9 ; python_version >= "3.10" and python_version < "4"
shellingham==1.5.4 ; python_version >= "3.10" and python_version < "4.0"
six==1.16.0 ; python_version >= "3.10" and python_version < "4.0"
sniffio==1.3.1 ; python_version >= "3.10" and python_version < "4.0"
sqlalchemy==2.0.0 ; python_version >= "3.10" and python_version < "4.0"
starlette==0.37.2 ; python_version >= "3.10" and python_version < "4.0"
tenacity==8.3.0 ; python_version >= "3.10" and python_version < "4.0"
typer==0.12.3 ; python_version >= "3.10" and python_version < "4.0"
typing-extensions==4.12.2 ; python_version >= "3.10" and python_version < "4.0"
ujson==5.10.0 ; python_version >= "3.10" and python_version < "4.0"
urllib3==2.2.2 ; python_version >= "3.10" and python_version < "4.0"
uvicorn[standard]==0.30.1 ; python_version >= "3.10" and python_version < "4.0"
uvloop==0.19.0 ; (sys_platform != "win32" and sys_platform != "cygwin") and platform_python_implementation != "PyPy" and python_version >= "3.10" and python_version < "4.0"
watchfiles==0.22.0 ; python_version >= "3.10" and python_version < "4.0"
websockets==12.0 ; python_version >= "3.10" and python_version < "4.0"

16
src/api/auth_example.py Normal file
View File

@ -0,0 +1,16 @@
from fastapi import APIRouter, HTTPException, Depends
# 校验授权token
from src.middleware.auth import security
router = APIRouter()
@router.get('/', dependencies=[Depends(security.access_token_required)])
def index():
return {"message": "Hello World"}
@router.get('/login')
def login(username: str, password: str):
if username == "test" and password == "test":
token = security.create_access_token(uid=username)
return {"access_token": token}
raise HTTPException(401, detail={"message": "Bad credentials"})

View File

@ -5,16 +5,20 @@ import aioredis
from fastapi import APIRouter, Depends, Query from fastapi import APIRouter, Depends, Query
from fastapi.security import OAuth2PasswordBearer from fastapi.security import OAuth2PasswordBearer
from fastapi_plugins import depends_redis from fastapi_plugins import depends_redis
from fastapi_sqlalchemy import db
from sqlalchemy.sql import text
from fastapi_limiter.depends import RateLimiter
from pydantic import BaseModel from pydantic import BaseModel
from src.biz.example import get_user_by_id from src.service.example import get_user_by_id
from src.dtos import response from src.dtos import response
from src.dtos.example import UserExampleListPagesResult from src.dtos.example import UserExampleListPagesResult
router = APIRouter() router = APIRouter()
# 并发数限制 每5秒最多100次请求
@router.get("/") @router.get("/", dependencies=[Depends(RateLimiter(times=100, seconds=5))])
def index(): def index():
return {"msg": "This is Index Page"} return {"msg": "This is Index Page"}
@ -59,6 +63,11 @@ async def get_user_list_pages(page: int = Query(..., description="当前页码")
res = response(data=data, message="Ok!") res = response(data=data, message="Ok!")
return res return res
# 数据库查询
@router.get('/get_db_version')
async def get_db_version():
result = db.session.execute(text("SELECT version()")).first()
return dict(result=result.tuple()[0])
# Redis 缓存查询 # Redis 缓存查询
@router.get("/ping") @router.get("/ping")

View File

@ -52,9 +52,9 @@ class SendCaptchaSuccess(BaseResponse):
class BadRequestError(ErrorModel): class BadRequestError(ErrorModel):
code = 400 code: int = 400
message = "BAD REQUEST" message: str = "BAD REQUEST"
details = "请求参数错误" details: str = "请求参数错误"
class BadRequestResponse(BaseResponse): class BadRequestResponse(BaseResponse):
@ -62,9 +62,9 @@ class BadRequestResponse(BaseResponse):
class ServerInternalError(ErrorModel): class ServerInternalError(ErrorModel):
code = 500 code: int = 500
message = "INTERNAL SERVER ERROR" message: str = "INTERNAL SERVER ERROR"
details = "服务器内部错误" details: str = "服务器内部错误"
class ServerInternalResponse(BaseResponse): class ServerInternalResponse(BaseResponse):

13
src/middleware/auth.py Normal file
View File

@ -0,0 +1,13 @@
from authx import AuthX, AuthXConfig
from datetime import timedelta
config = AuthXConfig()
config.JWT_ALGORITHM = "HS256"
config.JWT_SECRET_KEY = "SECRET_KEY"
config.JWT_TOKEN_LOCATION = ["headers", "query", "cookies", "json"]
config.JWT_HEADER_NAME = "X-Token"
config.JWT_HEADER_TYPE = ""
# access token expires in 24 hours
config.JWT_ACCESS_TOKEN_EXPIRES = timedelta(hours=24)
security = AuthX(config=config)

View File

@ -1,22 +0,0 @@
# coding: utf-8
from sqlalchemy import Column, DateTime, Integer, text
from sqlalchemy.dialects.mysql import VARCHAR
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
metadata = Base.metadata
class DevicesPlace(Base):
__tablename__ = 'devices_place'
__table_args__ = {'comment': '设备:地址表'}
id = Column(Integer, primary_key=True)
mid = Column(VARCHAR(10), nullable=False, index=True, comment='设备管理ID')
customer_account = Column(VARCHAR(12), nullable=False, index=True, comment='甲方')
region_id = Column(Integer, nullable=False, server_default=text("'0'"), comment='区域id')
building_id = Column(Integer, nullable=False, server_default=text("'0'"), comment='所在楼建筑的id0为待定')
floor = Column(Integer, nullable=False, server_default=text("'0'"), comment='楼层0为待定可为负数')
place = Column(VARCHAR(50), comment='位置(通常是房间号)')
ctime = Column(DateTime, nullable=False, index=True, server_default=text("CURRENT_TIMESTAMP"), comment='记录创建时间')
utime = Column(DateTime, nullable=False, index=True, server_default=text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"), comment='记录更新时间')

View File

@ -1,4 +1,4 @@
from src.biz import execute_sql from src.service import execute_sql
def get_user_by_id(user_id): def get_user_by_id(user_id):

View File

@ -1,9 +1,10 @@
import logging import logging
from collections import defaultdict
from fastapi import Request, status from fastapi import Request, status
from fastapi.exceptions import HTTPException, RequestValidationError from fastapi.exceptions import HTTPException, RequestValidationError
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
import authx.exceptions as authx_exceptions
from src.dtos import BaseResponse, ErrorModel from src.dtos import BaseResponse, ErrorModel
logger = logging.getLogger("uvicorn.error") logger = logging.getLogger("uvicorn.error")
@ -11,35 +12,32 @@ logger = logging.getLogger("uvicorn.error")
def request_validation_error_handler(req: Request, exc: RequestValidationError): def request_validation_error_handler(req: Request, exc: RequestValidationError):
""" """
请求响应校验错误处理 请求校验错误处理
:param req: :param req:
:param exc: :param exc:
:return: :return:
""" """
errors = exc.errors() errors = exc.errors()
logger.error(f"{req.method} {req.url} , errors: {errors}") logger.error(f"{req.method} {req.url} , errors: {errors}")
response_errors = [error for error in errors if error["loc"][0] == "response"] group_err = defaultdict(list)
if len(response_errors) > 0: for err in errors:
message = "接口响应异常 " group_err[err["type"]].append(err)
else: details = ""
message = "接口请求异常 " for err_type, err_list in group_err.items():
json_err = [err["msg"] for err in errors if "value_error.jsondecode" == err["type"]] match err_type:
if json_err: case "missing" | "value_error.missing" :
message += "请求参数JSON编码有误;" details += f"缺少字段:{', '.join([err['loc'][-1] for err in err_list])};"
value_err = [err["msg"] for err in errors if "value_error" == err["type"]] case "value_error.jsondecode":
if value_err: details += "请求参数JSON编码有误;"
message += "; ".join(value_err) case "value_error":
missing_err = [err["loc"][-1] for err in errors if "value_error.missing" == err["type"]] details += "; ".join([err["msg"] for err in err_list])
if missing_err: case "type_error":
message += "缺少字段:{} ".format(", ".join(missing_err)) details += "; ".join([err["msg"].replace("value", f"{err['loc'][1]}") for err in err_list])
type_err = [err["msg"].replace("value", f"{err['loc'][1]}") for err in errors if "type_error" == err["type"]] case "type_error.none.not_allowed":
if type_err: details += f"字段:{', '.join([err['loc'][-1] for err in err_list])} 不能为空;"
message += "; ".join(type_err) case _:
not_none_err = [err["loc"][-1] for err in errors if "type_error.none.not_allowed" == err["type"]] pass
if not_none_err: err_model = ErrorModel(code=status.HTTP_400_BAD_REQUEST, message="接口请求校验异常", details=details)
message += "字段:{} 不能为空".format(", ".join(not_none_err))
err_model = ErrorModel(code=status.HTTP_400_BAD_REQUEST, message=message, details=message)
res = BaseResponse(error=err_model) res = BaseResponse(error=err_model)
return JSONResponse(status_code=200, content=res.dict()) return JSONResponse(status_code=200, content=res.dict())
@ -54,4 +52,23 @@ def http_exception_handler(req: Request, exc: HTTPException):
logger.error(f"{req.method} {req.url} , exception:{exc.detail}") logger.error(f"{req.method} {req.url} , exception:{exc.detail}")
err = ErrorModel(code=exc.status_code, message=exc.detail) err = ErrorModel(code=exc.status_code, message=exc.detail)
res = BaseResponse(result=None, error=err) res = BaseResponse(result=None, error=err)
return JSONResponse(status_code=exc.status_code, content=res.dict()) return JSONResponse(status_code=exc.status_code, content=res.model_dump())
def authx_exception_handler(req: Request, exc: authx_exceptions.AuthXException):
"""
AuthX Exception 错误处理
:param req:
:param exc:
:return:
"""
logger.error(f"{req.method} {req.url} , exception:{exc}")
match type(exc):
case authx_exceptions.MissingTokenError:
err = ErrorModel(code=status.HTTP_401_UNAUTHORIZED, message="请求头缺失X-Token")
case authx_exceptions.JWTDecodeError:
err = ErrorModel(code=status.HTTP_401_UNAUTHORIZED, message="X-Token解析失败")
case _:
err = ErrorModel(code=status.HTTP_401_UNAUTHORIZED, message="未知的错误导致认证失败")
res = BaseResponse(result=None, error=err)
return JSONResponse(status_code=err.code, content=res.model_dump())

View File

@ -1,2 +1,2 @@
set FAST_API_ENV=dev set FAST_API_ENV=dev
uvicorn main:fast_api_app --reload --host 0.0.0.0 --port 21021 uvicorn main:fast_api_app --reload --host 0.0.0.0 --port 8088

2
start.sh Normal file → Executable file
View File

@ -1,2 +1,2 @@
export FAST_API_ENV=dev export FAST_API_ENV=dev
uvicorn main:app --reload --host 0.0.0.0 --port 21021 uvicorn main:fast_api_app --reload --host 0.0.0.0 --port 8088