flask starter: tadpole

tadpole 是一个flask starter 项目。从平时flask项目的开发过程中提出来的一些通用的功能,如通过gunicorn管理flask应用的配置文件和启动脚本,初始化virtualenv环境同时安装必要的依赖库,生成flask secret以及提供restful route, 自动为sqlalchey model注册restful接口, 登录认证,权限管理, restful支持等等技能。

一、系统和环境要求

posix, Python 2.x >= 2.6

二、 安装方法

pip install tadpole

三、git地址

https://github.com/echoyuanliang/tadpole

欢迎pull request, 一起做好这个项目, 如果觉得不错, 欢迎star。

四、 使用方法

tadpole init -n PROJECT_NAME -v PROJECT_VERSION -o PROJECT_OWNER -e PROJECT_EMAIL

其中PROJECT_NAME是初始化的项目名,PROJECT_VERSION是初始化的版本号(默认为0.0.1), PROJECT_OWNER为项目负责人,
PROJECT_EMAIL为项目邮件组(用于接收邮件)。

也可以直接执行tadpole init 会提示填入项目名,其他采用默认, 例如:

tadpole-init

五、项目结构

至此,已经使用tadpole初始化了一个新的flask项目,进入tadpole-demo目录可以看到

demo-struct
  • requirements.txt 新项目依赖第三方库列表(其中包含了flask项目常用依赖库)
  • venv 为新项目生成的virtualenv 环境,其中已经安装了requirements.txt中声明的依赖项
  • main.py 项目入口文件,其中定义了Flask app
  • app 主要代码目录
  • config.py 配置文件(会上传git,主要包含不涉秘配置项)
  • instance 其中由instance/config.py, 本地配置项,已经加入.gitignore, 不会上传git,其中为涉密配置或本地特殊配置,其中配置可以覆盖config.py中配置
  • gun.py gunicorn配置文件
  • data 项目数据目录,已经加入.gitignore
  • logs 项目日志目录,已经加入.gitignore
  • dev 本地调试管理脚本,由于具有项目的所有权限,因此已经加入.gitignore,仅供本地调试
  • tadpole-demo 以项目名gunicorn管理脚本,提供了gunicorn启动/停止/重新加载等技能

六、管理脚本的使用

6.1 dev的使用

dev是提供给开发者在开发环境下使用的工具,其中提供了如下技能

  • create_db: 根据model自动生成表结构
  • url: 展示所有注册url
  • clean: 删除项目目录下所有pyc,pyo文件
  • shell: 以命令行方式侵入应用
  • runserver: 使用flask内置的web服务器在5000端口启动应用

此处仅以 url 为例:

dev-url

可以看到新初始化的项目已经有这么多注册的url了,其中prefix为/api/v0.0.1/rest_db开头的url都是为已经创建的
user,role,resource三张表自动生成的restful api。另外一个/health则用于健康检查。最后的/static/则是flask默认提供的。

6.2 tadpole-demo的使用

  • start 用gunicorn启动flask应用,如:
demo-start
  • status 用于显示gunicorn应用状态,如:
demo-status
  • stop 用于停止gunicorn应用
  • reload 用于重新加载gunicorn配置文件,同时重新启动worker进程(目前只支持linux,不支持mac)

七、提供的基本技能

7.1 sqlalchemy restful model

工作中经常会有人要接口查询数据,但是很多数据只需要执行sql语句就能拿到数据,但是又不能直接把DB权限给别人,
因此提供了一个把简单sql语句自动对应到restful查询的技能。这个技能实际上市面上已经有很多库提供了,但是
并没有遇到让我自己用的很舒服的库,因此自己写了一个,这个技能之只需要用户写Model类,并直接或间接的
import到app/models/init.py中即可为其自动注册restful接口。

为了初始化出来的项目可以开箱即用, 会给默认的db(sqlite数据库,文件位于app.db)中创建user,role,resource等
表结构,同时会插入部分数据,因此访问已经注册的rest_db url是可以直接拿到数据的, 例如:

curl http://127.0.0.1:8080/api/v0.0.1/rest_db/user 

{
    "code": 200,
    "msg": "ok",
    "result": {
        "next_page": "http://127.0.0.1:8080/api/v0.0.1/rest_db/user?__page=2&__page_size=200",
        "page": 1,
        "page_size": 200,
        "prev_page": null,
        "result": [
            {
                "__roles_link": "http://127.0.0.1:8080/api/v0.0.1/rest_db/user/1/roles",
                "account": "tadpole",
                "create_time": "2017-11-26 17:53:13",
                "email": "tadpole@tadpole.com",
                "id": 1,
                "name": "tadpole"
            }
        ]
    }
}

可以看到user表已经有一条记录了,同时__roles_link链接到了每个用户所拥有的角色,直接访问可以看到

curl http://127.0.0.1:8080/api/v0.0.1/rest_db/user/1/roles 


