├── .gitattributes ├── LICENSE ├── README.md ├── __init__.py ├── app.py ├── cap.py ├── data.py ├── doo.postman_collection.json ├── example.xlsx ├── example.yml └── mock.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 tonglei 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Doo](https://sweeter.io/docs/_media/doo.png) 2 | 3 | # Doo 4 | 5 | Doo 是一款简单易用的接口管理解决方案,支持接口文档管理、Mock服务,接口测试等功能。接口文档采用 yaml 或 Excel 格式书写,简单快捷,Mock 基于该文档,无需数据库,一条命令秒变 Mock 服务。 6 | 7 | ## 安装 8 | 9 | ### 初次安装 10 | 11 | ```shell 12 | pip install doo 13 | ``` 14 | 15 | ### 升级 16 | 17 | ```shell 18 | pip install -U doo 19 | ``` 20 | 21 | ### Apistar 版本说明 22 | 23 | Doo 在底层选择了 Apistar 作为 Web 框架,但 Apistar 从 0.6.0 开始转型为 api 工具,不再兼容原有功能; 24 | 所以,如果 Apistar 已经为 0.6.0,请用如下命令降级: 25 | 26 | ```shell 27 | pip install -U "apistar<0.6.0" 28 | ``` 29 | 30 | ## 快速体验 31 | 32 | ### Mock 33 | 34 | 在合适的目录,如 D:\\doo 目录下,打开 CMD 命令行窗口,输入如下命令 35 | 36 | ```shell 37 | doo 38 | cd doo_example 39 | python app.py 40 | ``` 41 | 42 | 如果看到如下信息 43 | 44 | ```shell 45 | * Restarting with stat 46 | * Debugger is active! 47 | * Debugger PIN: 248-052-080 48 | * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) 49 | ``` 50 | 51 | OK,示例 Mock 已经启动起来了。 52 | 53 | > 详细文档:https://sweeter.io/#/doo/ 54 | 55 | ## 加入我们 56 | 57 | QQ 交流群:**941761748** 58 | > (验证码:python) 注意首字母小写 59 | 60 | 微信公众号:**喜文测试** 61 | 62 | ![QQ2](https://sweeter.io/docs/_media/QQ.png)![WeChat](https://sweeter.io/docs/_media/WeChat.png) 63 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from shutil import copyfile 4 | 5 | 6 | def mkdir(path): 7 | path = path.strip() 8 | path = path.rstrip("\\") 9 | 10 | isExists = os.path.exists(path) 11 | 12 | if not isExists: 13 | #print(path + ' 创建成功') 14 | os.makedirs(path) 15 | return True 16 | else: 17 | print(path + ' 目录已存在') 18 | return False 19 | 20 | 21 | def doo(): 22 | sweetest_dir = os.path.dirname(os.path.realpath(__file__)) 23 | current_dir = os.getcwd() 24 | doo_folder = os.path.join(current_dir, 'doo_example') 25 | if not mkdir(doo_folder): 26 | return 27 | copyfile(os.path.join(sweetest_dir, 'example.xlsx'), 28 | os.path.join(doo_folder, 'example.xlsx')) 29 | copyfile(os.path.join(sweetest_dir, 'example.yml'), 30 | os.path.join(doo_folder, 'example.yml')) 31 | copyfile(os.path.join(sweetest_dir, 'app.py'), 32 | os.path.join(doo_folder, 'app.py')) 33 | copyfile(os.path.join(sweetest_dir, 'doo.postman_collection.json'), 34 | os.path.join(doo_folder, 'doo.postman_collection.json')) 35 | 36 | print('\n生成 Doo example 成功\n\n本框架是 Sweetest 姊妹篇,使用同一公众号和QQ群,详细使用说明请关注\n公众号:Sweetest自动化测试\nQQ交流群:158755338 (验证码:python)') 37 | print('\n\n请运行如下命令启动示例 Mock 服务\n\ncd doo_example\npython app.py example.yml') 38 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from pathlib import Path 3 | from doo.mock import app, extra_files 4 | 5 | 6 | if __name__ == '__main__': 7 | app.serve('127.0.0.1', 5000, debug=True, extra_files=extra_files) 8 | -------------------------------------------------------------------------------- /cap.py: -------------------------------------------------------------------------------- 1 | from apistar import App 2 | from apistar.http import Response 3 | from apistar.server.wsgi import RESPONSE_STATUS_TEXT, WSGIStartResponse 4 | 5 | 6 | def cap(o, s): 7 | c = [x.capitalize() for x in o.split(s)] 8 | return s.join(c) 9 | 10 | 11 | class CapApp(App): 12 | ''' 13 | 由于 apistar 把 headers 的字段都改为了小写,故写此类 14 | 继承 App,重构 Response.headers,把字段改为首字母大写 15 | ''' 16 | def __init__(self, 17 | routes, 18 | template_dir=None, 19 | static_dir=None, 20 | packages=None, 21 | schema_url='/schema/', 22 | docs_url='/docs/', 23 | static_url='/static/', 24 | components=None, 25 | event_hooks=None): 26 | 27 | super().__init__(routes, 28 | template_dir=None, 29 | static_dir=None, 30 | packages=None, 31 | schema_url='/schema/', 32 | docs_url='/docs/', 33 | static_url='/static/', 34 | components=None, 35 | event_hooks=None) 36 | 37 | def finalize_wsgi(self, response: Response, start_response: WSGIStartResponse): 38 | if self.debug and response.exc_info is not None: 39 | exc_info = response.exc_info 40 | raise exc_info[0].with_traceback(exc_info[1], exc_info[2]) 41 | 42 | response.headers = [(cap(x[0], '-'), x[1]) for x in response.headers] 43 | start_response( 44 | RESPONSE_STATUS_TEXT[response.status_code], 45 | list(response.headers), 46 | response.exc_info 47 | ) 48 | return [response.content] 49 | -------------------------------------------------------------------------------- /data.py: -------------------------------------------------------------------------------- 1 | import xlrd 2 | import xlsxwriter 3 | import json 4 | import copy 5 | import yaml 6 | import sys 7 | from pathlib import Path 8 | 9 | class Excel: 10 | 11 | def __init__(self, file_name, mode='r'): 12 | if mode == 'r': 13 | self.workbook = xlrd.open_workbook(file_name) 14 | elif mode == 'w': 15 | self.workbook = xlsxwriter.Workbook(file_name) 16 | else: 17 | raise Exception( 18 | 'Error: init Excel class with error mode: %s' % mode) 19 | 20 | def get_sheets_name(self): 21 | names = [] 22 | for name in self.workbook.sheet_names(): 23 | names.append(name) 24 | return names 25 | 26 | def read_sheet(self, sheet_name): 27 | ''' 28 | sheet_name: Excel 中标签页名称 29 | return:[[],[]……] 30 | ''' 31 | sheet = self.workbook.sheet_by_name(sheet_name) 32 | nrows = sheet.nrows 33 | data = [] 34 | for i in range(nrows): 35 | data.append(sheet.row_values(i)) 36 | return data 37 | 38 | def read_sheets(self): 39 | sheets = self.get_sheets_name() 40 | if 'INDEX' not in sheets: 41 | raise Exception('No INDEX sheet') 42 | 43 | data = {} 44 | for sheet in sheets: 45 | d = self.read_sheet(sheet) 46 | data[sheet] = d 47 | return data 48 | 49 | def get_data(self): 50 | data = self.read_sheets() 51 | self.index = index2json(data.pop('INDEX')) 52 | self.doc = doc2json(data, copy.deepcopy(self.index)) 53 | return self.doc 54 | 55 | def close(self): 56 | self.workbook.close() 57 | 58 | 59 | def index2json(data): 60 | index = {'Title': '', 'Description': '', 'Version': '', 61 | 'BasePath': '', 'Request_Headers': {}, 'Response_Headers': {}} 62 | flag = '' 63 | for d in data: 64 | if d[0] in index.keys(): 65 | index[d[0]] = d[1] 66 | elif d[0].replace(' ','').lower() == 'requestheaders': 67 | flag = 'REQUEST' 68 | continue 69 | elif d[0].replace(' ','').lower() == 'responseheaders': 70 | flag = 'RESPONSE' 71 | continue 72 | 73 | if flag == 'REQUEST' and d[1]: 74 | index['Request_Headers'][d[1]] = d[2] 75 | 76 | if flag == 'RESPONSE': 77 | if d[1]: 78 | index['Response_Headers'][d[1]] = d[2] 79 | else: 80 | break 81 | 82 | return index 83 | 84 | 85 | def message(protocol, d, one, headers=False, num=0): 86 | if d[0].upper() == 'HEADERS': 87 | headers = True 88 | elif d[0].upper() == 'BODY': 89 | headers = False 90 | 91 | if d[1]: 92 | data = [v for v in d[6:6 + num]] 93 | field = [d[3], d[4], d[2], d[5]] 94 | if headers: 95 | one[protocol]['Headers'][d[1]] = data[0] 96 | else: 97 | one[protocol]['Body'][d[1]] = field 98 | for i in range(num): 99 | if not one.get('DATA'+str(i+1)): 100 | one['DATA'+str(i+1)] = {'REQUEST':{}, 'RESPONSE': {}} 101 | t = data[i] 102 | 103 | if d[3] == 'string': 104 | t = str(data[i]) 105 | elif d[3] == 'int': 106 | t = int(data[i]) 107 | elif d[3] == 'float': 108 | t = float(data[i]) 109 | elif d[3] == 'json': 110 | try: 111 | t = json.loads(data[i]) 112 | except: 113 | raise Exception(f'Excel\'s json error,key {d[1]}:\n{data[i]}') 114 | 115 | if d[1] == 'Body': 116 | one['DATA'+str(i+1)][protocol] = dict(one['DATA'+str(i+1)][protocol], **t) 117 | else: 118 | one['DATA'+str(i+1)][protocol][d[1]] = t 119 | 120 | return one 121 | 122 | 123 | def doc2json(data, index): 124 | doc = {} 125 | cn = {'名称': 'Name', '描述': 'Desc', '接口': 'Path', '方法': 'Method', '权限': 'Auth'} 126 | # key:sheet_name, value:sheet_data 127 | for key, value in data.items(): 128 | flag = 'NEW' 129 | headers = False 130 | num = 0 131 | # d: sheet_row,value: sheet_rows 132 | for d in value: 133 | 134 | if flag == 'NEW': 135 | one = {'Name': '', 'Desc': '', 'Path': '', 'Method': '', 'Auth': '', 'GROUP': key, 136 | 'REQUEST': {'Headers': copy.deepcopy(index['Request_Headers']), 'Body': {}}, 137 | 'RESPONSE': {'Headers': copy.deepcopy(index['Response_Headers']), 'Body': {}}} 138 | flag = 'N' 139 | headers = False 140 | 141 | if d[0] in cn.keys(): 142 | k = cn[d[0]] 143 | one[k] = d[1] 144 | elif d[0].upper() in ('请求', 'REQUEST'): 145 | flag = 'REQUEST' 146 | num = len([v for v in d[6:] if v]) 147 | if one['Method'] == 'GET': 148 | one['REQUEST']['Headers'].pop('Content-Type') 149 | for i in range(num): 150 | if not one.get('DATA'+str(i+1)): 151 | one['DATA'+str(i+1)] = {'REQUEST':{}, 'RESPONSE': {}} 152 | continue 153 | elif d[0].upper() in ('响应', 'RESPONSE'): 154 | flag = 'RESPONSE' 155 | for i in range(num): 156 | one['DATA'+str(i+1)]['status_code'] = int(d[6+i]) if d[6+i] else 200 157 | continue 158 | elif d[0].upper() in ('响应延时', 'DELAY'): 159 | for i in range(num): 160 | one['DATA'+str(i+1)]['delay'] = float(d[6+i]) if d[6+i] else 0 161 | 162 | elif d[0].upper() in ('测试数据备注', 'REMARK'): 163 | flag = 'NEW' 164 | for i in range(num): 165 | one['DATA'+str(i+1)]['remark'] = d[6+i] if d[6+i] else '' 166 | 167 | if flag in ('REQUEST', 'RESPONSE'): 168 | one = message(flag, d, one, headers, num) 169 | 170 | if flag == 'NEW': 171 | one['Name'] = one['Name'].upper() 172 | doc[one['Name']] = one 173 | 174 | 175 | return doc 176 | 177 | 178 | class Yaml(): 179 | def __init__(self, path, mode='r'): 180 | self.files = [] 181 | if path.is_dir(): 182 | self.files = list(path.glob('*.yml')) 183 | elif path.exists(): 184 | if path.suffix == '.yml': 185 | self.files.append(path) 186 | 187 | def get_data(self): 188 | index = {'REQUEST_Headers': {}, 'RESPONSE_Headers': {}} 189 | doc = {} 190 | for yaml_file in self.files: 191 | f = open(yaml_file, encoding='utf-8') 192 | cont =f.read() 193 | y = yaml.load_all(cont) 194 | for api in y: 195 | if api.get('Name'): 196 | doc[api['Name'].upper()] = api 197 | elif api.get('Title'): 198 | index = api 199 | 200 | for name,api in doc.items(): 201 | if not api['REQUEST'].get('Headers'): 202 | api['REQUEST']['Headers'] = {} 203 | api['REQUEST']['Headers'] = dict(index['REQUEST_Headers'], **api['REQUEST']['Headers']) 204 | if api['Method'] == 'GET': 205 | api['REQUEST']['Headers'].pop('Content-Type') 206 | 207 | return doc 208 | 209 | 210 | def star_sort(doc): 211 | for api in doc: 212 | star = {} 213 | for k in api: 214 | if 'DATA' in k: 215 | for field,value in api[k]['REQUEST'].items(): 216 | if isinstance(value, str) and value == '*': 217 | if not api[k].get('star'): 218 | api[k]['star'] = 1 219 | else: 220 | api[k]['star'] += 1 221 | if api[k].get('star'): 222 | star[k] = api[k]['star'] 223 | star = sorted(star.items(), key=lambda d:d[1],reverse=False) 224 | for k in dict(star): 225 | v = api[k] 226 | api.pop(k) 227 | api[k] = v 228 | 229 | return doc 230 | 231 | 232 | def get_doc(): 233 | 234 | extra_files = [] 235 | doc = {} 236 | if len(sys.argv) >1: 237 | api_file = sys.argv[1] 238 | path = Path(api_file) 239 | 240 | if path.exists(): 241 | if path.suffix == '.xlsx': 242 | e = Excel(api_file) 243 | doc = e.get_data() 244 | extra_files.append(path) 245 | else : 246 | y = Yaml(path) 247 | doc = y.get_data() 248 | extra_files = y.files 249 | else: 250 | print(f'--- The api file/folder:{api_file} is not exists ---') 251 | sys.exit(-1) 252 | else: 253 | if Path('example.yml').exists(): 254 | y = Yaml(Path('example.yml')) 255 | doc = y.get_data() 256 | extra_files.append(Path('example.yml')) 257 | elif Path('example.xlsx').exists(): 258 | e = Excel(Path('example.xlsx')) 259 | doc = e.get_data() 260 | extra_files.append(Path('example.xlsx')) 261 | else: 262 | print('--- Please input .xlsx or .yml file ---') 263 | sys.exit(-1) 264 | 265 | return star_sort(doc), extra_files 266 | 267 | 268 | if __name__ == '__main__': 269 | 270 | doc = get_doc() 271 | 272 | print('\n--- DOC ---') 273 | print(json.dumps(doc, ensure_ascii=False, indent=4)) 274 | -------------------------------------------------------------------------------- /doo.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "4d045ba4-94f6-4af2-bd0b-6fd1f7a29f75", 4 | "name": "Doo", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" 6 | }, 7 | "item": [ 8 | { 9 | "name": "Login: admin 账号登录", 10 | "request": { 11 | "method": "POST", 12 | "header": [ 13 | { 14 | "key": "Content-Type", 15 | "value": "application/json" 16 | } 17 | ], 18 | "body": { 19 | "mode": "raw", 20 | "raw": "{\r\n\t\"account\": \"admin\",\r\n\t\"password\": \"123456\"\r\n}" 21 | }, 22 | "url": { 23 | "raw": "http://127.0.0.1:5000/api/authentication/login", 24 | "protocol": "http", 25 | "host": [ 26 | "127", 27 | "0", 28 | "0", 29 | "1" 30 | ], 31 | "port": "5000", 32 | "path": [ 33 | "api", 34 | "authentication", 35 | "login" 36 | ] 37 | } 38 | }, 39 | "response": [] 40 | }, 41 | { 42 | "name": "Login: 错误账号登录", 43 | "request": { 44 | "method": "POST", 45 | "header": [ 46 | { 47 | "key": "Content-Type", 48 | "value": "application/json" 49 | } 50 | ], 51 | "body": { 52 | "mode": "raw", 53 | "raw": "{\r\n\t\"account\": \"xiaoming\",\r\n\t\"password\": \"Oxhhddd\"\r\n}" 54 | }, 55 | "url": { 56 | "raw": "http://127.0.0.1:5000/api/authentication/login", 57 | "protocol": "http", 58 | "host": [ 59 | "127", 60 | "0", 61 | "0", 62 | "1" 63 | ], 64 | "port": "5000", 65 | "path": [ 66 | "api", 67 | "authentication", 68 | "login" 69 | ] 70 | } 71 | }, 72 | "response": [] 73 | }, 74 | { 75 | "name": "Login: guess 任意密码登录", 76 | "request": { 77 | "method": "POST", 78 | "header": [ 79 | { 80 | "key": "Content-Type", 81 | "value": "application/json" 82 | } 83 | ], 84 | "body": { 85 | "mode": "raw", 86 | "raw": "{\r\n\t\"account\": \"guess\",\r\n\t\"password\": \"1234567890\"\r\n}" 87 | }, 88 | "url": { 89 | "raw": "http://127.0.0.1:5000/api/authentication/login", 90 | "protocol": "http", 91 | "host": [ 92 | "127", 93 | "0", 94 | "0", 95 | "1" 96 | ], 97 | "port": "5000", 98 | "path": [ 99 | "api", 100 | "authentication", 101 | "login" 102 | ] 103 | } 104 | }, 105 | "response": [] 106 | }, 107 | { 108 | "name": "User: 获取用户信息", 109 | "request": { 110 | "method": "POST", 111 | "header": [ 112 | { 113 | "key": "Content-Type", 114 | "value": "application/json" 115 | } 116 | ], 117 | "body": { 118 | "mode": "raw", 119 | "raw": "{\n\t\"type\": \"base\"\n}" 120 | }, 121 | "url": { 122 | "raw": "http://127.0.0.1:5000/api/user/100001", 123 | "protocol": "http", 124 | "host": [ 125 | "127", 126 | "0", 127 | "0", 128 | "1" 129 | ], 130 | "port": "5000", 131 | "path": [ 132 | "api", 133 | "user", 134 | "100001" 135 | ] 136 | } 137 | }, 138 | "response": [] 139 | }, 140 | { 141 | "name": "News: 查询新闻#10002", 142 | "request": { 143 | "method": "GET", 144 | "header": [], 145 | "body": { 146 | "mode": "raw", 147 | "raw": "" 148 | }, 149 | "url": { 150 | "raw": "http://127.0.0.1:5000/api/news?id=10002", 151 | "protocol": "http", 152 | "host": [ 153 | "127", 154 | "0", 155 | "0", 156 | "1" 157 | ], 158 | "port": "5000", 159 | "path": [ 160 | "api", 161 | "news" 162 | ], 163 | "query": [ 164 | { 165 | "key": "id", 166 | "value": "10002" 167 | } 168 | ] 169 | } 170 | }, 171 | "response": [] 172 | } 173 | ] 174 | } -------------------------------------------------------------------------------- /example.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonglei100/doo/0ed8caf51ae23a6872bb01b1fb73971ca1634f97/example.xlsx -------------------------------------------------------------------------------- /example.yml: -------------------------------------------------------------------------------- 1 | --- # 通用描述 2 | Title: Example 3 | Description: Example 接口文档 4 | Version: '1.0' 5 | BasePath: http://example.com 6 | REQUEST_Headers: 7 | Content-Type: application/json 8 | RESPONSE_Headers: 9 | Content-Type: application/json 10 | 11 | 12 | --- # 最简版接口描述 13 | Name: LOGIN 14 | Desc: 账号登录 15 | Path: /api/authentication/login 16 | Method: POST 17 | 18 | REQUEST: 19 | Body: 20 | # 参数名: [类型, 是否必传, 中文名称, 备注] 21 | account: [string, Y, 用户名, 手机号/邮箱] 22 | password: [string,Y, 密码, 6~12位数字字母组合] 23 | 24 | RESPONSE: 25 | Body: 26 | # json 支持多层嵌套 27 | Body: {code: [string, Y, 错误码, 报文里的错误码], message: [string, Y, 提示信息, 出错时信息]} 28 | nickname: [string, N, 昵称, 用户昵称] 29 | 30 | 31 | --- # 完整版接口描述 32 | Name: LOGIN 33 | Desc: 账号登录 34 | Path: /api/authentication/login 35 | Method: POST 36 | GROUP: USER 37 | Auth: None 38 | 39 | REQUEST: 40 | Headers: 41 | Content-Type: application/json 42 | Body: 43 | # 参数名: [类型, 是否必传, 中文名称, 备注] 44 | account: [string, Y, 用户名, 手机号/邮箱] 45 | password: [string,Y, 密码, 6~12位数字字母组合] 46 | 47 | RESPONSE: 48 | Headers: 49 | Content-Type: application/json 50 | Body: 51 | # json 支持多层嵌套 52 | Body: {code: [string, Y, 错误码, 报文里的错误码], message: [string, Y, 提示信息, 出错时信息]} 53 | nickname: [string, N, 昵称, 用户昵称] 54 | 55 | # 可选,需要调用的外部接口,可以依此生成系统调用拓扑图。 56 | Link: 57 | AD: [Account, DB] 58 | BS: [User, Info] 59 | WeChat: Pay 60 | 61 | # 以下为 Mock 测试数据,根据需要填写 62 | DATA1: 63 | REQUEST: 64 | account: admin 65 | password: '123456' 66 | RESPONSE: 67 | code: '0' 68 | message: success 69 | nickname: admin 70 | status_code: 200 #可选,默认为 200 71 | delay: 0.5 #可选,默认为 0 72 | remark: admin 账户登录 #可选,默认为 '' 73 | 74 | DATA2: 75 | REQUEST: 76 | account: xiaoming 77 | password: '123456' 78 | RESPONSE: 79 | code: '0' 80 | message: success 81 | nickname: xiaoming 82 | 83 | DATA3: 84 | REQUEST: 85 | account: xiaoming 86 | password: '*' 87 | RESPONSE: 88 | code: '10001' 89 | message: account or password error 90 | status_code: 401 91 | delay: 1 92 | remark: 密码错误登录 93 | 94 | DATA4: 95 | REQUEST: 96 | account: guess 97 | password: '*' 98 | RESPONSE: 99 | code: '0' 100 | message: success 101 | nickname: guess 102 | 103 | --- 104 | Name: USER 105 | Desc: 获取用户信息 106 | Path: /api/user/{user_id} 107 | Method: POST 108 | Auth: None 109 | GROUP: USER 110 | 111 | REQUEST: 112 | Headers: 113 | Content-Type: application/json 114 | Body: 115 | "{user_id}": [int, Y, 用户ID, ''] 116 | type: [string, N, 信息类型, ''] 117 | 118 | RESPONSE: 119 | Headers: 120 | Content-Type: application/json 121 | Body: 122 | Body: [json, Y, 报文Body, json格式] 123 | 124 | DATA1: 125 | REQUEST: 126 | "{user_id}": 100001 127 | type: base 128 | RESPONSE: 129 | name: admin 130 | age: 18 131 | status_code: 200 132 | remark: 正常场景 133 | 134 | DATA2: 135 | REQUEST: 136 | "{user_id}": 100002 137 | type: base 138 | RESPONSE: 139 | code: '401' 140 | message: fail 141 | status_code: 401 142 | remark: '' 143 | 144 | --- 145 | Name: NEWS 146 | Desc: 获取新闻详情 147 | Path: /api/news 148 | Method: GET 149 | Auth: None 150 | GROUP: USER 151 | 152 | REQUEST: 153 | Body: 154 | id: [string, Y, 新闻ID, ''] 155 | 156 | RESPONSE: 157 | Headers: 158 | Content-Type: application/json 159 | Body: 160 | Body: [json, Y, 报文Body, json格式] 161 | 162 | DATA1: 163 | REQUEST: 164 | id: '10001' 165 | RESPONSE: 166 | code: '0' 167 | message: success 168 | context: This is a test 169 | status_code: 200 170 | remark: 新闻1 171 | 172 | DATA2: 173 | REQUEST: 174 | id: '10002' 175 | RESPONSE: 176 | code: '0' 177 | message: success 178 | context: This is a test2 179 | status_code: 200 180 | remark: 新闻2 181 | 182 | DATA3: 183 | REQUEST: 184 | id: '10003' 185 | RESPONSE: 186 | code: '0' 187 | message: No news id 188 | status_code: 404 189 | remark: 无此新闻 190 | -------------------------------------------------------------------------------- /mock.py: -------------------------------------------------------------------------------- 1 | from apistar import Route, Include, http 2 | import copy 3 | import sys 4 | from pathlib import Path 5 | from time import sleep 6 | from doo.cap import CapApp 7 | from doo.data import Excel, Yaml, get_doc 8 | 9 | 10 | type_map = {'int': 'int', 11 | 'string': 'str', 12 | 'float': 'float', 13 | 'bool': 'bool' 14 | } 15 | 16 | 17 | def func(): 18 | return f"""def {key.lower()}(request: http.Request, params: http.QueryParams{pkts}): 19 | ''' 20 | 描述: {desc} 21 | ''' 22 | 23 | return response('{key}', request, params{pkws})""" 24 | 25 | 26 | def home(): 27 | ''' 28 | 描述: Doo 首页 29 | ''' 30 | 31 | return {'Name': 'Doo', 32 | 'Author': 'tonglei', 33 | 'Github': 'https://github.com/tonglei100/doo'} 34 | 35 | 36 | def check_body(body_doc, body_real, **kwarg): 37 | for k,v in body_doc.items(): 38 | # 如果 Mock 数据的值是减号(-),则真实请求中该字段应该不存在 39 | if v == '-': 40 | if body_real.get(k): 41 | return False 42 | # 如果是加号(+),则真实请求中,该字段应该存在,值可以为任意值 43 | elif v == '+': 44 | if not body_real.get(k): 45 | return False 46 | # 如果是星号(*),则真实请求中,该字段存在或不存在都可以 47 | elif v == '*': 48 | continue 49 | # 如果是星号(*)开头,则真实请求中,模糊匹配 50 | elif isinstance(v, str): 51 | if v.startswith('*'): 52 | if not isinstance(body_real.get(k), str): 53 | return False 54 | elif v[1:] not in body_real.get(k): 55 | return False 56 | # 如果是上尖号(^)开头,则真实请求中,开头匹配 57 | elif v.startswith('^'): 58 | if not isinstance(body_real.get(k), str): 59 | return False 60 | elif not body_real.get(k).startswith(v[1:]): 61 | return False 62 | # 如果是 Dollar($)开头,则真实请求中,末尾匹配 63 | elif v.startswith('$'): 64 | if not isinstance(body_real.get(k), str): 65 | return False 66 | elif not body_real.get(k).endswith(v[1:]): 67 | return False 68 | elif v.startswith('#'): 69 | if v[1:] == str(body_real.get(k)): 70 | return False 71 | 72 | 73 | if isinstance(body_doc[k], str) and body_doc[k].startswith('\\'): 74 | body_doc[k] = body_doc[k][1:] 75 | if k.startswith('{') and k.endswith('}'): 76 | if body_doc[k] != kwarg.get(k[1:-1]): 77 | return False 78 | else: 79 | if body_doc[k] != body_real.get(k): 80 | return False 81 | return True 82 | 83 | 84 | def response(api, request: http.Request, params, **kwarg): 85 | params = dict(params) 86 | headers = dict(request.headers) 87 | if doc[api]['Method'] == 'POST': 88 | body = eval(request.body.decode('utf-8')) 89 | else: 90 | body = {} 91 | 92 | headers_doc = doc[api]['REQUEST']['Headers'] 93 | for k in headers_doc: 94 | if headers_doc[k] != headers.get(k.lower()): 95 | return http.JSONResponse(f'Headers is not matching\ndoc \ 96 | {k}:{headers_doc[k]}\nreal {k}:{headers.get(k)}', status_code=404) 97 | 98 | body = dict(body, **params) 99 | for data in doc[api]: 100 | if 'DATA' in data: 101 | result = check_body(doc[api][data]['REQUEST'], body, **kwarg) 102 | if result: 103 | if doc[api][data].get('delay'): 104 | sleep(doc[api][data]['delay']) 105 | return http.JSONResponse(doc[api][data]['RESPONSE'], \ 106 | status_code=doc[api][data].get('status_code', 200), headers=doc[api]['RESPONSE']['Headers']) 107 | 108 | return http.JSONResponse('No body data matching', status_code=404) 109 | 110 | doc, extra_files = get_doc() 111 | 112 | for key in doc: 113 | desc = doc[key]['Desc'] 114 | pkts = '' 115 | pkws = '' 116 | body = doc[key]['REQUEST']['Body'] 117 | for k in body: 118 | if k.startswith('{') and k.endswith('}'): 119 | pkts += ', ' + k[1:-1] + ': ' + type_map[body[k][0].lower()] 120 | pkws += ', ' + k[1:-1] + '=' + k[1:-1] 121 | exec(func()) 122 | 123 | routes = [Route('/', method='GET', handler=home)] 124 | 125 | for key in doc: 126 | url = doc[key]['Path'] 127 | method = doc[key]['Method'] 128 | handler = getattr(sys.modules[__name__], key.lower()) 129 | 130 | routes.append(Route(url, method=method, handler=handler)) 131 | 132 | app = CapApp(routes=routes) 133 | 134 | 135 | if __name__ == '__main__': 136 | app.serve('127.0.0.1', 5000, debug=True) 137 | --------------------------------------------------------------------------------