master 89925ab2f980 cached
51 files
64.8 KB
21.2k tokens
110 symbols
1 requests
Download .txt
Repository: CoderCharm/fastapi-mysql-generator
Branch: master
Commit: 89925ab2f980
Files: 51
Total size: 64.8 KB

Directory structure:
gitextract__cxflj8k/

├── .gitignore
├── Pipfile
├── README-en.md
├── README.md
├── cookiecutter.json
├── examples/
│   ├── demo_casbin/
│   │   ├── 01_demo.py
│   │   ├── 02_orm.py
│   │   ├── 03_custom_orm.py
│   │   ├── README.md
│   │   ├── custom_model.conf
│   │   ├── model.conf
│   │   └── policy.csv
│   └── demo_scheduler/
│       └── main.py
└── {{cookiecutter.project_name}}/
    ├── api/
    │   └── v1/
    │       ├── items.py
    │       ├── sys_scheduler.py
    │       └── user.py
    ├── common/
    │   ├── __init__.py
    │   ├── custom_exc.py
    │   ├── deps.py
    │   ├── logger.py
    │   ├── session.py
    │   ├── sys_redis.py
    │   └── sys_schedule.py
    ├── core/
    │   ├── celery_app.py
    │   ├── config/
    │   │   ├── __init__.py
    │   │   └── config.py.example
    │   ├── security.py
    │   └── server.py
    ├── db/
    │   └── mysql/
    │       └── migrations/
    │           └── 20211209-init.sql
    ├── dbconfig.yml
    ├── dbconfig.yml.example
    ├── logic/
    │   └── user_logic.py
    ├── main.py
    ├── models/
    │   └── users.py
    ├── requirements-dev.txt
    ├── requirements.txt
    ├── router/
    │   ├── __init__.py
    │   └── v1_router.py
    ├── schemas/
    │   ├── request/
    │   │   ├── sys_api.py
    │   │   ├── sys_authority_schema.py
    │   │   ├── sys_casbin.py
    │   │   └── sys_user_schema.py
    │   └── response/
    │       └── resp.py
    ├── tests/
    │   ├── README.md
    │   ├── api/
    │   │   └── v1/
    │   │       ├── test_casbin.py
    │   │       ├── test_cron.py
    │   │       └── test_user.py
    │   ├── conftest.py
    │   └── utils/
    │       └── user.py
    └── utils/
        ├── cron_task.py
        └── tools_func.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
.idea

.DS_Store
node_modules/
dist/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
package-lock.json
tests/**/coverage/

# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
*.sqlite

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# Jupyter Notebook
.ipynb_checkpoints

# pyenv
.python-version

# celery beat schedule file
celerybeat-schedule

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/

*/*.yaml
*/core/config/config.py

================================================
FILE: Pipfile
================================================
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true

[dev-packages]

[packages]

[requires]
python_version = "3.8"


================================================
FILE: README-en.md
================================================
# FastAPI and MySql - Base Project Generator

