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:
|
variables:
|
||||||
PROJECT_NAME: fastapi_app_template
|
PROJECT_NAME: fastapi_app_template
|
||||||
DOCKER_IMAGE_DOMAIN: 192.168.2.237:8088
|
DOCKER_IMAGE_DOMAIN: hub.airpig.cn
|
||||||
LATEST_VERSION: latest
|
LATEST_VERSION: latest
|
||||||
K8S_NS: default
|
K8S_NS: default
|
||||||
DEPLOYMENT_NAME: fastapi-app-template
|
DEPLOYMENT_NAME: fastapi-app-template
|
||||||
|
|
|
||||||
|
|
@ -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 busybox
|
||||||
FROM python:3.8.6-slim as build
|
|
||||||
|
|
||||||
RUN mkdir /install
|
RUN mkdir -p /data /app
|
||||||
|
|
||||||
WORKDIR /install
|
WORKDIR /data
|
||||||
|
|
||||||
COPY requirements.txt .
|
COPY . /data
|
||||||
|
|
||||||
RUN sed -i s@/deb.debian.org/@/mirrors.aliyun.com/@g /etc/apt/sources.list
|
ENTRYPOINT [ "/bin/cp", "-r", "/data/*", "/app"]
|
||||||
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install gcc -y \
|
|
||||||
&& apt-get clean
|
|
||||||
|
|
||||||
RUN pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/ --prefix=/install
|
|
||||||
|
|
||||||
# 应用启动
|
|
||||||
FROM python:3.8.6-slim
|
|
||||||
|
|
||||||
COPY --from=build /install /usr/local
|
|
||||||
|
|
||||||
COPY . /app
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
ENV FAST_API_ENV=dev
|
|
||||||
|
|
||||||
CMD ["/usr/local/bin/uvicorn", "main:fast_api_app", "--reload", "--host", "0.0.0.0", "--port", "21021"]
|
|
||||||
|
|
|
||||||
|
|
@ -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 文档
|
# FastAPI App 文档
|
||||||
|
|
||||||
## 0 开发说明
|
## 0 开发说明
|
||||||
Python(3.8.6+) and pip(20.2.4+)
|
Python(3.10+) and pip(20.2.4+)
|
||||||
### 开发命名规范
|
### 开发命名规范
|
||||||
> * 避免采用的名字
|
> * 避免采用的名字
|
||||||
> 不要使用字符‘l’(小写字母el),‘O’(大写字母oh)或‘I’(大写字母eye)作为单字符变量名。
|
> 不要使用字符‘l’(小写字母el),‘O’(大写字母oh)或‘I’(大写字母eye)作为单字符变量名。
|
||||||
|
|
@ -32,9 +32,12 @@ database=test
|
||||||
|
|
||||||
## 2 开发环境下安装依赖和运行项目
|
## 2 开发环境下安装依赖和运行项目
|
||||||
``` bash
|
``` bash
|
||||||
pip install -r requirements.txt
|
pip install poetry -i https://pypi.tuna.tsinghua.edu.cn/simple/ \
|
||||||
|
&& poetry source add --priority=primary mirrors https://pypi.tuna.tsinghua.edu.cn/simple/ \
|
||||||
|
&& poetry config virtualenvs.path /install \
|
||||||
|
&& poetry install
|
||||||
./start.bat (windows)
|
./start.bat (windows)
|
||||||
./start.sh (mac)
|
./start.sh (mac/linux)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -55,22 +58,31 @@ sqlacodegen.exe --tables permission_info --outfile .\Desktop\fastapi_app\models\
|
||||||
logs *日志文件目录
|
logs *日志文件目录
|
||||||
src *源码目录
|
src *源码目录
|
||||||
|--- api *接口目录
|
|--- api *接口目录
|
||||||
|--- biz *逻辑目录
|
|--- service *逻辑目录
|
||||||
|--- dtos *接口参数和返回值目录
|
|--- dtos *接口参数和返回值目录
|
||||||
|--- models *数据model目录
|
|--- models *数据model目录
|
||||||
|
|--- middleware *中间件目录
|
||||||
|
|--- router *路由目录 ----- TODO
|
||||||
|--- utils *工具类目录
|
|--- utils *工具类目录
|
||||||
|--- captcha_tools *验证码
|
|--- captcha *验证码
|
||||||
|--- common_tools *常规
|
|--- common *常规
|
||||||
|--- exception_tools *异常处理
|
|--- exception *异常处理
|
||||||
|--- file_upload_tools *文件上传
|
|--- file_upload *文件上传
|
||||||
|--- qiniu_tools *七牛
|
|--- qiniu_tools *七牛
|
||||||
|--- sms_tools *短信
|
|--- sms *短信
|
||||||
static *静态文件目录
|
static *静态文件目录
|
||||||
test *测试目录
|
test *测试目录
|
||||||
|
.gitignore *git忽略文件
|
||||||
.gitlab-ci.yml * CI/CD文件
|
.gitlab-ci.yml * CI/CD文件
|
||||||
main.py *入口文件
|
main.py *入口文件
|
||||||
config.py *配置入口文件
|
config.py *配置入口文件
|
||||||
Dockerfile *dockerfile容器文件
|
Dockerfile *dockerfile容器代码
|
||||||
requirements.txt *依赖包安装文件
|
RuntimeDockerfile *dockerfile容器运行环境
|
||||||
|
requirements.txt * pip依赖包安装文件
|
||||||
|
pyproject.toml * poetry依赖包安装文件
|
||||||
|
poetry.lock * poetry依赖包安装文件
|
||||||
|
Pipfile * pipenv安装文件
|
||||||
|
Pipfile.lock * pipenv安装文件
|
||||||
|
README.md * 项目说明文件
|
||||||
start.bat *Windows开发环境下启动文件
|
start.bat *Windows开发环境下启动文件
|
||||||
start.sh *Unix开发环境下启动文件
|
start.sh *Unix开发环境下启动文件
|
||||||
|
|
@ -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]
|
[mysql]
|
||||||
username=root
|
username=root
|
||||||
password=123456
|
password=123456
|
||||||
host=192.168.2.94
|
host=127.0.0.1
|
||||||
port=3306
|
port=3306
|
||||||
database=admgs_v2
|
database=test
|
||||||
|
|
||||||
|
|
||||||
[redis]
|
[redis]
|
||||||
redis_host=192.168.2.94
|
redis_host=127.0.0.1
|
||||||
redis_port=6379
|
redis_port=6379
|
||||||
|
redis_password=
|
||||||
|
|
||||||
[rabbitmq]
|
[rabbitmq]
|
||||||
rabbitmq_host=192.168.2.94
|
rabbitmq_host=rabbitmq
|
||||||
rabbitmq_user=root
|
rabbitmq_user=root
|
||||||
rabbitmq_password=123123
|
rabbitmq_password=123123
|
||||||
|
|
@ -4,17 +4,18 @@ lifetime_seconds=3600
|
||||||
|
|
||||||
[mysql]
|
[mysql]
|
||||||
username=root
|
username=root
|
||||||
password=123456
|
password=Chenweijia113!
|
||||||
host=192.168.2.94
|
host=mysql
|
||||||
port=3306
|
port=3306
|
||||||
database=admgs_v2
|
database=test
|
||||||
|
|
||||||
|
|
||||||
[redis]
|
[redis]
|
||||||
redis_host=192.168.2.94
|
redis_host=redis
|
||||||
redis_port=6379
|
redis_port=6379
|
||||||
|
redis_password=Chenweijia113!
|
||||||
|
|
||||||
[rabbitmq]
|
[rabbitmq]
|
||||||
rabbitmq_host=192.168.2.94
|
rabbitmq_host=rabbitmq
|
||||||
rabbitmq_user=root
|
rabbitmq_user=root
|
||||||
rabbitmq_password=123123
|
rabbitmq_password=123123
|
||||||
|
|
@ -3,7 +3,7 @@ from configparser import ConfigParser
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from fastapi_plugins import RedisSettings
|
from fastapi_plugins import RedisSettings
|
||||||
from pydantic import BaseSettings
|
from pydantic_settings import BaseSettings
|
||||||
|
|
||||||
|
|
||||||
class ReConfigParser(ConfigParser):
|
class ReConfigParser(ConfigParser):
|
||||||
|
|
@ -34,7 +34,6 @@ class MySQLConfig(BaseSettings):
|
||||||
|
|
||||||
|
|
||||||
class RedisConfig(RedisSettings):
|
class RedisConfig(RedisSettings):
|
||||||
redis_url: str = None
|
|
||||||
redis_host: Optional[str] = 'localhost'
|
redis_host: Optional[str] = 'localhost'
|
||||||
redis_port: Optional[int] = 6379
|
redis_port: Optional[int] = 6379
|
||||||
redis_password: str = None
|
redis_password: str = None
|
||||||
|
|
@ -60,7 +59,7 @@ def init_config():
|
||||||
# common_config = CommonConfig(**dict(config.items('common')))
|
# common_config = CommonConfig(**dict(config.items('common')))
|
||||||
mysql_config = MySQLConfig(**dict(config.items('mysql')))
|
mysql_config = MySQLConfig(**dict(config.items('mysql')))
|
||||||
redis_config = RedisConfig(**dict(config.items('redis')))
|
redis_config = RedisConfig(**dict(config.items('redis')))
|
||||||
rabbitmq_config = RabbitmqConfig(**dict(config.items("rabbitmq")))
|
# rabbitmq_config = RabbitmqConfig(**dict(config.items("rabbitmq")))
|
||||||
# return common_config, mysql_config, redis_config, rabbitmq_config
|
# return common_config, mysql_config, redis_config, rabbitmq_config
|
||||||
return mysql_config, redis_config
|
return mysql_config, redis_config
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
48
main.py
48
main.py
|
|
@ -1,23 +1,26 @@
|
||||||
import logging.config as logging_config
|
import logging.config as logging_config
|
||||||
import os
|
import os
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
import fastapi_plugins
|
import fastapi_plugins
|
||||||
from fastapi import FastAPI, Request
|
from fastapi import FastAPI, Request
|
||||||
from fastapi.exceptions import HTTPException, RequestValidationError
|
from fastapi.exceptions import HTTPException, RequestValidationError
|
||||||
from fastapi.middleware.wsgi import WSGIMiddleware
|
# from fastapi.middleware.wsgi import WSGIMiddleware
|
||||||
from fastapi_sqlalchemy import DBSessionMiddleware
|
from fastapi_sqlalchemy import DBSessionMiddleware
|
||||||
|
from authx.exceptions import AuthXException
|
||||||
from config import init_config
|
from config import init_config
|
||||||
# from src.middleware.flask import flask_app
|
# from src.middleware.flask import flask_app
|
||||||
from src.utils.exception import (http_exception_handler,
|
|
||||||
request_validation_error_handler)
|
from src.utils.exception import http_exception_handler, request_validation_error_handler, authx_exception_handler
|
||||||
|
# 请求限制
|
||||||
|
import redis.asyncio as aio_redis
|
||||||
|
from fastapi_limiter import FastAPILimiter
|
||||||
|
|
||||||
|
|
||||||
def create_app():
|
def create_app():
|
||||||
app = FastAPI()
|
mysql_config, redis_config = init_config()
|
||||||
|
|
||||||
@app.on_event("startup")
|
@asynccontextmanager
|
||||||
async def startup_event():
|
async def lifespan(app: FastAPI):
|
||||||
# 创建日志文件夹和临时文件上传文件夹
|
# 创建日志文件夹和临时文件上传文件夹
|
||||||
if not os.path.exists("files"):
|
if not os.path.exists("files"):
|
||||||
os.mkdir("files")
|
os.mkdir("files")
|
||||||
|
|
@ -27,31 +30,34 @@ def create_app():
|
||||||
os.mkdir("logs")
|
os.mkdir("logs")
|
||||||
logging_config.fileConfig('conf/log.ini')
|
logging_config.fileConfig('conf/log.ini')
|
||||||
# 初始化配置文件
|
# 初始化配置文件
|
||||||
mysql_config, redis_config = init_config()
|
|
||||||
# 添加sqlalchemy数据库中间件
|
|
||||||
# once the middleware is applied, any route can then access the database session from the global ``db``
|
|
||||||
app.add_middleware(DBSessionMiddleware, db_url=mysql_config.sqlalchemy_db_uri)
|
|
||||||
# Redis 缓存初始化
|
# Redis 缓存初始化
|
||||||
await fastapi_plugins.redis_plugin.init_app(app, redis_config)
|
await fastapi_plugins.redis_plugin.init_app(app, redis_config)
|
||||||
await fastapi_plugins.redis_plugin.init()
|
await fastapi_plugins.redis_plugin.init()
|
||||||
|
# 请求限制
|
||||||
@app.on_event("shutdown")
|
await FastAPILimiter.init(redis=aio_redis.from_url("redis://localhost:6379", encoding="utf8"))
|
||||||
async def shutdown_event():
|
yield
|
||||||
|
# 应用关闭时,关闭redis连接
|
||||||
await fastapi_plugins.redis_plugin.terminate()
|
await fastapi_plugins.redis_plugin.terminate()
|
||||||
|
await FastAPILimiter.close()
|
||||||
|
app = FastAPI(lifespan=lifespan)
|
||||||
|
# 添加sqlalchemy数据库中间件
|
||||||
|
# once the middleware is applied, any route can then access the database session from the global ``db``
|
||||||
|
app.add_middleware(DBSessionMiddleware, db_url=mysql_config.sqlalchemy_db_uri)
|
||||||
# 添加异常处理
|
# 添加异常处理
|
||||||
app.add_exception_handler(HTTPException, http_exception_handler)
|
app.add_exception_handler(HTTPException, http_exception_handler)
|
||||||
app.add_exception_handler(RequestValidationError, request_validation_error_handler)
|
app.add_exception_handler(RequestValidationError, request_validation_error_handler)
|
||||||
|
# AuthX异常处理
|
||||||
|
app.add_exception_handler(AuthXException, authx_exception_handler)
|
||||||
|
|
||||||
|
|
||||||
# 可以在这里挂载Flask的应用,复用之前项目的相关代码
|
# 可以在这里挂载Flask的应用,复用之前项目的相关代码
|
||||||
# app.mount("/v1", WSGIMiddleware(flask_app))
|
# app.mount("/v1", WSGIMiddleware(flask_app))
|
||||||
|
|
||||||
|
|
||||||
# 在这里添加API route
|
# 在这里添加API route
|
||||||
from src.api import example
|
from src.api import example,auth_example
|
||||||
app.include_router(example.router, tags=["API示例"], prefix="/example")
|
app.include_router(example.router, tags=["API示例"], prefix="/v1/example")
|
||||||
|
app.include_router(auth_example.router, tags=["认证示例"], prefix="/v1/auth_example")
|
||||||
|
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
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
|
--index-url https://pypi.tuna.tsinghua.edu.cn/simple
|
||||||
aioredis==2.0.1
|
|
||||||
aiomysql==0.1.1
|
aiojobs==1.2.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
bcrypt==4.0.1
|
aiomysql==0.1.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
email-validator==1.3.1
|
aioredis==2.0.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
fastapi==0.89.1
|
annotated-types==0.7.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
fastapi-plugins==0.11.0
|
anyio==4.4.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
FastAPI-SQLAlchemy==0.2.1
|
async-timeout==4.0.3 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
pydantic==1.10.4
|
authx==1.3.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
# pydantic-sqlalchemy==0.0.9
|
captcha==0.6.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
python-multipart==0.0.5
|
certifi==2024.6.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
pytest==7.2.1
|
cffi==1.17.0 ; python_version >= "3.10" and python_version < "4.0" and platform_python_implementation != "PyPy"
|
||||||
requests==2.28.2
|
charset-normalizer==3.3.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
sqlacodegen==2.3.0
|
click==8.1.7 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
SQLAlchemy==2.0.1
|
colorama==0.4.6 ; python_version >= "3.10" and python_version < "4.0" and (sys_platform == "win32" or platform_system == "Windows")
|
||||||
uvicorn==0.20.0
|
cryptography==43.0.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
PyJWT==2.6.0
|
dnspython==2.6.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
passlib==1.7.4
|
ecdsa==0.19.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
Pillow==9.4.0
|
email-validator==2.1.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
captcha==0.4
|
exceptiongroup==1.2.1 ; python_version >= "3.10" and python_version < "3.11"
|
||||||
jinja2==3.1.2
|
fastapi-cli==0.0.4 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
pycryptodome==3.17
|
fastapi-limiter==0.1.6 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
qiniu==7.10.0
|
fastapi-plugins==0.13.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
pytz==2022.7.1
|
fastapi-sqlalchemy==0.2.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
fastapi==0.111.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
greenlet==3.0.3 ; python_version >= "3.10" and python_version < "4.0" and (platform_machine == "aarch64" or platform_machine == "ppc64le" or platform_machine == "x86_64" or platform_machine == "amd64" or platform_machine == "AMD64" or platform_machine == "win32" or platform_machine == "WIN32")
|
||||||
|
h11==0.14.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
hiredis==2.3.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
httpcore==1.0.5 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
httptools==0.6.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
httpx==0.27.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
idna==3.7 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
itsdangerous==2.2.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
jinja2==3.1.4 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
markdown-it-py==3.0.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
markupsafe==2.1.5 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
mdurl==0.1.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
orjson==3.10.4 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
passlib==1.7.4 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
pillow==10.4.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
pyasn1==0.6.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
pycparser==2.22 ; python_version >= "3.10" and python_version < "4.0" and platform_python_implementation != "PyPy"
|
||||||
|
pydantic-core==2.18.4 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
pydantic-settings==2.3.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
pydantic==2.7.3 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
pygments==2.18.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
pyjwt[crypto]==2.9.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
pymysql==1.1.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
python-dateutil==2.9.0.post0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
python-dotenv==1.0.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
python-jose==3.3.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
python-json-logger==2.0.7 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
python-multipart==0.0.9 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
pytz==2024.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
pyyaml==6.0.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
qiniu==7.13.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
redis==5.0.5 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
redis[hiredis]==5.0.5 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
requests==2.32.3 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
rich==13.7.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
rsa==4.9 ; python_version >= "3.10" and python_version < "4"
|
||||||
|
shellingham==1.5.4 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
six==1.16.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
sniffio==1.3.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
sqlalchemy==2.0.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
starlette==0.37.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
tenacity==8.3.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
typer==0.12.3 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
typing-extensions==4.12.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
ujson==5.10.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
urllib3==2.2.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
uvicorn[standard]==0.30.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
uvloop==0.19.0 ; (sys_platform != "win32" and sys_platform != "cygwin") and platform_python_implementation != "PyPy" and python_version >= "3.10" and python_version < "4.0"
|
||||||
|
watchfiles==0.22.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
websockets==12.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
|
|
||||||
|
|
@ -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 import APIRouter, Depends, Query
|
||||||
from fastapi.security import OAuth2PasswordBearer
|
from fastapi.security import OAuth2PasswordBearer
|
||||||
from fastapi_plugins import depends_redis
|
from fastapi_plugins import depends_redis
|
||||||
|
from fastapi_sqlalchemy import db
|
||||||
|
from sqlalchemy.sql import text
|
||||||
|
from fastapi_limiter.depends import RateLimiter
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from src.biz.example import get_user_by_id
|
from src.service.example import get_user_by_id
|
||||||
from src.dtos import response
|
from src.dtos import response
|
||||||
from src.dtos.example import UserExampleListPagesResult
|
from src.dtos.example import UserExampleListPagesResult
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
# 并发数限制 每5秒最多100次请求
|
||||||
@router.get("/")
|
@router.get("/", dependencies=[Depends(RateLimiter(times=100, seconds=5))])
|
||||||
def index():
|
def index():
|
||||||
return {"msg": "This is Index Page"}
|
return {"msg": "This is Index Page"}
|
||||||
|
|
||||||
|
|
@ -59,6 +63,11 @@ async def get_user_list_pages(page: int = Query(..., description="当前页码")
|
||||||
res = response(data=data, message="Ok!")
|
res = response(data=data, message="Ok!")
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
# 数据库查询
|
||||||
|
@router.get('/get_db_version')
|
||||||
|
async def get_db_version():
|
||||||
|
result = db.session.execute(text("SELECT version()")).first()
|
||||||
|
return dict(result=result.tuple()[0])
|
||||||
|
|
||||||
# Redis 缓存查询
|
# Redis 缓存查询
|
||||||
@router.get("/ping")
|
@router.get("/ping")
|
||||||
|
|
|
||||||
|
|
@ -52,9 +52,9 @@ class SendCaptchaSuccess(BaseResponse):
|
||||||
|
|
||||||
|
|
||||||
class BadRequestError(ErrorModel):
|
class BadRequestError(ErrorModel):
|
||||||
code = 400
|
code: int = 400
|
||||||
message = "BAD REQUEST"
|
message: str = "BAD REQUEST"
|
||||||
details = "请求参数错误"
|
details: str = "请求参数错误"
|
||||||
|
|
||||||
|
|
||||||
class BadRequestResponse(BaseResponse):
|
class BadRequestResponse(BaseResponse):
|
||||||
|
|
@ -62,9 +62,9 @@ class BadRequestResponse(BaseResponse):
|
||||||
|
|
||||||
|
|
||||||
class ServerInternalError(ErrorModel):
|
class ServerInternalError(ErrorModel):
|
||||||
code = 500
|
code: int = 500
|
||||||
message = "INTERNAL SERVER ERROR"
|
message: str = "INTERNAL SERVER ERROR"
|
||||||
details = "服务器内部错误"
|
details: str = "服务器内部错误"
|
||||||
|
|
||||||
|
|
||||||
class ServerInternalResponse(BaseResponse):
|
class ServerInternalResponse(BaseResponse):
|
||||||
|
|
|
||||||
|
|
@ -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):
|
def get_user_by_id(user_id):
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import logging
|
import logging
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
from fastapi import Request, status
|
from fastapi import Request, status
|
||||||
from fastapi.exceptions import HTTPException, RequestValidationError
|
from fastapi.exceptions import HTTPException, RequestValidationError
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
|
import authx.exceptions as authx_exceptions
|
||||||
from src.dtos import BaseResponse, ErrorModel
|
from src.dtos import BaseResponse, ErrorModel
|
||||||
|
|
||||||
logger = logging.getLogger("uvicorn.error")
|
logger = logging.getLogger("uvicorn.error")
|
||||||
|
|
@ -11,35 +12,32 @@ logger = logging.getLogger("uvicorn.error")
|
||||||
|
|
||||||
def request_validation_error_handler(req: Request, exc: RequestValidationError):
|
def request_validation_error_handler(req: Request, exc: RequestValidationError):
|
||||||
"""
|
"""
|
||||||
请求响应校验错误处理
|
请求校验错误处理
|
||||||
:param req:
|
:param req:
|
||||||
:param exc:
|
:param exc:
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
errors = exc.errors()
|
errors = exc.errors()
|
||||||
logger.error(f"{req.method} {req.url} , errors: {errors}")
|
logger.error(f"{req.method} {req.url} , errors: {errors}")
|
||||||
response_errors = [error for error in errors if error["loc"][0] == "response"]
|
group_err = defaultdict(list)
|
||||||
if len(response_errors) > 0:
|
for err in errors:
|
||||||
message = "接口响应异常 "
|
group_err[err["type"]].append(err)
|
||||||
else:
|
details = ""
|
||||||
message = "接口请求异常 "
|
for err_type, err_list in group_err.items():
|
||||||
json_err = [err["msg"] for err in errors if "value_error.jsondecode" == err["type"]]
|
match err_type:
|
||||||
if json_err:
|
case "missing" | "value_error.missing" :
|
||||||
message += "请求参数JSON编码有误;"
|
details += f"缺少字段:{', '.join([err['loc'][-1] for err in err_list])};"
|
||||||
value_err = [err["msg"] for err in errors if "value_error" == err["type"]]
|
case "value_error.jsondecode":
|
||||||
if value_err:
|
details += "请求参数JSON编码有误;"
|
||||||
message += "; ".join(value_err)
|
case "value_error":
|
||||||
missing_err = [err["loc"][-1] for err in errors if "value_error.missing" == err["type"]]
|
details += "; ".join([err["msg"] for err in err_list])
|
||||||
if missing_err:
|
case "type_error":
|
||||||
message += "缺少字段:{} ".format(", ".join(missing_err))
|
details += "; ".join([err["msg"].replace("value", f"{err['loc'][1]}") for err in err_list])
|
||||||
type_err = [err["msg"].replace("value", f"{err['loc'][1]}") for err in errors if "type_error" == err["type"]]
|
case "type_error.none.not_allowed":
|
||||||
if type_err:
|
details += f"字段:{', '.join([err['loc'][-1] for err in err_list])} 不能为空;"
|
||||||
message += "; ".join(type_err)
|
case _:
|
||||||
not_none_err = [err["loc"][-1] for err in errors if "type_error.none.not_allowed" == err["type"]]
|
pass
|
||||||
if not_none_err:
|
err_model = ErrorModel(code=status.HTTP_400_BAD_REQUEST, message="接口请求校验异常", details=details)
|
||||||
message += "字段:{} 不能为空".format(", ".join(not_none_err))
|
|
||||||
|
|
||||||
err_model = ErrorModel(code=status.HTTP_400_BAD_REQUEST, message=message, details=message)
|
|
||||||
res = BaseResponse(error=err_model)
|
res = BaseResponse(error=err_model)
|
||||||
return JSONResponse(status_code=200, content=res.dict())
|
return JSONResponse(status_code=200, content=res.dict())
|
||||||
|
|
||||||
|
|
@ -54,4 +52,23 @@ def http_exception_handler(req: Request, exc: HTTPException):
|
||||||
logger.error(f"{req.method} {req.url} , exception:{exc.detail}")
|
logger.error(f"{req.method} {req.url} , exception:{exc.detail}")
|
||||||
err = ErrorModel(code=exc.status_code, message=exc.detail)
|
err = ErrorModel(code=exc.status_code, message=exc.detail)
|
||||||
res = BaseResponse(result=None, error=err)
|
res = BaseResponse(result=None, error=err)
|
||||||
return JSONResponse(status_code=exc.status_code, content=res.dict())
|
return JSONResponse(status_code=exc.status_code, content=res.model_dump())
|
||||||
|
|
||||||
|
|
||||||
|
def authx_exception_handler(req: Request, exc: authx_exceptions.AuthXException):
|
||||||
|
"""
|
||||||
|
AuthX Exception 错误处理
|
||||||
|
:param req:
|
||||||
|
:param exc:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
logger.error(f"{req.method} {req.url} , exception:{exc}")
|
||||||
|
match type(exc):
|
||||||
|
case authx_exceptions.MissingTokenError:
|
||||||
|
err = ErrorModel(code=status.HTTP_401_UNAUTHORIZED, message="请求头缺失X-Token")
|
||||||
|
case authx_exceptions.JWTDecodeError:
|
||||||
|
err = ErrorModel(code=status.HTTP_401_UNAUTHORIZED, message="X-Token解析失败")
|
||||||
|
case _:
|
||||||
|
err = ErrorModel(code=status.HTTP_401_UNAUTHORIZED, message="未知的错误导致认证失败")
|
||||||
|
res = BaseResponse(result=None, error=err)
|
||||||
|
return JSONResponse(status_code=err.code, content=res.model_dump())
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
set FAST_API_ENV=dev
|
set FAST_API_ENV=dev
|
||||||
uvicorn main:fast_api_app --reload --host 0.0.0.0 --port 21021
|
uvicorn main:fast_api_app --reload --host 0.0.0.0 --port 8088
|
||||||
Loading…
Reference in New Issue