Compare commits
23 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
fc5d6b8953 | |
|
|
6d69a18b03 | |
|
|
61f91fa932 | |
|
|
ee14c5591a | |
|
|
dce678ca95 | |
|
|
8982dd82d8 | |
|
|
8459e0c447 | |
|
|
bd9dac23be | |
|
|
c6767778e2 | |
|
|
088bee0199 | |
|
|
b23786319e | |
|
|
2d4e8397ef | |
|
|
9bf965587e | |
|
|
598bb654c7 | |
|
|
2ee8beb276 | |
|
|
59d4f098ea | |
|
|
d93ec2b0f3 | |
|
|
6e60009711 | |
|
|
0ef5ce6fd8 | |
|
|
3f0827e64f | |
|
|
7dfc9ff205 | |
|
|
dad5d2aaa5 | |
|
|
c3fda4c8b5 |
|
|
@ -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 }}."
|
||||
|
|
@ -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 }}."
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
variables:
|
||||
PROJECT_NAME: fastapi_app_template
|
||||
DOCKER_IMAGE_DOMAIN: 192.168.2.237:8088
|
||||
DOCKER_IMAGE_DOMAIN: hub.airpig.cn
|
||||
LATEST_VERSION: latest
|
||||
K8S_NS: default
|
||||
DEPLOYMENT_NAME: fastapi-app-template
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
29
Dockerfile
29
Dockerfile
|
|
@ -1,29 +1,10 @@
|
|||
# 安装依赖阶段
|
||||
FROM python:3.8.6-slim as build
|
||||
FROM busybox
|
||||
|
||||
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"]
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
36
README.md
36
README.md
|
|
@ -1,7 +1,7 @@
|
|||
# FastAPI App 文档
|
||||
|
||||
## 0 开发说明
|
||||
Python(3.8.6+) and pip(20.2.4+)
|
||||
Python(3.10+) and pip(20.2.4+)
|
||||
### 开发命名规范
|
||||
> * 避免采用的名字
|
||||
> 不要使用字符‘l’(小写字母el),‘O’(大写字母oh)或‘I’(大写字母eye)作为单字符变量名。
|
||||
|
|
@ -32,9 +32,12 @@ database=test
|
|||
|
||||
## 2 开发环境下安装依赖和运行项目
|
||||
``` 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.sh (mac)
|
||||
./start.sh (mac/linux)
|
||||
```
|
||||
|
||||
|
||||
|
|
@ -55,22 +58,31 @@ sqlacodegen.exe --tables permission_info --outfile .\Desktop\fastapi_app\models\
|
|||
logs *日志文件目录
|
||||
src *源码目录
|
||||
|--- api *接口目录
|
||||
|--- biz *逻辑目录
|
||||
|--- service *逻辑目录
|
||||
|--- dtos *接口参数和返回值目录
|
||||
|--- models *数据model目录
|
||||
|--- middleware *中间件目录
|
||||
|--- router *路由目录 ----- TODO
|
||||
|--- utils *工具类目录
|
||||
|--- captcha_tools *验证码
|
||||
|--- common_tools *常规
|
||||
|--- exception_tools *异常处理
|
||||
|--- file_upload_tools *文件上传
|
||||
|--- qiniu_tools *七牛
|
||||
|--- sms_tools *短信
|
||||
|--- captcha *验证码
|
||||
|--- common *常规
|
||||
|--- exception *异常处理
|
||||
|--- file_upload *文件上传
|
||||
|--- qiniu_tools *七牛
|
||||
|--- sms *短信
|
||||
static *静态文件目录
|
||||
test *测试目录
|
||||
.gitignore *git忽略文件
|
||||
.gitlab-ci.yml * CI/CD文件
|
||||
main.py *入口文件
|
||||
config.py *配置入口文件
|
||||
Dockerfile *dockerfile容器文件
|
||||
requirements.txt *依赖包安装文件
|
||||
Dockerfile *dockerfile容器代码
|
||||
RuntimeDockerfile *dockerfile容器运行环境
|
||||
requirements.txt * pip依赖包安装文件
|
||||
pyproject.toml * poetry依赖包安装文件
|
||||
poetry.lock * poetry依赖包安装文件
|
||||
Pipfile * pipenv安装文件
|
||||
Pipfile.lock * pipenv安装文件
|
||||
README.md * 项目说明文件
|
||||
start.bat *Windows开发环境下启动文件
|
||||
start.sh *Unix开发环境下启动文件
|
||||
|
|
@ -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"]
|
||||
|
|
@ -5,16 +5,17 @@ lifetime_seconds=3600
|
|||
[mysql]
|
||||
username=root
|
||||
password=123456
|
||||
host=192.168.2.94
|
||||
host=127.0.0.1
|
||||
port=3306
|
||||
database=admgs_v2
|
||||
database=test
|
||||
|
||||
|
||||
[redis]
|
||||
redis_host=192.168.2.94
|
||||
redis_host=127.0.0.1
|
||||
redis_port=6379
|
||||
redis_password=
|
||||
|
||||
[rabbitmq]
|
||||
rabbitmq_host=192.168.2.94
|
||||
rabbitmq_host=rabbitmq
|
||||
rabbitmq_user=root
|
||||
rabbitmq_password=123123
|
||||
|
|
@ -4,17 +4,18 @@ lifetime_seconds=3600
|
|||
|
||||
[mysql]
|
||||
username=root
|
||||
password=123456
|
||||
host=192.168.2.94
|
||||
password=Chenweijia113!
|
||||
host=mysql
|
||||
port=3306
|
||||
database=admgs_v2
|
||||
database=test
|
||||
|
||||
|
||||
[redis]
|
||||
redis_host=192.168.2.94
|
||||
redis_host=redis
|
||||
redis_port=6379
|
||||
redis_password=Chenweijia113!
|
||||
|
||||
[rabbitmq]
|
||||
rabbitmq_host=192.168.2.94
|
||||
rabbitmq_host=rabbitmq
|
||||
rabbitmq_user=root
|
||||
rabbitmq_password=123123
|
||||
|
|
@ -3,7 +3,7 @@ from configparser import ConfigParser
|
|||
from typing import Optional
|
||||
|
||||
from fastapi_plugins import RedisSettings
|
||||
from pydantic import BaseSettings
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class ReConfigParser(ConfigParser):
|
||||
|
|
@ -34,7 +34,6 @@ class MySQLConfig(BaseSettings):
|
|||
|
||||
|
||||
class RedisConfig(RedisSettings):
|
||||
redis_url: str = None
|
||||
redis_host: Optional[str] = 'localhost'
|
||||
redis_port: Optional[int] = 6379
|
||||
redis_password: str = None
|
||||
|
|
@ -60,7 +59,7 @@ def init_config():
|
|||
# common_config = CommonConfig(**dict(config.items('common')))
|
||||
mysql_config = MySQLConfig(**dict(config.items('mysql')))
|
||||
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 mysql_config, redis_config
|
||||
except Exception as e:
|
||||
|
|
|
|||
48
main.py
48
main.py
|
|
@ -1,23 +1,26 @@
|
|||
import logging.config as logging_config
|
||||
import os
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
import fastapi_plugins
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.exceptions import HTTPException, RequestValidationError
|
||||
from fastapi.middleware.wsgi import WSGIMiddleware
|
||||
# from fastapi.middleware.wsgi import WSGIMiddleware
|
||||
from fastapi_sqlalchemy import DBSessionMiddleware
|
||||
|
||||
from authx.exceptions import AuthXException
|
||||
from config import init_config
|
||||
# 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():
|
||||
app = FastAPI()
|
||||
mysql_config, redis_config = init_config()
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
# 创建日志文件夹和临时文件上传文件夹
|
||||
if not os.path.exists("files"):
|
||||
os.mkdir("files")
|
||||
|
|
@ -27,31 +30,34 @@ def create_app():
|
|||
os.mkdir("logs")
|
||||
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 缓存初始化
|
||||
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 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数据库中间件
|
||||
# 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(RequestValidationError, request_validation_error_handler)
|
||||
# AuthX异常处理
|
||||
app.add_exception_handler(AuthXException, authx_exception_handler)
|
||||
|
||||
|
||||
# 可以在这里挂载Flask的应用,复用之前项目的相关代码
|
||||
# app.mount("/v1", WSGIMiddleware(flask_app))
|
||||
|
||||
|
||||
# 在这里添加API route
|
||||
from src.api import example
|
||||
app.include_router(example.router, tags=["API示例"], prefix="/example")
|
||||
|
||||
|
||||
|
||||
from src.api import example,auth_example
|
||||
app.include_router(example.router, tags=["API示例"], prefix="/v1/example")
|
||||
app.include_router(auth_example.router, tags=["认证示例"], prefix="/v1/auth_example")
|
||||
return app
|
||||
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -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"
|
||||
|
|
@ -1,24 +1,75 @@
|
|||
aiofiles==22.1.0
|
||||
aioredis==2.0.1
|
||||
aiomysql==0.1.1
|
||||
bcrypt==4.0.1
|
||||
email-validator==1.3.1
|
||||
fastapi==0.89.1
|
||||
fastapi-plugins==0.11.0
|
||||
FastAPI-SQLAlchemy==0.2.1
|
||||
pydantic==1.10.4
|
||||
# pydantic-sqlalchemy==0.0.9
|
||||
python-multipart==0.0.5
|
||||
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
|
||||
--index-url https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
|
||||
aiojobs==1.2.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
aiomysql==0.1.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
aioredis==2.0.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
annotated-types==0.7.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
anyio==4.4.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
async-timeout==4.0.3 ; python_version >= "3.10" and python_version < "4.0"
|
||||
authx==1.3.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
captcha==0.6.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
certifi==2024.6.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||
cffi==1.17.0 ; python_version >= "3.10" and python_version < "4.0" and platform_python_implementation != "PyPy"
|
||||
charset-normalizer==3.3.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||
click==8.1.7 ; python_version >= "3.10" and python_version < "4.0"
|
||||
colorama==0.4.6 ; python_version >= "3.10" and python_version < "4.0" and (sys_platform == "win32" or platform_system == "Windows")
|
||||
cryptography==43.0.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
dnspython==2.6.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
ecdsa==0.19.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
email-validator==2.1.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
exceptiongroup==1.2.1 ; python_version >= "3.10" and python_version < "3.11"
|
||||
fastapi-cli==0.0.4 ; python_version >= "3.10" and python_version < "4.0"
|
||||
fastapi-limiter==0.1.6 ; python_version >= "3.10" and python_version < "4.0"
|
||||
fastapi-plugins==0.13.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -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"})
|
||||
|
|
@ -5,16 +5,20 @@ import aioredis
|
|||
from fastapi import APIRouter, Depends, Query
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
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 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.example import UserExampleListPagesResult
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/")
|
||||
# 并发数限制 每5秒最多100次请求
|
||||
@router.get("/", dependencies=[Depends(RateLimiter(times=100, seconds=5))])
|
||||
def index():
|
||||
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!")
|
||||
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 缓存查询
|
||||
@router.get("/ping")
|
||||
|
|
|
|||
|
|
@ -52,9 +52,9 @@ class SendCaptchaSuccess(BaseResponse):
|
|||
|
||||
|
||||
class BadRequestError(ErrorModel):
|
||||
code = 400
|
||||
message = "BAD REQUEST"
|
||||
details = "请求参数错误"
|
||||
code: int = 400
|
||||
message: str = "BAD REQUEST"
|
||||
details: str = "请求参数错误"
|
||||
|
||||
|
||||
class BadRequestResponse(BaseResponse):
|
||||
|
|
@ -62,9 +62,9 @@ class BadRequestResponse(BaseResponse):
|
|||
|
||||
|
||||
class ServerInternalError(ErrorModel):
|
||||
code = 500
|
||||
message = "INTERNAL SERVER ERROR"
|
||||
details = "服务器内部错误"
|
||||
code: int = 500
|
||||
message: str = "INTERNAL SERVER ERROR"
|
||||
details: str = "服务器内部错误"
|
||||
|
||||
|
||||
class ServerInternalResponse(BaseResponse):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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='所在楼(建筑)的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,4 +1,4 @@
|
|||
from src.biz import execute_sql
|
||||
from src.service import execute_sql
|
||||
|
||||
|
||||
def get_user_by_id(user_id):
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
import logging
|
||||
from collections import defaultdict
|
||||
|
||||
from fastapi import Request, status
|
||||
from fastapi.exceptions import HTTPException, RequestValidationError
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
import authx.exceptions as authx_exceptions
|
||||
from src.dtos import BaseResponse, ErrorModel
|
||||
|
||||
logger = logging.getLogger("uvicorn.error")
|
||||
|
|
@ -11,35 +12,32 @@ logger = logging.getLogger("uvicorn.error")
|
|||
|
||||
def request_validation_error_handler(req: Request, exc: RequestValidationError):
|
||||
"""
|
||||
请求响应校验错误处理
|
||||
请求校验错误处理
|
||||
:param req:
|
||||
:param exc:
|
||||
:return:
|
||||
"""
|
||||
errors = exc.errors()
|
||||
logger.error(f"{req.method} {req.url} , errors: {errors}")
|
||||
response_errors = [error for error in errors if error["loc"][0] == "response"]
|
||||
if len(response_errors) > 0:
|
||||
message = "接口响应异常 "
|
||||
else:
|
||||
message = "接口请求异常 "
|
||||
json_err = [err["msg"] for err in errors if "value_error.jsondecode" == err["type"]]
|
||||
if json_err:
|
||||
message += "请求参数JSON编码有误;"
|
||||
value_err = [err["msg"] for err in errors if "value_error" == err["type"]]
|
||||
if value_err:
|
||||
message += "; ".join(value_err)
|
||||
missing_err = [err["loc"][-1] for err in errors if "value_error.missing" == err["type"]]
|
||||
if missing_err:
|
||||
message += "缺少字段:{} ".format(", ".join(missing_err))
|
||||
type_err = [err["msg"].replace("value", f"{err['loc'][1]}") for err in errors if "type_error" == err["type"]]
|
||||
if type_err:
|
||||
message += "; ".join(type_err)
|
||||
not_none_err = [err["loc"][-1] for err in errors if "type_error.none.not_allowed" == err["type"]]
|
||||
if not_none_err:
|
||||
message += "字段:{} 不能为空".format(", ".join(not_none_err))
|
||||
|
||||
err_model = ErrorModel(code=status.HTTP_400_BAD_REQUEST, message=message, details=message)
|
||||
group_err = defaultdict(list)
|
||||
for err in errors:
|
||||
group_err[err["type"]].append(err)
|
||||
details = ""
|
||||
for err_type, err_list in group_err.items():
|
||||
match err_type:
|
||||
case "missing" | "value_error.missing" :
|
||||
details += f"缺少字段:{', '.join([err['loc'][-1] for err in err_list])};"
|
||||
case "value_error.jsondecode":
|
||||
details += "请求参数JSON编码有误;"
|
||||
case "value_error":
|
||||
details += "; ".join([err["msg"] for err in err_list])
|
||||
case "type_error":
|
||||
details += "; ".join([err["msg"].replace("value", f"{err['loc'][1]}") for err in err_list])
|
||||
case "type_error.none.not_allowed":
|
||||
details += f"字段:{', '.join([err['loc'][-1] for err in err_list])} 不能为空;"
|
||||
case _:
|
||||
pass
|
||||
err_model = ErrorModel(code=status.HTTP_400_BAD_REQUEST, message="接口请求校验异常", details=details)
|
||||
res = BaseResponse(error=err_model)
|
||||
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}")
|
||||
err = ErrorModel(code=exc.status_code, message=exc.detail)
|
||||
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())
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
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
|
||||
Loading…
Reference in New Issue