├── .env.example ├── .env.test ├── .gitignore ├── .pre-commit-config.yaml ├── .workflow ├── branch-pipeline.yml ├── master-pipeline.yml └── pr-pipeline.yml ├── LICENSE ├── README.md ├── alembic.ini ├── alembic ├── README ├── env.py ├── script.py.mako └── versions │ └── .gitkeep ├── docs ├── PEP.md ├── backgroup_tasks.md ├── 使用 TypeVar 和直接使用实体类型的区别.md └── 单节点 MongoDB 数据库开启副本集.md ├── images └── image-20240518230154682.png ├── kinit_fast_task ├── __init__.py ├── app │ ├── __init__.py │ ├── cruds │ │ ├── __init__.py │ │ ├── auth_role_crud.py │ │ ├── auth_user_crud.py │ │ ├── base │ │ │ ├── __init__.py │ │ │ ├── mongo.py │ │ │ └── orm.py │ │ └── record_operation_crud.py │ ├── depends │ │ ├── Paging.py │ │ └── __init__.py │ ├── models │ │ ├── __init__.py │ │ ├── auth_role_model.py │ │ ├── auth_user_model.py │ │ ├── auth_user_to_role_model.py │ │ └── base │ │ │ ├── __init__.py │ │ │ └── orm.py │ ├── routers │ │ ├── __init__.py │ │ ├── auth_role │ │ │ ├── __init__.py │ │ │ ├── params.py │ │ │ └── views.py │ │ ├── auth_user │ │ │ ├── __init__.py │ │ │ ├── params.py │ │ │ ├── services.py │ │ │ └── views.py │ │ ├── system_record │ │ │ ├── __init__.py │ │ │ ├── params.py │ │ │ └── views.py │ │ └── system_storage │ │ │ ├── __init__.py │ │ │ └── views.py │ ├── schemas │ │ ├── __init__.py │ │ ├── auth_role_schema.py │ │ ├── auth_user_schema.py │ │ ├── base │ │ │ ├── __init__.py │ │ │ └── base.py │ │ ├── global_schema.py │ │ └── record_operation_schema.py │ └── system │ │ ├── __init__.py │ │ └── docs │ │ ├── __init__.py │ │ └── views.py ├── config.py ├── core │ ├── __init__.py │ ├── event.py │ ├── exception.py │ ├── middleware.py │ ├── register.py │ └── types.py ├── db │ ├── __init__.py │ ├── async_base.py │ ├── database_factory.py │ ├── mongo │ │ ├── __init__.py │ │ └── asyncio.py │ ├── orm │ │ ├── __init__.py │ │ ├── async_base_model.py │ │ └── asyncio.py │ └── redis │ │ ├── __init__.py │ │ └── asyncio.py ├── main.py ├── scripts │ ├── __init__.py │ └── app_generate │ │ ├── __init__.py │ │ ├── main.py │ │ ├── utils │ │ ├── __init__.py │ │ ├── generate_base.py │ │ ├── model_to_json_base.py │ │ └── schema_base.py │ │ └── v1 │ │ ├── __init__.py │ │ ├── generate_code.py │ │ ├── generate_crud.py │ │ ├── generate_params.py │ │ ├── generate_schema.py │ │ ├── generate_view.py │ │ ├── json_config_schema.py │ │ └── model_to_json.py ├── static │ ├── redoc_ui │ │ ├── 9e68c24da9f5fd56991c.worker.js.map │ │ └── redoc.standalone.js │ └── swagger_ui │ │ ├── 8c3f9d33cp7f3f2c361ff9d124edf2f9.jpg │ │ ├── swagger-ui-bundle.js │ │ ├── swagger-ui-bundle.js.map │ │ ├── swagger-ui.css │ │ └── swagger-ui.css.map └── utils │ ├── __init__.py │ ├── aes_crypto.py │ ├── count.py │ ├── enum.py │ ├── logger.py │ ├── love.py │ ├── pdf_to_word.py │ ├── ppt_to_pdf.py │ ├── response.py │ ├── response_code.py │ ├── send_email.py │ ├── singleton.py │ ├── socket_client.py │ ├── storage │ ├── __init__.py │ ├── abs.py │ ├── kodo │ │ ├── __init__.py │ │ └── kodo.py │ ├── local │ │ ├── __init__.py │ │ └── local.py │ ├── oss │ │ ├── __init__.py │ │ └── oss.py │ ├── storage_factory.py │ └── temp │ │ ├── __init__.py │ │ └── temp.py │ ├── tools.py │ └── validator.py ├── main.py ├── pyproject.toml ├── pytest.ini ├── ruff.toml └── tests ├── __init__.py └── conftest.py /.env.example: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------- 2 | # system 配置项 3 | # SERVER_HOST: 项目监听主机IP,默认开放给本网络所有主机 4 | # SERVER_PORT:项目监听端口 5 | # DEMO_ENV:否开启演示功能, 为 True 则取消所有POST,DELETE,PUT操作权限 6 | # LOG_CONSOLE_OUT:是否将日志打印在控制台 7 | # API_DOCS_ENABLE:是否开启接口文档访问 8 | # ----------------------------------------------- 9 | SERVER_HOST = "0.0.0.0" 10 | SERVER_PORT = 9000 11 | DEMO_ENV = False 12 | LOG_CONSOLE_OUT = True 13 | API_DOCS_ENABLE = True 14 | 15 | 16 | # ----------------------------------------------- 17 | # router 配置项 18 | # APPS:需要启用的 app router,该顺序也是文档展示顺序 19 | # ----------------------------------------------- 20 | APPS = ["system_storage", "system_record", "auth_user", "auth_role"] 21 | 22 | 23 | # ----------------------------------------------- 24 | # db 配置项 25 | # 26 | # ORM 配置项 27 | # ORM_DB_ENABLE: 是否选择使用 ORM 数据库 28 | # ORM_DB_ECHO: 是否选择输出 ORM 操作日志到控制台 29 | # ORM_DATABASE_URL: ORM 数据库连接地址,默认使用 postgresql, 格式:"postgresql+asyncpg://账号:密码@地址:端口号/数据库名称" 30 | # 31 | # Redis 配置项 32 | # REDIS_DB_ENABLE: 是否选择使用 Redis 数据库 33 | # REDIS_DB_URL: Redis 数据库地址地址, 格式:"redis://:密码@地址:端口/数据库名称" 34 | # 35 | # MongoDB 配置项 36 | # MONGO_DB_ENABLE: 是否选择使用 MongoDB 数据库 37 | # MONGO_DB_URL: MongoDB 数据库连接地址, 格式:"mongodb://用户名:密码@地址:端口/?authSource=数据库名称" 38 | # ----------------------------------------------- 39 | # ORM 配置项 40 | ORM_DB_ENABLE = True 41 | ORM_DB_ECHO = True 42 | ORM_DATABASE_URL = "postgresql+asyncpg://user:123456@127.0.0.1:5432/kinit" 43 | 44 | # Redis 数据库配置 45 | REDIS_DB_ENABLE = False 46 | REDIS_DB_URL = "redis://:123456@127.0.0.1:6379/0" 47 | 48 | # MongoDB 数据库配置 49 | MONGO_DB_ENABLE = True 50 | MONGO_DB_URL = "mongodb://user:123456@127.0.0.1:27017/?authSource=kinit" 51 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------- 2 | # system 配置项 3 | # SERVER_HOST: 项目监听主机IP,默认开放给本网络所有主机 4 | # SERVER_PORT:项目监听端口 5 | # DEMO_ENV:否开启演示功能, 为 True 则取消所有POST,DELETE,PUT操作权限 6 | # LOG_CONSOLE_OUT:是否将日志打印在控制台 7 | # API_DOCS_ENABLE:是否开启接口文档访问 8 | # ----------------------------------------------- 9 | SERVER_HOST = "0.0.0.0" 10 | SERVER_PORT = 9000 11 | DEMO_ENV = False 12 | LOG_CONSOLE_OUT = True 13 | API_DOCS_ENABLE = True 14 | 15 | 16 | # ----------------------------------------------- 17 | # router 配置项 18 | # APPS:需要启用的 app router,该顺序也是文档展示顺序 19 | # ----------------------------------------------- 20 | APPS = ["system_storage", "system_record", "auth_user", "auth_role"] 21 | 22 | 23 | # ----------------------------------------------- 24 | # db 配置项 25 | # 26 | # ORM 配置项 27 | # ORM_DB_ENABLE: 是否选择使用 ORM 数据库 28 | # ORM_DB_ECHO: 是否选择输出 ORM 操作日志到控制台 29 | # ORM_DATABASE_URL: ORM 数据库连接地址,默认使用 postgresql, 格式:"postgresql+asyncpg://账号:密码@地址:端口号/数据库名称" 30 | # 31 | # Redis 配置项 32 | # REDIS_DB_ENABLE: 是否选择使用 Redis 数据库 33 | # REDIS_DB_URL: Redis 数据库地址地址, 格式:"redis://:密码@地址:端口/数据库名称" 34 | # 35 | # MongoDB 配置项 36 | # MONGO_DB_ENABLE: 是否选择使用 MongoDB 数据库 37 | # MONGO_DB_URL: MongoDB 数据库连接地址, 格式:"mongodb://用户名:密码@地址:端口/?authSource=数据库名称" 38 | # ----------------------------------------------- 39 | # ORM 配置项 40 | ORM_DB_ENABLE = True 41 | ORM_DB_ECHO = True 42 | ORM_DATABASE_URL = "postgresql+asyncpg://user:123456@127.0.0.1:5432/kinit" 43 | 44 | # Redis 数据库配置 45 | REDIS_DB_ENABLE = False 46 | REDIS_DB_URL = "redis://:123456@127.0.0.1:6379/0" 47 | 48 | # MongoDB 数据库配置 49 | MONGO_DB_ENABLE = True 50 | MONGO_DB_URL = "mongodb://user:123456@127.0.0.1:27017/?authSource=kinit" 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Editor directories and files 2 | .idea/ 3 | .idea 4 | .vscode 5 | *.suo 6 | *.ntvs* 7 | *.njsproj 8 | *.sln 9 | kinit_fast_task/logs/* 10 | kinit_fast_task/temp/* 11 | kinit_fast_task/static/* 12 | !kinit_fast_task/static/redoc_ui 13 | !kinit_fast_task/static/swagger_ui 14 | !kinit_fast_task/static/system/favicon.ico 15 | !kinit_fast_task/static/system/logo.png 16 | alembic/versions/* 17 | !alembic/versions/.gitkeep 18 | 19 | # dotenv 20 | .env 21 | 22 | # virtualenv 23 | venv/ 24 | ENV/ 25 | 26 | # Spyder project settings 27 | .spyderproject 28 | 29 | # Rope project settings 30 | .ropeproject 31 | *.db 32 | .DS_Store 33 | __pycache__ 34 | !migrations/__init__.py 35 | *.pyc!/example/ 36 | !/example/ 37 | 38 | 39 | # ruff 40 | .ruff_cache 41 | 42 | 43 | example/* 44 | /backup/ 45 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | 4 | # 安装步骤:只有安装了才会启用,没有安装不会有效果 5 | # 1. poetry add pre-commit --dev 6 | # 2. pre-commit install 7 | 8 | # 不使用则可以卸载: 9 | # pre-commit uninstall 10 | 11 | # pre-commit 命令: 12 | # autoupdate:自动更新 pre-commit 配置到最新仓库版本。 13 | # clean:清理 pre-commit 文件。 14 | # gc:清理未使用的缓存仓库。 15 | # init-templatedir:在一个目录中安装 hook 脚本,该目录用于与 git config init.templateDir 结合使用。 16 | # install:安装 pre-commit 脚本。 17 | # install-hooks:为配置文件中的所有环境安装 hook 环境。您可能会发现 pre-commit install --install-hooks 更有用。 18 | # migrate-config:将列表配置迁移到新的映射配置。 19 | # run:运行 hooks。 20 | # sample-config:生成一个示例的 .pre-commit-config.yaml 文件。 21 | # try-repo:尝试一个仓库中的 hooks,对于开发新 hooks 很有用。 22 | # uninstall:卸载 pre-commit 脚本。 23 | # validate-config:验证 .pre-commit-config.yaml 文件。 24 | # validate-manifest:验证 .pre-commit-hooks.yaml 文件。 25 | # help:显示特定命令的帮助信息。 26 | default_language_version: 27 | python: python3.10 28 | repos: 29 | - repo: https://github.com/pre-commit/pre-commit-hooks # pre-commit 的官方钩子集合,包含了多个常用的钩子 30 | rev: v4.4.0 31 | hooks: 32 | - id: check-added-large-files # 检查是否添加了大文件 33 | - id: check-toml # 检查 TOML 文件的语法 34 | - id: check-yaml # 检查 YAML 文件的语法,并接受额外参数 --unsafe 35 | args: 36 | - --unsafe 37 | - id: end-of-file-fixer # 确保文件以一个空行结束 38 | - id: trailing-whitespace # 删除行尾的空白字符 39 | - repo: https://github.com/charliermarsh/ruff-pre-commit # ruff 格式化工具的钩子 40 | rev: v0.2.0 41 | # 在以下操作中,如果修改了文件,那么需要重新 add 文件才可 commit 42 | hooks: 43 | - id: ruff # 运行 ruff 进行代码检查,并接受 --fix 参数自动修复问题 44 | args: 45 | - --fix 46 | - id: ruff-format # 专门用于格式化代码 47 | -------------------------------------------------------------------------------- /.workflow/branch-pipeline.yml: -------------------------------------------------------------------------------- 1 | version: '1.0' 2 | name: branch-pipeline 3 | displayName: BranchPipeline 4 | stages: 5 | - stage: 6 | name: compile 7 | displayName: 编译 8 | steps: 9 | - step: build@python 10 | name: build_python 11 | displayName: Python 构建 12 | pythonVersion: '3.9' 13 | # 非必填字段,开启后表示将构建产物暂存,但不会上传到制品库中,7天后自动清除 14 | artifacts: 15 | # 构建产物名字,作为产物的唯一标识可向下传递,支持自定义,默认为BUILD_ARTIFACT。在下游可以通过${BUILD_ARTIFACT}方式引用来获取构建物地址 16 | - name: BUILD_ARTIFACT 17 | # 构建产物获取路径,是指代码编译完毕之后构建物的所在路径 18 | path: 19 | - ./ 20 | commands: 21 | - python3 -m pip install --upgrade pip 22 | - pip3 config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple 23 | - pip3 install -r requirements.txt 24 | - python3 ./main.py 25 | - step: publish@general_artifacts 26 | name: publish_general_artifacts 27 | displayName: 上传制品 28 | # 上游构建任务定义的产物名,默认BUILD_ARTIFACT 29 | dependArtifact: BUILD_ARTIFACT 30 | # 上传到制品库时的制品命名,默认output 31 | artifactName: output 32 | dependsOn: build_python 33 | - stage: 34 | name: release 35 | displayName: 发布 36 | steps: 37 | - step: publish@release_artifacts 38 | name: publish_release_artifacts 39 | displayName: '发布' 40 | # 上游上传制品任务的产出 41 | dependArtifact: output 42 | # 发布制品版本号 43 | version: '1.0.0.0' 44 | # 是否开启版本号自增,默认开启 45 | autoIncrement: true 46 | triggers: 47 | push: 48 | branches: 49 | exclude: 50 | - master 51 | include: 52 | - .* 53 | -------------------------------------------------------------------------------- /.workflow/master-pipeline.yml: -------------------------------------------------------------------------------- 1 | version: '1.0' 2 | name: master-pipeline 3 | displayName: MasterPipeline 4 | stages: 5 | - stage: 6 | name: compile 7 | displayName: 编译 8 | steps: 9 | - step: build@python 10 | name: build_python 11 | displayName: Python 构建 12 | pythonVersion: '3.9' 13 | # 非必填字段,开启后表示将构建产物暂存,但不会上传到制品库中,7天后自动清除 14 | artifacts: 15 | # 构建产物名字,作为产物的唯一标识可向下传递,支持自定义,默认为BUILD_ARTIFACT。在下游可以通过${BUILD_ARTIFACT}方式引用来获取构建物地址 16 | - name: BUILD_ARTIFACT 17 | # 构建产物获取路径,是指代码编译完毕之后构建物的所在路径 18 | path: 19 | - ./ 20 | commands: 21 | - python3 -m pip install --upgrade pip 22 | - pip3 config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple 23 | - pip3 install -r requirements.txt 24 | - python3 ./main.py 25 | - step: publish@general_artifacts 26 | name: publish_general_artifacts 27 | displayName: 上传制品 28 | # 上游构建任务定义的产物名,默认BUILD_ARTIFACT 29 | dependArtifact: BUILD_ARTIFACT 30 | # 上传到制品库时的制品命名,默认output 31 | artifactName: output 32 | dependsOn: build_python 33 | - stage: 34 | name: release 35 | displayName: 发布 36 | steps: 37 | - step: publish@release_artifacts 38 | name: publish_release_artifacts 39 | displayName: '发布' 40 | # 上游上传制品任务的产出 41 | dependArtifact: output 42 | # 发布制品版本号 43 | version: '1.0.0.0' 44 | # 是否开启版本号自增,默认开启 45 | autoIncrement: true 46 | triggers: 47 | push: 48 | branches: 49 | include: 50 | - master -------------------------------------------------------------------------------- /.workflow/pr-pipeline.yml: -------------------------------------------------------------------------------- 1 | version: '1.0' 2 | name: pr-pipeline 3 | displayName: PRPipeline 4 | stages: 5 | - stage: 6 | name: compile 7 | displayName: 编译 8 | steps: 9 | - step: build@python 10 | name: build_python 11 | displayName: Python 构建 12 | pythonVersion: '3.9' 13 | # 非必填字段,开启后表示将构建产物暂存,但不会上传到制品库中,7天后自动清除 14 | artifacts: 15 | # 构建产物名字,作为产物的唯一标识可向下传递,支持自定义,默认为BUILD_ARTIFACT。在下游可以通过${BUILD_ARTIFACT}方式引用来获取构建物地址 16 | - name: BUILD_ARTIFACT 17 | # 构建产物获取路径,是指代码编译完毕之后构建物的所在路径 18 | path: 19 | - ./ 20 | commands: 21 | - python3 -m pip install --upgrade pip 22 | - pip3 config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple 23 | - pip3 install -r requirements.txt 24 | - python3 ./main.py 25 | - step: publish@general_artifacts 26 | name: publish_general_artifacts 27 | displayName: 上传制品 28 | # 上游构建任务定义的产物名,默认BUILD_ARTIFACT 29 | dependArtifact: BUILD_ARTIFACT 30 | # 上传到制品库时的制品命名,默认output 31 | artifactName: output 32 | dependsOn: build_python 33 | triggers: 34 | pr: 35 | branches: 36 | include: 37 | - master 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 ktianc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # path to migration scripts 5 | script_location = ./alembic 6 | 7 | # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s 8 | # Uncomment the line below if you want the files to be prepended with date and time 9 | file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s 10 | 11 | # sys.path path, will be prepended to sys.path if present. 12 | # defaults to the current working directory. 13 | prepend_sys_path = . 14 | 15 | # timezone to use when rendering the date within the migration file 16 | # as well as the filename. 17 | # If specified, requires the python>=3.9 or backports.zoneinfo library. 18 | # Any required deps can installed by adding `alembic[tz]` to the pip requirements 19 | # string value is passed to ZoneInfo() 20 | # leave blank for localtime 21 | # timezone = 22 | 23 | # max length of characters to apply to the 24 | # "slug" field 25 | # truncate_slug_length = 40 26 | 27 | # set to 'true' to run the environment during 28 | # the 'revision' command, regardless of autogenerate 29 | # revision_environment = false 30 | 31 | # set to 'true' to allow .pyc and .pyo files without 32 | # a source .py file to be detected as revisions in the 33 | # versions/ directory 34 | # sourceless = false 35 | 36 | # version location specification; This defaults 37 | # to ./alembic/versions. When using multiple version 38 | # directories, initial revisions must be specified with --version-path. 39 | # The path separator used here should be the separator specified by "version_path_separator" below. 40 | # version_locations = %(here)s/bar:%(here)s/bat:./alembic/versions 41 | 42 | # version path separator; As mentioned above, this is the character used to split 43 | # version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. 44 | # If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. 45 | # Valid values for version_path_separator are: 46 | # 47 | # version_path_separator = : 48 | # version_path_separator = ; 49 | # version_path_separator = space 50 | version_path_separator = os # Use os.pathsep. Default configuration used for new projects. 51 | 52 | # set to 'true' to search source files recursively 53 | # in each "version_locations" directory 54 | # new in Alembic version 1.10 55 | # recursive_version_locations = false 56 | 57 | # the output encoding used when revision files 58 | # are written from script.py.mako 59 | # output_encoding = utf-8 60 | 61 | sqlalchemy.url = driver://user:pass@localhost/dbname 62 | 63 | 64 | [post_write_hooks] 65 | # post_write_hooks defines scripts or Python functions that are run 66 | # on newly generated revision scripts. See the documentation for further 67 | # detail and examples 68 | 69 | # format using "black" - use the console_scripts runner, against the "black" entrypoint 70 | # hooks = black 71 | # black.type = console_scripts 72 | # black.entrypoint = black 73 | # black.options = -l 79 REVISION_SCRIPT_FILENAME 74 | 75 | # lint with attempts to fix using "ruff" - use the exec runner, execute a binary 76 | # hooks = ruff 77 | # ruff.type = exec 78 | # ruff.executable = %(here)s/.venv/bin/ruff 79 | # ruff.options = --fix REVISION_SCRIPT_FILENAME 80 | 81 | # Logging configuration 82 | [loggers] 83 | keys = root,sqlalchemy,alembic 84 | 85 | [handlers] 86 | keys = console 87 | 88 | [formatters] 89 | keys = generic 90 | 91 | [logger_root] 92 | level = WARN 93 | handlers = console 94 | qualname = 95 | 96 | [logger_sqlalchemy] 97 | level = WARN 98 | handlers = 99 | qualname = sqlalchemy.engine 100 | 101 | [logger_alembic] 102 | level = INFO 103 | handlers = 104 | qualname = alembic 105 | 106 | [handler_console] 107 | class = StreamHandler 108 | args = (sys.stderr,) 109 | level = NOTSET 110 | formatter = generic 111 | 112 | [formatter_generic] 113 | format = %(levelname)-5.5s [%(name)s] %(message)s 114 | datefmt = %H:%M:%S 115 | -------------------------------------------------------------------------------- /alembic/README: -------------------------------------------------------------------------------- 1 | ## 介绍 2 | 3 | 官方文档:https://alembic.sqlalchemy.org/en/latest/tutorial.html#creating-an-environment 4 | 5 | ## 创建环境命令 6 | 7 | ```shell 8 | alembic init --template async ./alembic 9 | ``` 10 | 11 | ## 数据库迁移 12 | 13 | ```shell 14 | alembic revision --autogenerate 15 | alembic upgrade head 16 | 17 | # 或 18 | 19 | python main.py migrate 20 | ``` 21 | 22 | -------------------------------------------------------------------------------- /alembic/env.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from logging.config import fileConfig 3 | 4 | from sqlalchemy import pool 5 | from sqlalchemy.engine import Connection 6 | from sqlalchemy.ext.asyncio import async_engine_from_config 7 | 8 | from alembic import context 9 | 10 | import sys 11 | from kinit_fast_task.db.orm.async_base_model import AsyncBaseORMModel 12 | from kinit_fast_task.config import settings 13 | 14 | # this is the Alembic Config object, which provides 15 | # access to the values within the .ini file in use. 16 | config = context.config 17 | 18 | # Interpret the config file for Python logging. 19 | # This line sets up loggers basically. 20 | if config.config_file_name is not None: 21 | fileConfig(config.config_file_name) 22 | 23 | # add your model's MetaData object here 24 | # for 'autogenerate' support 25 | # from myapp import mymodel 26 | # target_metadata = mymodel.Base.metadata 27 | # target_metadata = None 28 | 29 | # other values from the config, defined by the needs of env.py, 30 | # can be acquired: 31 | # my_important_option = config.get_main_option("my_important_option") 32 | # ... etc. 33 | 34 | # 添加当前项目路径到环境变量 35 | sys.path.append(str(settings.BASE_PATH)) 36 | 37 | # 导入项目中的基本映射类,与 需要迁移的 ORM 模型 38 | from kinit_fast_task.app.models import * 39 | 40 | # 修改配置中的参数 41 | target_metadata = AsyncBaseORMModel.metadata 42 | 43 | config.set_main_option('sqlalchemy.url', settings.db.ORM_DATABASE_URL.unicode_string()) 44 | 45 | 46 | def run_migrations_offline() -> None: 47 | """Run migrations in 'offline' mode. 48 | 49 | This configures the context with just a URL 50 | and not an Engine, though an Engine is acceptable 51 | here as well. By skipping the Engine creation 52 | we don't even need a DBAPI to be available. 53 | 54 | Calls to context.execute() here emit the given string to the 55 | script output. 56 | 57 | """ 58 | url = config.get_main_option("sqlalchemy.url") 59 | context.configure( 60 | url=url, 61 | target_metadata=target_metadata, 62 | literal_binds=True, 63 | dialect_opts={"paramstyle": "named"}, 64 | ) 65 | 66 | with context.begin_transaction(): 67 | context.run_migrations() 68 | 69 | 70 | def do_run_migrations(connection: Connection) -> None: 71 | context.configure(connection=connection, target_metadata=target_metadata) 72 | 73 | with context.begin_transaction(): 74 | context.run_migrations() 75 | 76 | 77 | async def run_async_migrations() -> None: 78 | """In this scenario we need to create an Engine 79 | and associate a connection with the context. 80 | 81 | """ 82 | 83 | connectable = async_engine_from_config( 84 | config.get_section(config.config_ini_section, {}), 85 | prefix="sqlalchemy.", 86 | poolclass=pool.NullPool, 87 | ) 88 | 89 | async with connectable.connect() as connection: 90 | await connection.run_sync(do_run_migrations) 91 | 92 | await connectable.dispose() 93 | 94 | 95 | def run_migrations_online() -> None: 96 | """Run migrations in 'online' mode.""" 97 | 98 | asyncio.run(run_async_migrations()) 99 | 100 | 101 | if context.is_offline_mode(): 102 | run_migrations_offline() 103 | else: 104 | run_migrations_online() 105 | -------------------------------------------------------------------------------- /alembic/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from typing import Sequence, Union 9 | 10 | from alembic import op 11 | import sqlalchemy as sa 12 | ${imports if imports else ""} 13 | 14 | # revision identifiers, used by Alembic. 15 | revision: str = ${repr(up_revision)} 16 | down_revision: Union[str, None] = ${repr(down_revision)} 17 | branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} 18 | depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} 19 | 20 | 21 | def upgrade() -> None: 22 | ${upgrades if upgrades else "pass"} 23 | 24 | 25 | def downgrade() -> None: 26 | ${downgrades if downgrades else "pass"} 27 | -------------------------------------------------------------------------------- /alembic/versions/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvandk/kinit-fast-task/1c01a44517344f85eb7a0edad122958eca0dda81/alembic/versions/.gitkeep -------------------------------------------------------------------------------- /docs/PEP.md: -------------------------------------------------------------------------------- 1 | # PEP 2 | 3 | PEP 是 Python Enhancement Proposal(Python 增强提案)的缩写。它是一种为 Python 社区提供新功能或改进现有功能建议的正式文档。PEP 的目的是为 Python 的开发和进化提供信息给广大的 Python 社区,或定义一个新的特性以及它的过程或环境。每个 PEP 都包含了一个提案的技术规格和理由,以及讨论该提案所必须的背景信息。 4 | 5 | PEP 分为三种类型: 6 | 7 | 1. **标准跟踪 PEP**:描述了对 Python 语言的新功能或实现的更改,需要广泛的社区共识。 8 | 2. **信息性 PEP**:提供一般的指导或信息给 Python 社区,但不包含新功能。 9 | 3. **流程 PEP**:描述了 Python 社区的流程或改进,包括决策过程以及环境或程序的变更。 10 | 11 | 每个 PEP 都有一个唯一的编号,通常以 "PEP" 后跟一个编号来表示(例如,PEP 8)。PEP 通过 Python 社区的讨论和反馈逐步完善,最终可能会被接受并实现在 Python 语言中,或者被拒绝或撤回。 12 | 13 | PEP 的流程通常包括草案的撰写、提交、讨论、可能的修改、以及最终的接受或拒绝。这个流程确保了 Python 社区的每个成员都有机会对 Python 的发展提供意见和反馈。 14 | 15 | 16 | 17 | ## PEP 585 18 | 19 | Type Hinting Generics In Standard Collections 20 | 21 | **PEP 585** 提出了一种改进,允许在标准集合和其他部分的 Python 类型提示中直接使用泛型类型,而不需要从 `typing` 模块导入特殊的类。这个改动使得类型提示更加自然和简洁。 22 | 23 | **主要特点** 24 | 25 | - **内置集合作为泛型**:在 Python 3.9 之前,要对内置集合(如 `list`、`dict`、`set` 等)使用泛型,需要从 `typing` 模块导入对应的泛型版本,例如 `List[T]`、`Dict[K, V]`。PEP 585 允许直接在这些内置集合上使用泛型注解,比如 `list[int]`、`dict[str, float]`。 26 | - **减少 `from typing import ...` 的需要**:这个改动减少了在使用类型提示时需要从 `typing` 模块导入的类型数量。 27 | - **类型提示和运行时行为的统一**:通过这种方式,类型提示更接近 Python 代码的实际运行时行为。 28 | 29 | **示例** 30 | 31 | 在 Python 3.9 以及更早的版本中,你可能需要这样写代码: 32 | 33 | ``` 34 | pythonCopy codefrom typing import List, Dict 35 | 36 | def process(items: List[int]) -> Dict[str, int]: 37 | ... 38 | ``` 39 | 40 | 在 Python 3.10 及以后的版本中,你可以这样写: 41 | 42 | ``` 43 | pythonCopy codedef process(items: list[int]) -> dict[str, int]: 44 | ... 45 | ``` 46 | 47 | ## PEP 604 48 | 49 | Allow writing union types as X | Y 50 | 51 | **PEP 604** 引入了一个新的语法,允许用 `X | Y` 来表示联合类型,这是对先前需要使用 `typing.Union[X, Y]` 的简化。 52 | 53 | **主要特点** 54 | 55 | - **简化联合类型的表示**:使用 `|` 操作符可以更简单直观地表示一个值可以是多个类型之一。 56 | - **减少 `from typing import Union` 的需要**:与 PEP 585 类似,这个改动也减少了对 `typing` 模块的依赖。 57 | - **与类型提示的其他部分保持一致性**:这种表示方法与其他类型提示特性(如泛型)更为一致。 58 | 59 | **示例** 60 | 61 | 在 Python 3.9 以及更早的版本中,表示一个变量可以是 `int` 或 `str` 类型,你需要这样写: 62 | 63 | ``` 64 | pythonCopy codefrom typing import Union 65 | 66 | def foo(bar: Union[int, str]): 67 | ... 68 | ``` 69 | 70 | 在 Python 3.10 及以后的版本中,你可以简单地写成: 71 | 72 | ``` 73 | pythonCopy codedef foo(bar: int | str): 74 | ... 75 | ``` 76 | 77 | 这两个 PEP 的实施极大地简化了 Python 的类型提示系统,使其更加易于使用和理解。 -------------------------------------------------------------------------------- /docs/backgroup_tasks.md: -------------------------------------------------------------------------------- 1 | 如果你想在 FastAPI 中使用 `BackgroundTasks` 而不通过依赖注入的方式,你可以直接在函数中创建 `BackgroundTasks` 实例并在返回的响应中明确地将其作为参数传递。这允许你在响应对象中手动管理后台任务,而不是依赖 FastAPI 的自动注入机制。 2 | 3 | 下面是如何在 FastAPI 路由函数中手动创建 `BackgroundTasks` 实例并确保任务执行的示例: 4 | 5 | ### 示例代码 6 | 7 | ```python 8 | from fastapi import FastAPI, BackgroundTasks, Response 9 | 10 | app = FastAPI() 11 | 12 | def write_log(): 13 | print("Log message: Background task is running") 14 | 15 | @app.post("/tasks/") 16 | async def run_background_task(): 17 | tasks = BackgroundTasks() 18 | tasks.add_task(write_log) 19 | # 返回响应时,手动传递 BackgroundTasks 实例 20 | return Response(content="Background task has been added", background=tasks) 21 | ``` 22 | 23 | ### 代码解释 24 | 25 | 1. **创建 `BackgroundTasks` 实例**:在函数 `run_background_task` 中,我们创建了一个 `BackgroundTasks` 对象的实例 `tasks`。 26 | 27 | 2. **添加任务**:通过调用 `tasks.add_task(write_log)`,我们把 `write_log` 函数作为后台任务添加到了 `tasks` 实例中。 28 | 29 | 3. **手动传递 BackgroundTasks 实例**:在返回响应时,我们使用 `Response` 对象,并通过 `background` 参数将 `BackgroundTasks` 实例传递进去。这样 FastAPI 就会在完成 HTTP 响应后执行指定的后台任务。 30 | 31 | ### 注意 32 | 33 | 这种方法不使用依赖注入,而是直接操作 `BackgroundTasks` 对象,并确保它与响应对象关联,从而让 FastAPI 能够正确地处理后台任务。这样做提供了对后台任务管理的完全控制,同时仍然利用 FastAPI 的内置支持来确保任务在适当的时机执行。 34 | 35 | 使用这种方法时,你需要确保正确地将 `BackgroundTasks` 实例与响应对象关联,否则后台任务不会执行。通过 `Response` 对象的 `background` 参数传递后台任务是一种确保 FastAPI 正确管理并执行这些任务的有效方式。 -------------------------------------------------------------------------------- /docs/使用 TypeVar 和直接使用实体类型的区别.md: -------------------------------------------------------------------------------- 1 | ## 使用 TypeVar 和实体类型的区别 2 | 3 | 使用 `TypeVar` 创建的类型变量,如 `SchemaUserModel = TypeVar("SchemaUserModel", bound=UserModel)`,和直接使用 `UserModel` 类型标注确实在许多情况下有类似的效果,但它们在使用上和意图上存在一些细微的差异。下面我将解释两者之间的区别和适用场景。 4 | 5 | ### 直接使用 `UserModel` 6 | 7 | 在你的示例中,当函数 `user_put` 参数的类型直接指定为 `UserModel` 时,这表明该函数接受 `UserModel` 或其任何子类的实例。这是多态的一种直接体现,允许任何继承自 `UserModel` 的类的实例作为参数。这种方式简单明了,对于大多数需要多态性的情况已经足够。 8 | 9 | ### 使用 `TypeVar` 绑定到 `UserModel` 10 | 11 | 当你使用 `TypeVar` 并将其绑定到 `UserModel`: 12 | 13 | ```python 14 | SchemaUserModel = TypeVar("SchemaUserModel", bound=UserModel) 15 | ``` 16 | 17 | 这种方式定义了一个类型变量,这个变量被限制为 `UserModel` 或其子类的实例。在表面上,这似乎与直接使用 `UserModel` 相同。然而,使用 `TypeVar` 的主要优势在于它提供了更高的灵活性和表达能力,尤其是在定义泛型类或函数时更为重要。例如: 18 | 19 | - **泛型函数或类**:如果你正在定义一个泛型函数或泛型类,其中涉及多个参数或返回类型与该类型变量相关,使用 `TypeVar` 可以确保这些相关类型之间保持一致。例如,你可能想要保证某个函数的输入和输出类型相同,或者确保两个不同参数是相同的类型。 20 | - **更复杂的类型关系**:`TypeVar` 允许定义涉及多个约束的复杂类型关系,这在直接使用基类作为类型时无法实现。 21 | 22 | ### 示例 23 | 24 | 考虑下面的泛型函数示例,它使用了 `TypeVar`: 25 | 26 | ```python 27 | codeT = TypeVar('T', bound=UserModel) 28 | 29 | def process_user(user: T) -> T: 30 | print(user.name) 31 | return user 32 | ``` 33 | 34 | 在这个例子中,`process_user` 保证输入类型和返回类型是相同的。这意味着如果你传入一个 `SonUserModel` 实例,返回的也会是 `SonUserModel` 类型的实例。这种类型保持是直接使用 `UserModel` 无法确保的。 35 | 36 | ### 结论 37 | 38 | 尽管在很多简单场景中直接使用 `UserModel` 和使用绑定到 `UserModel` 的 `TypeVar` 效果相似,但 `TypeVar` 提供了更高级的类型操作,特别是在需要维护类型一致性的复杂泛型编程中。直接使用 `UserModel` 更简单直接,适合大多数情况。选择使用哪种方式取决于你的具体需求和预期的代码复用级别。 39 | 40 | 41 | 42 | ## 真实案例 43 | 44 | ```python 45 | from typing import TypeVar 46 | 47 | 48 | class UserModel: 49 | 50 | def __init__(self, name: str, age: int): 51 | self.name = name 52 | self.age = age 53 | 54 | 55 | class SonUserModel(UserModel): 56 | 57 | def __init__(self, name: str, age: int, gender: str = 'male'): 58 | super().__init__(name, age) 59 | self.gender = gender 60 | 61 | 62 | def user_1(data: UserModel) -> UserModel: 63 | print(data.name) 64 | print(data.age) 65 | return data 66 | 67 | 68 | # 定义类型变量,这个变量被限制为 UserModel 或其子类的实例 69 | UserModelSchema = TypeVar("UserModelSchema", bound=UserModel) 70 | 71 | 72 | def user_2(data: UserModelSchema) -> UserModelSchema: 73 | print(data.name) 74 | print(data.age) 75 | return data 76 | 77 | 78 | if __name__ == '__main__': 79 | _user_1 = SonUserModel(name="John", age=18) 80 | user_1(_user_1) 81 | 82 | _user_2 = SonUserModel(name="John", age=18, gender='male') 83 | user_2(_user_2) 84 | ``` 85 | 86 | 87 | 88 | ### 实体类型 89 | 90 | 在以上代码示例中,`user_1` 函数的参数 `data` 类型被指定为 `UserModel` 类。 91 | 92 | **在Python中,这意味着 `user_1` 函数可以接受任何类型为 `UserModel` 或其任何子类的实例。** 93 | 94 | 这是因为Python的类型系统支持多态,其中子类的实例可以被视为其父类的实例。 95 | 96 | **这是面向对象编程中的一个基本原则,称为“里氏替换原则”(Liskov Substitution Principle),即子类对象应该能够替换其父类对象被使用,而不影响程序的正确性。** 97 | 98 | 返回类型同样为为 `UserModel` 或其任何子类的实例。 99 | 100 | **可接受的类型** 101 | 102 | 由于 `SonUserModel` 是 `UserModel` 的子类,因此以下类型的实例都可以作为参数传递给 `user_put` 函数: 103 | 104 | - `UserModel` 的实例。 105 | - `SonUserModel` 的实例,或任何其他从 `UserModel` 继承的子类的实例。 106 | 107 | 这表明你可以用 `UserModel` 以及任何继承自 `UserModel` 的类的实例来调用 `user_1` 函数。由于 `SonUserModel` 扩展了 `UserModel`(增加了 `gender` 属性并保留了从 `UserModel` 继承的属性和方法),`SonUserModel` 的实例在被用作 `UserModel` 的实例时,仍然保持了接口的一致性,即它至少具有 `UserModel` 的所有属性和方法。 108 | 109 | **适用场景**: 110 | 111 | - 当函数或方法预期只与一个具体类及其子类交互时。 112 | - 代码中没有需要泛型或多个类型保持一致性的需求。 113 | 114 | **优点**: 115 | 116 | - 简单直接,易于理解。 117 | - 代码更加直观,易于维护和阅读。 118 | 119 | ### 使用 `TypeVar` 绑定到实体类型 120 | 121 | 在以上代码示例中,在入参情况下 `TypeVar` 与直接使用实体类型是一致的,但是在返回时, `TypeVar` 可以确保当你传入的是什么类型,就会标记返回什么类型,比如你传入的是 `UserModel` 的子类 `SonUserModel` ,则返回的也是 `SonUserModel` 的实例 122 | 123 | **适用场景**: 124 | 125 | - 在实现泛型编程时,尤其是当需要确保多个参数或返回值之间的类型一致性时。 126 | - 当需要将类型参数限定于某个特定的基类或接口时,但又希望保持一定的类型灵活性。 127 | 128 | **优点**: 129 | 130 | - 提高了代码的灵活性和可重用性。 131 | - 可以在多个函数或类之间保持类型一致性,提高代码的安全性。 132 | 133 | -------------------------------------------------------------------------------- /docs/单节点 MongoDB 数据库开启副本集.md: -------------------------------------------------------------------------------- 1 | # 单节点 MongoDB 数据库开启副本集 2 | 3 | 使用Motor操作MongoDB事务时需要MongoDB启用副本集 4 | 5 | MongoDB的事务功能(尤其是多文档事务)依赖于副本集来保证数据的一致性和原子性 6 | 7 | 因此,即使是单节点MongoDB实例,要使用事务也需要配置为副本集 8 | 9 | ## 使用 Docker 部署 10 | 11 | 工作目录:/opt/mongo 12 | 13 | 数据目录:/opt/mongo/db 14 | 15 | ``` 16 | mkdir -p /opt/mongo/db 17 | 18 | chmod -R 755 /opt/mongo/db 19 | ``` 20 | 21 | ### 步骤1:新建配置文件 22 | 23 | ```shell 24 | vim /opt/mongo/mongod.conf 25 | ``` 26 | 27 | 配置文件内容: 28 | 29 | ```yaml 30 | # 存储相关设置 31 | storage: 32 | dbPath: /data/db # 数据存储路径 33 | 34 | # 网络相关设置 35 | net: 36 | bindIp: 0.0.0.0 # 绑定 IP 地址,0.0.0.0 表示所有 IP 37 | port: 27017 # MongoDB 监听端口 38 | 39 | # 副本集相关设置 40 | replication: 41 | replSetName: rs0 # 副本集名称 42 | 43 | # 安全相关设置 44 | security: 45 | authorization: enabled # 启用授权,设置用户访问控制 46 | keyFile: /data/conf/keyfileone # 密钥文件路径 47 | ``` 48 | 49 | ### 步骤2:创建密钥文件 50 | 51 | ```shell 52 | # 使用 OpenSSL 或其他工具生成一个 base64 编码的随机字符串作为密钥 53 | openssl rand -base64 756 > keyfileone 54 | 55 | # 确保密钥文件的权限设置为 400,这意味着只有文件所有者可以读取它 56 | chmod 400 keyfileone 57 | 58 | # 确保文件所有者是 MongoDB 用户(通常是 UID 999) 59 | # 在 Docker 容器中,MongoDB 通常以 UID 999 运行 60 | # 如果不确定,也可以先启动一个没有配置的容器查看一下: 61 | # docker run --rm -it mongo:latest --name mongo 62 | # docker exec -it mongo id mongodb 63 | chown 999:999 /opt/mongo/keyfileone 64 | ``` 65 | 66 | ### 步骤3:启动容器 67 | 68 | ``` 69 | # 用于测试 --rm 停止后会自动删除容器 70 | docker run --rm -it \ 71 | --name kinit-mongo \ 72 | -v /opt/mongo:/data/conf \ 73 | -v /opt/mongo/db:/data/db \ 74 | -p 27017:27017 \ 75 | -e MONGO_INITDB_ROOT_USERNAME=admin \ 76 | -e MONGO_INITDB_ROOT_PASSWORD=123456 \ 77 | mongo:latest \ 78 | mongod --config /data/conf/mongod.conf 79 | ``` 80 | 81 | ### 步骤4:初始化副本集 82 | 83 | ```shell 84 | # 进入容器 mongosh 命令行 85 | docker exec -it kinit-mongo mongosh -u admin -p 123456 86 | 87 | # 初始化副本集 - 使用默认配置 88 | rs.initiate() 89 | 90 | # 返回: 91 | # test> rs.initiate() 92 | # { 93 | # info2: 'no configuration specified. Using a default configuration for the set', 94 | # me: '92cb97763019:27017', 95 | # ok: 1 96 | # } 97 | ``` 98 | 99 | 现在,你的单节点MongoDB实例已经配置为副本集并支持事务操作了。 100 | 101 | ## 使用事务的示例代码 102 | 103 | 在配置好副本集之后,你可以使用事务操作,例如: 104 | 105 | ```python 106 | import motor.motor_asyncio 107 | import asyncio 108 | from pymongo.errors import PyMongoError 109 | 110 | # 连接MongoDB 111 | client = motor.motor_asyncio.AsyncIOMotorClient('mongodb://localhost:27017') 112 | db = client['your_database'] 113 | 114 | async def insert_with_transaction(): 115 | async with await client.start_session() as session: 116 | async with session.start_transaction(): 117 | try: 118 | collection = db['your_collection'] 119 | 120 | # 插入文档 121 | await collection.insert_one({"name": "John", "age": 30}, session=session) 122 | print("Inserted document.") 123 | 124 | # 插入另一个文档(可选) 125 | await collection.insert_one({"name": "Jane", "age": 25}, session=session) 126 | print("Inserted another document.") 127 | 128 | # 提交事务 129 | await session.commit_transaction() 130 | print("Transaction committed.") 131 | except PyMongoError as e: 132 | print(f"Transaction aborted due to an error: {e}") 133 | await session.abort_transaction() 134 | 135 | # 运行异步函数 136 | asyncio.run(insert_with_transaction()) 137 | ``` 138 | 139 | -------------------------------------------------------------------------------- /images/image-20240518230154682.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvandk/kinit-fast-task/1c01a44517344f85eb7a0edad122958eca0dda81/images/image-20240518230154682.png -------------------------------------------------------------------------------- /kinit_fast_task/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvandk/kinit-fast-task/1c01a44517344f85eb7a0edad122958eca0dda81/kinit_fast_task/__init__.py -------------------------------------------------------------------------------- /kinit_fast_task/app/__init__.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2022/2/24 10:19 3 | # @File : __init__.py 4 | # @IDE : PyCharm 5 | # @Desc : 简要说明 6 | -------------------------------------------------------------------------------- /kinit_fast_task/app/cruds/__init__.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/3/23 22:52 3 | # @File : __init__.py 4 | # @IDE : PyCharm 5 | # @Desc : 描述信息 6 | -------------------------------------------------------------------------------- /kinit_fast_task/app/cruds/auth_role_crud.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/05/17 18:14 3 | # @File : auth_role.py 4 | # @IDE : PyCharm 5 | # @Desc : 数据操作 6 | 7 | from sqlalchemy.ext.asyncio import AsyncSession 8 | from kinit_fast_task.app.cruds.base import ORMCrud 9 | from kinit_fast_task.app.cruds.base.orm import ORMModel 10 | from kinit_fast_task.app.schemas import auth_role_schema as role_s 11 | from kinit_fast_task.app.models.auth_role_model import AuthRoleModel 12 | from kinit_fast_task.core.exception import CustomException 13 | 14 | 15 | class AuthRoleCRUD(ORMCrud[AuthRoleModel]): 16 | def __init__(self, session: AsyncSession): 17 | super().__init__() 18 | self.session = session 19 | self.model = AuthRoleModel 20 | self.simple_out_schema = role_s.AuthRoleSimpleOutSchema 21 | 22 | async def create_data(self, data: role_s.AuthRoleCreateSchema, *, v_return_obj: bool = False) -> ORMModel | str: 23 | """ 24 | 重写创建角色方法 25 | """ 26 | role_key = await self.get_data(role_key=data.role_key, v_return_none=True) 27 | if role_key: 28 | raise CustomException("角色唯一标识已存在!") 29 | return await super().create_data(data, v_return_obj=v_return_obj) 30 | 31 | async def create_datas( 32 | self, datas: list[role_s.AuthRoleCreateSchema], v_return_objs: bool = False 33 | ) -> list[ORMModel] | str: 34 | """ 35 | 重写批量创建角色方法 36 | """ 37 | key_list = [r.role_key for r in datas] 38 | if len(key_list) != len(set(key_list)): 39 | raise CustomException("数据中存在重复的角色唯一标识, 请检查数据!") 40 | count = await self.get_count(role_key=("in", key_list)) 41 | if count > 0: 42 | raise CustomException("角色唯一标识已存在, 请检查数据!") 43 | return await super().create_datas(datas, v_return_objs=v_return_objs) 44 | 45 | async def update_data( 46 | self, data_id: int, data: role_s.AuthRoleBaseSchema | dict, *, v_return_obj: bool = False 47 | ) -> ORMModel | str: 48 | """ 49 | 重写更新角色方法 50 | """ 51 | role_obj = await self.get_data(role_key=data.role_key, v_return_none=True) 52 | if role_obj and role_obj.id != data_id: 53 | raise CustomException("角色唯一标识已存在!") 54 | return await super().update_data(data_id, data, v_return_obj=v_return_obj) 55 | -------------------------------------------------------------------------------- /kinit_fast_task/app/cruds/auth_user_crud.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/05/17 14:37 3 | # @File : auth_user.py 4 | # @IDE : PyCharm 5 | # @Desc : 数据操作 6 | from fastapi.encoders import jsonable_encoder 7 | from pydantic import BaseModel as AbstractSchemaModel 8 | from sqlalchemy.ext.asyncio import AsyncSession 9 | from sqlalchemy.orm import selectinload 10 | 11 | from kinit_fast_task.app.cruds.base.orm import ORMCrud, ORMModel 12 | from kinit_fast_task.app.schemas import auth_user_schema as user_s 13 | from kinit_fast_task.app.models.auth_user_model import AuthUserModel 14 | from kinit_fast_task.app.cruds.auth_role_crud import AuthRoleCRUD 15 | from kinit_fast_task.core.exception import CustomException 16 | 17 | 18 | class AuthUserCRUD(ORMCrud[AuthUserModel]): 19 | def __init__(self, session: AsyncSession): 20 | super().__init__() 21 | self.session = session 22 | self.model = AuthUserModel 23 | self.simple_out_schema = user_s.AuthUserSimpleOutSchema 24 | 25 | async def create_data( 26 | self, data: user_s.AuthUserCreateSchema, *, v_return_obj: bool = False 27 | ) -> AuthUserModel | str: 28 | """ 29 | 重写创建用户, 增加创建关联角色数据 30 | 31 | :param data: 创建数据 32 | :param v_return_obj: 是否返回 ORM 对象 33 | :return: 34 | """ # noqa E501 35 | 36 | # 验证数据是否合格 37 | telephone = await self.get_data(telephone=data.telephone, v_return_none=True) 38 | if telephone: 39 | raise CustomException("手机号已注册!") 40 | if data.email: 41 | email = await self.get_data(email=data.email, v_return_none=True) 42 | if email: 43 | raise CustomException("邮箱已注册!") 44 | 45 | # 开始创建操作 46 | data = jsonable_encoder(data) 47 | role_ids = data.pop("role_ids") 48 | obj = self.model(**data) 49 | if role_ids: 50 | roles = await AuthRoleCRUD(self.session).get_datas(limit=0, id=("in", role_ids), v_return_type="model") 51 | if len(role_ids) != len(roles): 52 | raise CustomException("关联角色异常, 请确保关联的角色存在!") 53 | for role in roles: 54 | obj.roles.add(role) 55 | await self.flush(obj) 56 | if v_return_obj: 57 | return obj 58 | return "创建成功" 59 | 60 | async def create_datas( 61 | self, datas: list[dict | AbstractSchemaModel], v_return_objs: bool = False 62 | ) -> list[ORMModel] | str: 63 | """ 64 | 重写批量创建用户方法 65 | """ 66 | raise NotImplementedError("未实现批量创建用户方法!") 67 | 68 | async def update_data( 69 | self, data_id: int, data: user_s.AuthUserUpdateSchema | dict, *, v_return_obj: bool = False 70 | ) -> AuthUserModel | str: 71 | """ 72 | 根据 id 更新用户信息 73 | 74 | :param data_id: 修改行数据的 ID 75 | :param data: 更新的数据内容 76 | :param v_return_obj: 是否返回对象 77 | """ 78 | v_options = [selectinload(AuthUserModel.roles)] 79 | obj: AuthUserModel = await self.get_data(data_id, v_options=v_options) 80 | 81 | # 验证数据是否合格 82 | if obj.telephone != data.telephone: 83 | telephone = await self.get_data(telephone=data.telephone, v_return_none=True) 84 | if telephone: 85 | raise CustomException("手机号已注册!") 86 | if obj.email != data.email and data.email is not None: 87 | email = await self.get_data(email=data.email, v_return_none=True) 88 | if email: 89 | raise CustomException("邮箱已注册!") 90 | 91 | # 开始更新操作 92 | data_dict = jsonable_encoder(data) 93 | for key, value in data_dict.items(): 94 | if key == "role_ids": 95 | if value: 96 | roles = await AuthRoleCRUD(self.session).get_datas(limit=0, id=("in", value), v_return_type="model") 97 | if len(value) != len(roles): 98 | raise CustomException("关联角色异常, 请确保关联的角色存在!") 99 | if obj.roles: 100 | obj.roles.clear() 101 | for role in roles: 102 | obj.roles.add(role) 103 | continue 104 | setattr(obj, key, value) 105 | await self.flush() 106 | if v_return_obj: 107 | return obj 108 | return "更新成功" 109 | -------------------------------------------------------------------------------- /kinit_fast_task/app/cruds/base/__init__.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/3/28 14:25 3 | # @File : __init__.py 4 | # @IDE : PyCharm 5 | # @Desc : 描述信息 6 | 7 | from kinit_fast_task.app.cruds.base.orm import ORMCrud 8 | from kinit_fast_task.app.cruds.base.mongo import MongoCrud 9 | -------------------------------------------------------------------------------- /kinit_fast_task/app/cruds/record_operation_crud.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2021/10/18 22:18 3 | # @File : crud.py 4 | # @IDE : PyCharm 5 | # @Desc : 数据库 增删改查操作 6 | 7 | from kinit_fast_task.app.cruds.base.mongo import MongoCrud 8 | from kinit_fast_task.app.schemas import record_operation_schema 9 | from motor.motor_asyncio import AsyncIOMotorClientSession 10 | 11 | 12 | class OperationCURD(MongoCrud): 13 | def __init__(self, session: AsyncIOMotorClientSession | None = None): 14 | super().__init__() 15 | self.session = session 16 | self.collection = self.db["record_operation"] 17 | self.simple_out_schema = record_operation_schema.OperationSimpleOutSchema 18 | self.is_object_id = True 19 | -------------------------------------------------------------------------------- /kinit_fast_task/app/depends/Paging.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/3/24 12:07 3 | # @File : Paging.py 4 | # @IDE : PyCharm 5 | # @Desc : 描述信息 6 | 7 | """ 8 | 类依赖项-官方文档:https://fastapi.tiangolo.com/zh/tutorial/dependencies/classes-as-dependencies/ 9 | """ 10 | 11 | import copy 12 | 13 | from fastapi import Query 14 | 15 | 16 | class QueryParams: 17 | def __init__(self, params=None): 18 | if params: 19 | self.page = params.page 20 | self.limit = params.limit 21 | self.v_order = params.v_order 22 | self.v_order_field = params.v_order_field 23 | 24 | def dict(self, exclude: list[str] = None) -> dict: 25 | """ 26 | 属性转字典 27 | :param exclude: 需排除的属性 28 | :return: 29 | """ 30 | result = copy.deepcopy(self.__dict__) 31 | if exclude: 32 | for item in exclude: 33 | try: 34 | del result[item] 35 | except KeyError: 36 | pass 37 | return result 38 | 39 | def to_count(self, exclude: list[str] = None) -> dict: 40 | """ 41 | 去除基础查询参数 42 | :param exclude: 另外需要去除的属性 43 | :return: 44 | """ 45 | params = self.dict(exclude=exclude) 46 | del params["page"] 47 | del params["limit"] 48 | del params["v_order"] 49 | del params["v_order_field"] 50 | return params 51 | 52 | 53 | class Paging(QueryParams): 54 | """ 55 | 列表分页 56 | """ 57 | 58 | def __init__( 59 | self, 60 | page: int = Query(1, description="当前页数"), 61 | limit: int = Query(10, description="每页多少条数据"), 62 | v_order_field: str = Query(None, description="排序字段"), 63 | v_order: str = Query(None, description="排序规则"), 64 | ): 65 | super().__init__() 66 | self.page = page 67 | self.limit = limit 68 | self.v_order = v_order 69 | self.v_order_field = v_order_field 70 | -------------------------------------------------------------------------------- /kinit_fast_task/app/depends/__init__.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/3/24 11:57 3 | # @File : __init__.py 4 | # @IDE : PyCharm 5 | # @Desc : 描述信息 6 | -------------------------------------------------------------------------------- /kinit_fast_task/app/models/__init__.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/4/7 10:11 3 | # @File : __init__.py 4 | # @IDE : PyCharm 5 | # @Desc : 自动遍历导入所有 models 6 | from pathlib import Path 7 | import importlib 8 | from kinit_fast_task.utils import log 9 | import ast 10 | 11 | 12 | def find_classes_ending_with_model(file_path: Path) -> list[str]: 13 | """ 14 | 读取文件,查找出以Model结尾的class名称 15 | """ 16 | try: 17 | tree = ast.parse(file_path.read_bytes(), filename=str(file_path)) 18 | except (SyntaxError, UnicodeDecodeError) as e: 19 | log.error(f"解析 Model 文件失败 {file_path}, 报错内容: {e}") 20 | return [] 21 | 22 | return [node.name for node in ast.walk(tree) if isinstance(node, ast.ClassDef) and node.name.endswith("Model")] 23 | 24 | 25 | def import_models_from_directory(directory: Path, excluded: set) -> None: 26 | """ 27 | 从指定目录导入符合条件的 Model 类 28 | """ 29 | for file in directory.glob("*.py"): 30 | if file.name in excluded: 31 | continue 32 | 33 | module_name = file.stem 34 | try: 35 | module = importlib.import_module(f".{module_name}", package=__name__) 36 | class_names = [module_name] if "_to_" in module_name else find_classes_ending_with_model(file) 37 | 38 | for class_name in class_names: 39 | try: 40 | cls = getattr(module, class_name) 41 | globals()[class_name] = cls 42 | # log.info(f"成功引入 Model: {cls}") 43 | except AttributeError as e: 44 | log.error(f"导入 Model 失败,未在 {module_name}.py 文件中找到 {class_name} Model,错误:{e}") 45 | except ImportError as e: 46 | log.error(f"导入模块 {module_name} 失败,错误:{e}") 47 | 48 | 49 | # 排除的 Model 文件 50 | excluded_files = {"__init__.py"} 51 | 52 | # 动态加载各目录下的已存在 __init__.py 文件中的 Model 53 | import_models_from_directory(Path(__file__).parent, excluded_files) 54 | -------------------------------------------------------------------------------- /kinit_fast_task/app/models/auth_role_model.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/5/15 下午6:21 3 | # @File : auth_role.py 4 | # @IDE : PyCharm 5 | # @Desc : 文件描述信息 6 | 7 | 8 | from sqlalchemy.orm import relationship, Mapped, mapped_column 9 | from kinit_fast_task.app.models.base.orm import AbstractORMModel 10 | from sqlalchemy import String 11 | from kinit_fast_task.app.models.auth_user_to_role_model import auth_user_to_role_model 12 | 13 | 14 | class AuthRoleModel(AbstractORMModel): 15 | __tablename__ = "auth_role" 16 | __table_args__ = {"comment": "角色表"} 17 | 18 | users: Mapped[set["AuthUserModel"]] = relationship(secondary=auth_user_to_role_model, back_populates="roles") 19 | 20 | name: Mapped[str] = mapped_column(String(255), index=True, comment="角色名称") 21 | role_key: Mapped[str] = mapped_column(String(11), comment="角色唯一标识") 22 | is_active: Mapped[bool] = mapped_column(default=True, comment="是否可用") 23 | -------------------------------------------------------------------------------- /kinit_fast_task/app/models/auth_user_model.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/5/15 下午6:21 3 | # @File : auth_user.py 4 | # @IDE : PyCharm 5 | # @Desc : 文件描述信息 6 | 7 | 8 | from sqlalchemy.orm import relationship, Mapped, mapped_column 9 | 10 | from kinit_fast_task.app.models.auth_user_to_role_model import auth_user_to_role_model 11 | from kinit_fast_task.app.models.base.orm import AbstractORMModel 12 | from sqlalchemy import String 13 | 14 | 15 | class AuthUserModel(AbstractORMModel): 16 | __tablename__ = "auth_user" 17 | __table_args__ = {"comment": "用户表"} 18 | 19 | roles: Mapped[set["AuthRoleModel"]] = relationship(secondary=auth_user_to_role_model, back_populates="users") 20 | 21 | name: Mapped[str] = mapped_column(String(255), index=True, comment="用户名") 22 | telephone: Mapped[str] = mapped_column(String(11), comment="手机号, 要求唯一") 23 | email: Mapped[str | None] = mapped_column(comment="邮箱, 要求唯一") 24 | is_active: Mapped[bool] = mapped_column(default=True, comment="是否可用") 25 | age: Mapped[int] = mapped_column(comment="年龄") 26 | -------------------------------------------------------------------------------- /kinit_fast_task/app/models/auth_user_to_role_model.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/5/15 下午7:04 3 | # @File : auth_user_to_role.py 4 | # @IDE : PyCharm 5 | # @Desc : user role 多对多关联表 6 | 7 | 8 | from kinit_fast_task.db.orm.async_base_model import AsyncBaseORMModel 9 | from sqlalchemy import ForeignKey, Column, Table, Integer 10 | 11 | 12 | auth_user_to_role_model = Table( 13 | "auth_user_to_role", 14 | AsyncBaseORMModel.metadata, 15 | Column("user_id", Integer, ForeignKey("auth_user.id", ondelete="CASCADE")), 16 | Column("role_id", Integer, ForeignKey("auth_role.id", ondelete="CASCADE")), 17 | ) 18 | -------------------------------------------------------------------------------- /kinit_fast_task/app/models/base/__init__.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/3/28 14:34 3 | # @File : __init__.py 4 | # @IDE : PyCharm 5 | # @Desc : 描述信息 6 | -------------------------------------------------------------------------------- /kinit_fast_task/app/models/base/orm.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2021/10/18 22:19 3 | # @File : orm.py 4 | # @IDE : PyCharm 5 | # @Desc : 数据库公共 ORM 模型 6 | 7 | from datetime import datetime 8 | from sqlalchemy.orm import Mapped, mapped_column 9 | from kinit_fast_task.db.orm.async_base_model import AsyncBaseORMModel 10 | from sqlalchemy import func, inspect 11 | 12 | 13 | class AbstractORMModel(AsyncBaseORMModel): 14 | """ 15 | 公共 ORM 模型,基表 16 | 17 | 表配置官方文档:https://docs.sqlalchemy.org/en/20/orm/declarative_tables.html 18 | 支持 Enum 与 Literal 官方文档:https://docs.sqlalchemy.org/en/20/orm/declarative_tables.html#using-python-enum-or-pep-586-literal-types-in-the-type-map 19 | 中文官方文档:https://docs.sqlalchemy.org.cn/en/20/orm/declarative_tables.html#using-annotated-declarative-table-type-annotated-forms-for-mapped-column 20 | """ # noqa E501 21 | 22 | __abstract__ = True 23 | 24 | id: Mapped[int] = mapped_column(primary_key=True, comment="主键ID") 25 | create_datetime: Mapped[datetime] = mapped_column(server_default=func.now(), comment="创建时间") 26 | update_datetime: Mapped[datetime] = mapped_column( 27 | server_default=func.now(), onupdate=func.now(), comment="更新时间" 28 | ) 29 | delete_datetime: Mapped[datetime | None] = mapped_column(comment="删除时间") 30 | is_delete: Mapped[bool] = mapped_column(default=False, comment="是否软删除") 31 | 32 | @classmethod 33 | def get_column_attrs(cls) -> list: 34 | """ 35 | 获取模型中除 relationships 外的所有字段名称 36 | :return: 37 | """ 38 | mapper = inspect(cls) 39 | 40 | # for column in mapper.columns: 41 | # assert isinstance(column, Column) 42 | # print(column) 43 | # print(column.__dict__) 44 | # print(type(column)) 45 | # print(column.name) 46 | # print(column.type, column.type.python_type, type(column.type), column.type.__dict__) 47 | # print(column.nullable) 48 | # print(column.default) 49 | # print(column.comment) 50 | 51 | return mapper.columns.keys() 52 | 53 | @classmethod 54 | def get_attrs(cls) -> list: 55 | """ 56 | 获取模型所有字段名称 57 | :return: 58 | """ 59 | mapper = inspect(cls) 60 | return mapper.attrs.keys() 61 | 62 | @classmethod 63 | def get_relationships_attrs(cls) -> list: 64 | """ 65 | 获取模型中 relationships 所有字段名称 66 | :return: 67 | """ 68 | mapper = inspect(cls) 69 | return mapper.relationships.keys() 70 | -------------------------------------------------------------------------------- /kinit_fast_task/app/routers/__init__.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Update Time : 2024/5/4 15:25 3 | # @File : __init__.py 4 | # @IDE : PyCharm 5 | # @Desc : 文件描述信息 6 | -------------------------------------------------------------------------------- /kinit_fast_task/app/routers/auth_role/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvandk/kinit-fast-task/1c01a44517344f85eb7a0edad122958eca0dda81/kinit_fast_task/app/routers/auth_role/__init__.py -------------------------------------------------------------------------------- /kinit_fast_task/app/routers/auth_role/params.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/05/17 18:14 3 | # @File : params.py 4 | # @IDE : PyCharm 5 | # @Desc : 角色 6 | 7 | from fastapi import Depends 8 | from kinit_fast_task.app.depends.Paging import Paging, QueryParams 9 | 10 | 11 | class PageParams(QueryParams): 12 | def __init__(self, params: Paging = Depends()): 13 | super().__init__(params) 14 | 15 | self.v_order = "desc" 16 | -------------------------------------------------------------------------------- /kinit_fast_task/app/routers/auth_role/views.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/05/17 18:14 3 | # @File : views.py 4 | # @IDE : PyCharm 5 | # @Desc : 路由,视图文件 6 | 7 | from sqlalchemy.ext.asyncio import AsyncSession 8 | from fastapi import APIRouter, Depends, Body, Query 9 | 10 | from kinit_fast_task.db import DBFactory 11 | from kinit_fast_task.utils.response import RestfulResponse, ResponseSchema, PageResponseSchema 12 | from kinit_fast_task.app.cruds.auth_role_crud import AuthRoleCRUD 13 | from kinit_fast_task.app.schemas import DeleteSchema 14 | from kinit_fast_task.app.schemas import auth_role_schema as role_s 15 | from .params import PageParams 16 | 17 | router = APIRouter(prefix="/auth/role", tags=["角色管理"]) 18 | 19 | 20 | @router.post("/create", response_model=ResponseSchema[str], summary="创建角色") 21 | async def create( 22 | data: role_s.AuthRoleCreateSchema, 23 | session: AsyncSession = Depends(DBFactory.get_instance("orm").db_transaction_getter), 24 | ): 25 | """ 26 | 示例数据: 27 | 28 | { 29 | "name": "管理员", 30 | "role_key": "admin", 31 | "is_active": true 32 | } 33 | """ 34 | return RestfulResponse.success(await AuthRoleCRUD(session).create_data(data=data)) 35 | 36 | 37 | @router.post("/batch/create", response_model=ResponseSchema[str], summary="批量创建角色") 38 | async def batch_create( 39 | datas: list[role_s.AuthRoleCreateSchema], 40 | session: AsyncSession = Depends(DBFactory.get_instance("orm").db_transaction_getter), 41 | ): 42 | """ 43 | 示例数据: 44 | 45 | [ 46 | { 47 | "name": "管理员", 48 | "role_key": "admin", 49 | "is_active": true 50 | }, 51 | ... 52 | ] 53 | """ 54 | return RestfulResponse.success(await AuthRoleCRUD(session).create_datas(datas)) 55 | 56 | 57 | @router.post("/update", response_model=ResponseSchema[str], summary="更新角色") 58 | async def update( 59 | data_id: int = Body(..., description="角色编号"), 60 | data: role_s.AuthRoleUpdateSchema = Body(..., description="更新内容"), 61 | session: AsyncSession = Depends(DBFactory.get_instance("orm").db_transaction_getter), 62 | ): 63 | return RestfulResponse.success(await AuthRoleCRUD(session).update_data(data_id, data)) 64 | 65 | 66 | @router.post("/delete", response_model=ResponseSchema[str], summary="批量删除角色") 67 | async def delete( 68 | data: DeleteSchema = Body(..., description="角色编号列表"), 69 | session: AsyncSession = Depends(DBFactory.get_instance("orm").db_transaction_getter), 70 | ): 71 | await AuthRoleCRUD(session).delete_datas(ids=data.data_ids) 72 | return RestfulResponse.success("删除成功") 73 | 74 | 75 | @router.get( 76 | "/list/query", 77 | response_model=PageResponseSchema[list[role_s.AuthRoleSimpleOutSchema]], 78 | summary="获取角色列表", 79 | ) 80 | async def list_query( 81 | params: PageParams = Depends(), 82 | session: AsyncSession = Depends(DBFactory.get_instance("orm").db_transaction_getter), 83 | ): 84 | datas = await AuthRoleCRUD(session).get_datas(**params.dict(), v_return_type="dict") 85 | total = await AuthRoleCRUD(session).get_count(**params.to_count()) 86 | return RestfulResponse.success(data=datas, total=total, page=params.page, limit=params.limit) 87 | 88 | 89 | @router.get("/one/query", summary="获取角色信息") 90 | async def one_query( 91 | data_id: int = Query(..., description="角色编号"), 92 | session: AsyncSession = Depends(DBFactory.get_instance("orm").db_transaction_getter), 93 | ): 94 | data = await AuthRoleCRUD(session).get_data(data_id, v_schema=role_s.AuthRoleSimpleOutSchema, v_return_type="dict") 95 | return RestfulResponse.success(data=data) 96 | -------------------------------------------------------------------------------- /kinit_fast_task/app/routers/auth_user/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvandk/kinit-fast-task/1c01a44517344f85eb7a0edad122958eca0dda81/kinit_fast_task/app/routers/auth_user/__init__.py -------------------------------------------------------------------------------- /kinit_fast_task/app/routers/auth_user/params.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/05/17 14:37 3 | # @File : params.py 4 | # @IDE : PyCharm 5 | # @Desc : 用户 6 | 7 | from fastapi import Depends 8 | from kinit_fast_task.app.depends.Paging import Paging, QueryParams 9 | 10 | 11 | class PageParams(QueryParams): 12 | def __init__(self, params: Paging = Depends()): 13 | super().__init__(params) 14 | 15 | self.v_order = "desc" 16 | -------------------------------------------------------------------------------- /kinit_fast_task/app/routers/auth_user/services.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/5/29 下午3:43 3 | # @File : services.py 4 | # @IDE : PyCharm 5 | # @Desc : 文件描述信息 6 | import datetime 7 | 8 | from sqlalchemy import select, func 9 | from sqlalchemy.ext.asyncio import AsyncSession 10 | from sqlalchemy.orm import selectinload 11 | 12 | from kinit_fast_task.app.models.auth_user_model import AuthUserModel 13 | from kinit_fast_task.app.models.auth_role_model import AuthRoleModel 14 | from kinit_fast_task.db import DBFactory 15 | from kinit_fast_task.app.cruds.auth_user_crud import AuthUserCRUD 16 | from kinit_fast_task.app.schemas import auth_user_schema as user_s 17 | 18 | 19 | class UserService: 20 | """ 21 | 业务逻辑处理,应该为单独的 service 文件,只有通用的处理 才会放在 crud 中 22 | """ 23 | 24 | def __init__(self, session: AsyncSession = None): 25 | self.session = session 26 | 27 | async def get_recent_users_count(self): 28 | """ 29 | 获取最近一个月的用户新增情况 30 | 31 | :return: 32 | """ 33 | one_month_ago = datetime.datetime.now() - datetime.timedelta(days=30) 34 | 35 | # 查询近一个月用户新增数据量 36 | stmt = ( 37 | select(func.date(AuthUserModel.create_datetime).label("date"), func.count(AuthUserModel.id).label("count")) 38 | .where(AuthUserModel.create_datetime >= one_month_ago) 39 | .group_by(func.date(AuthUserModel.create_datetime)) 40 | ) 41 | result = await self.session.execute(stmt) 42 | records = result.all() 43 | 44 | # 生成一个完整的日期范围 45 | start_date = one_month_ago.date() 46 | end_date = datetime.datetime.utcnow().date() 47 | date_range = [start_date + datetime.timedelta(days=x) for x in range((end_date - start_date).days + 1)] 48 | 49 | # 创建一个包含所有日期的字典,并将计数初始化为0 50 | user_count_by_date = {str(date): 0 for date in date_range} 51 | 52 | # 更新字典中的计数值 53 | for record in records: 54 | user_count_by_date[str(record.date)] = record.count 55 | 56 | return user_count_by_date 57 | 58 | async def orm_db_getter_01_test(self): 59 | """ 60 | orm db 手动事务示例 61 | 62 | :return: 63 | """ 64 | orm_db: AsyncSession = DBFactory.get_instance("orm").db_getter() 65 | 66 | # 手动开启一个事务,事务在关闭时会自动 commit ,请勿在事务中使用 commit 67 | # 不关联的两种操作,请开启两个事务进行处理,比如第二个失败,不影响第一个 commit 68 | async with orm_db.begin(): 69 | # 创建一个用户 70 | new_user = user_s.AuthUserCreateSchema( 71 | name="orm_db_test", telephone="19920240505", is_active=True, age=3, role_ids=[1] 72 | ) 73 | user = await AuthUserCRUD(orm_db).create_data(new_user, v_return_obj=True) 74 | print("用户创建成功", user) 75 | 76 | # 更新一个用户 77 | user = await AuthUserCRUD(orm_db).update_data(user.id, {"is_active": False}, v_return_obj=True) 78 | print("用户更新成功", user.is_active) 79 | 80 | # 会触发回滚操作, 或者可以说, 该事务还未结束, 并没有触发 commit 操作, 所以以上操作都会回滚 81 | # raise ValueError("事务内抛出异常, 测试回滚操作") 82 | 83 | # 事务外触发异常, 因为以上事务已经 commit , 所以这里不会回滚, 以上事务内操作都会进入到数据库 84 | # raise ValueError("抛出异常,测试回滚操作") 85 | 86 | async def orm_db_getter_02_test(self): 87 | """ 88 | orm db 示例 89 | 90 | :return: 91 | """ 92 | orm_db: AsyncSession = DBFactory.get_instance("orm").db_getter() 93 | 94 | # 查询不需要事务 95 | 96 | # 获取 id=1 的用户,没有获取到会返回 None 97 | user = await orm_db.get(AuthUserModel, 2) 98 | if user is None: 99 | print("未获取到指定用户") 100 | else: 101 | print("获取用户成功", user) 102 | 103 | async def orm_03_test(self): 104 | """ 105 | ORM 多对多(多对一也可用)关联查询测试 106 | 107 | 获取拥有管理员角色的用户: 108 | 1. 使用快速方式,不加载外键关联数据 109 | 2. 使用快速方式,并加载外键关联数据 110 | 3. 使用原生方式,不加载外键关联数据 111 | 4. 使用原生方式,并加载外键关联数据 112 | """ 113 | # 1. 使用快速方式,不加载外键关联数据 114 | # v_join = [["roles"]] # 指定外键查询字段 115 | # v_where = [AuthRoleModel.name == "管理员"] # 外键查询条件 116 | # # limit=0 表示查询出所有数据,否则默认为第一页的10条数据 117 | # users = await AuthUserCRUD(session=self.session).get_datas( 118 | # limit=0, 119 | # v_join=v_join, 120 | # v_where=v_where, 121 | # v_return_type="model" 122 | # ) 123 | # for user in users: 124 | # # 无法通过 user.roles 获取用户关联的所有角色数据 125 | # print("用户查询结果:", user.id, user.name) 126 | 127 | # 2. 使用快速方式,并加载外键关联数据 128 | v_options = [selectinload(AuthUserModel.roles)] # 加载外键字段,使其可以通过 . 访问到外键数据 129 | v_join = [["roles"]] # 指定外键查询字段 130 | v_where = [AuthRoleModel.name == "管理员"] # 外键查询条件 131 | # limit=0 表示查询出所有数据,否则默认为第一页的10条数据 132 | users = await AuthUserCRUD(session=self.session).get_datas( 133 | limit=0, v_join=v_join, v_where=v_where, v_options=v_options, v_return_type="model" 134 | ) 135 | for user in users: 136 | print("用户查询结果:", user.id, user.name) 137 | for role in user.roles: 138 | print(f"{user.name} 用户关联角色查询结果:", role.id, role.name) 139 | 140 | # 3. 使用原生方式,不加载外键关联数据 141 | # users_sql = select(AuthUserModel).join(AuthUserModel.roles).where(AuthRoleModel.name == "管理员") 142 | # users = (await self.session.scalars(users_sql)).all() 143 | # for user in users: 144 | # # 无法通过 user.roles 获取用户关联的所有角色数据 145 | # print("用户查询结果:", user.id, user.name) 146 | 147 | # 4. 使用原生方式,并加载外键关联数据 148 | # users_sql = select(AuthUserModel).join(AuthUserModel.roles).where(AuthRoleModel.name == "管理员").options( 149 | # selectinload(AuthUserModel.roles) 150 | # ) 151 | # users = (await self.session.scalars(users_sql)).all() 152 | # for user in users: 153 | # print("用户查询结果:", user.id, user.name) 154 | # for role in user.roles: 155 | # print(f"{user.name} 用户关联角色查询结果:", role.id, role.name) 156 | -------------------------------------------------------------------------------- /kinit_fast_task/app/routers/auth_user/views.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/05/17 14:37 3 | # @File : views.py 4 | # @IDE : PyCharm 5 | # @Desc : 路由,视图文件 6 | 7 | from sqlalchemy.ext.asyncio import AsyncSession 8 | from fastapi import APIRouter, Depends, Body, Query 9 | from sqlalchemy.orm import selectinload 10 | 11 | from kinit_fast_task.db import DBFactory 12 | from kinit_fast_task.utils.response import RestfulResponse, ResponseSchema, PageResponseSchema 13 | from kinit_fast_task.app.cruds.auth_user_crud import AuthUserCRUD 14 | from kinit_fast_task.app.schemas import auth_user_schema as user_s, DeleteSchema 15 | from kinit_fast_task.app.models.auth_user_model import AuthUserModel 16 | from .params import PageParams 17 | from .services import UserService 18 | 19 | router = APIRouter(prefix="/auth/user", tags=["用户管理"]) 20 | 21 | 22 | @router.post("/create", response_model=ResponseSchema[str], summary="创建用户") 23 | async def create( 24 | data: user_s.AuthUserCreateSchema, 25 | session: AsyncSession = Depends(DBFactory.get_instance("orm").db_transaction_getter), 26 | ): 27 | """ 28 | 示例数据: 29 | 30 | { 31 | "name": "kinit", 32 | "telephone": "18820240528", 33 | "email": "user@email.com", 34 | "is_active": true, 35 | "age": 3, 36 | "role_ids": [ 37 | 1, 2 38 | ] 39 | } 40 | """ 41 | return RestfulResponse.success(await AuthUserCRUD(session).create_data(data=data)) 42 | 43 | 44 | @router.post("/update", response_model=ResponseSchema[str], summary="更新用户") 45 | async def update( 46 | data_id: int = Body(..., description="用户编号"), 47 | data: user_s.AuthUserUpdateSchema = Body(..., description="更新内容"), 48 | session: AsyncSession = Depends(DBFactory.get_instance("orm").db_transaction_getter), 49 | ): 50 | return RestfulResponse.success(await AuthUserCRUD(session).update_data(data_id, data)) 51 | 52 | 53 | @router.post("/delete", response_model=ResponseSchema[str], summary="批量删除用户") 54 | async def delete( 55 | data: DeleteSchema = Body(..., description="用户编号列表"), 56 | session: AsyncSession = Depends(DBFactory.get_instance("orm").db_transaction_getter), 57 | ): 58 | await AuthUserCRUD(session).delete_datas(ids=data.data_ids) 59 | return RestfulResponse.success("删除成功") 60 | 61 | 62 | @router.get("/list/query", response_model=PageResponseSchema[list[user_s.AuthUserOutSchema]], summary="获取用户列表") 63 | async def list_query( 64 | params: PageParams = Depends(), 65 | session: AsyncSession = Depends(DBFactory.get_instance("orm").db_transaction_getter), 66 | ): 67 | v_options = [selectinload(AuthUserModel.roles)] 68 | v_schema = user_s.AuthUserOutSchema 69 | datas = await AuthUserCRUD(session).get_datas( 70 | **params.dict(), v_options=v_options, v_return_type="dict", v_schema=v_schema 71 | ) 72 | total = await AuthUserCRUD(session).get_count(**params.to_count()) 73 | return RestfulResponse.success(data=datas, total=total, page=params.page, limit=params.limit) 74 | 75 | 76 | @router.get("/one/query", response_model=PageResponseSchema[user_s.AuthUserOutSchema], summary="获取用户信息") 77 | async def one_query( 78 | data_id: int = Query(..., description="用户编号"), 79 | session: AsyncSession = Depends(DBFactory.get_instance("orm").db_transaction_getter), 80 | ): 81 | v_options = [selectinload(AuthUserModel.roles)] 82 | data = await AuthUserCRUD(session).get_data( 83 | data_id, v_schema=user_s.AuthUserOutSchema, v_options=v_options, v_return_type="dict" 84 | ) 85 | return RestfulResponse.success(data=data) 86 | 87 | 88 | @router.get("/recent/month/user/query", response_model=PageResponseSchema[dict], summary="获取最近一个月的用户新增情况") 89 | async def recent_month_user_query( 90 | session: AsyncSession = Depends(DBFactory.get_instance("orm").db_transaction_getter), 91 | ): 92 | return RestfulResponse.success(data=await UserService(session).get_recent_users_count()) 93 | 94 | 95 | @router.post("/orm/db/getter/01/test", response_model=ResponseSchema[str], summary="ORM db_getter 手动事务测试") 96 | async def orm_db_getter_test(): 97 | """ 98 | ORM db_getter 手动事务测试 99 | """ 100 | await UserService().orm_db_getter_01_test() 101 | return RestfulResponse.success() 102 | 103 | 104 | @router.post("/orm/db/getter/02/test", response_model=ResponseSchema[str], summary="ORM db_getter 测试") 105 | async def orm_db_getter_test_02(): 106 | """ 107 | ORM db_getter 测试 108 | """ 109 | await UserService().orm_db_getter_02_test() 110 | return RestfulResponse.success() 111 | 112 | 113 | @router.post("/orm/03/test", response_model=ResponseSchema[str], summary="ORM 多对多(多对一也可用)关联查询测试") 114 | async def orm_03_test(session: AsyncSession = Depends(DBFactory.get_instance("orm").db_transaction_getter)): 115 | """ 116 | ORM 多对多(多对一也可用)关联查询测试 117 | """ 118 | await UserService(session).orm_03_test() 119 | return RestfulResponse.success() 120 | -------------------------------------------------------------------------------- /kinit_fast_task/app/routers/system_record/__init__.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/5/28 上午11:12 3 | # @File : __init__.py.py 4 | # @IDE : PyCharm 5 | # @Desc : 文件描述信息 6 | -------------------------------------------------------------------------------- /kinit_fast_task/app/routers/system_record/params.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/05/17 14:37 3 | # @File : params.py 4 | # @IDE : PyCharm 5 | # @Desc : 6 | 7 | from fastapi import Depends 8 | from kinit_fast_task.app.depends.Paging import Paging, QueryParams 9 | 10 | 11 | class PageParams(QueryParams): 12 | def __init__(self, params: Paging = Depends()): 13 | super().__init__(params) 14 | 15 | self.v_order_field = "create_datetime" 16 | self.v_order = "desc" 17 | -------------------------------------------------------------------------------- /kinit_fast_task/app/routers/system_record/views.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/05/17 14:37 3 | # @File : views.py 4 | # @IDE : PyCharm 5 | # @Desc : 路由,视图文件 6 | 7 | from fastapi import APIRouter, Depends 8 | 9 | from kinit_fast_task.app.cruds.base.mongo import ReturnType 10 | from kinit_fast_task.app.routers.system_record.params import PageParams 11 | from kinit_fast_task.utils.response import RestfulResponse, PageResponseSchema 12 | from kinit_fast_task.app.schemas import record_operation_schema as oper_s 13 | from kinit_fast_task.app.cruds.record_operation_crud import OperationCURD 14 | 15 | router = APIRouter(prefix="/system/record", tags=["系统记录管理"]) 16 | 17 | 18 | @router.get( 19 | "/operation/list/query", 20 | response_model=PageResponseSchema[list[oper_s.OperationSimpleOutSchema]], 21 | summary="获取系统操作记录列表", 22 | ) 23 | async def operation_list_query(params: PageParams = Depends()): 24 | """ 25 | 可以在 kinit_fast_task/config.py:SystemSettings.OPERATION_LOG_RECORD 中选择开启或关闭系统操作记录功能 26 | """ 27 | datas = await OperationCURD().get_datas(**params.dict(), v_return_type=ReturnType.DICT) 28 | total = await OperationCURD().get_count(**params.to_count()) 29 | return RestfulResponse.success(data=datas, total=total, page=params.page, limit=params.limit) 30 | -------------------------------------------------------------------------------- /kinit_fast_task/app/routers/system_storage/__init__.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/7/8 3 | # @File : __init__.py.py 4 | # @IDE : PyCharm 5 | # @Desc : 文件描述信息 6 | -------------------------------------------------------------------------------- /kinit_fast_task/app/routers/system_storage/views.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/7/8 3 | # @File : views.py 4 | # @IDE : PyCharm 5 | # @Desc : 文件描述信息 6 | 7 | 8 | from fastapi import APIRouter, UploadFile, File 9 | 10 | from kinit_fast_task.utils.response import RestfulResponse, ResponseSchema 11 | from kinit_fast_task.utils.storage import StorageFactory 12 | 13 | router = APIRouter(prefix="/system/storage", tags=["系统存储管理"]) 14 | 15 | 16 | @router.post("/local/image/create", response_model=ResponseSchema[str], summary="上传图片到本地目录") 17 | async def local_image_create(file: UploadFile = File(..., description="图片文件")): 18 | storage = StorageFactory.get_instance("local") 19 | result = await storage.save_image(file, path="image") 20 | return RestfulResponse.success(data=result) 21 | 22 | 23 | @router.post("/local/audio/create", response_model=ResponseSchema[str], summary="上传音频到本地目录") 24 | async def local_audio_create(file: UploadFile = File(..., description="音频文件")): 25 | storage = StorageFactory.get_instance("local") 26 | path = f"audio/{storage.get_today_timestamp()}" 27 | result = await storage.save_audio(file, path=path) 28 | return RestfulResponse.success(data=result) 29 | 30 | 31 | @router.post("/local/video/create", response_model=ResponseSchema[str], summary="上传视频到本地目录") 32 | async def local_video_create(file: UploadFile = File(..., description="视频文件")): 33 | storage = StorageFactory.get_instance("local") 34 | path = f"video/{storage.get_today_timestamp()}" 35 | result = await storage.save_video(file, path=path) 36 | return RestfulResponse.success(data=result) 37 | 38 | 39 | @router.post("/local/create", response_model=ResponseSchema[str], summary="上传文件到本地目录") 40 | async def local_create(file: UploadFile = File(..., description="上传文件")): 41 | storage = StorageFactory.get_instance("local") 42 | result = await storage.save(file) 43 | return RestfulResponse.success(data=result) 44 | 45 | 46 | @router.post("/oss/image/create", response_model=ResponseSchema[str], summary="上传图片到 OSS") 47 | async def oss_image_create(file: UploadFile = File(..., description="图片文件")): 48 | storage = StorageFactory.get_instance("oss") 49 | result = await storage.save(file) 50 | return RestfulResponse.success(data=result) 51 | 52 | 53 | @router.post("/temp/image/create", response_model=ResponseSchema[str], summary="上传临时文件到本地临时目录") 54 | async def temp_image_create(file: UploadFile = File(..., description="上传文件")): 55 | storage = StorageFactory.get_instance("temp") 56 | result = await storage.save(file) 57 | return RestfulResponse.success(data=result) 58 | -------------------------------------------------------------------------------- /kinit_fast_task/app/schemas/__init__.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/3/23 22:48 3 | # @File : __init__.py 4 | # @IDE : PyCharm 5 | # @Desc : 描述信息 6 | 7 | from kinit_fast_task.app.schemas.base.base import BaseSchema 8 | from kinit_fast_task.app.schemas.global_schema import DeleteSchema 9 | -------------------------------------------------------------------------------- /kinit_fast_task/app/schemas/auth_role_schema.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/05/17 18:14 3 | # @File : auth_role.py 4 | # @IDE : PyCharm 5 | # @Desc : Pydantic 模型,用于数据库序列化操作 6 | 7 | from pydantic import Field 8 | from kinit_fast_task.core.types import DatetimeStr 9 | from kinit_fast_task.app.schemas.base import BaseSchema 10 | 11 | 12 | class AuthRoleBaseSchema(BaseSchema): 13 | name: str = Field(..., description="角色名称") 14 | role_key: str = Field(..., description="标识") 15 | is_active: bool = Field(True, description="是否可用") 16 | 17 | 18 | class AuthRoleCreateSchema(AuthRoleBaseSchema): 19 | pass 20 | 21 | 22 | class AuthRoleUpdateSchema(AuthRoleBaseSchema): 23 | pass 24 | 25 | 26 | class AuthRoleSimpleOutSchema(AuthRoleBaseSchema): 27 | id: int = Field(..., description="编号") 28 | create_datetime: DatetimeStr = Field(..., description="创建时间") 29 | update_datetime: DatetimeStr = Field(..., description="更新时间") 30 | -------------------------------------------------------------------------------- /kinit_fast_task/app/schemas/auth_user_schema.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/05/17 14:37 3 | # @File : auth_user.py 4 | # @IDE : PyCharm 5 | # @Desc : Pydantic 模型,用于数据库序列化操作 6 | 7 | from pydantic import Field 8 | from kinit_fast_task.core.types import DatetimeStr, Email, Telephone 9 | from kinit_fast_task.app.schemas.base.base import BaseSchema 10 | from kinit_fast_task.app.schemas import auth_role_schema 11 | 12 | 13 | class AuthUserSchema(BaseSchema): 14 | name: str = Field(..., description="用户名") 15 | telephone: Telephone = Field(..., description="手机号") 16 | email: Email | None = Field(None, description="邮箱") 17 | is_active: bool = Field(True, description="是否可用") 18 | age: int = Field(..., description="年龄") 19 | 20 | 21 | class AuthUserCreateSchema(AuthUserSchema): 22 | role_ids: list[int] = Field(..., description="关联角色列表") 23 | 24 | 25 | class AuthUserUpdateSchema(AuthUserSchema): 26 | role_ids: list[int] = Field(..., description="关联角色列表") 27 | 28 | 29 | class AuthUserSimpleOutSchema(AuthUserSchema): 30 | id: int = Field(..., description="编号") 31 | create_datetime: DatetimeStr = Field(..., description="创建时间") 32 | update_datetime: DatetimeStr = Field(..., description="更新时间") 33 | 34 | 35 | class AuthUserOutSchema(AuthUserSimpleOutSchema): 36 | roles: list[auth_role_schema.AuthRoleSimpleOutSchema] = Field(..., description="角色列表") 37 | -------------------------------------------------------------------------------- /kinit_fast_task/app/schemas/base/__init__.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/3/28 14:37 3 | # @File : __init__.py 4 | # @IDE : PyCharm 5 | # @Desc : 描述信息 6 | 7 | from kinit_fast_task.app.schemas.base.base import BaseSchema 8 | -------------------------------------------------------------------------------- /kinit_fast_task/app/schemas/base/base.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/3/28 16:00 3 | # @File : base.py 4 | # @IDE : PyCharm 5 | # @Desc : 描述信息 6 | 7 | from typing import Any 8 | from fastapi.encoders import jsonable_encoder 9 | from pydantic import BaseModel, ConfigDict 10 | 11 | 12 | def add_schema_extra(schema: dict[str, Any]) -> None: 13 | """ 14 | 自定义 JSON Schema 的输出,主要功能包括在自动生成的接口文档中添加示例数据、隐藏特定字段等操作 15 | 16 | 在接口文档中默认不展示 hidden=True 的字段 17 | 18 | 并不会影响序列化操作 19 | 20 | :param schema: 该参数是一个字典,表示生成的JSON Schema。你可以通过修改这个字典来定制最终生成的JSON Schema。例如,可以从中删除或修改字段属性,添加自定义属性等。 21 | :return: 22 | """ # noqa E501 23 | if "properties" in schema: 24 | # 收集需要删除的键 25 | keys_to_delete = [key for key, value in schema["properties"].items() if value.get("hidden") is True] 26 | 27 | # 删除包含 hidden=True 的项 28 | for key in keys_to_delete: 29 | del schema["properties"][key] 30 | 31 | 32 | class BaseSchema(BaseModel): 33 | """ 34 | from_attributes:允许将非字典格式的数据(例如,具有属性的对象)转化为 Pydantic 模型实例,例如:ORM Model 35 | """ # noqa E501 36 | 37 | model_config = ConfigDict(from_attributes=True, json_schema_extra=add_schema_extra) 38 | 39 | def serializable_dict(self, **kwargs) -> dict: 40 | """ 41 | 返回一个仅包含可序列化字段的字典 42 | :param kwargs: 43 | :return: 返回 JSON 可序列化的字典 44 | """ 45 | default_dict = self.model_dump() 46 | return jsonable_encoder(default_dict) 47 | -------------------------------------------------------------------------------- /kinit_fast_task/app/schemas/global_schema.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/7/11 3 | # @File : global_schema.py 4 | # @IDE : PyCharm 5 | # @Desc : 文件描述信息 6 | from pydantic import Field 7 | 8 | from kinit_fast_task.app.schemas import BaseSchema 9 | 10 | 11 | class DeleteSchema(BaseSchema): 12 | data_ids: list[int] = Field(..., description="需要删除的数据编号列表") 13 | -------------------------------------------------------------------------------- /kinit_fast_task/app/schemas/record_operation_schema.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/4/23 17:00 3 | # @File : operation.py 4 | # @IDE : PyCharm 5 | # @Desc : 操作日志 6 | 7 | from pydantic import Field 8 | from kinit_fast_task.core.types import DatetimeStr, ObjectIdStr 9 | from kinit_fast_task.app.schemas.base.base import BaseSchema 10 | 11 | 12 | class OperationSchema(BaseSchema): 13 | status_code: int | None = Field(None, description="响应状态 Code") 14 | client_ip: str | None = Field(None, description="客户端 IP") 15 | request_method: str | None = Field(None, description="请求方式") 16 | api_path: str | None = Field(None, description="请求路径") 17 | system: str | None = Field(None, description="客户端系统") 18 | browser: str | None = Field(None, description="客户端浏览器") 19 | summary: str | None = Field(None, description="接口名称") 20 | route_name: str | None = Field(None, description="路由函数名称") 21 | description: str | None = Field(None, description="路由文档") 22 | tags: list[str] | None = Field(None, description="路由标签") 23 | process_time: float | None = Field(None, description="耗时") 24 | params: str | None = Field(None, description="请求参数") 25 | 26 | 27 | class OperationSimpleOutSchema(OperationSchema): 28 | id: ObjectIdStr = Field(..., alias="_id") 29 | create_datetime: DatetimeStr = Field(..., description="创建时间") 30 | update_datetime: DatetimeStr = Field(..., description="更新时间") 31 | -------------------------------------------------------------------------------- /kinit_fast_task/app/system/__init__.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/5/17 下午4:25 3 | # @File : __init__.py.py 4 | # @IDE : PyCharm 5 | # @Desc : 文件描述信息 6 | -------------------------------------------------------------------------------- /kinit_fast_task/app/system/docs/__init__.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/5/17 下午4:40 3 | # @File : __init__.py.py 4 | # @IDE : PyCharm 5 | # @Desc : 文件描述信息 6 | -------------------------------------------------------------------------------- /kinit_fast_task/app/system/docs/views.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/5/17 下午4:40 3 | # @File : views.py 4 | # @IDE : PyCharm 5 | # @Desc : 接口文档 6 | 7 | 8 | from fastapi import Request, FastAPI 9 | from fastapi.openapi.docs import get_swagger_ui_html, get_swagger_ui_oauth2_redirect_html, get_redoc_html 10 | from kinit_fast_task.config import settings 11 | 12 | 13 | def load_system_routes(app: FastAPI): 14 | """ 15 | 加载系统系统 16 | :param app: 17 | :return: 18 | """ 19 | 20 | @app.get("/docs", summary="Swagger UI API Docs", include_in_schema=False) 21 | def rewrite_generate_swagger_ui(request: Request): 22 | return get_swagger_ui_html( 23 | openapi_url=request.app.openapi_url, 24 | title=f"{request.app.title} - Swagger UI", 25 | oauth2_redirect_url=request.app.swagger_ui_oauth2_redirect_url, 26 | swagger_js_url=f"{settings.storage.LOCAL_BASE_URL}/swagger_ui/swagger-ui-bundle.js", 27 | swagger_css_url=f"{settings.storage.LOCAL_BASE_URL}/swagger_ui/swagger-ui.css", 28 | ) 29 | 30 | @app.get("/swagger-redirect", summary="Swagger UI Redirect", include_in_schema=False) 31 | def rewrite_swagger_ui_redirect(): 32 | return get_swagger_ui_oauth2_redirect_html() 33 | 34 | @app.get("/redoc", summary="Redoc HTML API Docs", include_in_schema=False) 35 | def rewrite_generate_redoc_html(request: Request): 36 | return get_redoc_html( 37 | openapi_url=request.app.openapi_url, 38 | title=f"{request.app.title} - ReDoc", 39 | redoc_js_url=f"{settings.storage.LOCAL_BASE_URL}/redoc_ui/redoc.standalone.js", 40 | ) 41 | -------------------------------------------------------------------------------- /kinit_fast_task/core/__init__.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/3/16 12:42 3 | # @File : __init__.py 4 | # @IDE : PyCharm 5 | # @Desc : 项目核心模块,在项目中深入使用的模块 6 | 7 | from kinit_fast_task.core.exception import CustomException 8 | -------------------------------------------------------------------------------- /kinit_fast_task/core/event.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/4/22 17:58 3 | # @File : event.py 4 | # @IDE : PyCharm 5 | # @Desc : 全局事件 6 | 7 | from fastapi import FastAPI 8 | from kinit_fast_task.db import DBFactory 9 | 10 | from kinit_fast_task.utils import log 11 | 12 | 13 | async def close_db_event(app: FastAPI, status: bool): 14 | """ 15 | 关闭数据库连接事件 16 | :param app: 17 | :param status: 用于判断是开始还是结束事件,为 True 说明是开始事件,反着关闭事件 18 | :return: 19 | """ 20 | if status: 21 | log.info("启动项目事件成功执行!") 22 | else: 23 | await DBFactory.clear() 24 | log.info("关闭项目事件成功执行!") 25 | -------------------------------------------------------------------------------- /kinit_fast_task/core/exception.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/16/19 15:47 3 | # @File : exception.py 4 | # @IDE : PyCharm 5 | # @Desc : 全局异常处理 6 | 7 | from fastapi.responses import JSONResponse 8 | from starlette.exceptions import HTTPException as StarletteHTTPException 9 | from fastapi.exceptions import RequestValidationError 10 | from kinit_fast_task.utils.response_code import Status as UtilsStatus 11 | from fastapi import status as fastapi_status 12 | from fastapi import Request 13 | from fastapi.encoders import jsonable_encoder 14 | from fastapi import FastAPI 15 | from kinit_fast_task.utils import log 16 | 17 | 18 | class CustomException(Exception): 19 | """ 20 | 自定义异常 21 | """ 22 | 23 | def __init__( 24 | self, 25 | message: str, 26 | *, 27 | code: int = UtilsStatus.HTTP_ERROR, 28 | status_code: int = fastapi_status.HTTP_200_OK, 29 | desc: str = None, 30 | ): 31 | """ 32 | 自定义异常 33 | :param message: 报错内容提示信息 34 | :param code: 描述 code 35 | :param status_code: 响应 code 36 | :param desc: 描述信息,不返回给前端,只在接口日志中输出 37 | """ 38 | self.message = message 39 | self.status_code = status_code 40 | if self.status_code != fastapi_status.HTTP_200_OK: 41 | self.code = self.status_code 42 | else: 43 | self.code = code 44 | self.desc = desc 45 | 46 | 47 | def refactoring_exception(app: FastAPI): 48 | """ 49 | 异常捕捉 50 | """ 51 | 52 | @app.exception_handler(CustomException) 53 | async def custom_exception_handler(request: Request, exc: CustomException): 54 | """ 55 | 自定义异常 56 | """ 57 | func_desc = "捕捉到自定义CustomException异常:custom_exception_handler" 58 | log.error(f"请求地址:{request.url.__str__()} {func_desc} 异常信息:{exc.message} {exc.desc}") 59 | return JSONResponse( 60 | status_code=exc.status_code, 61 | content={"message": exc.message, "code": exc.code}, 62 | ) 63 | 64 | @app.exception_handler(StarletteHTTPException) 65 | async def unicorn_exception_handler(request: Request, exc: StarletteHTTPException): 66 | """ 67 | 重写HTTPException异常处理器 68 | """ 69 | func_desc = "捕捉到重写HTTPException异常异常:unicorn_exception_handler" 70 | log.error(f"请求地址:{request.url.__str__()} {func_desc} 异常信息:{exc.detail}") 71 | return JSONResponse( 72 | status_code=exc.status_code, 73 | content={ 74 | "code": exc.status_code, 75 | "message": exc.detail, 76 | }, 77 | ) 78 | 79 | @app.exception_handler(RequestValidationError) 80 | async def validation_exception_handler(request: Request, exc: RequestValidationError): 81 | """ 82 | 重写请求验证异常处理器 83 | """ 84 | func_desc = "捕捉到重写请求验证异常:validation_exception_handler" 85 | log.error(f"请求地址:{request.url.__str__()} {func_desc} 异常信息:{exc.errors()}") 86 | msg = exc.errors()[0].get("msg") 87 | if msg == "field required": 88 | msg = "请求失败,缺少必填项!" 89 | elif msg == "value is not a valid list": 90 | msg = "类型错误,提交参数应该为列表!" 91 | elif msg == "value is not a valid int": 92 | msg = "类型错误,提交参数应该为整数!" 93 | elif msg == "value could not be parsed to a boolean": 94 | msg = "类型错误,提交参数应该为布尔值!" 95 | elif msg == "Input should be a valid list": 96 | msg = "类型错误,输入应该是一个有效的列表!" 97 | return JSONResponse( 98 | status_code=fastapi_status.HTTP_200_OK, 99 | content=jsonable_encoder({"message": msg, "body": exc.body, "code": UtilsStatus.HTTP_ERROR}), 100 | ) 101 | 102 | @app.exception_handler(ValueError) 103 | async def value_exception_handler(request: Request, exc: ValueError): 104 | """ 105 | 捕获值异常 106 | """ 107 | func_desc = "捕捉到值异常:value_exception_handler" 108 | log.error(f"请求地址:{request.url.__str__()} {func_desc} 异常信息:{exc.__str__()}") 109 | return JSONResponse( 110 | status_code=fastapi_status.HTTP_200_OK, 111 | content=jsonable_encoder({"message": exc.__str__(), "code": UtilsStatus.HTTP_ERROR}), 112 | ) 113 | 114 | @app.exception_handler(Exception) 115 | async def all_exception_handler(request: Request, exc: Exception): 116 | """ 117 | 捕捉到全局异常 118 | """ 119 | func_desc = "捕捉到全局异常:all_exception_handler" 120 | log.error(f"请求地址:{request.url.__str__()} {func_desc} 异常信息:{exc.__str__()}") 121 | return JSONResponse( 122 | status_code=fastapi_status.HTTP_500_INTERNAL_SERVER_ERROR, 123 | content=jsonable_encoder({"message": "接口异常,请联系管理员!", "code": UtilsStatus.HTTP_500}), 124 | ) 125 | -------------------------------------------------------------------------------- /kinit_fast_task/core/middleware.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2021/10/19 15:47 3 | # @File : middleware.py 4 | # @IDE : PyCharm 5 | # @Desc : 中间件 6 | 7 | """ 8 | 官方文档——中间件:https://fastapi.tiangolo.com/tutorial/middleware/ 9 | 官方文档——高级中间件:https://fastapi.tiangolo.com/advanced/middleware/ 10 | 11 | 经测试:中间件中报错,或使用 raise 无论哪种类型的 Exception,都会被全局异常捕获,无法被特定异常捕获 12 | 所以最好是改变返回的 response 13 | """ 14 | 15 | import datetime 16 | import json 17 | import time 18 | 19 | from fastapi import Request 20 | from kinit_fast_task.utils import log 21 | from fastapi import FastAPI 22 | from fastapi.routing import APIRoute 23 | from user_agents import parse 24 | from kinit_fast_task.config import settings 25 | from kinit_fast_task.app.cruds.record_operation_crud import OperationCURD 26 | from kinit_fast_task.utils.response import RestfulResponse 27 | from kinit_fast_task.utils.response_code import Status 28 | 29 | 30 | def register_request_log_middleware(app: FastAPI): 31 | """ 32 | 记录请求日志中间件 33 | :param app: 34 | :return: 35 | """ 36 | 37 | @app.middleware("http") 38 | async def request_log_middleware(request: Request, call_next): 39 | start_time = time.time() 40 | response = await call_next(request) 41 | process_time = f"{int((time.time() - start_time) * 1000)}ms" 42 | response.headers["X-Process-Time"] = str(process_time) 43 | http_version = f"http/{request.scope['http_version']}" 44 | content_length = response.raw_headers[0][1] 45 | process_time = response.headers["X-Process-Time"] 46 | content = ( 47 | f"request router: '{request.method} {request.url} {http_version}' {response.status_code} " 48 | f"{response.charset} {content_length} {process_time}" 49 | ) 50 | if response.status_code != 200: 51 | log.error(content) 52 | else: 53 | log.info(content) 54 | return response 55 | 56 | 57 | def register_operation_record_middleware(app: FastAPI): 58 | """ 59 | 操作记录中间件 60 | 用于将使用认证的操作全部记录到 mongodb 数据库中 61 | :param app: 62 | :return: 63 | """ 64 | 65 | @app.middleware("http") 66 | async def operation_record_middleware(request: Request, call_next): 67 | if not settings.db.MONGO_DB_ENABLE: 68 | log.error("未开启 MongoDB 数据库,无法存入操作记录,请在 config.py:OPERATION_LOG_RECORD 中关闭操作记录") 69 | return RestfulResponse.error("系统异常,请联系管理员", code=Status.HTTP_500) 70 | 71 | start_time = time.time() 72 | # multipart/form-data 类型数据不保存 73 | if request.headers.get("content-type") and "multipart/form-data" in request.headers.get("content-type"): 74 | body_params = "" 75 | else: 76 | body_params = (await request.body()).decode("utf-8", errors="ignore") 77 | response = await call_next(request) 78 | route = request.scope.get("route") 79 | if ( 80 | request.method not in settings.system.OPERATION_RECORD_METHOD 81 | or route.path in settings.system.IGNORE_OPERATION_ROUTER 82 | ): 83 | return response 84 | process_time = time.time() - start_time 85 | user_agent = parse(request.headers.get("user-agent")) 86 | system = f"{user_agent.os.family} {user_agent.os.version_string}" 87 | browser = f"{user_agent.browser.family} {user_agent.browser.version_string}" 88 | query_params = dict(request.query_params.multi_items()) 89 | path_params = request.path_params 90 | params = { 91 | "body_params": body_params, 92 | "query_params": query_params if query_params else None, 93 | "path_params": path_params if path_params else None, 94 | } 95 | content_length = response.raw_headers[0][1] 96 | assert isinstance(route, APIRoute) 97 | document = { 98 | "process_time": process_time, 99 | "request_api": request.url.__str__(), 100 | "client_ip": request.client.host, 101 | "system": system, 102 | "browser": browser, 103 | "request_method": request.method, 104 | "api_path": route.path, 105 | "summary": route.summary, 106 | "description": route.description, 107 | "tags": route.tags, 108 | "route_name": route.name, 109 | "status_code": response.status_code, 110 | "content_length": content_length, 111 | "create_datetime": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), 112 | "params": json.dumps(params), 113 | } 114 | await OperationCURD().create_data(document) 115 | return response 116 | 117 | 118 | def register_demo_env_middleware(app: FastAPI): 119 | """ 120 | 演示环境中间件 121 | """ 122 | 123 | @app.middleware("http") 124 | async def demo_env_middleware(request: Request, call_next): 125 | path = request.scope.get("path") 126 | # if request.method != "GET": 127 | # print("路由:", path, request.method) 128 | if settings.demo.DEMO_ENV and request.method != "GET": 129 | if path in settings.demo.DEMO_BLACK_LIST_PATH: 130 | return RestfulResponse.error("演示环境,禁止操作") 131 | elif path not in settings.demo.DEMO_WHITE_LIST_PATH: 132 | return RestfulResponse.error("演示环境,禁止操作") 133 | return await call_next(request) 134 | -------------------------------------------------------------------------------- /kinit_fast_task/core/register.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/03/25 18:08 3 | # @File : register.py 4 | # @IDE : PyCharm 5 | # @Desc : 功能注册 6 | import importlib 7 | import sys 8 | from contextlib import asynccontextmanager 9 | from kinit_fast_task.core.exception import refactoring_exception 10 | from kinit_fast_task.utils.tools import import_modules, import_modules_async 11 | from fastapi.middleware.cors import CORSMiddleware 12 | from kinit_fast_task.config import settings 13 | from starlette.staticfiles import StaticFiles # 依赖安装:poetry add aiofiles 14 | from fastapi import FastAPI 15 | 16 | 17 | @asynccontextmanager 18 | async def register_event(app: FastAPI): 19 | await import_modules_async(settings.system.EVENTS, "全局事件", app=app, status=True) 20 | 21 | yield 22 | 23 | await import_modules_async(settings.system.EVENTS, "全局事件", app=app, status=False) 24 | 25 | 26 | def register_middleware(app: FastAPI): 27 | """ 28 | 注册中间件 29 | """ 30 | import_modules(settings.system.MIDDLEWARES, "中间件", app=app) 31 | 32 | if settings.system.CORS_ORIGIN_ENABLE: 33 | app.add_middleware( 34 | CORSMiddleware, 35 | allow_origins=settings.system.ALLOW_ORIGINS, 36 | allow_credentials=settings.system.ALLOW_CREDENTIALS, 37 | allow_methods=settings.system.ALLOW_METHODS, 38 | allow_headers=settings.system.ALLOW_HEADERS, 39 | ) 40 | 41 | 42 | def register_exception(app: FastAPI): 43 | """ 44 | 注册异常 45 | """ 46 | refactoring_exception(app) 47 | 48 | 49 | def register_static(app: FastAPI): 50 | """ 51 | 挂载静态文件目录 52 | """ 53 | app.mount(settings.storage.LOCAL_BASE_URL, app=StaticFiles(directory=settings.storage.LOCAL_PATH)) 54 | 55 | 56 | def register_router(app: FastAPI): 57 | """ 58 | 注册路由 59 | """ 60 | sys.path.append(settings.router.APPS_PATH) 61 | for app_module_str in settings.router.APPS: 62 | module_views = importlib.import_module(f"{app_module_str}.views") 63 | app.include_router(module_views.router) 64 | 65 | 66 | def register_system_router(app: FastAPI): 67 | """ 68 | 注册系统路由 69 | """ 70 | from kinit_fast_task.app.system.docs import views as docs_views 71 | 72 | if settings.system.API_DOCS_ENABLE: 73 | docs_views.load_system_routes(app) 74 | -------------------------------------------------------------------------------- /kinit_fast_task/core/types.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2023/7/16 12:42 3 | # @File : types.py 4 | # @IDE : PyCharm 5 | # @Desc : 自定义数据类型 6 | 7 | """ 8 | 自定义数据类型 - 官方文档:https://docs.pydantic.dev/dev/concepts/types/#adding-validation-and-serialization 9 | """ 10 | 11 | from typing import Annotated, Any 12 | from pydantic import AfterValidator, PlainSerializer, WithJsonSchema 13 | from kinit_fast_task.utils.validator import ( 14 | vali_email, 15 | vali_telephone, 16 | date_str_vali, 17 | datetime_str_vali, 18 | object_id_str_vali, 19 | dict_str_vali, 20 | str_dict_vali, 21 | str_list_vali, 22 | ) 23 | import datetime 24 | 25 | 26 | # ----------------------------------------------- 27 | # 实现自定义一个日期时间字符串的数据类型 28 | # 输入类型:str | datetime.datetime | int | float | dict 29 | # 输出类型:str 30 | # ----------------------------------------------- 31 | DatetimeStr = Annotated[ 32 | str | datetime.datetime | int | float | dict, 33 | AfterValidator(datetime_str_vali), 34 | PlainSerializer(lambda x: x, return_type=str), 35 | WithJsonSchema({"type": "string"}, mode="serialization"), 36 | ] 37 | 38 | 39 | # ----------------------------------------------- 40 | # 实现自定义一个手机号验证类型 41 | # 输入类型:str 42 | # 输出类型:str 43 | # ----------------------------------------------- 44 | Telephone = Annotated[ 45 | str, 46 | AfterValidator(lambda x: vali_telephone(x)), 47 | PlainSerializer(lambda x: x, return_type=str), 48 | WithJsonSchema({"type": "string"}, mode="serialization"), 49 | ] 50 | 51 | 52 | # ----------------------------------------------- 53 | # 实现自定义一个邮箱验证类型 54 | # 输入类型:str 55 | # 输出类型:str 56 | # ----------------------------------------------- 57 | Email = Annotated[ 58 | str, 59 | AfterValidator(lambda x: vali_email(x)), 60 | PlainSerializer(lambda x: x, return_type=str), 61 | WithJsonSchema({"type": "string"}, mode="serialization"), 62 | ] 63 | 64 | 65 | # ----------------------------------------------- 66 | # 实现自定义一个日期字符串的数据类型 67 | # 输入类型:str | datetime.date | int | float 68 | # 输出类型:str 69 | # ----------------------------------------------- 70 | DateStr = Annotated[ 71 | str | datetime.date | int | float, 72 | AfterValidator(date_str_vali), 73 | PlainSerializer(lambda x: x, return_type=str), 74 | WithJsonSchema({"type": "string"}, mode="serialization"), 75 | ] 76 | 77 | 78 | # ----------------------------------------------- 79 | # 实现自定义一个ObjectId字符串的数据类型 80 | # 输入类型:str | dict | ObjectId 81 | # 输出类型:str 82 | # ----------------------------------------------- 83 | ObjectIdStr = Annotated[ 84 | Any, 85 | AfterValidator(object_id_str_vali), 86 | PlainSerializer(lambda x: x, return_type=str), 87 | WithJsonSchema({"type": "string"}, mode="serialization"), 88 | ] 89 | 90 | 91 | # ----------------------------------------------- 92 | # 实现自定义一个字典字符串的数据类型 93 | # 输入类型:str | dict 94 | # 输出类型:str 95 | # ----------------------------------------------- 96 | DictStr = Annotated[ 97 | str | dict, 98 | AfterValidator(dict_str_vali), 99 | PlainSerializer(lambda x: x, return_type=str), 100 | WithJsonSchema({"type": "string"}, mode="serialization"), 101 | ] 102 | 103 | # ----------------------------------------------- 104 | # 实现自定义一个字符串转列表的数据类型 105 | # 输入类型:str | list 106 | # 输出类型:list 107 | # ----------------------------------------------- 108 | StrList = Annotated[ 109 | str | list, 110 | AfterValidator(str_list_vali), 111 | PlainSerializer(lambda x: x, return_type=list), 112 | WithJsonSchema({"type": "list"}, mode="serialization"), 113 | ] 114 | 115 | # ----------------------------------------------- 116 | # 实现自定义一个字符串转字典的数据类型 117 | # 输入类型:str | dict 118 | # 输出类型:dict 119 | # ----------------------------------------------- 120 | StrDict = Annotated[ 121 | str | dict, 122 | AfterValidator(str_dict_vali), 123 | PlainSerializer(lambda x: x, return_type=dict), 124 | WithJsonSchema({"type": "dict"}, mode="serialization"), 125 | ] 126 | -------------------------------------------------------------------------------- /kinit_fast_task/db/__init__.py: -------------------------------------------------------------------------------- 1 | from kinit_fast_task.db.database_factory import DBFactory 2 | -------------------------------------------------------------------------------- /kinit_fast_task/db/async_base.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/04/12 22:19 3 | # @File : async.py 4 | # @IDE : PyCharm 5 | # @Desc : 数据库操作抽象类 6 | 7 | 8 | from abc import ABC, abstractmethod 9 | 10 | 11 | class AsyncAbstractDatabase(ABC): 12 | """ 13 | 数据库操作抽象类 14 | """ 15 | 16 | @abstractmethod 17 | def create_connection(self): 18 | """ 19 | 创建数据库连接 20 | 21 | :return: 22 | """ 23 | 24 | @abstractmethod 25 | def db_getter(self): 26 | """ 27 | 获取数据库连接 28 | 29 | :return: 30 | """ 31 | 32 | @abstractmethod 33 | async def db_transaction_getter(self): 34 | """ 35 | 获取数据库事务 36 | 37 | :return: 38 | """ 39 | 40 | @abstractmethod 41 | async def test_connection(self): 42 | """ 43 | 测试数据库连接 44 | 45 | :return: 46 | """ 47 | 48 | @abstractmethod 49 | async def close_connection(self): 50 | """ 51 | 关闭数据库连接 52 | 53 | :return: 54 | """ 55 | -------------------------------------------------------------------------------- /kinit_fast_task/db/database_factory.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/5/27 上午11:13 3 | # @File : database_factory.py 4 | # @IDE : PyCharm 5 | # @Desc : 数据库工厂类 6 | 7 | from kinit_fast_task.db.async_base import AsyncAbstractDatabase 8 | from kinit_fast_task.db.mongo.asyncio import MongoDatabase 9 | from kinit_fast_task.db.orm.asyncio import ORMDatabase 10 | from kinit_fast_task.db.redis.asyncio import RedisDatabase 11 | from kinit_fast_task.utils.singleton import Singleton 12 | from typing import Literal 13 | 14 | 15 | class DBFactory(metaclass=Singleton): 16 | """ 17 | 数据库工厂类,使用单例模式来管理数据库实例的创建和获取 18 | 19 | :ivar _config_loader: 存储数据库实例的字典,键为数据库类型和加载器名称的组合,值为数据库实例 20 | 21 | Methods 22 | ------- 23 | get_instance(db_type, loader_name='default', db_url=None) 24 | 获取指定类型和加载器名称的数据库实例,如果实例不存在则创建并加载到配置加载器 25 | 26 | register(loader_name, loader) 27 | 在配置加载管理器中注册一个配置加载器 28 | 29 | remove(loader_name) 30 | 从配置加载管理器中删除加载器 31 | 32 | clear() 33 | 清空配置加载管理器 34 | """ 35 | 36 | _config_loader: dict[str, AsyncAbstractDatabase] = {} 37 | 38 | @classmethod 39 | def get_instance( 40 | cls, loader_type: Literal["orm", "mongo", "redis"], *, loader_name: str = "default", db_url: str = None 41 | ) -> AsyncAbstractDatabase: 42 | """ 43 | 获取指定类型和加载器名称的数据库实例,如果实例不存在则创建并加载到配置加载器 44 | 45 | :param loader_type: 数据库类型,可选值为 "orm", "mongo", "redis" 46 | :param loader_name: 配置加载器名称,第一次创建连接成功后,存入 _config_db,存入方式默认与 db_type 拼接 47 | :param db_url: 数据库连接地址 48 | :return: 49 | """ 50 | db_key = f"{loader_type}-{loader_name}" 51 | if db_key in cls._config_loader: 52 | return cls._config_loader[db_key] 53 | 54 | if loader_type == "mongo": 55 | loader = MongoDatabase() 56 | elif loader_type == "orm": 57 | loader = ORMDatabase() 58 | elif loader_type == "redis": 59 | loader = RedisDatabase() 60 | else: 61 | raise ValueError(f"不存在的数据库类型: {loader_type}") 62 | loader.create_connection(db_url) 63 | cls.register(db_key, loader) 64 | return loader 65 | 66 | @classmethod 67 | def register(cls, loader_name: str, loader: AsyncAbstractDatabase) -> None: 68 | """ 69 | 在配置加载管理器中注册一个配置加载器 70 | 71 | :param loader_name: 配置加载器名称 72 | :param loader: 配置加载器实例 73 | :return: 74 | """ 75 | cls._config_loader[loader_name] = loader 76 | 77 | @classmethod 78 | async def remove(cls, loader_name) -> bool: 79 | """ 80 | 从配置加载管理器中删除加载器 81 | 82 | :param loader_name: 配置加载器名称 83 | :return: 删除成功返回 True 84 | """ 85 | if loader_name in cls._config_loader: 86 | loader = cls._config_loader.pop(loader_name) 87 | await loader.close_connection() 88 | 89 | return True 90 | 91 | @classmethod 92 | async def clear(cls) -> bool: 93 | """ 94 | 清空加载器 95 | 96 | :return: 清空成功返回 True 97 | """ 98 | for loader in cls._config_loader.values(): 99 | await loader.close_connection() 100 | 101 | cls._config_loader.clear() 102 | return True 103 | -------------------------------------------------------------------------------- /kinit_fast_task/db/mongo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvandk/kinit-fast-task/1c01a44517344f85eb7a0edad122958eca0dda81/kinit_fast_task/db/mongo/__init__.py -------------------------------------------------------------------------------- /kinit_fast_task/db/mongo/asyncio.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/4/1 13:58 3 | # @File : asyncio.py 4 | # @IDE : PyCharm 5 | # @Desc : mongodb 6 | from urllib.parse import urlparse, parse_qs 7 | 8 | from kinit_fast_task.db.async_base import AsyncAbstractDatabase 9 | from kinit_fast_task.config import settings 10 | from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase, AsyncIOMotorClientSession 11 | from kinit_fast_task.core import CustomException 12 | from pymongo.errors import ServerSelectionTimeoutError 13 | from kinit_fast_task.utils import log 14 | 15 | 16 | class MongoDatabase(AsyncAbstractDatabase): 17 | """ 18 | 安装:poetry add motor 19 | 官方文档:https://motor.readthedocs.io/en/stable/index.html 20 | 官方文档:https://motor.readthedocs.io/en/stable/tutorial-asyncio.html# 21 | 22 | Motor 中使用事务 23 | Motor与MongoDB事务一起使用需要MongoDB 4.0及以上版本,并且数据库需要配置为复制集(replica set)模式 24 | 25 | MongoDB 复制集 26 | MongoDB的复制集(replica set)模式是一种高可用性和数据冗余解决方案 27 | 复制集由一组MongoDB实例组成,这些实例维护相同的数据集副本,提供数据冗余和故障转移能力 28 | 复制集至少包含一个主节点(primary)和一个或多个从节点(secondary) 29 | 在复制集中,主节点负责处理所有写操作,而从节点则从主节点复制数据并保持数据的冗余 30 | """ 31 | 32 | def __init__(self): 33 | """ 34 | 实例化 MongoDB 数据库连接 35 | """ 36 | self._engine: AsyncIOMotorClient | None = None 37 | self._db: AsyncIOMotorDatabase | None = None 38 | 39 | def create_connection(self, db_url: str = None) -> None: 40 | """ 41 | 创建数据库连接 42 | 43 | :return: 44 | """ 45 | # maxPoolSize:连接池中的最大连接数。这决定了最多可以有多少个活跃的连接同时存在。 46 | # minPoolSize:连接池中的最小连接数。即使在空闲时,连接池也会尝试保持这么多的连接打开。 47 | # maxIdleTimeMS:连接可以保持空闲状态的最大毫秒数,之后连接会被关闭。这有助于避免保持长时间不使用的连接。 48 | # serverSelectionTimeoutMS 参数设置了服务器选择的超时时间(以毫秒为单位),在这里设置为 5000 毫秒(5 秒) 49 | if not db_url: 50 | db_url = settings.db.MONGO_DB_URL.unicode_string() 51 | self._engine = AsyncIOMotorClient( 52 | db_url, maxPoolSize=10, minPoolSize=2, maxIdleTimeMS=300000, serverSelectionTimeoutMS=5000 53 | ) 54 | 55 | # 从数据库链接中提取数据库名称 56 | parsed_url = urlparse(db_url) 57 | query_params = parse_qs(parsed_url.query) 58 | db_name = query_params.get("authSource")[0] 59 | 60 | self._db = self._engine[db_name] 61 | if self._db is None: 62 | raise CustomException("MongoDB 数据库连接失败!") 63 | 64 | async def db_transaction_getter(self) -> AsyncIOMotorClientSession: 65 | """ 66 | 获取数据库事务 67 | 68 | :return: 69 | """ 70 | async with await self._engine.start_session() as session: 71 | async with session.start_transaction(): 72 | yield session 73 | 74 | def db_getter(self) -> AsyncIOMotorDatabase: 75 | """ 76 | 获取数据库连接 77 | 78 | :return: 79 | """ 80 | if not self._engine: 81 | raise CustomException("未连接 MongoDB 数据库!") 82 | if self._db is None: 83 | raise CustomException("MongoDB 数据库连接失败!") 84 | return self._db 85 | 86 | async def test_connection(self) -> None: 87 | """ 88 | 测试数据库连接 89 | 90 | :return: 91 | """ 92 | db = self.db_getter() 93 | try: 94 | # 发送 ping 命令以测试连接 95 | result = await db.command("ping") 96 | log.info(f"MongoDB Ping successful: {result}") 97 | except ServerSelectionTimeoutError as e: 98 | log.error(f"MongoDB Server Connection Timed Out, content: {e}") 99 | raise 100 | 101 | async def close_connection(self) -> None: 102 | """ 103 | 关闭连接池 104 | 105 | :return: 106 | """ 107 | if self._engine: 108 | self._engine.close() 109 | -------------------------------------------------------------------------------- /kinit_fast_task/db/orm/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvandk/kinit-fast-task/1c01a44517344f85eb7a0edad122958eca0dda81/kinit_fast_task/db/orm/__init__.py -------------------------------------------------------------------------------- /kinit_fast_task/db/orm/async_base_model.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/4/16 15:34 3 | # @File : base_model.py 4 | # @IDE : PyCharm 5 | # @Desc : 文件描述信息 6 | 7 | from sqlalchemy.ext.asyncio import AsyncAttrs 8 | from sqlalchemy.orm import DeclarativeBase, declared_attr 9 | 10 | 11 | class AsyncBaseORMModel(AsyncAttrs, DeclarativeBase): 12 | """ 13 | 创建声明式基本映射类 14 | 稍后,我们将继承该类,创建每个 ORM 模型 15 | """ 16 | 17 | @declared_attr.directive 18 | def __tablename__(cls) -> str: 19 | """ 20 | 将表名改为小写 21 | 如果有自定义表名就取自定义,没有就取小写类名 22 | """ 23 | table_name = cls.__tablename__ 24 | if not table_name: 25 | model_name = cls.__name__ 26 | ls = [] 27 | for index, char in enumerate(model_name): 28 | if char.isupper() and index != 0: 29 | ls.append("_") 30 | ls.append(char) 31 | table_name = "".join(ls).lower() 32 | return table_name 33 | -------------------------------------------------------------------------------- /kinit_fast_task/db/orm/asyncio.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/4/1 13:58 3 | # @File : async_base.py 4 | # @IDE : PyCharm 5 | # @Desc : SQLAlchemy ORM 会话管理 6 | 7 | from collections.abc import AsyncGenerator 8 | from sqlalchemy import text, QueuePool 9 | 10 | from kinit_fast_task.core import CustomException 11 | from kinit_fast_task.db.async_base import AsyncAbstractDatabase 12 | from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker, AsyncEngine 13 | from kinit_fast_task.config import settings 14 | from kinit_fast_task.utils import log 15 | 16 | 17 | class ORMDatabase(AsyncAbstractDatabase): 18 | """ 19 | SQLAlchemy ORM 连接 会话管理 20 | 安装: poetry add sqlalchemy[asyncio] 21 | 官方文档:https://docs.sqlalchemy.org/en/20/intro.html#installation 22 | """ 23 | 24 | def __init__(self): 25 | """ 26 | 实例化 SQLAlchemy ORM 数据库连接 27 | """ 28 | self._engine: AsyncEngine | None = None 29 | self._session_factory: async_sessionmaker[AsyncSession] | None = None 30 | 31 | def create_connection(self, db_url: str = None) -> None: 32 | """ 33 | 创建数据库引擎与会话工厂 34 | 35 | :param db_url: 数据库连接 36 | :return: 37 | """ 38 | """ 39 | # 创建数据库引擎与连接池并初始化相关的数据库连接信息。 40 | # 这个引擎对象会负责管理连接池中的连接资源,包括从池中获取连接、执行 SQL 命令、处理事务、连接回收等操作。 41 | # echo=False:当设置为 True 时,引擎将会在控制台输出所有生成的 SQL 语句 42 | # echo_pool=False:当设置为 True 时,连接池相关的操作(如连接的获取和释放)也会被输出到控制台。 43 | # pool_pre_ping=False:如果设置为 True,每次从连接池获取连接前都会执行一个轻量级的 SQL 命令(如 SELECT 1),以检查连接是否仍然有效。 44 | # pool_recycle=-1:此设置导致池在给定的秒数后重新使用连接。默认为-1,即没有超时。例如,将其设置为3600意味着在一小时后重新使用连接。 45 | # pool_size=5:在连接池内保持打开的连接数。pool_size设置为0表示没有限制 46 | # max_overflow 参数用于配置连接池中允许的连接 "溢出" 数量。这个参数用于在高负载情况下处理连接请求的峰值。 47 | # 当连接池的所有连接都在使用中时,如果有新的连接请求到达,连接池可以创建额外的连接来满足这些请求,最多创建的数量由 max_overflow 参数决定。 48 | """ # noqa E501 49 | if not db_url: 50 | db_url = settings.db.ORM_DATABASE_URL.unicode_string() 51 | self._engine = create_async_engine( 52 | db_url, 53 | echo=settings.db.ORM_DB_ECHO, 54 | echo_pool=False, 55 | pool_pre_ping=True, 56 | pool_recycle=3, 57 | pool_size=5, 58 | max_overflow=2, 59 | connect_args={}, 60 | ) 61 | 62 | self._session_factory = async_sessionmaker( 63 | autocommit=False, autoflush=False, bind=self._engine, expire_on_commit=True, class_=AsyncSession 64 | ) 65 | 66 | def get_pool_status(self): 67 | """ 68 | 获取当前连接池状态 69 | """ 70 | pool = self._engine.pool 71 | if isinstance(pool, QueuePool): 72 | status = { 73 | "checked_out": pool.checkedout(), # 当前使用中的连接数 74 | "overflow": pool._overflow, # 当前溢出的连接数 75 | "pool_size": pool.size(), # 连接池大小 76 | "checked_in": pool.size() - pool.checkedout(), # 空闲连接数 77 | } 78 | print("\n=======================BEGIN=======================") 79 | print(f"当前使用中的连接数: {status['checked_out']}") 80 | print(f"连接池大小: {status['pool_size']}") 81 | print(f"当前溢出的连接数: {status['overflow']}") 82 | print(f"空闲连接数: {status['checked_in']}") 83 | print("========================EOF========================") 84 | return status 85 | else: 86 | raise TypeError("Pool is not a QueuePool instance") 87 | 88 | async def db_transaction_getter(self) -> AsyncGenerator[AsyncSession, None]: 89 | """ 90 | 从数据库会话工厂中获取数据库事务,它将在单个请求中使用,最后在请求完成后将其关闭 91 | 92 | 函数的返回类型被注解为 AsyncGenerator[AsyncSession, None] 93 | 其中 AsyncSession 是生成的值的类型,而 None 表示异步生成器没有终止条件。 94 | 95 | :return: 96 | """ # noqa E501 97 | if not self._engine or not self._session_factory: 98 | raise CustomException("未连接 SQLAlchemy ORM 数据库!") 99 | async with self._session_factory() as session: 100 | """ 101 | 开始一个新的事务,这个事务将在该异步块结束时自动关闭。 102 | 如果块中的代码正常执行(没有引发异常),事务会在离开该异步上下文管理器时自动提交,也就是自动执行 commit。 103 | 如果块中的代码引发异常,事务会自动回滚。 104 | 因此,在这个上下文管理器中,你不需要手动调用 commit 方法;事务的提交或回滚将根据执行过程中是否发生异常自动进行。 105 | 这是 session.begin() 上下文管理器的一个优点,因为它简化了事务的管理,确保事务能在适当的时机正确提交或回滚。 106 | """ # noqa E501 107 | async with session.begin(): 108 | yield session 109 | 110 | def db_getter(self) -> AsyncSession: 111 | """ 112 | 获取数据库 session 113 | 114 | :return: 115 | """ 116 | if not self._engine or not self._session_factory: 117 | raise CustomException("未连接 SQLAlchemy ORM 数据库!") 118 | return self._session_factory() 119 | 120 | async def test_connection(self) -> None: 121 | """ 122 | 测试数据库连接 123 | 124 | :return: 125 | """ 126 | session = self.db_getter() 127 | try: 128 | result = await session.execute(text("SELECT now();")) 129 | log.info(f"ORM DB Ping successful: {result.scalar()}") 130 | except Exception as e: 131 | log.error(f"ORM DB Connection Fail, content: {e}") 132 | raise 133 | finally: 134 | await session.close() 135 | 136 | async def close_connection(self) -> None: 137 | """ 138 | 关闭数据库引擎 139 | 140 | :return: 141 | """ 142 | if self._engine: 143 | await self._engine.dispose() 144 | -------------------------------------------------------------------------------- /kinit_fast_task/db/redis/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvandk/kinit-fast-task/1c01a44517344f85eb7a0edad122958eca0dda81/kinit_fast_task/db/redis/__init__.py -------------------------------------------------------------------------------- /kinit_fast_task/db/redis/asyncio.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/4/1 13:58 3 | # @File : asyncio.py 4 | # @IDE : PyCharm 5 | # @Desc : mongodb 6 | 7 | 8 | from kinit_fast_task.db.async_base import AsyncAbstractDatabase 9 | from kinit_fast_task.config import settings 10 | import redis.asyncio as redis 11 | from kinit_fast_task.core import CustomException 12 | from kinit_fast_task.utils import log 13 | 14 | 15 | class RedisDatabase(AsyncAbstractDatabase): 16 | """ 17 | 安装: poetry add redis 18 | GitHub: https://github.com/redis/redis-py 19 | 异步连接官方文档:https://redis.readthedocs.io/en/stable/examples/asyncio_examples.html 20 | """ 21 | 22 | def __init__(self): 23 | """ 24 | 实例化 Redis 数据库连接 25 | """ 26 | self._engine: redis.ConnectionPool | None = None 27 | 28 | def create_connection(self, db_url: str = None) -> None: 29 | """ 30 | 创建 redis 数据库连接 31 | 32 | 官方文档:https://redis.readthedocs.io/en/stable/connections.html#connectionpool-async 33 | :return: 34 | """ 35 | # 创建一个异步连接池 36 | # decode_responses: 自动解码响应为字符串 37 | # protocol: 指定使用 RESP3 协议 38 | # max_connections: 最大连接数 39 | if not db_url: 40 | db_url = settings.db.REDIS_DB_URL.unicode_string() 41 | self._engine = redis.ConnectionPool.from_url( 42 | db_url, encoding="utf-8", decode_responses=True, protocol=3, max_connections=10 43 | ) 44 | 45 | def db_getter(self) -> redis.Redis: 46 | """ 47 | 返回一个从连接池获取的 Redis 客户端 48 | :return: 49 | """ 50 | if not self._engine: 51 | raise CustomException("未连接 Redis 数据库!") 52 | return redis.Redis(connection_pool=self._engine) 53 | 54 | async def db_transaction_getter(self): 55 | """ 56 | 获取数据库事务 57 | 58 | :return: 59 | """ 60 | raise NotImplementedError("未实现 Redis 事务功能!") 61 | 62 | async def test_connection(self) -> None: 63 | """ 64 | 测试数据库连接 65 | :return: 66 | """ 67 | rd = self.db_getter() 68 | try: 69 | result = await rd.ping() 70 | log.info(f"Redis Ping successful: {result}") 71 | except Exception as e: 72 | log.error(f"Redis Server Connection Fail, content: {e}") 73 | raise 74 | 75 | async def close_connection(self) -> None: 76 | """ 77 | 关闭 redis 连接池 78 | :return: 79 | """ 80 | if not self._engine: 81 | return None 82 | await self._engine.aclose() 83 | -------------------------------------------------------------------------------- /kinit_fast_task/main.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/3/24 19:55 3 | # @File : main.py 4 | # @IDE : PyCharm 5 | # @Desc : 项目启动 6 | from rich.padding import Padding 7 | from rich.panel import Panel 8 | from rich import print 9 | 10 | from kinit_fast_task.config import settings 11 | from kinit_fast_task.core.register import ( 12 | register_middleware, 13 | register_static, 14 | register_router, 15 | register_system_router, 16 | register_exception, 17 | register_event, 18 | ) 19 | from fastapi import FastAPI 20 | 21 | from kinit_fast_task.utils.response import ErrorResponseSchema 22 | 23 | import logging 24 | 25 | # 关闭 Uvicorn HTTP 请求日志记录 26 | uvicorn_access_logger = logging.getLogger("uvicorn.access") 27 | uvicorn_access_logger.handlers = [] 28 | uvicorn_access_logger.propagate = False 29 | 30 | 31 | # uvicorn 主日志处理器 32 | uvicorn_logger = logging.getLogger("uvicorn") 33 | 34 | 35 | def create_app(): 36 | """ 37 | 启动项目 38 | docs_url:配置交互文档的路由地址,如果禁用则为None,默认为 /docs 39 | redoc_url: 配置 Redoc 文档的路由地址,如果禁用则为None,默认为 /redoc 40 | openapi_url:配置接口文件json数据文件路由地址,如果禁用则为None,默认为/openapi.json 41 | """ 42 | 43 | description = """ 44 | 本项目基于Python社区FastAPI技术栈编写而成,本意为所有需要使用FastAPI开发的人提供一个合适的脚手架,避免重复开发。 45 | 46 | 在项目中也融合了很多FastAPI技术栈可以参考使用。 47 | """ # noqa E501 48 | host = str(settings.system.SERVER_HOST) 49 | port = settings.system.SERVER_PORT 50 | server_address = f"http://{'127.0.0.1' if host == '0.0.0.0' else host}:{port}" 51 | 52 | serving_str = f"[dim]API Server URL:[/dim] [link]http://{host}:{port}[/link]" 53 | if settings.system.API_DOCS_ENABLE: 54 | serving_str += f"\n\n[dim]Swagger UI Docs:[/dim] [link]{server_address}/docs[/link]" 55 | serving_str += f"\n\n[dim]Redoc HTML Docs:[/dim] [link]{server_address}/redoc[/link]" 56 | else: 57 | serving_str += "\n\n[dim]Swagger UI Docs:[/dim] not enabled" 58 | serving_str += "\n\n[dim]Redoc HTML Docs:[/dim] not enabled" 59 | 60 | # 踩坑1:rich Panel 使用中文会导致边框对不齐的情况 61 | panel = Panel( 62 | serving_str, 63 | title=f"{settings.system.PROJECT_NAME}", 64 | expand=False, 65 | padding=(1, 2), 66 | style="black on yellow", 67 | ) 68 | print(Padding(panel, 1)) 69 | 70 | # 异常 Response 定义 71 | responses = {400: {"model": ErrorResponseSchema, "description": "请求失败"}} 72 | 73 | _app = FastAPI( 74 | title="KINIT FAST TASK", 75 | description=description, 76 | lifespan=register_event, 77 | docs_url=None, 78 | redoc_url=None, 79 | responses=responses, 80 | ) 81 | 82 | # 全局异常捕捉处理 83 | register_exception(_app) 84 | 85 | # 注册中间件 86 | register_middleware(_app) 87 | 88 | # 挂在静态目录 89 | register_static(_app) 90 | 91 | # 引入应用中的路由 92 | register_router(_app) 93 | 94 | # 加载系统路由 95 | register_system_router(_app) 96 | 97 | uvicorn_logger.info(f"Load API Number:{len(_app.routes)}, Custom Add Number:{len(_app.routes) - 5}") 98 | uvicorn_logger.info(f"Load APPS ({len(settings.router.APPS)}):{settings.router.APPS}") 99 | uvicorn_logger.info(f"Load APPS Path:{settings.router.APPS_PATH}") 100 | 101 | return _app 102 | 103 | 104 | app = create_app() 105 | -------------------------------------------------------------------------------- /kinit_fast_task/scripts/__init__.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/5/16 上午10:23 3 | # @File : __init__.py.py 4 | # @IDE : PyCharm 5 | # @Desc : 文件描述信息 6 | -------------------------------------------------------------------------------- /kinit_fast_task/scripts/app_generate/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvandk/kinit-fast-task/1c01a44517344f85eb7a0edad122958eca0dda81/kinit_fast_task/scripts/app_generate/__init__.py -------------------------------------------------------------------------------- /kinit_fast_task/scripts/app_generate/main.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import json 3 | from typing import Literal 4 | 5 | from kinit_fast_task.app.models.base.orm import AbstractORMModel 6 | from kinit_fast_task.config import settings 7 | from kinit_fast_task.scripts.app_generate.v1.model_to_json import ModelToJson as ModelToJsonV1 8 | from kinit_fast_task.scripts.app_generate.v1.generate_code import GenerateCode as GenerateCodeV1 9 | from kinit_fast_task.utils import log 10 | 11 | VERSIONS = Literal["1.0"] 12 | 13 | 14 | class AppGenerate: 15 | """ 16 | 生成 APP 代码: 17 | 18 | 1. 基于 ORM Model 生成 Json 配置文件 19 | 2. 基于 JSON 配置文件生成代码 20 | 3. 基于 ORM Model 生成代码(先生成 JSON 配置后基于 JSON 生成代码) 21 | """ 22 | 23 | SCRIPT_PATH = settings.BASE_PATH / "scripts" / "app_generate" 24 | 25 | def __init__(self, verbose: bool = False): 26 | """ 27 | 初始化 AppGenerate 类 28 | """ 29 | self.verbose = verbose 30 | self.task_log = log.create_task(verbose=verbose) 31 | 32 | @staticmethod 33 | def model_mapping(model_class_name: str) -> type[AbstractORMModel]: 34 | """ 35 | 映射 ORM Model 36 | 37 | :return: ORM Model 类 38 | """ 39 | module = importlib.import_module(f"{settings.system.PROJECT_NAME}.app.models") 40 | return getattr(module, model_class_name) 41 | 42 | @staticmethod 43 | def write_json(filename: str, data: dict) -> None: 44 | """ 45 | 写入 json 文件 46 | 47 | :param filename: 48 | :param data: 49 | :return: 50 | """ 51 | if filename is not None: 52 | with open(filename, "w", encoding="utf-8") as json_file: 53 | json.dump(data, json_file, ensure_ascii=False, indent=4) 54 | 55 | @staticmethod 56 | def read_json(filename: str) -> dict: 57 | """ 58 | 读取 json 文件 59 | 60 | :param filename: 61 | :return: 62 | """ 63 | with open(filename, encoding="utf-8") as json_file: 64 | json_config = json.load(json_file) 65 | return json_config 66 | 67 | def model_to_json( 68 | self, *, model_class_name: str, app_name: str, app_desc: str, version: VERSIONS = "1.0", filename: str = None 69 | ) -> dict: 70 | """ 71 | 基于单个 model 输出 JSON 配置文件 72 | 73 | :param model_class_name: Model 类名, 示例:AuthUserModel 74 | :param app_name: app 名称, 示例:auth_user 75 | :param app_desc: app 描述, 示例:AuthUserModel 76 | :param version: json 版本 77 | :param filename: 文件名称, 为 None 则不写入文件 78 | :return: dict 79 | """ 80 | model = self.model_mapping(model_class_name) 81 | if version == "1.0": 82 | mtj = ModelToJsonV1(model, app_name, app_desc) 83 | else: 84 | raise NotImplementedError(f"version {version} not implemented") 85 | 86 | json_config = mtj.to_json() 87 | 88 | if filename: 89 | self.write_json(filename, json_config) 90 | 91 | return json_config 92 | 93 | def json_to_code( 94 | self, *, json_config_file: str = None, json_config: dict = None, is_write: bool = False, overwrite: bool = False 95 | ) -> None: 96 | """ 97 | 基于 JSON 配置文件生成代码 98 | 99 | :param json_config_file: json 配置文件地址 100 | :param json_config: json 配置 101 | :param is_write: 是否将生成结果直接写入文件 102 | :param overwrite: 是否在写入时覆盖文件 103 | """ 104 | if json_config_file: 105 | self.task_log.info("基于 JSON 配置生成代码, 配置文件:", json_config_file, is_verbose=True) 106 | json_config = self.read_json(json_config_file) 107 | 108 | version = json_config["version"] 109 | self.task_log.info("基于 JSON 配置生成代码, 版本:", version, is_verbose=True) 110 | 111 | if version == "1.0": 112 | gc = GenerateCodeV1(json_config, task_log=self.task_log) 113 | else: 114 | raise NotImplementedError(f"version {version} not implemented") 115 | 116 | if is_write: 117 | self.task_log.info("基于 JSON 配置生成代码, 开始生成, 并将生成结果直接写入文件") 118 | gc.write_generate(overwrite=overwrite) 119 | else: 120 | self.task_log.info("基于 JSON 配置生成代码, 开始生成, 只输出代码, 不写入文件") 121 | gc.generate() 122 | self.task_log.success("基于 JSON 配置生成代码, 执行成功", is_verbose=True) 123 | if is_write: 124 | self.task_log.info("推荐执行代码格式化命令:") 125 | self.task_log.info("1. ruff format") 126 | self.task_log.info("2. ruff check --fix") 127 | self.task_log.info("如若使用还需进行以下两步操作:") 128 | migrate_command = "python main.py migrate" 129 | model_class = gc.json_config.model.class_name 130 | self.task_log.info( 131 | f"1. 请确认 {model_class} 数据表已完成迁移至数据库, 若还没迁移, 可执行:{migrate_command} 迁移命令!" 132 | ) 133 | self.task_log.info(f"2. 请确认在 config.py:RouterSettings.APPS 配置中添加 {gc.json_config.app_name} 路由!") 134 | self.task_log.end() 135 | 136 | def model_to_code( 137 | self, *, model_class_name: str, app_name: str, app_desc: str, write_only: bool = False, overwrite: bool = False 138 | ) -> None: 139 | """ 140 | 基于单个 model 生成代码 141 | 142 | :param model_class_name: Model 类名, 示例:AuthUserModel 143 | :param app_name: app 名称, 示例:auth_user 144 | :param app_desc: app 描述, 示例:AuthUserModel 145 | :param write_only: 是否只写入文件 146 | :param overwrite: 是否在写入时覆盖文件 147 | """ 148 | 149 | json_config = self.model_to_json( 150 | model_class_name=model_class_name, app_name=app_name, app_desc=app_desc, version="1.0" 151 | ) 152 | self.json_to_code(json_config=json_config, is_write=write_only, overwrite=overwrite) 153 | 154 | 155 | if __name__ == "__main__": 156 | app = AppGenerate(verbose=False) 157 | 158 | # config = app.model_to_json( 159 | # model_class_name="AuthRoleModel", 160 | # app_name="auth_role", 161 | # app_desc="角色", 162 | # filename="role_data.json" 163 | # ) 164 | # print(json.dumps(config, indent=4, ensure_ascii=False)) 165 | 166 | # app.json_to_code(json_config_file="role_data.json", is_write=True, overwrite=False) 167 | 168 | app.model_to_code( 169 | model_class_name="AuthTestModel", app_name="auth_test", app_desc="测试", write_only=True, overwrite=True 170 | ) 171 | -------------------------------------------------------------------------------- /kinit_fast_task/scripts/app_generate/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvandk/kinit-fast-task/1c01a44517344f85eb7a0edad122958eca0dda81/kinit_fast_task/scripts/app_generate/utils/__init__.py -------------------------------------------------------------------------------- /kinit_fast_task/scripts/app_generate/utils/generate_base.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import re 3 | from pathlib import Path 4 | 5 | 6 | class GenerateBase: 7 | @staticmethod 8 | def create_pag(pag_path: str | Path): 9 | """ 10 | 创建包 11 | 12 | :param pag_path: 13 | :return: 14 | """ 15 | if isinstance(pag_path, str): 16 | pag_path = Path(pag_path) 17 | 18 | # 创建目录,如果不存在 19 | pag_path.mkdir(parents=True, exist_ok=True) 20 | 21 | # 在目录中创建 __init__.py 文件 22 | init_file = pag_path / "__init__.py" 23 | init_file.touch(exist_ok=True) 24 | 25 | @staticmethod 26 | def camel_to_snake(name: str) -> str: 27 | """ 28 | 将大驼峰命名(CamelCase)转换为下划线命名(snake_case) 29 | 在大写字母前添加一个空格,然后将字符串分割并用下划线拼接 30 | :param name: 大驼峰命名(CamelCase) 31 | :return: 32 | """ 33 | s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name) 34 | return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower() 35 | 36 | @staticmethod 37 | def snake_to_camel(name: str) -> str: 38 | """ 39 | 将下划线命名(snake_case)转换为大驼峰命名(CamelCase) 40 | 根据下划线分割,然后将字符串转为第一个字符大写后拼接 41 | :param name: 下划线命名(snake_case) 42 | :return: 43 | """ 44 | # 按下划线分割字符串 45 | words = name.split("_") 46 | # 将每个单词的首字母大写,然后拼接 47 | return "".join(word.capitalize() for word in words) 48 | 49 | @staticmethod 50 | def generate_file_desc(filename: str, version: str = "1.0", desc: str = "") -> str: 51 | """ 52 | 生成文件注释 53 | :param filename: 54 | :param version: 55 | :param desc: 56 | :return: 57 | """ 58 | code = f"# @Version : {version}" 59 | code += f"\n# @Create Time : {datetime.datetime.now().strftime('%Y/%m/%d')}" 60 | code += f"\n# @File : {filename}" 61 | code += "\n# @IDE : PyCharm" 62 | code += f"\n# @Desc : {desc}" 63 | code += "\n" 64 | return code 65 | 66 | @staticmethod 67 | def generate_modules_code(modules: dict[str, list]) -> str: 68 | """ 69 | 生成模块导入代码 70 | :param modules: 导入得模块 71 | :return: 72 | """ 73 | code = "\n" 74 | args = modules.pop("args", []) 75 | for k, v in modules.items(): 76 | code += f"from {k} import {', '.join(v)}\n" 77 | if args: 78 | code += f"import {', '.join(args)}\n" 79 | return code 80 | 81 | @staticmethod 82 | def update_init_file(init_file: Path, code: str): 83 | """ 84 | __init__ 文件添加导入内容 85 | :param init_file: 86 | :param code: 87 | :return: 88 | """ 89 | content = init_file.read_text() 90 | if content and code in content: 91 | return 92 | if content: 93 | if content.endswith("\n"): 94 | with init_file.open("a+", encoding="utf-8") as f: 95 | f.write(f"{code}\n") 96 | else: 97 | with init_file.open("a+", encoding="utf-8") as f: 98 | f.write(f"\n{code}\n") 99 | else: 100 | init_file.write_text(f"{code}\n", encoding="utf-8") 101 | 102 | @staticmethod 103 | def module_code_to_dict(code: str) -> dict: 104 | """ 105 | 将 from import 语句代码转为 dict 格式 106 | :param code: 107 | :return: 108 | """ 109 | # 分解代码为单行 110 | lines = code.strip().split("\n") 111 | 112 | # 初始化字典 113 | modules = {} 114 | 115 | # 遍历每行代码 116 | for line in lines: 117 | # 处理 'from ... import ...' 类型的导入 118 | if line.startswith("from"): 119 | parts = line.split(" import ") 120 | module = parts[0][5:] # 移除 'from ' 并获取模块路径 121 | imports = parts[1].split(",") # 使用逗号分割导入项 122 | imports = [item.strip() for item in imports] # 移除多余空格 123 | if module in modules: 124 | modules[module].extend(imports) 125 | else: 126 | modules[module] = imports 127 | 128 | # 处理 'import ...' 类型的导入 129 | elif line.startswith("import"): 130 | imports = line.split("import ")[1] 131 | # 分割多个导入项 132 | imports = imports.split(", ") 133 | for imp in imports: 134 | # 处理直接导入的模块 135 | modules.setdefault("args", []).append(imp) 136 | return modules 137 | 138 | @classmethod 139 | def file_code_split_module(cls, file: Path) -> list: 140 | """ 141 | 文件代码内容拆分,分为以下三部分 142 | 1. 文件开头的注释。 143 | 2. 全局层面的from import语句。该代码格式会被转换为 dict 格式 144 | 3. 其他代码内容。 145 | :param file: 146 | :return: 147 | """ 148 | content = file.read_text(encoding="utf-8") 149 | if not content: 150 | return [] 151 | lines = content.split("\n") 152 | part1 = [] # 文件开头注释 153 | part2 = [] # from import 语句 154 | part3 = [] # 其他代码内容 155 | 156 | # 标记是否已超过注释部分 157 | past_comments = False 158 | 159 | for line in lines: 160 | # 检查是否为注释行 161 | if line.startswith("#") and not past_comments: 162 | part1.append(line) 163 | else: 164 | # 标记已超过注释部分 165 | past_comments = True 166 | # 检查是否为 from import 语句 167 | if line.startswith("from ") or line.startswith("import "): 168 | part2.append(line) 169 | else: 170 | part3.append(line) 171 | 172 | part2 = cls.module_code_to_dict("\n".join(part2)) 173 | 174 | return ["\n".join(part1), part2, "\n".join(part3)] 175 | 176 | @staticmethod 177 | def merge_dictionaries(dict1, dict2): 178 | """ 179 | 合并两个键为字符串、值为列表的字典 180 | :param dict1: 181 | :param dict2: 182 | :return: 183 | """ 184 | # 初始化结果字典 185 | merged_dict = {} 186 | 187 | # 合并两个字典中的键值对 188 | for key in set(dict1) | set(dict2): # 获取两个字典的键的并集 189 | merged_dict[key] = list(set(dict1.get(key, []) + dict2.get(key, []))) 190 | 191 | return merged_dict 192 | -------------------------------------------------------------------------------- /kinit_fast_task/scripts/app_generate/utils/model_to_json_base.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/6/7 下午1:45 3 | # @File : model_to_json.py 4 | # @IDE : PyCharm 5 | # @Desc : model to json 6 | 7 | from sqlalchemy import inspect as sa_inspect, Column 8 | from sqlalchemy.sql.schema import ScalarElementColumnDefault 9 | 10 | from kinit_fast_task.app.models.base.orm import AbstractORMModel 11 | from kinit_fast_task.scripts.app_generate.utils import schema_base 12 | 13 | 14 | class ModelToJsonBase: 15 | def __init__(self, model: type(AbstractORMModel), app_name: str, app_desc: str): 16 | """ 17 | 基于单个 model 输出 JSON 配置文件 18 | 19 | :param model: Model 类 20 | :param app_name: app 名称, 示例:auth_user 21 | :param app_desc: app 描述, 示例:AuthUserModel 22 | """ 23 | self.model = model 24 | self.app_name = app_name 25 | self.app_desc = app_desc 26 | 27 | def parse_model_fields(self) -> list[schema_base.ModelFieldSchema]: 28 | """ 29 | 解析模型字段 30 | 31 | :return: 32 | """ 33 | fields = [] 34 | mapper = sa_inspect(self.model) 35 | for column in mapper.columns: 36 | assert isinstance(column, Column) 37 | default = column.default 38 | if default is not None: 39 | assert isinstance(default, ScalarElementColumnDefault) 40 | default = default.arg 41 | field_type = type(column.type).__name__ 42 | params = column.type.__dict__ 43 | if field_type == "String": 44 | keys = ["length"] 45 | elif field_type == "DECIMAL": 46 | keys = ["precision", "scale"] 47 | else: 48 | keys = [] 49 | field_kwargs = {key: params[key] for key in keys} 50 | item = schema_base.ModelFieldSchema( 51 | name=column.name, 52 | field_type=field_type, 53 | field_python_type=column.type.python_type.__name__, 54 | field_kwargs=field_kwargs, 55 | nullable=column.nullable, 56 | default=default, 57 | comment=column.comment, 58 | ) 59 | fields.append(item) 60 | return fields 61 | -------------------------------------------------------------------------------- /kinit_fast_task/scripts/app_generate/utils/schema_base.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/6/14 下午5:12 3 | # @File : json_config_schema.py 4 | # @IDE : PyCharm 5 | # @Desc : 文件描述信息 6 | from typing import Any 7 | from pydantic import BaseModel, Field 8 | 9 | 10 | class ModelFieldSchema(BaseModel): 11 | name: str = Field(..., description="字段名称") 12 | field_type: str = Field(..., description="字段类型(SQLAlchemy 类型)") 13 | field_python_type: str = Field(..., description="字段类型(Python 类型)") 14 | field_kwargs: dict = Field(..., description="字段属性") 15 | nullable: bool = Field(False, description="是否可以为空") 16 | default: Any = Field(None, description="默认值") 17 | comment: str | None = Field(None, description="字段描述") 18 | -------------------------------------------------------------------------------- /kinit_fast_task/scripts/app_generate/v1/__init__.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/6/25 3 | # @File : __init__.py.py 4 | # @IDE : PyCharm 5 | # @Desc : 文件描述信息 6 | -------------------------------------------------------------------------------- /kinit_fast_task/scripts/app_generate/v1/generate_code.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/6/14 下午6:58 3 | # @File : generate_code.py 4 | # @IDE : PyCharm 5 | # @Desc : 文件描述信息 6 | 7 | from kinit_fast_task.scripts.app_generate.v1.json_config_schema import JSONConfigSchema 8 | from kinit_fast_task.scripts.app_generate.v1.generate_schema import SchemaGenerate 9 | from kinit_fast_task.scripts.app_generate.v1.generate_crud import CrudGenerate 10 | from kinit_fast_task.scripts.app_generate.v1.generate_params import ParamsGenerate 11 | from kinit_fast_task.scripts.app_generate.v1.generate_view import ViewGenerate 12 | from kinit_fast_task.utils.logger import TaskLogger 13 | 14 | 15 | class GenerateCode: 16 | def __init__(self, json_config: dict, task_log: TaskLogger): 17 | self.json_config = JSONConfigSchema(**json_config) 18 | self.task_log = task_log 19 | 20 | def generate(self): 21 | """ 22 | 代码生成 23 | """ 24 | 25 | schema = SchemaGenerate(self.json_config, self.task_log) 26 | schema_code = schema.generate_code() 27 | self.task_log.info(f"\n{schema_code}", is_verbose=False) 28 | self.task_log.success("Schema 代码生成完成") 29 | 30 | crud = CrudGenerate(self.json_config, self.task_log) 31 | crud_code = crud.generate_code() 32 | self.task_log.info(f"\n{crud_code}", is_verbose=False) 33 | self.task_log.success("CRUD 代码生成完成") 34 | 35 | params = ParamsGenerate(self.json_config, self.task_log) 36 | params_code = params.generate_code() 37 | self.task_log.info(f"\n{params_code}", is_verbose=False) 38 | self.task_log.success("Params 代码生成完成") 39 | 40 | views = ViewGenerate(self.json_config, self.task_log) 41 | views_code = views.generate_code() 42 | self.task_log.info(f"\n{views_code}", is_verbose=False) 43 | self.task_log.success("Views 代码生成完成") 44 | 45 | def write_generate(self, overwrite: bool = False): 46 | """ 47 | 写入生成代码 48 | 49 | :param overwrite: 是否在写入时覆盖文件 50 | """ 51 | schema = SchemaGenerate(self.json_config, self.task_log) 52 | schema.write_generate_code(overwrite=overwrite) 53 | 54 | crud = CrudGenerate(self.json_config, self.task_log) 55 | crud.write_generate_code(overwrite=overwrite) 56 | 57 | params = ParamsGenerate(self.json_config, self.task_log) 58 | params.write_generate_code(overwrite=overwrite) 59 | 60 | views = ViewGenerate(self.json_config, self.task_log) 61 | views.write_generate_code(overwrite=overwrite) 62 | -------------------------------------------------------------------------------- /kinit_fast_task/scripts/app_generate/v1/generate_crud.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from kinit_fast_task.config import settings 4 | from kinit_fast_task.scripts.app_generate.utils.generate_base import GenerateBase 5 | from kinit_fast_task.scripts.app_generate.v1.json_config_schema import JSONConfigSchema 6 | from kinit_fast_task.utils.logger import TaskLogger 7 | 8 | 9 | class CrudGenerate(GenerateBase): 10 | def __init__(self, json_config: JSONConfigSchema, task_log: TaskLogger): 11 | """ 12 | 初始化工作 13 | 14 | :param json_config: 15 | :param task_log: 16 | """ 17 | self.json_config = json_config 18 | self.file_path = settings.BASE_PATH / "app" / "cruds" / self.json_config.crud.filename 19 | self.project_name = settings.system.PROJECT_NAME 20 | self.task_log = task_log 21 | self.task_log.info("开始生成 CRUD 代码, CRUD 文件地址为:", self.file_path, is_verbose=True) 22 | 23 | def write_generate_code(self, overwrite: bool = False): 24 | """ 25 | 生成 crud 文件,以及代码内容 26 | """ 27 | if self.file_path.exists(): 28 | if overwrite: 29 | self.task_log.warning("CRUD 文件已存在, 已选择覆盖, 正在删除重新写入.") 30 | self.file_path.unlink() 31 | else: 32 | self.task_log.warning("CRUD 文件已存在, 未选择覆盖, 不再进行 CRUD 代码生成.") 33 | return False 34 | 35 | self.file_path.parent.mkdir(parents=True, exist_ok=True) 36 | self.file_path.touch() 37 | 38 | code = self.generate_code() 39 | self.file_path.write_text(code, "utf-8") 40 | self.task_log.success("CRUD 代码写入完成") 41 | 42 | def generate_code(self): 43 | """ 44 | 代码生成 45 | """ 46 | code = self.generate_file_desc(self.file_path.name, "1.0", "数据操作") 47 | code += self.generate_modules_code(self.get_base_module_config()) 48 | code += self.get_base_code_content() 49 | return code 50 | 51 | def get_base_module_config(self): 52 | """ 53 | 获取基础模块导入配置 54 | """ 55 | schema_file_name = Path(self.json_config.schemas.filename).stem 56 | model_file_name = Path(self.json_config.model.filename).stem 57 | 58 | modules = { 59 | "sqlalchemy.ext.asyncio": ["AsyncSession"], 60 | f"{self.project_name}.app.cruds.base": ["ORMCrud"], 61 | f"{self.project_name}.app.schemas": [schema_file_name], 62 | f"{self.project_name}.app.models.{model_file_name}": [self.json_config.model.class_name], 63 | } 64 | return modules 65 | 66 | def get_base_code_content(self): 67 | """ 68 | 获取基础代码内容 69 | """ 70 | schema_file_name = Path(self.json_config.schemas.filename).stem 71 | schema_out_class_name = self.json_config.schemas.simple_out_class_name 72 | 73 | base_code = f"\n\nclass {self.json_config.crud.class_name}(ORMCrud[{self.json_config.model.class_name}]):\n" 74 | base_code += "\n\tdef __init__(self, session: AsyncSession):" 75 | base_code += "\n\t\tsuper().__init__()" 76 | base_code += "\n\t\tself.session = session" 77 | base_code += f"\n\t\tself.model = {self.json_config.model.class_name}" 78 | base_code += f"\n\t\tself.simple_out_schema = {schema_file_name}.{schema_out_class_name}" 79 | base_code += "\n" 80 | return base_code.replace("\t", " ") 81 | -------------------------------------------------------------------------------- /kinit_fast_task/scripts/app_generate/v1/generate_params.py: -------------------------------------------------------------------------------- 1 | # ruff: noqa: E501 2 | from pathlib import Path 3 | from kinit_fast_task.config import settings 4 | from kinit_fast_task.scripts.app_generate.utils.generate_base import GenerateBase 5 | from kinit_fast_task.scripts.app_generate.v1.json_config_schema import JSONConfigSchema 6 | from kinit_fast_task.utils.logger import TaskLogger 7 | 8 | 9 | class ParamsGenerate(GenerateBase): 10 | def __init__(self, json_config: JSONConfigSchema, task_log: TaskLogger): 11 | """ 12 | 初始化工作 13 | 14 | :param json_config: 15 | """ 16 | self.json_config = json_config 17 | self.file_path = Path(settings.router.APPS_PATH) / json_config.app_name / self.json_config.params.filename 18 | self.project_name = settings.system.PROJECT_NAME 19 | self.task_log = task_log 20 | self.task_log.info("开始生成 Params 代码, Params 文件地址为:", self.file_path, is_verbose=True) 21 | 22 | def write_generate_code(self, overwrite: bool = False): 23 | """ 24 | 生成 params 文件,以及代码内容 25 | """ 26 | if self.file_path.exists(): 27 | if overwrite: 28 | self.task_log.warning("Params 文件已存在, 已选择覆盖, 正在删除重新写入.") 29 | self.file_path.unlink() 30 | else: 31 | self.task_log.warning("Params 文件已存在, 未选择覆盖, 不再进行 Params 代码生成.") 32 | return False 33 | else: 34 | self.create_pag(self.file_path.parent) 35 | self.file_path.touch() 36 | 37 | code = self.generate_code() 38 | self.file_path.write_text(code, "utf-8") 39 | self.task_log.success("Params 代码写入完成") 40 | 41 | def generate_code(self) -> str: 42 | """ 43 | 生成 schema 代码内容 44 | """ 45 | code = self.generate_file_desc(self.file_path.name, "1.0", self.json_config.app_desc) 46 | 47 | modules = { 48 | "fastapi": ["Depends"], 49 | f"{self.project_name}.app.depends.Paging": ["Paging", "QueryParams"], 50 | } 51 | code += self.generate_modules_code(modules) 52 | 53 | base_code = f"\n\nclass {self.json_config.params.class_name}(QueryParams):" 54 | base_code += "\n\tdef __init__(self, params: Paging = Depends()):" 55 | base_code += "\n\t\tsuper().__init__(params)" 56 | base_code += "\n" 57 | code += base_code 58 | return code.replace("\t", " ") 59 | -------------------------------------------------------------------------------- /kinit_fast_task/scripts/app_generate/v1/generate_schema.py: -------------------------------------------------------------------------------- 1 | # @version : 1.0 2 | # @Create Time : 2024/1/12 17:28 3 | # @File : schema_generate.py 4 | # @IDE : PyCharm 5 | # @desc : schema 代码生成 6 | 7 | from kinit_fast_task.scripts.app_generate.utils.generate_base import GenerateBase 8 | from kinit_fast_task.scripts.app_generate.v1.json_config_schema import JSONConfigSchema 9 | from kinit_fast_task.config import settings 10 | from kinit_fast_task.utils.logger import TaskLogger 11 | 12 | 13 | class SchemaGenerate(GenerateBase): 14 | BASE_FIELDS = ["id", "create_datetime", "update_datetime", "delete_datetime", "is_delete"] 15 | 16 | def __init__(self, json_config: JSONConfigSchema, task_log: TaskLogger): 17 | """ 18 | 初始化工作 19 | 20 | :param json_config: 21 | :param task_log: 22 | """ 23 | self.json_config = json_config 24 | self.file_path = settings.BASE_PATH / "app" / "schemas" / self.json_config.schemas.filename 25 | self.project_name = settings.system.PROJECT_NAME 26 | self.task_log = task_log 27 | self.task_log.info("开始生成 Schema 代码, Schema 文件地址为:", self.file_path, is_verbose=True) 28 | 29 | def write_generate_code(self, overwrite: bool = False): 30 | """ 31 | 生成 schema 文件,以及代码内容 32 | """ 33 | 34 | if self.file_path.exists(): 35 | if overwrite: 36 | self.task_log.warning("Schema 文件已存在, 已选择覆盖, 正在删除重新写入.") 37 | self.file_path.unlink() 38 | else: 39 | self.task_log.warning("Schema 文件已存在, 未选择覆盖, 不再进行 Schema 代码生成.") 40 | return False 41 | 42 | self.file_path.parent.mkdir(parents=True, exist_ok=True) 43 | self.file_path.touch() 44 | 45 | code = self.generate_code() 46 | self.file_path.write_text(code, "utf-8") 47 | self.task_log.success("Schema 代码写入完成") 48 | 49 | def generate_code(self) -> str: 50 | """ 51 | 生成 schema 代码内容 52 | """ 53 | schema = self.json_config.schemas 54 | code = self.generate_file_desc(self.file_path.name, "1.0", "Pydantic 模型,用于数据库序列化操作") 55 | 56 | modules = { 57 | "pydantic": ["Field"], 58 | f"{self.project_name}.core.types": ["DatetimeStr"], 59 | f"{self.project_name}.app.schemas.base": ["BaseSchema"], 60 | } 61 | code += self.generate_modules_code(modules) 62 | 63 | # 生成字段列表 64 | schema_field_code = "" 65 | for item in self.json_config.model.fields: 66 | if item.name in self.BASE_FIELDS: 67 | continue 68 | field = f'\n\t{item.name}: {item.field_python_type} {"| None " if item.nullable else ""}' 69 | default = None 70 | if item.default is not None: 71 | if item.field_python_type == "str": 72 | default = f'"{item.default}"' 73 | else: 74 | default = item.default 75 | elif default is None and not item.nullable: 76 | default = "..." 77 | 78 | field += f'= Field({default}, description="{item.comment}")' 79 | schema_field_code += field 80 | schema_field_code += "\n" 81 | 82 | base_schema_code = f"\n\nclass {schema.base_class_name}(BaseSchema):" 83 | base_schema_code += schema_field_code 84 | code += base_schema_code 85 | 86 | create_schema_code = f"\n\nclass {schema.create_class_name}({schema.base_class_name}):" 87 | create_schema_code += "\n\tpass\n" 88 | code += create_schema_code 89 | 90 | update_schema_code = f"\n\nclass {schema.update_class_name}({schema.base_class_name}):" 91 | update_schema_code += "\n\tpass\n" 92 | code += update_schema_code 93 | 94 | base_out_schema_code = f"\n\nclass {schema.simple_out_class_name}({schema.base_class_name}):" 95 | base_out_schema_code += '\n\tid: int = Field(..., description="编号")' 96 | base_out_schema_code += '\n\tcreate_datetime: DatetimeStr = Field(..., description="创建时间")' 97 | base_out_schema_code += '\n\tupdate_datetime: DatetimeStr = Field(..., description="更新时间")' 98 | base_out_schema_code += "\n" 99 | code += base_out_schema_code 100 | return code.replace("\t", " ") 101 | -------------------------------------------------------------------------------- /kinit_fast_task/scripts/app_generate/v1/generate_view.py: -------------------------------------------------------------------------------- 1 | # ruff: noqa: E501 2 | from pathlib import Path 3 | from kinit_fast_task.config import settings 4 | from kinit_fast_task.scripts.app_generate.utils.generate_base import GenerateBase 5 | from kinit_fast_task.scripts.app_generate.v1.json_config_schema import JSONConfigSchema 6 | from kinit_fast_task.utils.logger import TaskLogger 7 | 8 | 9 | class ViewGenerate(GenerateBase): 10 | def __init__(self, json_config: JSONConfigSchema, task_log: TaskLogger): 11 | """ 12 | 初始化工作 13 | 14 | :param json_config: 15 | """ 16 | self.json_config = json_config 17 | self.file_path = Path(settings.router.APPS_PATH) / json_config.app_name / self.json_config.views.filename 18 | self.project_name = settings.system.PROJECT_NAME 19 | self.task_log = task_log 20 | self.task_log.info("开始生成 Views 代码, Views 文件地址为:", self.file_path, is_verbose=True) 21 | 22 | def write_generate_code(self, overwrite: bool = False): 23 | """ 24 | 生成 view 文件,以及代码内容 25 | """ 26 | if self.file_path.exists(): 27 | if overwrite: 28 | self.task_log.warning("Views 文件已存在, 已选择覆盖, 正在删除重新写入.") 29 | self.file_path.unlink() 30 | else: 31 | self.task_log.warning("Views 文件已存在, 未选择覆盖, 不再进行 Views 代码生成.") 32 | return False 33 | else: 34 | self.create_pag(self.file_path.parent) 35 | 36 | self.file_path.touch() 37 | code = self.generate_code() 38 | self.file_path.write_text(code, encoding="utf-8") 39 | self.task_log.success("Views 代码写入完成") 40 | 41 | def generate_code(self) -> str: 42 | """ 43 | 生成代码 44 | """ 45 | code = self.generate_file_desc(self.file_path.name, "1.0", "路由,视图文件") 46 | code += self.generate_modules_code(self.get_base_module_config()) 47 | router = self.json_config.app_name.replace("_", "/") 48 | code += f'\n\nrouter = APIRouter(prefix="/{router}", tags=["{self.json_config.app_desc}管理"])\n' 49 | code += self.get_base_code_content() 50 | 51 | return code.replace("\t", " ") 52 | 53 | def get_base_module_config(self): 54 | """ 55 | 获取基础模块导入配置 56 | """ 57 | crud_file_name = Path(self.json_config.crud.filename).stem 58 | schema_file_name = Path(self.json_config.schemas.filename).stem 59 | modules = { 60 | "sqlalchemy.ext.asyncio": ["AsyncSession"], 61 | "fastapi": ["APIRouter", "Depends", "Body", "Query"], 62 | f"{self.project_name}.utils.response": [ 63 | "RestfulResponse", 64 | "ResponseSchema", 65 | "PageResponseSchema", 66 | ], 67 | f"{self.project_name}.db.database_factory": ["DBFactory"], 68 | f"{self.project_name}.app.cruds.{crud_file_name}": [self.json_config.crud.class_name], 69 | f"{self.project_name}.app.schemas": [schema_file_name, "DeleteSchema"], 70 | ".params": ["PageParams"], 71 | } 72 | return modules 73 | 74 | def get_base_code_content(self): 75 | """ 76 | 获取基础代码内容 77 | :return: 78 | """ 79 | # fmt: off 80 | zh_name = self.json_config.app_desc 81 | schema_file_name = Path(self.json_config.schemas.filename).stem 82 | 83 | create_schema = f"{schema_file_name}.{self.json_config.schemas.create_class_name}" 84 | update_schema = f'{schema_file_name}.{self.json_config.schemas.update_class_name} = Body(..., description="更新内容")' 85 | simple_out_schema = f"{schema_file_name}.{self.json_config.schemas.simple_out_class_name}" 86 | 87 | session = 'session: AsyncSession = Depends(DBFactory.get_instance("orm").db_transaction_getter)' 88 | crud = self.json_config.crud.class_name 89 | 90 | base_code = f'\n\n@router.post("/create", response_model=ResponseSchema[str], summary="创建{zh_name}")' 91 | base_code += f'\nasync def create(data: {create_schema}, {session}):' 92 | base_code += f'\n\treturn RestfulResponse.success(await {crud}(session).create_data(data=data))\n' 93 | 94 | base_code += f'\n\n@router.post("/update", response_model=ResponseSchema[str], summary="更新{zh_name}")' 95 | base_code += f'\nasync def update(data_id: int = Body(..., description="{zh_name}编号"), data: {update_schema}, {session}):' 96 | base_code += f'\n\treturn RestfulResponse.success(await {crud}(session).update_data(data_id, data))\n' 97 | 98 | base_code += f'\n\n@router.post("/delete", response_model=ResponseSchema[str], summary="批量删除{zh_name}")' 99 | base_code += f'\nasync def delete(data: DeleteSchema = Body(..., description="{zh_name}编号列表"), {session}):' 100 | base_code += f'\n\tawait {crud}(session).delete_datas(ids=data.data_ids)' 101 | base_code += '\n\treturn RestfulResponse.success("删除成功")\n' 102 | 103 | base_code += f'\n\n@router.get("/list/query", response_model=PageResponseSchema[list[{simple_out_schema}]], summary="获取{zh_name}列表")' 104 | base_code += f'\nasync def list_query(params: PageParams = Depends(), {session}):' 105 | base_code += f'\n\tdatas = await {crud}(session).get_datas(**params.dict(), v_return_type="dict")' 106 | base_code += f'\n\ttotal = await {crud}(session).get_count(**params.to_count())' 107 | base_code += '\n\treturn RestfulResponse.success(data=datas, total=total, page=params.page, limit=params.limit)\n' 108 | 109 | base_code += f'\n\n@router.get("/one/query", summary=\"获取{zh_name}信息\")' 110 | base_code += f'\nasync def one_query(data_id: int = Query(..., description="{zh_name}编号"), {session}):' 111 | base_code += f'\n\tdata = await {crud}(session).get_data(data_id, v_schema={simple_out_schema}, v_return_type="dict")' 112 | base_code += '\n\treturn RestfulResponse.success(data=data)\n' 113 | base_code += '\n' 114 | # fmt: on 115 | return base_code.replace("\t", " ") 116 | -------------------------------------------------------------------------------- /kinit_fast_task/scripts/app_generate/v1/json_config_schema.py: -------------------------------------------------------------------------------- 1 | from typing import Literal 2 | from pydantic import BaseModel, Field 3 | from kinit_fast_task.scripts.app_generate.utils.schema_base import ModelFieldSchema 4 | 5 | 6 | class ModelConfigSchema(BaseModel): 7 | filename: str = Field(..., description="文件名称") 8 | class_name: str = Field(..., description="class 名称") 9 | table_args: dict = Field(..., description="表参数") 10 | fields: list[ModelFieldSchema] = Field(..., description="字段列表") 11 | 12 | 13 | class SchemaConfigSchema(BaseModel): 14 | filename: str = Field(..., description="文件名称") 15 | base_class_name: str = Field(..., description="base schema 类名") 16 | create_class_name: str = Field(..., description="create schema 类名") 17 | update_class_name: str = Field(..., description="update schema 类名") 18 | simple_out_class_name: str = Field(..., description="simple_out schema 类名") 19 | 20 | 21 | class CRUDConfigSchema(BaseModel): 22 | filename: str = Field(..., description="文件名称") 23 | class_name: str = Field(..., description="类名") 24 | 25 | 26 | class ParamsConfigSchema(BaseModel): 27 | filename: str = Field(..., description="文件名称") 28 | class_name: str = Field(..., description="类名") 29 | 30 | 31 | RouterAction = Literal["create", "update", "delete", "list_query", "one_query"] 32 | 33 | 34 | class ViewsConfigSchema(BaseModel): 35 | filename: str = Field(..., description="文件名称") 36 | routers: list[RouterAction] = Field(..., description="需要生成的路由") 37 | 38 | 39 | class JSONConfigSchema(BaseModel): 40 | version: str = Field("1.0", description="版本") 41 | app_name: str = Field(..., description="应用名称") 42 | app_desc: str = Field(..., description="应用描述") 43 | model: ModelConfigSchema = Field(..., description="ORM Model 配置") 44 | schemas: SchemaConfigSchema = Field(..., description="schema 序列化配置") 45 | crud: CRUDConfigSchema = Field(..., description="crud 配置") 46 | params: ParamsConfigSchema = Field(..., description="params 配置") 47 | views: ViewsConfigSchema = Field(..., description="views 配置") 48 | -------------------------------------------------------------------------------- /kinit_fast_task/scripts/app_generate/v1/model_to_json.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/6/7 3 | # @File : model_to_json.py 4 | # @IDE : PyCharm 5 | # @Desc : model to json 6 | 7 | import inspect 8 | from pathlib import Path 9 | from kinit_fast_task.scripts.app_generate.v1 import json_config_schema 10 | from kinit_fast_task.scripts.app_generate.utils.generate_base import GenerateBase 11 | from kinit_fast_task.scripts.app_generate.utils.model_to_json_base import ModelToJsonBase 12 | 13 | 14 | class ModelToJson(ModelToJsonBase): 15 | def to_json(self) -> dict: 16 | """ 17 | 转为 1.0 JSON 配置文件 18 | """ 19 | fields = self.parse_model_fields() 20 | json_config = json_config_schema.JSONConfigSchema(**{ 21 | "version": "1.0", 22 | "app_name": self.app_name, 23 | "app_desc": self.app_desc, 24 | "model": { 25 | "filename": Path(inspect.getfile(self.model)).name, 26 | "class_name": self.model.__name__, 27 | "table_args": self.model.__table_args__, 28 | "fields": fields, 29 | }, 30 | "schemas": { 31 | "filename": f"{self.app_name}_schema.py", 32 | "base_class_name": f"{GenerateBase.snake_to_camel(self.app_name)}Schema", 33 | "create_class_name": f"{GenerateBase.snake_to_camel(self.app_name)}CreateSchema", 34 | "update_class_name": f"{GenerateBase.snake_to_camel(self.app_name)}UpdateSchema", 35 | "simple_out_class_name": f"{GenerateBase.snake_to_camel(self.app_name)}SimpleOutSchema", 36 | }, 37 | "crud": { 38 | "filename": f"{self.app_name}_crud.py", 39 | "class_name": f"{GenerateBase.snake_to_camel(self.app_name)}CRUD", 40 | }, 41 | "params": { 42 | "filename": "params.py", 43 | "class_name": "PageParams", 44 | }, 45 | "views": {"filename": "views.py", "routers": ["create", "update", "delete", "list_query", "one_query"]}, 46 | }) 47 | return json_config.model_dump() 48 | -------------------------------------------------------------------------------- /kinit_fast_task/static/swagger_ui/8c3f9d33cp7f3f2c361ff9d124edf2f9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvandk/kinit-fast-task/1c01a44517344f85eb7a0edad122958eca0dda81/kinit_fast_task/static/swagger_ui/8c3f9d33cp7f3f2c361ff9d124edf2f9.jpg -------------------------------------------------------------------------------- /kinit_fast_task/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from kinit_fast_task.utils.logger import log 2 | -------------------------------------------------------------------------------- /kinit_fast_task/utils/aes_crypto.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2022/11/3 17:23 3 | # @File : count.py 4 | # @IDE : PyCharm 5 | # @Desc : AES 加解密工具 6 | import base64 7 | from Crypto.Cipher import AES # 需要先通过poetry add pycryptodome安装此库 8 | 9 | 10 | class AESEncryption: 11 | """ 12 | AES加解密工具类,采用CBC模式,并结合Base64进行编码和解码 13 | """ 14 | 15 | def __init__(self, key: str = "0CoJUm6Qywm6ts68", iv: str = "0102030405060708"): 16 | """ 17 | 初始化AES加解密器,设置密钥和初始化向量(IV) 18 | :param key: 密钥,此处为默认密钥,用户可根据实际情况自定义 19 | :param iv: 初始化向量(IV),此处为默认值,CBC模式下需固定且长度为16字节 20 | """ 21 | self.key = key.encode("utf8") # 将密钥转换为字节类型 22 | self.iv = iv.encode("utf8") # 将初始向量转换为字节类型 23 | 24 | @staticmethod 25 | def pad(s: str): 26 | """ 27 | 字符串填充函数,使得字符串长度能被16整除 28 | :param s: 需要填充的字符串 29 | :return: 填充后的字符串 30 | """ 31 | return s + (16 - len(s) % 16) * chr(16 - len(s) % 16) 32 | 33 | @staticmethod 34 | def un_pad(s: bytes): 35 | """ 36 | 移除字符串填充,恢复原始数据 37 | :param s: 经过填充的字节数据 38 | :return: 去除填充后的原始字符串对应的字节数据 39 | """ 40 | return s[0 : -s[-1]] 41 | 42 | def encrypt(self, plaintext: str): 43 | """ 44 | 对给定明文字符串进行AES加密,并返回Base64编码后的密文字符串 45 | :param plaintext: 待加密的明文字符串 46 | :return: Base64编码后的密文字符串 47 | """ 48 | padded_data = self.pad(plaintext) 49 | cipher = AES.new(self.key, AES.MODE_CBC, self.iv) 50 | encrypted_bytes = cipher.encrypt(padded_data.encode("utf8")) 51 | encoded_str = base64.urlsafe_b64encode(encrypted_bytes).decode("utf8") 52 | return encoded_str 53 | 54 | def decrypt(self, ciphertext: str): 55 | """ 56 | 对Base64解码后的密文进行AES解密,并返回原始明文字符串 57 | :param ciphertext: Base64编码的密文字符串 58 | :return: 解密后的原始明文字符串 59 | """ 60 | decoded_bytes = base64.urlsafe_b64decode(ciphertext.encode("utf8")) 61 | cipher = AES.new(self.key, AES.MODE_CBC, self.iv) 62 | decrypted_bytes = cipher.decrypt(decoded_bytes) 63 | return self.un_pad(decrypted_bytes).decode("utf8") 64 | 65 | 66 | # 示例使用 67 | if __name__ == "__main__": 68 | _plaintext = "16658273438153332588-95YEUPJR" # 需要加密的内容 69 | 70 | aes_helper = AESEncryption() 71 | encrypted_text = aes_helper.encrypt(_plaintext) 72 | print(f"加密后的文本: {encrypted_text}") 73 | 74 | decrypted_text = aes_helper.decrypt(encrypted_text) 75 | print(f"解密后的文本: {decrypted_text}") 76 | -------------------------------------------------------------------------------- /kinit_fast_task/utils/count.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2022/11/3 17:23 3 | # @File : count.py 4 | # @IDE : PyCharm 5 | # @Desc : 计数 6 | 7 | 8 | from redis.asyncio.client import Redis 9 | 10 | 11 | class Count: 12 | """ 13 | redis 计数 14 | """ 15 | 16 | def __init__(self, rd: Redis, key): 17 | self.rd = rd 18 | self.key = key 19 | 20 | async def add(self, ex: int = None) -> int: 21 | """ 22 | 增加 23 | :param ex: 24 | :return: 总数 25 | """ 26 | await self.rd.set(self.key, await self.get_count() + 1, ex=ex) 27 | return await self.get_count() 28 | 29 | async def subtract(self, ex: int = None) -> int: 30 | """ 31 | 减少 32 | :param ex: 33 | :return: 34 | """ 35 | await self.rd.set(self.key, await self.get_count() - 1, ex=ex) 36 | return await self.get_count() 37 | 38 | async def get_count(self) -> int: 39 | """ 40 | 获取当前总数 41 | :return: 42 | """ 43 | number = await self.rd.get(self.key) 44 | if number: 45 | return int(number) 46 | return 0 47 | 48 | async def reset(self) -> None: 49 | """ 50 | 重置计数 51 | :return: 52 | """ 53 | await self.rd.set(self.key, 0) 54 | 55 | async def delete(self) -> None: 56 | """ 57 | 删除 key 58 | :return: 59 | """ 60 | await self.rd.delete(self.key) 61 | -------------------------------------------------------------------------------- /kinit_fast_task/utils/enum.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2023/02/12 22:18 3 | # @File : enum.py 4 | # @IDE : PyCharm 5 | # @Desc : 增加枚举类方法 6 | 7 | from enum import Enum 8 | 9 | 10 | class SuperEnum(Enum): 11 | @classmethod 12 | def to_dict(cls): 13 | """ 14 | 返回枚举的字典表示形式 15 | :return: 16 | """ 17 | return {e.name: e.value for e in cls} 18 | 19 | @classmethod 20 | def keys(cls): 21 | """ 22 | 返回所有枚举键的列表 23 | :return: 24 | """ 25 | return cls._member_names_ 26 | 27 | @classmethod 28 | def values(cls): 29 | """ 30 | 返回所有枚举值的列表 31 | :return: 32 | """ 33 | return list(cls._value2member_map_.keys()) 34 | -------------------------------------------------------------------------------- /kinit_fast_task/utils/love.py: -------------------------------------------------------------------------------- 1 | # 晚上星月争辉,美梦陪你入睡 2 | import random 3 | from math import sin, cos, pi, log 4 | from tkinter import * 5 | 6 | CANVAS_WIDTH = 640 # 画布的宽 7 | CANVAS_HEIGHT = 480 # 画布的高 8 | CANVAS_CENTER_X = CANVAS_WIDTH / 2 # 画布中心的X轴坐标 9 | CANVAS_CENTER_Y = CANVAS_HEIGHT / 2 # 画布中心的Y轴坐标 10 | IMAGE_ENLARGE = 11 # 放大比例 11 | HEART_COLOR = "#ff2121" # 心的颜色,这个是中国红 12 | 13 | 14 | def heart_function(t, shrink_ratio: float = IMAGE_ENLARGE): 15 | """ 16 | “爱心函数生成器” 17 | :param shrink_ratio: 放大比例 18 | :param t: 参数 19 | :return: 坐标 20 | """ 21 | # 基础函数 22 | x = 16 * (sin(t) ** 3) 23 | y = -(13 * cos(t) - 5 * cos(2 * t) - 2 * cos(3 * t) - cos(4 * t)) 24 | 25 | # 放大 26 | x *= shrink_ratio 27 | y *= shrink_ratio 28 | 29 | # 移到画布中央 30 | x += CANVAS_CENTER_X 31 | y += CANVAS_CENTER_Y 32 | 33 | return int(x), int(y) 34 | 35 | 36 | def scatter_inside(x, y, beta=0.15): 37 | """ 38 | 随机内部扩散 39 | :param x: 原x 40 | :param y: 原y 41 | :param beta: 强度 42 | :return: 新坐标 43 | """ 44 | ratio_x = -beta * log(random.random()) 45 | ratio_y = -beta * log(random.random()) 46 | 47 | dx = ratio_x * (x - CANVAS_CENTER_X) 48 | dy = ratio_y * (y - CANVAS_CENTER_Y) 49 | 50 | return x - dx, y - dy 51 | 52 | 53 | def shrink(x, y, ratio): 54 | """ 55 | 抖动 56 | :param x: 原x 57 | :param y: 原y 58 | :param ratio: 比例 59 | :return: 新坐标 60 | """ 61 | force = -1 / (((x - CANVAS_CENTER_X) ** 2 + (y - CANVAS_CENTER_Y) ** 2) ** 0.6) # 这个参数... 62 | dx = ratio * force * (x - CANVAS_CENTER_X) 63 | dy = ratio * force * (y - CANVAS_CENTER_Y) 64 | return x - dx, y - dy 65 | 66 | 67 | def curve(p): 68 | """ 69 | 自定义曲线函数,调整跳动周期 70 | :param p: 参数 71 | :return: 正弦 72 | """ 73 | # 可以尝试换其他的动态函数,达到更有力量的效果(贝塞尔?) 74 | return 2 * (2 * sin(4 * p)) / (2 * pi) 75 | 76 | 77 | class Heart: 78 | """ 79 | 爱心类 80 | """ 81 | 82 | def __init__(self, generate_frame=20): 83 | self._points = set() # 原始爱心坐标集合 84 | self._edge_diffusion_points = set() # 边缘扩散效果点坐标集合 85 | self._center_diffusion_points = set() # 中心扩散效果点坐标集合 86 | self.all_points = {} # 每帧动态点坐标 87 | self.build(2000) 88 | 89 | self.random_halo = 1000 90 | 91 | self.generate_frame = generate_frame 92 | for frame in range(generate_frame): 93 | self.calc(frame) 94 | 95 | def build(self, number): 96 | # 爱心 97 | for _ in range(number): 98 | t = random.uniform(0, 2 * pi) # 随机不到的地方造成爱心有缺口 99 | x, y = heart_function(t) 100 | self._points.add((x, y)) 101 | 102 | # 爱心内扩散 103 | for _x, _y in list(self._points): 104 | for _ in range(3): 105 | x, y = scatter_inside(_x, _y, 0.05) 106 | self._edge_diffusion_points.add((x, y)) 107 | 108 | # 爱心内再次扩散 109 | point_list = list(self._points) 110 | for _ in range(4000): 111 | x, y = random.choice(point_list) 112 | x, y = scatter_inside(x, y, 0.17) 113 | self._center_diffusion_points.add((x, y)) 114 | 115 | @staticmethod 116 | def calc_position(x, y, ratio): 117 | # 调整缩放比例 118 | force = 1 / (((x - CANVAS_CENTER_X) ** 2 + (y - CANVAS_CENTER_Y) ** 2) ** 0.520) # 魔法参数 119 | 120 | dx = ratio * force * (x - CANVAS_CENTER_X) + random.randint(-1, 1) 121 | dy = ratio * force * (y - CANVAS_CENTER_Y) + random.randint(-1, 1) 122 | 123 | return x - dx, y - dy 124 | 125 | def calc(self, generate_frame): 126 | ratio = 10 * curve(generate_frame / 10 * pi) # 圆滑的周期的缩放比例 127 | 128 | halo_radius = int(4 + 6 * (1 + curve(generate_frame / 10 * pi))) 129 | halo_number = int(3000 + 4000 * abs(curve(generate_frame / 10 * pi) ** 2)) 130 | 131 | all_points = [] 132 | 133 | # 光环 134 | heart_halo_point = set() # 光环的点坐标集合 135 | for _ in range(halo_number): 136 | t = random.uniform(0, 2 * pi) # 随机不到的地方造成爱心有缺口 137 | x, y = heart_function(t, shrink_ratio=11.6) # 魔法参数 138 | x, y = shrink(x, y, halo_radius) 139 | if (x, y) not in heart_halo_point: 140 | # 处理新的点 141 | heart_halo_point.add((x, y)) 142 | x += random.randint(-14, 14) 143 | y += random.randint(-14, 14) 144 | size = random.choice((1, 2, 2)) 145 | all_points.append((x, y, size)) 146 | 147 | # 轮廓 148 | for x, y in self._points: 149 | x, y = self.calc_position(x, y, ratio) 150 | size = random.randint(1, 3) 151 | all_points.append((x, y, size)) 152 | 153 | # 内容 154 | for x, y in self._edge_diffusion_points: 155 | x, y = self.calc_position(x, y, ratio) 156 | size = random.randint(1, 2) 157 | all_points.append((x, y, size)) 158 | 159 | for x, y in self._center_diffusion_points: 160 | x, y = self.calc_position(x, y, ratio) 161 | size = random.randint(1, 2) 162 | all_points.append((x, y, size)) 163 | 164 | self.all_points[generate_frame] = all_points 165 | 166 | def render(self, render_canvas, render_frame): 167 | for x, y, size in self.all_points[render_frame % self.generate_frame]: 168 | render_canvas.create_rectangle(x, y, x + size, y + size, width=0, fill=HEART_COLOR) 169 | 170 | 171 | def draw(main: Tk, render_canvas: Canvas, render_heart: Heart, render_frame=0): 172 | render_canvas.delete("all") 173 | render_heart.render(render_canvas, render_frame) 174 | main.after(160, draw, main, render_canvas, render_heart, render_frame + 1) 175 | 176 | 177 | if __name__ == "__main__": 178 | root = Tk() # 一个Tk 179 | canvas = Canvas(root, bg="black", height=CANVAS_HEIGHT, width=CANVAS_WIDTH) 180 | canvas.pack() 181 | heart = Heart() # 心 182 | draw(root, canvas, heart) # 开始画画~ 183 | root.mainloop() 184 | -------------------------------------------------------------------------------- /kinit_fast_task/utils/pdf_to_word.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/4/22 15:27 3 | # @File : pdf_to_word.py 4 | # @IDE : PyCharm 5 | # @Desc : 文件描述信息 6 | 7 | 8 | from pdf2docx import Converter 9 | 10 | 11 | def pdf_to_word(pdf_file_path, word_file_path): 12 | """ 13 | 将PDF文件转换为Word文件。 14 | :param pdf_file_path: PDF文件的路径 15 | :param word_file_path: 生成的Word文件的路径 16 | """ 17 | # 创建一个转换器对象,传入PDF文件路径 18 | cv = Converter(pdf_file_path) 19 | 20 | # 开始转换过程,这里转换全部页面 21 | cv.convert(word_file_path, start=0, end=None) 22 | 23 | # 转换结束后清理并关闭文件 24 | cv.close() 25 | 26 | 27 | if __name__ == "__main__": 28 | # 示例使用:将'example.pdf'文件转换为'output.docx' 29 | pdf_to_word("1.pdf", "1.docx") 30 | -------------------------------------------------------------------------------- /kinit_fast_task/utils/ppt_to_pdf.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2023/3/27 9:48 3 | # @File : ppt_to_pdf.py 4 | # @IDE : PyCharm 5 | # @Desc : 只支持 windows 系统使用 6 | 7 | 8 | import os 9 | from win32com.client import gencache # 安装命令,只能在 Windows 系统下安装:poetry add pywin32 10 | import comtypes.client # 安装命令,只能在 Windows 系统下安装:poetry add comtypes 11 | from src.kinit_fast_task.core.logger import log 12 | 13 | 14 | def ppt_to_pdf_1(ppt_path: str, pdf_path: str): 15 | """ 16 | ppt 转 pdf,会弹出 office 软件 17 | :param ppt_path: 18 | :param pdf_path: 19 | :return: 20 | """ 21 | # 创建PDF 22 | powerpoint = comtypes.client.CreateObject("Powerpoint.Application") 23 | powerpoint.Visible = 1 24 | slide = powerpoint.Presentations.Open(ppt_path) 25 | # 保存PDF 26 | slide.SaveAs(pdf_path, 32) 27 | slide.Close() 28 | # 退出 office 软件 29 | powerpoint.Quit() 30 | 31 | 32 | def ppt_to_pdf_2(ppt_path: str, pdf_path: str): 33 | """ 34 | 完美办法,PPT 转 PDF 35 | :param ppt_path: 36 | :param pdf_path: 37 | :return: 38 | """ 39 | p = gencache.EnsureDispatch("PowerPoint.Application") 40 | try: 41 | ppt = p.Presentations.Open(ppt_path, False, False, False) 42 | ppt.ExportAsFixedFormat(pdf_path, 2, PrintRange=None) 43 | ppt.Close() 44 | p.Quit() 45 | except Exception as e: 46 | print(os.path.split(ppt_path)[1], f"转化失败,失败原因:{e}") 47 | log.info(os.path.split(ppt_path)[1], f"转化失败,失败原因:{e}") 48 | -------------------------------------------------------------------------------- /kinit_fast_task/utils/response.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2023/3/27 9:48 3 | # @File : response.py 4 | # @IDE : PyCharm 5 | # @Desc : 全局响应 6 | 7 | 8 | from pydantic import BaseModel, Field 9 | from kinit_fast_task.utils.response_code import Status 10 | from typing import Generic, TypeVar 11 | from fastapi import status as fastapi_status 12 | from fastapi.responses import ORJSONResponse 13 | 14 | DataT = TypeVar("DataT") 15 | 16 | 17 | class ResponseSchema(BaseModel, Generic[DataT]): 18 | """ 19 | 默认响应模型 20 | """ 21 | 22 | code: int = Field(Status.HTTP_SUCCESS, description="响应状态码(响应体内)") 23 | message: str = Field("success", description="响应结果描述") 24 | data: DataT = Field(None, description="响应结果数据") 25 | 26 | 27 | class PageResponseSchema(ResponseSchema): 28 | """ 29 | 带有分页的响应模型 30 | """ 31 | 32 | total: int = Field(0, description="总数据量") 33 | page: int = Field(1, description="当前页数") 34 | limit: int = Field(10, description="每页多少条数据") 35 | 36 | 37 | class ErrorResponseSchema(BaseModel, Generic[DataT]): 38 | """ 39 | 默认请求失败响应模型 40 | """ 41 | 42 | code: int = Field(Status.HTTP_ERROR, description="响应状态码(响应体内)") 43 | message: str = Field("请求失败,请联系管理员", description="响应结果描述") 44 | data: DataT = Field(None, description="响应结果数据") 45 | 46 | 47 | ResponseSchemaT = TypeVar("ResponseSchemaT", bound=ResponseSchema) 48 | 49 | 50 | class RestfulResponse: 51 | """ 52 | 响应体 53 | """ 54 | 55 | @staticmethod 56 | def success( 57 | message: str = "success", 58 | *, 59 | code: int = Status.HTTP_SUCCESS, 60 | data: DataT = None, 61 | status_code: int = fastapi_status.HTTP_200_OK, 62 | **kwargs, 63 | ) -> ORJSONResponse: 64 | """ 65 | 成功响应 66 | 67 | :param message: 响应结果描述 68 | :param code: 业务响应体状态码 69 | :param data: 响应结果数据 70 | :param status_code: HTTP 响应状态码 71 | :param kwargs: 额外参数 72 | :return: 73 | """ 74 | content = ResponseSchema(code=code, message=message, data=data) 75 | content = content.model_dump() | kwargs 76 | return ORJSONResponse(content=content, status_code=status_code) 77 | 78 | @staticmethod 79 | def error( 80 | message: str, 81 | *, 82 | code: int = Status.HTTP_ERROR, 83 | data: DataT = None, 84 | status_code: int = fastapi_status.HTTP_200_OK, 85 | **kwargs, 86 | ) -> ORJSONResponse: 87 | """ 88 | 失败响应 89 | 90 | :param message: 响应结果描述 91 | :param code: 业务响应体状态码 92 | :param data: 响应结果数据 93 | :param status_code: HTTP 响应状态码 94 | :return: 95 | """ 96 | content = ErrorResponseSchema(code=code, message=message, data=data) 97 | content = content.model_dump() | kwargs 98 | return ORJSONResponse(content=content, status_code=status_code) 99 | -------------------------------------------------------------------------------- /kinit_fast_task/utils/response_code.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/5/7 17:20 3 | # @File : response_code.py 4 | # @IDE : PyCharm 5 | # @Desc : 常用的响应状态码(响应体 Code) 6 | 7 | 8 | class Status: 9 | """ 10 | 常用的响应状态码,可自定义,自定义的前提应与前端提前对接好 11 | 该状态码只应用于响应体中的状态码,因为该状态码是可变的 12 | 13 | HTTP状态码本身已经提供了关于请求是否成功以及失败的原因的信息,为什么还要在请求体中额外添加 `code` 参数? 14 | 15 | 1. 细化错误类型:虽然HTTP状态码能够描述请求的基本状态(如200代表成功,404代表未找到等),但它们的信息量有限通过在响应体中添加`code` 16 | 参数后端可以提供更详细的错误代码,这些错误代码可以更具体地描述问题(如数据库错误、业务逻辑错误等)这对于调试和用户反馈是非常有用的。 17 | 18 | 2. 业务逻辑状态表示:在复杂的业务逻辑中,可能需要返回更多的状态信息,这些信息不仅仅是关于请求成功或失败的。例如,一个操作可能部分成功, 19 | 或者有特定的警告信息需要传达。`code`参数可以用来传递这些特定的业务逻辑相关的状态信息。 20 | 21 | 3. 前后端分离:在现代的应用开发中,前后端分离是一种常见的架构模式。在这种模式中,前端应用往往需要根据不同的业务代码执行不同的逻辑处理。 22 | 包含具体业务相关的`code`可以让前端开发者更精确地控制用户界面的反应。 23 | 24 | 4. 国际化和本地化:HTTP状态码的描述通常是标准化且不变的。通过在响应体中包含`message`字段,可以提供本地化的错误信息,这有助于提升用户 25 | 体验,特别是在多语言应用中。 26 | 27 | 5. 向后兼容性:在现有系统中引入新的错误类型或状态码时,使用HTTP状态码可能需要大的变动或可能影响到旧版本客户端的处理逻辑。使用`code`和 28 | `message`提供额外的上下文可以在不影响旧版本客户端的情况下,增加新的逻辑。 29 | 30 | HTTP 状态码大全:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status 31 | """ # noqa E501 32 | 33 | HTTP_SUCCESS = 200 # OK 请求成功 34 | HTTP_ERROR = 400 # BAD_REQUEST 因客户端错误的原因请求失败 35 | HTTP_401 = 401 # UNAUTHORIZED: 未授权 36 | HTTP_403 = 403 # FORBIDDEN: 禁止访问 37 | HTTP_404 = 404 # NOT_FOUND: 未找到 38 | HTTP_405 = 405 # METHOD_NOT_ALLOWED: 方法不允许 39 | HTTP_408 = 408 # REQUEST_TIMEOUT: 请求超时 40 | HTTP_500 = 500 # INTERNAL_SERVER_ERROR: 服务器内部错误 41 | HTTP_502 = 502 # BAD_GATEWAY: 错误的网关 42 | HTTP_503 = 503 # SERVICE_UNAVAILABLE: 服务不可用 43 | -------------------------------------------------------------------------------- /kinit_fast_task/utils/send_email.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2023/3/27 9:48 3 | # @File : send_email.py 4 | # @IDE : PyCharm 5 | # @Desc : 发送邮件封装类 6 | 7 | 8 | import smtplib 9 | from email.mime.text import MIMEText 10 | from email.mime.multipart import MIMEMultipart 11 | from email.mime.application import MIMEApplication 12 | from kinit_fast_task.core import CustomException 13 | 14 | 15 | class EmailSender: 16 | def __init__(self, email: str, password: str, smtp_server: str, smtp_port: int) -> None: 17 | """ 18 | 初始化配置 19 | :param email: 20 | :param password: 21 | :param smtp_server: 22 | :param smtp_port: 23 | """ 24 | self.email = email 25 | self.password = password 26 | self.smtp_server = smtp_server 27 | self.smtp_port = smtp_port 28 | self.server = self.__login_server() 29 | 30 | def __login_server(self) -> smtplib.SMTP: 31 | """ 32 | 登录至邮箱服务器 33 | """ 34 | server = smtplib.SMTP_SSL(self.smtp_server, self.smtp_port) 35 | try: 36 | server.login(self.email, self.password) 37 | return server 38 | except smtplib.SMTPAuthenticationError as exc: 39 | raise CustomException("邮件发送失败,邮箱服务器认证失败!") from exc 40 | except AttributeError as exc: 41 | raise CustomException("邮件发送失败,邮箱服务器认证失败!") from exc 42 | 43 | def send_email(self, to_emails: list[str], subject: str, body: str, attachments: list[str] = None) -> bool: 44 | """ 45 | 发送邮件 46 | :param to_emails: 收件人,一个或多个 47 | :param subject: 主题 48 | :param body: 内容 49 | :param attachments: 附件 50 | """ 51 | message = MIMEMultipart() 52 | message["From"] = self.email 53 | message["To"] = ", ".join(to_emails) 54 | message["Subject"] = subject 55 | body = MIMEText(body) 56 | message.attach(body) 57 | if attachments: 58 | for attachment in attachments: 59 | with open(attachment, "rb") as f: 60 | file_data = f.read() 61 | filename = attachment.split("/")[-1] 62 | attachment = MIMEApplication(file_data, Name=filename) 63 | attachment["Content-Disposition"] = f'attachment; filename="{filename}"' 64 | message.attach(attachment) 65 | try: 66 | result = self.server.sendmail(self.email, to_emails, message.as_string()) 67 | self.server.quit() 68 | print("邮件发送结果", result) 69 | return not result 70 | except smtplib.SMTPException as e: 71 | self.server.quit() 72 | print("邮件发送失败!错误信息:", e) 73 | return False 74 | -------------------------------------------------------------------------------- /kinit_fast_task/utils/singleton.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/5/27 上午11:29 3 | # @File : singleton.py 4 | # @IDE : PyCharm 5 | # @Desc : 单例模式 6 | 7 | 8 | import threading 9 | 10 | 11 | def singleton(cls): 12 | """ 13 | 单例模式函数 14 | 可通过装饰器使用,不支持单例类 15 | """ 16 | _instance = {} 17 | lock = threading.Lock() 18 | 19 | def inner(*args, **kwargs): 20 | with lock: 21 | if cls not in _instance: 22 | _instance[cls] = cls(*args, **kwargs) 23 | 24 | return _instance[cls] 25 | 26 | return inner 27 | 28 | 29 | class Singleton(type): 30 | """ 31 | 单例模式基类 32 | 可通过指定类 metaclass 实现单例模式 33 | 34 | 使用例子: 35 | 36 | >>> class DBFactory(metaclass=Singleton): 37 | ... 38 | 39 | 实现类时指定元类为单例类即可实现单例模式 40 | """ 41 | 42 | _instances = {} 43 | lock = threading.Lock() 44 | 45 | def __call__(cls, *args, **kwargs): 46 | with cls.lock: 47 | if cls not in cls._instances: 48 | cls._instances[cls] = super().__call__(*args, **kwargs) 49 | return cls._instances[cls] 50 | -------------------------------------------------------------------------------- /kinit_fast_task/utils/socket_client.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2022/8/10 22:20 3 | # @File : socket_client.py 4 | # @IDE : PyCharm 5 | # @Desc : socket 客户端操作 6 | 7 | import json 8 | import socket 9 | 10 | 11 | class SocketClient: 12 | """ 13 | socket 客户端操作 14 | """ 15 | 16 | def __init__(self, host: str = "127.0.0.1", port: int = 3636, send_type: str = "tcp"): 17 | """ 18 | :param host: socket server 地址 19 | :param port: socket server 端口 20 | :param send_type: 通信协议 21 | """ 22 | self.send_type = send_type 23 | if self.send_type == "tcp": 24 | socket_type = socket.SOCK_STREAM 25 | elif self.send_type == "udp": 26 | socket_type = socket.SOCK_DGRAM 27 | else: 28 | print("不支持通信协议") 29 | raise ValueError("不支持的通信协议") 30 | self.client_socket = socket.socket(socket.AF_INET, socket_type) 31 | self.host = host 32 | self.port = port 33 | if self.send_type == "tcp": 34 | self.tcp_connect() 35 | 36 | def tcp_connect(self): 37 | """ 38 | TCP 连接服务端 39 | :return: 40 | """ 41 | self.client_socket.connect((self.host, self.port)) 42 | print("tcp 连接成功") 43 | 44 | def udp_send_message(self, message: str): 45 | """ 46 | UDP 发送消息 47 | :param message: 48 | :return: 49 | """ 50 | self.client_socket.sendto(message.encode("utf-8"), (self.host, self.port)) 51 | print("udp 消息发送成功:", message) 52 | 53 | def tcp_send_message(self, message: str): 54 | """ 55 | TCP 发送消息 56 | :param message: 57 | :return: 58 | """ 59 | self.client_socket.sendall(message.encode("utf-8")) 60 | print("tcp 消息发送成功:", message) 61 | 62 | def send_message(self, message: str): 63 | """ 64 | TCP 发送消息 65 | :param message: 66 | :return: 67 | """ 68 | if self.send_type == "tcp": 69 | self.tcp_send_message(message) 70 | elif self.send_type == "udp": 71 | self.udp_send_message(message) 72 | else: 73 | print("不支持协议") 74 | raise ValueError("不支持的协议") 75 | 76 | def close(self): 77 | """ 78 | 关闭 socket 连接 79 | :return: 80 | """ 81 | self.client_socket.close() 82 | 83 | 84 | if __name__ == "__main__": 85 | _host = "127.0.0.1" 86 | _port = 3636 87 | 88 | SC = SocketClient() 89 | SC.tcp_send_message(json.dumps({"label": "ceshi", "value": 1})) 90 | SC.close() 91 | -------------------------------------------------------------------------------- /kinit_fast_task/utils/storage/__init__.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Update Time : 2024/6/30 3 | # @File : __init__.py 4 | # @IDE : PyCharm 5 | # @Desc : 文件描述信息 6 | 7 | from kinit_fast_task.utils.storage.abs import AbstractStorage 8 | from kinit_fast_task.utils.storage.local.local import LocalStorage 9 | from kinit_fast_task.utils.storage.oss.oss import OSSStorage 10 | from kinit_fast_task.utils.storage.kodo.kodo import KodoStorage 11 | from kinit_fast_task.utils.storage.temp.temp import TempStorage 12 | from kinit_fast_task.utils.storage.storage_factory import StorageFactory 13 | -------------------------------------------------------------------------------- /kinit_fast_task/utils/storage/abs.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/7/1 3 | # @File : abs.py 4 | # @IDE : PyCharm 5 | # @Desc : 文件描述信息 6 | import datetime 7 | from abc import ABC, abstractmethod 8 | 9 | from fastapi import UploadFile 10 | 11 | from kinit_fast_task.core import CustomException 12 | 13 | 14 | class AbstractStorage(ABC): 15 | """ 16 | 数据库操作抽象类 17 | """ 18 | 19 | IMAGE_ACCEPT = ["image/png", "image/jpeg", "image/gif", "image/x-icon"] 20 | VIDEO_ACCEPT = ["video/mp4", "video/mpeg"] 21 | AUDIO_ACCEPT = ["audio/wav", "audio/mp3", "audio/m4a", "audio/wma", "audio/ogg", "audio/mpeg", "audio/x-wav"] 22 | ALL_ACCEPT = [*IMAGE_ACCEPT, *VIDEO_ACCEPT, *AUDIO_ACCEPT] 23 | 24 | async def save_image(self, file: UploadFile, *, path: str | None = None, max_size: int = 10) -> str: 25 | """ 26 | 保存图片文件 27 | """ 28 | return await self.save(file, path=path, accept=self.IMAGE_ACCEPT, max_size=max_size) 29 | 30 | async def save_audio(self, file: UploadFile, *, path: str | None = None, max_size: int = 50) -> str: 31 | """ 32 | 保存音频文件 33 | """ 34 | return await self.save(file, path=path, accept=self.AUDIO_ACCEPT, max_size=max_size) 35 | 36 | async def save_video(self, file: UploadFile, *, path: str | None = None, max_size: int = 50) -> str: 37 | """ 38 | 保存视频文件 39 | """ 40 | return await self.save(file, path=path, accept=self.VIDEO_ACCEPT, max_size=max_size) 41 | 42 | @abstractmethod 43 | async def save(self, file: UploadFile, *, path: str | None = None, accept: list = None, max_size: int = 50) -> str: 44 | """ 45 | 保存通用文件 46 | 47 | :param file: 文件 48 | :param path: 上传路径 49 | :param accept: 支持的文件类型 50 | :param max_size: 支持的文件最大值,单位 MB 51 | :return: 文件访问地址,POSIX 风格路径, 示例:/media/word/test.docs 52 | """ 53 | 54 | @classmethod 55 | async def validate_file(cls, file: UploadFile, *, max_size: int = None, mime_types: list = None) -> bool: 56 | """ 57 | 验证文件是否符合格式 58 | 59 | :param file: 文件 60 | :param max_size: 文件最大值,单位 MB 61 | :param mime_types: 支持的文件类型 62 | """ 63 | if mime_types is None: 64 | mime_types = cls.ALL_ACCEPT 65 | if max_size: 66 | size = len(await file.read()) / 1024 / 1024 67 | if size > max_size: 68 | raise CustomException(f"上传文件过大,不能超过{max_size}MB") 69 | await file.seek(0) 70 | if mime_types: 71 | if file.content_type not in mime_types: 72 | raise CustomException(f"上传文件格式错误,只支持 {','.join(mime_types)} 格式!") 73 | return True 74 | 75 | @classmethod 76 | def get_today_timestamp(cls) -> str: 77 | """ 78 | 获取当天时间戳 79 | :return: 80 | """ 81 | return str(int((datetime.datetime.now().replace(hour=0, minute=0, second=0)).timestamp())) 82 | -------------------------------------------------------------------------------- /kinit_fast_task/utils/storage/kodo/__init__.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/7/1 3 | # @File : __init__.py 4 | # @IDE : PyCharm 5 | # @Desc : 七牛云对象存储 6 | 7 | 8 | -------------------------------------------------------------------------------- /kinit_fast_task/utils/storage/kodo/kodo.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/7/1 3 | # @File : kodo.py 4 | # @IDE : PyCharm 5 | # @Desc : 文件描述信息 6 | from fastapi import UploadFile 7 | 8 | from kinit_fast_task.utils.storage import AbstractStorage 9 | 10 | 11 | class KodoStorage(AbstractStorage): 12 | async def save(self, file: UploadFile, *, path: str | None = None, accept: list = None, max_size: int = 50) -> str: 13 | """ 14 | 保存通用文件 15 | 16 | :param file: 文件 17 | :param path: 上传路径 18 | :param accept: 支持的文件类型 19 | :param max_size: 支持的文件最大值,单位 MB 20 | :return: 文件访问地址 21 | """ 22 | raise NotImplementedError("未实现七牛云文件上传功能") 23 | -------------------------------------------------------------------------------- /kinit_fast_task/utils/storage/local/__init__.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/7/1 3 | # @File : __init__.py 4 | # @IDE : PyCharm 5 | # @Desc : 本地静态文件存储 6 | -------------------------------------------------------------------------------- /kinit_fast_task/utils/storage/local/local.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/7/1 3 | # @File : local.py 4 | # @IDE : PyCharm 5 | # @Desc : 本地文件存储 6 | import os 7 | import uuid 8 | 9 | from aiopathlib import AsyncPath 10 | from fastapi import UploadFile 11 | 12 | from kinit_fast_task.utils.storage import AbstractStorage 13 | from kinit_fast_task.config import settings 14 | 15 | 16 | class LocalStorage(AbstractStorage): 17 | async def save(self, file: UploadFile, *, path: str | None = None, accept: list = None, max_size: int = 50) -> str: 18 | """ 19 | 保存通用文件 20 | 21 | :param file: 文件 22 | :param path: 上传路径 23 | :param accept: 支持的文件类型 24 | :param max_size: 支持的文件最大值,单位 MB 25 | :return: 文件访问地址,POSIX 风格路径, 示例:/media/word/test.docs 26 | """ 27 | await self.validate_file(file, max_size=max_size, mime_types=accept) 28 | if path is None: 29 | path = self.get_today_timestamp() 30 | 31 | # 生成随机文件名称 32 | filename = f"{uuid.uuid4().hex}{os.path.splitext(file.filename)[1]}" 33 | 34 | save_path = AsyncPath(settings.storage.LOCAL_PATH) / path / filename 35 | if not await save_path.parent.exists(): 36 | await save_path.parent.mkdir(parents=True, exist_ok=True) 37 | await save_path.write_bytes(await file.read()) 38 | request_url = AsyncPath(settings.storage.LOCAL_BASE_URL) / path / filename 39 | return request_url.as_posix() 40 | -------------------------------------------------------------------------------- /kinit_fast_task/utils/storage/oss/__init__.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/7/1 3 | # @File : __init__.py 4 | # @IDE : PyCharm 5 | # @Desc : 阿里云对象存储 6 | 7 | 8 | -------------------------------------------------------------------------------- /kinit_fast_task/utils/storage/oss/oss.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/7/1 3 | # @File : oss.py 4 | # @IDE : PyCharm 5 | # @Desc : 文件描述信息 6 | import os 7 | import uuid 8 | from urllib.parse import urljoin 9 | 10 | import oss2 11 | from fastapi import UploadFile 12 | from oss2.models import PutObjectResult 13 | 14 | from kinit_fast_task.core import CustomException 15 | from kinit_fast_task.utils.storage import AbstractStorage 16 | from kinit_fast_task.config import settings 17 | from kinit_fast_task.utils import log 18 | 19 | 20 | class OSSStorage(AbstractStorage): 21 | """ 22 | 阿里云对象存储 23 | 24 | 常见报错:https://help.aliyun.com/document_detail/185228.htm?spm=a2c4g.11186623.0.0.6de530e5pxNK76#concept-1957777 25 | 官方文档:https://help.aliyun.com/document_detail/32026.html 26 | 27 | 使用Python SDK时,大部分操作都是通过oss2.Service和oss2.Bucket两个类进行 28 | oss2.Service类用于列举存储空间 29 | oss2.Bucket类用于上传、下载、删除文件以及对存储空间进行各种配置 30 | """ # noqa E501 31 | 32 | def __init__(self): 33 | # 阿里云账号AccessKey拥有所有API的访问权限,风险很高 34 | # 官方建议创建并使用RAM用户进行API访问或日常运维, 请登录RAM控制台创建RAM用户 35 | auth = oss2.Auth(settings.storage.OSS_ACCESS_KEY_ID, settings.storage.OSS_ACCESS_KEY_SECRET) 36 | # 创建Bucket对象,所有Object相关的接口都可以通过Bucket对象来进行 37 | self.bucket = oss2.Bucket(auth, settings.storage.OSS_ENDPOINT, settings.storage.OSS_BUCKET) 38 | self.baseUrl = settings.storage.OSS_BASE_URL 39 | 40 | async def save(self, file: UploadFile, *, path: str | None = None, accept: list = None, max_size: int = 50) -> str: 41 | """ 42 | 保存通用文件 43 | 44 | :param file: 文件 45 | :param path: 上传路径 46 | :param accept: 支持的文件类型 47 | :param max_size: 支持的文件最大值,单位 MB 48 | :return: 文件访问地址, 示例:https://ktianc.oss-cn-beijing.aliyuncs.com/resource/images/20240703/1719969784NPMyq0Jv.jpg 49 | """ # noqa E501 50 | await self.validate_file(file, max_size=max_size, mime_types=accept) 51 | if path is None: 52 | path = self.get_today_timestamp() 53 | 54 | # 生成随机文件名称 55 | filename = f"{uuid.uuid4().hex}{os.path.splitext(file.filename)[1]}" 56 | 57 | save_path = f"{path}/{filename}" 58 | result = self.bucket.put_object(save_path, await file.read()) 59 | assert isinstance(result, PutObjectResult) 60 | if result.status != 200: 61 | log.error(f"文件上传到OSS失败, 状态码:{result.status}") 62 | raise CustomException("文件上传到OSS失败!") 63 | return urljoin(self.baseUrl, save_path) 64 | -------------------------------------------------------------------------------- /kinit_fast_task/utils/storage/storage_factory.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Update Time : 2024/6/30 3 | # @File : file_factory.py 4 | # @IDE : PyCharm 5 | # @Desc : 文件上传工厂类 6 | from typing import Literal 7 | 8 | from kinit_fast_task.config import settings 9 | from kinit_fast_task.utils.storage import AbstractStorage 10 | from kinit_fast_task.utils.singleton import Singleton 11 | from kinit_fast_task.utils.storage import LocalStorage 12 | from kinit_fast_task.utils.storage import OSSStorage 13 | from kinit_fast_task.utils.storage import TempStorage 14 | 15 | 16 | class StorageFactory(metaclass=Singleton): 17 | _config_loader: dict[str, AbstractStorage] = {} 18 | 19 | @classmethod 20 | def get_instance(cls, loader_type: Literal["local", "temp", "oss", "kodo"]) -> AbstractStorage: 21 | """ 22 | 获取指定类型和加载器名称的文件存储实例,如果实例不存在则创建并加载到配置加载器 23 | """ 24 | if loader_type in cls._config_loader: 25 | return cls._config_loader[loader_type] 26 | 27 | if loader_type == "local": 28 | if not settings.storage.LOCAL_ENABLE: 29 | raise PermissionError("未启动本地文件存储功能, 如需要请开启 settings.storage.LOCAL_ENABLE!") 30 | loader = LocalStorage() 31 | elif loader_type == "temp": 32 | if not settings.storage.TEMP_ENABLE: 33 | raise PermissionError("未启动 TEMP 存储功能, 如需要请开启 settings.storage.TEMP_ENABLE!") 34 | loader = TempStorage() 35 | elif loader_type == "kodo": 36 | raise NotImplementedError("未实现七牛云文件上传功能!") 37 | # loader = KodoStorage() 38 | elif loader_type == "oss": 39 | if not settings.storage.OSS_ENABLE: 40 | raise PermissionError("未启动 OSS 存储功能, 如需要请开启 settings.storage.OSS_ENABLE!") 41 | loader = OSSStorage() 42 | else: 43 | raise KeyError(f"不存在的文件存储类型: {loader_type}") 44 | cls.register(loader_type, loader) 45 | return loader 46 | 47 | @classmethod 48 | def register(cls, loader_name: str, loader: AbstractStorage) -> None: 49 | """ 50 | 在配置加载管理器中注册一个配置加载器 51 | 52 | :param loader_name: 配置加载器名称 53 | :param loader: 配置加载器实例 54 | :return: 55 | """ 56 | cls._config_loader[loader_name] = loader 57 | 58 | @classmethod 59 | async def remove(cls, loader_name) -> bool: 60 | """ 61 | 从配置加载管理器中删除加载器 62 | 63 | :param loader_name: 配置加载器名称 64 | :return: 删除成功返回 True 65 | """ 66 | if loader_name in cls._config_loader: 67 | cls._config_loader.pop(loader_name) 68 | 69 | return True 70 | 71 | @classmethod 72 | async def clear(cls) -> bool: 73 | """ 74 | 清空加载器 75 | 76 | :return: 清空成功返回 True 77 | """ 78 | cls._config_loader.clear() 79 | 80 | return True 81 | -------------------------------------------------------------------------------- /kinit_fast_task/utils/storage/temp/__init__.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/7/8 3 | # @File : __init__.py.py 4 | # @IDE : PyCharm 5 | # @Desc : 文件描述信息 6 | -------------------------------------------------------------------------------- /kinit_fast_task/utils/storage/temp/temp.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/7/8 3 | # @File : temp.py 4 | # @IDE : PyCharm 5 | # @Desc : 临时文件存储 6 | 7 | import os 8 | import uuid 9 | 10 | from aiopathlib import AsyncPath 11 | from fastapi import UploadFile 12 | 13 | from kinit_fast_task.utils.storage import AbstractStorage 14 | from kinit_fast_task.config import settings 15 | 16 | 17 | class TempStorage(AbstractStorage): 18 | async def save(self, file: UploadFile, *, path: str | None = None, accept: list = None, max_size: int = 50) -> str: 19 | """ 20 | 保存通用文件 21 | 22 | :param file: 文件 23 | :param path: 上传路径 24 | :param accept: 支持的文件类型 25 | :param max_size: 支持的文件最大值,单位 MB 26 | :return: 文件保存地址,POSIX 风格路径, 示例:/temp/word/test.docs 27 | """ 28 | await self.validate_file(file, max_size=max_size, mime_types=accept) 29 | if path is None: 30 | path = self.get_today_timestamp() 31 | 32 | # 生成随机文件名称 33 | filename = f"{uuid.uuid4().hex}{os.path.splitext(file.filename)[1]}" 34 | 35 | save_path = AsyncPath(settings.storage.TEMP_PATH) / path / filename 36 | if not await save_path.parent.exists(): 37 | await save_path.parent.mkdir(parents=True, exist_ok=True) 38 | await save_path.write_bytes(await file.read()) 39 | return save_path.as_posix() 40 | -------------------------------------------------------------------------------- /kinit_fast_task/utils/tools.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2022/10/9 17:09 3 | # @File : tools.py 4 | # @IDE : PyCharm 5 | # @Desc : 工具类 6 | 7 | import datetime 8 | import random 9 | import re 10 | import string 11 | import importlib 12 | import subprocess 13 | from kinit_fast_task.utils import log 14 | from kinit_fast_task.core import CustomException 15 | 16 | 17 | def camel_to_snake(name: str) -> str: 18 | """ 19 | 将大驼峰命名(CamelCase)转换为下划线命名(snake_case) 20 | 在大写字母前添加一个空格,然后将字符串分割并用下划线拼接 21 | :param name: 大驼峰命名(CamelCase) 22 | :return: 23 | """ 24 | s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name) 25 | return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower() 26 | 27 | 28 | def snake_to_camel(name: str) -> str: 29 | """ 30 | 将下划线命名(snake_case)转换为大驼峰命名(CamelCase) 31 | 根据下划线分割,然后将字符串转为第一个字符大写后拼接 32 | :param name: 下划线命名(snake_case) 33 | :return: 34 | """ 35 | # 按下划线分割字符串 36 | words = name.split("_") 37 | # 将每个单词的首字母大写,然后拼接 38 | return "".join(word.capitalize() for word in words) 39 | 40 | 41 | def test_password(password: str) -> str | bool: 42 | """ 43 | 检测密码强度 44 | :param password: 原始密码 45 | :return: 46 | """ 47 | if len(password) < 8 or len(password) > 16: 48 | return "长度需为8-16个字符,请重新输入。" 49 | else: 50 | for i in password: 51 | if 0x4E00 <= ord(i) <= 0x9FA5 or ord(i) == 0x20: # Ox4e00等十六进制数分别为中文字符和空格的Unicode编码 52 | return "不能使用空格、中文,请重新输入。" 53 | else: 54 | key = 0 55 | key += 1 if bool(re.search(r"\d", password)) else 0 56 | key += 1 if bool(re.search(r"[A-Za-z]", password)) else 0 57 | key += 1 if bool(re.search(r"\W", password)) else 0 58 | if key >= 2: 59 | return True 60 | else: 61 | return "至少含数字/字母/字符2种组合,请重新输入。" 62 | 63 | 64 | def list_dict_find(options: list[dict], key: str, value: any) -> dict | None: 65 | """ 66 | 字典列表查找 67 | :param options: 68 | :param key: 69 | :param value: 70 | :return: 71 | """ 72 | return next((item for item in options if item.get(key) == value), None) 73 | 74 | 75 | def get_time_interval(start_time: str, end_time: str, interval: int, time_format: str = "%H:%M:%S") -> list: 76 | """ 77 | 获取时间间隔 78 | :param end_time: 结束时间 79 | :param start_time: 开始时间 80 | :param interval: 间隔时间(分) 81 | :param time_format: 字符串格式化,默认:%H:%M:%S 82 | """ 83 | if start_time.count(":") == 1: 84 | start_time = f"{start_time}:00" 85 | if end_time.count(":") == 1: 86 | end_time = f"{end_time}:00" 87 | start_time = datetime.datetime.strptime(start_time, "%H:%M:%S") 88 | end_time = datetime.datetime.strptime(end_time, "%H:%M:%S") 89 | time_range = [] 90 | while end_time > start_time: 91 | time_range.append(start_time.strftime(time_format)) 92 | start_time = start_time + datetime.timedelta(minutes=interval) 93 | return time_range 94 | 95 | 96 | def generate_string(length: int = 8) -> str: 97 | """ 98 | 生成随机字符串 99 | :param length: 字符串长度 100 | """ 101 | return "".join(random.sample(string.ascii_letters + string.digits, length)) 102 | 103 | 104 | def import_modules(modules: list, desc: str, **kwargs): 105 | """ 106 | 通过反射执行方法 107 | :param modules: 108 | :param desc: 109 | :param kwargs: 110 | :return: 111 | """ 112 | for module in modules: 113 | if not module: 114 | continue 115 | try: 116 | module_pag = importlib.import_module(module[0 : module.rindex(".")]) 117 | getattr(module_pag, module[module.rindex(".") + 1 :])(**kwargs) 118 | except ModuleNotFoundError as e: 119 | log.error(f"AttributeError:导入{desc}失败,模块:{module},详细报错信息:{e}") 120 | except AttributeError as e: 121 | log.error(f"ModuleNotFoundError:导入{desc}失败,模块方法:{module},详细报错信息:{e}") 122 | 123 | 124 | async def import_modules_async(modules: list, desc: str, **kwargs): 125 | """ 126 | 通过反射执行异步方法 127 | :param modules: 128 | :param desc: 129 | :param kwargs: 130 | :return: 131 | """ 132 | for module in modules: 133 | if not module: 134 | continue 135 | try: 136 | module_pag = importlib.import_module(module[0 : module.rindex(".")]) 137 | await getattr(module_pag, module[module.rindex(".") + 1 :])(**kwargs) 138 | except ModuleNotFoundError as e: 139 | log.error(f"AttributeError:导入{desc}失败,模块:{module},详细报错信息:{e}") 140 | except AttributeError as e: 141 | log.error(f"ModuleNotFoundError:导入{desc}失败,模块方法:{module},详细报错信息:{e}") 142 | 143 | 144 | def exec_shell_command( 145 | command: str, error_text: str = "命令执行失败", cwd: str = None, shell: bool = True, check: bool = False 146 | ) -> str: 147 | """ 148 | 执行 shell 命令 149 | :param command: 150 | :param error_text: 报错时的文本输出内容 151 | :param cwd: 命令执行工作目录 152 | :param shell: 153 | 当 shell=True 时,Python 将使用系统的默认 shell(比如在 Windows 下是 cmd.exe,而在 Unix/Linux 下是 /bin/sh)来执行指定的命令。 154 | 当 shell=False 时,Python 直接执行指定的命令,不经过 shell 解释器。 155 | 使用 shell=True 时,你可以像在命令行中一样使用一些特殊的 shell 功能,比如重定向 (>)、管道 (|) 等。 156 | 但要注意,使用 shell=True 也可能存在一些安全风险,因为它可以执行更复杂的命令,并且可能受到 shell 注入攻击的影响。 157 | :param check: 是否忽略错误 158 | :return: 159 | """ # noqa: E501 160 | result = subprocess.run([item for item in command.split(" ")], shell=shell, capture_output=True, text=True, cwd=cwd) 161 | if result.returncode != 0 and not check: 162 | raise CustomException( 163 | f"{error_text},执行命令:{command},结果 Code:{result.returncode},报错内容:{result.stdout}" 164 | ) 165 | return result.stdout 166 | 167 | 168 | def ruff_format_code(): 169 | """ 170 | 使用 ruff 格式化生成的代码 171 | """ 172 | try: 173 | exec_shell_command("ruff check --fix", check=True) 174 | exec_shell_command("ruff format", check=True) 175 | log.info("已完成代码格式化") 176 | except Exception as e: 177 | log.error(f"代码格式化失败: {e}") 178 | -------------------------------------------------------------------------------- /kinit_fast_task/utils/validator.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2021/10/18 22:19 3 | # @File : validator.py 4 | # @IDE : PyCharm 5 | # @Desc : pydantic 模型重用验证器 6 | 7 | """ 8 | 官方文档:https://pydantic-docs.helpmanual.io/usage/validators/#reuse-validators 9 | """ 10 | 11 | import json 12 | import re 13 | import datetime 14 | from bson import ObjectId 15 | 16 | 17 | def vali_telephone(value: str) -> str: 18 | """ 19 | 手机号验证器 20 | :param value: 手机号 21 | :return: 手机号 22 | """ 23 | if not value or len(value) != 11 or not value.isdigit(): 24 | raise ValueError("请输入正确手机号") 25 | 26 | regex = r"^1(3\d|4[4-9]|5[0-35-9]|6[67]|7[013-8]|8[0-9]|9[0-9])\d{8}$" 27 | 28 | if not re.match(regex, value): 29 | raise ValueError("请输入正确手机号") 30 | 31 | return value 32 | 33 | 34 | def vali_email(value: str) -> str: 35 | """ 36 | 邮箱地址验证器 37 | :param value: 邮箱 38 | :return: 邮箱 39 | """ 40 | if not value: 41 | raise ValueError("请输入邮箱地址") 42 | 43 | regex = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$" 44 | 45 | if not re.match(regex, value): 46 | raise ValueError("请输入正确邮箱地址") 47 | 48 | return value 49 | 50 | 51 | def datetime_str_vali(value: str | datetime.datetime | int | float | dict): 52 | """ 53 | 日期时间字符串验证 日期时间类型转字符串 54 | 如果我传入的是字符串,那么直接返回,如果我传入的是一个日期类型,那么会转为字符串格式后返回 55 | 因为在 pydantic 2.0 中是支持 int 或 float 自动转换类型的,所以我这里添加进去,但是在处理时会使这两种类型报错 56 | 57 | 官方文档:https://docs.pydantic.dev/dev-v2/usage/types/datetime/ 58 | """ 59 | if isinstance(value, str): 60 | pattern = "%Y-%m-%d %H:%M:%S" 61 | try: 62 | datetime.datetime.strptime(value, pattern) 63 | return value 64 | except ValueError: 65 | pass 66 | elif isinstance(value, datetime.datetime): 67 | return value.strftime("%Y-%m-%d %H:%M:%S") 68 | elif isinstance(value, dict): 69 | # 用于处理 mongodb 日期时间数据类型 70 | date_str = value.get("$date") 71 | date_format = "%Y-%m-%dT%H:%M:%S.%fZ" 72 | # 将字符串转换为datetime.datetime类型 73 | datetime_obj = datetime.datetime.strptime(date_str, date_format) 74 | # 将datetime.datetime对象转换为指定的字符串格式 75 | return datetime_obj.strftime("%Y-%m-%d %H:%M:%S") 76 | raise ValueError("无效的日期时间或字符串数据") 77 | 78 | 79 | def date_str_vali(value: str | datetime.date | int | float): 80 | """ 81 | 日期字符串验证 日期类型转字符串 82 | 如果我传入的是字符串,那么直接返回,如果我传入的是一个日期类型,那么会转为字符串格式后返回 83 | 因为在 pydantic 2.0 中是支持 int 或 float 自动转换类型的,所以我这里添加进去,但是在处理时会使这两种类型报错 84 | 85 | 官方文档:https://docs.pydantic.dev/dev-v2/usage/types/datetime/ 86 | """ 87 | if isinstance(value, str): 88 | pattern = "%Y-%m-%d" 89 | try: 90 | datetime.datetime.strptime(value, pattern) 91 | return value 92 | except ValueError: 93 | pass 94 | elif isinstance(value, datetime.date): 95 | return value.strftime("%Y-%m-%d") 96 | raise ValueError("无效的日期时间或字符串数据") 97 | 98 | 99 | def object_id_str_vali(value: str | dict | ObjectId): 100 | """ 101 | 官方文档:https://docs.pydantic.dev/dev-v2/usage/types/datetime/ 102 | """ 103 | if isinstance(value, str): 104 | return value 105 | elif isinstance(value, dict): 106 | return value.get("$oid") 107 | elif isinstance(value, ObjectId): 108 | return str(value) 109 | raise ValueError("无效的 ObjectId 数据类型") 110 | 111 | 112 | def dict_str_vali(value: str | dict): 113 | """ 114 | dict字符串验证 dict类型转字符串 115 | 如果我传入的是字符串,那么直接返回,如果我传入的是一个dict类型,那么会通过json序列化转为字符串格式后返回 116 | """ 117 | if isinstance(value, str): 118 | return value 119 | elif isinstance(value, dict): 120 | return json.dumps(value) 121 | raise ValueError("无效的 Dict 数据或字符串数据") 122 | 123 | 124 | def str_dict_vali(value: str | dict): 125 | """ 126 | dict str 验证 字符串 转 dict类型 127 | """ 128 | if isinstance(value, str): 129 | return json.loads(value) 130 | elif isinstance(value, dict): 131 | return value 132 | raise ValueError("无效的 Dict 数据或字符串数据") 133 | 134 | 135 | def str_list_vali(value: str | list): 136 | """ 137 | list str 验证 字符串 转 list 类型 138 | """ 139 | if isinstance(value, str): 140 | return json.loads(value) 141 | elif isinstance(value, list): 142 | return value 143 | raise ValueError("无效的 list 数据或 str 数据") 144 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2021/10/19 15:47 3 | # @File : main.py 4 | # @IDE : PyCharm 5 | # @Desc : 主程序入口 6 | 7 | """ 8 | FastApi 更新文档:https://github.com/tiangolo/fastapi/releases 9 | FastApi Github:https://github.com/tiangolo/fastapi 10 | Typer 官方文档:https://typer.tiangolo.com/ 11 | """ 12 | 13 | import typer 14 | 15 | from kinit_fast_task.utils import log 16 | 17 | shell_app = typer.Typer(rich_markup_mode="rich") 18 | 19 | 20 | @shell_app.command() 21 | def run(reload: bool = typer.Option(default=False, help="是否自动重载")): 22 | """ 23 | 项目启动 24 | 25 | 命令行执行(自动重启):python main.py run --reload 26 | 命令行执行(不自动重启):python main.py run 27 | 28 | 原始命令行执行(uvicorn):uvicorn kinit_fast_task.main:app 29 | 30 | # 在 pycharm 中使用自动重载是有 bug 的,很慢,截至 2024-04-30 还未修复,在使用 pycharm 开发时,不推荐使用 reload 功能 31 | 32 | :param reload: 是否自动重启 33 | :return: 34 | """ # noqa E501 35 | import uvicorn 36 | 37 | from kinit_fast_task.config import settings 38 | 39 | import sys 40 | import os 41 | 42 | sys.path.append(os.path.abspath(__file__)) 43 | 44 | uvicorn.run( 45 | app="kinit_fast_task.main:app", 46 | host=str(settings.system.SERVER_HOST), 47 | port=settings.system.SERVER_PORT, 48 | reload=reload, 49 | lifespan="on", 50 | ) 51 | 52 | 53 | @shell_app.command() 54 | def migrate(): 55 | """ 56 | 将模型迁移到数据库,更新数据库表结构 57 | 58 | 命令示例:python main.py migrate 59 | 60 | :return: 61 | """ 62 | from kinit_fast_task.utils.tools import exec_shell_command 63 | 64 | log.info("开始更新数据库表") 65 | exec_shell_command("alembic revision --autogenerate", "生成迁移文件失败") 66 | exec_shell_command("alembic upgrade head", "迁移至数据库失败") 67 | log.info("数据库表迁移完成") 68 | 69 | 70 | @shell_app.command() 71 | def generate( 72 | model: str = typer.Option(..., "--model", "-m", help="Model 类名, 示例:AuthUserModel"), 73 | app_name: str = typer.Option(..., "--app-name", "-n", help="功能英文名称, 主要用于 Schema, Params, CRUD, URL 命名"), 74 | app_desc: str = typer.Option(..., "--app-desc", "-d", help="功能中文名称, 主要用于描述和注释"), 75 | write_only: bool = typer.Option(False, "--write-only", "-w", help="是否只写入文件"), 76 | overwrite: bool = typer.Option(False, "--overwrite", "-o", help="是否在写入时覆盖文件"), 77 | ): 78 | """ 79 | 基于 ORM Model 生成 app 代码 80 | 81 | 命令示例(输出代码模式):python main.py generate -m AuthTestModel -n auth_test -d 测试 82 | 命令示例(写入代码不覆盖模式):python main.py generate -m AuthTestModel -n auth_test -d 测试 -w 83 | 命令示例(写入代码并覆盖模式):python main.py generate -m AuthTestModel -n auth_test -d 测试 -w -o 84 | """ # noqa E501 85 | from kinit_fast_task.scripts.app_generate.main import AppGenerate 86 | 87 | ag = AppGenerate(verbose=False) 88 | ag.model_to_code( 89 | model_class_name=model, app_name=app_name, app_desc=app_desc, write_only=write_only, overwrite=overwrite 90 | ) 91 | 92 | 93 | if __name__ == "__main__": 94 | shell_app() 95 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "kinit-fast-task" 3 | version = "5.0.0" 4 | description = "FastAPI 技术栈最佳实践" 5 | authors = ["ktianc <2445667550@qq.com>"] 6 | license = "MIT" 7 | readme = "README.md" 8 | homepage = "https://python-poetry.org/" 9 | repository = "https://github.com/python-poetry/poetry" 10 | documentation = "https://python-poetry.org/docs/main/pyproject/" 11 | packages = [ 12 | { include = "kinit_fast_task" }, 13 | ] 14 | 15 | 16 | [tool.poetry.dependencies] 17 | python = "^3.10" 18 | pydantic-settings = "^2.2.1" 19 | fastapi = "^0.111.0" 20 | sqlalchemy = "^2.0.29" 21 | pydantic = "^2.6.4" 22 | loguru = "^0.7.2" 23 | user-agents = "^2.2.0" 24 | redis = "^5.0.3" 25 | pycryptodome = "^3.20.0" 26 | alibabacloud-tea-openapi = "^0.3.8" 27 | alibabacloud-dysmsapi20170525 = "^2.0.24" 28 | oss2 = "^2.18.4" 29 | aiopathlib = "^0.5.0" 30 | aioshutil = "^1.3" 31 | openpyxl = "^3.1.2" 32 | xlsxwriter = "^3.2.0" 33 | uvicorn = "^0.29.0" 34 | typer = "^0.12.3" 35 | asyncpg = "^0.29.0" 36 | alembic = "^1.13.1" 37 | motor = "^3.4.0" 38 | 39 | [tool.poetry.group.dev.dependencies] 40 | pytest = "^8.1.1" 41 | pytest-html = "^4.1.1" 42 | pytest-dotenv = "^0.5.2" 43 | ruff = "^0.4.1" 44 | pre-commit = "^3.7.0" 45 | 46 | 47 | [build-system] 48 | requires = ["poetry-core"] 49 | build-backend = "poetry.core.masonry.api" 50 | 51 | 52 | [[tool.poetry.source]] 53 | name = "aliyun" 54 | url = "https://mirrors.aliyun.com/pypi/simple/" 55 | priority = "primary" -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | env_files = .env.test 3 | addopts = --html=report.html --self-contained-html 4 | minversion = 8.0 5 | testpaths = tests 6 | python_files = test_*.py 7 | python_classes = Test* 8 | python_functions = test_* 9 | -------------------------------------------------------------------------------- /ruff.toml: -------------------------------------------------------------------------------- 1 | # ruff 官方文档:https://docs.astral.sh/ruff/ 2 | # ruff 所有规则:https://docs.astral.sh/ruff/rules/ 3 | # ruff 所有配置:https://docs.astral.sh/ruff/settings/ 4 | # ruff GitHub:https://github.com/astral-sh/ruff 5 | 6 | # 如果选择使用 ruff 工具,那么你需要花一定的时间来了解它,适配它,这样它才会成为一个友好的工具, 7 | # 以下规则都不是定死的,如果有些规则在使用中使你很不舒服,那么我建议你将它添加到 ignore 中,这样就可以将它忽略掉 8 | 9 | # ruff 命令行工具参数详解:官方文档:https://docs.astral.sh/ruff/configuration/#full-command-line-interface 10 | # check - 在指定的文件或目录上运行 Ruff 进行检查(默认命令)。这是主要命令,用于分析代码,查找潜在的错误或代码风格问题。 11 | # rule - 解释一个或所有规则。这个命令可以用来获取特定规则的详细信息或者展示所有可用规则的列表及其说明。 12 | # config - 列出或描述可用的配置选项。通过这个命令,用户可以查看或调整 ruff 的配置设置。 13 | # linter - 列出所有支持的上游 linter(代码检查工具)。这个命令有助于了解 ruff 集成了哪些其他的代码检查工具。 14 | # clean - 清除当前目录及其所有子目录中的任何缓存。这通常在解决性能问题或缓存相关的错误时使用。 15 | # format - 在给定的文件或目录上运行 Ruff 格式化器。这个命令用于自动调整代码格式,以符合一定的编码标准。 16 | # server - 运行语言服务器。这通常用于在编辑器或 IDE 中实时提供代码分析和自动完成建议。 17 | # version - 显示 Ruff 的版本。通过这个命令可以检查正在使用的 Ruff 版本。 18 | # help - 打印此消息或给定子命令的帮助信息。如果你需要了解更多关于任何特定命令的信息,这个命令可以提供帮助。 19 | 20 | # 预览模式:官方文档:https://docs.astral.sh/ruff/preview/ 21 | # 1. 开启预览模式会启用了一系列不稳定的功能,例如新的lint规则和修复、格式化器样式更改、界面更新等 22 | # 2. 在使用预览模式时,有关已弃用功能的警告可能会变成错误 23 | 24 | line-length = 120 # 允许行长度最长为 120 25 | exclude = [".venv", "alembic", "docs", "kinit_fast_task/utils/love.py", "example"] # 要从格式设置和 linting 中排除的文件列表 26 | indent-width = 4 # 每个缩进级别的空格数 27 | unsafe-fixes = false # 不允许执行不安全的修复操作 28 | cache-dir = ".ruff_cache" # 指定 Ruff 的缓存目录为 .ruff_cache, Ruff 在执行 lint 或 format 等操作时可能会生成一些缓存文件,以提高后续运行时的效率 29 | target-version = "py310" # 指定 Ruff 的目标 Python 版本为 3.10, 这个配置项告诉 Ruff 应该按照 Python 3.10 的语法规范来检查和格式化代码 30 | fix = false # 不启用自动修复功能 31 | 32 | [lint.per-file-ignores] 33 | # 忽略所有 `__init__.py` 文件中的 `F401` 34 | "__init__.py" = ["F401"] 35 | 36 | [lint] # 代码检查与代码修复规则,代码书写不规范的检查与修复 37 | select = [ # 选择特定的错误类型进行检查 38 | "E", # pycodestyle 39 | "F", # Pyflakes 40 | "UP", # pyupgrade 41 | "B", # flake8-bugbear 42 | # "SIM", # flake8-simplify,简洁语法,觉得这个是非必须的规则 43 | "I", # isort 44 | "FA", # flake8-future-annotations 45 | ] 46 | preview = true # 启用预览模式,即会使用最新的规则,以及去掉标记弃用规则 47 | ignore = ["I001", "E203", "B008", "F821", "B904"] # 指定完全忽略的错误代码列表 48 | fixable = ["ALL"] # 允许自动修复所有可修复的规则 49 | unfixable = [] # 列出不可自动修复的规则 50 | dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" # 定义哪些变量名被视为占位符或“哑”变量,通常以一个或多个下划线开头 51 | 52 | 53 | # 代码格式化命令: 使用 black 的过程: 54 | # 指定目录进行格式化:ruff format you_dir_name 55 | # 检查有哪些文件需要格式化: ruff format --check 56 | # 检查格式化的代码差异: ruff format --check --diff 57 | # black future style:https://black.readthedocs.io/en/stable/the_black_code_style/future_style.html 58 | [format] # 代码格式化,代码格式不规范的修复 59 | preview = true # 启用预览模式, 使用 black future style, 60 | quote-style = "double" # 使用双引号来包围字符串 61 | indent-style = "space" # 使用空格进行缩进 62 | skip-magic-trailing-comma = false # 不跳过魔术尾随逗号,即保留在多行结构末尾自动添加的逗号 63 | line-ending = "auto" # 自动检测并使用合适的行结束符 64 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/4/7 16:50 3 | # @File : __init__.py 4 | # @IDE : PyCharm 5 | # @Desc : 描述信息 6 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # @Version : 1.0 2 | # @Create Time : 2024/4/1 13:58 3 | # @File : conftest.py 4 | # @IDE : PyCharm 5 | # @Desc : 描述信息 6 | 7 | import pytest 8 | 9 | 10 | @pytest.fixture(scope="function") 11 | def client(): 12 | pass 13 | 14 | 15 | @pytest.fixture(scope="function") 16 | def resource_setup(): 17 | yield 18 | 19 | 20 | @pytest.fixture(scope="session") 21 | def session_resource_setup(): 22 | yield 23 | --------------------------------------------------------------------------------