{
      "msg": "ok",
      "code": 200,
      "result": {
        "next_page": "http://127.0.0.1:8080/api/v0.0.1/rest_db/user/1/roles?__page=2&__page_size=200",
        "prev_page": null,
        "result": [
          {
            "description": "super admin",
            "__resources_link": "http://127.0.0.1:8080/api/v0.0.1/rest_db/role/1/resources",
            "__users_link": "http://127.0.0.1:8080/api/v0.0.1/rest_db/role/1/users",
            "create_time": "2017-11-26 17:42:52",
            "id": 1,
            "name": "root"
          }
        ],
        "page_size": 200,
        "page": 1
     }
}

可以看到tadpole这个用户已经拥有了一个root角色, 每一条记录除了返回自己的的列之外还以__{relation}_link的形式返回了其关联关系的链接。

7.1.1 支持的查询条件

 OPERATORS = ('lt', 'le', 'gt', 'ge', 'eq', 'like', 'in', 'between')
PROCESSES = ('__show', '__order')
PAGINATE = ('__page', '__page_size')

查询条件分为3类,一类是基本的运算符在OPERATORS中,另一类是对查询的数据进行一些处理,如排序、只展示部分列等,另一类则是分页。
联合使用这些查询条件:

curl http://127.0.0.1:5000/api/v0.0.1/rest_db/user?name=tadpole&account.like=tad%&__show=account,email&__order=id.asc,name.desc 
{
  "msg": "ok",
  "code": 200,
  "result": {
    "next_page": "http://127.0.0.1:5000/api/v0.0.1/rest_db/user?name=tadpole&__page_size=200&__page=2&account.like=tad%25&__order=id.asc%2Cname.desc&__show=account%2Cemail",
    "prev_page": null,
    "result": Array[1][
      {
        "account": "tadpole",
        "email": "tadpole@tadpole.com"
      }
    ],
    "page_size": 200,
    "page": 1
  }
}

可以看到仅仅返回了__show中列出的列,而且按照name=tadpole,account.like=tad%过滤的结果,并且根据__order中的排序条件进行了排序。

7.1.2 url规则

可以看出生成的url都是有一定规则的, prefix为/api/v0.0.1,其中v0.0.1是项目的版本号,但是这个是可以定制的。通过配置文件中的BP_PREFIX就可以配置每一个bluprint对应的prefix,例如rest_db这个blueprint(即rest model使用的)的配置可以如下:

BP_PREFIX = {
'rest_db': '/api/{0}/rest_db/'.format(VERSION)
}

除了prefix之外,后面紧跟着的则是表名,如果是关联查询则是{prefix}/{table_name}/{pk_id}/{relation_name}

7.1.3 自动隐藏

并不是所有的列都适合展示,有些列(比如密码,并不适合对外开放),初始化出来的项目对user表的password列就做了隐藏,如下:

class User(Model):

# columns in __hide__ does'nt show in rest_db
__hide__ = ('password',)

account = Column(
    db.String(128),
    nullable=False,
    default=u'-',
    index=True,
    unique=True)
name = Column(db.String(32), nullable=False, default=u'-')
email = Column(db.Email(128), nullable=False, default=u'-')
password = Column(db.Password(schemes=['pbkdf2_sha512', 'md5_crypt'],
                              deprecated=['md5_crypt']), nullable=False, default=u'-')

声明Model时, __hide__元组中的列不会在自动生成的restful接口中展示

7.2 登录控制和权限管理

对于每一个应用来说,都有不适合对所有人开放的资源,因此需要登录控制和权限管理。tadpole默认在app/models/auth包中实现了用户和权限依赖的Model,
在app/lib/auth.py中实现了有关登录和权限验证的逻辑。登录验证目前采用的是Http Basic认证, 因为密码是单向加密存储的,所以有些验证方法(如Http Digest)不能直接使用,有需求可以对代码进行扩展。扩展也十分容易,有兴趣的朋友可以阅读实现源码进行扩展。权限校验则是简单的查询数据库看用户有没有对某一资源执行某一操作的权限(此处也可以很容易扩展自己的校验方式),权限校验默认对restful接口的http method进行了支持,因此只需要在数据库中添加合适的记录既可以做到接口的权限控制。为了应用可以开箱即用,已经对/api/v0.0.1/rest_db/开头的url做了权限限制,其POST,DELETE,PUT方法仅有root权限用户可以操作,如:

role_resource
user_role