![Python版本](https://img.shields.io/badge/Python-3.7+-brightgreen.svg "版本号")
![FastAPI版本](https://img.shields.io/badge/FastAPI-0.61.1-ff69b4.svg "版本号")

[中文说明](./README.md) | [English](./README-en.md)

## Intro
Web application generator. Using FastAPI, MySql as databas.I am referring to tianolo's full-stack-fastapi-postgresql

https://github.com/tiangolo/full-stack-fastapi-postgresql

I changed it to my favorite format.

![demo](images/demo1.png)


## Features
- JWT token authentication.
- SQLAlchemy models(MySql).
- Alembic migrations.
- Use redis demo.
- File upload demo.
- apscheduler cron task (:noqa)


## How to use it

Go to the directory where you want to create your project and run:

```
pip install cookiecutter
cookiecutter https://github.com/CoderCharm/fastapi-mysql-generator

cd your_project/
pip install -r requirements.txt
```

## Configure your database

In the `project/app/core/config/development_config.py` or `production_config.py` file.

MySql and Redis config info

## Migrate the database

```
cd your_project/

# Generate mapping
alembic revision --autogenerate -m "init commit"

# Generate tables
alembic upgrade head
```

## Create admin user

```
cd your_project/app
python create_user.py
```

## Run
```
cd your_project/app

python main.py

or 

uvicorn main:app --host=127.0.0.1 --port=8010 --reload
```

online doc address
```
http://127.0.0.1:8010/api/docs
```

================================================
FILE: README.md
================================================
# FastAPI and MySQL - 项目生成器

![Python版本](https://img.shields.io/badge/Python-3.7+-brightgreen.svg "版本号")
![FastAPI版本](https://img.shields.io/badge/FastAPI-0.62+-ff69b4.svg "版本号")

[中文说明](./README.md) | [English](./README-en.md)

## 简介
使用FastAPI + MySql 作为数据库的项目生成器, 我是参考FastAPI作者[tiangolo](https://github.com/tiangolo)的 [full-stack-fastapi-postgresql](https://github.com/tiangolo/full-stack-fastapi-postgresql) 项目做的。

我把它改成了自己喜欢的格式。很大程度参考了[奇淼 gin-vue-admin项目](https://github.com/flipped-aurora/gin-vue-admin)

![demo](images/demo1.png)


## 功能
- JWT token 认证。
- 使用SQLAlchemy models(MySql).
- Alembic migrations 数据迁移.
- redis使用演示.
- 文件上传演示.
- apscheduler 定时任务 (不保证稳定 noqa)
- 基于 casbin 的权限验证 (基于 [gin-vue-admin](https://github.com/flipped-aurora/gin-vue-admin) 复刻)


## 学习博客

<details>
<summary>项目学习博客点击展开</summary>

- [【FastAPI 学习 一】配置文件](https://www.cnblogs.com/CharmCode/p/14191077.html)
- [【FastAPI 学习 二】SqlAlchemy Model模型类](https://www.cnblogs.com/CharmCode/p/14191082.html)
- [【FastAPI 学习 三】FastAPI SqlAlchemy MySql表迁移](https://www.cnblogs.com/CharmCode/p/14191090.html)
- [【FastAPI 学习 四】日志配置](https://www.cnblogs.com/CharmCode/p/14191091.html)
- [【FastAPI 学习 五】统一响应json数据格式](https://www.cnblogs.com/CharmCode/p/14191093.html)
- [【FastAPI 学习 六】异常处理](https://www.cnblogs.com/CharmCode/p/14191103.html)
- [【FastAPI 学习 七】GET和POST请求参数接收以及验证](https://www.cnblogs.com/CharmCode/p/14191108.html)
- [【FastAPI 学习 八】JWT token认证登陆](https://www.cnblogs.com/CharmCode/p/14191112.html)
- [【FastAPI 学习 九】图片文件上传](https://www.cnblogs.com/CharmCode/p/14191116.html)
- [【FastAPI 学习 十】使用Redis](https://www.cnblogs.com/CharmCode/p/14191119.html)
- [【FastAPI 学习 十一】项目目录结构demo(自己改版)](https://www.cnblogs.com/CharmCode/p/14191126.html)
- [【FastAPI 学习 十二】定时任务篇](https://www.cnblogs.com/CharmCode/p/14191009.html)
- [【FastAPI 学习 十三】基于`casbin`的权限校验](https://www.cnblogs.com/CharmCode/p/14377547.html)

</details>

## 项目文件组织

> 参考Django文件组织,FastAPI官方推荐项目生成,Flask工厂函数,[gin-vue-admin server文件组织](https://github.com/flipped-aurora/gin-vue-admin)

<details>
<summary>点击展开项目文件结构</summary>

```
.your_project
|__alembic                       // alembic 自动生成的迁移配置文件夹,迁移不正确时 产看其中的env.py文件
| |__versions/                   // 使用 alembic revision --autogenerate -m "注释" 迁移命令后 会产生映射文件
| |__env.py                      // 自动生成的文件
| |__script.py.mako
|__alembic.ini                   // alembic 自动生成的迁移配置文件

|____api                         // API文件夹
| |____v1                        // 版本区分
| | | |____items.py              // 一些接口示例
| | | |____sys_api.py            // API操作 用于权限管理
| | | |____sys_casbin.py         // 添加指定角色权限
| | | |____sys_scheduler.py      // 定时任务调度模块
| | | |____sys_user.py           // user 模块

| |____common                    // 项目通用文件夹
| | |______init__.py             // 导出日志文件方便导入
| | |____custom_exc.py           // 自定义异常
| | |____deps.py                 // 通用依赖文件,如数据库操作对象,权限验证对象
| | |____logger.py               // 扩展日志 loguru 简单配置
| | |____sys_casbin.py           // 生成 casbin
| | |____sys_scheduler.py        // 定义 apscheduler 在core/server 下初始化

|____core                        
| |____config                    // 配置文件
| | |______init__.py             // 根据虚拟环境导入不同配置
| | |____development_config.py   // 开发配置
| | |____production_config.py    // 生产配置
| |____celery_app.py             // celery (目前没有使用)
| |____security.py               // token password验证  
| |____server.py                 // 核心服务文件(重要) 初始化连接等操作
        
| |____db                        // 数据库
| | |____base.py                 // 导出全部models 给alembic迁移用
| | |____base_class.py           // orm model 基类
| | |____session.py              // 链接数据库会话
| | |____sys_redis.py            // 生成redis对象

|____logs/                       // 日志文件夹

| |____models                    // orm models 在这里面新增 (记得导入到 /db/base.py 下面才会迁移成功)
| | |____sys_api.py              // 项目API model
| | |____sys_auth.py             // 用户模块orm
         
| |____resource                  // 存放casbin model
| | |____rbac_model.conf         // casbin model匹配规则

| |____router                    // 路由模块
| | |____v1_router.py            // V1 API分组路由文件(可在这里按照分组添加权限验证)

| |____schemas                   // 请求 或者 响应的 Pydantic model(这里的schemas和model应该整合到一起 作者目前也写了一个 pydantic-sqlalchemy 互转的库 https://github.com/tiangolo/pydantic-sqlalchemy 但是感觉不太完善)
| | |____request                 // 数据验证 model 
| | |____response                // 数据响应 model(我项目里面暂时没有写响应model) 

| |____service                   // ORM 操作文件夹
| | |____curd_base.py            // curd通用基础操作对象
| | |____sys_user.py             // user curd操作

|____static/                     // 静态资源文件(测试时使用,生产建议使用nginx静态资源服务器或者七牛云)
         
|____tests/                      // 测试文件夹

| |____utils                     // 工具类
| | |____cron_task.py            // 定时任务task文件
| | |____tools_func.py           // 序列化orm特殊时间(但是感觉不太优雅)

|____jobs.sqlite                 // 定时任务持久化sqlite 也可以使用其他的比如redis
|____main.py                     // 启动文件
|____init_db.py                  // 生成初始化角色和用户
|____requirements.txt            // 依赖文件

```

</details>


## 如何使用

进入你想要生成项目的文件夹下,并且运行以下命令。

```
pip install cookiecutter
cookiecutter https://github.com/CoderCharm/fastapi-mysql-generator

cd your_project/
# 安装依赖库
pip install -r requirements.txt

# 建议使用 --upgrade 安装最新版 (windows系统下uvloop当前版本可能有问题  https://github.com/MagicStack/uvloop/issues/14)
pip install --upgrade -r requirements-dev.txt
```

## 配置你的数据库环境

在这个文件下 `project/app/core/config/development_config.py` 或者 `production_config.py`。

配置MySQL和Redis信息

## 迁移数据库

> 放弃ORM自带的迁移功能,使用第三方工具 `sql-migrate`,更直观便于管理。可参考 [sql-migrate](https://github.com/rubenv/sql-migrate)


`sql-migrate status -env=dev-mysql`

`sql-migrate up -env=dev-mysql`

`sql-migrate down -env=dev-mysql`



## 创建用户
> 会默认创建两个角色一个为超级管理员角色,一个为普通角色,
超级管理员拥有目前接口的所有调用权限,普通用户只能登录和获取自身用户信息.
```
cd your_project/
python init_db.py
```

## 运行启动

```
# 进入项目文件夹下
cd your_project/

# 命令行运行(开发模式)
uvicorn main:app --host=127.0.0.1 --port=8010 --reload

# 直接运行main文件(会打印两次路由)
python main.py
```

<details>
<summary>可能会出现的常见路径倒入问题</summary>

```
# 如下两种解决方式

# pycharm中设置 标记为sources root
https://www.jetbrains.com/help/pycharm/configuring-content-roots.html#specify-folder-categories

# 命令行中标记为 sources root
https://stackoverflow.com/questions/30461982/how-to-provide-make-directory-as-source-root-from-pycharm-to-terminal

```
</details>

在线文档地址(在配置文件里面设置路径或者关闭)
```
http://127.0.0.1:8010/api/docs
```

## 接口测试

需要安装

```shell
pip install pytest
```

在项目下 启动测试用例
```
cd your_project/
pytest

# 接口测试结果
collected 10 items                                                                                                                             

tests/api/v1/test_casbin.py ....                                                                                                         [ 40%]
tests/api/v1/test_cron.py ...                                                                                                            [ 70%]
tests/api/v1/test_user.py ...                                                                                                            [100%]

============================================================== 10 passed in 1.77s ==============================================================
```


## 部署

部署的时候,可以关闭在线文档,见学习文章一配置篇。

```shell
在main.py同文件下下启动 去掉 --reload 选项 增加 --workers
uvicorn main:app --host=127.0.0.1 --port=8010 --workers=4

# 同样可以也可以配合gunicorn多进程启动  main.py同文件下下启动 默认127.0.0.1:8010端口 gunicorn需要安装
# 参考http://www.uvicorn.org/#running-with-gunicorn
gunicorn main:app -w 4 -k uvicorn.workers.UvicornWorker -b 127.0.0.1:8010
```
<details>
<summary>点击查看托管到后台运行</summary>
  
```shell
# 1 如果为了简单省事 可以直接使用 nohup 命令 如下: run.log文件需要自行创建
nohup /env_path/uvicorn main:app --host=127.0.0.1 --port=8010 --workers=4 > run.log 2>&1 &

# 2 可以使用supervisor托管后台运行部署, 当然也可以使用其他的
# supervisor可以参考我总结的文章 https://www.cnblogs.com/CharmCode/p/14210280.html
```
</details>


================================================
FILE: cookiecutter.json
================================================
{
    "project_name": "fastapi_project"
}

================================================
FILE: examples/demo_casbin/01_demo.py
================================================
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2021/1/27 19:54
# @Author  : CoderCharm
# @File    : simple_example.py
# @Software: PyCharm
# @Github  : github/CoderCharm
# @Email   : wg_python@163.com
# @Desc    :
"""

主要参考官方readme


"""

import casbin

e = casbin.Enforcer('./model.conf', "./policy.csv", True)

sub = "nick"  # the user that wants to access a resource.
obj = "data1"  # the resource that is going to be accessed.
act = "read"  # the operation that the user performs on the resource.

# 添加
# e.add_policy("alice", "data1", "read")
# e.remove_policy("nick", "data1", "read")


if e.enforce(sub, obj, act):
    # permit alice to read data1casbin_sqlalchemy_adapter
    print("通过")
else:
    # deny the request, show an error
    print("拒绝")


================================================
FILE: examples/demo_casbin/02_orm.py
================================================
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2021/1/27 20:06
# @Author  : CoderCharm
# @File    : casbin_orm.py
# @Software: PyCharm
# @Github  : github/CoderCharm
# @Email   : wg_python@163.com
# @Desc    :
"""
https://github.com/pycasbin/sqlalchemy-adapter/blob/master/README.md

pip install casbin_sqlalchemy_adapter

"""

import casbin
import casbin_sqlalchemy_adapter

adapter = casbin_sqlalchemy_adapter.Adapter('sqlite:///test.db')

e = casbin.Enforcer('./model.conf', adapter, True)

sub = "nick"  # the user that wants to access a resource.
obj = "data1"  # the resource that is going to be accessed.
act = "read"  # the operation that the user performs on the resource.

# 添加
# res = e.add_policy("alice", "data1", "read")
# res = e.remove_policy("alice", "data1", "read")
print(res)

# if e.enforce(sub, obj, act):
#     # permit alice to read data1casbin_sqlalchemy_adapter
#     print("通过")
# else:
#     # deny the request, show an error
#     print("拒绝")



================================================
FILE: examples/demo_casbin/03_custom_orm.py
================================================
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2021/1/29 20:49
# @Author  : CoderCharm
# @File    : casbin_custom_orm.py
# @Software: PyCharm
# @Github  : github/CoderCharm
# @Email   : wg_python@163.com
# @Desc    :
"""

加一个自定义的解析函数

"""

import casbin
from casbin import util
import casbin_sqlalchemy_adapter

adapter = casbin_sqlalchemy_adapter.Adapter('sqlite:///test.db')

e = casbin.Enforcer('./custom_model.conf', adapter, True)


# 添加
# e.add_policy("999", "/api/user", "GET")
# 删除
# e.remove_policy("999", "/api/user", "GET")


def params_match(full_name_k1: str, key2: str):
    """
    去掉url ?后面的参数 只取路径
    :param full_name_k1:
    :param key2:
    :return:
    """
    key1 = full_name_k1.split("?")[0]
    return util.key_match2(key1, key2)


def params_match_func(*args):
    name1 = args[0]
    name2 = args[1]
    return params_match(name1, name2)


e.add_function("ParamsMatch", params_match_func)

sub = "999"  # the user that wants to access a resource.
obj = "/api/user?aaa=1"  # the resource that is going to be accessed.
act = "GET"  # the operation that the user performs on the resource.

if e.enforce(sub, obj, act):
    # permit alice to read data1casbin_sqlalchemy_adapter
    print("通过")
else:
    # deny the request, show an error
    print("拒绝")


================================================
FILE: examples/demo_casbin/README.md
================================================
# casbin 权限认证管理

> 一个权限认证的库,主要是用于Go语言的,也实现了其他语言的扩展。我也是看到[奇淼](https://github.com/piexlmax)的B站关于这个的介绍,所以才接触到这个
所以想把这引入到FastAPI中。

## 安装依赖
```
pip install casbin
pip install casbin_sqlalchemy_adapter
```

## 学前准备

可以先在 [官方编辑器](https://casbin.org/zh-CN/editor)里面弄明白不同访问策略的原理。


## 参考

- [官网](https://casbin.org/zh-CN/)
- [官方编辑器](https://casbin.org/zh-CN/editor)
- [奇淼B站关于casbin介绍 基础概念理解](https://www.bilibili.com/video/BV1qz4y167XP)


================================================
FILE: examples/demo_casbin/custom_model.conf
================================================
[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[role_definition]
g = _, _

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = r.sub == p.sub && ParamsMatch(r.obj,p.obj) && r.act == p.act

================================================
FILE: examples/demo_casbin/model.conf
================================================
[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act

================================================
FILE: examples/demo_casbin/policy.csv
================================================
p, nick, data1, read
p, nick, data2, write

================================================
FILE: examples/demo_scheduler/main.py
================================================
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2020/12/25 20:08
# @Author  : CoderCharm
# @File    : main.py
# @Software: PyCharm
# @Github  : github/CoderCharm
# @Email   : wg_python@163.com
# @Desc    :
"""

基础的任务调度演示

"""
import time
from typing import Union
from datetime import datetime

from fastapi import FastAPI, Query, Body
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
from apscheduler.triggers.cron import CronTrigger

Schedule = AsyncIOScheduler(
    jobstores={
        'default': SQLAlchemyJobStore(url='sqlite:///jobs.sqlite')
    }
)
Schedule.start()

app = FastAPI()


# 简单定义返回
def resp_ok(*, code=0, msg="ok", data: Union[list, dict, str] = None) -> dict:
    return {"code": code, "msg": msg, "data": data}


def resp_fail(*, code=1, msg="fail", data: Union[list, dict, str] = None):
    return {"code": code, "msg": msg, "data": data}


def cron_task(a1: str) -> None:
    print(a1, time.strftime("'%Y-%m-%d %H:%M:%S'"))


@app.get("/jobs/all", tags=["schedule"], summary="获取所有job信息")
async def get_scheduled_syncs():
    """
    获取所有job
    :return:
    """
    schedules = []
    for job in Schedule.get_jobs():
        schedules.append(
            {"job_id": job.id, "func_name": job.func_ref, "func_args": job.args, "cron_model": str(job.trigger),
             "next_run": str(job.next_run_time)}
        )
    return resp_ok(data=schedules)


@app.get("/jobs/once", tags=["schedule"], summary="获取指定的job信息")
async def get_target_sync(
        job_id: str = Query("job1", title="任务id")
):
    job = Schedule.get_job(job_id=job_id)

    if not job:
        return resp_fail(msg=f"not found job {job_id}")

    return resp_ok(
        data={"job_id": job.id, "func_name": job.func_ref, "func_args": job.args, "cron_model": str(job.trigger),
              "next_run": str(job.next_run_time)})


# interval 固定间隔时间调度
@app.post("/job/interval/schedule/", tags=["schedule"], summary="开启定时:间隔时间循环")
async def add_interval_job(
        seconds: int = Body(120, title="循环间隔时间/秒,默认120s", embed=True),
        job_id: str = Body(..., title="任务id", embed=True),
        run_time: int =Body(time.time(), title="第一次运行时间", description="默认立即执行", embed=True)
):
    res = Schedule.get_job(job_id=job_id)
    if res:
        return resp_fail(msg=f"{job_id} job already exists")
    schedule_job = Schedule.add_job(cron_task,
                                    'interval',
                                    args=(job_id,),
                                    seconds=seconds,  # 循环间隔时间 秒
                                    id=job_id,  # job ID
                                    next_run_time=datetime.fromtimestamp(run_time)  # 立即执行
                                    )
    return resp_ok(data={"job_id": schedule_job.id})


# date 某个特定时间点只运行一次
@app.post("/job/date/schedule/", tags=["schedule"], summary="开启定时:固定只运行一次时间")
async def add_date_job(
        run_time: int = Body(..., title="时间戳", description="固定只运行一次时间", embed=True),
        job_id: str = Body(..., title="任务id", embed=True),
):
    res = Schedule.get_job(job_id=job_id)
    if res:
        return resp_fail(msg=f"{job_id} job already exists")
    schedule_job = Schedule.add_job(cron_task,
                                    'date',
                                    args=(job_id,),
                                    run_date=datetime.fromtimestamp(run_time),
                                    id=job_id,  # job ID
                                    )
    return resp_ok(data={"job_id": schedule_job.id})


# cron 更灵活的定时任务 可以使用crontab表达式
@app.post("/job/cron/schedule/", tags=["schedule"], summary="开启定时:crontab表达式")
async def add_cron_job(
        job_id: str = Body(..., title="任务id", embed=True),
        crontab: str = Body('*/1 * * * *', title="crontab 表达式"),
        run_time: int =Body(time.time(), title="第一次运行时间", description="默认立即执行", embed=True)
):
    res = Schedule.get_job(job_id=job_id)
    if res:
        return resp_fail(msg=f"{job_id} job already exists")
    schedule_job = Schedule.add_job(cron_task,
                                    CronTrigger.from_crontab(crontab),
                                    args=(job_id,),
                                    id=job_id,  # job ID
                                    next_run_time=datetime.fromtimestamp(run_time)
                                    )
    return resp_ok(data={"job_id": schedule_job.id})


@app.post("/job/del", tags=["schedule"], summary="移除任务")
async def remove_schedule(
        job_id: str = Body(..., title="任务id", embed=True)
):
    res = Schedule.get_job(job_id=job_id)
    if not res:
        return resp_fail(msg=f"not found job {job_id}")
    Schedule.remove_job(job_id)
    return resp_ok()

# 暂停和恢复任务 暂时没看


if __name__ == "__main__":
    import uvicorn

    # 官方推荐是用命令后启动 uvicorn main:app --host=127.0.0.1 --port=8150 --reload
    uvicorn.run(app='main:app', host="127.0.0.1", port=8151, reload=False, debug=True)


================================================
FILE: {{cookiecutter.project_name}}/api/v1/items.py
================================================
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2020/10/16 15:21
# @Author  : CoderCharm
# @File    : endpoints.py
# @Software: PyCharm
# @Github  : github/CoderCharm
# @Email   : wg_python@163.com
# @Desc    :
"""

"""
import os
from typing import Any
import shutil

from tempfile import NamedTemporaryFile
from pathlib import Path

from fastapi import APIRouter, File, UploadFile, Query

from schemas.response import resp
from core.config import settings
from common.sys_redis import redis_client
from models.users import User

router = APIRouter()


@router.get("/test", name="测试接口")
def items_test(
        *,
        bar: str = Query(..., title="测试字段", description="测试字段描述")
) -> Any:
    """
    用户登录
    :param bar:
    :param db:
    :return:
    """
    # 测试redis使用
    redis_client.set("test_items", bar, ex=60)
    redis_test = redis_client.get("test_items")

    return resp.ok(data=redis_test)


@router.get("/user/all", summary="获取所有用户信息", name="获取用户信息")
def get_all_user_info(
        page: int = Query(1),  # 分页等通用字段可以提取出来封装
        page_size: int = Query(20),
):
    user, pagination = User().fetch_all(page=page,page_size=page_size)

    return resp.ok(data=user, pagination=pagination)


@router.post("/upload/file", summary="上传图片", name="上传图片")
def upload_image(
        file: UploadFile = File(...),
):
    # 本地存储临时方案,一般生产都是使用第三方云存储OSS(如七牛云, 阿里云)
    # 建议计算并记录一次 文件md5值 避免重复存储相同资源
    save_dir = f"{settings.BASE_PATH}/static/img"

    if not os.path.exists(save_dir):
        os.mkdir(save_dir)
    try:
        suffix = Path(file.filename).suffix

        with NamedTemporaryFile(delete=False, suffix=suffix, dir=save_dir) as tmp:
            shutil.copyfileobj(file.file, tmp)
            tmp_file_name = Path(tmp.name).name
    finally:
        file.file.close()

    return resp.ok(data={"image": f"/static/img/{tmp_file_name}"})


================================================
FILE: {{cookiecutter.project_name}}/api/v1/sys_scheduler.py
================================================
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2020/12/24 18:24
# @Author  : CoderCharm
# @File    : sys_scheduler.py
# @Software: PyCharm
# @Github  : github/CoderCharm
# @Email   : wg_python@163.com
# @Desc    :
"""
系统调度
"""

from datetime import datetime

from fastapi import APIRouter, Query, Body

from common.sys_schedule import schedule
from schemas.response import resp
from utils.cron_task import demo_task

router = APIRouter()


@router.get("/jobs/all", summary="获取所有job信息", name="获取所有定时任务")
async def get_scheduled_syncs():
    """
    获取所有job
    :return:
    """
    schedules = []
    for job in schedule.get_jobs():
        schedules.append(
            {"job_id": job.id, "func_name": job.func_ref, "func_args": job.args, "cron_model": str(job.trigger),
             "next_run": str(job.next_run_time)}
        )

    return resp.ok(data=schedules)


@router.get("/jobs/once", summary="获取指定的job信息", name="获取指定定时任务")
async def get_target_sync(
        job_id: str = Query(..., title="任务id")
):
    job = schedule.get_job(job_id=job_id)

    if not job:
        return resp.fail(resp.DataNotFound.set_msg(f"not found job {job_id}"))

    return resp.ok(
        data={"job_id": job.id, "func_name": job.func_ref, "func_args": job.args, "cron_model": str(job.trigger),
              "next_run": str(job.next_run_time)})


@router.post("/job/schedule", summary="开始job调度", name="启动定时任务")
async def add_job_to_scheduler(
        *,
        seconds: int = Body(120, title="循环间隔时间/秒,默认120s", embed=True),
        job_id: str = Body(..., title="任务id", embed=True),
):
    """
    简易的任务调度演示 可自行参考文档 https://apscheduler.readthedocs.io/en/stable/
    三种模式
    date: use when you want to run the job just once at a certain point of time
    interval: use when you want to run the job at fixed intervals of time
    cron: use when you want to run the job periodically at certain time(s) of day
    :param seconds:
    :param job_id:
    :return:
    """
    res = schedule.get_job(job_id=job_id)
    if res:
        return resp.fail(resp.InvalidRequest.set_msg(f"{job_id} job already exists"))

    schedule_job = schedule.add_job(demo_task,
                                    'interval',
                                    args=(job_id,),
                                    seconds=seconds,  # 循环间隔时间 秒
                                    id=job_id,  # job ID
                                    next_run_time=datetime.now()  # 立即执行
                                    )
    return resp.ok(data={"id": schedule_job.id})


@router.post("/job/del", summary="移除任务", name="删除定时任务")
async def remove_schedule(
        job_id: str = Body(..., title="job_id", embed=True)
):
    res = schedule.get_job(job_id=job_id)
    if not res:
        return resp.fail(resp.DataNotFound.set_msg(f"not found job {job_id}"))

    schedule.remove_job(job_id)

    return resp.ok()


================================================
FILE: {{cookiecutter.project_name}}/api/v1/user.py
================================================
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2020/10/16 13:38
# @Author  : CoderCharm
# @File    : user.py
# @Software: PyCharm
# @Github  : github/CoderCharm
# @Email   : wg_python@163.com
# @Desc    :
"""

"""

from typing import Any
from datetime import timedelta

from fastapi import APIRouter, Depends

from core import security
from models.users import User
from common import deps, logger
from schemas.response import resp
from core.config import settings
from schemas.request import sys_user_schema
from playhouse.shortcuts import model_to_dict

from logic.user_logic import UserLogic

router = APIRouter()


@router.post("/login", summary="用户登录认证", name="登录")
def login_access_token(
        *,
        req: sys_user_schema.UserPhoneAuth,
) -> Any:
    """
    简单实现登录
    :param req:
    :return:
    """

    # 验证用户 简短的业务可以写在这里
    # user = User.single_by_phone(phone=req.username)
    # if not user:
    #     return resp.fail(resp.DataNotFound.set_msg("账号或密码错误"))
    #
    # if not security.verify_password(req.password, user.password):
    #     return resp.fail(resp.DataNotFound.set_msg("账号或密码错误"))
    #
    # access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
    #
    # # 登录token 存储了user.id
    # return resp.ok(data={
    #     "token": security.create_access_token(user.id, expires_delta=access_token_expires),
    # })

    # 复杂的业务逻辑建议 抽离到 logic文件夹下
    token = UserLogic().user_login_logic(req.username, req.password)
    return resp.ok(data={"token": token})


@router.get("/user/info", summary="获取用户信息", name="获取用户信息", description="此API没有验证权限")
def get_user_info(
        *,
        current_user: User = Depends(deps.get_current_user)
) -> Any:
    """
    获取用户信息 这个路由分组没有验证权限
    :param current_user:
    :return:
    """
    return resp.ok(data=model_to_dict(current_user))


================================================
FILE: {{cookiecutter.project_name}}/common/__init__.py
================================================

from .logger import logger

================================================
FILE: {{cookiecutter.project_name}}/common/custom_exc.py
================================================
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2020/10/15 16:57
# @Author  : CoderCharm
# @File    : custom_exc.py
# @Software: PyCharm
# @Github  : github/CoderCharm
# @Email   : wg_python@163.com
# @Desc    :
"""
自定义异常
"""


class TokenAuthError(Exception):
    def __init__(self, err_desc: str = "User Authentication Failed"):
        self.err_desc = err_desc


class TokenExpired(Exception):
    def __init__(self, err_desc: str = "Token has expired"):
        self.err_desc = err_desc


class AuthenticationError(Exception):
    def __init__(self, err_desc: str = "Permission denied"):
        self.err_desc = err_desc



================================================
FILE: {{cookiecutter.project_name}}/common/deps.py
================================================
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2020/10/16 11:51
# @Author  : CoderCharm
# @File    : deps.py
# @Software: PyCharm
# @Github  : github/CoderCharm
# @Email   : wg_python@163.com
# @Desc    :
"""

一些通用的依赖功能

"""
from typing import Any, Union, Optional
#
from jose import jwt
from fastapi import Header, Depends, Request
from pydantic import ValidationError

from common import custom_exc
from models.users import User
from core.config import settings


def check_jwt_token(
        token: Optional[str] = Header(..., description="登录token")
) -> Union[str, Any]:
    """
    解析验证token  默认验证headers里面为token字段的数据
    可以给 headers 里面token替换别名, 以下示例为 X-Token
    token: Optional[str] = Header(None, alias="X-Token")
    :param token:
    :return:
    """

    try:
        payload = jwt.decode(
            token,
            settings.SECRET_KEY, algorithms=[settings.ALGORITHM]
        )
        return payload
    except jwt.ExpiredSignatureError:
        raise custom_exc.TokenExpired()
    except (jwt.JWTError, ValidationError, AttributeError):
        raise custom_exc.TokenAuthError()


def get_current_user(
        token: Optional[str] = Depends(check_jwt_token)
) -> User:
    """
    根据header中token 获取当前用户
    :param db:
    :param token:
    :return:
    """
    user = User.single_by_id(uid=token.get("sub"))
    if not user:
        raise custom_exc.TokenAuthError(err_desc="User not found")
    return user

================================================
FILE: {{cookiecutter.project_name}}/common/logger.py
================================================
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2020/10/15 16:58
# @Author  : CoderCharm
# @File    : logger.py
# @Software: PyCharm
# @Github  : github/CoderCharm
# @Email   : wg_python@163.com
# @Desc    :
"""

日志文件配置 参考链接
https://github.com/Delgan/loguru

# 本来是想 像flask那样把日志对象挂载到app对象上,作者建议直接使用全局对象
https://github.com/tiangolo/fastapi/issues/81#issuecomment-473677039

考虑是否应该把logger 改成单例

"""

import os
import time
from loguru import logger

from core.config import settings

# 定位到log日志文件
log_path = os.path.join(settings.BASE_PATH, 'logs')

if not os.path.exists(log_path):
    os.mkdir(log_path)

log_path_info = os.path.join(log_path, f'{time.strftime("%Y-%m-%d")}_info.log')
log_path_warning = os.path.join(log_path, f'{time.strftime("%Y-%m-%d")}_warning.log')
log_path_error = os.path.join(log_path, f'{time.strftime("%Y-%m-%d")}_error.log')

# 日志简单配置 文件区分不同级别的日志
logger.add(log_path_info, rotation="500 MB", encoding='utf-8', enqueue=True, level='INFO')
logger.add(log_path_warning, rotation="500 MB", encoding='utf-8', enqueue=True, level='WARNING')
logger.add(log_path_error, rotation="500 MB", encoding='utf-8', enqueue=True, level='ERROR')


__all__ = ["logger"]


================================================
FILE: {{cookiecutter.project_name}}/common/session.py
================================================
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2020/10/16 11:42
# @Author  : CoderCharm
# @File    : session.py
# @Software: PyCharm
# @Github  : github/CoderCharm
# @Email   : wg_python@163.com
# @Desc    :
"""

"""
import math
import datetime

from peewee import _ConnectionState, Model, ModelSelect, SQL, DateTimeField
from contextvars import ContextVar
from playhouse.pool import PooledMySQLDatabase

from core.config import settings

db_state_default = {"closed": None, "conn": None, "ctx": None, "transactions": None}
db_state = ContextVar("db_state", default=db_state_default.copy())


# reference to https://fastapi.tiangolo.com/advanced/sql-databases-peewee/#context-variable-sub-dependency
class PeeweeConnectionState(_ConnectionState):
    def __init__(self, **kwargs):
        super().__setattr__("_state", db_state)
        super().__init__(**kwargs)

    def __setattr__(self, name, value):
        self._state.get()[name] = value

    def __getattr__(self, name):
        return self._state.get()[name]


db = PooledMySQLDatabase(
    settings.MYSQL_DATABASE,
    max_connections=8,
    stale_timeout=300,
    user=settings.MYSQL_USERNAME,
    host=settings.MYSQL_HOST,
    password=settings.MYSQL_PASSWORD,
    port=settings.MYSQL_PORT
)

db._state = PeeweeConnectionState()


class BaseModel(Model):
    deleted_at = DateTimeField()
    created_at = DateTimeField(default=datetime.datetime.now())
    updated_at = DateTimeField(default=datetime.datetime.now())

    @classmethod
    def undelete(cls):
        # for logic delete
        return cls.select().where(SQL("deleted_at is NULL"))

    class Meta:
        database = db


def paginator(query: ModelSelect, page: int, page_size: int, order_by: str = "id ASC"):
    count = query.count()
    if page < 1:
        page = 1

    if page_size <= 0:
        page_size = 10

    if page_size >= 100:
        page_size = 100

    if page == 1:
        offset = 0
    else:
        offset = (page - 1) * page_size

    query = query.offset(offset).limit(page_size).order_by(SQL(order_by))

    total_pages = math.ceil(count / page_size)

    paginate = {
        "total_pages": total_pages,
        "count": count,
        "current_page": page,
        "pre_page": page - 1 if page > 1 else page,
        "next_page": page if page == total_pages else page + 1
    }

    return list(query.dicts()), paginate


================================================
FILE: {{cookiecutter.project_name}}/common/sys_redis.py
================================================
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2021/1/28 20:34
# @Author  : CoderCharm
# @File    : sys_redis.py
# @Software: PyCharm
# @Github  : github/CoderCharm
# @Email   : wg_python@163.com
# @Desc    :
"""

通过class 实例化对象可以直接修改内部属性的特性
再通过魔法方法,赋予实例化对象 具有内部属性_redis_client的方法和属性

主要参考 flask-redis扩展实现
https://github.com/underyx/flask-redis/blob/master/flask_redis/client.py

redis 连接

"""
import sys
from redis import Redis, AuthenticationError

from common.logger import logger
from core.config import settings


class RedisCli(object):

    def __init__(self, *, host: str, port: int, password: str, db: int, socket_timeout: int = 5):
        # redis对象 在 @app.on_event("startup") 中连接创建
        self._redis_client = None
        self.host = host
        self.port = port
        self.password = password
        self.db = db
        self.socket_timeout = socket_timeout

    def init_redis_connect(self) -> None:
        """
        初始化连接
        :return:
        """
        try:
            self._redis_client = Redis(
                host=self.host,
                port=self.port,
                password=self.password,
                db=self.db,
                socket_timeout=self.socket_timeout,
                decode_responses=True  # 解码
            )
            if not self._redis_client.ping():
                logger.info("连接redis超时")
                sys.exit()
        except (AuthenticationError, Exception) as e:
            logger.info(f"连接redis异常 {e}")
            sys.exit()

    # 使实例化后的对象 赋予redis对象的的方法和属性
    def __getattr__(self, name):
        return getattr(self._redis_client, name)

    def __getitem__(self, name):
        return self._redis_client[name]

    def __setitem__(self, name, value):
        self._redis_client[name] = value

    def __delitem__(self, name):
        del self._redis_client[name]


# 创建redis连接对象
redis_client: Redis = RedisCli(
    host=settings.REDIS_HOST,
    port=settings.REDIS_PORT,
    password=settings.REDIS_PASSWORD,
    db=settings.REDIS_DB,
    socket_timeout=settings.REDIS_TIMEOUT
)

# 只允许导出 redis_client 实例化对象
__all__ = ["redis_client"]


================================================
FILE: {{cookiecutter.project_name}}/common/sys_schedule.py
================================================
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2021/1/29 20:33
# @Author  : CoderCharm
# @File    : sys_schedule.py
# @Software: PyCharm
# @Github  : github/CoderCharm
# @Email   : wg_python@163.com
# @Desc    :
"""
初始化 apscheduler
模仿 redis

"""

from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore


class ScheduleCli(object):
    _instance = None

    def __new__(cls, *args, **kw):
        if cls._instance is None:
            cls._instance = object.__new__(cls, *args, **kw)
        return cls._instance

    def __init__(self):
        # 对象 在 @app.on_event("startup") 中初始化
        self._schedule = None

    def init_scheduler(self) -> None:
        """
        初始化 apscheduler
        :return:
        """
        job_stores = {
            'default': SQLAlchemyJobStore(url='sqlite:///jobs.sqlite')
        }
        self._schedule = AsyncIOScheduler(jobstores=job_stores)
        self._schedule.start()

    # 使实例化后的对象 赋予apscheduler对象的方法和属性
    def __getattr__(self, name):
        return getattr(self._schedule, name)

    def __getitem__(self, name):
        return self._schedule[name]

    def __setitem__(self, name, value):
        self._schedule[name] = value

    def __delitem__(self, name):
        del self._schedule[name]


# 创建schedule对象
schedule: AsyncIOScheduler = ScheduleCli()

# 只允许导出 redis_client 实例化对象
__all__ = ["schedule"]


================================================
FILE: {{cookiecutter.project_name}}/core/celery_app.py
================================================
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2020/10/17 15:03
# @Author  : CoderCharm
# @File    : celery_app.py
# @Software: PyCharm
# @Github  : github/CoderCharm
# @Email   : wg_python@163.com
# @Desc    :
"""

"""


from celery import Celery

celery_app = Celery("worker", broker="amqp://guest@queue//")

celery_app.conf.task_routes = {"app.worker.test_celery": "main-queue"}


================================================
FILE: {{cookiecutter.project_name}}/core/config/__init__.py
================================================
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2020/9/18 13:50
# @Author  : CoderCharm
# @File    : __init__.py.py
# @Software: PyCharm
# @Github  : github/CoderCharm
# @Email   : wg_python@163.com
# @Desc    :
"""

配置文件区分生产和开发

我这种是一种方式,简单直观
还有一种是服务一个固定路径放一个配置文件如 /etc/conf 下 xxx.ini 或者 xxx.py文件
然后项目默认读取 /etc/conf 目录下的配置文件,能读取则为生产环境,
读取不到则为开发环境,开发环境配置可以直接写在代码里面(或者配置ide环境变量)

根据环境变量ENV是否有值 区分生产开发
以上弃用

"""

import os

from .config import settings



================================================
FILE: {{cookiecutter.project_name}}/core/config/config.py.example
================================================


import os

from typing import Union, Optional

from pydantic import AnyHttpUrl, BaseSettings, IPvAnyAddress


class Settings(BaseSettings):
    # 开发模式配置
    DEBUG: bool = True
    # 项目文档
    TITLE: str = "FastAPI+MySQL项目生成"
    DESCRIPTION: str = "更多FastAPI知识,请关注我的个人网站 https://www.charmcode.cn/"
    # 文档地址 默认为docs 生产环境关闭 None
    DOCS_URL: str = "/api/docs"
    # 文档关联请求数据接口
    OPENAPI_URL: str = "/api/openapi.json"
    # redoc 文档
    REDOC_URL: Optional[str] = "/api/redoc"

    # token过期时间 分钟
    ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8

    # 生成token的加密算法
    ALGORITHM: str = "HS256"

    # 生产环境保管好 SECRET_KEY
    SECRET_KEY: str = 'xxxxxxxx'

    # 项目根路径
    BASE_PATH: str = os.path.dirname(os.path.dirname(os.path.dirname((os.path.abspath(__file__)))))

    # 配置你的Mysql环境
    MYSQL_USERNAME: str = "root"
    MYSQL_PASSWORD: str = "root"
    MYSQL_HOST: str = "127.0.0.1"
    MYSQL_PORT: int = 3306
    MYSQL_DATABASE: str = 'fastapi'

    # redis配置
    REDIS_HOST: str = "127.0.0.1"
    REDIS_PASSWORD: str = ""
    REDIS_DB: int = 0
    REDIS_PORT: int = 6379
    REDIS_URL: str = f"redis://:{REDIS_PASSWORD}@{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB}?encoding=utf-8"
    REDIS_TIMEOUT: int = 5  # redis连接超时时间

    CASBIN_MODEL_PATH: str = "./resource/rbac_model.conf"


settings = Settings()


================================================
FILE: {{cookiecutter.project_name}}/core/security.py
================================================
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2020/9/22 13:20
# @Author  : CoderCharm
# @File    : security.py
# @Software: PyCharm
# @Github  : github/CoderCharm
# @Email   : wg_python@163.com
# @Desc    :
"""
token password 验证
pip install python-jose
pip install passlib
pip install bcrypt

"""
from typing import Any, Union
from datetime import datetime, timedelta

from jose import jwt
from passlib.context import CryptContext

from core.config import settings

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")


def create_access_token(
        subject: Union[str, Any],
        expires_delta: timedelta = None
) -> str:
    """
    生成token
    :param subject:需要存储到token的数据(注意token里面的数据,属于公开的)
    :param expires_delta:
    :return:
    """
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(
            minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES
        )
    to_encode = {"exp": expire, "sub": str(subject)}
    encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
    return encoded_jwt


def verify_password(plain_password: str, hashed_password: str) -> bool:
    """
    验证密码
    :param plain_password: 原密码
    :param hashed_password: hash后的密码
    :return:
    """
    return pwd_context.verify(plain_password, hashed_password)


def get_password_hash(password: str) -> str:
    """
    获取 hash 后的密码
    :param password:
    :return:
    """
    return pwd_context.hash(password)


================================================
FILE: {{cookiecutter.project_name}}/core/server.py
================================================
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2020/10/15 16:24
# @Author  : CoderCharm
# @File    : server.py.py
# @Software: PyCharm
# @Github  : github/CoderCharm
# @Email   : wg_python@163.com
# @Desc    :
"""
包导入的时候最好遵循包导入顺序原则

标准库
第三方库
项目自定义
"""

import traceback

from fastapi import FastAPI, Request, Response
from starlette.middleware.cors import CORSMiddleware
from fastapi.exceptions import RequestValidationError, ValidationError

from router.v1_router import api_v1_router
from core.config import settings
from common.logger import logger
from common import custom_exc
from common.sys_schedule import schedule
from common.sys_redis import redis_client
from common.session import db
from schemas.response import resp


def create_app() -> FastAPI:
    """
    生成FatAPI对象
    :return:
    """
    app = FastAPI(
        debug=settings.DEBUG,
        title=settings.TITLE,
        description=settings.DESCRIPTION,
        docs_url=settings.DOCS_URL,
        openapi_url=settings.OPENAPI_URL,
        redoc_url=settings.REDOC_URL
    )

    # 其余的一些全局配置可以写在这里 多了可以考虑拆分到其他文件夹

    # 跨域设置
    register_cors(app)

    # 注册路由
    register_router(app)

    # 注册捕获全局异常
    register_exception(app)

    # 请求拦截
    register_hook(app)

    # 取消挂载在 request对象上面的操作,感觉特别麻烦,直接使用全局的
    register_init(app)

    if settings.DEBUG:
        # 注册静态文件
        register_static_file(app)

    return app


def register_static_file(app: FastAPI) -> None:
    """
    静态文件交互开发模式使用
    生产使用 nginx 静态资源服务
    这里是开发是方便本地
    :param app:
    :return:
    """
    import os
    from fastapi.staticfiles import StaticFiles
    if not os.path.exists("./static"):
        os.mkdir("./static")
    app.mount("/static", StaticFiles(directory="static"), name="static")


def register_router(app: FastAPI) -> None:
    """
    注册路由
    :param app:
    :return:
    """
    # 项目API
    app.include_router(
        api_v1_router,
    )


def register_cors(app: FastAPI) -> None:
    """
    支持跨域
    :param app:
    :return:
    """
    if settings.DEBUG:
        app.add_middleware(
            CORSMiddleware,
            allow_origins=["*"],
            allow_credentials=True,
            allow_methods=["*"],
            allow_headers=["*"],
        )


def register_exception(app: FastAPI) -> None:
    """
    全局异常捕获
    注意 别手误多敲一个s
    exception_handler
    exception_handlers
    两者有区别
        如果只捕获一个异常 启动会报错
        @exception_handlers(UserNotFound)
    TypeError: 'dict' object is not callable
    :param app:
    :return:
    """

    # 自定义异常 捕获
    @app.exception_handler(custom_exc.TokenExpired)
    async def user_not_found_exception_handler(request: Request, exc: custom_exc.TokenExpired):
        """
        token过期
        :param request:
        :param exc:
        :return:
        """
        logger.error(
            f"token未知用户\nURL:{request.method}{request.url}\nHeaders:{request.headers}\n{traceback.format_exc()}")

        return resp.fail(message=exc.err_desc)

    @app.exception_handler(custom_exc.TokenAuthError)
    async def user_token_exception_handler(request: Request, exc: custom_exc.TokenAuthError):
        """
        用户token异常
        :param request:
        :param exc:
        :return:
        """
        logger.error(f"用户认证异常\nURL:{request.method}{request.url}\nHeaders:{request.headers}\n{traceback.format_exc()}")

        return resp.fail(resp.DataNotFound.set_msg(exc.err_desc))

    @app.exception_handler(custom_exc.AuthenticationError)
    async def user_not_found_exception_handler(request: Request, exc: custom_exc.AuthenticationError):
        """
        用户权限不足
        :param request:
        :param exc:
        :return:
        """
        logger.error(f"用户权限不足 \nURL:{request.method}{request.url}")
        return resp.fail(resp.PermissionDenied)

    @app.exception_handler(ValidationError)
    async def inner_validation_exception_handler(request: Request, exc: ValidationError):
        """
        内部参数验证异常
        :param request:
        :param exc:
        :return:
        """
        logger.error(
            f"内部参数验证错误\nURL:{request.method}{request.url}\nHeaders:{request.headers}\n{traceback.format_exc()}")
        return resp.fail(resp.BusinessError.set_msg(exc.errors()))

    @app.exception_handler(RequestValidationError)
    async def request_validation_exception_handler(request: Request, exc: RequestValidationError):
        """
        请求参数验证异常
        :param request:
        :param exc:
        :return:
        """
        logger.error(
            f"请求参数格式错误\nURL:{request.method}{request.url}\nHeaders:{request.headers}\n{traceback.format_exc()}")
        # return response_code.resp_4001(message='; '.join([f"{e['loc'][1]}: {e['msg']}" for e in exc.errors()]))
        return resp.fail(resp.InvalidParams.set_msg(exc.errors()))

    # 捕获全部异常
    @app.exception_handler(Exception)
    async def all_exception_handler(request: Request, exc: Exception):
        """
        全局所有异常
        :param request:
        :param exc:
        :return:
        """
        logger.error(f"全局异常\n{request.method}URL:{request.url}\nHeaders:{request.headers}\n{traceback.format_exc()}")
        return resp.fail(resp.ServerError)


def register_hook(app: FastAPI) -> None:
    """
    请求响应拦截 hook
    https://fastapi.tiangolo.com/tutorial/middleware/
    :param app:
    :return:
    """

    @app.middleware("http")
    async def logger_request(request: Request, call_next) -> Response:
        # https://stackoverflow.com/questions/60098005/fastapi-starlette-get-client-real-ip
        # logger.info(f"访问记录:{request.method} url:{request.url}\nheaders:{request.headers}\nIP:{request.client.host}")
        response = await call_next(request)
        return response


def register_init(app: FastAPI) -> None:
    """
    初始化连接
    :param app:
    :return:
    """

    @app.on_event("startup")
    async def init_connect():
        # 连接redis
        redis_client.init_redis_connect()

        # 初始化 apscheduler
        schedule.init_scheduler()

        db.connect()

    @app.on_event('shutdown')
    async def shutdown_connect():
        """
        关闭
        :return:
        """
        schedule.shutdown()

        if not db.is_closed():
            db.close()



================================================
FILE: {{cookiecutter.project_name}}/db/mysql/migrations/20211209-init.sql
================================================

-- +migrate Up
CREATE TABLE IF NOT EXISTS `users` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `email` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `password` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `phone` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `username` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `avatar` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '头像',
  `deleted_at` timestamp NULL DEFAULT NULL,
  `created_at` timestamp NULL DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `users_email_unique` (`email`),
  UNIQUE KEY `users_phone_unique` (`phone`),
  UNIQUE KEY `users_username_unique` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='用户表';

/******************************************/
/*  密码都是hash后的 12345    */
/******************************************/
INSERT 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);
INSERT 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);

-- +migrate Down
DROP TABLE IF EXISTS `users`;
DELETE FROM  users where id =1
DELETE FROM  users where id =2




================================================
FILE: {{cookiecutter.project_name}}/dbconfig.yml
================================================
dev-mysql:
  dialect: mysql
  datasource: root:123456@tcp(127.0.0.1:3306)/fastapi?parseTime=true
  dir: db/mysql/migrations
  table: migrations

================================================
FILE: {{cookiecutter.project_name}}/dbconfig.yml.example
================================================
dev-mysql:
  dialect: mysql
  datasource: root:123456@tcp(127.0.0.1:3306)/fastapi?parseTime=true
  dir: db/mysql/migrations
  table: migrations

================================================
FILE: {{cookiecutter.project_name}}/logic/user_logic.py
================================================
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# 业务逻辑多了就写在这里

from datetime import timedelta

from models.users import User
from schemas.response import resp
from core import security
from core.config import settings
from common import custom_exc


class UserLogic(object):

    @staticmethod
    def user_login_logic(phone: int, password: str):
        user = User.single_by_phone(phone=phone)
        if not user:
            raise custom_exc.TokenAuthError(err_desc="账号或密码错误")

        if not security.verify_password(password, user.password):
            raise custom_exc.TokenAuthError(err_desc="账号或密码错误")

        access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)

        return security.create_access_token(user.id, expires_delta=access_token_expires)

    def xxx_logic(self):
        pass


================================================
FILE: {{cookiecutter.project_name}}/main.py
================================================
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2020/10/15 16:24
# @Author  : CoderCharm
# @File    : main.py
# @Software: PyCharm
# @Github  : github/CoderCharm
# @Email   : wg_python@163.com
# @Desc    :

"""
pip install uvicorn
# 推荐启动方式 main指当前文件名字 app指FastAPI实例化后对象名称
uvicorn main:app --host=127.0.0.1 --port=8010 --reload

类似flask 工厂模式创建
# 生产启动命令 去掉热重载 (可用supervisor托管后台运行)

在main.py同文件下下启动
uvicorn main:app --host=127.0.0.1 --port=8010 --workers=4

# 同样可以也可以配合gunicorn多进程启动  main.py同文件下下启动 默认127.0.0.1:8000端口
gunicorn main:app -w 4 -k uvicorn.workers.UvicornWorker -b 127.0.0.1:8020

"""


from core.server import create_app


app = create_app()

if __name__ == "__main__":
    import uvicorn

    # 输出所有的路由
    for route in app.routes:
        if hasattr(route, "methods"):
            print({'path': route.path, 'name': route.name, 'methods': route.methods})

    uvicorn.run(app='main:app', host="127.0.0.1", port=8010, reload=True, debug=True)



================================================
FILE: {{cookiecutter.project_name}}/models/users.py
================================================
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2020/10/16 13:31
# @Author  : CoderCharm
# @File    : auth.py
# @Software: PyCharm
# @Github  : github/CoderCharm
# @Email   : wg_python@163.com
# @Desc    :
"""
纯增删改查操作,写在model里面
"""

from common.session import BaseModel, paginator
from peewee import CharField, IntegerField
# from playhouse.shortcuts import model_to_dict, dict_to_model


class User(BaseModel):
    """
    用户表
    """
    id = IntegerField()
    name = CharField()
    email = CharField()
    phone = IntegerField()
    username = CharField()
    avatar = CharField()
    password = CharField()

    class Meta:
        table_name = 'users'

    @classmethod
    def single_by_id(cls, uid: int):
        db = User.undelete().select(User.id, User.name, User.email, User.phone, User.username, User.avatar).\
            where(User.id == uid)
        return db.first()

    @classmethod
    def single_by_phone(cls, phone: int = 0):
        db = User.select()

        if phone != 0:
            db = db.where(User.phone == phone)
        return db.first()
        # if db:
        #     return model_to_dict(db)

    @classmethod
    def fetch_all(cls, page: int = 1, page_size: int = 10):
        db = User.undelete().select(User.name, User.email, User.phone, User.username, User.avatar, User.created_at,
                                    User.deleted_at)

        user_list, paginate = paginator(db, page, page_size, "id desc")

        return user_list, paginate




================================================
FILE: {{cookiecutter.project_name}}/requirements-dev.txt
================================================
aiofiles
aioredis
alembic
loguru
fastapi
uvicorn
SQLAlchemy
python-jose
passlib
PyMySQL
gunicorn
databases[mysql]

================================================
FILE: {{cookiecutter.project_name}}/requirements.txt
================================================
aiofiles==0.6.0
aioredis==1.3.1
alembic==1.4.3
amqp==5.0.1
APScheduler==3.6.3
arrow==0.17.0
async-timeout==3.0.1
attrs==20.3.0
bcrypt==3.1.7
billiard==3.6.3.0
binaryornot==0.4.4
celery==5.0.0
certifi==2020.6.20
cffi==1.14.0
chardet==3.0.4
click==7.1.2
click-didyoumean==0.0.3
click-repl==0.1.6
cookiecutter==1.7.2
dnspython==1.16.0
ecdsa==0.14.1
email-validator==1.1.1
fastapi==0.63.0
gunicorn==20.0.4
h11==0.9.0
hiredis==1.1.0
httptools==0.1.1
idna==2.9
iniconfig==1.1.1
install==1.3.4
Jinja2==2.11.2
jinja2-time==0.2.0
kombu==5.0.2
loguru==0.5.3
Mako==1.1.3
MarkupSafe==1.1.1
packaging==20.8
passlib==1.7.4
pluggy==0.13.1
poyo==0.5.0
prompt-toolkit==3.0.8
py==1.10.0
pyasn1==0.4.8
pycparser==2.20
pydantic==1.5.1
PyMySQL==1.0.2
pyparsing==2.4.7
pytest==6.2.1
python-dateutil==2.8.1
python-editor==1.0.4
python-jose==3.2.0
python-multipart==0.0.5
python-slugify==4.0.1
pytz==2020.1
requests==2.24.0
rsa==4.6
six==1.15.0
SQLAlchemy==1.4.6
starlette==0.13.6
text-unidecode==1.3
toml==0.10.2
tzlocal==2.1
urllib3==1.25.10
uvicorn==0.13.4
uvloop==0.14.0
vine==5.0.0
wcwidth==0.2.5
websockets==8.1


================================================
FILE: {{cookiecutter.project_name}}/router/__init__.py
================================================
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2021/1/21 09:03
# @Author  : CoderCharm
# @File    : __init__.py.py
# @Software: PyCharm
# @Github  : github/CoderCharm
# @Email   : wg_python@163.com
# @Desc    :
"""

"""



================================================
FILE: {{cookiecutter.project_name}}/router/v1_router.py
================================================
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2020/10/15 17:45
# @Author  : CoderCharm
# @File    : __init__.py
# @Software: PyCharm
# @Github  : github/CoderCharm
# @Email   : wg_python@163.com
# @Desc    :
"""

版本路由区分

# 可以在这里添加所需要的依赖
https://fastapi.tiangolo.com/tutorial/bigger-applications/#import-fastapi

"""

from fastapi import APIRouter
# from common.deps import check_authority

from api.v1.user import router as user_router
from api.v1.items import router as items_router
from api.v1.sys_scheduler import router as scheduler_router


api_v1_router = APIRouter()

# api_v1_router.include_router(items_router, tags=["测试API"], dependencies=[Depends(check_jwt_token)])
# check_authority 权限验证内部包含了 token 验证 如果不校验权限可直接 dependencies=[Depends(check_jwt_token)]
api_v1_router.include_router(items_router, tags=["测试接口"])
api_v1_router.include_router(user_router, prefix="/user", tags=["用户"])
api_v1_router.include_router(scheduler_router, tags=["任务调度"])


================================================
FILE: {{cookiecutter.project_name}}/schemas/request/sys_api.py
================================================
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2021/1/29 21:11
# @Author  : CoderCharm
# @File    : sys_api.py
# @Software: PyCharm
# @Github  : github/CoderCharm
# @Email   : wg_python@163.com
# @Desc    :
"""
path = Column(VARCHAR(128), comment="API路径")
description = Column(VARCHAR(64), comment="API描述")
api_group = Column(VARCHAR(32), comment="API分组")
method = Column(VARCHAR(16), comment="请求方法")
"""
from pydantic import BaseModel


# 创建API
class ApiCreate(BaseModel):
    path: str
    description: str
    api_group: str
    method: str


class UpdateApi(BaseModel):
    id: str


class DelApi(BaseModel):
    id: str


================================================
FILE: {{cookiecutter.project_name}}/schemas/request/sys_authority_schema.py
================================================
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2021/1/28 19:40
# @Author  : CoderCharm
# @File    : sys_authority_schema.py
# @Software: PyCharm
# @Github  : github/CoderCharm
# @Email   : wg_python@163.com
# @Desc    :
"""

角色的校验规则

"""
from pydantic import BaseModel


# 角色创建
class AuthorityCreate(BaseModel):
    authority_id: int
    authority_name: str
    parent_id: int


# 修改角色(只允许修改角色名)
class AuthorityUpdate(BaseModel):
    authority_name: str


================================================
FILE: {{cookiecutter.project_name}}/schemas/request/sys_casbin.py
================================================
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2021/1/29 20:30
# @Author  : CoderCharm
# @File    : sys_casbin.py
# @Software: PyCharm
# @Github  : github/CoderCharm
# @Email   : wg_python@163.com
# @Desc    :
"""
校验casbin
"""

from pydantic import BaseModel


# 创建API
class AuthCreate(BaseModel):
    authority_id: str
    path: str
    method: str


================================================
FILE: {{cookiecutter.project_name}}/schemas/request/sys_user_schema.py
================================================
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2020/10/16 13:43
# @Author  : CoderCharm
# @File    : sys_user_schema.py
# @Software: PyCharm
# @Github  : github/CoderCharm
# @Email   : wg_python@163.com
# @Desc    :
"""
管理员表的 字段model模型 验证 响应(没写)等
"""

from typing import Optional

from pydantic import BaseModel, EmailStr, AnyHttpUrl


# Shared properties
class UserBase(BaseModel):
    email: Optional[EmailStr] = None
    phone: int = None
    is_active: Optional[bool] = True


class UserAuth(BaseModel):
    password: str


# 邮箱登录认证 验证数据字段都叫username
class UserEmailAuth(UserAuth):
    username: EmailStr


# 手机号登录认证 验证数据字段都叫username
class UserPhoneAuth(UserAuth):
    username: int


# 创建账号需要验证的条件
class UserCreate(UserBase):
    nickname: str
    email: EmailStr
    password: str
    authority_id: int = 1
    avatar: Optional[AnyHttpUrl] = None


# Properties to receive via API on update
class UserUpdate(UserBase):
    password: Optional[str] = None


class UserInDBBase(UserBase):
    id: Optional[int] = None

    class Config:
        orm_mode = True


class UserInDB(UserInDBBase):
    hashed_password: str


# 返回的用户信息
class UserInfo(BaseModel):
    role_id: int
    role: str
    nickname: str
    avatar: AnyHttpUrl



================================================
FILE: {{cookiecutter.project_name}}/schemas/response/resp.py
================================================
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2020/9/22 13:32
# @Author  : CoderCharm
# @File    : response_code.py
# @Software: PyCharm
# @Github  : github/CoderCharm
# @Email   : wg_python@163.com
# @Desc    :
"""

统一响应状态码

"""
from typing import Union

from fastapi import status as http_status
from fastapi.responses import JSONResponse, Response
from fastapi.encoders import jsonable_encoder


class Resp(object):
    def __init__(self, status: int, msg: str, code: int):
        self.status = status
        self.msg = msg
        self.code = code

    def set_msg(self, msg):
        self.msg = msg
        return self


InvalidRequest: Resp = Resp(1000, "无效的请求", http_status.HTTP_400_BAD_REQUEST)
InvalidParams: Resp = Resp(1002, "无效的参数", http_status.HTTP_400_BAD_REQUEST)
BusinessError: Resp = Resp(1003, "业务错误", http_status.HTTP_400_BAD_REQUEST)
DataNotFound: Resp = Resp(1004, "查询失败", http_status.HTTP_400_BAD_REQUEST)
DataStoreFail: Resp = Resp(1005, "新增失败", http_status.HTTP_400_BAD_REQUEST)
DataUpdateFail: Resp = Resp(1006, "更新失败", http_status.HTTP_400_BAD_REQUEST)
DataDestroyFail: Resp = Resp(1007, "删除失败", http_status.HTTP_400_BAD_REQUEST)
PermissionDenied: Resp = Resp(1008, "权限拒绝", http_status.HTTP_403_FORBIDDEN)
ServerError: Resp = Resp(5000, "服务器繁忙", http_status.HTTP_500_INTERNAL_SERVER_ERROR)


def ok(*, data: Union[list, dict, str] = None, pagination: dict = None, msg: str = "success") -> Response:
    return JSONResponse(
        status_code=http_status.HTTP_200_OK,
        content=jsonable_encoder({
            'status': 200,
            'msg': msg,
            'data': data,
            'pagination': pagination
        })
    )


def fail(resp: Resp) -> Response:
    return JSONResponse(
        status_code=resp.code,
        content=jsonable_encoder({
            'status': resp.status,
            'msg': resp.msg,
        })
    )


================================================
FILE: {{cookiecutter.project_name}}/tests/README.md
================================================
## 单元测试

> 后续添加一个测试环境配置

`pytest` 后面的百分比表示测试进度,没有报错说明测试正常

https://stackoverflow.com/questions/49738081/whats-the-meaning-of-the-percentages-displayed-for-each-test-on-pytest

## 主要参考作者全栈项目生成器测试文件

https://github.com/tiangolo/full-stack-fastapi-postgresql/tree/master/%7B%7Bcookiecutter.project_slug%7D%7D/backend/app/app/tests



================================================
FILE: {{cookiecutter.project_name}}/tests/api/v1/test_casbin.py
================================================
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2021/2/8 10:00
# @Author  : CoderCharm
# @File    : test_casbin.py
# @Software: PyCharm
# @Github  : github/CoderCharm
# @Email   : wg_python@163.com
# @Desc    :
"""
测试casbin权限是否有效
"""

from fastapi.testclient import TestClient


def test_ordinary_add_auth(client: TestClient, ordinary_token_headers: dict):
    """
    测试普通用户设置权限是否被拦截
    :return:
    """
    response = client.post("/add/auth", json={
        "authority_id": "100",
        "path": "/add/auth",
        "method": "POST"
    }, headers=ordinary_token_headers)

    assert response.status_code == 200
    assert response.json()["code"] == 4003


def test_ordinary_del_auth(client: TestClient, ordinary_token_headers: dict):
    """
    测试普通用户删除权限是否被拦截
    :return:
    """
    response = client.post("/del/auth", json={
        "authority_id": "100",
        "path": "/add/auth",
        "method": "POST"
    }, headers=ordinary_token_headers)

    assert response.status_code == 200
    assert response.json()["code"] == 4003


def test_admin_add_auth(client: TestClient, superuser_token_headers: dict):
    """
    测试管理员设置权限是否被拦截
    :return:
    """
    response = client.post("/add/auth", json={
        "authority_id": "100",
        "path": "/add/auth",
        "method": "POST"
    }, headers=superuser_token_headers)
    assert response.status_code == 200
    assert response.json()["code"] == 200


def test_admin_del_auth(client: TestClient, superuser_token_headers: dict):
    """
    测试管理员删除权限是否被拦截
    :return:
    """
    response = client.post("/del/auth", json={
        "authority_id": "100",
        "path": "/add/auth",
        "method": "POST"
    }, headers=superuser_token_headers)

    assert response.status_code == 200
    assert response.json()["code"] == 200


================================================
FILE: {{cookiecutter.project_name}}/tests/api/v1/test_cron.py
================================================
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2021/1/21 11:49
# @Author  : CoderCharm
# @File    : test_cron.py
# @Software: PyCharm
# @Github  : github/CoderCharm
# @Email   : wg_python@163.com
# @Desc    :
"""
测试任务调度

"""

from fastapi.testclient import TestClient


def test_add_job(
        client: TestClient,
        job_id: str,
        superuser_token_headers: dict
) -> None:
    response = client.post("/job/schedule", json={
        "seconds": 5,
        "job_id": job_id
    }, headers=superuser_token_headers)
    assert response.status_code == 200
    assert response.json()["code"] == 200
    assert response.json()["data"]["id"] == job_id


def test_get_all_job(client: TestClient, superuser_token_headers: dict) -> None:
    response = client.get("/jobs/all", headers=superuser_token_headers)
    assert response.status_code == 200
    assert response.json()["code"] == 200
    assert isinstance(response.json()["data"], list)


def test_del_job(client: TestClient, job_id: str, superuser_token_headers: dict) -> None:
    response = client.post("/job/del", json={
        "job_id": job_id
    }, headers=superuser_token_headers)
    assert response.status_code == 200
    assert response.json()["code"] == 200


================================================
FILE: {{cookiecutter.project_name}}/tests/api/v1/test_user.py
================================================
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2020/12/26 20:18
# @Author  : CoderCharm
# @File    : test_user.py
# @Software: PyCharm
# @Github  : github/CoderCharm
# @Email   : wg_python@163.com
# @Desc    :
"""
需要安装 pytest
pip install pytest

"""

from fastapi.testclient import TestClient


def test_login(client: TestClient) -> None:
    """
    测试登录
    自行使用 /app/create_user.py 创建任意测试用户
    test@test.com
    test
    :return:
    """
    response = client.post("/admin/auth/login/access-token", json={
        "username": "test@test.com",
        "password": "test"
    })
    assert response.status_code == 200
    assert response.json()["code"] == 200
    assert response.json()["data"]["token"]


def test_error_login(client: TestClient) -> None:
    """
    测试错误密码是否能登录
    test@test.com
    test
    :return:
    """
    response = client.post("/admin/auth/login/access-token", json={
        "username": "test1@test.com",
        "password": "t"
    })
    assert response.status_code == 200
    assert response.json()["code"] == 4003


def test_get_user(client: TestClient, ordinary_token_headers: dict):
    """
    测试获取用户信息的接口
    :return:
    """
    response = client.get("/admin/auth/user/info", headers=ordinary_token_headers)

    assert response.status_code == 200
    assert response.json()["code"] == 200
    assert isinstance(response.json()["data"]["nickname"], str)


================================================
FILE: {{cookiecutter.project_name}}/tests/conftest.py
================================================
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2021/2/5 13:33
# @Author  : CoderCharm
# @File    : conftest.py
# @Software: PyCharm
# @Github  : github/CoderCharm
# @Email   : wg_python@163.com
# @Desc    :
"""

测试时需要用到的依赖

"""
import os
import sys

# 解决包导入问题
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from typing import Generator

import pytest

from fastapi.testclient import TestClient

from core.server import create_app
from common.session import SessionLocal
from tests.utils.user import user_authentication_headers


@pytest.fixture(scope="session")
def db() -> Generator:
    yield SessionLocal()


@pytest.fixture(scope="module")
def client() -> Generator:
    """
    FastAPI对象
    :return:
    """
    app = create_app()
    with TestClient(app) as c:
        yield c


@pytest.fixture(scope="module")
def job_id() -> str:
    """
    测试定时任务用到的 job_id
    :return:
    """
    return "123"


@pytest.fixture(scope="module")
def superuser_token_headers(client: TestClient) -> dict:
    """
    管理员 admin 用户测试, 后续新增 一份测试环境
    :param client:
    :return:
    """
    return user_authentication_headers(
        client=client,
        email="admin@admin.com",
        password="admin"
    )


@pytest.fixture(scope="module")
def ordinary_token_headers(client: TestClient) -> dict:
    """
    普通用户
    :param client:
    :return:
    """
    return user_authentication_headers(
        client=client,
        email="test@test.com",
        password="test"
    )


================================================
FILE: {{cookiecutter.project_name}}/tests/utils/user.py
================================================
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2021/2/5 14:00
# @Author  : CoderCharm
# @File    : user.py
# @Software: PyCharm
# @Github  : github/CoderCharm
# @Email   : wg_python@163.com
# @Desc    :
"""

"""
from typing import Dict
from fastapi.testclient import TestClient


def user_authentication_headers(
        *, client: TestClient, email: str, password: str
) -> Dict[str, str]:
    data = {"username": email,
            "password": password}

    resp = client.post("/admin/auth/login/access-token", json=data)
    response = resp.json()
    token = response["data"]["token"]
    headers = {"token": token}
    return headers


================================================
FILE: {{cookiecutter.project_name}}/utils/cron_task.py
================================================
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2020/12/24 18:21
# @Author  : CoderCharm
# @File    : cron_task.py
# @Software: PyCharm
# @Github  : github/CoderCharm
# @Email   : wg_python@163.com
# @Desc    :
"""

"""
import time


def demo_task(a1: str):
    print(a1, time.time())


================================================
FILE: {{cookiecutter.project_name}}/utils/tools_func.py
================================================
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2020/10/16 15:48
# @Author  : CoderCharm
# @File    : tools_func.py
# @Software: PyCharm
# @Github  : github/CoderCharm
# @Email   : wg_python@163.com
# @Desc    :
"""

"""

import json
import decimal
import datetime
from typing import Union


def _alchemy_encoder(obj):
    """
    处理序列化中的时间和小数
    :param obj:
    :return:
    """
    if isinstance(obj, datetime.date):
        return obj.strftime("%Y-%m-%d %H:%M:%S")
    elif isinstance(obj, decimal.Decimal):
        return float(obj)


def serialize_sqlalchemy_obj(obj) -> Union[dict, list]:
    """
    序列化fetchall()后的sqlalchemy对象
    https://codeandlife.com/2014/12/07/sqlalchemy-results-to-json-the-easy-way/
    :param obj:
    :return:
    """
    if isinstance(obj, list):
        # 转换fetchall()的结果集
        return json.loads(json.dumps([dict(r) for r in obj], default=_alchemy_encoder))
    else:
        # 转换fetchone()后的对象
        return json.loads(json.dumps(dict(obj), default=_alchemy_encoder))
Download .txt
gitextract__cxflj8k/

├── .gitignore
├── Pipfile
├── README-en.md
├── README.md
├── cookiecutter.json
├── examples/
│   ├── demo_casbin/
│   │   ├── 01_demo.py
│   │   ├── 02_orm.py
│   │   ├── 03_custom_orm.py
│   │   ├── README.md
│   │   ├── custom_model.conf
│   │   ├── model.conf
│   │   └── policy.csv
│   └── demo_scheduler/
│       └── main.py
└── {{cookiecutter.project_name}}/
    ├── api/
    │   └── v1/
    │       ├── items.py
    │       ├── sys_scheduler.py
    │       └── user.py
    ├── common/
    │   ├── __init__.py
    │   ├── custom_exc.py
    │   ├── deps.py
    │   ├── logger.py
    │   ├── session.py
    │   ├── sys_redis.py
    │   └── sys_schedule.py
    ├── core/
    │   ├── celery_app.py
    │   ├── config/
    │   │   ├── __init__.py
    │   │   └── config.py.example
    │   ├── security.py
    │   └── server.py
    ├── db/
    │   └── mysql/
    │       └── migrations/
    │           └── 20211209-init.sql
    ├── dbconfig.yml
    ├── dbconfig.yml.example
    ├── logic/
    │   └── user_logic.py
    ├── main.py
    ├── models/
    │   └── users.py
    ├── requirements-dev.txt
    ├── requirements.txt
    ├── router/
    │   ├── __init__.py
    │   └── v1_router.py
    ├── schemas/
    │   ├── request/
    │   │   ├── sys_api.py
    │   │   ├── sys_authority_schema.py
    │   │   ├── sys_casbin.py
    │   │   └── sys_user_schema.py
    │   └── response/
    │       └── resp.py
    ├── tests/
    │   ├── README.md
    │   ├── api/
    │   │   └── v1/
    │   │       ├── test_casbin.py
    │   │       ├── test_cron.py
    │   │       └── test_user.py
    │   ├── conftest.py
    │   └── utils/
    │       └── user.py
    └── utils/
        ├── cron_task.py
        └── tools_func.py
Download .txt
SYMBOL INDEX (110 symbols across 27 files)

FILE: examples/demo_casbin/03_custom_orm.py
  function params_match (line 31) | def params_match(full_name_k1: str, key2: str):
  function params_match_func (line 42) | def params_match_func(*args):

FILE: examples/demo_scheduler/main.py
  function resp_ok (line 35) | def resp_ok(*, code=0, msg="ok", data: Union[list, dict, str] = None) ->...
  function resp_fail (line 39) | def resp_fail(*, code=1, msg="fail", data: Union[list, dict, str] = None):
  function cron_task (line 43) | def cron_task(a1: str) -> None:
  function get_scheduled_syncs (line 48) | async def get_scheduled_syncs():
  function get_target_sync (line 63) | async def get_target_sync(
  function add_interval_job (line 78) | async def add_interval_job(
  function add_date_job (line 98) | async def add_date_job(
  function add_cron_job (line 116) | async def add_cron_job(
  function remove_schedule (line 134) | async def remove_schedule(

FILE: {{cookiecutter.project_name}}/api/v1/items.py
  function items_test (line 31) | def items_test(
  function get_all_user_info (line 49) | def get_all_user_info(
  function upload_image (line 59) | def upload_image(

FILE: {{cookiecutter.project_name}}/api/v1/sys_scheduler.py
  function get_scheduled_syncs (line 26) | async def get_scheduled_syncs():
  function get_target_sync (line 42) | async def get_target_sync(
  function add_job_to_scheduler (line 56) | async def add_job_to_scheduler(
  function remove_schedule (line 86) | async def remove_schedule(

FILE: {{cookiecutter.project_name}}/api/v1/user.py
  function login_access_token (line 33) | def login_access_token(
  function get_user_info (line 64) | def get_user_info(

FILE: {{cookiecutter.project_name}}/common/custom_exc.py
  class TokenAuthError (line 15) | class TokenAuthError(Exception):
    method __init__ (line 16) | def __init__(self, err_desc: str = "User Authentication Failed"):
  class TokenExpired (line 20) | class TokenExpired(Exception):
    method __init__ (line 21) | def __init__(self, err_desc: str = "Token has expired"):
  class AuthenticationError (line 25) | class AuthenticationError(Exception):
    method __init__ (line 26) | def __init__(self, err_desc: str = "Permission denied"):

FILE: {{cookiecutter.project_name}}/common/deps.py
  function check_jwt_token (line 26) | def check_jwt_token(
  function get_current_user (line 49) | def get_current_user(

FILE: {{cookiecutter.project_name}}/common/session.py
  class PeeweeConnectionState (line 27) | class PeeweeConnectionState(_ConnectionState):
    method __init__ (line 28) | def __init__(self, **kwargs):
    method __setattr__ (line 32) | def __setattr__(self, name, value):
    method __getattr__ (line 35) | def __getattr__(self, name):
  class BaseModel (line 52) | class BaseModel(Model):
    method undelete (line 58) | def undelete(cls):
    class Meta (line 62) | class Meta:
  function paginator (line 66) | def paginator(query: ModelSelect, page: int, page_size: int, order_by: s...

FILE: {{cookiecutter.project_name}}/common/sys_redis.py
  class RedisCli (line 28) | class RedisCli(object):
    method __init__ (line 30) | def __init__(self, *, host: str, port: int, password: str, db: int, so...
    method init_redis_connect (line 39) | def init_redis_connect(self) -> None:
    method __getattr__ (line 61) | def __getattr__(self, name):
    method __getitem__ (line 64) | def __getitem__(self, name):
    method __setitem__ (line 67) | def __setitem__(self, name, value):
    method __delitem__ (line 70) | def __delitem__(self, name):

FILE: {{cookiecutter.project_name}}/common/sys_schedule.py
  class ScheduleCli (line 20) | class ScheduleCli(object):
    method __new__ (line 23) | def __new__(cls, *args, **kw):
    method __init__ (line 28) | def __init__(self):
    method init_scheduler (line 32) | def init_scheduler(self) -> None:
    method __getattr__ (line 44) | def __getattr__(self, name):
    method __getitem__ (line 47) | def __getitem__(self, name):
    method __setitem__ (line 50) | def __setitem__(self, name, value):
    method __delitem__ (line 53) | def __delitem__(self, name):

FILE: {{cookiecutter.project_name}}/core/security.py
  function create_access_token (line 28) | def create_access_token(
  function verify_password (line 49) | def verify_password(plain_password: str, hashed_password: str) -> bool:
  function get_password_hash (line 59) | def get_password_hash(password: str) -> str:

FILE: {{cookiecutter.project_name}}/core/server.py
  function create_app (line 34) | def create_app() -> FastAPI:
  function register_static_file (line 72) | def register_static_file(app: FastAPI) -> None:
  function register_router (line 87) | def register_router(app: FastAPI) -> None:
  function register_cors (line 99) | def register_cors(app: FastAPI) -> None:
  function register_exception (line 115) | def register_exception(app: FastAPI) -> None:
  function register_hook (line 204) | def register_hook(app: FastAPI) -> None:
  function register_init (line 220) | def register_init(app: FastAPI) -> None:

FILE: {{cookiecutter.project_name}}/db/mysql/migrations/20211209-init.sql
  type `users` (line 3) | CREATE TABLE IF NOT EXISTS `users` (

FILE: {{cookiecutter.project_name}}/logic/user_logic.py
  class UserLogic (line 14) | class UserLogic(object):
    method user_login_logic (line 17) | def user_login_logic(phone: int, password: str):
    method xxx_logic (line 29) | def xxx_logic(self):

FILE: {{cookiecutter.project_name}}/models/users.py
  class User (line 19) | class User(BaseModel):
    class Meta (line 31) | class Meta:
    method single_by_id (line 35) | def single_by_id(cls, uid: int):
    method single_by_phone (line 41) | def single_by_phone(cls, phone: int = 0):
    method fetch_all (line 51) | def fetch_all(cls, page: int = 1, page_size: int = 10):

FILE: {{cookiecutter.project_name}}/schemas/request/sys_api.py
  class ApiCreate (line 20) | class ApiCreate(BaseModel):
  class UpdateApi (line 27) | class UpdateApi(BaseModel):
  class DelApi (line 31) | class DelApi(BaseModel):

FILE: {{cookiecutter.project_name}}/schemas/request/sys_authority_schema.py
  class AuthorityCreate (line 19) | class AuthorityCreate(BaseModel):
  class AuthorityUpdate (line 26) | class AuthorityUpdate(BaseModel):

FILE: {{cookiecutter.project_name}}/schemas/request/sys_casbin.py
  class AuthCreate (line 18) | class AuthCreate(BaseModel):

FILE: {{cookiecutter.project_name}}/schemas/request/sys_user_schema.py
  class UserBase (line 20) | class UserBase(BaseModel):
  class UserAuth (line 26) | class UserAuth(BaseModel):
  class UserEmailAuth (line 31) | class UserEmailAuth(UserAuth):
  class UserPhoneAuth (line 36) | class UserPhoneAuth(UserAuth):
  class UserCreate (line 41) | class UserCreate(UserBase):
  class UserUpdate (line 50) | class UserUpdate(UserBase):
  class UserInDBBase (line 54) | class UserInDBBase(UserBase):
    class Config (line 57) | class Config:
  class UserInDB (line 61) | class UserInDB(UserInDBBase):
  class UserInfo (line 66) | class UserInfo(BaseModel):

FILE: {{cookiecutter.project_name}}/schemas/response/resp.py
  class Resp (line 22) | class Resp(object):
    method __init__ (line 23) | def __init__(self, status: int, msg: str, code: int):
    method set_msg (line 28) | def set_msg(self, msg):
  function ok (line 44) | def ok(*, data: Union[list, dict, str] = None, pagination: dict = None, ...
  function fail (line 56) | def fail(resp: Resp) -> Response:

FILE: {{cookiecutter.project_name}}/tests/api/v1/test_casbin.py
  function test_ordinary_add_auth (line 17) | def test_ordinary_add_auth(client: TestClient, ordinary_token_headers: d...
  function test_ordinary_del_auth (line 32) | def test_ordinary_del_auth(client: TestClient, ordinary_token_headers: d...
  function test_admin_add_auth (line 47) | def test_admin_add_auth(client: TestClient, superuser_token_headers: dict):
  function test_admin_del_auth (line 61) | def test_admin_del_auth(client: TestClient, superuser_token_headers: dict):

FILE: {{cookiecutter.project_name}}/tests/api/v1/test_cron.py
  function test_add_job (line 18) | def test_add_job(
  function test_get_all_job (line 32) | def test_get_all_job(client: TestClient, superuser_token_headers: dict) ...
  function test_del_job (line 39) | def test_del_job(client: TestClient, job_id: str, superuser_token_header...

FILE: {{cookiecutter.project_name}}/tests/api/v1/test_user.py
  function test_login (line 19) | def test_login(client: TestClient) -> None:
  function test_error_login (line 36) | def test_error_login(client: TestClient) -> None:
  function test_get_user (line 51) | def test_get_user(client: TestClient, ordinary_token_headers: dict):

FILE: {{cookiecutter.project_name}}/tests/conftest.py
  function db (line 33) | def db() -> Generator:
  function client (line 38) | def client() -> Generator:
  function job_id (line 49) | def job_id() -> str:
  function superuser_token_headers (line 58) | def superuser_token_headers(client: TestClient) -> dict:
  function ordinary_token_headers (line 72) | def ordinary_token_headers(client: TestClient) -> dict:

FILE: {{cookiecutter.project_name}}/tests/utils/user.py
  function user_authentication_headers (line 17) | def user_authentication_headers(

FILE: {{cookiecutter.project_name}}/utils/cron_task.py
  function demo_task (line 16) | def demo_task(a1: str):

FILE: {{cookiecutter.project_name}}/utils/tools_func.py
  function _alchemy_encoder (line 20) | def _alchemy_encoder(obj):
  function serialize_sqlalchemy_obj (line 32) | def serialize_sqlalchemy_obj(obj) -> Union[dict, list]:
Condensed preview — 51 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (81K chars).
[
  {
    "path": ".gitignore",
    "chars": 1442,
    "preview": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n.idea\n\n.DS_Store\nnode_modules/\ndist/\nnpm-debug"
  },
  {
    "path": "Pipfile",
    "chars": 138,
    "preview": "[[source]]\nname = \"pypi\"\nurl = \"https://pypi.org/simple\"\nverify_ssl = true\n\n[dev-packages]\n\n[packages]\n\n[requires]\npytho"
  },
  {
    "path": "README-en.md",
    "chars": 1445,
    "preview": "# FastAPI and MySql - Base Project Generator\n\n![Python版本](https://img.shields.io/badge/Python-3.7+-brightgreen.svg \"版本号\""
  },
  {
    "path": "README.md",
    "chars": 7875,
    "preview": "# FastAPI and MySQL - 项目生成器\n\n![Python版本](https://img.shields.io/badge/Python-3.7+-brightgreen.svg \"版本号\")\n![FastAPI版本](ht"
  },
  {
    "path": "cookiecutter.json",
    "chars": 41,
    "preview": "{\n    \"project_name\": \"fastapi_project\"\n}"
  },
  {
    "path": "examples/demo_casbin/01_demo.py",
    "chars": 768,
    "preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2021/1/27 19:54\n# @Author  : CoderCharm\n# @File    : simple_e"
  },
  {
    "path": "examples/demo_casbin/02_orm.py",
    "chars": 985,
    "preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2021/1/27 20:06\n# @Author  : CoderCharm\n# @File    : casbin_o"
  },
  {
    "path": "examples/demo_casbin/03_custom_orm.py",
    "chars": 1289,
    "preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2021/1/29 20:49\n# @Author  : CoderCharm\n# @File    : casbin_c"
  },
  {
    "path": "examples/demo_casbin/README.md",
    "chars": 429,
    "preview": "# casbin 权限认证管理\n\n> 一个权限认证的库,主要是用于Go语言的,也实现了其他语言的扩展。我也是看到[奇淼](https://github.com/piexlmax)的B站关于这个的介绍,所以才接触到这个\n所以想把这引入到Fas"
  },
  {
    "path": "examples/demo_casbin/custom_model.conf",
    "chars": 232,
    "preview": "[request_definition]\nr = sub, obj, act\n\n[policy_definition]\np = sub, obj, act\n\n[role_definition]\ng = _, _\n\n[policy_effec"
  },
  {
    "path": "examples/demo_casbin/model.conf",
    "chars": 194,
    "preview": "[request_definition]\nr = sub, obj, act\n\n[policy_definition]\np = sub, obj, act\n\n[policy_effect]\ne = some(where (p.eft == "
  },
  {
    "path": "examples/demo_casbin/policy.csv",
    "chars": 42,
    "preview": "p, nick, data1, read\np, nick, data2, write"
  },
  {
    "path": "examples/demo_scheduler/main.py",
    "chars": 4991,
    "preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2020/12/25 20:08\n# @Author  : CoderCharm\n# @File    : main.py"
  },
  {
    "path": "{{cookiecutter.project_name}}/api/v1/items.py",
    "chars": 1867,
    "preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2020/10/16 15:21\n# @Author  : CoderCharm\n# @File    : endpoin"
  },
  {
    "path": "{{cookiecutter.project_name}}/api/v1/sys_scheduler.py",
    "chars": 2879,
    "preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2020/12/24 18:24\n# @Author  : CoderCharm\n# @File    : sys_sch"
  },
  {
    "path": "{{cookiecutter.project_name}}/api/v1/user.py",
    "chars": 1844,
    "preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2020/10/16 13:38\n# @Author  : CoderCharm\n# @File    : user.py"
  },
  {
    "path": "{{cookiecutter.project_name}}/common/__init__.py",
    "chars": 27,
    "preview": "\nfrom .logger import logger"
  },
  {
    "path": "{{cookiecutter.project_name}}/common/custom_exc.py",
    "chars": 637,
    "preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2020/10/15 16:57\n# @Author  : CoderCharm\n# @File    : custom_"
  },
  {
    "path": "{{cookiecutter.project_name}}/common/deps.py",
    "chars": 1440,
    "preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2020/10/16 11:51\n# @Author  : CoderCharm\n# @File    : deps.py"
  },
  {
    "path": "{{cookiecutter.project_name}}/common/logger.py",
    "chars": 1188,
    "preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2020/10/15 16:58\n# @Author  : CoderCharm\n# @File    : logger."
  },
  {
    "path": "{{cookiecutter.project_name}}/common/session.py",
    "chars": 2387,
    "preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2020/10/16 11:42\n# @Author  : CoderCharm\n# @File    : session"
  },
  {
    "path": "{{cookiecutter.project_name}}/common/sys_redis.py",
    "chars": 2126,
    "preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2021/1/28 20:34\n# @Author  : CoderCharm\n# @File    : sys_redi"
  },
  {
    "path": "{{cookiecutter.project_name}}/common/sys_schedule.py",
    "chars": 1438,
    "preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2021/1/29 20:33\n# @Author  : CoderCharm\n# @File    : sys_sche"
  },
  {
    "path": "{{cookiecutter.project_name}}/core/celery_app.py",
    "chars": 394,
    "preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2020/10/17 15:03\n# @Author  : CoderCharm\n# @File    : celery_"
  },
  {
    "path": "{{cookiecutter.project_name}}/core/config/__init__.py",
    "chars": 463,
    "preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2020/9/18 13:50\n# @Author  : CoderCharm\n# @File    : __init__"
  },
  {
    "path": "{{cookiecutter.project_name}}/core/config/config.py.example",
    "chars": 1309,
    "preview": "\n\nimport os\n\nfrom typing import Union, Optional\n\nfrom pydantic import AnyHttpUrl, BaseSettings, IPvAnyAddress\n\n\nclass Se"
  },
  {
    "path": "{{cookiecutter.project_name}}/core/security.py",
    "chars": 1536,
    "preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2020/9/22 13:20\n# @Author  : CoderCharm\n# @File    : security"
  },
  {
    "path": "{{cookiecutter.project_name}}/core/server.py",
    "chars": 6225,
    "preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2020/10/15 16:24\n# @Author  : CoderCharm\n# @File    : server."
  },
  {
    "path": "{{cookiecutter.project_name}}/db/mysql/migrations/20211209-init.sql",
    "chars": 1825,
    "preview": "\n-- +migrate Up\nCREATE TABLE IF NOT EXISTS `users` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `name` varchar(1"
  },
  {
    "path": "{{cookiecutter.project_name}}/dbconfig.yml",
    "chars": 143,
    "preview": "dev-mysql:\n  dialect: mysql\n  datasource: root:123456@tcp(127.0.0.1:3306)/fastapi?parseTime=true\n  dir: db/mysql/migrati"
  },
  {
    "path": "{{cookiecutter.project_name}}/dbconfig.yml.example",
    "chars": 143,
    "preview": "dev-mysql:\n  dialect: mysql\n  datasource: root:123456@tcp(127.0.0.1:3306)/fastapi?parseTime=true\n  dir: db/mysql/migrati"
  },
  {
    "path": "{{cookiecutter.project_name}}/logic/user_logic.py",
    "chars": 827,
    "preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# 业务逻辑多了就写在这里\n\nfrom datetime import timedelta\n\nfrom models.users import Us"
  },
  {
    "path": "{{cookiecutter.project_name}}/main.py",
    "chars": 966,
    "preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2020/10/15 16:24\n# @Author  : CoderCharm\n# @File    : main.py"
  },
  {
    "path": "{{cookiecutter.project_name}}/models/users.py",
    "chars": 1497,
    "preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2020/10/16 13:31\n# @Author  : CoderCharm\n# @File    : auth.py"
  },
  {
    "path": "{{cookiecutter.project_name}}/requirements-dev.txt",
    "chars": 113,
    "preview": "aiofiles\naioredis\nalembic\nloguru\nfastapi\nuvicorn\nSQLAlchemy\npython-jose\npasslib\nPyMySQL\ngunicorn\ndatabases[mysql]"
  },
  {
    "path": "{{cookiecutter.project_name}}/requirements.txt",
    "chars": 1094,
    "preview": "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=="
  },
  {
    "path": "{{cookiecutter.project_name}}/router/__init__.py",
    "chars": 233,
    "preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2021/1/21 09:03\n# @Author  : CoderCharm\n# @File    : __init__"
  },
  {
    "path": "{{cookiecutter.project_name}}/router/v1_router.py",
    "chars": 969,
    "preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2020/10/15 17:45\n# @Author  : CoderCharm\n# @File    : __init_"
  },
  {
    "path": "{{cookiecutter.project_name}}/schemas/request/sys_api.py",
    "chars": 637,
    "preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2021/1/29 21:11\n# @Author  : CoderCharm\n# @File    : sys_api."
  },
  {
    "path": "{{cookiecutter.project_name}}/schemas/request/sys_authority_schema.py",
    "chars": 466,
    "preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2021/1/28 19:40\n# @Author  : CoderCharm\n# @File    : sys_auth"
  },
  {
    "path": "{{cookiecutter.project_name}}/schemas/request/sys_casbin.py",
    "chars": 362,
    "preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2021/1/29 20:30\n# @Author  : CoderCharm\n# @File    : sys_casb"
  },
  {
    "path": "{{cookiecutter.project_name}}/schemas/request/sys_user_schema.py",
    "chars": 1244,
    "preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2020/10/16 13:43\n# @Author  : CoderCharm\n# @File    : sys_use"
  },
  {
    "path": "{{cookiecutter.project_name}}/schemas/response/resp.py",
    "chars": 1884,
    "preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2020/9/22 13:32\n# @Author  : CoderCharm\n# @File    : response"
  },
  {
    "path": "{{cookiecutter.project_name}}/tests/README.md",
    "chars": 329,
    "preview": "## 单元测试\n\n> 后续添加一个测试环境配置\n\n`pytest` 后面的百分比表示测试进度,没有报错说明测试正常\n\nhttps://stackoverflow.com/questions/49738081/whats-the-meanin"
  },
  {
    "path": "{{cookiecutter.project_name}}/tests/api/v1/test_casbin.py",
    "chars": 1813,
    "preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2021/2/8 10:00\n# @Author  : CoderCharm\n# @File    : test_casb"
  },
  {
    "path": "{{cookiecutter.project_name}}/tests/api/v1/test_cron.py",
    "chars": 1241,
    "preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2021/1/21 11:49\n# @Author  : CoderCharm\n# @File    : test_cro"
  },
  {
    "path": "{{cookiecutter.project_name}}/tests/api/v1/test_user.py",
    "chars": 1406,
    "preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2020/12/26 20:18\n# @Author  : CoderCharm\n# @File    : test_us"
  },
  {
    "path": "{{cookiecutter.project_name}}/tests/conftest.py",
    "chars": 1518,
    "preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2021/2/5 13:33\n# @Author  : CoderCharm\n# @File    : conftest."
  },
  {
    "path": "{{cookiecutter.project_name}}/tests/utils/user.py",
    "chars": 652,
    "preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2021/2/5 14:00\n# @Author  : CoderCharm\n# @File    : user.py\n#"
  },
  {
    "path": "{{cookiecutter.project_name}}/utils/cron_task.py",
    "chars": 296,
    "preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2020/12/24 18:21\n# @Author  : CoderCharm\n# @File    : cron_ta"
  },
  {
    "path": "{{cookiecutter.project_name}}/utils/tools_func.py",
    "chars": 1021,
    "preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n# @Time    : 2020/10/16 15:48\n# @Author  : CoderCharm\n# @File    : tools_f"
  }
]

About this extraction

This page contains the full source code of the CoderCharm/fastapi-mysql-generator GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 51 files (64.8 KB), approximately 21.2k tokens, and a symbol index with 110 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!