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初始化了一個新的flask項目,進入tadpole-demo目錄可以看到
- 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 爲例:
可以看到新初始化的項目已經有這麼多註冊的url了,其中prefix爲/api/v0.0.1/rest_db開頭的url都是爲已經創建的
user,role,resource三張表自動生成的restful api。另外一個/health則用於健康檢查。最後的/static/則是flask默認提供的。
6.2 tadpole-demo的使用
- start 用gunicorn啓動flask應用,如:
- status 用於顯示gunicorn應用狀態,如:
- 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": "[email protected]",
"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": "[email protected]"
}
],
"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權限用戶可以操作,如:
對於新初始化的項目,已經添加了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