对于新初始化的项目,已经添加了account=tadpole-demo,password=12qwaszx的用户,并且赋予了root角色,可以执行POST /api/v0.0.1/rest_db/*测试权限校验是否正确。

只需要把resource 和 role关联起来即可以仅开放给对应角色的用户。数据库中没有记录的resource以及没有关联role的resource是对所有人开放的。一个资源开放给的用户是资源名称可以正则匹配的到所有resource.name,且对资源的操作在resource.operation(用','分割)中的资源列表所开放给的角色所拥有的用户。 例如对于http restful接口的权限校验, 会拿出所有匹配path 和 method的resource,然后查询这些resource开放的role列表,要求用户只有满足所有这些role,才可以访问对应接口。

7.3 关于rest_route

对于restful接口来说, 一是参数的校验几乎都需要,二是希望可以返回python对象,由框架自动处理成json格式。rest_route对这些做了支持。
如:

from main import app


validator = {
    'required': ['user_name']
}


@app.rest_route('/welcome', methods=['GET'], validator=validator)
def welcome(data):
    return 'hell0', data['user_name']

首先validator中可以对参数进行校验,默认实现了几种常用校验,也可以自己扩充,除此之外还实现了custom校验,即传入用户自己的校验函数,这段代码提供了对user_name参数必填的校验。除此之外,为了提供统一的提交数据入口,所有提交数据都被merge到data参数中了,rest_route接口的POST方法必须提交json格式数据。最后返回一个元组,在rest_route中会自动将其转化为json list,请求这个接口返回如下:

curl http://127.0.0.1:5000/welcome


{
  "msg": "param user_name is required",
  "code": 400
}


curl http://127.0.0.1:5000/welcome?user_name=tadpole

{
  "msg": "ok",
  "code": 200,
  "result": Array[2][
    "hell0",
    "tadpole"
  ]
}

返回结果不仅支持直接返回元组,还支持sqlalchemy查询结果直接返回,set返回等等。对于用户自定义的对象如果要支持直接返回,只需要实现to_dict/_as_dict方法将对象转化成dict即可。

7.3.1 默认支持的参数校验方式

参数校验是通过app/lib/validator实现的,有兴趣的朋友可以直接看源码,实现很简单,也可以自己扩展。目前实现的校验方式有以下:

  • required: 必填参数列表, 用户必须填充但可以为空字符串. 数据类型为list
  • nonempty: 必填且不能为空参数列表。数据类型为list
  • types: 参数类型校验, 数据类型为dict,例如:
    validator = {
        'types': {
            'age': int,
            'active': bool,
        }
    }
  • oneof: 如果参数名称位于oneof中,其值必须要属于oneof[param_name]中的一个,数据类型为dict
  • unique: 对list类型参数做去重,数据类型为list
  • length: 对参数长度组校验, 数据类型为dict
  • default: 对参数提供默认值, 数据类型为list
  • override: 用对参数处理后的结果取代参数的值,数据
  • custom: 用户自定义校验方法,数据类型为list

例如:

validator = {
    'required': ['task_id'],  # 必填参数

    'nonempty': ['project', 'env', 'ip_list', 'component'], # 不能为空

    'types': {
        'ip_list': list,
        'mem': int
    },
    
    'unique': ['ip_list'], # 对ip_list参数去重

    'default': {
        'region': 'Shanghai' # 如果用户没有填写region参数,则用Shanghai填充
    },

    'oneof': {
        'region': ['Shanghai', 'Beijing'], # region参数必属于Shanghai和Beijing之一
    }
}

7.3.2 关于异常处理

已经实现了异常的自动捕捉,并返回合适的信息,默认提供的异常在app/lib/exceptions.py中,
所有继承自CustomError的异常都会被捕捉,并且返回msg作为错误信息,code作为返回码,因此可以直接抛出这些异常给用户,
不需要再进行处理。也可以扩展自定义的异常。默认异常定义示例:

class CustomError(Exception):

def __init__(self, msg):
    super(CustomError, self).__init__(msg)
    self.msg = msg
    self.code = 500

def to_dict(self):
    return dict(code=self.code, msg=self.msg)

def __unicode__(self):
    return unicode(self.msg)

def __str__(self):
    return str(self.msg)


class InternalError(CustomError):

    def __init__(self, msg):
        super(InternalError, self).__init__(msg)
        self.code = 500
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容

  • 22年12月更新:个人网站关停,如果仍旧对旧教程有兴趣参考 Github 的markdown内容[https://...
    tangyefei阅读 35,159评论 22 257
  • 本文首发于Gevin的博客 原文链接:Flask 入门指南 未经 Gevin 授权,禁止转载 1. 初识Flask...
    Gevin阅读 16,717评论 10 237
  • # Python 资源大全中文版 我想很多程序员应该记得 GitHub 上有一个 Awesome - XXX 系列...
    aimaile阅读 26,436评论 6 428
  • 子不曰日月,日月自明;人不论远近,亲疏自然;时不若静,人不若初;唯沧海一粟,独断一行;或践行渐远,或长词以贺;悲喜...
    左手包袱右手锤阅读 237评论 0 1
  • 坚持LSD已有两年,一直想要写点什么,但拖延症一直发酵,迟迟没有写,现在终于下定决心写写,业余的跑者简单记录自己跑...
    Lanffy食色空间阅读 437评论 5 2