[
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n.idea\n\n.DS_Store\nnode_modules/\ndist/\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npackage-lock.json\ntests/**/coverage/\n\n# Editor directories and files\n.idea\n.vscode\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n.hypothesis/\n.pytest_cache/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\n*.sqlite\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# pyenv\n.python-version\n\n# celery beat schedule file\ncelerybeat-schedule\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n\n*/*.yaml\n*/core/config/config.py"
  },
  {
    "path": "Pipfile",
    "content": "[[source]]\nname = \"pypi\"\nurl = \"https://pypi.org/simple\"\nverify_ssl = true\n\n[dev-packages]\n\n[packages]\n\n[requires]\npython_version = \"3.8\"\n"
  },
  {
    "path": "README-en.md",
    "content": "# FastAPI and MySql - Base Project Generator\n\n![Python版本](https://img.shields.io/badge/Python-3.7+-brightgreen.svg \"版本号\")\n![FastAPI版本](https://img.shields.io/badge/FastAPI-0.61.1-ff69b4.svg \"版本号\")\n\n[中文说明](./README.md) | [English](./README-en.md)\n\n## Intro\nWeb application generator. Using FastAPI, MySql as databas.I am referring to tianolo's full-stack-fastapi-postgresql\n\nhttps://github.com/tiangolo/full-stack-fastapi-postgresql\n\nI changed it to my favorite format.\n\n![demo](images/demo1.png)\n\n\n## Features\n- JWT token authentication.\n- SQLAlchemy models(MySql).\n- Alembic migrations.\n- Use redis demo.\n- File upload demo.\n- apscheduler cron task (:noqa)\n\n\n## How to use it\n\nGo to the directory where you want to create your project and run:\n\n```\npip install cookiecutter\ncookiecutter https://github.com/CoderCharm/fastapi-mysql-generator\n\ncd your_project/\npip install -r requirements.txt\n```\n\n## Configure your database\n\nIn the `project/app/core/config/development_config.py` or `production_config.py` file.\n\nMySql and Redis config info\n\n## Migrate the database\n\n```\ncd your_project/\n\n# Generate mapping\nalembic revision --autogenerate -m \"init commit\"\n\n# Generate tables\nalembic upgrade head\n```\n\n## Create admin user\n\n```\ncd your_project/app\npython create_user.py\n```\n\n## Run\n```\ncd your_project/app\n\npython main.py\n\nor \n\nuvicorn main:app --host=127.0.0.1 --port=8010 --reload\n```\n\nonline doc address\n```\nhttp://127.0.0.1:8010/api/docs\n```"
  },
  {
    "path": "README.md",
    "content": "# FastAPI and MySQL - 项目生成器\n\n![Python版本](https://img.shields.io/badge/Python-3.7+-brightgreen.svg \"版本号\")\n![FastAPI版本](https://img.shields.io/badge/FastAPI-0.62+-ff69b4.svg \"版本号\")\n\n[中文说明](./README.md) | [English](./README-en.md)\n\n## 简介\n使用FastAPI + MySql 作为数据库的项目生成器, 我是参考FastAPI作者[tiangolo](https://github.com/tiangolo)的 [full-stack-fastapi-postgresql](https://github.com/tiangolo/full-stack-fastapi-postgresql) 项目做的。\n\n我把它改成了自己喜欢的格式。很大程度参考了[奇淼 gin-vue-admin项目](https://github.com/flipped-aurora/gin-vue-admin)\n\n![demo](images/demo1.png)\n\n\n## 功能\n- JWT token 认证。\n- 使用SQLAlchemy models(MySql).\n- Alembic migrations 数据迁移.\n- redis使用演示.\n- 文件上传演示.\n- apscheduler 定时任务 (不保证稳定 noqa)\n- 基于 casbin 的权限验证 (基于 [gin-vue-admin](https://github.com/flipped-aurora/gin-vue-admin) 复刻)\n\n\n## 学习博客\n\n<details>\n<summary>项目学习博客点击展开</summary>\n\n- [【FastAPI 学习 一】配置文件](https://www.cnblogs.com/CharmCode/p/14191077.html)\n- [【FastAPI 学习 二】SqlAlchemy Model模型类](https://www.cnblogs.com/CharmCode/p/14191082.html)\n- [【FastAPI 学习 三】FastAPI SqlAlchemy MySql表迁移](https://www.cnblogs.com/CharmCode/p/14191090.html)\n- [【FastAPI 学习 四】日志配置](https://www.cnblogs.com/CharmCode/p/14191091.html)\n- [【FastAPI 学习 五】统一响应json数据格式](https://www.cnblogs.com/CharmCode/p/14191093.html)\n- [【FastAPI 学习 六】异常处理](https://www.cnblogs.com/CharmCode/p/14191103.html)\n- [【FastAPI 学习 七】GET和POST请求参数接收以及验证](https://www.cnblogs.com/CharmCode/p/14191108.html)\n- [【FastAPI 学习 八】JWT token认证登陆](https://www.cnblogs.com/CharmCode/p/14191112.html)\n- [【FastAPI 学习 九】图片文件上传](https://www.cnblogs.com/CharmCode/p/14191116.html)\n- [【FastAPI 学习 十】使用Redis](https://www.cnblogs.com/CharmCode/p/14191119.html)\n- [【FastAPI 学习 十一】项目目录结构demo(自己改版)](https://www.cnblogs.com/CharmCode/p/14191126.html)\n- [【FastAPI 学习 十二】定时任务篇](https://www.cnblogs.com/CharmCode/p/14191009.html)\n- [【FastAPI 学习 十三】基于`casbin`的权限校验](https://www.cnblogs.com/CharmCode/p/14377547.html)\n\n</details>\n\n## 项目文件组织\n\n> 参考Django文件组织,FastAPI官方推荐项目生成,Flask工厂函数,[gin-vue-admin server文件组织](https://github.com/flipped-aurora/gin-vue-admin)\n\n<details>\n<summary>点击展开项目文件结构</summary>\n\n```\n.your_project\n|__alembic                       // alembic 自动生成的迁移配置文件夹,迁移不正确时 产看其中的env.py文件\n| |__versions/                   // 使用 alembic revision --autogenerate -m \"注释\" 迁移命令后 会产生映射文件\n| |__env.py                      // 自动生成的文件\n| |__script.py.mako\n|__alembic.ini                   // alembic 自动生成的迁移配置文件\n\n|____api                         // API文件夹\n| |____v1                        // 版本区分\n| | | |____items.py              // 一些接口示例\n| | | |____sys_api.py            // API操作 用于权限管理\n| | | |____sys_casbin.py         // 添加指定角色权限\n| | | |____sys_scheduler.py      // 定时任务调度模块\n| | | |____sys_user.py           // user 模块\n\n| |____common                    // 项目通用文件夹\n| | |______init__.py             // 导出日志文件方便导入\n| | |____custom_exc.py           // 自定义异常\n| | |____deps.py                 // 通用依赖文件,如数据库操作对象,权限验证对象\n| | |____logger.py               // 扩展日志 loguru 简单配置\n| | |____sys_casbin.py           // 生成 casbin\n| | |____sys_scheduler.py        // 定义 apscheduler 在core/server 下初始化\n\n|____core                        \n| |____config                    // 配置文件\n| | |______init__.py             // 根据虚拟环境导入不同配置\n| | |____development_config.py   // 开发配置\n| | |____production_config.py    // 生产配置\n| |____celery_app.py             // celery (目前没有使用)\n| |____security.py               // token password验证  \n| |____server.py                 // 核心服务文件(重要) 初始化连接等操作\n        \n| |____db                        // 数据库\n| | |____base.py                 // 导出全部models 给alembic迁移用\n| | |____base_class.py           // orm model 基类\n| | |____session.py              // 链接数据库会话\n| | |____sys_redis.py            // 生成redis对象\n\n|____logs/                       // 日志文件夹\n\n| |____models                    // orm models 在这里面新增 (记得导入到 /db/base.py 下面才会迁移成功)\n| | |____sys_api.py              // 项目API model\n| | |____sys_auth.py             // 用户模块orm\n         \n| |____resource                  // 存放casbin model\n| | |____rbac_model.conf         // casbin model匹配规则\n\n| |____router                    // 路由模块\n| | |____v1_router.py            // V1 API分组路由文件(可在这里按照分组添加权限验证)\n\n| |____schemas                   // 请求 或者 响应的 Pydantic model(这里的schemas和model应该整合到一起 作者目前也写了一个 pydantic-sqlalchemy 互转的库 https://github.com/tiangolo/pydantic-sqlalchemy 但是感觉不太完善)\n| | |____request                 // 数据验证 model \n| | |____response                // 数据响应 model(我项目里面暂时没有写响应model) \n\n| |____service                   // ORM 操作文件夹\n| | |____curd_base.py            // curd通用基础操作对象\n| | |____sys_user.py             // user curd操作\n\n|____static/                     // 静态资源文件(测试时使用，生产建议使用nginx静态资源服务器或者七牛云)\n         \n|____tests/                      // 测试文件夹\n\n| |____utils                     // 工具类\n| | |____cron_task.py            // 定时任务task文件\n| | |____tools_func.py           // 序列化orm特殊时间(但是感觉不太优雅)\n\n|____jobs.sqlite                 // 定时任务持久化sqlite 也可以使用其他的比如redis\n|____main.py                     // 启动文件\n|____init_db.py                  // 生成初始化角色和用户\n|____requirements.txt            // 依赖文件\n\n```\n\n</details>\n\n\n## 如何使用\n\n进入你想要生成项目的文件夹下，并且运行以下命令。\n\n```\npip install cookiecutter\ncookiecutter https://github.com/CoderCharm/fastapi-mysql-generator\n\ncd your_project/\n# 安装依赖库\npip install -r requirements.txt\n\n# 建议使用 --upgrade 安装最新版 (windows系统下uvloop当前版本可能有问题  https://github.com/MagicStack/uvloop/issues/14)\npip install --upgrade -r requirements-dev.txt\n```\n\n## 配置你的数据库环境\n\n在这个文件下 `project/app/core/config/development_config.py` 或者 `production_config.py`。\n\n配置MySQL和Redis信息\n\n## 迁移数据库\n\n> 放弃ORM自带的迁移功能，使用第三方工具 `sql-migrate`，更直观便于管理。可参考 [sql-migrate](https://github.com/rubenv/sql-migrate)\n\n\n`sql-migrate status -env=dev-mysql`\n\n`sql-migrate up -env=dev-mysql`\n\n`sql-migrate down -env=dev-mysql`\n\n\n\n## 创建用户\n> 会默认创建两个角色一个为超级管理员角色，一个为普通角色，\n超级管理员拥有目前接口的所有调用权限，普通用户只能登录和获取自身用户信息.\n```\ncd your_project/\npython init_db.py\n```\n\n## 运行启动\n\n```\n# 进入项目文件夹下\ncd your_project/\n\n# 命令行运行(开发模式)\nuvicorn main:app --host=127.0.0.1 --port=8010 --reload\n\n# 直接运行main文件(会打印两次路由)\npython main.py\n```\n\n<details>\n<summary>可能会出现的常见路径倒入问题</summary>\n\n```\n# 如下两种解决方式\n\n# pycharm中设置 标记为sources root\nhttps://www.jetbrains.com/help/pycharm/configuring-content-roots.html#specify-folder-categories\n\n# 命令行中标记为 sources root\nhttps://stackoverflow.com/questions/30461982/how-to-provide-make-directory-as-source-root-from-pycharm-to-terminal\n\n```\n</details>\n\n在线文档地址(在配置文件里面设置路径或者关闭)\n```\nhttp://127.0.0.1:8010/api/docs\n```\n\n## 接口测试\n\n需要安装\n\n```shell\npip install pytest\n```\n\n在项目下 启动测试用例\n```\ncd your_project/\npytest\n\n# 接口测试结果\ncollected 10 items                                                                                                                             \n\ntests/api/v1/test_casbin.py ....                                                                                                         [ 40%]\ntests/api/v1/test_cron.py ...                                                                                                            [ 70%]\ntests/api/v1/test_user.py ...                                                                                                            [100%]\n\n============================================================== 10 passed in 1.77s ==============================================================\n```\n\n\n## 部署\n\n部署的时候，可以关闭在线文档，见学习文章一配置篇。\n\n```shell\n在main.py同文件下下启动 去掉 --reload 选项 增加 --workers\nuvicorn main:app --host=127.0.0.1 --port=8010 --workers=4\n\n# 同样可以也可以配合gunicorn多进程启动  main.py同文件下下启动 默认127.0.0.1:8010端口 gunicorn需要安装\n# 参考http://www.uvicorn.org/#running-with-gunicorn\ngunicorn main:app -w 4 -k uvicorn.workers.UvicornWorker -b 127.0.0.1:8010\n```\n<details>\n<summary>点击查看托管到后台运行</summary>\n  \n```shell\n# 1 如果为了简单省事 可以直接使用 nohup 命令 如下: run.log文件需要自行创建\nnohup /env_path/uvicorn main:app --host=127.0.0.1 --port=8010 --workers=4 > run.log 2>&1 &\n\n# 2 可以使用supervisor托管后台运行部署, 当然也可以使用其他的\n# supervisor可以参考我总结的文章 https://www.cnblogs.com/CharmCode/p/14210280.html\n```\n</details>\n"
  },
  {
    "path": "cookiecutter.json",
    "content": "{\n    \"project_name\": \"fastapi_project\"\n}"
  },
  {
    "path": "examples/demo_casbin/01_demo.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2021/1/27 19:54\n# @Author  : CoderCharm\n# @File    : simple_example.py\n# @Software: PyCharm\n# @Github  : github/CoderCharm\n# @Email   : wg_python@163.com\n# @Desc    :\n\"\"\"\n\n主要参考官方readme\n\n\n\"\"\"\n\nimport casbin\n\ne = casbin.Enforcer('./model.conf', \"./policy.csv\", True)\n\nsub = \"nick\"  # the user that wants to access a resource.\nobj = \"data1\"  # the resource that is going to be accessed.\nact = \"read\"  # the operation that the user performs on the resource.\n\n# 添加\n# e.add_policy(\"alice\", \"data1\", \"read\")\n# e.remove_policy(\"nick\", \"data1\", \"read\")\n\n\nif e.enforce(sub, obj, act):\n    # permit alice to read data1casbin_sqlalchemy_adapter\n    print(\"通过\")\nelse:\n    # deny the request, show an error\n    print(\"拒绝\")\n"
  },
  {
    "path": "examples/demo_casbin/02_orm.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2021/1/27 20:06\n# @Author  : CoderCharm\n# @File    : casbin_orm.py\n# @Software: PyCharm\n# @Github  : github/CoderCharm\n# @Email   : wg_python@163.com\n# @Desc    :\n\"\"\"\nhttps://github.com/pycasbin/sqlalchemy-adapter/blob/master/README.md\n\npip install casbin_sqlalchemy_adapter\n\n\"\"\"\n\nimport casbin\nimport casbin_sqlalchemy_adapter\n\nadapter = casbin_sqlalchemy_adapter.Adapter('sqlite:///test.db')\n\ne = casbin.Enforcer('./model.conf', adapter, True)\n\nsub = \"nick\"  # the user that wants to access a resource.\nobj = \"data1\"  # the resource that is going to be accessed.\nact = \"read\"  # the operation that the user performs on the resource.\n\n# 添加\n# res = e.add_policy(\"alice\", \"data1\", \"read\")\n# res = e.remove_policy(\"alice\", \"data1\", \"read\")\nprint(res)\n\n# if e.enforce(sub, obj, act):\n#     # permit alice to read data1casbin_sqlalchemy_adapter\n#     print(\"通过\")\n# else:\n#     # deny the request, show an error\n#     print(\"拒绝\")\n\n"
  },
  {
    "path": "examples/demo_casbin/03_custom_orm.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2021/1/29 20:49\n# @Author  : CoderCharm\n# @File    : casbin_custom_orm.py\n# @Software: PyCharm\n# @Github  : github/CoderCharm\n# @Email   : wg_python@163.com\n# @Desc    :\n\"\"\"\n\n加一个自定义的解析函数\n\n\"\"\"\n\nimport casbin\nfrom casbin import util\nimport casbin_sqlalchemy_adapter\n\nadapter = casbin_sqlalchemy_adapter.Adapter('sqlite:///test.db')\n\ne = casbin.Enforcer('./custom_model.conf', adapter, True)\n\n\n# 添加\n# e.add_policy(\"999\", \"/api/user\", \"GET\")\n# 删除\n# e.remove_policy(\"999\", \"/api/user\", \"GET\")\n\n\ndef params_match(full_name_k1: str, key2: str):\n    \"\"\"\n    去掉url ?后面的参数 只取路径\n    :param full_name_k1:\n    :param key2:\n    :return:\n    \"\"\"\n    key1 = full_name_k1.split(\"?\")[0]\n    return util.key_match2(key1, key2)\n\n\ndef params_match_func(*args):\n    name1 = args[0]\n    name2 = args[1]\n    return params_match(name1, name2)\n\n\ne.add_function(\"ParamsMatch\", params_match_func)\n\nsub = \"999\"  # the user that wants to access a resource.\nobj = \"/api/user?aaa=1\"  # the resource that is going to be accessed.\nact = \"GET\"  # the operation that the user performs on the resource.\n\nif e.enforce(sub, obj, act):\n    # permit alice to read data1casbin_sqlalchemy_adapter\n    print(\"通过\")\nelse:\n    # deny the request, show an error\n    print(\"拒绝\")\n"
  },
  {
    "path": "examples/demo_casbin/README.md",
    "content": "# casbin 权限认证管理\n\n> 一个权限认证的库，主要是用于Go语言的，也实现了其他语言的扩展。我也是看到[奇淼](https://github.com/piexlmax)的B站关于这个的介绍，所以才接触到这个\n所以想把这引入到FastAPI中。\n\n## 安装依赖\n```\npip install casbin\npip install casbin_sqlalchemy_adapter\n```\n\n## 学前准备\n\n可以先在 [官方编辑器](https://casbin.org/zh-CN/editor)里面弄明白不同访问策略的原理。\n\n\n## 参考\n\n- [官网](https://casbin.org/zh-CN/)\n- [官方编辑器](https://casbin.org/zh-CN/editor)\n- [奇淼B站关于casbin介绍 基础概念理解](https://www.bilibili.com/video/BV1qz4y167XP)\n"
  },
  {
    "path": "examples/demo_casbin/custom_model.conf",
    "content": "[request_definition]\nr = sub, obj, act\n\n[policy_definition]\np = sub, obj, act\n\n[role_definition]\ng = _, _\n\n[policy_effect]\ne = some(where (p.eft == allow))\n\n[matchers]\nm = r.sub == p.sub && ParamsMatch(r.obj,p.obj) && r.act == p.act"
  },
  {
    "path": "examples/demo_casbin/model.conf",
    "content": "[request_definition]\nr = sub, obj, act\n\n[policy_definition]\np = sub, obj, act\n\n[policy_effect]\ne = some(where (p.eft == allow))\n\n[matchers]\nm = r.sub == p.sub && r.obj == p.obj && r.act == p.act"
  },
  {
    "path": "examples/demo_casbin/policy.csv",
    "content": "p, nick, data1, read\np, nick, data2, write"
  },
  {
    "path": "examples/demo_scheduler/main.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2020/12/25 20:08\n# @Author  : CoderCharm\n# @File    : main.py\n# @Software: PyCharm\n# @Github  : github/CoderCharm\n# @Email   : wg_python@163.com\n# @Desc    :\n\"\"\"\n\n基础的任务调度演示\n\n\"\"\"\nimport time\nfrom typing import Union\nfrom datetime import datetime\n\nfrom fastapi import FastAPI, Query, Body\nfrom apscheduler.schedulers.asyncio import AsyncIOScheduler\nfrom apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore\nfrom apscheduler.triggers.cron import CronTrigger\n\nSchedule = AsyncIOScheduler(\n    jobstores={\n        'default': SQLAlchemyJobStore(url='sqlite:///jobs.sqlite')\n    }\n)\nSchedule.start()\n\napp = FastAPI()\n\n\n# 简单定义返回\ndef resp_ok(*, code=0, msg=\"ok\", data: Union[list, dict, str] = None) -> dict:\n    return {\"code\": code, \"msg\": msg, \"data\": data}\n\n\ndef resp_fail(*, code=1, msg=\"fail\", data: Union[list, dict, str] = None):\n    return {\"code\": code, \"msg\": msg, \"data\": data}\n\n\ndef cron_task(a1: str) -> None:\n    print(a1, time.strftime(\"'%Y-%m-%d %H:%M:%S'\"))\n\n\n@app.get(\"/jobs/all\", tags=[\"schedule\"], summary=\"获取所有job信息\")\nasync def get_scheduled_syncs():\n    \"\"\"\n    获取所有job\n    :return:\n    \"\"\"\n    schedules = []\n    for job in Schedule.get_jobs():\n        schedules.append(\n            {\"job_id\": job.id, \"func_name\": job.func_ref, \"func_args\": job.args, \"cron_model\": str(job.trigger),\n             \"next_run\": str(job.next_run_time)}\n        )\n    return resp_ok(data=schedules)\n\n\n@app.get(\"/jobs/once\", tags=[\"schedule\"], summary=\"获取指定的job信息\")\nasync def get_target_sync(\n        job_id: str = Query(\"job1\", title=\"任务id\")\n):\n    job = Schedule.get_job(job_id=job_id)\n\n    if not job:\n        return resp_fail(msg=f\"not found job {job_id}\")\n\n    return resp_ok(\n        data={\"job_id\": job.id, \"func_name\": job.func_ref, \"func_args\": job.args, \"cron_model\": str(job.trigger),\n              \"next_run\": str(job.next_run_time)})\n\n\n# interval 固定间隔时间调度\n@app.post(\"/job/interval/schedule/\", tags=[\"schedule\"], summary=\"开启定时:间隔时间循环\")\nasync def add_interval_job(\n        seconds: int = Body(120, title=\"循环间隔时间/秒,默认120s\", embed=True),\n        job_id: str = Body(..., title=\"任务id\", embed=True),\n        run_time: int =Body(time.time(), title=\"第一次运行时间\", description=\"默认立即执行\", embed=True)\n):\n    res = Schedule.get_job(job_id=job_id)\n    if res:\n        return resp_fail(msg=f\"{job_id} job already exists\")\n    schedule_job = Schedule.add_job(cron_task,\n                                    'interval',\n                                    args=(job_id,),\n                                    seconds=seconds,  # 循环间隔时间 秒\n                                    id=job_id,  # job ID\n                                    next_run_time=datetime.fromtimestamp(run_time)  # 立即执行\n                                    )\n    return resp_ok(data={\"job_id\": schedule_job.id})\n\n\n# date 某个特定时间点只运行一次\n@app.post(\"/job/date/schedule/\", tags=[\"schedule\"], summary=\"开启定时:固定只运行一次时间\")\nasync def add_date_job(\n        run_time: int = Body(..., title=\"时间戳\", description=\"固定只运行一次时间\", embed=True),\n        job_id: str = Body(..., title=\"任务id\", embed=True),\n):\n    res = Schedule.get_job(job_id=job_id)\n    if res:\n        return resp_fail(msg=f\"{job_id} job already exists\")\n    schedule_job = Schedule.add_job(cron_task,\n                                    'date',\n                                    args=(job_id,),\n                                    run_date=datetime.fromtimestamp(run_time),\n                                    id=job_id,  # job ID\n                                    )\n    return resp_ok(data={\"job_id\": schedule_job.id})\n\n\n# cron 更灵活的定时任务 可以使用crontab表达式\n@app.post(\"/job/cron/schedule/\", tags=[\"schedule\"], summary=\"开启定时:crontab表达式\")\nasync def add_cron_job(\n        job_id: str = Body(..., title=\"任务id\", embed=True),\n        crontab: str = Body('*/1 * * * *', title=\"crontab 表达式\"),\n        run_time: int =Body(time.time(), title=\"第一次运行时间\", description=\"默认立即执行\", embed=True)\n):\n    res = Schedule.get_job(job_id=job_id)\n    if res:\n        return resp_fail(msg=f\"{job_id} job already exists\")\n    schedule_job = Schedule.add_job(cron_task,\n                                    CronTrigger.from_crontab(crontab),\n                                    args=(job_id,),\n                                    id=job_id,  # job ID\n                                    next_run_time=datetime.fromtimestamp(run_time)\n                                    )\n    return resp_ok(data={\"job_id\": schedule_job.id})\n\n\n@app.post(\"/job/del\", tags=[\"schedule\"], summary=\"移除任务\")\nasync def remove_schedule(\n        job_id: str = Body(..., title=\"任务id\", embed=True)\n):\n    res = Schedule.get_job(job_id=job_id)\n    if not res:\n        return resp_fail(msg=f\"not found job {job_id}\")\n    Schedule.remove_job(job_id)\n    return resp_ok()\n\n# 暂停和恢复任务 暂时没看\n\n\nif __name__ == \"__main__\":\n    import uvicorn\n\n    # 官方推荐是用命令后启动 uvicorn main:app --host=127.0.0.1 --port=8150 --reload\n    uvicorn.run(app='main:app', host=\"127.0.0.1\", port=8151, reload=False, debug=True)\n"
  },
  {
    "path": "{{cookiecutter.project_name}}/api/v1/items.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2020/10/16 15:21\n# @Author  : CoderCharm\n# @File    : endpoints.py\n# @Software: PyCharm\n# @Github  : github/CoderCharm\n# @Email   : wg_python@163.com\n# @Desc    :\n\"\"\"\n\n\"\"\"\nimport os\nfrom typing import Any\nimport shutil\n\nfrom tempfile import NamedTemporaryFile\nfrom pathlib import Path\n\nfrom fastapi import APIRouter, File, UploadFile, Query\n\nfrom schemas.response import resp\nfrom core.config import settings\nfrom common.sys_redis import redis_client\nfrom models.users import User\n\nrouter = APIRouter()\n\n\n@router.get(\"/test\", name=\"测试接口\")\ndef items_test(\n        *,\n        bar: str = Query(..., title=\"测试字段\", description=\"测试字段描述\")\n) -> Any:\n    \"\"\"\n    用户登录\n    :param bar:\n    :param db:\n    :return:\n    \"\"\"\n    # 测试redis使用\n    redis_client.set(\"test_items\", bar, ex=60)\n    redis_test = redis_client.get(\"test_items\")\n\n    return resp.ok(data=redis_test)\n\n\n@router.get(\"/user/all\", summary=\"获取所有用户信息\", name=\"获取用户信息\")\ndef get_all_user_info(\n        page: int = Query(1),  # 分页等通用字段可以提取出来封装\n        page_size: int = Query(20),\n):\n    user, pagination = User().fetch_all(page=page,page_size=page_size)\n\n    return resp.ok(data=user, pagination=pagination)\n\n\n@router.post(\"/upload/file\", summary=\"上传图片\", name=\"上传图片\")\ndef upload_image(\n        file: UploadFile = File(...),\n):\n    # 本地存储临时方案，一般生产都是使用第三方云存储OSS(如七牛云, 阿里云)\n    # 建议计算并记录一次 文件md5值 避免重复存储相同资源\n    save_dir = f\"{settings.BASE_PATH}/static/img\"\n\n    if not os.path.exists(save_dir):\n        os.mkdir(save_dir)\n    try:\n        suffix = Path(file.filename).suffix\n\n        with NamedTemporaryFile(delete=False, suffix=suffix, dir=save_dir) as tmp:\n            shutil.copyfileobj(file.file, tmp)\n            tmp_file_name = Path(tmp.name).name\n    finally:\n        file.file.close()\n\n    return resp.ok(data={\"image\": f\"/static/img/{tmp_file_name}\"})\n"
  },
  {
    "path": "{{cookiecutter.project_name}}/api/v1/sys_scheduler.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2020/12/24 18:24\n# @Author  : CoderCharm\n# @File    : sys_scheduler.py\n# @Software: PyCharm\n# @Github  : github/CoderCharm\n# @Email   : wg_python@163.com\n# @Desc    :\n\"\"\"\n系统调度\n\"\"\"\n\nfrom datetime import datetime\n\nfrom fastapi import APIRouter, Query, Body\n\nfrom common.sys_schedule import schedule\nfrom schemas.response import resp\nfrom utils.cron_task import demo_task\n\nrouter = APIRouter()\n\n\n@router.get(\"/jobs/all\", summary=\"获取所有job信息\", name=\"获取所有定时任务\")\nasync def get_scheduled_syncs():\n    \"\"\"\n    获取所有job\n    :return:\n    \"\"\"\n    schedules = []\n    for job in schedule.get_jobs():\n        schedules.append(\n            {\"job_id\": job.id, \"func_name\": job.func_ref, \"func_args\": job.args, \"cron_model\": str(job.trigger),\n             \"next_run\": str(job.next_run_time)}\n        )\n\n    return resp.ok(data=schedules)\n\n\n@router.get(\"/jobs/once\", summary=\"获取指定的job信息\", name=\"获取指定定时任务\")\nasync def get_target_sync(\n        job_id: str = Query(..., title=\"任务id\")\n):\n    job = schedule.get_job(job_id=job_id)\n\n    if not job:\n        return resp.fail(resp.DataNotFound.set_msg(f\"not found job {job_id}\"))\n\n    return resp.ok(\n        data={\"job_id\": job.id, \"func_name\": job.func_ref, \"func_args\": job.args, \"cron_model\": str(job.trigger),\n              \"next_run\": str(job.next_run_time)})\n\n\n@router.post(\"/job/schedule\", summary=\"开始job调度\", name=\"启动定时任务\")\nasync def add_job_to_scheduler(\n        *,\n        seconds: int = Body(120, title=\"循环间隔时间/秒,默认120s\", embed=True),\n        job_id: str = Body(..., title=\"任务id\", embed=True),\n):\n    \"\"\"\n    简易的任务调度演示 可自行参考文档 https://apscheduler.readthedocs.io/en/stable/\n    三种模式\n    date: use when you want to run the job just once at a certain point of time\n    interval: use when you want to run the job at fixed intervals of time\n    cron: use when you want to run the job periodically at certain time(s) of day\n    :param seconds:\n    :param job_id:\n    :return:\n    \"\"\"\n    res = schedule.get_job(job_id=job_id)\n    if res:\n        return resp.fail(resp.InvalidRequest.set_msg(f\"{job_id} job already exists\"))\n\n    schedule_job = schedule.add_job(demo_task,\n                                    'interval',\n                                    args=(job_id,),\n                                    seconds=seconds,  # 循环间隔时间 秒\n                                    id=job_id,  # job ID\n                                    next_run_time=datetime.now()  # 立即执行\n                                    )\n    return resp.ok(data={\"id\": schedule_job.id})\n\n\n@router.post(\"/job/del\", summary=\"移除任务\", name=\"删除定时任务\")\nasync def remove_schedule(\n        job_id: str = Body(..., title=\"job_id\", embed=True)\n):\n    res = schedule.get_job(job_id=job_id)\n    if not res:\n        return resp.fail(resp.DataNotFound.set_msg(f\"not found job {job_id}\"))\n\n    schedule.remove_job(job_id)\n\n    return resp.ok()\n"
  },
  {
    "path": "{{cookiecutter.project_name}}/api/v1/user.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2020/10/16 13:38\n# @Author  : CoderCharm\n# @File    : user.py\n# @Software: PyCharm\n# @Github  : github/CoderCharm\n# @Email   : wg_python@163.com\n# @Desc    :\n\"\"\"\n\n\"\"\"\n\nfrom typing import Any\nfrom datetime import timedelta\n\nfrom fastapi import APIRouter, Depends\n\nfrom core import security\nfrom models.users import User\nfrom common import deps, logger\nfrom schemas.response import resp\nfrom core.config import settings\nfrom schemas.request import sys_user_schema\nfrom playhouse.shortcuts import model_to_dict\n\nfrom logic.user_logic import UserLogic\n\nrouter = APIRouter()\n\n\n@router.post(\"/login\", summary=\"用户登录认证\", name=\"登录\")\ndef login_access_token(\n        *,\n        req: sys_user_schema.UserPhoneAuth,\n) -> Any:\n    \"\"\"\n    简单实现登录\n    :param req:\n    :return:\n    \"\"\"\n\n    # 验证用户 简短的业务可以写在这里\n    # user = User.single_by_phone(phone=req.username)\n    # if not user:\n    #     return resp.fail(resp.DataNotFound.set_msg(\"账号或密码错误\"))\n    #\n    # if not security.verify_password(req.password, user.password):\n    #     return resp.fail(resp.DataNotFound.set_msg(\"账号或密码错误\"))\n    #\n    # access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)\n    #\n    # # 登录token 存储了user.id\n    # return resp.ok(data={\n    #     \"token\": security.create_access_token(user.id, expires_delta=access_token_expires),\n    # })\n\n    # 复杂的业务逻辑建议 抽离到 logic文件夹下\n    token = UserLogic().user_login_logic(req.username, req.password)\n    return resp.ok(data={\"token\": token})\n\n\n@router.get(\"/user/info\", summary=\"获取用户信息\", name=\"获取用户信息\", description=\"此API没有验证权限\")\ndef get_user_info(\n        *,\n        current_user: User = Depends(deps.get_current_user)\n) -> Any:\n    \"\"\"\n    获取用户信息 这个路由分组没有验证权限\n    :param current_user:\n    :return:\n    \"\"\"\n    return resp.ok(data=model_to_dict(current_user))\n"
  },
  {
    "path": "{{cookiecutter.project_name}}/common/__init__.py",
    "content": "\nfrom .logger import logger"
  },
  {
    "path": "{{cookiecutter.project_name}}/common/custom_exc.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2020/10/15 16:57\n# @Author  : CoderCharm\n# @File    : custom_exc.py\n# @Software: PyCharm\n# @Github  : github/CoderCharm\n# @Email   : wg_python@163.com\n# @Desc    :\n\"\"\"\n自定义异常\n\"\"\"\n\n\nclass TokenAuthError(Exception):\n    def __init__(self, err_desc: str = \"User Authentication Failed\"):\n        self.err_desc = err_desc\n\n\nclass TokenExpired(Exception):\n    def __init__(self, err_desc: str = \"Token has expired\"):\n        self.err_desc = err_desc\n\n\nclass AuthenticationError(Exception):\n    def __init__(self, err_desc: str = \"Permission denied\"):\n        self.err_desc = err_desc\n\n"
  },
  {
    "path": "{{cookiecutter.project_name}}/common/deps.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2020/10/16 11:51\n# @Author  : CoderCharm\n# @File    : deps.py\n# @Software: PyCharm\n# @Github  : github/CoderCharm\n# @Email   : wg_python@163.com\n# @Desc    :\n\"\"\"\n\n一些通用的依赖功能\n\n\"\"\"\nfrom typing import Any, Union, Optional\n#\nfrom jose import jwt\nfrom fastapi import Header, Depends, Request\nfrom pydantic import ValidationError\n\nfrom common import custom_exc\nfrom models.users import User\nfrom core.config import settings\n\n\ndef check_jwt_token(\n        token: Optional[str] = Header(..., description=\"登录token\")\n) -> Union[str, Any]:\n    \"\"\"\n    解析验证token  默认验证headers里面为token字段的数据\n    可以给 headers 里面token替换别名, 以下示例为 X-Token\n    token: Optional[str] = Header(None, alias=\"X-Token\")\n    :param token:\n    :return:\n    \"\"\"\n\n    try:\n        payload = jwt.decode(\n            token,\n            settings.SECRET_KEY, algorithms=[settings.ALGORITHM]\n        )\n        return payload\n    except jwt.ExpiredSignatureError:\n        raise custom_exc.TokenExpired()\n    except (jwt.JWTError, ValidationError, AttributeError):\n        raise custom_exc.TokenAuthError()\n\n\ndef get_current_user(\n        token: Optional[str] = Depends(check_jwt_token)\n) -> User:\n    \"\"\"\n    根据header中token 获取当前用户\n    :param db:\n    :param token:\n    :return:\n    \"\"\"\n    user = User.single_by_id(uid=token.get(\"sub\"))\n    if not user:\n        raise custom_exc.TokenAuthError(err_desc=\"User not found\")\n    return user"
  },
  {
    "path": "{{cookiecutter.project_name}}/common/logger.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2020/10/15 16:58\n# @Author  : CoderCharm\n# @File    : logger.py\n# @Software: PyCharm\n# @Github  : github/CoderCharm\n# @Email   : wg_python@163.com\n# @Desc    :\n\"\"\"\n\n日志文件配置 参考链接\nhttps://github.com/Delgan/loguru\n\n# 本来是想 像flask那样把日志对象挂载到app对象上，作者建议直接使用全局对象\nhttps://github.com/tiangolo/fastapi/issues/81#issuecomment-473677039\n\n考虑是否应该把logger 改成单例\n\n\"\"\"\n\nimport os\nimport time\nfrom loguru import logger\n\nfrom core.config import settings\n\n# 定位到log日志文件\nlog_path = os.path.join(settings.BASE_PATH, 'logs')\n\nif not os.path.exists(log_path):\n    os.mkdir(log_path)\n\nlog_path_info = os.path.join(log_path, f'{time.strftime(\"%Y-%m-%d\")}_info.log')\nlog_path_warning = os.path.join(log_path, f'{time.strftime(\"%Y-%m-%d\")}_warning.log')\nlog_path_error = os.path.join(log_path, f'{time.strftime(\"%Y-%m-%d\")}_error.log')\n\n# 日志简单配置 文件区分不同级别的日志\nlogger.add(log_path_info, rotation=\"500 MB\", encoding='utf-8', enqueue=True, level='INFO')\nlogger.add(log_path_warning, rotation=\"500 MB\", encoding='utf-8', enqueue=True, level='WARNING')\nlogger.add(log_path_error, rotation=\"500 MB\", encoding='utf-8', enqueue=True, level='ERROR')\n\n\n__all__ = [\"logger\"]\n"
  },
  {
    "path": "{{cookiecutter.project_name}}/common/session.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2020/10/16 11:42\n# @Author  : CoderCharm\n# @File    : session.py\n# @Software: PyCharm\n# @Github  : github/CoderCharm\n# @Email   : wg_python@163.com\n# @Desc    :\n\"\"\"\n\n\"\"\"\nimport math\nimport datetime\n\nfrom peewee import _ConnectionState, Model, ModelSelect, SQL, DateTimeField\nfrom contextvars import ContextVar\nfrom playhouse.pool import PooledMySQLDatabase\n\nfrom core.config import settings\n\ndb_state_default = {\"closed\": None, \"conn\": None, \"ctx\": None, \"transactions\": None}\ndb_state = ContextVar(\"db_state\", default=db_state_default.copy())\n\n\n# reference to https://fastapi.tiangolo.com/advanced/sql-databases-peewee/#context-variable-sub-dependency\nclass PeeweeConnectionState(_ConnectionState):\n    def __init__(self, **kwargs):\n        super().__setattr__(\"_state\", db_state)\n        super().__init__(**kwargs)\n\n    def __setattr__(self, name, value):\n        self._state.get()[name] = value\n\n    def __getattr__(self, name):\n        return self._state.get()[name]\n\n\ndb = PooledMySQLDatabase(\n    settings.MYSQL_DATABASE,\n    max_connections=8,\n    stale_timeout=300,\n    user=settings.MYSQL_USERNAME,\n    host=settings.MYSQL_HOST,\n    password=settings.MYSQL_PASSWORD,\n    port=settings.MYSQL_PORT\n)\n\ndb._state = PeeweeConnectionState()\n\n\nclass BaseModel(Model):\n    deleted_at = DateTimeField()\n    created_at = DateTimeField(default=datetime.datetime.now())\n    updated_at = DateTimeField(default=datetime.datetime.now())\n\n    @classmethod\n    def undelete(cls):\n        # for logic delete\n        return cls.select().where(SQL(\"deleted_at is NULL\"))\n\n    class Meta:\n        database = db\n\n\ndef paginator(query: ModelSelect, page: int, page_size: int, order_by: str = \"id ASC\"):\n    count = query.count()\n    if page < 1:\n        page = 1\n\n    if page_size <= 0:\n        page_size = 10\n\n    if page_size >= 100:\n        page_size = 100\n\n    if page == 1:\n        offset = 0\n    else:\n        offset = (page - 1) * page_size\n\n    query = query.offset(offset).limit(page_size).order_by(SQL(order_by))\n\n    total_pages = math.ceil(count / page_size)\n\n    paginate = {\n        \"total_pages\": total_pages,\n        \"count\": count,\n        \"current_page\": page,\n        \"pre_page\": page - 1 if page > 1 else page,\n        \"next_page\": page if page == total_pages else page + 1\n    }\n\n    return list(query.dicts()), paginate\n"
  },
  {
    "path": "{{cookiecutter.project_name}}/common/sys_redis.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2021/1/28 20:34\n# @Author  : CoderCharm\n# @File    : sys_redis.py\n# @Software: PyCharm\n# @Github  : github/CoderCharm\n# @Email   : wg_python@163.com\n# @Desc    :\n\"\"\"\n\n通过class 实例化对象可以直接修改内部属性的特性\n再通过魔法方法，赋予实例化对象 具有内部属性_redis_client的方法和属性\n\n主要参考 flask-redis扩展实现\nhttps://github.com/underyx/flask-redis/blob/master/flask_redis/client.py\n\nredis 连接\n\n\"\"\"\nimport sys\nfrom redis import Redis, AuthenticationError\n\nfrom common.logger import logger\nfrom core.config import settings\n\n\nclass RedisCli(object):\n\n    def __init__(self, *, host: str, port: int, password: str, db: int, socket_timeout: int = 5):\n        # redis对象 在 @app.on_event(\"startup\") 中连接创建\n        self._redis_client = None\n        self.host = host\n        self.port = port\n        self.password = password\n        self.db = db\n        self.socket_timeout = socket_timeout\n\n    def init_redis_connect(self) -> None:\n        \"\"\"\n        初始化连接\n        :return:\n        \"\"\"\n        try:\n            self._redis_client = Redis(\n                host=self.host,\n                port=self.port,\n                password=self.password,\n                db=self.db,\n                socket_timeout=self.socket_timeout,\n                decode_responses=True  # 解码\n            )\n            if not self._redis_client.ping():\n                logger.info(\"连接redis超时\")\n                sys.exit()\n        except (AuthenticationError, Exception) as e:\n            logger.info(f\"连接redis异常 {e}\")\n            sys.exit()\n\n    # 使实例化后的对象 赋予redis对象的的方法和属性\n    def __getattr__(self, name):\n        return getattr(self._redis_client, name)\n\n    def __getitem__(self, name):\n        return self._redis_client[name]\n\n    def __setitem__(self, name, value):\n        self._redis_client[name] = value\n\n    def __delitem__(self, name):\n        del self._redis_client[name]\n\n\n# 创建redis连接对象\nredis_client: Redis = RedisCli(\n    host=settings.REDIS_HOST,\n    port=settings.REDIS_PORT,\n    password=settings.REDIS_PASSWORD,\n    db=settings.REDIS_DB,\n    socket_timeout=settings.REDIS_TIMEOUT\n)\n\n# 只允许导出 redis_client 实例化对象\n__all__ = [\"redis_client\"]\n"
  },
  {
    "path": "{{cookiecutter.project_name}}/common/sys_schedule.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2021/1/29 20:33\n# @Author  : CoderCharm\n# @File    : sys_schedule.py\n# @Software: PyCharm\n# @Github  : github/CoderCharm\n# @Email   : wg_python@163.com\n# @Desc    :\n\"\"\"\n初始化 apscheduler\n模仿 redis\n\n\"\"\"\n\nfrom apscheduler.schedulers.asyncio import AsyncIOScheduler\nfrom apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore\n\n\nclass ScheduleCli(object):\n    _instance = None\n\n    def __new__(cls, *args, **kw):\n        if cls._instance is None:\n            cls._instance = object.__new__(cls, *args, **kw)\n        return cls._instance\n\n    def __init__(self):\n        # 对象 在 @app.on_event(\"startup\") 中初始化\n        self._schedule = None\n\n    def init_scheduler(self) -> None:\n        \"\"\"\n        初始化 apscheduler\n        :return:\n        \"\"\"\n        job_stores = {\n            'default': SQLAlchemyJobStore(url='sqlite:///jobs.sqlite')\n        }\n        self._schedule = AsyncIOScheduler(jobstores=job_stores)\n        self._schedule.start()\n\n    # 使实例化后的对象 赋予apscheduler对象的方法和属性\n    def __getattr__(self, name):\n        return getattr(self._schedule, name)\n\n    def __getitem__(self, name):\n        return self._schedule[name]\n\n    def __setitem__(self, name, value):\n        self._schedule[name] = value\n\n    def __delitem__(self, name):\n        del self._schedule[name]\n\n\n# 创建schedule对象\nschedule: AsyncIOScheduler = ScheduleCli()\n\n# 只允许导出 redis_client 实例化对象\n__all__ = [\"schedule\"]\n"
  },
  {
    "path": "{{cookiecutter.project_name}}/core/celery_app.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2020/10/17 15:03\n# @Author  : CoderCharm\n# @File    : celery_app.py\n# @Software: PyCharm\n# @Github  : github/CoderCharm\n# @Email   : wg_python@163.com\n# @Desc    :\n\"\"\"\n\n\"\"\"\n\n\nfrom celery import Celery\n\ncelery_app = Celery(\"worker\", broker=\"amqp://guest@queue//\")\n\ncelery_app.conf.task_routes = {\"app.worker.test_celery\": \"main-queue\"}\n"
  },
  {
    "path": "{{cookiecutter.project_name}}/core/config/__init__.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2020/9/18 13:50\n# @Author  : CoderCharm\n# @File    : __init__.py.py\n# @Software: PyCharm\n# @Github  : github/CoderCharm\n# @Email   : wg_python@163.com\n# @Desc    :\n\"\"\"\n\n配置文件区分生产和开发\n\n我这种是一种方式，简单直观\n还有一种是服务一个固定路径放一个配置文件如 /etc/conf 下 xxx.ini 或者 xxx.py文件\n然后项目默认读取 /etc/conf 目录下的配置文件，能读取则为生产环境，\n读取不到则为开发环境，开发环境配置可以直接写在代码里面(或者配置ide环境变量)\n\n根据环境变量ENV是否有值 区分生产开发\n以上弃用\n\n\"\"\"\n\nimport os\n\nfrom .config import settings\n\n"
  },
  {
    "path": "{{cookiecutter.project_name}}/core/config/config.py.example",
    "content": "\n\nimport os\n\nfrom typing import Union, Optional\n\nfrom pydantic import AnyHttpUrl, BaseSettings, IPvAnyAddress\n\n\nclass Settings(BaseSettings):\n    # 开发模式配置\n    DEBUG: bool = True\n    # 项目文档\n    TITLE: str = \"FastAPI+MySQL项目生成\"\n    DESCRIPTION: str = \"更多FastAPI知识，请关注我的个人网站 https://www.charmcode.cn/\"\n    # 文档地址 默认为docs 生产环境关闭 None\n    DOCS_URL: str = \"/api/docs\"\n    # 文档关联请求数据接口\n    OPENAPI_URL: str = \"/api/openapi.json\"\n    # redoc 文档\n    REDOC_URL: Optional[str] = \"/api/redoc\"\n\n    # token过期时间 分钟\n    ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8\n\n    # 生成token的加密算法\n    ALGORITHM: str = \"HS256\"\n\n    # 生产环境保管好 SECRET_KEY\n    SECRET_KEY: str = 'xxxxxxxx'\n\n    # 项目根路径\n    BASE_PATH: str = os.path.dirname(os.path.dirname(os.path.dirname((os.path.abspath(__file__)))))\n\n    # 配置你的Mysql环境\n    MYSQL_USERNAME: str = \"root\"\n    MYSQL_PASSWORD: str = \"root\"\n    MYSQL_HOST: str = \"127.0.0.1\"\n    MYSQL_PORT: int = 3306\n    MYSQL_DATABASE: str = 'fastapi'\n\n    # redis配置\n    REDIS_HOST: str = \"127.0.0.1\"\n    REDIS_PASSWORD: str = \"\"\n    REDIS_DB: int = 0\n    REDIS_PORT: int = 6379\n    REDIS_URL: str = f\"redis://:{REDIS_PASSWORD}@{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB}?encoding=utf-8\"\n    REDIS_TIMEOUT: int = 5  # redis连接超时时间\n\n    CASBIN_MODEL_PATH: str = \"./resource/rbac_model.conf\"\n\n\nsettings = Settings()\n"
  },
  {
    "path": "{{cookiecutter.project_name}}/core/security.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2020/9/22 13:20\n# @Author  : CoderCharm\n# @File    : security.py\n# @Software: PyCharm\n# @Github  : github/CoderCharm\n# @Email   : wg_python@163.com\n# @Desc    :\n\"\"\"\ntoken password 验证\npip install python-jose\npip install passlib\npip install bcrypt\n\n\"\"\"\nfrom typing import Any, Union\nfrom datetime import datetime, timedelta\n\nfrom jose import jwt\nfrom passlib.context import CryptContext\n\nfrom core.config import settings\n\npwd_context = CryptContext(schemes=[\"bcrypt\"], deprecated=\"auto\")\n\n\ndef create_access_token(\n        subject: Union[str, Any],\n        expires_delta: timedelta = None\n) -> str:\n    \"\"\"\n    生成token\n    :param subject:需要存储到token的数据(注意token里面的数据，属于公开的)\n    :param expires_delta:\n    :return:\n    \"\"\"\n    if expires_delta:\n        expire = datetime.utcnow() + expires_delta\n    else:\n        expire = datetime.utcnow() + timedelta(\n            minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES\n        )\n    to_encode = {\"exp\": expire, \"sub\": str(subject)}\n    encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)\n    return encoded_jwt\n\n\ndef verify_password(plain_password: str, hashed_password: str) -> bool:\n    \"\"\"\n    验证密码\n    :param plain_password: 原密码\n    :param hashed_password: hash后的密码\n    :return:\n    \"\"\"\n    return pwd_context.verify(plain_password, hashed_password)\n\n\ndef get_password_hash(password: str) -> str:\n    \"\"\"\n    获取 hash 后的密码\n    :param password:\n    :return:\n    \"\"\"\n    return pwd_context.hash(password)\n"
  },
  {
    "path": "{{cookiecutter.project_name}}/core/server.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2020/10/15 16:24\n# @Author  : CoderCharm\n# @File    : server.py.py\n# @Software: PyCharm\n# @Github  : github/CoderCharm\n# @Email   : wg_python@163.com\n# @Desc    :\n\"\"\"\n包导入的时候最好遵循包导入顺序原则\n\n标准库\n第三方库\n项目自定义\n\"\"\"\n\nimport traceback\n\nfrom fastapi import FastAPI, Request, Response\nfrom starlette.middleware.cors import CORSMiddleware\nfrom fastapi.exceptions import RequestValidationError, ValidationError\n\nfrom router.v1_router import api_v1_router\nfrom core.config import settings\nfrom common.logger import logger\nfrom common import custom_exc\nfrom common.sys_schedule import schedule\nfrom common.sys_redis import redis_client\nfrom common.session import db\nfrom schemas.response import resp\n\n\ndef create_app() -> FastAPI:\n    \"\"\"\n    生成FatAPI对象\n    :return:\n    \"\"\"\n    app = FastAPI(\n        debug=settings.DEBUG,\n        title=settings.TITLE,\n        description=settings.DESCRIPTION,\n        docs_url=settings.DOCS_URL,\n        openapi_url=settings.OPENAPI_URL,\n        redoc_url=settings.REDOC_URL\n    )\n\n    # 其余的一些全局配置可以写在这里 多了可以考虑拆分到其他文件夹\n\n    # 跨域设置\n    register_cors(app)\n\n    # 注册路由\n    register_router(app)\n\n    # 注册捕获全局异常\n    register_exception(app)\n\n    # 请求拦截\n    register_hook(app)\n\n    # 取消挂载在 request对象上面的操作，感觉特别麻烦，直接使用全局的\n    register_init(app)\n\n    if settings.DEBUG:\n        # 注册静态文件\n        register_static_file(app)\n\n    return app\n\n\ndef register_static_file(app: FastAPI) -> None:\n    \"\"\"\n    静态文件交互开发模式使用\n    生产使用 nginx 静态资源服务\n    这里是开发是方便本地\n    :param app:\n    :return:\n    \"\"\"\n    import os\n    from fastapi.staticfiles import StaticFiles\n    if not os.path.exists(\"./static\"):\n        os.mkdir(\"./static\")\n    app.mount(\"/static\", StaticFiles(directory=\"static\"), name=\"static\")\n\n\ndef register_router(app: FastAPI) -> None:\n    \"\"\"\n    注册路由\n    :param app:\n    :return:\n    \"\"\"\n    # 项目API\n    app.include_router(\n        api_v1_router,\n    )\n\n\ndef register_cors(app: FastAPI) -> None:\n    \"\"\"\n    支持跨域\n    :param app:\n    :return:\n    \"\"\"\n    if settings.DEBUG:\n        app.add_middleware(\n            CORSMiddleware,\n            allow_origins=[\"*\"],\n            allow_credentials=True,\n            allow_methods=[\"*\"],\n            allow_headers=[\"*\"],\n        )\n\n\ndef register_exception(app: FastAPI) -> None:\n    \"\"\"\n    全局异常捕获\n    注意 别手误多敲一个s\n    exception_handler\n    exception_handlers\n    两者有区别\n        如果只捕获一个异常 启动会报错\n        @exception_handlers(UserNotFound)\n    TypeError: 'dict' object is not callable\n    :param app:\n    :return:\n    \"\"\"\n\n    # 自定义异常 捕获\n    @app.exception_handler(custom_exc.TokenExpired)\n    async def user_not_found_exception_handler(request: Request, exc: custom_exc.TokenExpired):\n        \"\"\"\n        token过期\n        :param request:\n        :param exc:\n        :return:\n        \"\"\"\n        logger.error(\n            f\"token未知用户\\nURL:{request.method}{request.url}\\nHeaders:{request.headers}\\n{traceback.format_exc()}\")\n\n        return resp.fail(message=exc.err_desc)\n\n    @app.exception_handler(custom_exc.TokenAuthError)\n    async def user_token_exception_handler(request: Request, exc: custom_exc.TokenAuthError):\n        \"\"\"\n        用户token异常\n        :param request:\n        :param exc:\n        :return:\n        \"\"\"\n        logger.error(f\"用户认证异常\\nURL:{request.method}{request.url}\\nHeaders:{request.headers}\\n{traceback.format_exc()}\")\n\n        return resp.fail(resp.DataNotFound.set_msg(exc.err_desc))\n\n    @app.exception_handler(custom_exc.AuthenticationError)\n    async def user_not_found_exception_handler(request: Request, exc: custom_exc.AuthenticationError):\n        \"\"\"\n        用户权限不足\n        :param request:\n        :param exc:\n        :return:\n        \"\"\"\n        logger.error(f\"用户权限不足 \\nURL:{request.method}{request.url}\")\n        return resp.fail(resp.PermissionDenied)\n\n    @app.exception_handler(ValidationError)\n    async def inner_validation_exception_handler(request: Request, exc: ValidationError):\n        \"\"\"\n        内部参数验证异常\n        :param request:\n        :param exc:\n        :return:\n        \"\"\"\n        logger.error(\n            f\"内部参数验证错误\\nURL:{request.method}{request.url}\\nHeaders:{request.headers}\\n{traceback.format_exc()}\")\n        return resp.fail(resp.BusinessError.set_msg(exc.errors()))\n\n    @app.exception_handler(RequestValidationError)\n    async def request_validation_exception_handler(request: Request, exc: RequestValidationError):\n        \"\"\"\n        请求参数验证异常\n        :param request:\n        :param exc:\n        :return:\n        \"\"\"\n        logger.error(\n            f\"请求参数格式错误\\nURL:{request.method}{request.url}\\nHeaders:{request.headers}\\n{traceback.format_exc()}\")\n        # return response_code.resp_4001(message='; '.join([f\"{e['loc'][1]}: {e['msg']}\" for e in exc.errors()]))\n        return resp.fail(resp.InvalidParams.set_msg(exc.errors()))\n\n    # 捕获全部异常\n    @app.exception_handler(Exception)\n    async def all_exception_handler(request: Request, exc: Exception):\n        \"\"\"\n        全局所有异常\n        :param request:\n        :param exc:\n        :return:\n        \"\"\"\n        logger.error(f\"全局异常\\n{request.method}URL:{request.url}\\nHeaders:{request.headers}\\n{traceback.format_exc()}\")\n        return resp.fail(resp.ServerError)\n\n\ndef register_hook(app: FastAPI) -> None:\n    \"\"\"\n    请求响应拦截 hook\n    https://fastapi.tiangolo.com/tutorial/middleware/\n    :param app:\n    :return:\n    \"\"\"\n\n    @app.middleware(\"http\")\n    async def logger_request(request: Request, call_next) -> Response:\n        # https://stackoverflow.com/questions/60098005/fastapi-starlette-get-client-real-ip\n        # logger.info(f\"访问记录:{request.method} url:{request.url}\\nheaders:{request.headers}\\nIP:{request.client.host}\")\n        response = await call_next(request)\n        return response\n\n\ndef register_init(app: FastAPI) -> None:\n    \"\"\"\n    初始化连接\n    :param app:\n    :return:\n    \"\"\"\n\n    @app.on_event(\"startup\")\n    async def init_connect():\n        # 连接redis\n        redis_client.init_redis_connect()\n\n        # 初始化 apscheduler\n        schedule.init_scheduler()\n\n        db.connect()\n\n    @app.on_event('shutdown')\n    async def shutdown_connect():\n        \"\"\"\n        关闭\n        :return:\n        \"\"\"\n        schedule.shutdown()\n\n        if not db.is_closed():\n            db.close()\n\n"
  },
  {
    "path": "{{cookiecutter.project_name}}/db/mysql/migrations/20211209-init.sql",
    "content": "\n-- +migrate Up\nCREATE TABLE IF NOT EXISTS `users` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `name` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n  `email` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n  `password` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n  `phone` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n  `username` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n  `avatar` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '头像',\n  `deleted_at` timestamp NULL DEFAULT NULL,\n  `created_at` timestamp NULL DEFAULT NULL,\n  `updated_at` timestamp NULL DEFAULT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `users_email_unique` (`email`),\n  UNIQUE KEY `users_phone_unique` (`phone`),\n  UNIQUE KEY `users_username_unique` (`username`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='用户表';\n\n/******************************************/\n/*  密码都是hash后的 12345    */\n/******************************************/\nINSERT INTO `fastapi`.`users`(`id`, `name`, `email`, `password`, `phone`, `username`, `avatar`, `deleted_at`, `created_at`, `updated_at`) VALUES (1, 'Jack', 'wxy@123.com', '$2b$12$9QqLfn..1PGFMLtoD1xvFOX8IGk/COLBHi8yKLFs.DTsiCyi9QmYq', '17722334455', '旱灾杰克', 'https://avatars.githubusercontent.com/u/33140097', NULL, '2021-12-09 20:29:22', NULL);\nINSERT INTO `fastapi`.`users`(`id`, `name`, `email`, `password`, `phone`, `username`, `avatar`, `deleted_at`, `created_at`, `updated_at`) VALUES (2, 'Pony', 'xxxy@123.com', '$2b$12$uV6BtGvW.5e8M6r9YWzkjeAwOq4kPc4U/Ge8wk7Kh0hAtvqG13uBK', '17712346677', '小马宝莉', 'https://avatars.githubusercontent.com/u/27198984', NULL, '2021-12-09 20:35:48', NULL);\n\n-- +migrate Down\nDROP TABLE IF EXISTS `users`;\nDELETE FROM  users where id =1\nDELETE FROM  users where id =2\n\n\n"
  },
  {
    "path": "{{cookiecutter.project_name}}/dbconfig.yml",
    "content": "dev-mysql:\n  dialect: mysql\n  datasource: root:123456@tcp(127.0.0.1:3306)/fastapi?parseTime=true\n  dir: db/mysql/migrations\n  table: migrations"
  },
  {
    "path": "{{cookiecutter.project_name}}/dbconfig.yml.example",
    "content": "dev-mysql:\n  dialect: mysql\n  datasource: root:123456@tcp(127.0.0.1:3306)/fastapi?parseTime=true\n  dir: db/mysql/migrations\n  table: migrations"
  },
  {
    "path": "{{cookiecutter.project_name}}/logic/user_logic.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# 业务逻辑多了就写在这里\n\nfrom datetime import timedelta\n\nfrom models.users import User\nfrom schemas.response import resp\nfrom core import security\nfrom core.config import settings\nfrom common import custom_exc\n\n\nclass UserLogic(object):\n\n    @staticmethod\n    def user_login_logic(phone: int, password: str):\n        user = User.single_by_phone(phone=phone)\n        if not user:\n            raise custom_exc.TokenAuthError(err_desc=\"账号或密码错误\")\n\n        if not security.verify_password(password, user.password):\n            raise custom_exc.TokenAuthError(err_desc=\"账号或密码错误\")\n\n        access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)\n\n        return security.create_access_token(user.id, expires_delta=access_token_expires)\n\n    def xxx_logic(self):\n        pass\n"
  },
  {
    "path": "{{cookiecutter.project_name}}/main.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2020/10/15 16:24\n# @Author  : CoderCharm\n# @File    : main.py\n# @Software: PyCharm\n# @Github  : github/CoderCharm\n# @Email   : wg_python@163.com\n# @Desc    :\n\n\"\"\"\npip install uvicorn\n# 推荐启动方式 main指当前文件名字 app指FastAPI实例化后对象名称\nuvicorn main:app --host=127.0.0.1 --port=8010 --reload\n\n类似flask 工厂模式创建\n# 生产启动命令 去掉热重载 (可用supervisor托管后台运行)\n\n在main.py同文件下下启动\nuvicorn main:app --host=127.0.0.1 --port=8010 --workers=4\n\n# 同样可以也可以配合gunicorn多进程启动  main.py同文件下下启动 默认127.0.0.1:8000端口\ngunicorn main:app -w 4 -k uvicorn.workers.UvicornWorker -b 127.0.0.1:8020\n\n\"\"\"\n\n\nfrom core.server import create_app\n\n\napp = create_app()\n\nif __name__ == \"__main__\":\n    import uvicorn\n\n    # 输出所有的路由\n    for route in app.routes:\n        if hasattr(route, \"methods\"):\n            print({'path': route.path, 'name': route.name, 'methods': route.methods})\n\n    uvicorn.run(app='main:app', host=\"127.0.0.1\", port=8010, reload=True, debug=True)\n\n"
  },
  {
    "path": "{{cookiecutter.project_name}}/models/users.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2020/10/16 13:31\n# @Author  : CoderCharm\n# @File    : auth.py\n# @Software: PyCharm\n# @Github  : github/CoderCharm\n# @Email   : wg_python@163.com\n# @Desc    :\n\"\"\"\n纯增删改查操作，写在model里面\n\"\"\"\n\nfrom common.session import BaseModel, paginator\nfrom peewee import CharField, IntegerField\n# from playhouse.shortcuts import model_to_dict, dict_to_model\n\n\nclass User(BaseModel):\n    \"\"\"\n    用户表\n    \"\"\"\n    id = IntegerField()\n    name = CharField()\n    email = CharField()\n    phone = IntegerField()\n    username = CharField()\n    avatar = CharField()\n    password = CharField()\n\n    class Meta:\n        table_name = 'users'\n\n    @classmethod\n    def single_by_id(cls, uid: int):\n        db = User.undelete().select(User.id, User.name, User.email, User.phone, User.username, User.avatar).\\\n            where(User.id == uid)\n        return db.first()\n\n    @classmethod\n    def single_by_phone(cls, phone: int = 0):\n        db = User.select()\n\n        if phone != 0:\n            db = db.where(User.phone == phone)\n        return db.first()\n        # if db:\n        #     return model_to_dict(db)\n\n    @classmethod\n    def fetch_all(cls, page: int = 1, page_size: int = 10):\n        db = User.undelete().select(User.name, User.email, User.phone, User.username, User.avatar, User.created_at,\n                                    User.deleted_at)\n\n        user_list, paginate = paginator(db, page, page_size, \"id desc\")\n\n        return user_list, paginate\n\n\n"
  },
  {
    "path": "{{cookiecutter.project_name}}/requirements-dev.txt",
    "content": "aiofiles\naioredis\nalembic\nloguru\nfastapi\nuvicorn\nSQLAlchemy\npython-jose\npasslib\nPyMySQL\ngunicorn\ndatabases[mysql]"
  },
  {
    "path": "{{cookiecutter.project_name}}/requirements.txt",
    "content": "aiofiles==0.6.0\naioredis==1.3.1\nalembic==1.4.3\namqp==5.0.1\nAPScheduler==3.6.3\narrow==0.17.0\nasync-timeout==3.0.1\nattrs==20.3.0\nbcrypt==3.1.7\nbilliard==3.6.3.0\nbinaryornot==0.4.4\ncelery==5.0.0\ncertifi==2020.6.20\ncffi==1.14.0\nchardet==3.0.4\nclick==7.1.2\nclick-didyoumean==0.0.3\nclick-repl==0.1.6\ncookiecutter==1.7.2\ndnspython==1.16.0\necdsa==0.14.1\nemail-validator==1.1.1\nfastapi==0.63.0\ngunicorn==20.0.4\nh11==0.9.0\nhiredis==1.1.0\nhttptools==0.1.1\nidna==2.9\niniconfig==1.1.1\ninstall==1.3.4\nJinja2==2.11.2\njinja2-time==0.2.0\nkombu==5.0.2\nloguru==0.5.3\nMako==1.1.3\nMarkupSafe==1.1.1\npackaging==20.8\npasslib==1.7.4\npluggy==0.13.1\npoyo==0.5.0\nprompt-toolkit==3.0.8\npy==1.10.0\npyasn1==0.4.8\npycparser==2.20\npydantic==1.5.1\nPyMySQL==1.0.2\npyparsing==2.4.7\npytest==6.2.1\npython-dateutil==2.8.1\npython-editor==1.0.4\npython-jose==3.2.0\npython-multipart==0.0.5\npython-slugify==4.0.1\npytz==2020.1\nrequests==2.24.0\nrsa==4.6\nsix==1.15.0\nSQLAlchemy==1.4.6\nstarlette==0.13.6\ntext-unidecode==1.3\ntoml==0.10.2\ntzlocal==2.1\nurllib3==1.25.10\nuvicorn==0.13.4\nuvloop==0.14.0\nvine==5.0.0\nwcwidth==0.2.5\nwebsockets==8.1\n"
  },
  {
    "path": "{{cookiecutter.project_name}}/router/__init__.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2021/1/21 09:03\n# @Author  : CoderCharm\n# @File    : __init__.py.py\n# @Software: PyCharm\n# @Github  : github/CoderCharm\n# @Email   : wg_python@163.com\n# @Desc    :\n\"\"\"\n\n\"\"\"\n\n"
  },
  {
    "path": "{{cookiecutter.project_name}}/router/v1_router.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2020/10/15 17:45\n# @Author  : CoderCharm\n# @File    : __init__.py\n# @Software: PyCharm\n# @Github  : github/CoderCharm\n# @Email   : wg_python@163.com\n# @Desc    :\n\"\"\"\n\n版本路由区分\n\n# 可以在这里添加所需要的依赖\nhttps://fastapi.tiangolo.com/tutorial/bigger-applications/#import-fastapi\n\n\"\"\"\n\nfrom fastapi import APIRouter\n# from common.deps import check_authority\n\nfrom api.v1.user import router as user_router\nfrom api.v1.items import router as items_router\nfrom api.v1.sys_scheduler import router as scheduler_router\n\n\napi_v1_router = APIRouter()\n\n# api_v1_router.include_router(items_router, tags=[\"测试API\"], dependencies=[Depends(check_jwt_token)])\n# check_authority 权限验证内部包含了 token 验证 如果不校验权限可直接 dependencies=[Depends(check_jwt_token)]\napi_v1_router.include_router(items_router, tags=[\"测试接口\"])\napi_v1_router.include_router(user_router, prefix=\"/user\", tags=[\"用户\"])\napi_v1_router.include_router(scheduler_router, tags=[\"任务调度\"])\n"
  },
  {
    "path": "{{cookiecutter.project_name}}/schemas/request/sys_api.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2021/1/29 21:11\n# @Author  : CoderCharm\n# @File    : sys_api.py\n# @Software: PyCharm\n# @Github  : github/CoderCharm\n# @Email   : wg_python@163.com\n# @Desc    :\n\"\"\"\npath = Column(VARCHAR(128), comment=\"API路径\")\ndescription = Column(VARCHAR(64), comment=\"API描述\")\napi_group = Column(VARCHAR(32), comment=\"API分组\")\nmethod = Column(VARCHAR(16), comment=\"请求方法\")\n\"\"\"\nfrom pydantic import BaseModel\n\n\n# 创建API\nclass ApiCreate(BaseModel):\n    path: str\n    description: str\n    api_group: str\n    method: str\n\n\nclass UpdateApi(BaseModel):\n    id: str\n\n\nclass DelApi(BaseModel):\n    id: str\n"
  },
  {
    "path": "{{cookiecutter.project_name}}/schemas/request/sys_authority_schema.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2021/1/28 19:40\n# @Author  : CoderCharm\n# @File    : sys_authority_schema.py\n# @Software: PyCharm\n# @Github  : github/CoderCharm\n# @Email   : wg_python@163.com\n# @Desc    :\n\"\"\"\n\n角色的校验规则\n\n\"\"\"\nfrom pydantic import BaseModel\n\n\n# 角色创建\nclass AuthorityCreate(BaseModel):\n    authority_id: int\n    authority_name: str\n    parent_id: int\n\n\n# 修改角色(只允许修改角色名)\nclass AuthorityUpdate(BaseModel):\n    authority_name: str\n"
  },
  {
    "path": "{{cookiecutter.project_name}}/schemas/request/sys_casbin.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2021/1/29 20:30\n# @Author  : CoderCharm\n# @File    : sys_casbin.py\n# @Software: PyCharm\n# @Github  : github/CoderCharm\n# @Email   : wg_python@163.com\n# @Desc    :\n\"\"\"\n校验casbin\n\"\"\"\n\nfrom pydantic import BaseModel\n\n\n# 创建API\nclass AuthCreate(BaseModel):\n    authority_id: str\n    path: str\n    method: str\n"
  },
  {
    "path": "{{cookiecutter.project_name}}/schemas/request/sys_user_schema.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2020/10/16 13:43\n# @Author  : CoderCharm\n# @File    : sys_user_schema.py\n# @Software: PyCharm\n# @Github  : github/CoderCharm\n# @Email   : wg_python@163.com\n# @Desc    :\n\"\"\"\n管理员表的 字段model模型 验证 响应(没写)等\n\"\"\"\n\nfrom typing import Optional\n\nfrom pydantic import BaseModel, EmailStr, AnyHttpUrl\n\n\n# Shared properties\nclass UserBase(BaseModel):\n    email: Optional[EmailStr] = None\n    phone: int = None\n    is_active: Optional[bool] = True\n\n\nclass UserAuth(BaseModel):\n    password: str\n\n\n# 邮箱登录认证 验证数据字段都叫username\nclass UserEmailAuth(UserAuth):\n    username: EmailStr\n\n\n# 手机号登录认证 验证数据字段都叫username\nclass UserPhoneAuth(UserAuth):\n    username: int\n\n\n# 创建账号需要验证的条件\nclass UserCreate(UserBase):\n    nickname: str\n    email: EmailStr\n    password: str\n    authority_id: int = 1\n    avatar: Optional[AnyHttpUrl] = None\n\n\n# Properties to receive via API on update\nclass UserUpdate(UserBase):\n    password: Optional[str] = None\n\n\nclass UserInDBBase(UserBase):\n    id: Optional[int] = None\n\n    class Config:\n        orm_mode = True\n\n\nclass UserInDB(UserInDBBase):\n    hashed_password: str\n\n\n# 返回的用户信息\nclass UserInfo(BaseModel):\n    role_id: int\n    role: str\n    nickname: str\n    avatar: AnyHttpUrl\n\n"
  },
  {
    "path": "{{cookiecutter.project_name}}/schemas/response/resp.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2020/9/22 13:32\n# @Author  : CoderCharm\n# @File    : response_code.py\n# @Software: PyCharm\n# @Github  : github/CoderCharm\n# @Email   : wg_python@163.com\n# @Desc    :\n\"\"\"\n\n统一响应状态码\n\n\"\"\"\nfrom typing import Union\n\nfrom fastapi import status as http_status\nfrom fastapi.responses import JSONResponse, Response\nfrom fastapi.encoders import jsonable_encoder\n\n\nclass Resp(object):\n    def __init__(self, status: int, msg: str, code: int):\n        self.status = status\n        self.msg = msg\n        self.code = code\n\n    def set_msg(self, msg):\n        self.msg = msg\n        return self\n\n\nInvalidRequest: Resp = Resp(1000, \"无效的请求\", http_status.HTTP_400_BAD_REQUEST)\nInvalidParams: Resp = Resp(1002, \"无效的参数\", http_status.HTTP_400_BAD_REQUEST)\nBusinessError: Resp = Resp(1003, \"业务错误\", http_status.HTTP_400_BAD_REQUEST)\nDataNotFound: Resp = Resp(1004, \"查询失败\", http_status.HTTP_400_BAD_REQUEST)\nDataStoreFail: Resp = Resp(1005, \"新增失败\", http_status.HTTP_400_BAD_REQUEST)\nDataUpdateFail: Resp = Resp(1006, \"更新失败\", http_status.HTTP_400_BAD_REQUEST)\nDataDestroyFail: Resp = Resp(1007, \"删除失败\", http_status.HTTP_400_BAD_REQUEST)\nPermissionDenied: Resp = Resp(1008, \"权限拒绝\", http_status.HTTP_403_FORBIDDEN)\nServerError: Resp = Resp(5000, \"服务器繁忙\", http_status.HTTP_500_INTERNAL_SERVER_ERROR)\n\n\ndef ok(*, data: Union[list, dict, str] = None, pagination: dict = None, msg: str = \"success\") -> Response:\n    return JSONResponse(\n        status_code=http_status.HTTP_200_OK,\n        content=jsonable_encoder({\n            'status': 200,\n            'msg': msg,\n            'data': data,\n            'pagination': pagination\n        })\n    )\n\n\ndef fail(resp: Resp) -> Response:\n    return JSONResponse(\n        status_code=resp.code,\n        content=jsonable_encoder({\n            'status': resp.status,\n            'msg': resp.msg,\n        })\n    )\n"
  },
  {
    "path": "{{cookiecutter.project_name}}/tests/README.md",
    "content": "## 单元测试\n\n> 后续添加一个测试环境配置\n\n`pytest` 后面的百分比表示测试进度，没有报错说明测试正常\n\nhttps://stackoverflow.com/questions/49738081/whats-the-meaning-of-the-percentages-displayed-for-each-test-on-pytest\n\n## 主要参考作者全栈项目生成器测试文件\n\nhttps://github.com/tiangolo/full-stack-fastapi-postgresql/tree/master/%7B%7Bcookiecutter.project_slug%7D%7D/backend/app/app/tests\n\n"
  },
  {
    "path": "{{cookiecutter.project_name}}/tests/api/v1/test_casbin.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2021/2/8 10:00\n# @Author  : CoderCharm\n# @File    : test_casbin.py\n# @Software: PyCharm\n# @Github  : github/CoderCharm\n# @Email   : wg_python@163.com\n# @Desc    :\n\"\"\"\n测试casbin权限是否有效\n\"\"\"\n\nfrom fastapi.testclient import TestClient\n\n\ndef test_ordinary_add_auth(client: TestClient, ordinary_token_headers: dict):\n    \"\"\"\n    测试普通用户设置权限是否被拦截\n    :return:\n    \"\"\"\n    response = client.post(\"/add/auth\", json={\n        \"authority_id\": \"100\",\n        \"path\": \"/add/auth\",\n        \"method\": \"POST\"\n    }, headers=ordinary_token_headers)\n\n    assert response.status_code == 200\n    assert response.json()[\"code\"] == 4003\n\n\ndef test_ordinary_del_auth(client: TestClient, ordinary_token_headers: dict):\n    \"\"\"\n    测试普通用户删除权限是否被拦截\n    :return:\n    \"\"\"\n    response = client.post(\"/del/auth\", json={\n        \"authority_id\": \"100\",\n        \"path\": \"/add/auth\",\n        \"method\": \"POST\"\n    }, headers=ordinary_token_headers)\n\n    assert response.status_code == 200\n    assert response.json()[\"code\"] == 4003\n\n\ndef test_admin_add_auth(client: TestClient, superuser_token_headers: dict):\n    \"\"\"\n    测试管理员设置权限是否被拦截\n    :return:\n    \"\"\"\n    response = client.post(\"/add/auth\", json={\n        \"authority_id\": \"100\",\n        \"path\": \"/add/auth\",\n        \"method\": \"POST\"\n    }, headers=superuser_token_headers)\n    assert response.status_code == 200\n    assert response.json()[\"code\"] == 200\n\n\ndef test_admin_del_auth(client: TestClient, superuser_token_headers: dict):\n    \"\"\"\n    测试管理员删除权限是否被拦截\n    :return:\n    \"\"\"\n    response = client.post(\"/del/auth\", json={\n        \"authority_id\": \"100\",\n        \"path\": \"/add/auth\",\n        \"method\": \"POST\"\n    }, headers=superuser_token_headers)\n\n    assert response.status_code == 200\n    assert response.json()[\"code\"] == 200\n"
  },
  {
    "path": "{{cookiecutter.project_name}}/tests/api/v1/test_cron.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2021/1/21 11:49\n# @Author  : CoderCharm\n# @File    : test_cron.py\n# @Software: PyCharm\n# @Github  : github/CoderCharm\n# @Email   : wg_python@163.com\n# @Desc    :\n\"\"\"\n测试任务调度\n\n\"\"\"\n\nfrom fastapi.testclient import TestClient\n\n\ndef test_add_job(\n        client: TestClient,\n        job_id: str,\n        superuser_token_headers: dict\n) -> None:\n    response = client.post(\"/job/schedule\", json={\n        \"seconds\": 5,\n        \"job_id\": job_id\n    }, headers=superuser_token_headers)\n    assert response.status_code == 200\n    assert response.json()[\"code\"] == 200\n    assert response.json()[\"data\"][\"id\"] == job_id\n\n\ndef test_get_all_job(client: TestClient, superuser_token_headers: dict) -> None:\n    response = client.get(\"/jobs/all\", headers=superuser_token_headers)\n    assert response.status_code == 200\n    assert response.json()[\"code\"] == 200\n    assert isinstance(response.json()[\"data\"], list)\n\n\ndef test_del_job(client: TestClient, job_id: str, superuser_token_headers: dict) -> None:\n    response = client.post(\"/job/del\", json={\n        \"job_id\": job_id\n    }, headers=superuser_token_headers)\n    assert response.status_code == 200\n    assert response.json()[\"code\"] == 200\n"
  },
  {
    "path": "{{cookiecutter.project_name}}/tests/api/v1/test_user.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2020/12/26 20:18\n# @Author  : CoderCharm\n# @File    : test_user.py\n# @Software: PyCharm\n# @Github  : github/CoderCharm\n# @Email   : wg_python@163.com\n# @Desc    :\n\"\"\"\n需要安装 pytest\npip install pytest\n\n\"\"\"\n\nfrom fastapi.testclient import TestClient\n\n\ndef test_login(client: TestClient) -> None:\n    \"\"\"\n    测试登录\n    自行使用 /app/create_user.py 创建任意测试用户\n    test@test.com\n    test\n    :return:\n    \"\"\"\n    response = client.post(\"/admin/auth/login/access-token\", json={\n        \"username\": \"test@test.com\",\n        \"password\": \"test\"\n    })\n    assert response.status_code == 200\n    assert response.json()[\"code\"] == 200\n    assert response.json()[\"data\"][\"token\"]\n\n\ndef test_error_login(client: TestClient) -> None:\n    \"\"\"\n    测试错误密码是否能登录\n    test@test.com\n    test\n    :return:\n    \"\"\"\n    response = client.post(\"/admin/auth/login/access-token\", json={\n        \"username\": \"test1@test.com\",\n        \"password\": \"t\"\n    })\n    assert response.status_code == 200\n    assert response.json()[\"code\"] == 4003\n\n\ndef test_get_user(client: TestClient, ordinary_token_headers: dict):\n    \"\"\"\n    测试获取用户信息的接口\n    :return:\n    \"\"\"\n    response = client.get(\"/admin/auth/user/info\", headers=ordinary_token_headers)\n\n    assert response.status_code == 200\n    assert response.json()[\"code\"] == 200\n    assert isinstance(response.json()[\"data\"][\"nickname\"], str)\n"
  },
  {
    "path": "{{cookiecutter.project_name}}/tests/conftest.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2021/2/5 13:33\n# @Author  : CoderCharm\n# @File    : conftest.py\n# @Software: PyCharm\n# @Github  : github/CoderCharm\n# @Email   : wg_python@163.com\n# @Desc    :\n\"\"\"\n\n测试时需要用到的依赖\n\n\"\"\"\nimport os\nimport sys\n\n# 解决包导入问题\nsys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))\n\nfrom typing import Generator\n\nimport pytest\n\nfrom fastapi.testclient import TestClient\n\nfrom core.server import create_app\nfrom common.session import SessionLocal\nfrom tests.utils.user import user_authentication_headers\n\n\n@pytest.fixture(scope=\"session\")\ndef db() -> Generator:\n    yield SessionLocal()\n\n\n@pytest.fixture(scope=\"module\")\ndef client() -> Generator:\n    \"\"\"\n    FastAPI对象\n    :return:\n    \"\"\"\n    app = create_app()\n    with TestClient(app) as c:\n        yield c\n\n\n@pytest.fixture(scope=\"module\")\ndef job_id() -> str:\n    \"\"\"\n    测试定时任务用到的 job_id\n    :return:\n    \"\"\"\n    return \"123\"\n\n\n@pytest.fixture(scope=\"module\")\ndef superuser_token_headers(client: TestClient) -> dict:\n    \"\"\"\n    管理员 admin 用户测试， 后续新增 一份测试环境\n    :param client:\n    :return:\n    \"\"\"\n    return user_authentication_headers(\n        client=client,\n        email=\"admin@admin.com\",\n        password=\"admin\"\n    )\n\n\n@pytest.fixture(scope=\"module\")\ndef ordinary_token_headers(client: TestClient) -> dict:\n    \"\"\"\n    普通用户\n    :param client:\n    :return:\n    \"\"\"\n    return user_authentication_headers(\n        client=client,\n        email=\"test@test.com\",\n        password=\"test\"\n    )\n"
  },
  {
    "path": "{{cookiecutter.project_name}}/tests/utils/user.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2021/2/5 14:00\n# @Author  : CoderCharm\n# @File    : user.py\n# @Software: PyCharm\n# @Github  : github/CoderCharm\n# @Email   : wg_python@163.com\n# @Desc    :\n\"\"\"\n\n\"\"\"\nfrom typing import Dict\nfrom fastapi.testclient import TestClient\n\n\ndef user_authentication_headers(\n        *, client: TestClient, email: str, password: str\n) -> Dict[str, str]:\n    data = {\"username\": email,\n            \"password\": password}\n\n    resp = client.post(\"/admin/auth/login/access-token\", json=data)\n    response = resp.json()\n    token = response[\"data\"][\"token\"]\n    headers = {\"token\": token}\n    return headers\n"
  },
  {
    "path": "{{cookiecutter.project_name}}/utils/cron_task.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2020/12/24 18:21\n# @Author  : CoderCharm\n# @File    : cron_task.py\n# @Software: PyCharm\n# @Github  : github/CoderCharm\n# @Email   : wg_python@163.com\n# @Desc    :\n\"\"\"\n\n\"\"\"\nimport time\n\n\ndef demo_task(a1: str):\n    print(a1, time.time())\n"
  },
  {
    "path": "{{cookiecutter.project_name}}/utils/tools_func.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2020/10/16 15:48\n# @Author  : CoderCharm\n# @File    : tools_func.py\n# @Software: PyCharm\n# @Github  : github/CoderCharm\n# @Email   : wg_python@163.com\n# @Desc    :\n\"\"\"\n\n\"\"\"\n\nimport json\nimport decimal\nimport datetime\nfrom typing import Union\n\n\ndef _alchemy_encoder(obj):\n    \"\"\"\n    处理序列化中的时间和小数\n    :param obj:\n    :return:\n    \"\"\"\n    if isinstance(obj, datetime.date):\n        return obj.strftime(\"%Y-%m-%d %H:%M:%S\")\n    elif isinstance(obj, decimal.Decimal):\n        return float(obj)\n\n\ndef serialize_sqlalchemy_obj(obj) -> Union[dict, list]:\n    \"\"\"\n    序列化fetchall()后的sqlalchemy对象\n    https://codeandlife.com/2014/12/07/sqlalchemy-results-to-json-the-easy-way/\n    :param obj:\n    :return:\n    \"\"\"\n    if isinstance(obj, list):\n        # 转换fetchall()的结果集\n        return json.loads(json.dumps([dict(r) for r in obj], default=_alchemy_encoder))\n    else:\n        # 转换fetchone()后的对象\n        return json.loads(json.dumps(dict(obj), default=_alchemy_encoder))\n"
  }
]