Compare commits

..

No commits in common. "master" and "aio-dev" have entirely different histories.

25 changed files with 153 additions and 2924 deletions

View File

@ -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 }}."

View File

@ -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 }}."

View File

@ -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

View File

@ -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"]

View File

@ -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
View File

@ -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"

View File

@ -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小写字母elO大写字母ohI大写字母eye作为单字符变量名。 > 不要使用字符l小写字母elO大写字母ohI大写字母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开发环境下启动文件

View File

@ -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"]

View File

@ -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

View File

@ -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

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_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:

48
main.py
View File

@ -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,
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():
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')
# 初始化配置文件 # 初始化配置文件
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()
# 请求限制
await FastAPILimiter.init(redis=aio_redis.from_url("redis://localhost:6379", encoding="utf8")) @app.on_event("shutdown")
yield async def shutdown_event():
# 应用关闭时关闭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,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

2478
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -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"

View File

@ -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"

View File

@ -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"})

View File

@ -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")

View File

@ -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):

View File

@ -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):

View File

@ -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)

View File

@ -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='所在楼建筑的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,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())

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 8088 uvicorn main:fast_api_app --reload --host 0.0.0.0 --port 21021

2
start.sh Executable file → Normal file
View File

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