Compare commits
No commits in common. "master" and "aio-dev" have entirely different histories.
|
|
@ -1,36 +0,0 @@
|
||||||
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 }}."
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
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 }}."
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
variables:
|
variables:
|
||||||
PROJECT_NAME: fastapi_app_template
|
PROJECT_NAME: fastapi_app_template
|
||||||
DOCKER_IMAGE_DOMAIN: hub.airpig.cn
|
DOCKER_IMAGE_DOMAIN: 192.168.2.237:8088
|
||||||
LATEST_VERSION: latest
|
LATEST_VERSION: latest
|
||||||
K8S_NS: default
|
K8S_NS: default
|
||||||
DEPLOYMENT_NAME: fastapi-app-template
|
DEPLOYMENT_NAME: fastapi-app-template
|
||||||
|
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
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"]
|
|
||||||
29
Dockerfile
29
Dockerfile
|
|
@ -1,10 +1,29 @@
|
||||||
FROM busybox
|
# 安装依赖阶段
|
||||||
|
FROM python:3.8.6-slim as build
|
||||||
|
|
||||||
RUN mkdir -p /data /app
|
RUN mkdir /install
|
||||||
|
|
||||||
WORKDIR /data
|
WORKDIR /install
|
||||||
|
|
||||||
COPY . /data
|
COPY requirements.txt .
|
||||||
|
|
||||||
ENTRYPOINT [ "/bin/cp", "-r", "/data/*", "/app"]
|
RUN sed -i s@/deb.debian.org/@/mirrors.aliyun.com/@g /etc/apt/sources.list
|
||||||
|
|
||||||
|
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
35
Pipfile
|
|
@ -1,35 +0,0 @@
|
||||||
[[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"
|
|
||||||
34
README.md
34
README.md
|
|
@ -1,7 +1,7 @@
|
||||||
# FastAPI App 文档
|
# FastAPI App 文档
|
||||||
|
|
||||||
## 0 开发说明
|
## 0 开发说明
|
||||||
Python(3.10+) and pip(20.2.4+)
|
Python(3.8.6+) and pip(20.2.4+)
|
||||||
### 开发命名规范
|
### 开发命名规范
|
||||||
> * 避免采用的名字
|
> * 避免采用的名字
|
||||||
> 不要使用字符‘l’(小写字母el),‘O’(大写字母oh)或‘I’(大写字母eye)作为单字符变量名。
|
> 不要使用字符‘l’(小写字母el),‘O’(大写字母oh)或‘I’(大写字母eye)作为单字符变量名。
|
||||||
|
|
@ -32,12 +32,9 @@ database=test
|
||||||
|
|
||||||
## 2 开发环境下安装依赖和运行项目
|
## 2 开发环境下安装依赖和运行项目
|
||||||
``` bash
|
``` bash
|
||||||
pip install poetry -i https://pypi.tuna.tsinghua.edu.cn/simple/ \
|
pip install -r requirements.txt
|
||||||
&& 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/linux)
|
./start.sh (mac)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -58,31 +55,22 @@ sqlacodegen.exe --tables permission_info --outfile .\Desktop\fastapi_app\models\
|
||||||
logs *日志文件目录
|
logs *日志文件目录
|
||||||
src *源码目录
|
src *源码目录
|
||||||
|--- api *接口目录
|
|--- api *接口目录
|
||||||
|--- service *逻辑目录
|
|--- biz *逻辑目录
|
||||||
|--- dtos *接口参数和返回值目录
|
|--- dtos *接口参数和返回值目录
|
||||||
|--- models *数据model目录
|
|--- models *数据model目录
|
||||||
|--- middleware *中间件目录
|
|
||||||
|--- router *路由目录 ----- TODO
|
|
||||||
|--- utils *工具类目录
|
|--- utils *工具类目录
|
||||||
|--- captcha *验证码
|
|--- captcha_tools *验证码
|
||||||
|--- common *常规
|
|--- common_tools *常规
|
||||||
|--- exception *异常处理
|
|--- exception_tools *异常处理
|
||||||
|--- file_upload *文件上传
|
|--- file_upload_tools *文件上传
|
||||||
|--- qiniu_tools *七牛
|
|--- qiniu_tools *七牛
|
||||||
|--- sms *短信
|
|--- sms_tools *短信
|
||||||
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容器文件
|
||||||
RuntimeDockerfile *dockerfile容器运行环境
|
requirements.txt *依赖包安装文件
|
||||||
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开发环境下启动文件
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
# 安装依赖阶段
|
|
||||||
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"]
|
|
||||||
|
|
@ -5,17 +5,16 @@ lifetime_seconds=3600
|
||||||
[mysql]
|
[mysql]
|
||||||
username=root
|
username=root
|
||||||
password=123456
|
password=123456
|
||||||
host=127.0.0.1
|
host=192.168.2.94
|
||||||
port=3306
|
port=3306
|
||||||
database=test
|
database=admgs_v2
|
||||||
|
|
||||||
|
|
||||||
[redis]
|
[redis]
|
||||||
redis_host=127.0.0.1
|
redis_host=192.168.2.94
|
||||||
redis_port=6379
|
redis_port=6379
|
||||||
redis_password=
|
|
||||||
|
|
||||||
[rabbitmq]
|
[rabbitmq]
|
||||||
rabbitmq_host=rabbitmq
|
rabbitmq_host=192.168.2.94
|
||||||
rabbitmq_user=root
|
rabbitmq_user=root
|
||||||
rabbitmq_password=123123
|
rabbitmq_password=123123
|
||||||
|
|
@ -4,18 +4,17 @@ lifetime_seconds=3600
|
||||||
|
|
||||||
[mysql]
|
[mysql]
|
||||||
username=root
|
username=root
|
||||||
password=Chenweijia113!
|
password=123456
|
||||||
host=mysql
|
host=192.168.2.94
|
||||||
port=3306
|
port=3306
|
||||||
database=test
|
database=admgs_v2
|
||||||
|
|
||||||
|
|
||||||
[redis]
|
[redis]
|
||||||
redis_host=redis
|
redis_host=192.168.2.94
|
||||||
redis_port=6379
|
redis_port=6379
|
||||||
redis_password=Chenweijia113!
|
|
||||||
|
|
||||||
[rabbitmq]
|
[rabbitmq]
|
||||||
rabbitmq_host=rabbitmq
|
rabbitmq_host=192.168.2.94
|
||||||
rabbitmq_user=root
|
rabbitmq_user=root
|
||||||
rabbitmq_password=123123
|
rabbitmq_password=123123
|
||||||
|
|
@ -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_settings import BaseSettings
|
from pydantic import BaseSettings
|
||||||
|
|
||||||
|
|
||||||
class ReConfigParser(ConfigParser):
|
class ReConfigParser(ConfigParser):
|
||||||
|
|
@ -34,6 +34,7 @@ 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
|
||||||
|
|
@ -59,7 +60,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:
|
||||||
|
|
|
||||||
50
main.py
50
main.py
|
|
@ -1,26 +1,23 @@
|
||||||
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,
|
||||||
from src.utils.exception import http_exception_handler, request_validation_error_handler, authx_exception_handler
|
request_validation_error_handler)
|
||||||
# 请求限制
|
|
||||||
import redis.asyncio as aio_redis
|
|
||||||
from fastapi_limiter import FastAPILimiter
|
|
||||||
|
|
||||||
|
|
||||||
def create_app():
|
def create_app():
|
||||||
mysql_config, redis_config = init_config()
|
app = FastAPI()
|
||||||
|
|
||||||
@asynccontextmanager
|
@app.on_event("startup")
|
||||||
async def lifespan(app: FastAPI):
|
async def startup_event():
|
||||||
# 创建日志文件夹和临时文件上传文件夹
|
# 创建日志文件夹和临时文件上传文件夹
|
||||||
if not os.path.exists("files"):
|
if not os.path.exists("files"):
|
||||||
os.mkdir("files")
|
os.mkdir("files")
|
||||||
|
|
@ -30,34 +27,31 @@ def create_app():
|
||||||
os.mkdir("logs")
|
os.mkdir("logs")
|
||||||
logging_config.fileConfig('conf/log.ini')
|
logging_config.fileConfig('conf/log.ini')
|
||||||
# 初始化配置文件
|
# 初始化配置文件
|
||||||
# Redis 缓存初始化
|
mysql_config, redis_config = init_config()
|
||||||
await fastapi_plugins.redis_plugin.init_app(app, redis_config)
|
|
||||||
await fastapi_plugins.redis_plugin.init()
|
|
||||||
# 请求限制
|
|
||||||
await FastAPILimiter.init(redis=aio_redis.from_url("redis://localhost:6379", encoding="utf8"))
|
|
||||||
yield
|
|
||||||
# 应用关闭时,关闭redis连接
|
|
||||||
await fastapi_plugins.redis_plugin.terminate()
|
|
||||||
await FastAPILimiter.close()
|
|
||||||
app = FastAPI(lifespan=lifespan)
|
|
||||||
# 添加sqlalchemy数据库中间件
|
# 添加sqlalchemy数据库中间件
|
||||||
# once the middleware is applied, any route can then access the database session from the global ``db``
|
# 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_middleware(DBSessionMiddleware, db_url=mysql_config.sqlalchemy_db_uri)
|
||||||
|
# Redis 缓存初始化
|
||||||
|
await fastapi_plugins.redis_plugin.init_app(app, redis_config)
|
||||||
|
await fastapi_plugins.redis_plugin.init()
|
||||||
|
|
||||||
|
@app.on_event("shutdown")
|
||||||
|
async def shutdown_event():
|
||||||
|
await fastapi_plugins.redis_plugin.terminate()
|
||||||
|
|
||||||
# 添加异常处理
|
# 添加异常处理
|
||||||
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,auth_example
|
from src.api import example
|
||||||
app.include_router(example.router, tags=["API示例"], prefix="/v1/example")
|
app.include_router(example.router, tags=["API示例"], prefix="/example")
|
||||||
app.include_router(auth_example.router, tags=["认证示例"], prefix="/v1/auth_example")
|
|
||||||
|
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,34 +0,0 @@
|
||||||
[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"
|
|
||||||
|
|
@ -1,75 +1,24 @@
|
||||||
--index-url https://pypi.tuna.tsinghua.edu.cn/simple
|
aiofiles==22.1.0
|
||||||
|
aioredis==2.0.1
|
||||||
aiojobs==1.2.1 ; python_version >= "3.10" and python_version < "4.0"
|
aiomysql==0.1.1
|
||||||
aiomysql==0.1.1 ; python_version >= "3.10" and python_version < "4.0"
|
bcrypt==4.0.1
|
||||||
aioredis==2.0.1 ; python_version >= "3.10" and python_version < "4.0"
|
email-validator==1.3.1
|
||||||
annotated-types==0.7.0 ; python_version >= "3.10" and python_version < "4.0"
|
fastapi==0.89.1
|
||||||
anyio==4.4.0 ; python_version >= "3.10" and python_version < "4.0"
|
fastapi-plugins==0.11.0
|
||||||
async-timeout==4.0.3 ; python_version >= "3.10" and python_version < "4.0"
|
FastAPI-SQLAlchemy==0.2.1
|
||||||
authx==1.3.0 ; python_version >= "3.10" and python_version < "4.0"
|
pydantic==1.10.4
|
||||||
captcha==0.6.0 ; python_version >= "3.10" and python_version < "4.0"
|
# pydantic-sqlalchemy==0.0.9
|
||||||
certifi==2024.6.2 ; python_version >= "3.10" and python_version < "4.0"
|
python-multipart==0.0.5
|
||||||
cffi==1.17.0 ; python_version >= "3.10" and python_version < "4.0" and platform_python_implementation != "PyPy"
|
pytest==7.2.1
|
||||||
charset-normalizer==3.3.2 ; python_version >= "3.10" and python_version < "4.0"
|
requests==2.28.2
|
||||||
click==8.1.7 ; python_version >= "3.10" and python_version < "4.0"
|
sqlacodegen==2.3.0
|
||||||
colorama==0.4.6 ; python_version >= "3.10" and python_version < "4.0" and (sys_platform == "win32" or platform_system == "Windows")
|
SQLAlchemy==2.0.1
|
||||||
cryptography==43.0.0 ; python_version >= "3.10" and python_version < "4.0"
|
uvicorn==0.20.0
|
||||||
dnspython==2.6.1 ; python_version >= "3.10" and python_version < "4.0"
|
PyJWT==2.6.0
|
||||||
ecdsa==0.19.0 ; python_version >= "3.10" and python_version < "4.0"
|
passlib==1.7.4
|
||||||
email-validator==2.1.1 ; python_version >= "3.10" and python_version < "4.0"
|
Pillow==9.4.0
|
||||||
exceptiongroup==1.2.1 ; python_version >= "3.10" and python_version < "3.11"
|
captcha==0.4
|
||||||
fastapi-cli==0.0.4 ; python_version >= "3.10" and python_version < "4.0"
|
jinja2==3.1.2
|
||||||
fastapi-limiter==0.1.6 ; python_version >= "3.10" and python_version < "4.0"
|
pycryptodome==3.17
|
||||||
fastapi-plugins==0.13.0 ; python_version >= "3.10" and python_version < "4.0"
|
qiniu==7.10.0
|
||||||
fastapi-sqlalchemy==0.2.1 ; python_version >= "3.10" and python_version < "4.0"
|
pytz==2022.7.1
|
||||||
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"
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
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"})
|
|
||||||
|
|
@ -5,20 +5,16 @@ 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.service.example import get_user_by_id
|
from src.biz.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("/", dependencies=[Depends(RateLimiter(times=100, seconds=5))])
|
@router.get("/")
|
||||||
def index():
|
def index():
|
||||||
return {"msg": "This is Index Page"}
|
return {"msg": "This is Index Page"}
|
||||||
|
|
||||||
|
|
@ -63,11 +59,6 @@ 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")
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from src.service import execute_sql
|
from src.biz import execute_sql
|
||||||
|
|
||||||
|
|
||||||
def get_user_by_id(user_id):
|
def get_user_by_id(user_id):
|
||||||
|
|
@ -52,9 +52,9 @@ class SendCaptchaSuccess(BaseResponse):
|
||||||
|
|
||||||
|
|
||||||
class BadRequestError(ErrorModel):
|
class BadRequestError(ErrorModel):
|
||||||
code: int = 400
|
code = 400
|
||||||
message: str = "BAD REQUEST"
|
message = "BAD REQUEST"
|
||||||
details: str = "请求参数错误"
|
details = "请求参数错误"
|
||||||
|
|
||||||
|
|
||||||
class BadRequestResponse(BaseResponse):
|
class BadRequestResponse(BaseResponse):
|
||||||
|
|
@ -62,9 +62,9 @@ class BadRequestResponse(BaseResponse):
|
||||||
|
|
||||||
|
|
||||||
class ServerInternalError(ErrorModel):
|
class ServerInternalError(ErrorModel):
|
||||||
code: int = 500
|
code = 500
|
||||||
message: str = "INTERNAL SERVER ERROR"
|
message = "INTERNAL SERVER ERROR"
|
||||||
details: str = "服务器内部错误"
|
details = "服务器内部错误"
|
||||||
|
|
||||||
|
|
||||||
class ServerInternalResponse(BaseResponse):
|
class ServerInternalResponse(BaseResponse):
|
||||||
|
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
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)
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
# 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='所在楼(建筑)的id,0为待定')
|
||||||
|
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='记录更新时间')
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
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")
|
||||||
|
|
@ -12,32 +11,35 @@ 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}")
|
||||||
group_err = defaultdict(list)
|
response_errors = [error for error in errors if error["loc"][0] == "response"]
|
||||||
for err in errors:
|
if len(response_errors) > 0:
|
||||||
group_err[err["type"]].append(err)
|
message = "接口响应异常 "
|
||||||
details = ""
|
else:
|
||||||
for err_type, err_list in group_err.items():
|
message = "接口请求异常 "
|
||||||
match err_type:
|
json_err = [err["msg"] for err in errors if "value_error.jsondecode" == err["type"]]
|
||||||
case "missing" | "value_error.missing" :
|
if json_err:
|
||||||
details += f"缺少字段:{', '.join([err['loc'][-1] for err in err_list])};"
|
message += "请求参数JSON编码有误;"
|
||||||
case "value_error.jsondecode":
|
value_err = [err["msg"] for err in errors if "value_error" == err["type"]]
|
||||||
details += "请求参数JSON编码有误;"
|
if value_err:
|
||||||
case "value_error":
|
message += "; ".join(value_err)
|
||||||
details += "; ".join([err["msg"] for err in err_list])
|
missing_err = [err["loc"][-1] for err in errors if "value_error.missing" == err["type"]]
|
||||||
case "type_error":
|
if missing_err:
|
||||||
details += "; ".join([err["msg"].replace("value", f"{err['loc'][1]}") for err in err_list])
|
message += "缺少字段:{} ".format(", ".join(missing_err))
|
||||||
case "type_error.none.not_allowed":
|
type_err = [err["msg"].replace("value", f"{err['loc'][1]}") for err in errors if "type_error" == err["type"]]
|
||||||
details += f"字段:{', '.join([err['loc'][-1] for err in err_list])} 不能为空;"
|
if type_err:
|
||||||
case _:
|
message += "; ".join(type_err)
|
||||||
pass
|
not_none_err = [err["loc"][-1] for err in errors if "type_error.none.not_allowed" == err["type"]]
|
||||||
err_model = ErrorModel(code=status.HTTP_400_BAD_REQUEST, message="接口请求校验异常", details=details)
|
if not_none_err:
|
||||||
|
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())
|
||||||
|
|
||||||
|
|
@ -52,23 +54,4 @@ 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.model_dump())
|
return JSONResponse(status_code=exc.status_code, content=res.dict())
|
||||||
|
|
||||||
|
|
||||||
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())
|
|
||||||
|
|
@ -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 8088
|
uvicorn main:fast_api_app --reload --host 0.0.0.0 --port 21021
|
||||||
Loading…
Reference in New Issue