├── .flake8 ├── .gitattributes ├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README-zh_CN.md ├── README.md ├── docs └── auth.txt ├── examples └── hello │ ├── hello.py │ └── meta.json ├── flask_restaction ├── __init__.py ├── api.py ├── auth.py ├── cli.py ├── docs │ ├── .babelrc │ ├── .jshintrc │ ├── app.py │ ├── dist │ │ ├── docs.css │ │ ├── docs.js │ │ ├── docs.min.css │ │ ├── docs.min.js │ │ ├── highlight.min.js │ │ ├── iconfont.eot │ │ ├── iconfont.ttf │ │ ├── marked.min.js │ │ ├── pace.min.js │ │ └── vue.min.js │ ├── docs.html │ ├── docs.js │ ├── docs.less │ ├── gulpfile.babel.js │ ├── highlight.less │ ├── iconfont.less │ ├── normalize.less │ ├── pace.less │ ├── package.json │ ├── resets.less │ └── variables.less ├── exporters.py ├── res.py └── resjs │ ├── .babelrc │ ├── .jshintrc │ ├── .npmignore │ ├── bin │ └── resjs │ ├── dist │ ├── index.js │ ├── res.node.js │ ├── res.rn.js │ ├── res.web.js │ └── res.web.min.js │ ├── gulpfile.babel.js │ ├── index.js │ ├── karma.conf.js │ ├── package.json │ ├── readme.md │ ├── res.base.js │ ├── res.proxy.js │ ├── res.rn.js │ └── test │ ├── index.html │ └── index.js ├── pytest.ini ├── requires-dev.txt ├── requires.txt ├── server ├── index.py └── meta.json ├── setup.py ├── tests ├── conftest.py ├── helper.py ├── test_api.py ├── test_auth.py ├── test_cli.py └── test_res.py └── tox.ini /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = 3 | .svn,CVS,.bzr,.hg,.git,__pycache__,.tox,.eggs,*.egg, 4 | node_modules,docs 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | flask_restaction/docs/dist/vue.min.js linguist-vendored 2 | flask_restaction/docs/dist/highlight.min.js linguist-vendored 3 | flask_restaction/docs/dist/marked.min.js linguist-vendored 4 | flask_restaction/docs/dist/pace.min.js linguist-vendored 5 | flask_restaction/docs/normalize.less linguist-vendored 6 | 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | **/examples/**/res.js 3 | **/examples/**/res.min.js 4 | **/tests/**/res.js 5 | **/tests/**/res.min.js 6 | **/test/**/res.js 7 | **/test/**/res.min.js 8 | **/docs/**/res.js 9 | **/docs/**/res.min.js 10 | 11 | tests/static/ 12 | node_modules/ 13 | 14 | .ropeproject/ 15 | .venv/ 16 | *.db 17 | 18 | # pkg resource 19 | MANIFEST 20 | 21 | # Byte-compiled / optimized / DLL files 22 | __pycache__/ 23 | *.py[cod] 24 | 25 | # C extensions 26 | *.so 27 | 28 | # Distribution / packaging 29 | .Python 30 | .eggs/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *,cover 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | 62 | # Sphinx documentation 63 | docs/_build/ 64 | docs/build/ 65 | 66 | # PyBuilder 67 | target/ 68 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - '3.3' 4 | - '3.4' 5 | - '3.5' 6 | install: 7 | - pip install -r requires-dev.txt 8 | - pip install . 9 | before_script: 10 | - (python server/index.py > /dev/null 2>&1 &) 11 | - sleep 3 12 | script: 13 | - export FLASK_RESTACTION=$(cd tests&&python -c "print(__import__('flask_restaction').__path__[0])") 14 | - pytest --cov=$FLASK_RESTACTION 15 | - flake8 16 | after_success: 17 | - codecov 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 guyskk 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. 22 | 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include MANIFEST.in 3 | include readme.md 4 | include requires.txt 5 | include pytest.ini 6 | include tox.ini 7 | 8 | recursive-include flask_restaction/resjs/dist * 9 | 10 | include flask_restaction/docs/docs.html 11 | recursive-include flask_restaction/docs/dist * 12 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | clean: 2 | rm -rf .tox .cache *.egg-info dist/* **/__pycache__ *.pyc 3 | publish: 4 | rm -rf dist/* && python setup.py sdist && twine upload dist/* 5 | -------------------------------------------------------------------------------- /README-zh_CN.md: -------------------------------------------------------------------------------- 1 | # Flask-Restaction 2 | 3 | [![travis-ci](https://api.travis-ci.org/guyskk/flask-restaction.svg)](https://travis-ci.org/guyskk/flask-restaction) [![codecov](https://codecov.io/gh/guyskk/flask-restaction/branch/master/graph/badge.svg)](https://codecov.io/gh/guyskk/flask-restaction) 4 | 5 | [README](README.md) | [中文文档](README-zh_CN.md) 6 | 7 | 为RESTful API而生的Web框架: 8 | 9 | - 创建RESTful API 10 | - 校验用户输入以及将输出转化成合适的响应格式 11 | - 身份验证和权限控制 12 | - 自动生成Javascript SDK和API文档 13 | 14 | 注意:仅支持Python3.3+ 15 | 16 | 17 | ## 安装 18 | 19 | pip install flask-restaction 20 | 21 | 22 | ## 文档 23 | 24 | 简体中文文档: http://restaction-zh-cn.readthedocs.io/ 25 | English Document: http://restaction.readthedocs.io/ 26 | 文档源文件: https://github.com/restaction 27 | 28 | 29 | ## 测试 30 | 31 | 测试之前: 32 | 33 | pip install -r requires.txt 34 | pip install -r requires-dev.txt 35 | pip install -e . 36 | python server/index.py 37 | 38 | 单元测试: 39 | 40 | pytest 41 | 42 | 代码风格测试: 43 | 44 | flake8 45 | 46 | 集成测试: 47 | 48 | tox 49 | 50 | 51 | ## License 52 | 53 | MIT License 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flask-Restaction 2 | 3 | [![travis-ci](https://api.travis-ci.org/guyskk/flask-restaction.svg)](https://travis-ci.org/guyskk/flask-restaction) [![codecov](https://codecov.io/gh/guyskk/flask-restaction/branch/master/graph/badge.svg)](https://codecov.io/gh/guyskk/flask-restaction) 4 | 5 | [README](README.md) | [中文文档](README-zh_CN.md) 6 | 7 | A web framwork born to create RESTful API 8 | 9 | - Create RESTful API 10 | - Validate request and Serialize response 11 | - Authorization and Permission control 12 | - Auto generate Javascript SDK and API document 13 | 14 | Note: Only support Python3.3+ 15 | 16 | 17 | ## Install 18 | 19 | pip install flask-restaction 20 | 21 | 22 | ## Document 23 | 24 | 简体中文文档: http://restaction-zh-cn.readthedocs.io/ 25 | English Document: http://restaction.readthedocs.io/ 26 | Document Sources: https://github.com/restaction 27 | 28 | 29 | ## Test 30 | 31 | Before test: 32 | 33 | pip install -r requires.txt 34 | pip install -r requires-dev.txt 35 | pip install -e . 36 | python server/index.py 37 | 38 | Pytest: 39 | 40 | pytest 41 | 42 | Code style: 43 | 44 | flake8 45 | 46 | Tox test: 47 | 48 | tox 49 | 50 | 51 | ## License 52 | 53 | MIT License 54 | -------------------------------------------------------------------------------- /docs/auth.txt: -------------------------------------------------------------------------------- 1 | +--------+ +--------------+ +--------------+ +-------------+ 2 | | | | | None | | guest | | 3 | Deny | User +---------------> decode_token +--------> get_role +---------X hello.get | 4 | | | | | | | | | 5 | +--------+ +--------------+ +--------------+ +-------------+ 6 | 7 | 8 | +--------+ +--------------+ +--------------+ +-------------+ 9 | | | | | None | | guest | | 10 | | User +---------------> decode_token +--------> get_role +---------> user.post | 11 | | | | | | | | | 12 | Login +---+----+ +--------------+ +--------------+ +------+------+ 13 | ^ | 14 | | +--------------+ | 15 | | Authorization | | g.token | 16 | +--------------------------------+ encode_token <----------------------------+ 17 | | | 18 | +--------------+ 19 | 20 | 21 | +--------+ +--------------+ +--------------+ +-------------+ 22 | | | Authorization | | token | | admin | | 23 | OK | User +---------------> decode_token +--------> get_role +---------> hello.get | 24 | | | | | | | | | 25 | +--------+ +--------------+ +--------------+ +-------------+ 26 | 27 | -------------------------------------------------------------------------------- /examples/hello/hello.py: -------------------------------------------------------------------------------- 1 | """ 2 | # Hello API 3 | 4 | Support markdown in docs: 5 | 6 | ![markdown](https://upload.wikimedia.org/wikipedia/commons/thumb/4/48/Markdown-mark.svg/500px-Markdown-mark.svg.png) 7 | 8 | 执行以下命令,将会在static目录下生成res.js文件: 9 | ``` 10 | resjs http://127.0.0.1:5000/docs -d static/res.js 11 | ``` 12 | 之后打开chrome控制台就可以用res.js调用API了。 13 | """ 14 | from flask import Flask, g 15 | from flask_restaction import Api, TokenAuth 16 | 17 | app = Flask(__name__) 18 | api = Api(app, docs=__doc__, metafile="meta.json") 19 | app.secret_key = b'secret_key' 20 | auth = TokenAuth(api) 21 | 22 | 23 | @auth.get_role 24 | def get_role(token): 25 | if token: 26 | return token.get('role', 'guest') 27 | return 'guest' 28 | 29 | 30 | class Welcome: 31 | 32 | def __init__(self, name): 33 | self.name = name 34 | self.message = "Hello %s, Welcome to flask-restaction!" % name 35 | 36 | 37 | class Hello: 38 | """ 39 | Hello world 40 | 41 | $shared: 42 | name: 43 | name?str&default="world": Your name 44 | message: 45 | message?str: Welcome message 46 | """ 47 | 48 | def __init__(self, api): 49 | self.api = api 50 | 51 | def get(self, name): 52 | """ 53 | Welcome to flask-restaction 54 | 55 | $input: "@name" 56 | $output: "@message" 57 | """ 58 | return Welcome(name) 59 | 60 | def get_me(self): 61 | """ 62 | Get welcome for me 63 | 64 | $output: "@message" 65 | """ 66 | return Welcome(g.token["name"]) 67 | 68 | def post(self, name): 69 | """Create auth headers 70 | 71 | $input: "@name" 72 | """ 73 | g.token = {"name": name, "role": "normal"} 74 | return "OK" 75 | 76 | 77 | api.add_resource(Hello, api) 78 | api.add_resource(type('Docs', (), {'get': api.meta_view})) 79 | 80 | if __name__ == '__main__': 81 | app.run(debug=True) 82 | -------------------------------------------------------------------------------- /examples/hello/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "$url_prefix": "http://127.0.0.1:5000", 3 | "$roles": { 4 | "guest": { 5 | "hello": ["get", "post"], 6 | "docs": ["get"] 7 | }, 8 | "normal": { 9 | "hello": ["get", "post", "get_me"], 10 | "docs": ["get"] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /flask_restaction/__init__.py: -------------------------------------------------------------------------------- 1 | from .api import Api, abort 2 | from .auth import TokenAuth 3 | from .res import Res 4 | from .exporters import exporters, exporter 5 | 6 | __all__ = ["Api", "TokenAuth", "Res", "exporters", "exporter", "abort"] 7 | -------------------------------------------------------------------------------- /flask_restaction/api.py: -------------------------------------------------------------------------------- 1 | """ 2 | API - Resource Manager 3 | """ 4 | import json 5 | import re 6 | import textwrap 7 | from collections import OrderedDict, defaultdict 8 | from os.path import basename, dirname, join 9 | 10 | import simple_yaml as yaml 11 | from flask import abort as flask_abort 12 | from flask import ( 13 | Response, current_app, make_response, request, send_from_directory 14 | ) 15 | from validr import Invalid, SchemaParser 16 | from validr.schema import MarkKey 17 | from werkzeug.wrappers import Response as ResponseBase 18 | 19 | from .cli import generate_code, parse_meta 20 | from .exporters import exporters 21 | from .res import Res 22 | 23 | PATTERN_ACTION = re.compile( 24 | r'^(get|post|put|delete|head|options|trace|patch){1}(?:_(.*))?$') 25 | PATTERN_ENDPOINT = re.compile(r"^(?:(.*)\.)?(\w*)(?:@(.*))?$") 26 | DEFAULT_AUTH = { 27 | "header": "Authorization", 28 | "algorithm": "HS256", 29 | "expiration": 3600, 30 | "cookie": None, 31 | "refresh": True 32 | } 33 | BUILTIN_ERROR = { 34 | "400.InvalidData": "request data invalid", 35 | "403.PermissionDeny": "permission deny", 36 | "500.ServerError": "internal server error" 37 | } 38 | DOCS_DIST = join(dirname(__file__), 'docs/dist') 39 | DOCS_HTML = join(dirname(__file__), 'docs/docs.html') 40 | 41 | 42 | def abort(code, error=None, message=None): 43 | """ 44 | Abort with suitable error response 45 | 46 | Args: 47 | code (int): status code 48 | error (str): error symbol or flask.Response 49 | message (str): error message 50 | """ 51 | if error is None: 52 | flask_abort(code) 53 | elif isinstance(error, Response): 54 | error.status_code = code 55 | flask_abort(code, response=error) 56 | else: 57 | body = { 58 | "status": code, 59 | "error": error, 60 | "message": message 61 | } 62 | flask_abort(code, response=export(body, code)) 63 | 64 | 65 | def unpack(rv): 66 | """ 67 | Convert rv to tuple(data, code, headers) 68 | 69 | Args: 70 | rv: data or tuple that contain code and headers 71 | Returns: 72 | tuple (rv, status, headers) 73 | """ 74 | status = headers = None 75 | if isinstance(rv, tuple): 76 | rv, status, headers = rv + (None,) * (3 - len(rv)) 77 | if isinstance(status, (dict, list)): 78 | headers, status = status, headers 79 | return (rv, status, headers) 80 | 81 | 82 | def export(rv, code=None, headers=None): 83 | """ 84 | Create a suitable response 85 | 86 | Args: 87 | rv: return value of action 88 | code: status code 89 | headers: response headers 90 | Returns: 91 | flask.Response 92 | """ 93 | if isinstance(rv, ResponseBase): 94 | return make_response(rv, code, headers) 95 | else: 96 | if code is None: 97 | code = 200 98 | mediatype = request.accept_mimetypes.best_match( 99 | exporters.keys(), default='application/json') 100 | return exporters[mediatype](rv, code, headers) 101 | 102 | 103 | def parse_docs(docs, marks): 104 | """ 105 | Parse YAML syntax content from docs 106 | 107 | If docs is None, return {} 108 | If docs has no YAML content, return {"$desc": docs} 109 | Else, parse YAML content, return {"$desc": docs, YAML} 110 | 111 | Args: 112 | docs (str): docs to be parsed 113 | marks (list): list of which indicate YAML content starts 114 | Returns: 115 | A dict contains information of docs 116 | """ 117 | if docs is None: 118 | return {} 119 | indexs = [] 120 | for mark in marks: 121 | i = docs.find(mark) 122 | if i >= 0: 123 | indexs.append(i) 124 | if not indexs: 125 | return {"$desc": textwrap.dedent(docs).strip()} 126 | start = min(indexs) 127 | start = docs.rfind("\n", 0, start) 128 | yamltext = textwrap.dedent(docs[start + 1:]) 129 | meta = yaml.load(yamltext) 130 | meta["$desc"] = textwrap.dedent(docs[:start]).strip() 131 | return meta 132 | 133 | 134 | def get_request_data(): 135 | """ 136 | Get request data based on request.method 137 | 138 | If method is GET or DELETE, get data from request.args 139 | If method is POST, PATCH or PUT, get data from request.form or request.json 140 | """ 141 | method = request.method.lower() 142 | if method in ["get", "delete"]: 143 | return request.args 144 | elif method in ["post", "put", "patch"]: 145 | if request.mimetype == 'application/json': 146 | try: 147 | return request.get_json() 148 | except: 149 | abort(400, "InvalidData", "invalid json content") 150 | else: 151 | return request.form 152 | else: 153 | return None 154 | 155 | 156 | def parse_request(): 157 | """Parse request endpoint and return (resource, action)""" 158 | find = None 159 | if request.endpoint is not None: 160 | find = PATTERN_ENDPOINT.findall(request.endpoint) 161 | if not find: 162 | raise ValueError("invalid endpoint %s" % request.endpoint) 163 | __, resource, action_name = find[0] 164 | if action_name: 165 | action = request.method.lower() + "_" + action_name 166 | else: 167 | action = request.method.lower() 168 | return resource, action 169 | 170 | 171 | def get_title(desc, default=None): 172 | """Get title of desc""" 173 | if not desc: 174 | return default 175 | lines = desc.strip('\n').split('\n') 176 | if not lines: 177 | return default 178 | return lines[0].lstrip('# ').rstrip(' ') 179 | 180 | 181 | class Api: 182 | """ 183 | Manager of Resource 184 | 185 | Args: 186 | app: Flask or Blueprint 187 | validators (dict): custom validators 188 | metafile (str): path of metafile 189 | docs (str): api docs 190 | Attributes: 191 | validators (dict): custom validators 192 | meta (dict): metadata of api 193 | """ 194 | 195 | def __init__(self, app, validators=None, metafile=None, docs=""): 196 | self.before_request_funcs = [] 197 | self.after_request_funcs = [] 198 | self.handle_error_func = None 199 | self.app = app 200 | if validators: 201 | self.validators = validators 202 | else: 203 | self.validators = {} 204 | if metafile is None: 205 | self.meta = {} 206 | else: 207 | with open(metafile) as f: 208 | self.meta = json.load(f) 209 | meta_api = parse_docs(docs, ["$shared", "$error"]) 210 | self.meta["$desc"] = meta_api.get("$desc", "") 211 | self.meta["$title"] = get_title(self.meta.get('$desc'), 'Document') 212 | self.meta["$shared"] = meta_api.get("$shared", OrderedDict()) 213 | self.meta["$error"] = BUILTIN_ERROR.copy() 214 | self.meta["$error"].update(meta_api.get("$error", {})) 215 | # check shared is valid or not 216 | if self.meta["$shared"]: 217 | with MarkKey("$shared"): 218 | SchemaParser(shared=self.meta["$shared"]) 219 | auth = DEFAULT_AUTH.copy() 220 | auth.update(self.meta.get("$auth", {})) 221 | self.meta["$auth"] = auth 222 | # TODO new feature: $requires 223 | self.requires = {} 224 | for k, v in self.meta.get("$requires", {}).items(): 225 | self.requires[k] = Res(v) 226 | self._resjs_cache = None 227 | 228 | def meta_view(self): 229 | """ 230 | Meta data / API document 231 | 232 | By default, this view func will return API document(HTML), 233 | you can set request header `Accept` to `application/json` 234 | or set query string `json` to get meta data(JSON). 235 | """ 236 | # API_URL_PREFIX maybe diffierent in development and production, 237 | # so pick it from app.config other than store it in metafile 238 | self.meta["$url_prefix"] = current_app.config.get("API_URL_PREFIX", "") 239 | mediatype = request.accept_mimetypes.best_match( 240 | ['text/html', 'application/json'], default='text/html') 241 | dumped = json.dumps( 242 | self.meta, indent=4, sort_keys=True, ensure_ascii=False) 243 | if mediatype == 'application/json' or 'json' in request.args: 244 | return make_response(dumped, { 245 | "Content-Type": "application/json; charset=utf-8" 246 | }) 247 | filename = request.args.get('f') 248 | if filename in ["res.js", "res.min.js"]: 249 | # cache parsed meta 250 | if self._resjs_cache is None: 251 | self._resjs_cache = parse_meta(self.meta) 252 | minify = filename == "res.min.js" 253 | code = generate_code(self._resjs_cache, 254 | prefix=self.meta["$url_prefix"], min=minify) 255 | response = make_response(code, { 256 | "Content-Type": "application/javascript" 257 | }) 258 | # handle etag 259 | response.add_etag() 260 | return response.make_conditional(request) 261 | if filename: 262 | return send_from_directory(DOCS_DIST, basename(filename)) 263 | with open(DOCS_HTML) as f: 264 | content = f.read()\ 265 | .replace('$(title)', self.meta.get('$title', ''))\ 266 | .replace('$(meta)', dumped) 267 | return make_response(content) 268 | 269 | def add_resource(self, resource, *class_args, **class_kwargs): 270 | """ 271 | Add resource 272 | 273 | Parse resource and it's actions, route actions by naming rule. 274 | 275 | Args: 276 | resource: resource class 277 | class_args: class_args 278 | class_kwargs: class_kwargs 279 | """ 280 | name = resource.__name__.lower() 281 | meta_resource = parse_docs(resource.__doc__, ["$shared"]) 282 | self.meta[name] = meta_resource 283 | shared = self.meta["$shared"].copy() 284 | shared.update(meta_resource.get("$shared", {})) 285 | with MarkKey("%s.$shared" % resource.__name__): 286 | sp = SchemaParser(validators=self.validators, shared=shared) 287 | with MarkKey(resource.__name__): 288 | resource = resource(*class_args, **class_kwargs) 289 | # group actions by it's name, and 290 | # make action group a view function 291 | actions = defaultdict(lambda: {}) 292 | for action in dir(resource): 293 | find = PATTERN_ACTION.findall(action) 294 | if not find: 295 | continue 296 | httpmethod, action_name = find[0] 297 | action_group = actions[action_name] 298 | fn = getattr(resource, action) 299 | meta_action = parse_docs( 300 | fn.__doc__, ["$input", "$output", "$error"]) 301 | meta_resource[action] = meta_action 302 | with MarkKey(fn.__name__): 303 | action_group[httpmethod] = \ 304 | self.make_action(fn, sp, meta_action) 305 | 306 | for action_name in actions: 307 | if action_name == "": 308 | url = "/" + name 309 | endpoint = name 310 | else: 311 | url = "/{0}/{1}".format(name, action_name) 312 | endpoint = "{0}@{1}".format(name, action_name) 313 | action_group = actions[action_name] 314 | self.app.add_url_rule( 315 | url, endpoint=endpoint, 316 | view_func=self.make_view(action_group), 317 | methods=set(action_group) 318 | ) 319 | 320 | def make_action(self, fn, schema_parser, meta): 321 | """ 322 | Make resource's method an action 323 | 324 | Validate input, output by schema in meta. 325 | If no input schema, call fn without params. 326 | If no output schema, will not validate return value. 327 | 328 | Args: 329 | fn: resource's method 330 | schema_parser: for parsing schema in meta 331 | meta: meta data of the action 332 | """ 333 | validate_input = validate_output = None 334 | if "$input" in meta: 335 | with MarkKey("$input"): 336 | validate_input = schema_parser.parse(meta["$input"]) 337 | if "$output" in meta: 338 | with MarkKey("$output"): 339 | validate_output = schema_parser.parse(meta["$output"]) 340 | 341 | def action(data): 342 | if validate_input: 343 | try: 344 | data = validate_input(data) 345 | except Invalid as ex: 346 | return abort(400, "InvalidData", str(ex)) 347 | if isinstance(data, dict): 348 | rv = fn(**data) 349 | else: 350 | rv = fn(data) 351 | else: 352 | rv = fn() 353 | rv, status, headers = unpack(rv) 354 | if validate_output: 355 | try: 356 | rv = validate_output(rv) 357 | except Invalid as ex: 358 | return abort(500, "ServerError", str(ex)) 359 | return rv, status, headers 360 | return action 361 | 362 | def make_view(self, action_group): 363 | """ 364 | Create a view function 365 | 366 | Check permission and Dispatch request to action by request.method 367 | """ 368 | def view(*args, **kwargs): 369 | try: 370 | httpmathod = request.method.lower() 371 | if httpmathod not in action_group: 372 | abort(405) 373 | resp = self._before_request() 374 | if resp is None: 375 | fn = action_group[httpmathod] 376 | resp = fn(get_request_data()) 377 | except Exception as ex: 378 | resp = self._handle_error(ex) 379 | if resp is None: 380 | raise 381 | resp = self._after_request(*unpack(resp)) 382 | return export(*resp) 383 | return view 384 | 385 | def authorize(self, role): 386 | """Check permission""" 387 | resource, action = parse_request() 388 | roles = self.meta.get("$roles", {}) 389 | message = "%s can't access %s.%s" % (role, resource, action) 390 | try: 391 | if action not in roles[role][resource]: 392 | abort(403, "PermissionDeny", message) 393 | except KeyError: 394 | abort(403, "PermissionDeny", message) 395 | 396 | def _before_request(self): 397 | for fn in self.before_request_funcs: 398 | rv = fn() 399 | if rv is not None: 400 | return rv 401 | return None 402 | 403 | def _after_request(self, rv, status, headers): 404 | for fn in self.after_request_funcs: 405 | rv, status, headers = fn(rv, status, headers) 406 | return rv, status, headers 407 | 408 | def _handle_error(self, ex): 409 | if self.handle_error_func: 410 | return self.handle_error_func(ex) 411 | return None 412 | 413 | def after_request(self, f): 414 | """Decorater""" 415 | self.after_request_funcs.append(f) 416 | return f 417 | 418 | def before_request(self, f): 419 | """Decorater""" 420 | self.before_request_funcs.append(f) 421 | return f 422 | 423 | def error_handler(self, f): 424 | """Decorater""" 425 | self.handle_error_func = f 426 | return f 427 | -------------------------------------------------------------------------------- /flask_restaction/auth.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | 3 | import jwt 4 | from flask import current_app, g, request 5 | from werkzeug.http import dump_cookie 6 | 7 | 8 | class TokenAuth: 9 | """Token based authorize and permission control""" 10 | 11 | def __init__(self, api): 12 | self.api = api 13 | self.config = api.meta["$auth"] 14 | self.get_role_func = None 15 | api.before_request(self.before_request) 16 | api.after_request(self.after_request) 17 | 18 | def before_request(self): 19 | token = None 20 | if token is None and self.config["header"]: 21 | token = request.headers.get(self.config["header"]) 22 | if token is None and self.config["cookie"]: 23 | token = request.cookies.get(self.config["cookie"]) 24 | if token: 25 | token = self.decode_token(token) 26 | g.token = token 27 | self.api.authorize(self.get_role_func(token)) 28 | 29 | def after_request(self, rv, status, headers): 30 | exp = self.calculate_expiration(g.token) 31 | if exp: 32 | g.token["exp"] = exp 33 | if headers is None: 34 | headers = {} 35 | headers.update(self.generate_headers(g.token)) 36 | return rv, status, headers 37 | 38 | def get_role(self, f): 39 | """Decorater for register get_role_func""" 40 | self.get_role_func = f 41 | return f 42 | 43 | def generate_headers(self, token): 44 | """Generate auth headers""" 45 | headers = {} 46 | token = self.encode_token(token) 47 | if self.config["header"]: 48 | headers[self.config["header"]] = token 49 | if self.config["cookie"]: 50 | headers["Set-Cookie"] = dump_cookie( 51 | self.config["cookie"], token, httponly=True, 52 | max_age=self.config["expiration"] 53 | ) 54 | return headers 55 | 56 | def calculate_expiration(self, token): 57 | """ 58 | Calculate token expiration 59 | 60 | return expiration if the token need to set expiration or refresh, 61 | otherwise return None. 62 | 63 | Args: 64 | token (dict): a decoded token 65 | """ 66 | if not token: 67 | return None 68 | now = datetime.utcnow() 69 | time_to_live = self.config["expiration"] 70 | if "exp" not in token: 71 | return now + timedelta(seconds=time_to_live) 72 | elif self.config["refresh"]: 73 | exp = datetime.utcfromtimestamp(token["exp"]) 74 | # 0.5: reduce refresh frequent 75 | if exp - now < timedelta(seconds=0.5 * time_to_live): 76 | return now + timedelta(seconds=time_to_live) 77 | return None 78 | 79 | def decode_token(self, token): 80 | """Decode Authorization token, return None if token invalid""" 81 | key = current_app.secret_key 82 | if key is None: 83 | if current_app.debug: 84 | current_app.logger.debug("app.secret_key not set") 85 | return None 86 | try: 87 | return jwt.decode( 88 | token, key, 89 | algorithms=[self.config["algorithm"]], 90 | options={'require_exp': True} 91 | ) 92 | except jwt.InvalidTokenError: 93 | return None 94 | 95 | def encode_token(self, token): 96 | """Encode Authorization token, return bytes token""" 97 | key = current_app.secret_key 98 | if key is None: 99 | raise RuntimeError( 100 | "please set app.secret_key before generate token") 101 | return jwt.encode(token, key, algorithm=self.config["algorithm"]) 102 | -------------------------------------------------------------------------------- /flask_restaction/cli.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | 4 | import requests 5 | 6 | from .res import res_to_url 7 | 8 | RESJS_DIST = os.path.join(os.path.dirname(__file__), 'resjs/dist') 9 | 10 | 11 | def parse_meta(meta): 12 | """ 13 | Parse metadata of API 14 | 15 | Args: 16 | meta: metadata of API 17 | Returns: 18 | tuple(url_prefix, auth_header, resources) 19 | """ 20 | resources = {} 21 | for name in meta: 22 | if name.startswith("$"): 23 | continue 24 | resources[name] = resource = {} 25 | for action in meta[name]: 26 | if action.startswith("$"): 27 | continue 28 | url, httpmethod = res_to_url(name, action) 29 | resource[action] = { 30 | "url": url, 31 | "method": httpmethod 32 | } 33 | url_prefix = meta.get("$url_prefix", "").rstrip("/") 34 | return url_prefix, meta["$auth"]["header"].lower(), resources 35 | 36 | 37 | def read_file(filename): 38 | fpath = os.path.join(RESJS_DIST, filename) 39 | with open(fpath, encoding="utf-8") as f: 40 | return f.read() 41 | 42 | 43 | def save_file(dest, content): 44 | with open(dest, "w", encoding="utf-8") as f: 45 | f.write(content) 46 | 47 | 48 | def render_core(url_prefix, auth_header, resources): 49 | """Generate res.core.js""" 50 | code = '' 51 | code += "function(root, init) {\n" 52 | code += " var q = init('%(auth_header)s', '%(url_prefix)s');\n" %\ 53 | {'url_prefix': url_prefix, 'auth_header': auth_header} 54 | code += " var r = null;\n" 55 | for key in resources: 56 | code += " r = root.%(key)s = {};\n" % {'key': key} 57 | for action, item in resources[key].items(): 58 | code += " r.%(action)s = q('%(url)s', '%(method)s');\n" %\ 59 | {'action': action, 60 | 'url': item['url'], 61 | 'method': item['method']} 62 | code += "}" 63 | return code 64 | 65 | 66 | def generate_code(meta, prefix=None, node=False, min=False): 67 | """ 68 | Generate res.js 69 | 70 | Args: 71 | meta: tuple(url_prefix, auth_header, resources) or metadata of API 72 | Returns: 73 | res.js source code 74 | """ 75 | if isinstance(meta, dict): 76 | url_prefix, auth_header, resources = parse_meta(meta) 77 | else: 78 | url_prefix, auth_header, resources = meta 79 | if prefix is not None: 80 | url_prefix = prefix 81 | core = render_core(url_prefix, auth_header, resources) 82 | if min: 83 | filename = 'res.web.min.js' 84 | else: 85 | filename = 'res.web.js' 86 | if node: 87 | filename = 'res.node.js' 88 | base = read_file(filename) 89 | return base.replace('"#res.core.js#"', core) 90 | 91 | 92 | def resjs(url, dest='./res.js', prefix=None, node=False, min=False): 93 | """Generate res.js and save it""" 94 | meta = requests.get(url, headers={'Accept': 'application/json'}).json() 95 | code = generate_code(meta, prefix, node, min) 96 | save_file(dest, code) 97 | 98 | 99 | def main(): 100 | parser = argparse.ArgumentParser( 101 | description="generate res.js for browser or nodejs") 102 | parser.add_argument("url", help="url of api meta") 103 | parser.add_argument("-d", "--dest", default="./res.js", 104 | help="dest path to save res.js") 105 | parser.add_argument("-p", "--prefix", default="", 106 | help="url prefix of generated res.js") 107 | parser.add_argument("-n", "--node", default=False, action='store_true', 108 | help="generate res.js for nodejs, default for browser") 109 | parser.add_argument("-m", "--min", default=False, action='store_true', 110 | help="minimize generated res.js, default not minimize") 111 | args = parser.parse_args() 112 | resjs(args.url, args.dest, args.prefix, args.node, args.min) 113 | print('OK, saved in: %s' % args.dest) 114 | -------------------------------------------------------------------------------- /flask_restaction/docs/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"], 3 | "plugins": ["transform-runtime"] 4 | } 5 | -------------------------------------------------------------------------------- /flask_restaction/docs/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "undef": true, 3 | "unused": true, 4 | "esversion": 6, 5 | "asi": true, 6 | "browser": true, 7 | "node": true, 8 | "devel": true, 9 | "predef": ["res", "Vue", "marked", "hljs"] 10 | } 11 | -------------------------------------------------------------------------------- /flask_restaction/docs/app.py: -------------------------------------------------------------------------------- 1 | """ 2 | ## PurePage 3 | 4 | PurePage 致力于达到以下目标: 5 | 6 | 1. 良好的写作体验和阅读体验 7 | 2. 易于分享和发现有价值的文章 8 | 3. 运行稳定,安全,快速 9 | 4. 开放API 10 | 11 | ![PurePage](https://raw.githubusercontent.com/guyskk/purepage/master/artwork/logo.png) 12 | 13 | > flask-restaction is awsome !! 14 | 15 | $shared: 16 | paging: 17 | page_num?int&min=1&default=1: 第几页,从1开始计算 18 | page_size?int&min=1&default=10: 每页的数量 19 | $error: 20 | 404.NotFound: 未找到页面 21 | 500.ServerError: 服务器错误 22 | """ 23 | from flask_restaction import Api 24 | from flask import Flask 25 | app = Flask(__name__) 26 | app.debug = True 27 | app.config['API_URL_PREFIX'] = '/api' 28 | 29 | api = Api(app, docs=__doc__) 30 | api.meta["$roles"] = { 31 | "管理员": { 32 | "hello": ["get", "post_login", "delete", "put", "post", "put_login"], 33 | "User": ["get", "post_login", "delete", "put", "post"], 34 | "Article": ["get", "post_login", "delete", "put"], 35 | "hello5": ["get", "post_login", "delete"], 36 | "hello6": ["get", "post_login"] 37 | }, 38 | "普通用户": { 39 | "hello": ["get", "post_login"] 40 | }, 41 | "访客": { 42 | "hello": ["post_login"] 43 | } 44 | } 45 | 46 | 47 | class Hello: 48 | """欢迎""" 49 | 50 | def get(self, name): 51 | """ 52 | Get helloGet helloGet helloGet helloGet helloGet helloGet helloGet he 53 | 54 | $input: 55 | name?str: your name 56 | $output: 57 | welcome?str: welcome message 58 | """ 59 | return {'welcome': name} 60 | 61 | def post(self, name): 62 | """ 63 | Get hello 64 | 65 | $input: 66 | name?str: your name 67 | $output: 68 | welcome?str: welcome message 69 | """ 70 | return {'welcome': name} 71 | 72 | def put(self, name): 73 | """ 74 | Get hello 75 | 76 | $input: 77 | name?str: your name 78 | $output: 79 | welcome?str: welcome message 80 | """ 81 | return {'welcome': name} 82 | 83 | 84 | class User(Hello): 85 | """用户模块""" 86 | 87 | 88 | class Article(Hello): 89 | """博客/文章""" 90 | 91 | 92 | class World(Hello): 93 | """Hello World""" 94 | 95 | 96 | api.add_resource(User) 97 | api.add_resource(Hello) 98 | api.add_resource(Article) 99 | api.add_resource(World) 100 | api.add_resource(type("Docs", (), {"get": api.meta_view})) 101 | 102 | 103 | app.route('/')(api.meta_view) 104 | if __name__ == '__main__': 105 | app.run() 106 | -------------------------------------------------------------------------------- /flask_restaction/docs/dist/docs.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v4.1.1 | MIT License | github.com/necolas/normalize.css */ 2 | /** 3 | * 1. Change the default font family in all browsers (opinionated). 4 | * 2. Correct the line height in all browsers. 5 | * 3. Prevent adjustments of font size after orientation changes in IE and iOS. 6 | */ 7 | html { 8 | font-family: sans-serif; 9 | /* 1 */ 10 | line-height: 1.15; 11 | /* 2 */ 12 | -ms-text-size-adjust: 100%; 13 | /* 3 */ 14 | -webkit-text-size-adjust: 100%; 15 | /* 3 */ 16 | } 17 | /** 18 | * Remove the margin in all browsers (opinionated). 19 | */ 20 | body { 21 | margin: 0; 22 | } 23 | /* HTML5 display definitions 24 | ========================================================================== */ 25 | /** 26 | * Add the correct display in IE 9-. 27 | * 1. Add the correct display in Edge, IE, and Firefox. 28 | * 2. Add the correct display in IE. 29 | */ 30 | article, 31 | aside, 32 | details, 33 | figcaption, 34 | figure, 35 | footer, 36 | header, 37 | main, 38 | menu, 39 | nav, 40 | section, 41 | summary { 42 | /* 1 */ 43 | display: block; 44 | } 45 | /** 46 | * Add the correct display in IE 9-. 47 | */ 48 | audio, 49 | canvas, 50 | progress, 51 | video { 52 | display: inline-block; 53 | } 54 | /** 55 | * Add the correct display in iOS 4-7. 56 | */ 57 | audio:not([controls]) { 58 | display: none; 59 | height: 0; 60 | } 61 | /** 62 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 63 | */ 64 | progress { 65 | vertical-align: baseline; 66 | } 67 | /** 68 | * Add the correct display in IE 10-. 69 | * 1. Add the correct display in IE. 70 | */ 71 | template, 72 | [hidden] { 73 | display: none; 74 | } 75 | /* Links 76 | ========================================================================== */ 77 | /** 78 | * 1. Remove the gray background on active links in IE 10. 79 | * 2. Remove gaps in links underline in iOS 8+ and Safari 8+. 80 | */ 81 | a { 82 | background-color: transparent; 83 | /* 1 */ 84 | -webkit-text-decoration-skip: objects; 85 | /* 2 */ 86 | } 87 | /** 88 | * Remove the outline on focused links when they are also active or hovered 89 | * in all browsers (opinionated). 90 | */ 91 | a:active, 92 | a:hover { 93 | outline-width: 0; 94 | } 95 | /* Text-level semantics 96 | ========================================================================== */ 97 | /** 98 | * 1. Remove the bottom border in Firefox 39-. 99 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 100 | */ 101 | abbr[title] { 102 | border-bottom: none; 103 | /* 1 */ 104 | text-decoration: underline; 105 | /* 2 */ 106 | text-decoration: underline dotted; 107 | /* 2 */ 108 | } 109 | /** 110 | * Prevent the duplicate application of `bolder` by the next rule in Safari 6. 111 | */ 112 | b, 113 | strong { 114 | font-weight: inherit; 115 | } 116 | /** 117 | * Add the correct font weight in Chrome, Edge, and Safari. 118 | */ 119 | b, 120 | strong { 121 | font-weight: bolder; 122 | } 123 | /** 124 | * Add the correct font style in Android 4.3-. 125 | */ 126 | dfn { 127 | font-style: italic; 128 | } 129 | /** 130 | * Correct the font size and margin on `h1` elements within `section` and 131 | * `article` contexts in Chrome, Firefox, and Safari. 132 | */ 133 | h1 { 134 | font-size: 2em; 135 | margin: 0.67em 0; 136 | } 137 | /** 138 | * Add the correct background and color in IE 9-. 139 | */ 140 | mark { 141 | background-color: #ff0; 142 | color: #000; 143 | } 144 | /** 145 | * Add the correct font size in all browsers. 146 | */ 147 | small { 148 | font-size: 80%; 149 | } 150 | /** 151 | * Prevent `sub` and `sup` elements from affecting the line height in 152 | * all browsers. 153 | */ 154 | sub, 155 | sup { 156 | font-size: 75%; 157 | line-height: 0; 158 | position: relative; 159 | vertical-align: baseline; 160 | } 161 | sub { 162 | bottom: -0.25em; 163 | } 164 | sup { 165 | top: -0.5em; 166 | } 167 | /* Embedded content 168 | ========================================================================== */ 169 | /** 170 | * Remove the border on images inside links in IE 10-. 171 | */ 172 | img { 173 | border-style: none; 174 | } 175 | /** 176 | * Hide the overflow in IE. 177 | */ 178 | svg:not(:root) { 179 | overflow: hidden; 180 | } 181 | /* Grouping content 182 | ========================================================================== */ 183 | /** 184 | * 1. Correct the inheritance and scaling of font size in all browsers. 185 | * 2. Correct the odd `em` font sizing in all browsers. 186 | */ 187 | code, 188 | kbd, 189 | pre, 190 | samp { 191 | font-family: monospace, monospace; 192 | /* 1 */ 193 | font-size: 1em; 194 | /* 2 */ 195 | } 196 | /** 197 | * Add the correct margin in IE 8. 198 | */ 199 | figure { 200 | margin: 1em 40px; 201 | } 202 | /** 203 | * 1. Add the correct box sizing in Firefox. 204 | * 2. Show the overflow in Edge and IE. 205 | */ 206 | hr { 207 | box-sizing: content-box; 208 | /* 1 */ 209 | height: 0; 210 | /* 1 */ 211 | overflow: visible; 212 | /* 2 */ 213 | } 214 | /* Forms 215 | ========================================================================== */ 216 | /** 217 | * 1. Change font properties to `inherit` in all browsers (opinionated). 218 | * 2. Remove the margin in Firefox and Safari. 219 | */ 220 | button, 221 | input, 222 | optgroup, 223 | select, 224 | textarea { 225 | font: inherit; 226 | /* 1 */ 227 | margin: 0; 228 | /* 2 */ 229 | } 230 | /** 231 | * Restore the font weight unset by the previous rule. 232 | */ 233 | optgroup { 234 | font-weight: bold; 235 | } 236 | /** 237 | * Show the overflow in IE. 238 | * 1. Show the overflow in Edge. 239 | */ 240 | button, 241 | input { 242 | /* 1 */ 243 | overflow: visible; 244 | } 245 | /** 246 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 247 | * 1. Remove the inheritance of text transform in Firefox. 248 | */ 249 | button, 250 | select { 251 | /* 1 */ 252 | text-transform: none; 253 | } 254 | /** 255 | * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` 256 | * controls in Android 4. 257 | * 2. Correct the inability to style clickable types in iOS and Safari. 258 | */ 259 | button, 260 | html [type="button"], 261 | [type="reset"], 262 | [type="submit"] { 263 | -webkit-appearance: button; 264 | /* 2 */ 265 | } 266 | /** 267 | * Remove the inner border and padding in Firefox. 268 | */ 269 | button::-moz-focus-inner, 270 | [type="button"]::-moz-focus-inner, 271 | [type="reset"]::-moz-focus-inner, 272 | [type="submit"]::-moz-focus-inner { 273 | border-style: none; 274 | padding: 0; 275 | } 276 | /** 277 | * Restore the focus styles unset by the previous rule. 278 | */ 279 | button:-moz-focusring, 280 | [type="button"]:-moz-focusring, 281 | [type="reset"]:-moz-focusring, 282 | [type="submit"]:-moz-focusring { 283 | outline: 1px dotted ButtonText; 284 | } 285 | /** 286 | * Change the border, margin, and padding in all browsers (opinionated). 287 | */ 288 | fieldset { 289 | border: 1px solid #c0c0c0; 290 | margin: 0 2px; 291 | padding: 0.35em 0.625em 0.75em; 292 | } 293 | /** 294 | * 1. Correct the text wrapping in Edge and IE. 295 | * 2. Correct the color inheritance from `fieldset` elements in IE. 296 | * 3. Remove the padding so developers are not caught out when they zero out 297 | * `fieldset` elements in all browsers. 298 | */ 299 | legend { 300 | box-sizing: border-box; 301 | /* 1 */ 302 | color: inherit; 303 | /* 2 */ 304 | display: table; 305 | /* 1 */ 306 | max-width: 100%; 307 | /* 1 */ 308 | padding: 0; 309 | /* 3 */ 310 | white-space: normal; 311 | /* 1 */ 312 | } 313 | /** 314 | * Remove the default vertical scrollbar in IE. 315 | */ 316 | textarea { 317 | overflow: auto; 318 | } 319 | /** 320 | * 1. Add the correct box sizing in IE 10-. 321 | * 2. Remove the padding in IE 10-. 322 | */ 323 | [type="checkbox"], 324 | [type="radio"] { 325 | box-sizing: border-box; 326 | /* 1 */ 327 | padding: 0; 328 | /* 2 */ 329 | } 330 | /** 331 | * Correct the cursor style of increment and decrement buttons in Chrome. 332 | */ 333 | [type="number"]::-webkit-inner-spin-button, 334 | [type="number"]::-webkit-outer-spin-button { 335 | height: auto; 336 | } 337 | /** 338 | * 1. Correct the odd appearance in Chrome and Safari. 339 | * 2. Correct the outline style in Safari. 340 | */ 341 | [type="search"] { 342 | -webkit-appearance: textfield; 343 | /* 1 */ 344 | outline-offset: -2px; 345 | /* 2 */ 346 | } 347 | /** 348 | * Remove the inner padding and cancel buttons in Chrome and Safari on OS X. 349 | */ 350 | [type="search"]::-webkit-search-cancel-button, 351 | [type="search"]::-webkit-search-decoration { 352 | -webkit-appearance: none; 353 | } 354 | /** 355 | * Correct the text style of placeholders in Chrome, Edge, and Safari. 356 | */ 357 | ::-webkit-input-placeholder { 358 | color: inherit; 359 | opacity: 0.54; 360 | } 361 | /** 362 | * 1. Correct the inability to style clickable types in iOS and Safari. 363 | * 2. Change font properties to `inherit` in Safari. 364 | */ 365 | ::-webkit-file-upload-button { 366 | -webkit-appearance: button; 367 | /* 1 */ 368 | font: inherit; 369 | /* 2 */ 370 | } 371 | html { 372 | box-sizing: border-box; 373 | -webkit-text-size-adjust: 100%; 374 | -ms-text-size-adjust: 100%; 375 | text-size-adjust: 100%; 376 | } 377 | *, 378 | *::after, 379 | *::before { 380 | box-sizing: inherit; 381 | } 382 | body { 383 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 384 | font-size: 18px; 385 | line-height: 1.5; 386 | min-width: 320px; 387 | color: #212121; 388 | background-color: #fff; 389 | } 390 | a { 391 | text-decoration: none; 392 | color: #4078c0; 393 | } 394 | a:hover { 395 | cursor: pointer; 396 | color: #2d5487; 397 | } 398 | a:active, 399 | a:hover { 400 | text-decoration: underline; 401 | } 402 | code, 403 | pre { 404 | background-color: #fafafa; 405 | } 406 | code { 407 | font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; 408 | padding: 2px 3px; 409 | } 410 | pre { 411 | font-size: 15px; 412 | line-height: 1.2; 413 | overflow: auto; 414 | padding: 15px; 415 | border-radius: 2px; 416 | } 417 | pre code { 418 | background-color: transparent; 419 | padding: 0; 420 | } 421 | blockquote { 422 | padding-left: 15px; 423 | border-left: 5px solid #757575; 424 | } 425 | @media (max-width: 768px) { 426 | pre { 427 | padding: 2px 3px; 428 | } 429 | blockquote { 430 | margin-right: 0; 431 | margin-left: 0; 432 | } 433 | } 434 | img { 435 | max-width: 100%; 436 | vertical-align: middle; 437 | } 438 | h1, 439 | h2, 440 | h3, 441 | h4, 442 | h5, 443 | h6 { 444 | margin-top: 30px; 445 | margin-bottom: 15px; 446 | } 447 | h1 { 448 | font-size: 46px; 449 | } 450 | h2 { 451 | font-size: 38px; 452 | } 453 | h3 { 454 | font-size: 31px; 455 | } 456 | h4 { 457 | font-size: 23px; 458 | } 459 | h5 { 460 | font-size: 18px; 461 | } 462 | h6 { 463 | font-size: 16px; 464 | } 465 | p, 466 | blockquote, 467 | table, 468 | hr, 469 | dl, 470 | ul, 471 | ol, 472 | pre, 473 | address, 474 | figure { 475 | margin-top: 15px; 476 | margin-bottom: 7.5px; 477 | } 478 | @font-face { 479 | font-family: "iconfont"; 480 | src: url('?f=iconfont.eot'); 481 | /* IE9*/ 482 | src: url('?f=iconfont.ttf') format('truetype'); 483 | } 484 | .iconfont { 485 | font-family: "iconfont" !important; 486 | font-size: 18px; 487 | font-style: normal; 488 | -webkit-font-smoothing: antialiased; 489 | -webkit-text-stroke-width: 0.2px; 490 | -moz-osx-font-smoothing: grayscale; 491 | } 492 | .icon-index:before { 493 | content: "\f0006"; 494 | } 495 | .icon-allow:before { 496 | content: "\e644"; 497 | } 498 | .icon-block:before { 499 | content: "\e62e"; 500 | } 501 | .pace { 502 | -webkit-user-select: none; 503 | -moz-user-select: none; 504 | -ms-user-select: none; 505 | user-select: none; 506 | pointer-events: none; 507 | } 508 | .pace-inactive { 509 | display: none; 510 | } 511 | .pace .pace-progress { 512 | position: fixed; 513 | z-index: 2000; 514 | top: 0; 515 | right: 100%; 516 | width: 100%; 517 | height: 2px; 518 | background: #29d; 519 | } 520 | /** 521 | * GitHub Gist Theme 522 | * Author : Louis Barranqueiro - https://github.com/LouisBarranqueiro 523 | */ 524 | .hljs { 525 | display: block; 526 | background-color: transparent; 527 | padding: 0.5em; 528 | color: #333333; 529 | overflow-x: auto; 530 | } 531 | .hljs-comment, 532 | .hljs-meta { 533 | color: #969896; 534 | } 535 | .hljs-string, 536 | .hljs-variable, 537 | .hljs-template-variable, 538 | .hljs-strong, 539 | .hljs-emphasis, 540 | .hljs-quote { 541 | color: #df5000; 542 | } 543 | .hljs-keyword, 544 | .hljs-selector-tag, 545 | .hljs-type { 546 | color: #a71d5d; 547 | } 548 | .hljs-literal, 549 | .hljs-symbol, 550 | .hljs-bullet, 551 | .hljs-attribute { 552 | color: #0086b3; 553 | } 554 | .hljs-section, 555 | .hljs-name { 556 | color: #63a35c; 557 | } 558 | .hljs-tag { 559 | color: #333333; 560 | } 561 | .hljs-title, 562 | .hljs-attr, 563 | .hljs-selector-id, 564 | .hljs-selector-class, 565 | .hljs-selector-attr, 566 | .hljs-selector-pseudo { 567 | color: #795da3; 568 | } 569 | .hljs-addition { 570 | color: #55a532; 571 | background-color: #eaffea; 572 | } 573 | .hljs-deletion { 574 | color: #bd2c00; 575 | background-color: #ffecec; 576 | } 577 | .hljs-link { 578 | text-decoration: underline; 579 | } 580 | .container { 581 | position: relative; 582 | width: 100%; 583 | overflow: hidden; 584 | } 585 | .sidebar { 586 | position: fixed; 587 | top: 0; 588 | bottom: 0; 589 | left: 0; 590 | width: 260px; 591 | padding: 15px; 592 | overflow-x: hidden; 593 | overflow-y: auto; 594 | z-index: 10; 595 | background-color: #00897b; 596 | color: #fff; 597 | } 598 | .sidebar ul, 599 | .sidebar li { 600 | margin: 0; 601 | } 602 | .sidebar ul { 603 | padding-left: 22.5px; 604 | } 605 | .sidebar a { 606 | color: #fff; 607 | } 608 | .sidebar span { 609 | cursor: default; 610 | } 611 | .content { 612 | margin-left: 260px; 613 | padding: 0 60px 30px; 614 | } 615 | .action-url { 616 | font-size: 32px; 617 | color: #212121; 618 | } 619 | @media (min-width: 768px) { 620 | .content-wrapper { 621 | max-width: 80%; 622 | margin: 0 auto; 623 | } 624 | } 625 | .toggle-sidebar { 626 | position: fixed; 627 | top: 0; 628 | right: 4px; 629 | color: #757575; 630 | } 631 | .toggle-sidebar:hover { 632 | color: #212121; 633 | cursor: pointer; 634 | } 635 | .toggle-sidebar i { 636 | font-size: 32px; 637 | } 638 | .show-on-mobile { 639 | display: none; 640 | } 641 | @media (max-width: 768px) { 642 | .container { 643 | padding: 2px 3px; 644 | } 645 | .sidebar { 646 | display: none; 647 | } 648 | .content { 649 | margin-left: 0; 650 | padding: 8px; 651 | } 652 | .show-on-mobile { 653 | display: block; 654 | } 655 | .hide-on-mobile { 656 | display: none; 657 | } 658 | } 659 | .section-role { 660 | overflow-x: auto; 661 | font-size: 15px; 662 | } 663 | .section-role table { 664 | border-collapse: collapse; 665 | margin-top: 30px; 666 | } 667 | .section-role td { 668 | border: 1px solid #ccc; 669 | padding: 15px; 670 | } 671 | .section-role .role-resource { 672 | font-weight: bold; 673 | color: #fff; 674 | background-color: #00897b; 675 | } 676 | .section-role .role-item { 677 | overflow: hidden; 678 | } 679 | .section-role .role-item-desc { 680 | max-width: 200px; 681 | white-space: nowrap; 682 | overflow: hidden; 683 | text-overflow: ellipsis; 684 | } 685 | .section-role .role-item-title .icon-allow { 686 | color: #5cb85c; 687 | } 688 | .section-role .role-item-title .icon-block { 689 | color: #757575; 690 | } 691 | -------------------------------------------------------------------------------- /flask_restaction/docs/dist/docs.js: -------------------------------------------------------------------------------- 1 | /******/ (function(modules) { // webpackBootstrap 2 | /******/ // The module cache 3 | /******/ var installedModules = {}; 4 | 5 | /******/ // The require function 6 | /******/ function __webpack_require__(moduleId) { 7 | 8 | /******/ // Check if module is in cache 9 | /******/ if(installedModules[moduleId]) 10 | /******/ return installedModules[moduleId].exports; 11 | 12 | /******/ // Create a new module (and put it into the cache) 13 | /******/ var module = installedModules[moduleId] = { 14 | /******/ exports: {}, 15 | /******/ id: moduleId, 16 | /******/ loaded: false 17 | /******/ }; 18 | 19 | /******/ // Execute the module function 20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 21 | 22 | /******/ // Flag the module as loaded 23 | /******/ module.loaded = true; 24 | 25 | /******/ // Return the exports of the module 26 | /******/ return module.exports; 27 | /******/ } 28 | 29 | 30 | /******/ // expose the modules object (__webpack_modules__) 31 | /******/ __webpack_require__.m = modules; 32 | 33 | /******/ // expose the module cache 34 | /******/ __webpack_require__.c = installedModules; 35 | 36 | /******/ // __webpack_public_path__ 37 | /******/ __webpack_require__.p = ""; 38 | 39 | /******/ // Load entry module and return exports 40 | /******/ return __webpack_require__(0); 41 | /******/ }) 42 | /************************************************************************/ 43 | /******/ ([ 44 | /* 0 */ 45 | /***/ function(module, exports, __webpack_require__) { 46 | 47 | 'use strict'; 48 | 49 | var _stringify = __webpack_require__(1); 50 | 51 | var _stringify2 = _interopRequireDefault(_stringify); 52 | 53 | var _keys = __webpack_require__(4); 54 | 55 | var _keys2 = _interopRequireDefault(_keys); 56 | 57 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 58 | 59 | function parseMeta(meta) { 60 | var basic = {}; 61 | var roles = null; 62 | var resources = {}; 63 | for (var k in meta) { 64 | if (k.slice(0, 1) == '$') { 65 | if (k != "$roles") { 66 | basic[k] = meta[k]; 67 | } else { 68 | roles = meta[k]; 69 | } 70 | } else { 71 | resources[k] = meta[k]; 72 | } 73 | } 74 | return { basic: basic, roles: roles, resources: resources }; 75 | } 76 | 77 | function isAllow(role, resource, action) { 78 | if (role[resource]) { 79 | if (role[resource].indexOf(action) >= 0) { 80 | return true; 81 | } 82 | } 83 | return false; 84 | } 85 | 86 | function strm(value) { 87 | // 去除字符串开头的 '#' 和 空白 88 | if (!value) { 89 | return value; 90 | } else { 91 | return value.replace(/^[#\s]+/, ''); 92 | } 93 | } 94 | 95 | function parseRole(role, resources) { 96 | var result = {}; 97 | for (var resource in resources) { 98 | result[resource] = {}; 99 | for (var action in resources[resource]) { 100 | if (isSpecial(action)) { 101 | continue; 102 | } 103 | result[resource][action] = { 104 | desc: strm(resources[resource][action].$desc), 105 | allow: isAllow(role, resource, action) 106 | }; 107 | } 108 | } 109 | return result; 110 | } 111 | 112 | function isEmpty(value) { 113 | return !value || (0, _keys2.default)(value).length === 0; 114 | } 115 | 116 | function isSpecial(value) { 117 | return !value || value.slice(0, 1) == '$'; 118 | } 119 | 120 | function route(app) { 121 | // 根据location.hash值显示对应的页面 122 | if (location.hash) { 123 | var state = location.hash.slice(1).split('.'); 124 | if (state.length == 1) { 125 | if (state[0] == 'desc') { 126 | return app.showBasic(); 127 | } else if (state[0] == 'meta') { 128 | return app.showMeta(); 129 | } 130 | } else if (state.length == 2) { 131 | if (state[0] == 'roles') { 132 | if (state[1] in app.roles) { 133 | return app.showRole(state[1]); 134 | } 135 | } else if (state[0] == 'res') { 136 | if (state[1] in app.resources) { 137 | return app.showResource(state[1]); 138 | } 139 | } 140 | } else if (state.length == 3) { 141 | if (state[0] == 'res') { 142 | if (state[1] in app.resources) { 143 | return app.showResource(state[1]); 144 | } 145 | } 146 | } 147 | } 148 | app.showBasic(); 149 | } 150 | 151 | var app = new Vue({ 152 | el: '#app', 153 | data: { 154 | meta: null, 155 | metaText: null, 156 | basic: null, 157 | roles: null, 158 | resources: null, 159 | view: null, 160 | role: null, 161 | roleName: null, 162 | resource: null, 163 | resourceName: null, 164 | sidebar: true, 165 | isSpecial: isSpecial, 166 | isEmpty: isEmpty 167 | }, 168 | methods: { 169 | showMeta: function showMeta() { 170 | this.view = 'meta'; 171 | }, 172 | showBasic: function showBasic() { 173 | this.view = 'basic'; 174 | }, 175 | showRole: function showRole(name) { 176 | if (name in this.roles) { 177 | this.roleName = name; 178 | this.role = parseRole(this.roles[name], this.resources); 179 | this.view = 'role'; 180 | } 181 | }, 182 | showResource: function showResource(name) { 183 | if (name in this.resources) { 184 | this.resourceName = name; 185 | this.resource = this.resources[name]; 186 | this.view = 'resource'; 187 | } 188 | }, 189 | toggleSidebar: function toggleSidebar() { 190 | if (this.sidebar === "none") { 191 | this.sidebar = "block"; 192 | } else { 193 | this.sidebar = "none"; 194 | } 195 | }, 196 | hideSidebar: function hideSidebar() { 197 | if (window.innerWidth < 768) { 198 | this.sidebar = "none"; 199 | } 200 | }, 201 | toUrl: function toUrl(resource, action) { 202 | var prefix = this.meta.basic.$url_prefix || ''; 203 | //Convert resource.action to "METHOD url", method is UpperCase 204 | var i = action.indexOf("_"); 205 | if (i < 0) { 206 | return action.toUpperCase() + ' ' + prefix + '/' + resource; 207 | } else { 208 | return action.slice(0, i).toUpperCase() + ' ' + prefix + '/' + action.slice(i + 1); 209 | } 210 | } 211 | }, 212 | created: function created() { 213 | var metaText = document.getElementById('meta-text').value; 214 | var meta = parseMeta(JSON.parse(metaText)); 215 | this.metaText = metaText; 216 | this.meta = meta; 217 | this.basic = meta.basic; 218 | this.roles = meta.roles; 219 | this.resources = meta.resources; 220 | }, 221 | mounted: function mounted() { 222 | route(this); 223 | }, 224 | directives: { 225 | marked: function (_marked) { 226 | function marked(_x, _x2) { 227 | return _marked.apply(this, arguments); 228 | } 229 | 230 | marked.toString = function () { 231 | return _marked.toString(); 232 | }; 233 | 234 | return marked; 235 | }(function (el, binding) { 236 | if (binding.value !== undefined) { 237 | el.innerHTML = marked(binding.value); 238 | } 239 | }), 240 | highlight: function highlight(el, binding) { 241 | if (binding.value !== undefined) { 242 | var value = null; 243 | if (typeof binding.value === "string") { 244 | value = binding.value; 245 | } else { 246 | value = (0, _stringify2.default)(binding.value, null, 4); 247 | } 248 | el.innerHTML = hljs.highlight("json", value, true).value; 249 | } 250 | } 251 | } 252 | }); 253 | 254 | window.app = app; 255 | 256 | /***/ }, 257 | /* 1 */ 258 | /***/ function(module, exports, __webpack_require__) { 259 | 260 | module.exports = { "default": __webpack_require__(2), __esModule: true }; 261 | 262 | /***/ }, 263 | /* 2 */ 264 | /***/ function(module, exports, __webpack_require__) { 265 | 266 | var core = __webpack_require__(3) 267 | , $JSON = core.JSON || (core.JSON = {stringify: JSON.stringify}); 268 | module.exports = function stringify(it){ // eslint-disable-line no-unused-vars 269 | return $JSON.stringify.apply($JSON, arguments); 270 | }; 271 | 272 | /***/ }, 273 | /* 3 */ 274 | /***/ function(module, exports) { 275 | 276 | var core = module.exports = {version: '2.4.0'}; 277 | if(typeof __e == 'number')__e = core; // eslint-disable-line no-undef 278 | 279 | /***/ }, 280 | /* 4 */ 281 | /***/ function(module, exports, __webpack_require__) { 282 | 283 | module.exports = { "default": __webpack_require__(5), __esModule: true }; 284 | 285 | /***/ }, 286 | /* 5 */ 287 | /***/ function(module, exports, __webpack_require__) { 288 | 289 | __webpack_require__(6); 290 | module.exports = __webpack_require__(3).Object.keys; 291 | 292 | /***/ }, 293 | /* 6 */ 294 | /***/ function(module, exports, __webpack_require__) { 295 | 296 | // 19.1.2.14 Object.keys(O) 297 | var toObject = __webpack_require__(7) 298 | , $keys = __webpack_require__(9); 299 | 300 | __webpack_require__(24)('keys', function(){ 301 | return function keys(it){ 302 | return $keys(toObject(it)); 303 | }; 304 | }); 305 | 306 | /***/ }, 307 | /* 7 */ 308 | /***/ function(module, exports, __webpack_require__) { 309 | 310 | // 7.1.13 ToObject(argument) 311 | var defined = __webpack_require__(8); 312 | module.exports = function(it){ 313 | return Object(defined(it)); 314 | }; 315 | 316 | /***/ }, 317 | /* 8 */ 318 | /***/ function(module, exports) { 319 | 320 | // 7.2.1 RequireObjectCoercible(argument) 321 | module.exports = function(it){ 322 | if(it == undefined)throw TypeError("Can't call method on " + it); 323 | return it; 324 | }; 325 | 326 | /***/ }, 327 | /* 9 */ 328 | /***/ function(module, exports, __webpack_require__) { 329 | 330 | // 19.1.2.14 / 15.2.3.14 Object.keys(O) 331 | var $keys = __webpack_require__(10) 332 | , enumBugKeys = __webpack_require__(23); 333 | 334 | module.exports = Object.keys || function keys(O){ 335 | return $keys(O, enumBugKeys); 336 | }; 337 | 338 | /***/ }, 339 | /* 10 */ 340 | /***/ function(module, exports, __webpack_require__) { 341 | 342 | var has = __webpack_require__(11) 343 | , toIObject = __webpack_require__(12) 344 | , arrayIndexOf = __webpack_require__(15)(false) 345 | , IE_PROTO = __webpack_require__(19)('IE_PROTO'); 346 | 347 | module.exports = function(object, names){ 348 | var O = toIObject(object) 349 | , i = 0 350 | , result = [] 351 | , key; 352 | for(key in O)if(key != IE_PROTO)has(O, key) && result.push(key); 353 | // Don't enum bug & hidden keys 354 | while(names.length > i)if(has(O, key = names[i++])){ 355 | ~arrayIndexOf(result, key) || result.push(key); 356 | } 357 | return result; 358 | }; 359 | 360 | /***/ }, 361 | /* 11 */ 362 | /***/ function(module, exports) { 363 | 364 | var hasOwnProperty = {}.hasOwnProperty; 365 | module.exports = function(it, key){ 366 | return hasOwnProperty.call(it, key); 367 | }; 368 | 369 | /***/ }, 370 | /* 12 */ 371 | /***/ function(module, exports, __webpack_require__) { 372 | 373 | // to indexed object, toObject with fallback for non-array-like ES3 strings 374 | var IObject = __webpack_require__(13) 375 | , defined = __webpack_require__(8); 376 | module.exports = function(it){ 377 | return IObject(defined(it)); 378 | }; 379 | 380 | /***/ }, 381 | /* 13 */ 382 | /***/ function(module, exports, __webpack_require__) { 383 | 384 | // fallback for non-array-like ES3 and non-enumerable old V8 strings 385 | var cof = __webpack_require__(14); 386 | module.exports = Object('z').propertyIsEnumerable(0) ? Object : function(it){ 387 | return cof(it) == 'String' ? it.split('') : Object(it); 388 | }; 389 | 390 | /***/ }, 391 | /* 14 */ 392 | /***/ function(module, exports) { 393 | 394 | var toString = {}.toString; 395 | 396 | module.exports = function(it){ 397 | return toString.call(it).slice(8, -1); 398 | }; 399 | 400 | /***/ }, 401 | /* 15 */ 402 | /***/ function(module, exports, __webpack_require__) { 403 | 404 | // false -> Array#indexOf 405 | // true -> Array#includes 406 | var toIObject = __webpack_require__(12) 407 | , toLength = __webpack_require__(16) 408 | , toIndex = __webpack_require__(18); 409 | module.exports = function(IS_INCLUDES){ 410 | return function($this, el, fromIndex){ 411 | var O = toIObject($this) 412 | , length = toLength(O.length) 413 | , index = toIndex(fromIndex, length) 414 | , value; 415 | // Array#includes uses SameValueZero equality algorithm 416 | if(IS_INCLUDES && el != el)while(length > index){ 417 | value = O[index++]; 418 | if(value != value)return true; 419 | // Array#toIndex ignores holes, Array#includes - not 420 | } else for(;length > index; index++)if(IS_INCLUDES || index in O){ 421 | if(O[index] === el)return IS_INCLUDES || index || 0; 422 | } return !IS_INCLUDES && -1; 423 | }; 424 | }; 425 | 426 | /***/ }, 427 | /* 16 */ 428 | /***/ function(module, exports, __webpack_require__) { 429 | 430 | // 7.1.15 ToLength 431 | var toInteger = __webpack_require__(17) 432 | , min = Math.min; 433 | module.exports = function(it){ 434 | return it > 0 ? min(toInteger(it), 0x1fffffffffffff) : 0; // pow(2, 53) - 1 == 9007199254740991 435 | }; 436 | 437 | /***/ }, 438 | /* 17 */ 439 | /***/ function(module, exports) { 440 | 441 | // 7.1.4 ToInteger 442 | var ceil = Math.ceil 443 | , floor = Math.floor; 444 | module.exports = function(it){ 445 | return isNaN(it = +it) ? 0 : (it > 0 ? floor : ceil)(it); 446 | }; 447 | 448 | /***/ }, 449 | /* 18 */ 450 | /***/ function(module, exports, __webpack_require__) { 451 | 452 | var toInteger = __webpack_require__(17) 453 | , max = Math.max 454 | , min = Math.min; 455 | module.exports = function(index, length){ 456 | index = toInteger(index); 457 | return index < 0 ? max(index + length, 0) : min(index, length); 458 | }; 459 | 460 | /***/ }, 461 | /* 19 */ 462 | /***/ function(module, exports, __webpack_require__) { 463 | 464 | var shared = __webpack_require__(20)('keys') 465 | , uid = __webpack_require__(22); 466 | module.exports = function(key){ 467 | return shared[key] || (shared[key] = uid(key)); 468 | }; 469 | 470 | /***/ }, 471 | /* 20 */ 472 | /***/ function(module, exports, __webpack_require__) { 473 | 474 | var global = __webpack_require__(21) 475 | , SHARED = '__core-js_shared__' 476 | , store = global[SHARED] || (global[SHARED] = {}); 477 | module.exports = function(key){ 478 | return store[key] || (store[key] = {}); 479 | }; 480 | 481 | /***/ }, 482 | /* 21 */ 483 | /***/ function(module, exports) { 484 | 485 | // https://github.com/zloirock/core-js/issues/86#issuecomment-115759028 486 | var global = module.exports = typeof window != 'undefined' && window.Math == Math 487 | ? window : typeof self != 'undefined' && self.Math == Math ? self : Function('return this')(); 488 | if(typeof __g == 'number')__g = global; // eslint-disable-line no-undef 489 | 490 | /***/ }, 491 | /* 22 */ 492 | /***/ function(module, exports) { 493 | 494 | var id = 0 495 | , px = Math.random(); 496 | module.exports = function(key){ 497 | return 'Symbol('.concat(key === undefined ? '' : key, ')_', (++id + px).toString(36)); 498 | }; 499 | 500 | /***/ }, 501 | /* 23 */ 502 | /***/ function(module, exports) { 503 | 504 | // IE 8- don't enum bug keys 505 | module.exports = ( 506 | 'constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf' 507 | ).split(','); 508 | 509 | /***/ }, 510 | /* 24 */ 511 | /***/ function(module, exports, __webpack_require__) { 512 | 513 | // most Object methods by ES6 should accept primitives 514 | var $export = __webpack_require__(25) 515 | , core = __webpack_require__(3) 516 | , fails = __webpack_require__(34); 517 | module.exports = function(KEY, exec){ 518 | var fn = (core.Object || {})[KEY] || Object[KEY] 519 | , exp = {}; 520 | exp[KEY] = exec(fn); 521 | $export($export.S + $export.F * fails(function(){ fn(1); }), 'Object', exp); 522 | }; 523 | 524 | /***/ }, 525 | /* 25 */ 526 | /***/ function(module, exports, __webpack_require__) { 527 | 528 | var global = __webpack_require__(21) 529 | , core = __webpack_require__(3) 530 | , ctx = __webpack_require__(26) 531 | , hide = __webpack_require__(28) 532 | , PROTOTYPE = 'prototype'; 533 | 534 | var $export = function(type, name, source){ 535 | var IS_FORCED = type & $export.F 536 | , IS_GLOBAL = type & $export.G 537 | , IS_STATIC = type & $export.S 538 | , IS_PROTO = type & $export.P 539 | , IS_BIND = type & $export.B 540 | , IS_WRAP = type & $export.W 541 | , exports = IS_GLOBAL ? core : core[name] || (core[name] = {}) 542 | , expProto = exports[PROTOTYPE] 543 | , target = IS_GLOBAL ? global : IS_STATIC ? global[name] : (global[name] || {})[PROTOTYPE] 544 | , key, own, out; 545 | if(IS_GLOBAL)source = name; 546 | for(key in source){ 547 | // contains in native 548 | own = !IS_FORCED && target && target[key] !== undefined; 549 | if(own && key in exports)continue; 550 | // export native or passed 551 | out = own ? target[key] : source[key]; 552 | // prevent global pollution for namespaces 553 | exports[key] = IS_GLOBAL && typeof target[key] != 'function' ? source[key] 554 | // bind timers to global for call from export context 555 | : IS_BIND && own ? ctx(out, global) 556 | // wrap global constructors for prevent change them in library 557 | : IS_WRAP && target[key] == out ? (function(C){ 558 | var F = function(a, b, c){ 559 | if(this instanceof C){ 560 | switch(arguments.length){ 561 | case 0: return new C; 562 | case 1: return new C(a); 563 | case 2: return new C(a, b); 564 | } return new C(a, b, c); 565 | } return C.apply(this, arguments); 566 | }; 567 | F[PROTOTYPE] = C[PROTOTYPE]; 568 | return F; 569 | // make static versions for prototype methods 570 | })(out) : IS_PROTO && typeof out == 'function' ? ctx(Function.call, out) : out; 571 | // export proto methods to core.%CONSTRUCTOR%.methods.%NAME% 572 | if(IS_PROTO){ 573 | (exports.virtual || (exports.virtual = {}))[key] = out; 574 | // export proto methods to core.%CONSTRUCTOR%.prototype.%NAME% 575 | if(type & $export.R && expProto && !expProto[key])hide(expProto, key, out); 576 | } 577 | } 578 | }; 579 | // type bitmap 580 | $export.F = 1; // forced 581 | $export.G = 2; // global 582 | $export.S = 4; // static 583 | $export.P = 8; // proto 584 | $export.B = 16; // bind 585 | $export.W = 32; // wrap 586 | $export.U = 64; // safe 587 | $export.R = 128; // real proto method for `library` 588 | module.exports = $export; 589 | 590 | /***/ }, 591 | /* 26 */ 592 | /***/ function(module, exports, __webpack_require__) { 593 | 594 | // optional / simple context binding 595 | var aFunction = __webpack_require__(27); 596 | module.exports = function(fn, that, length){ 597 | aFunction(fn); 598 | if(that === undefined)return fn; 599 | switch(length){ 600 | case 1: return function(a){ 601 | return fn.call(that, a); 602 | }; 603 | case 2: return function(a, b){ 604 | return fn.call(that, a, b); 605 | }; 606 | case 3: return function(a, b, c){ 607 | return fn.call(that, a, b, c); 608 | }; 609 | } 610 | return function(/* ...args */){ 611 | return fn.apply(that, arguments); 612 | }; 613 | }; 614 | 615 | /***/ }, 616 | /* 27 */ 617 | /***/ function(module, exports) { 618 | 619 | module.exports = function(it){ 620 | if(typeof it != 'function')throw TypeError(it + ' is not a function!'); 621 | return it; 622 | }; 623 | 624 | /***/ }, 625 | /* 28 */ 626 | /***/ function(module, exports, __webpack_require__) { 627 | 628 | var dP = __webpack_require__(29) 629 | , createDesc = __webpack_require__(37); 630 | module.exports = __webpack_require__(33) ? function(object, key, value){ 631 | return dP.f(object, key, createDesc(1, value)); 632 | } : function(object, key, value){ 633 | object[key] = value; 634 | return object; 635 | }; 636 | 637 | /***/ }, 638 | /* 29 */ 639 | /***/ function(module, exports, __webpack_require__) { 640 | 641 | var anObject = __webpack_require__(30) 642 | , IE8_DOM_DEFINE = __webpack_require__(32) 643 | , toPrimitive = __webpack_require__(36) 644 | , dP = Object.defineProperty; 645 | 646 | exports.f = __webpack_require__(33) ? Object.defineProperty : function defineProperty(O, P, Attributes){ 647 | anObject(O); 648 | P = toPrimitive(P, true); 649 | anObject(Attributes); 650 | if(IE8_DOM_DEFINE)try { 651 | return dP(O, P, Attributes); 652 | } catch(e){ /* empty */ } 653 | if('get' in Attributes || 'set' in Attributes)throw TypeError('Accessors not supported!'); 654 | if('value' in Attributes)O[P] = Attributes.value; 655 | return O; 656 | }; 657 | 658 | /***/ }, 659 | /* 30 */ 660 | /***/ function(module, exports, __webpack_require__) { 661 | 662 | var isObject = __webpack_require__(31); 663 | module.exports = function(it){ 664 | if(!isObject(it))throw TypeError(it + ' is not an object!'); 665 | return it; 666 | }; 667 | 668 | /***/ }, 669 | /* 31 */ 670 | /***/ function(module, exports) { 671 | 672 | module.exports = function(it){ 673 | return typeof it === 'object' ? it !== null : typeof it === 'function'; 674 | }; 675 | 676 | /***/ }, 677 | /* 32 */ 678 | /***/ function(module, exports, __webpack_require__) { 679 | 680 | module.exports = !__webpack_require__(33) && !__webpack_require__(34)(function(){ 681 | return Object.defineProperty(__webpack_require__(35)('div'), 'a', {get: function(){ return 7; }}).a != 7; 682 | }); 683 | 684 | /***/ }, 685 | /* 33 */ 686 | /***/ function(module, exports, __webpack_require__) { 687 | 688 | // Thank's IE8 for his funny defineProperty 689 | module.exports = !__webpack_require__(34)(function(){ 690 | return Object.defineProperty({}, 'a', {get: function(){ return 7; }}).a != 7; 691 | }); 692 | 693 | /***/ }, 694 | /* 34 */ 695 | /***/ function(module, exports) { 696 | 697 | module.exports = function(exec){ 698 | try { 699 | return !!exec(); 700 | } catch(e){ 701 | return true; 702 | } 703 | }; 704 | 705 | /***/ }, 706 | /* 35 */ 707 | /***/ function(module, exports, __webpack_require__) { 708 | 709 | var isObject = __webpack_require__(31) 710 | , document = __webpack_require__(21).document 711 | // in old IE typeof document.createElement is 'object' 712 | , is = isObject(document) && isObject(document.createElement); 713 | module.exports = function(it){ 714 | return is ? document.createElement(it) : {}; 715 | }; 716 | 717 | /***/ }, 718 | /* 36 */ 719 | /***/ function(module, exports, __webpack_require__) { 720 | 721 | // 7.1.1 ToPrimitive(input [, PreferredType]) 722 | var isObject = __webpack_require__(31); 723 | // instead of the ES6 spec version, we didn't implement @@toPrimitive case 724 | // and the second argument - flag - preferred type is a string 725 | module.exports = function(it, S){ 726 | if(!isObject(it))return it; 727 | var fn, val; 728 | if(S && typeof (fn = it.toString) == 'function' && !isObject(val = fn.call(it)))return val; 729 | if(typeof (fn = it.valueOf) == 'function' && !isObject(val = fn.call(it)))return val; 730 | if(!S && typeof (fn = it.toString) == 'function' && !isObject(val = fn.call(it)))return val; 731 | throw TypeError("Can't convert object to primitive value"); 732 | }; 733 | 734 | /***/ }, 735 | /* 37 */ 736 | /***/ function(module, exports) { 737 | 738 | module.exports = function(bitmap, value){ 739 | return { 740 | enumerable : !(bitmap & 1), 741 | configurable: !(bitmap & 2), 742 | writable : !(bitmap & 4), 743 | value : value 744 | }; 745 | }; 746 | 747 | /***/ } 748 | /******/ ]); -------------------------------------------------------------------------------- /flask_restaction/docs/dist/docs.min.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v4.1.1 | MIT License | github.com/necolas/normalize.css */progress,sub,sup{vertical-align:baseline}button,hr,input{overflow:visible}img,legend{max-width:100%}html,legend{box-sizing:border-box}pre,textarea{overflow:auto}a,pre code{background-color:transparent}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;text-size-adjust:100%}article,aside,details,figcaption,figure,footer,header,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{-webkit-text-decoration-skip:objects;text-decoration:none;color:#4078c0}a:active,a:hover{outline-width:0;text-decoration:underline}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}dfn{font-style:italic}h1{margin:.67em 0}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none;vertical-align:middle}svg:not(:root){overflow:hidden}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}button,input,optgroup,select,textarea{font:inherit;margin:0}optgroup{font-weight:700}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:ButtonText dotted 1px}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{color:inherit;display:table;padding:0;white-space:normal}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-input-placeholder{color:inherit;opacity:.54}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}*,::after,::before{box-sizing:inherit}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-size:18px;line-height:1.5;min-width:320px;color:#212121;background-color:#fff}a:hover{cursor:pointer;color:#2d5487}code,pre{background-color:#fafafa}code{font-family:Consolas,"Liberation Mono",Menlo,Courier,monospace;padding:2px 3px}pre{font-size:15px;line-height:1.2;padding:15px;border-radius:2px}pre code{padding:0}blockquote{padding-left:15px;border-left:5px solid #757575}@media (max-width:768px){pre{padding:2px 3px}blockquote{margin-right:0;margin-left:0}}h1,h2,h3,h4,h5,h6{margin-top:30px;margin-bottom:15px}h1{font-size:46px}h2{font-size:38px}h3{font-size:31px}h4{font-size:23px}h5{font-size:18px}h6{font-size:16px}address,blockquote,dl,figure,hr,ol,p,pre,table,ul{margin-top:15px;margin-bottom:7.5px}@font-face{font-family:iconfont;src:url(?f=iconfont.eot);src:url(?f=iconfont.ttf) format('truetype')}.iconfont{font-family:iconfont!important;font-size:18px;font-style:normal;-webkit-font-smoothing:antialiased;-webkit-text-stroke-width:.2px;-moz-osx-font-smoothing:grayscale}.action-url,.toggle-sidebar i{font-size:32px}.icon-index:before{content:"\f0006"}.icon-allow:before{content:"\e644"}.icon-block:before{content:"\e62e"}.pace{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;pointer-events:none}.pace-inactive{display:none}.pace .pace-progress{position:fixed;z-index:2000;top:0;right:100%;width:100%;height:2px;background:#29d}.hljs{display:block;background-color:transparent;padding:.5em;color:#333;overflow-x:auto}.hljs-comment,.hljs-meta{color:#969896}.hljs-emphasis,.hljs-quote,.hljs-string,.hljs-strong,.hljs-template-variable,.hljs-variable{color:#df5000}.hljs-keyword,.hljs-selector-tag,.hljs-type{color:#a71d5d}.hljs-attribute,.hljs-bullet,.hljs-literal,.hljs-symbol{color:#0086b3}.hljs-name,.hljs-section{color:#63a35c}.hljs-tag{color:#333}.hljs-attr,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-selector-pseudo,.hljs-title{color:#795da3}.hljs-addition{color:#55a532;background-color:#eaffea}.hljs-deletion{color:#bd2c00;background-color:#ffecec}.sidebar,.sidebar a{color:#fff}.hljs-link{text-decoration:underline}.container{position:relative;width:100%;overflow:hidden}.sidebar{position:fixed;top:0;bottom:0;left:0;width:260px;padding:15px;overflow-x:hidden;overflow-y:auto;z-index:10;background-color:#00897b}.sidebar li,.sidebar ul{margin:0}.sidebar ul{padding-left:22.5px}.sidebar span{cursor:default}.content{margin-left:260px;padding:0 60px 30px}.action-url{color:#212121}@media (min-width:768px){.content-wrapper{max-width:80%;margin:0 auto}}.toggle-sidebar{position:fixed;top:0;right:4px;color:#757575}.toggle-sidebar:hover{color:#212121;cursor:pointer}.show-on-mobile{display:none}@media (max-width:768px){.container{padding:2px 3px}.sidebar{display:none}.content{margin-left:0;padding:8px}.show-on-mobile{display:block}.hide-on-mobile{display:none}}.section-role{overflow-x:auto;font-size:15px}.section-role table{border-collapse:collapse;margin-top:30px}.section-role td{border:1px solid #ccc;padding:15px}.section-role .role-resource{font-weight:700;color:#fff;background-color:#00897b}.section-role .role-item{overflow:hidden}.section-role .role-item-desc{max-width:200px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.section-role .role-item-title .icon-allow{color:#5cb85c}.section-role .role-item-title .icon-block{color:#757575} -------------------------------------------------------------------------------- /flask_restaction/docs/dist/docs.min.js: -------------------------------------------------------------------------------- 1 | !function(n){function t(r){if(e[r])return e[r].exports;var o=e[r]={exports:{},id:r,loaded:!1};return n[r].call(o.exports,o,o.exports,t),o.loaded=!0,o.exports}var e={};return t.m=n,t.c=e,t.p="",t(0)}([function(n,t,e){"use strict";function r(n){return n&&n.__esModule?n:{"default":n}}function o(n){var t={},e=null,r={};for(var o in n)"$"==o.slice(0,1)?"$roles"!=o?t[o]=n[o]:e=n[o]:r[o]=n[o];return{basic:t,roles:e,resources:r}}function i(n,t,e){return!!(n[t]&&n[t].indexOf(e)>=0)}function u(n){return n?n.replace(/^[#\s]+/,""):n}function c(n,t){var e={};for(var r in t){e[r]={};for(var o in t[r])f(o)||(e[r][o]={desc:u(t[r][o].$desc),allow:i(n,r,o)})}return e}function s(n){return!n||0===(0,v["default"])(n).length}function f(n){return!n||"$"==n.slice(0,1)}function a(n){if(location.hash){var t=location.hash.slice(1).split(".");if(1==t.length){if("desc"==t[0])return n.showBasic();if("meta"==t[0])return n.showMeta()}else if(2==t.length){if("roles"==t[0]){if(t[1]in n.roles)return n.showRole(t[1])}else if("res"==t[0]&&t[1]in n.resources)return n.showResource(t[1])}else if(3==t.length&&"res"==t[0]&&t[1]in n.resources)return n.showResource(t[1])}n.showBasic()}var l=e(1),p=r(l),h=e(4),v=r(h),d=new Vue({el:"#app",data:{meta:null,metaText:null,basic:null,roles:null,resources:null,view:null,role:null,roleName:null,resource:null,resourceName:null,sidebar:!0,isSpecial:f,isEmpty:s},methods:{showMeta:function(){this.view="meta"},showBasic:function(){this.view="basic"},showRole:function(n){n in this.roles&&(this.roleName=n,this.role=c(this.roles[n],this.resources),this.view="role")},showResource:function(n){n in this.resources&&(this.resourceName=n,this.resource=this.resources[n],this.view="resource")},toggleSidebar:function(){"none"===this.sidebar?this.sidebar="block":this.sidebar="none"},hideSidebar:function(){window.innerWidth<768&&(this.sidebar="none")},toUrl:function(n,t){var e=this.meta.basic.$url_prefix||"",r=t.indexOf("_");return r<0?t.toUpperCase()+" "+e+"/"+n:t.slice(0,r).toUpperCase()+" "+e+"/"+t.slice(r+1)}},created:function(){var n=document.getElementById("meta-text").value,t=o(JSON.parse(n));this.metaText=n,this.meta=t,this.basic=t.basic,this.roles=t.roles,this.resources=t.resources},mounted:function(){a(this)},directives:{marked:function(n){function t(t,e){return n.apply(this,arguments)}return t.toString=function(){return n.toString()},t}(function(n,t){void 0!==t.value&&(n.innerHTML=marked(t.value))}),highlight:function(n,t){if(void 0!==t.value){var e=null;e="string"==typeof t.value?t.value:(0,p["default"])(t.value,null,4),n.innerHTML=hljs.highlight("json",e,!0).value}}}});window.app=d},function(n,t,e){n.exports={"default":e(2),__esModule:!0}},function(n,t,e){var r=e(3),o=r.JSON||(r.JSON={stringify:JSON.stringify});n.exports=function(n){return o.stringify.apply(o,arguments)}},function(n,t){var e=n.exports={version:"2.4.0"};"number"==typeof __e&&(__e=e)},function(n,t,e){n.exports={"default":e(5),__esModule:!0}},function(n,t,e){e(6),n.exports=e(3).Object.keys},function(n,t,e){var r=e(7),o=e(9);e(24)("keys",function(){return function(n){return o(r(n))}})},function(n,t,e){var r=e(8);n.exports=function(n){return Object(r(n))}},function(n,t){n.exports=function(n){if(void 0==n)throw TypeError("Can't call method on "+n);return n}},function(n,t,e){var r=e(10),o=e(23);n.exports=Object.keys||function(n){return r(n,o)}},function(n,t,e){var r=e(11),o=e(12),i=e(15)(!1),u=e(19)("IE_PROTO");n.exports=function(n,t){var e,c=o(n),s=0,f=[];for(e in c)e!=u&&r(c,e)&&f.push(e);for(;t.length>s;)r(c,e=t[s++])&&(~i(f,e)||f.push(e));return f}},function(n,t){var e={}.hasOwnProperty;n.exports=function(n,t){return e.call(n,t)}},function(n,t,e){var r=e(13),o=e(8);n.exports=function(n){return r(o(n))}},function(n,t,e){var r=e(14);n.exports=Object("z").propertyIsEnumerable(0)?Object:function(n){return"String"==r(n)?n.split(""):Object(n)}},function(n,t){var e={}.toString;n.exports=function(n){return e.call(n).slice(8,-1)}},function(n,t,e){var r=e(12),o=e(16),i=e(18);n.exports=function(n){return function(t,e,u){var c,s=r(t),f=o(s.length),a=i(u,f);if(n&&e!=e){for(;f>a;)if(c=s[a++],c!=c)return!0}else for(;f>a;a++)if((n||a in s)&&s[a]===e)return n||a||0;return!n&&-1}}},function(n,t,e){var r=e(17),o=Math.min;n.exports=function(n){return n>0?o(r(n),9007199254740991):0}},function(n,t){var e=Math.ceil,r=Math.floor;n.exports=function(n){return isNaN(n=+n)?0:(n>0?r:e)(n)}},function(n,t,e){var r=e(17),o=Math.max,i=Math.min;n.exports=function(n,t){return n=r(n),n<0?o(n+t,0):i(n,t)}},function(n,t,e){var r=e(20)("keys"),o=e(22);n.exports=function(n){return r[n]||(r[n]=o(n))}},function(n,t,e){var r=e(21),o="__core-js_shared__",i=r[o]||(r[o]={});n.exports=function(n){return i[n]||(i[n]={})}},function(n,t){var e=n.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=e)},function(n,t){var e=0,r=Math.random();n.exports=function(n){return"Symbol(".concat(void 0===n?"":n,")_",(++e+r).toString(36))}},function(n,t){n.exports="constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split(",")},function(n,t,e){var r=e(25),o=e(3),i=e(34);n.exports=function(n,t){var e=(o.Object||{})[n]||Object[n],u={};u[n]=t(e),r(r.S+r.F*i(function(){e(1)}),"Object",u)}},function(n,t,e){var r=e(21),o=e(3),i=e(26),u=e(28),c="prototype",s=function(n,t,e){var f,a,l,p=n&s.F,h=n&s.G,v=n&s.S,d=n&s.P,x=n&s.B,y=n&s.W,w=h?o:o[t]||(o[t]={}),b=w[c],m=h?r:v?r[t]:(r[t]||{})[c];h&&(e=t);for(f in e)a=!p&&m&&void 0!==m[f],a&&f in w||(l=a?m[f]:e[f],w[f]=h&&"function"!=typeof m[f]?e[f]:x&&a?i(l,r):y&&m[f]==l?function(n){var t=function(t,e,r){if(this instanceof n){switch(arguments.length){case 0:return new n;case 1:return new n(t);case 2:return new n(t,e)}return new n(t,e,r)}return n.apply(this,arguments)};return t[c]=n[c],t}(l):d&&"function"==typeof l?i(Function.call,l):l,d&&((w.virtual||(w.virtual={}))[f]=l,n&s.R&&b&&!b[f]&&u(b,f,l)))};s.F=1,s.G=2,s.S=4,s.P=8,s.B=16,s.W=32,s.U=64,s.R=128,n.exports=s},function(n,t,e){var r=e(27);n.exports=function(n,t,e){if(r(n),void 0===t)return n;switch(e){case 1:return function(e){return n.call(t,e)};case 2:return function(e,r){return n.call(t,e,r)};case 3:return function(e,r,o){return n.call(t,e,r,o)}}return function(){return n.apply(t,arguments)}}},function(n,t){n.exports=function(n){if("function"!=typeof n)throw TypeError(n+" is not a function!");return n}},function(n,t,e){var r=e(29),o=e(37);n.exports=e(33)?function(n,t,e){return r.f(n,t,o(1,e))}:function(n,t,e){return n[t]=e,n}},function(n,t,e){var r=e(30),o=e(32),i=e(36),u=Object.defineProperty;t.f=e(33)?Object.defineProperty:function(n,t,e){if(r(n),t=i(t,!0),r(e),o)try{return u(n,t,e)}catch(c){}if("get"in e||"set"in e)throw TypeError("Accessors not supported!");return"value"in e&&(n[t]=e.value),n}},function(n,t,e){var r=e(31);n.exports=function(n){if(!r(n))throw TypeError(n+" is not an object!");return n}},function(n,t){n.exports=function(n){return"object"==typeof n?null!==n:"function"==typeof n}},function(n,t,e){n.exports=!e(33)&&!e(34)(function(){return 7!=Object.defineProperty(e(35)("div"),"a",{get:function(){return 7}}).a})},function(n,t,e){n.exports=!e(34)(function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a})},function(n,t){n.exports=function(n){try{return!!n()}catch(t){return!0}}},function(n,t,e){var r=e(31),o=e(21).document,i=r(o)&&r(o.createElement);n.exports=function(n){return i?o.createElement(n):{}}},function(n,t,e){var r=e(31);n.exports=function(n,t){if(!r(n))return n;var e,o;if(t&&"function"==typeof(e=n.toString)&&!r(o=e.call(n)))return o;if("function"==typeof(e=n.valueOf)&&!r(o=e.call(n)))return o;if(!t&&"function"==typeof(e=n.toString)&&!r(o=e.call(n)))return o;throw TypeError("Can't convert object to primitive value")}},function(n,t){n.exports=function(n,t){return{enumerable:!(1&n),configurable:!(2&n),writable:!(4&n),value:t}}}]); -------------------------------------------------------------------------------- /flask_restaction/docs/dist/iconfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guyskk/flask-restaction/17db010b0cbba16bbc2408081975955b03553eb7/flask_restaction/docs/dist/iconfont.eot -------------------------------------------------------------------------------- /flask_restaction/docs/dist/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guyskk/flask-restaction/17db010b0cbba16bbc2408081975955b03553eb7/flask_restaction/docs/dist/iconfont.ttf -------------------------------------------------------------------------------- /flask_restaction/docs/dist/marked.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * marked - a markdown parser 3 | * Copyright (c) 2011-2014, Christopher Jeffrey. (MIT Licensed) 4 | * https://github.com/chjj/marked 5 | */ 6 | (function(){var block={newline:/^\n+/,code:/^( {4}[^\n]+\n*)+/,fences:noop,hr:/^( *[-*_]){3,} *(?:\n+|$)/,heading:/^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/,nptable:noop,lheading:/^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/,blockquote:/^( *>[^\n]+(\n(?!def)[^\n]+)*\n*)+/,list:/^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/,html:/^ *(?:comment *(?:\n|\s*$)|closed *(?:\n{2,}|\s*$)|closing *(?:\n{2,}|\s*$))/,def:/^ *\[([^\]]+)\]: *]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/,table:noop,paragraph:/^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+)\n*/,text:/^[^\n]+/};block.bullet=/(?:[*+-]|\d+\.)/;block.item=/^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/;block.item=replace(block.item,"gm")(/bull/g,block.bullet)();block.list=replace(block.list)(/bull/g,block.bullet)("hr","\\n+(?=\\1?(?:[-*_] *){3,}(?:\\n+|$))")("def","\\n+(?="+block.def.source+")")();block.blockquote=replace(block.blockquote)("def",block.def)();block._tag="(?!(?:"+"a|em|strong|small|s|cite|q|dfn|abbr|data|time|code"+"|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo"+"|span|br|wbr|ins|del|img)\\b)\\w+(?!:/|[^\\w\\s@]*@)\\b";block.html=replace(block.html)("comment",//)("closed",/<(tag)[\s\S]+?<\/\1>/)("closing",/])*?>/)(/tag/g,block._tag)();block.paragraph=replace(block.paragraph)("hr",block.hr)("heading",block.heading)("lheading",block.lheading)("blockquote",block.blockquote)("tag","<"+block._tag)("def",block.def)();block.normal=merge({},block);block.gfm=merge({},block.normal,{fences:/^ *(`{3,}|~{3,})[ \.]*(\S+)? *\n([\s\S]*?)\s*\1 *(?:\n+|$)/,paragraph:/^/,heading:/^ *(#{1,6}) +([^\n]+?) *#* *(?:\n+|$)/});block.gfm.paragraph=replace(block.paragraph)("(?!","(?!"+block.gfm.fences.source.replace("\\1","\\2")+"|"+block.list.source.replace("\\1","\\3")+"|")();block.tables=merge({},block.gfm,{nptable:/^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*/,table:/^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*/});function Lexer(options){this.tokens=[];this.tokens.links={};this.options=options||marked.defaults;this.rules=block.normal;if(this.options.gfm){if(this.options.tables){this.rules=block.tables}else{this.rules=block.gfm}}}Lexer.rules=block;Lexer.lex=function(src,options){var lexer=new Lexer(options);return lexer.lex(src)};Lexer.prototype.lex=function(src){src=src.replace(/\r\n|\r/g,"\n").replace(/\t/g," ").replace(/\u00a0/g," ").replace(/\u2424/g,"\n");return this.token(src,true)};Lexer.prototype.token=function(src,top,bq){var src=src.replace(/^ +$/gm,""),next,loose,cap,bull,b,item,space,i,l;while(src){if(cap=this.rules.newline.exec(src)){src=src.substring(cap[0].length);if(cap[0].length>1){this.tokens.push({type:"space"})}}if(cap=this.rules.code.exec(src)){src=src.substring(cap[0].length);cap=cap[0].replace(/^ {4}/gm,"");this.tokens.push({type:"code",text:!this.options.pedantic?cap.replace(/\n+$/,""):cap});continue}if(cap=this.rules.fences.exec(src)){src=src.substring(cap[0].length);this.tokens.push({type:"code",lang:cap[2],text:cap[3]||""});continue}if(cap=this.rules.heading.exec(src)){src=src.substring(cap[0].length);this.tokens.push({type:"heading",depth:cap[1].length,text:cap[2]});continue}if(top&&(cap=this.rules.nptable.exec(src))){src=src.substring(cap[0].length);item={type:"table",header:cap[1].replace(/^ *| *\| *$/g,"").split(/ *\| */),align:cap[2].replace(/^ *|\| *$/g,"").split(/ *\| */),cells:cap[3].replace(/\n$/,"").split("\n")};for(i=0;i ?/gm,"");this.token(cap,top,true);this.tokens.push({type:"blockquote_end"});continue}if(cap=this.rules.list.exec(src)){src=src.substring(cap[0].length);bull=cap[2];this.tokens.push({type:"list_start",ordered:bull.length>1});cap=cap[0].match(this.rules.item);next=false;l=cap.length;i=0;for(;i1&&b.length>1)){src=cap.slice(i+1).join("\n")+src;i=l-1}}loose=next||/\n\n(?!\s*$)/.test(item);if(i!==l-1){next=item.charAt(item.length-1)==="\n";if(!loose)loose=next}this.tokens.push({type:loose?"loose_item_start":"list_item_start"});this.token(item,false,bq);this.tokens.push({type:"list_item_end"})}this.tokens.push({type:"list_end"});continue}if(cap=this.rules.html.exec(src)){src=src.substring(cap[0].length);this.tokens.push({type:this.options.sanitize?"paragraph":"html",pre:!this.options.sanitizer&&(cap[1]==="pre"||cap[1]==="script"||cap[1]==="style"),text:cap[0]});continue}if(!bq&&top&&(cap=this.rules.def.exec(src))){src=src.substring(cap[0].length);this.tokens.links[cap[1].toLowerCase()]={href:cap[2],title:cap[3]};continue}if(top&&(cap=this.rules.table.exec(src))){src=src.substring(cap[0].length);item={type:"table",header:cap[1].replace(/^ *| *\| *$/g,"").split(/ *\| */),align:cap[2].replace(/^ *|\| *$/g,"").split(/ *\| */),cells:cap[3].replace(/(?: *\| *)?\n$/,"").split("\n")};for(i=0;i])/,autolink:/^<([^ >]+(@|:\/)[^ >]+)>/,url:noop,tag:/^|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>/,link:/^!?\[(inside)\]\(href\)/,reflink:/^!?\[(inside)\]\s*\[([^\]]*)\]/,nolink:/^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/,strong:/^__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)/,em:/^\b_((?:[^_]|__)+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/,code:/^(`+)\s*([\s\S]*?[^`])\s*\1(?!`)/,br:/^ {2,}\n(?!\s*$)/,del:noop,text:/^[\s\S]+?(?=[\\?(?:\s+['"]([\s\S]*?)['"])?\s*/;inline.link=replace(inline.link)("inside",inline._inside)("href",inline._href)();inline.reflink=replace(inline.reflink)("inside",inline._inside)();inline.normal=merge({},inline);inline.pedantic=merge({},inline.normal,{strong:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,em:/^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/});inline.gfm=merge({},inline.normal,{escape:replace(inline.escape)("])","~|])")(),url:/^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/,del:/^~~(?=\S)([\s\S]*?\S)~~/,text:replace(inline.text)("]|","~]|")("|","|https?://|")()});inline.breaks=merge({},inline.gfm,{br:replace(inline.br)("{2,}","*")(),text:replace(inline.gfm.text)("{2,}","*")()});function InlineLexer(links,options){this.options=options||marked.defaults;this.links=links;this.rules=inline.normal;this.renderer=this.options.renderer||new Renderer;this.renderer.options=this.options;if(!this.links){throw new Error("Tokens array requires a `links` property.")}if(this.options.gfm){if(this.options.breaks){this.rules=inline.breaks}else{this.rules=inline.gfm}}else if(this.options.pedantic){this.rules=inline.pedantic}}InlineLexer.rules=inline;InlineLexer.output=function(src,links,options){var inline=new InlineLexer(links,options);return inline.output(src)};InlineLexer.prototype.output=function(src){var out="",link,text,href,cap;while(src){if(cap=this.rules.escape.exec(src)){src=src.substring(cap[0].length);out+=cap[1];continue}if(cap=this.rules.autolink.exec(src)){src=src.substring(cap[0].length);if(cap[2]==="@"){text=cap[1].charAt(6)===":"?this.mangle(cap[1].substring(7)):this.mangle(cap[1]);href=this.mangle("mailto:")+text}else{text=escape(cap[1]);href=text}out+=this.renderer.link(href,null,text);continue}if(!this.inLink&&(cap=this.rules.url.exec(src))){src=src.substring(cap[0].length);text=escape(cap[1]);href=text;out+=this.renderer.link(href,null,text);continue}if(cap=this.rules.tag.exec(src)){if(!this.inLink&&/^/i.test(cap[0])){this.inLink=false}src=src.substring(cap[0].length);out+=this.options.sanitize?this.options.sanitizer?this.options.sanitizer(cap[0]):escape(cap[0]):cap[0];continue}if(cap=this.rules.link.exec(src)){src=src.substring(cap[0].length);this.inLink=true;out+=this.outputLink(cap,{href:cap[2],title:cap[3]});this.inLink=false;continue}if((cap=this.rules.reflink.exec(src))||(cap=this.rules.nolink.exec(src))){src=src.substring(cap[0].length);link=(cap[2]||cap[1]).replace(/\s+/g," ");link=this.links[link.toLowerCase()];if(!link||!link.href){out+=cap[0].charAt(0);src=cap[0].substring(1)+src;continue}this.inLink=true;out+=this.outputLink(cap,link);this.inLink=false;continue}if(cap=this.rules.strong.exec(src)){src=src.substring(cap[0].length);out+=this.renderer.strong(this.output(cap[2]||cap[1]));continue}if(cap=this.rules.em.exec(src)){src=src.substring(cap[0].length);out+=this.renderer.em(this.output(cap[2]||cap[1]));continue}if(cap=this.rules.code.exec(src)){src=src.substring(cap[0].length);out+=this.renderer.codespan(escape(cap[2],true));continue}if(cap=this.rules.br.exec(src)){src=src.substring(cap[0].length);out+=this.renderer.br();continue}if(cap=this.rules.del.exec(src)){src=src.substring(cap[0].length);out+=this.renderer.del(this.output(cap[1]));continue}if(cap=this.rules.text.exec(src)){src=src.substring(cap[0].length);out+=this.renderer.text(escape(this.smartypants(cap[0])));continue}if(src){throw new Error("Infinite loop on byte: "+src.charCodeAt(0))}}return out};InlineLexer.prototype.outputLink=function(cap,link){var href=escape(link.href),title=link.title?escape(link.title):null;return cap[0].charAt(0)!=="!"?this.renderer.link(href,title,this.output(cap[1])):this.renderer.image(href,title,escape(cap[1]))};InlineLexer.prototype.smartypants=function(text){if(!this.options.smartypants)return text;return text.replace(/---/g,"—").replace(/--/g,"–").replace(/(^|[-\u2014/(\[{"\s])'/g,"$1‘").replace(/'/g,"’").replace(/(^|[-\u2014/(\[{\u2018\s])"/g,"$1“").replace(/"/g,"”").replace(/\.{3}/g,"…")};InlineLexer.prototype.mangle=function(text){if(!this.options.mangle)return text;var out="",l=text.length,i=0,ch;for(;i.5){ch="x"+ch.toString(16)}out+="&#"+ch+";"}return out};function Renderer(options){this.options=options||{}}Renderer.prototype.code=function(code,lang,escaped){if(this.options.highlight){var out=this.options.highlight(code,lang);if(out!=null&&out!==code){escaped=true;code=out}}if(!lang){return"
"+(escaped?code:escape(code,true))+"\n
"}return'
'+(escaped?code:escape(code,true))+"\n
\n"};Renderer.prototype.blockquote=function(quote){return"
\n"+quote+"
\n"};Renderer.prototype.html=function(html){return html};Renderer.prototype.heading=function(text,level,raw){return"'+text+"\n"};Renderer.prototype.hr=function(){return this.options.xhtml?"
\n":"
\n"};Renderer.prototype.list=function(body,ordered){var type=ordered?"ol":"ul";return"<"+type+">\n"+body+"\n"};Renderer.prototype.listitem=function(text){return"
  • "+text+"
  • \n"};Renderer.prototype.paragraph=function(text){return"

    "+text+"

    \n"};Renderer.prototype.table=function(header,body){return"\n"+"\n"+header+"\n"+"\n"+body+"\n"+"
    \n"};Renderer.prototype.tablerow=function(content){return"\n"+content+"\n"};Renderer.prototype.tablecell=function(content,flags){var type=flags.header?"th":"td";var tag=flags.align?"<"+type+' style="text-align:'+flags.align+'">':"<"+type+">";return tag+content+"\n"};Renderer.prototype.strong=function(text){return""+text+""};Renderer.prototype.em=function(text){return""+text+""};Renderer.prototype.codespan=function(text){return""+text+""};Renderer.prototype.br=function(){return this.options.xhtml?"
    ":"
    "};Renderer.prototype.del=function(text){return""+text+""};Renderer.prototype.link=function(href,title,text){if(this.options.sanitize){try{var prot=decodeURIComponent(unescape(href)).replace(/[^\w:]/g,"").toLowerCase()}catch(e){return""}if(prot.indexOf("javascript:")===0||prot.indexOf("vbscript:")===0){return""}}var out='
    ";return out};Renderer.prototype.image=function(href,title,text){var out=''+text+'":">";return out};Renderer.prototype.text=function(text){return text};function Parser(options){this.tokens=[];this.token=null;this.options=options||marked.defaults;this.options.renderer=this.options.renderer||new Renderer;this.renderer=this.options.renderer;this.renderer.options=this.options}Parser.parse=function(src,options,renderer){var parser=new Parser(options,renderer);return parser.parse(src)};Parser.prototype.parse=function(src){this.inline=new InlineLexer(src.links,this.options,this.renderer);this.tokens=src.reverse();var out="";while(this.next()){out+=this.tok()}return out};Parser.prototype.next=function(){return this.token=this.tokens.pop()};Parser.prototype.peek=function(){return this.tokens[this.tokens.length-1]||0};Parser.prototype.parseText=function(){var body=this.token.text;while(this.peek().type==="text"){body+="\n"+this.next().text}return this.inline.output(body)};Parser.prototype.tok=function(){switch(this.token.type){case"space":{return""}case"hr":{return this.renderer.hr()}case"heading":{return this.renderer.heading(this.inline.output(this.token.text),this.token.depth,this.token.text)}case"code":{return this.renderer.code(this.token.text,this.token.lang,this.token.escaped)}case"table":{var header="",body="",i,row,cell,flags,j;cell="";for(i=0;i/g,">").replace(/"/g,""").replace(/'/g,"'")}function unescape(html){return html.replace(/&([#\w]+);/g,function(_,n){n=n.toLowerCase();if(n==="colon")return":";if(n.charAt(0)==="#"){return n.charAt(1)==="x"?String.fromCharCode(parseInt(n.substring(2),16)):String.fromCharCode(+n.substring(1))}return""})}function replace(regex,opt){regex=regex.source;opt=opt||"";return function self(name,val){if(!name)return new RegExp(regex,opt);val=val.source||val;val=val.replace(/(^|[^\[])\^/g,"$1");regex=regex.replace(name,val);return self}}function noop(){}noop.exec=noop;function merge(obj){var i=1,target,key;for(;iAn error occured:

    "+escape(e.message+"",true)+"
    "}throw e}}marked.options=marked.setOptions=function(opt){merge(marked.defaults,opt);return marked};marked.defaults={gfm:true,tables:true,breaks:false,pedantic:false,sanitize:false,sanitizer:null,mangle:true,smartLists:false,silent:false,highlight:null,langPrefix:"lang-",smartypants:false,headerPrefix:"",renderer:new Renderer,xhtml:false};marked.Parser=Parser;marked.parser=Parser.parse;marked.Renderer=Renderer;marked.Lexer=Lexer;marked.lexer=Lexer.lex;marked.InlineLexer=InlineLexer;marked.inlineLexer=InlineLexer.output;marked.parse=marked;if(typeof module!=="undefined"&&typeof exports==="object"){module.exports=marked}else if(typeof define==="function"&&define.amd){define(function(){return marked})}else{this.marked=marked}}).call(function(){return this||(typeof window!=="undefined"?window:global)}()); -------------------------------------------------------------------------------- /flask_restaction/docs/dist/pace.min.js: -------------------------------------------------------------------------------- 1 | /*! pace 1.0.0 */ 2 | (function(){var a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X=[].slice,Y={}.hasOwnProperty,Z=function(a,b){function c(){this.constructor=a}for(var d in b)Y.call(b,d)&&(a[d]=b[d]);return c.prototype=b.prototype,a.prototype=new c,a.__super__=b.prototype,a},$=[].indexOf||function(a){for(var b=0,c=this.length;c>b;b++)if(b in this&&this[b]===a)return b;return-1};for(u={catchupTime:100,initialRate:.03,minTime:250,ghostTime:100,maxProgressPerFrame:20,easeFactor:1.25,startOnPageLoad:!0,restartOnPushState:!0,restartOnRequestAfter:500,target:"body",elements:{checkInterval:100,selectors:["body"]},eventLag:{minSamples:10,sampleCount:3,lagThreshold:3},ajax:{trackMethods:["GET"],trackWebSockets:!0,ignoreURLs:[]}},C=function(){var a;return null!=(a="undefined"!=typeof performance&&null!==performance&&"function"==typeof performance.now?performance.now():void 0)?a:+new Date},E=window.requestAnimationFrame||window.mozRequestAnimationFrame||window.webkitRequestAnimationFrame||window.msRequestAnimationFrame,t=window.cancelAnimationFrame||window.mozCancelAnimationFrame,null==E&&(E=function(a){return setTimeout(a,50)},t=function(a){return clearTimeout(a)}),G=function(a){var b,c;return b=C(),(c=function(){var d;return d=C()-b,d>=33?(b=C(),a(d,function(){return E(c)})):setTimeout(c,33-d)})()},F=function(){var a,b,c;return c=arguments[0],b=arguments[1],a=3<=arguments.length?X.call(arguments,2):[],"function"==typeof c[b]?c[b].apply(c,a):c[b]},v=function(){var a,b,c,d,e,f,g;for(b=arguments[0],d=2<=arguments.length?X.call(arguments,1):[],f=0,g=d.length;g>f;f++)if(c=d[f])for(a in c)Y.call(c,a)&&(e=c[a],null!=b[a]&&"object"==typeof b[a]&&null!=e&&"object"==typeof e?v(b[a],e):b[a]=e);return b},q=function(a){var b,c,d,e,f;for(c=b=0,e=0,f=a.length;f>e;e++)d=a[e],c+=Math.abs(d),b++;return c/b},x=function(a,b){var c,d,e;if(null==a&&(a="options"),null==b&&(b=!0),e=document.querySelector("[data-pace-"+a+"]")){if(c=e.getAttribute("data-pace-"+a),!b)return c;try{return JSON.parse(c)}catch(f){return d=f,"undefined"!=typeof console&&null!==console?console.error("Error parsing inline pace options",d):void 0}}},g=function(){function a(){}return a.prototype.on=function(a,b,c,d){var e;return null==d&&(d=!1),null==this.bindings&&(this.bindings={}),null==(e=this.bindings)[a]&&(e[a]=[]),this.bindings[a].push({handler:b,ctx:c,once:d})},a.prototype.once=function(a,b,c){return this.on(a,b,c,!0)},a.prototype.off=function(a,b){var c,d,e;if(null!=(null!=(d=this.bindings)?d[a]:void 0)){if(null==b)return delete this.bindings[a];for(c=0,e=[];cQ;Q++)K=U[Q],D[K]===!0&&(D[K]=u[K]);i=function(a){function b(){return V=b.__super__.constructor.apply(this,arguments)}return Z(b,a),b}(Error),b=function(){function a(){this.progress=0}return a.prototype.getElement=function(){var a;if(null==this.el){if(a=document.querySelector(D.target),!a)throw new i;this.el=document.createElement("div"),this.el.className="pace pace-active",document.body.className=document.body.className.replace(/pace-done/g,""),document.body.className+=" pace-running",this.el.innerHTML='
    \n
    \n
    \n
    ',null!=a.firstChild?a.insertBefore(this.el,a.firstChild):a.appendChild(this.el)}return this.el},a.prototype.finish=function(){var a;return a=this.getElement(),a.className=a.className.replace("pace-active",""),a.className+=" pace-inactive",document.body.className=document.body.className.replace("pace-running",""),document.body.className+=" pace-done"},a.prototype.update=function(a){return this.progress=a,this.render()},a.prototype.destroy=function(){try{this.getElement().parentNode.removeChild(this.getElement())}catch(a){i=a}return this.el=void 0},a.prototype.render=function(){var a,b,c,d,e,f,g;if(null==document.querySelector(D.target))return!1;for(a=this.getElement(),d="translate3d("+this.progress+"%, 0, 0)",g=["webkitTransform","msTransform","transform"],e=0,f=g.length;f>e;e++)b=g[e],a.children[0].style[b]=d;return(!this.lastRenderedProgress||this.lastRenderedProgress|0!==this.progress|0)&&(a.children[0].setAttribute("data-progress-text",""+(0|this.progress)+"%"),this.progress>=100?c="99":(c=this.progress<10?"0":"",c+=0|this.progress),a.children[0].setAttribute("data-progress",""+c)),this.lastRenderedProgress=this.progress},a.prototype.done=function(){return this.progress>=100},a}(),h=function(){function a(){this.bindings={}}return a.prototype.trigger=function(a,b){var c,d,e,f,g;if(null!=this.bindings[a]){for(f=this.bindings[a],g=[],d=0,e=f.length;e>d;d++)c=f[d],g.push(c.call(this,b));return g}},a.prototype.on=function(a,b){var c;return null==(c=this.bindings)[a]&&(c[a]=[]),this.bindings[a].push(b)},a}(),P=window.XMLHttpRequest,O=window.XDomainRequest,N=window.WebSocket,w=function(a,b){var c,d,e,f;f=[];for(d in b.prototype)try{e=b.prototype[d],f.push(null==a[d]&&"function"!=typeof e?a[d]=e:void 0)}catch(g){c=g}return f},A=[],j.ignore=function(){var a,b,c;return b=arguments[0],a=2<=arguments.length?X.call(arguments,1):[],A.unshift("ignore"),c=b.apply(null,a),A.shift(),c},j.track=function(){var a,b,c;return b=arguments[0],a=2<=arguments.length?X.call(arguments,1):[],A.unshift("track"),c=b.apply(null,a),A.shift(),c},J=function(a){var b;if(null==a&&(a="GET"),"track"===A[0])return"force";if(!A.length&&D.ajax){if("socket"===a&&D.ajax.trackWebSockets)return!0;if(b=a.toUpperCase(),$.call(D.ajax.trackMethods,b)>=0)return!0}return!1},k=function(a){function b(){var a,c=this;b.__super__.constructor.apply(this,arguments),a=function(a){var b;return b=a.open,a.open=function(d,e){return J(d)&&c.trigger("request",{type:d,url:e,request:a}),b.apply(a,arguments)}},window.XMLHttpRequest=function(b){var c;return c=new P(b),a(c),c};try{w(window.XMLHttpRequest,P)}catch(d){}if(null!=O){window.XDomainRequest=function(){var b;return b=new O,a(b),b};try{w(window.XDomainRequest,O)}catch(d){}}if(null!=N&&D.ajax.trackWebSockets){window.WebSocket=function(a,b){var d;return d=null!=b?new N(a,b):new N(a),J("socket")&&c.trigger("request",{type:"socket",url:a,protocols:b,request:d}),d};try{w(window.WebSocket,N)}catch(d){}}}return Z(b,a),b}(h),R=null,y=function(){return null==R&&(R=new k),R},I=function(a){var b,c,d,e;for(e=D.ajax.ignoreURLs,c=0,d=e.length;d>c;c++)if(b=e[c],"string"==typeof b){if(-1!==a.indexOf(b))return!0}else if(b.test(a))return!0;return!1},y().on("request",function(b){var c,d,e,f,g;return f=b.type,e=b.request,g=b.url,I(g)?void 0:j.running||D.restartOnRequestAfter===!1&&"force"!==J(f)?void 0:(d=arguments,c=D.restartOnRequestAfter||0,"boolean"==typeof c&&(c=0),setTimeout(function(){var b,c,g,h,i,k;if(b="socket"===f?e.readyState<2:0<(h=e.readyState)&&4>h){for(j.restart(),i=j.sources,k=[],c=0,g=i.length;g>c;c++){if(K=i[c],K instanceof a){K.watch.apply(K,d);break}k.push(void 0)}return k}},c))}),a=function(){function a(){var a=this;this.elements=[],y().on("request",function(){return a.watch.apply(a,arguments)})}return a.prototype.watch=function(a){var b,c,d,e;return d=a.type,b=a.request,e=a.url,I(e)?void 0:(c="socket"===d?new n(b):new o(b),this.elements.push(c))},a}(),o=function(){function a(a){var b,c,d,e,f,g,h=this;if(this.progress=0,null!=window.ProgressEvent)for(c=null,a.addEventListener("progress",function(a){return h.progress=a.lengthComputable?100*a.loaded/a.total:h.progress+(100-h.progress)/2},!1),g=["load","abort","timeout","error"],d=0,e=g.length;e>d;d++)b=g[d],a.addEventListener(b,function(){return h.progress=100},!1);else f=a.onreadystatechange,a.onreadystatechange=function(){var b;return 0===(b=a.readyState)||4===b?h.progress=100:3===a.readyState&&(h.progress=50),"function"==typeof f?f.apply(null,arguments):void 0}}return a}(),n=function(){function a(a){var b,c,d,e,f=this;for(this.progress=0,e=["error","open"],c=0,d=e.length;d>c;c++)b=e[c],a.addEventListener(b,function(){return f.progress=100},!1)}return a}(),d=function(){function a(a){var b,c,d,f;for(null==a&&(a={}),this.elements=[],null==a.selectors&&(a.selectors=[]),f=a.selectors,c=0,d=f.length;d>c;c++)b=f[c],this.elements.push(new e(b))}return a}(),e=function(){function a(a){this.selector=a,this.progress=0,this.check()}return a.prototype.check=function(){var a=this;return document.querySelector(this.selector)?this.done():setTimeout(function(){return a.check()},D.elements.checkInterval)},a.prototype.done=function(){return this.progress=100},a}(),c=function(){function a(){var a,b,c=this;this.progress=null!=(b=this.states[document.readyState])?b:100,a=document.onreadystatechange,document.onreadystatechange=function(){return null!=c.states[document.readyState]&&(c.progress=c.states[document.readyState]),"function"==typeof a?a.apply(null,arguments):void 0}}return a.prototype.states={loading:0,interactive:50,complete:100},a}(),f=function(){function a(){var a,b,c,d,e,f=this;this.progress=0,a=0,e=[],d=0,c=C(),b=setInterval(function(){var g;return g=C()-c-50,c=C(),e.push(g),e.length>D.eventLag.sampleCount&&e.shift(),a=q(e),++d>=D.eventLag.minSamples&&a=100&&(this.done=!0),b===this.last?this.sinceLastUpdate+=a:(this.sinceLastUpdate&&(this.rate=(b-this.last)/this.sinceLastUpdate),this.catchup=(b-this.progress)/D.catchupTime,this.sinceLastUpdate=0,this.last=b),b>this.progress&&(this.progress+=this.catchup*a),c=1-Math.pow(this.progress/100,D.easeFactor),this.progress+=c*this.rate*a,this.progress=Math.min(this.lastProgress+D.maxProgressPerFrame,this.progress),this.progress=Math.max(0,this.progress),this.progress=Math.min(100,this.progress),this.lastProgress=this.progress,this.progress},a}(),L=null,H=null,r=null,M=null,p=null,s=null,j.running=!1,z=function(){return D.restartOnPushState?j.restart():void 0},null!=window.history.pushState&&(T=window.history.pushState,window.history.pushState=function(){return z(),T.apply(window.history,arguments)}),null!=window.history.replaceState&&(W=window.history.replaceState,window.history.replaceState=function(){return z(),W.apply(window.history,arguments)}),l={ajax:a,elements:d,document:c,eventLag:f},(B=function(){var a,c,d,e,f,g,h,i;for(j.sources=L=[],g=["ajax","elements","document","eventLag"],c=0,e=g.length;e>c;c++)a=g[c],D[a]!==!1&&L.push(new l[a](D[a]));for(i=null!=(h=D.extraSources)?h:[],d=0,f=i.length;f>d;d++)K=i[d],L.push(new K(D));return j.bar=r=new b,H=[],M=new m})(),j.stop=function(){return j.trigger("stop"),j.running=!1,r.destroy(),s=!0,null!=p&&("function"==typeof t&&t(p),p=null),B()},j.restart=function(){return j.trigger("restart"),j.stop(),j.start()},j.go=function(){var a;return j.running=!0,r.render(),a=C(),s=!1,p=G(function(b,c){var d,e,f,g,h,i,k,l,n,o,p,q,t,u,v,w;for(l=100-r.progress,e=p=0,f=!0,i=q=0,u=L.length;u>q;i=++q)for(K=L[i],o=null!=H[i]?H[i]:H[i]=[],h=null!=(w=K.elements)?w:[K],k=t=0,v=h.length;v>t;k=++t)g=h[k],n=null!=o[k]?o[k]:o[k]=new m(g),f&=n.done,n.done||(e++,p+=n.tick(b));return d=p/e,r.update(M.tick(b,d)),r.done()||f||s?(r.update(100),j.trigger("done"),setTimeout(function(){return r.finish(),j.running=!1,j.trigger("hide")},Math.max(D.ghostTime,Math.max(D.minTime-(C()-a),0)))):c()})},j.start=function(a){v(D,a),j.running=!0;try{r.render()}catch(b){i=b}return document.querySelector(".pace")?(j.trigger("start"),j.go()):setTimeout(j.start,50)},"function"==typeof define&&define.amd?define(function(){return j}):"object"==typeof exports?module.exports=j:D.startOnPageLoad&&j.start()}).call(this); -------------------------------------------------------------------------------- /flask_restaction/docs/docs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | $(title) 9 | 10 | 11 | 12 | 13 | 14 | 15 |
    16 | 43 |
    44 |
    45 | 46 |
    47 |
    48 |

    Auth

    49 |
    
     50 |                     
    51 |

    Shared

    52 |
    53 |

    @{{name}}

    54 |
    
     55 |                         
    56 |
    57 |

    Error

    58 |
    No Error Info
    59 |
    60 |
    61 |

    Meta

    62 |
    
     63 |                 
    64 | 79 |
    80 |

    {{resourceName}}

    81 |

    82 |
    83 |

    Shared

    84 |
    85 |

    @{{name}}

    86 |
    
     87 |                         
    88 |
    89 |
    90 |

    91 | {{action}}: 92 | {{toUrl(resourceName,action)}} 93 |

    94 |

    95 |

    Input

    96 |
    No Input Schema
    97 |

    Output

    98 |
    No Output Schema
    99 |

    Error

    100 |
    No Error Info
    101 |
    102 |
    103 |
    104 |
    105 |
    106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /flask_restaction/docs/docs.js: -------------------------------------------------------------------------------- 1 | function parseMeta(meta) { 2 | let basic = {} 3 | let roles = null 4 | let resources = {} 5 | for (let k in meta) { 6 | if (k.slice(0, 1) == '$') { 7 | if (k != "$roles") { 8 | basic[k] = meta[k] 9 | } else { 10 | roles = meta[k] 11 | } 12 | } else { 13 | resources[k] = meta[k] 14 | } 15 | } 16 | return { basic, roles, resources } 17 | } 18 | 19 | function isAllow(role, resource, action) { 20 | if (role[resource]) { 21 | if (role[resource].indexOf(action) >= 0) { 22 | return true 23 | } 24 | } 25 | return false 26 | } 27 | 28 | function strm(value) { 29 | // 去除字符串开头的 '#' 和 空白 30 | if (!value) { 31 | return value 32 | } else { 33 | return value.replace(/^[#\s]+/, '') 34 | } 35 | } 36 | 37 | function parseRole(role, resources) { 38 | let result = {} 39 | for (let resource in resources) { 40 | result[resource] = {} 41 | for (let action in resources[resource]) { 42 | if (isSpecial(action)) { 43 | continue 44 | } 45 | result[resource][action] = { 46 | desc: strm(resources[resource][action].$desc), 47 | allow: isAllow(role, resource, action) 48 | } 49 | } 50 | } 51 | return result 52 | } 53 | 54 | function isEmpty(value) { 55 | return !value || Object.keys(value).length === 0 56 | } 57 | 58 | function isSpecial(value) { 59 | return !value || value.slice(0, 1) == '$' 60 | } 61 | 62 | function route(app) { 63 | // 根据location.hash值显示对应的页面 64 | if (location.hash) { 65 | let state = location.hash.slice(1).split('.') 66 | if (state.length == 1) { 67 | if (state[0] == 'desc') { 68 | return app.showBasic() 69 | } else if (state[0] == 'meta') { 70 | return app.showMeta() 71 | } 72 | } else if (state.length == 2) { 73 | if (state[0] == 'roles') { 74 | if (state[1] in app.roles) { 75 | return app.showRole(state[1]) 76 | } 77 | } else if (state[0] == 'res') { 78 | if (state[1] in app.resources) { 79 | return app.showResource(state[1]) 80 | } 81 | } 82 | } else if (state.length == 3) { 83 | if (state[0] == 'res') { 84 | if (state[1] in app.resources) { 85 | return app.showResource(state[1]) 86 | } 87 | } 88 | } 89 | } 90 | app.showBasic() 91 | } 92 | 93 | let app = new Vue({ 94 | el: '#app', 95 | data: { 96 | meta: null, 97 | metaText: null, 98 | basic: null, 99 | roles: null, 100 | resources: null, 101 | view: null, 102 | role: null, 103 | roleName: null, 104 | resource: null, 105 | resourceName: null, 106 | sidebar: true, 107 | isSpecial, 108 | isEmpty 109 | }, 110 | methods: { 111 | showMeta() { 112 | this.view = 'meta' 113 | }, 114 | showBasic() { 115 | this.view = 'basic' 116 | }, 117 | showRole(name) { 118 | if (name in this.roles) { 119 | this.roleName = name 120 | this.role = parseRole(this.roles[name], this.resources) 121 | this.view = 'role' 122 | } 123 | }, 124 | showResource(name) { 125 | if (name in this.resources) { 126 | this.resourceName = name 127 | this.resource = this.resources[name] 128 | this.view = 'resource' 129 | } 130 | }, 131 | toggleSidebar() { 132 | if (this.sidebar === "none") { 133 | this.sidebar = "block" 134 | } else { 135 | this.sidebar = "none" 136 | } 137 | }, 138 | hideSidebar() { 139 | if (window.innerWidth < 768) { 140 | this.sidebar = "none" 141 | } 142 | }, 143 | toUrl(resource, action) { 144 | let prefix = this.meta.basic.$url_prefix || '' 145 | //Convert resource.action to "METHOD url", method is UpperCase 146 | let i = action.indexOf("_") 147 | if (i < 0) { 148 | return `${action.toUpperCase()} ${prefix}/${resource}` 149 | } else { 150 | return `${action.slice(0, i).toUpperCase()} ${prefix}/${action.slice(i+1)}` 151 | } 152 | } 153 | }, 154 | created: function() { 155 | let metaText = document.getElementById('meta-text').value 156 | let meta = parseMeta(JSON.parse(metaText)) 157 | this.metaText = metaText 158 | this.meta = meta 159 | this.basic = meta.basic 160 | this.roles = meta.roles 161 | this.resources = meta.resources 162 | }, 163 | mounted: function() { 164 | route(this) 165 | }, 166 | directives: { 167 | marked: function(el, binding) { 168 | if (binding.value !== undefined) { 169 | el.innerHTML = marked(binding.value) 170 | } 171 | }, 172 | highlight: function(el, binding) { 173 | if (binding.value !== undefined) { 174 | let value = null 175 | if (typeof(binding.value) === "string") { 176 | value = binding.value 177 | } else { 178 | value = JSON.stringify(binding.value, null, 4) 179 | } 180 | el.innerHTML = hljs.highlight("json", value, true).value 181 | } 182 | } 183 | } 184 | }) 185 | 186 | window.app = app 187 | -------------------------------------------------------------------------------- /flask_restaction/docs/docs.less: -------------------------------------------------------------------------------- 1 | @import "normalize.less"; 2 | @import "variables.less"; 3 | @import "resets.less"; 4 | @import "iconfont.less"; 5 | @import "pace.less"; 6 | @import "highlight.less"; 7 | 8 | 9 | .truncate(@width) { 10 | max-width: @width; 11 | white-space: nowrap; 12 | overflow: hidden; 13 | text-overflow: ellipsis; 14 | } 15 | 16 | .container { 17 | position: relative; 18 | width: 100%; 19 | overflow: hidden; 20 | } 21 | 22 | .sidebar { 23 | position: fixed; 24 | top: 0; 25 | bottom: 0; 26 | left: 0; 27 | width: 260px; 28 | padding: @width-gap; 29 | overflow-x: hidden; 30 | overflow-y: auto; 31 | z-index: 10; 32 | background-color: @color-background-dark; 33 | color: @color-text-light; 34 | 35 | ul,li { 36 | margin: 0; 37 | } 38 | ul { 39 | padding-left: @width-gap*1.5; 40 | } 41 | 42 | a { 43 | color: @color-text-light; 44 | } 45 | span{ 46 | cursor: default; 47 | } 48 | } 49 | 50 | .content { 51 | margin-left: 260px; 52 | padding: 0 @width-gap*4 @width-gap*2; 53 | } 54 | 55 | .action-url { 56 | font-size: 32px; 57 | color: @color-text-dark; 58 | } 59 | 60 | @media (min-width: @width-breakpoint) { 61 | .content-wrapper { 62 | max-width: 80%; 63 | margin: 0 auto; 64 | } 65 | } 66 | 67 | .toggle-sidebar { 68 | position: fixed; 69 | top: 0; 70 | right: 4px; 71 | color: @color-text-gray; 72 | &:hover { 73 | color: @color-text-dark; 74 | cursor: pointer; 75 | } 76 | i { 77 | font-size: 32px; 78 | } 79 | } 80 | 81 | .show-on-mobile { 82 | display: none; 83 | } 84 | 85 | @media (max-width: @width-breakpoint) { 86 | .container{ 87 | padding: @padding-small; 88 | } 89 | .sidebar { 90 | display: none; 91 | } 92 | .content { 93 | margin-left: 0; 94 | padding: 8px; 95 | } 96 | .show-on-mobile{ 97 | display: block; 98 | } 99 | .hide-on-mobile{ 100 | display: none; 101 | } 102 | } 103 | 104 | .section-role { 105 | overflow-x: auto; 106 | font-size: @font-size-small; 107 | 108 | table { 109 | border-collapse: collapse; 110 | margin-top:2*@width-gap; 111 | } 112 | td { 113 | border: 1px solid #ccc; 114 | padding: @width-gap; 115 | } 116 | .role-resource{ 117 | font-weight: bold; 118 | color: @color-text-light; 119 | background-color: @color-background-dark; 120 | } 121 | .role-item{ 122 | overflow: hidden; 123 | } 124 | .role-item-desc{ 125 | .truncate(200px) 126 | } 127 | .role-item-title{ 128 | .icon-allow{ 129 | color: #5cb85c; 130 | } 131 | .icon-block{ 132 | color: @color-text-gray; 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /flask_restaction/docs/gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp' 2 | import less from 'gulp-less' 3 | import rename from 'gulp-rename' 4 | import autoprefixer from 'gulp-autoprefixer' 5 | import webpack from 'webpack' 6 | import webpackStream from 'webpack-stream' 7 | import cleanCss from 'gulp-clean-css' 8 | 9 | 10 | let webpackModule = { 11 | loaders: [{ 12 | test: /\.js$/, 13 | loader: 'babel', 14 | exclude: /node_modules/ 15 | }] 16 | } 17 | let webpackDefinePlugin = new webpack.DefinePlugin({ 18 | 'process.env': { 19 | 'NODE_ENV': JSON.stringify('production') 20 | } 21 | }) 22 | let webpackUglifyJsPlugin = new webpack.optimize.UglifyJsPlugin({ 23 | compress: { 24 | warnings: false 25 | } 26 | }) 27 | 28 | gulp.task('build:js', () => { 29 | return gulp.src('docs.js') 30 | .pipe(webpackStream({ 31 | entry: './docs.js', 32 | output: { 33 | filename: 'docs.js' 34 | }, 35 | module: webpackModule, 36 | plugins: [webpackDefinePlugin] 37 | })) 38 | .pipe(gulp.dest('dist')) 39 | }) 40 | gulp.task('build:js-min', () => { 41 | return gulp.src('docs.js') 42 | .pipe(webpackStream({ 43 | entry: './docs.js', 44 | output: { 45 | filename: 'docs.min.js' 46 | }, 47 | module: webpackModule, 48 | plugins: [webpackDefinePlugin, webpackUglifyJsPlugin] 49 | })) 50 | .pipe(gulp.dest('dist')) 51 | }) 52 | gulp.task('build:css', () => { 53 | return gulp.src('docs.less') 54 | .pipe(less({ 55 | paths: ['./'] 56 | })) 57 | .pipe(autoprefixer()) 58 | .pipe(gulp.dest('dist')) 59 | .pipe(cleanCss()) 60 | .pipe(rename('docs.min.css')) 61 | .pipe(gulp.dest('dist')) 62 | }) 63 | 64 | gulp.task('build', ['build:js', 'build:css', 'build:js-min']) 65 | gulp.task('default', ['build']) 66 | gulp.task('watch', ['build'], () => { 67 | gulp.watch('*.*', ['build']) 68 | }) 69 | -------------------------------------------------------------------------------- /flask_restaction/docs/highlight.less: -------------------------------------------------------------------------------- 1 | /** 2 | * GitHub Gist Theme 3 | * Author : Louis Barranqueiro - https://github.com/LouisBarranqueiro 4 | */ 5 | 6 | .hljs { 7 | display: block; 8 | background-color: transparent; 9 | padding: 0.5em; 10 | color: #333333; 11 | overflow-x: auto; 12 | } 13 | 14 | .hljs-comment, 15 | .hljs-meta { 16 | color: #969896; 17 | } 18 | 19 | .hljs-string, 20 | .hljs-variable, 21 | .hljs-template-variable, 22 | .hljs-strong, 23 | .hljs-emphasis, 24 | .hljs-quote { 25 | color: #df5000; 26 | } 27 | 28 | .hljs-keyword, 29 | .hljs-selector-tag, 30 | .hljs-type { 31 | color: #a71d5d; 32 | } 33 | 34 | .hljs-literal, 35 | .hljs-symbol, 36 | .hljs-bullet, 37 | .hljs-attribute { 38 | color: #0086b3; 39 | } 40 | 41 | .hljs-section, 42 | .hljs-name { 43 | color: #63a35c; 44 | } 45 | 46 | .hljs-tag { 47 | color: #333333; 48 | } 49 | 50 | .hljs-title, 51 | .hljs-attr, 52 | .hljs-selector-id, 53 | .hljs-selector-class, 54 | .hljs-selector-attr, 55 | .hljs-selector-pseudo { 56 | color: #795da3; 57 | } 58 | 59 | .hljs-addition { 60 | color: #55a532; 61 | background-color: #eaffea; 62 | } 63 | 64 | .hljs-deletion { 65 | color: #bd2c00; 66 | background-color: #ffecec; 67 | } 68 | 69 | .hljs-link { 70 | text-decoration: underline; 71 | } 72 | -------------------------------------------------------------------------------- /flask_restaction/docs/iconfont.less: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "iconfont"; 3 | src: url('?f=iconfont.eot'); /* IE9*/ 4 | src: url('?f=iconfont.ttf') format('truetype'); 5 | } 6 | 7 | .iconfont { 8 | font-family:"iconfont" !important; 9 | font-size:@font-size; 10 | font-style:normal; 11 | -webkit-font-smoothing: antialiased; 12 | -webkit-text-stroke-width: 0.2px; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | .icon-index:before { content: "\f0006"; } 16 | .icon-allow:before { content: "\e644"; } 17 | .icon-block:before { content: "\e62e"; } 18 | -------------------------------------------------------------------------------- /flask_restaction/docs/normalize.less: -------------------------------------------------------------------------------- 1 | /*! normalize.css v4.1.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /** 4 | * 1. Change the default font family in all browsers (opinionated). 5 | * 2. Correct the line height in all browsers. 6 | * 3. Prevent adjustments of font size after orientation changes in IE and iOS. 7 | */ 8 | 9 | html { 10 | font-family: sans-serif; /* 1 */ 11 | line-height: 1.15; /* 2 */ 12 | -ms-text-size-adjust: 100%; /* 3 */ 13 | -webkit-text-size-adjust: 100%; /* 3 */ 14 | } 15 | 16 | /** 17 | * Remove the margin in all browsers (opinionated). 18 | */ 19 | 20 | body { 21 | margin: 0; 22 | } 23 | 24 | /* HTML5 display definitions 25 | ========================================================================== */ 26 | 27 | /** 28 | * Add the correct display in IE 9-. 29 | * 1. Add the correct display in Edge, IE, and Firefox. 30 | * 2. Add the correct display in IE. 31 | */ 32 | 33 | article, 34 | aside, 35 | details, /* 1 */ 36 | figcaption, 37 | figure, 38 | footer, 39 | header, 40 | main, /* 2 */ 41 | menu, 42 | nav, 43 | section, 44 | summary { /* 1 */ 45 | display: block; 46 | } 47 | 48 | /** 49 | * Add the correct display in IE 9-. 50 | */ 51 | 52 | audio, 53 | canvas, 54 | progress, 55 | video { 56 | display: inline-block; 57 | } 58 | 59 | /** 60 | * Add the correct display in iOS 4-7. 61 | */ 62 | 63 | audio:not([controls]) { 64 | display: none; 65 | height: 0; 66 | } 67 | 68 | /** 69 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 70 | */ 71 | 72 | progress { 73 | vertical-align: baseline; 74 | } 75 | 76 | /** 77 | * Add the correct display in IE 10-. 78 | * 1. Add the correct display in IE. 79 | */ 80 | 81 | template, /* 1 */ 82 | [hidden] { 83 | display: none; 84 | } 85 | 86 | /* Links 87 | ========================================================================== */ 88 | 89 | /** 90 | * 1. Remove the gray background on active links in IE 10. 91 | * 2. Remove gaps in links underline in iOS 8+ and Safari 8+. 92 | */ 93 | 94 | a { 95 | background-color: transparent; /* 1 */ 96 | -webkit-text-decoration-skip: objects; /* 2 */ 97 | } 98 | 99 | /** 100 | * Remove the outline on focused links when they are also active or hovered 101 | * in all browsers (opinionated). 102 | */ 103 | 104 | a:active, 105 | a:hover { 106 | outline-width: 0; 107 | } 108 | 109 | /* Text-level semantics 110 | ========================================================================== */ 111 | 112 | /** 113 | * 1. Remove the bottom border in Firefox 39-. 114 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 115 | */ 116 | 117 | abbr[title] { 118 | border-bottom: none; /* 1 */ 119 | text-decoration: underline; /* 2 */ 120 | text-decoration: underline dotted; /* 2 */ 121 | } 122 | 123 | /** 124 | * Prevent the duplicate application of `bolder` by the next rule in Safari 6. 125 | */ 126 | 127 | b, 128 | strong { 129 | font-weight: inherit; 130 | } 131 | 132 | /** 133 | * Add the correct font weight in Chrome, Edge, and Safari. 134 | */ 135 | 136 | b, 137 | strong { 138 | font-weight: bolder; 139 | } 140 | 141 | /** 142 | * Add the correct font style in Android 4.3-. 143 | */ 144 | 145 | dfn { 146 | font-style: italic; 147 | } 148 | 149 | /** 150 | * Correct the font size and margin on `h1` elements within `section` and 151 | * `article` contexts in Chrome, Firefox, and Safari. 152 | */ 153 | 154 | h1 { 155 | font-size: 2em; 156 | margin: 0.67em 0; 157 | } 158 | 159 | /** 160 | * Add the correct background and color in IE 9-. 161 | */ 162 | 163 | mark { 164 | background-color: #ff0; 165 | color: #000; 166 | } 167 | 168 | /** 169 | * Add the correct font size in all browsers. 170 | */ 171 | 172 | small { 173 | font-size: 80%; 174 | } 175 | 176 | /** 177 | * Prevent `sub` and `sup` elements from affecting the line height in 178 | * all browsers. 179 | */ 180 | 181 | sub, 182 | sup { 183 | font-size: 75%; 184 | line-height: 0; 185 | position: relative; 186 | vertical-align: baseline; 187 | } 188 | 189 | sub { 190 | bottom: -0.25em; 191 | } 192 | 193 | sup { 194 | top: -0.5em; 195 | } 196 | 197 | /* Embedded content 198 | ========================================================================== */ 199 | 200 | /** 201 | * Remove the border on images inside links in IE 10-. 202 | */ 203 | 204 | img { 205 | border-style: none; 206 | } 207 | 208 | /** 209 | * Hide the overflow in IE. 210 | */ 211 | 212 | svg:not(:root) { 213 | overflow: hidden; 214 | } 215 | 216 | /* Grouping content 217 | ========================================================================== */ 218 | 219 | /** 220 | * 1. Correct the inheritance and scaling of font size in all browsers. 221 | * 2. Correct the odd `em` font sizing in all browsers. 222 | */ 223 | 224 | code, 225 | kbd, 226 | pre, 227 | samp { 228 | font-family: monospace, monospace; /* 1 */ 229 | font-size: 1em; /* 2 */ 230 | } 231 | 232 | /** 233 | * Add the correct margin in IE 8. 234 | */ 235 | 236 | figure { 237 | margin: 1em 40px; 238 | } 239 | 240 | /** 241 | * 1. Add the correct box sizing in Firefox. 242 | * 2. Show the overflow in Edge and IE. 243 | */ 244 | 245 | hr { 246 | box-sizing: content-box; /* 1 */ 247 | height: 0; /* 1 */ 248 | overflow: visible; /* 2 */ 249 | } 250 | 251 | /* Forms 252 | ========================================================================== */ 253 | 254 | /** 255 | * 1. Change font properties to `inherit` in all browsers (opinionated). 256 | * 2. Remove the margin in Firefox and Safari. 257 | */ 258 | 259 | button, 260 | input, 261 | optgroup, 262 | select, 263 | textarea { 264 | font: inherit; /* 1 */ 265 | margin: 0; /* 2 */ 266 | } 267 | 268 | /** 269 | * Restore the font weight unset by the previous rule. 270 | */ 271 | 272 | optgroup { 273 | font-weight: bold; 274 | } 275 | 276 | /** 277 | * Show the overflow in IE. 278 | * 1. Show the overflow in Edge. 279 | */ 280 | 281 | button, 282 | input { /* 1 */ 283 | overflow: visible; 284 | } 285 | 286 | /** 287 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 288 | * 1. Remove the inheritance of text transform in Firefox. 289 | */ 290 | 291 | button, 292 | select { /* 1 */ 293 | text-transform: none; 294 | } 295 | 296 | /** 297 | * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` 298 | * controls in Android 4. 299 | * 2. Correct the inability to style clickable types in iOS and Safari. 300 | */ 301 | 302 | button, 303 | html [type="button"], /* 1 */ 304 | [type="reset"], 305 | [type="submit"] { 306 | -webkit-appearance: button; /* 2 */ 307 | } 308 | 309 | /** 310 | * Remove the inner border and padding in Firefox. 311 | */ 312 | 313 | button::-moz-focus-inner, 314 | [type="button"]::-moz-focus-inner, 315 | [type="reset"]::-moz-focus-inner, 316 | [type="submit"]::-moz-focus-inner { 317 | border-style: none; 318 | padding: 0; 319 | } 320 | 321 | /** 322 | * Restore the focus styles unset by the previous rule. 323 | */ 324 | 325 | button:-moz-focusring, 326 | [type="button"]:-moz-focusring, 327 | [type="reset"]:-moz-focusring, 328 | [type="submit"]:-moz-focusring { 329 | outline: 1px dotted ButtonText; 330 | } 331 | 332 | /** 333 | * Change the border, margin, and padding in all browsers (opinionated). 334 | */ 335 | 336 | fieldset { 337 | border: 1px solid #c0c0c0; 338 | margin: 0 2px; 339 | padding: 0.35em 0.625em 0.75em; 340 | } 341 | 342 | /** 343 | * 1. Correct the text wrapping in Edge and IE. 344 | * 2. Correct the color inheritance from `fieldset` elements in IE. 345 | * 3. Remove the padding so developers are not caught out when they zero out 346 | * `fieldset` elements in all browsers. 347 | */ 348 | 349 | legend { 350 | box-sizing: border-box; /* 1 */ 351 | color: inherit; /* 2 */ 352 | display: table; /* 1 */ 353 | max-width: 100%; /* 1 */ 354 | padding: 0; /* 3 */ 355 | white-space: normal; /* 1 */ 356 | } 357 | 358 | /** 359 | * Remove the default vertical scrollbar in IE. 360 | */ 361 | 362 | textarea { 363 | overflow: auto; 364 | } 365 | 366 | /** 367 | * 1. Add the correct box sizing in IE 10-. 368 | * 2. Remove the padding in IE 10-. 369 | */ 370 | 371 | [type="checkbox"], 372 | [type="radio"] { 373 | box-sizing: border-box; /* 1 */ 374 | padding: 0; /* 2 */ 375 | } 376 | 377 | /** 378 | * Correct the cursor style of increment and decrement buttons in Chrome. 379 | */ 380 | 381 | [type="number"]::-webkit-inner-spin-button, 382 | [type="number"]::-webkit-outer-spin-button { 383 | height: auto; 384 | } 385 | 386 | /** 387 | * 1. Correct the odd appearance in Chrome and Safari. 388 | * 2. Correct the outline style in Safari. 389 | */ 390 | 391 | [type="search"] { 392 | -webkit-appearance: textfield; /* 1 */ 393 | outline-offset: -2px; /* 2 */ 394 | } 395 | 396 | /** 397 | * Remove the inner padding and cancel buttons in Chrome and Safari on OS X. 398 | */ 399 | 400 | [type="search"]::-webkit-search-cancel-button, 401 | [type="search"]::-webkit-search-decoration { 402 | -webkit-appearance: none; 403 | } 404 | 405 | /** 406 | * Correct the text style of placeholders in Chrome, Edge, and Safari. 407 | */ 408 | 409 | ::-webkit-input-placeholder { 410 | color: inherit; 411 | opacity: 0.54; 412 | } 413 | 414 | /** 415 | * 1. Correct the inability to style clickable types in iOS and Safari. 416 | * 2. Change font properties to `inherit` in Safari. 417 | */ 418 | 419 | ::-webkit-file-upload-button { 420 | -webkit-appearance: button; /* 1 */ 421 | font: inherit; /* 2 */ 422 | } 423 | -------------------------------------------------------------------------------- /flask_restaction/docs/pace.less: -------------------------------------------------------------------------------- 1 | .pace { 2 | user-select: none; 3 | pointer-events: none; 4 | } 5 | 6 | .pace-inactive { 7 | display: none; 8 | } 9 | 10 | .pace .pace-progress { 11 | position: fixed; 12 | z-index: 2000; 13 | top: 0; 14 | right: 100%; 15 | width: 100%; 16 | height: 2px; 17 | background: #29d; 18 | } 19 | -------------------------------------------------------------------------------- /flask_restaction/docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flask-resttion-docs", 3 | "version": "0.0.1", 4 | "description": "flask-resttion docs", 5 | "main": "dist/docs.html", 6 | "scripts": { 7 | "build": "gulp build", 8 | "resjs": "resjs http://127.0.0.1:5000/docs dist/res.js --prefix /docs", 9 | "resjs-min": "resjs http://127.0.0.1:5000/docs dist/res.min.js --prefix /docs --min" 10 | }, 11 | "author": "guyskk", 12 | "license": "MIT", 13 | "devDependencies": { 14 | "babel-core": "^6.14.0", 15 | "babel-loader": "^6.2.5", 16 | "babel-plugin-transform-runtime": "^6.12.0", 17 | "babel-preset-es2015": "^6.14.0", 18 | "gulp": "^3.9.1", 19 | "gulp-autoprefixer": "^3.1.1", 20 | "gulp-clean-css": "^2.0.12", 21 | "gulp-less": "^3.1.0", 22 | "gulp-rename": "^1.2.2", 23 | "gulp-uglify": "^2.0.0", 24 | "resjs": "0.0.3", 25 | "webpack-stream": "^3.2.0" 26 | }, 27 | "dependencies": { 28 | "babel-runtime": "^6.11.6" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /flask_restaction/docs/resets.less: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | text-size-adjust: 100%; 4 | } 5 | 6 | *, 7 | *::after, 8 | *::before { 9 | box-sizing: inherit; 10 | } 11 | 12 | body { 13 | font-family: @font-family; 14 | font-size: @font-size; 15 | line-height: @line-height; 16 | min-width: 320px; 17 | color: @color-text-dark; 18 | background-color: @color-background-light; 19 | } 20 | 21 | a { 22 | text-decoration: none; 23 | color: @color-link; 24 | 25 | &:hover { 26 | cursor: pointer; 27 | color: darken(@color-link, 15%); 28 | } 29 | 30 | &:active, 31 | &:hover { 32 | text-decoration: underline; 33 | } 34 | } 35 | 36 | code, pre { 37 | background-color: @color-background-gray; 38 | } 39 | 40 | code { 41 | font-family: @font-family-monospace; 42 | padding: @padding-small; 43 | } 44 | 45 | pre { 46 | font-size: @font-size-small; 47 | line-height: @line-height-small; 48 | overflow: auto; 49 | padding: @width-gap; 50 | border-radius: 2px; 51 | 52 | code { 53 | background-color: transparent; 54 | padding: 0; 55 | 56 | } 57 | } 58 | 59 | blockquote { 60 | padding-left: @width-gap; 61 | border-left: @width-gap / 3 solid @color-text-gray; 62 | } 63 | 64 | @media (max-width: @width-breakpoint) { 65 | pre { 66 | padding: @padding-small; 67 | } 68 | blockquote { 69 | margin-right: 0; 70 | margin-left: 0; 71 | } 72 | } 73 | 74 | img { 75 | max-width: 100%; 76 | vertical-align: middle; 77 | } 78 | 79 | h1, h2, h3, h4, h5, h6 { 80 | margin-top: 2 * @width-gap; 81 | margin-bottom: @width-gap; 82 | } 83 | 84 | h1 { font-size: floor(2.6 * @font-size); } 85 | h2 { font-size: floor(2.15 * @font-size); } 86 | h3 { font-size: ceil(1.7 * @font-size); } 87 | h4 { font-size: ceil(1.25 * @font-size); } 88 | h5 { font-size: ceil(1.0 * @font-size); } 89 | h6 { font-size: ceil(0.85 * @font-size); } 90 | 91 | p, 92 | blockquote, 93 | table, 94 | hr, 95 | dl, 96 | ul, 97 | ol, 98 | pre, 99 | address, 100 | figure { 101 | margin-top: @width-gap; 102 | margin-bottom: @width-gap / 2; 103 | } 104 | -------------------------------------------------------------------------------- /flask_restaction/docs/variables.less: -------------------------------------------------------------------------------- 1 | @color-background-light: #fff; 2 | @color-background-gray: #fafafa; 3 | @color-background-dark: #00897b; 4 | 5 | @color-text-dark: mix(#000, #fff, 87%); 6 | @color-text-gray: mix(#000, #fff, 54%); 7 | @color-text-light: #fff; 8 | 9 | @color-link: #4078c0; 10 | 11 | @width-breakpoint: 768px; 12 | @width-container: 970px; 13 | @width-gap: 15px; 14 | 15 | @line-height: 1.5; 16 | @font-size: 18px; 17 | @line-height-small: 1.2; 18 | @font-size-small: 15px; 19 | @padding-small: 2px 3px; 20 | 21 | @font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 22 | @font-family-monospace: Consolas, "Liberation Mono", Menlo, Courier, monospace; 23 | -------------------------------------------------------------------------------- /flask_restaction/exporters.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from flask import current_app 4 | 5 | exporters = {} 6 | 7 | 8 | def exporter(mediatype): 9 | """ 10 | Decorater for register exporter 11 | 12 | Args: 13 | mediatype: mediatype, eg: `application/json` 14 | """ 15 | def wraper(fn): 16 | register_exporter(mediatype, fn) 17 | return fn 18 | return wraper 19 | 20 | 21 | def register_exporter(mediatype, fn): 22 | """ 23 | Register exporter 24 | 25 | Args: 26 | mediatype: mediatype, eg: `application/json` 27 | fn: exporter function 28 | """ 29 | exporters[mediatype] = fn 30 | 31 | 32 | @exporter('application/json') 33 | def export_json(data, status, headers): 34 | """ 35 | Creates a JSON response 36 | 37 | JSON content is encoded by utf-8, not unicode escape. 38 | 39 | Args: 40 | data: any type object that can dump to json 41 | status (int): http status code 42 | headers (dict): http headers 43 | """ 44 | dumped = json.dumps(data, ensure_ascii=False) 45 | resp = current_app.response_class( 46 | dumped, status=status, headers=headers, 47 | content_type='application/json; charset=utf-8') 48 | return resp 49 | -------------------------------------------------------------------------------- /flask_restaction/res.py: -------------------------------------------------------------------------------- 1 | from json import dumps, loads 2 | 3 | import flask 4 | import requests 5 | 6 | 7 | def resp_json(resp): 8 | """ 9 | Get JSON from response if success, raise requests.HTTPError otherwise. 10 | 11 | Args: 12 | resp: requests.Response or flask.Response 13 | Retuens: 14 | JSON value 15 | """ 16 | if isinstance(resp, flask.Response): 17 | if 400 <= resp.status_code < 600: 18 | msg = resp.status 19 | try: 20 | result = loads(resp.data.decode("utf-8")) 21 | if isinstance(result, str): 22 | msg = "%s, %s" % (resp.status, result) 23 | else: 24 | msg = "%s %s, %s" % ( 25 | resp.status_code, result["error"], result["message"]) 26 | except Exception: 27 | pass 28 | raise requests.HTTPError(msg, response=resp) 29 | else: 30 | return loads(resp.data.decode("utf-8")) 31 | else: 32 | try: 33 | resp.raise_for_status() 34 | except requests.HTTPError as ex: 35 | # the response may contains {"error": "", "message": ""} 36 | # append error and message to exception if possible 37 | try: 38 | result = resp.json() 39 | ex.args += (result["error"], result["message"]) 40 | except (ValueError, KeyError): 41 | pass 42 | raise 43 | return resp.json() 44 | 45 | 46 | def res_to_url(resource, action): 47 | """Convert resource.action to (url, HTTP_METHOD)""" 48 | i = action.find("_") 49 | if i < 0: 50 | url = "/" + resource 51 | httpmethod = action 52 | else: 53 | url = "/%s/%s" % (resource, action[i + 1:]) 54 | httpmethod = action[:i] 55 | return url, httpmethod.upper() 56 | 57 | 58 | class TestClientSession: 59 | """ 60 | Wrapper Flask.test_client like requests.Session 61 | 62 | Attributes: 63 | headers: request headers 64 | """ 65 | 66 | def __init__(self, test_client): 67 | self.test_client = test_client 68 | self.headers = {} 69 | 70 | def request(self, method, url, params=None, data=None, 71 | headers=None, json=None): 72 | if headers is None: 73 | headers = self.headers 74 | else: 75 | headers.update(self.headers) 76 | params = { 77 | "path": url, 78 | "method": method, 79 | "query_string": params, 80 | "headers": headers, 81 | "follow_redirects": True 82 | } 83 | if json is not None: 84 | params["data"] = dumps(json, ensure_ascii=False) 85 | params["content_type"] = "application/json" 86 | with self.test_client() as c: 87 | resp = c.open(**params) 88 | return resp 89 | 90 | 91 | class Res: 92 | """ 93 | A tool for calling API 94 | 95 | Will keep a session and handle auth token automatic 96 | 97 | Usage: 98 | 99 | >>> res = Res(test_client=app.test_client) # used in testing 100 | >>> res = Res("http://127.0.0.1:5000") # request remote api 101 | >>> res.ajax("/hello") 102 | {'message': 'Hello world, Welcome to flask-restaction!'} 103 | >>> res.hello.get() 104 | {'message': 'Hello world, Welcome to flask-restaction!'} 105 | >>> res.hello.get({"name":"kk"}) 106 | {'message': 'Hello kk, Welcome to flask-restaction!'} 107 | >>> res.xxx.get() 108 | ... 109 | requests.exceptions.HTTPError: 110 | 404 Client Error: NOT FOUND for url: http://127.0.0.1:5000/xxx 111 | 112 | Args: 113 | url_prefix: url prefix of API 114 | auth_header: auth header name of API 115 | Attributes: 116 | url_prefix: url prefix 117 | auth_header: auth header 118 | session: requests.Session or TestClientSession 119 | """ 120 | 121 | def __init__(self, url_prefix="", test_client=None, 122 | auth_header="Authorization"): 123 | self.url_prefix = url_prefix 124 | self.auth_header = auth_header 125 | if test_client is None: 126 | self.session = requests.Session() 127 | else: 128 | self.session = TestClientSession(test_client) 129 | self.session.headers.update({'Accept': 'application/json'}) 130 | 131 | def ajax(self, url, method="GET", data=None, headers=None): 132 | """Send request""" 133 | params = { 134 | "method": method, 135 | "url": self.url_prefix + url, 136 | "headers": headers 137 | } 138 | if data is not None: 139 | if method in ["GET", "DELETE"]: 140 | params["params"] = data 141 | elif method in ["POST", "PUT", "PATCH"]: 142 | params["json"] = data 143 | resp = self.session.request(**params) 144 | if self.auth_header in resp.headers: 145 | self.session.headers[ 146 | self.auth_header] = resp.headers[self.auth_header] 147 | return resp_json(resp) 148 | 149 | def _request(self, resource, action, data=None, headers=None): 150 | """ 151 | Send request 152 | 153 | Args: 154 | resource: resource 155 | action: action 156 | data: string or object which can be json.dumps 157 | headers: http headers 158 | """ 159 | url, httpmethod = res_to_url(resource, action) 160 | return self.ajax(url, httpmethod, data, headers) 161 | 162 | def __getattr__(self, resource): 163 | return Resource(self, resource) 164 | 165 | 166 | class Resource: 167 | 168 | def __init__(self, res, resource): 169 | self._res = res 170 | self._resource = resource 171 | 172 | def __getattr__(self, action): 173 | return Action(self, action) 174 | 175 | 176 | class Action: 177 | 178 | def __init__(self, resource, action): 179 | self.res = resource._res 180 | self.resource = resource._resource 181 | self.action = action 182 | 183 | def __call__(self, data=None, headers=None): 184 | return self.res._request(self.resource, self.action, data, headers) 185 | -------------------------------------------------------------------------------- /flask_restaction/resjs/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"], 3 | "plugins": ["transform-runtime"] 4 | } 5 | -------------------------------------------------------------------------------- /flask_restaction/resjs/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "undef": true, 3 | "unused": true, 4 | "esversion": 6, 5 | "asi": true, 6 | "browser": true, 7 | "node": true, 8 | "devel": true, 9 | "predef": ["describe", "it", "assert", "res", "Vue"] 10 | } 11 | -------------------------------------------------------------------------------- /flask_restaction/resjs/.npmignore: -------------------------------------------------------------------------------- 1 | ** 2 | !dist 3 | -------------------------------------------------------------------------------- /flask_restaction/resjs/bin/resjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var resjs = require('../dist/index.js') 4 | require('commander') 5 | .version(require('../package').version) 6 | .description('generate res.js for browser or nodejs') 7 | .arguments('') 8 | .option('-d, --dest [dest]', 'dest path to save res.js') 9 | .option('-p, --prefix [prefix]', 'url prefix of generated res.js') 10 | .option('-n, --node [node]', 'generate res.js for nodejs, default for browser') 11 | .option('-r, --rn [rn]', 'generate res.js for react-native') 12 | .option('-m, --min [min]', 'minimize generated res.js, default not minimize') 13 | .action(function(url, options) { 14 | return resjs(url, options.dest, options.prefix, options.node, options.rn, options.min) 15 | }).parse(process.argv); 16 | -------------------------------------------------------------------------------- /flask_restaction/resjs/dist/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _slicedToArray2 = require('babel-runtime/helpers/slicedToArray'); 4 | 5 | var _slicedToArray3 = _interopRequireDefault(_slicedToArray2); 6 | 7 | var _promise = require('babel-runtime/core-js/promise'); 8 | 9 | var _promise2 = _interopRequireDefault(_promise); 10 | 11 | var _fs = require('fs'); 12 | 13 | var _fs2 = _interopRequireDefault(_fs); 14 | 15 | var _path = require('path'); 16 | 17 | var _superagent = require('superagent'); 18 | 19 | var _superagent2 = _interopRequireDefault(_superagent); 20 | 21 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 22 | 23 | function toUrl(resource, action) { 24 | //Convert resource.action to {url, method}, method is UpperCase 25 | var i = action.indexOf("_"); 26 | if (i < 0) { 27 | return { 28 | url: "/" + resource, 29 | method: action.toUpperCase() 30 | }; 31 | } else { 32 | return { 33 | url: '/' + resource + '/' + action.slice(i + 1), 34 | method: action.slice(0, i).toUpperCase() 35 | }; 36 | } 37 | } 38 | 39 | function readMeta(url) { 40 | return new _promise2.default(function (resolve, reject) { 41 | (0, _superagent2.default)('GET', url).set('Accept', 'application/json').end(function (error, response) { 42 | if (error) { 43 | reject(error); 44 | } else { 45 | resolve(response.body); 46 | } 47 | }); 48 | }); 49 | } 50 | 51 | function parseMeta(url) { 52 | // authHeader is LowerCase 53 | return readMeta(url).then(function (meta) { 54 | var authHeader = meta.$auth ? meta.$auth.header.toLowerCase() : null; 55 | var urlPrefix = meta.$url_prefix ? meta.$url_prefix : ''; 56 | var resources = {}; 57 | for (var k in meta) { 58 | if (k.slice(0, 1) != '$') { 59 | resources[k] = {}; 60 | for (var action in meta[k]) { 61 | if (action.slice(0, 1) != '$') { 62 | resources[k][action] = toUrl(k, action); 63 | } 64 | } 65 | } 66 | } 67 | return { authHeader: authHeader, urlPrefix: urlPrefix, resources: resources }; 68 | }); 69 | } 70 | 71 | function renderCore(meta) { 72 | /*Generate res.code.js*/ 73 | var code = ''; 74 | code += 'function(root, init) {\n'; 75 | code += ' var q = init(\'' + meta.authHeader + '\', \'' + meta.urlPrefix + '\');\n'; 76 | code += ' var r = null;\n'; 77 | for (var key in meta.resources) { 78 | code += ' r = root.' + key + ' = {};\n'; 79 | for (var action in meta.resources[key]) { 80 | var item = meta.resources[key][action]; 81 | code += ' r.' + action + ' = q(\'' + item.url + '\', \'' + item.method + '\');\n'; 82 | } 83 | } 84 | code += '}'; 85 | return code; 86 | } 87 | 88 | function parseResjs() { 89 | var node = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; 90 | var rn = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; 91 | var min = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; 92 | 93 | var filename = min ? 'res.web.min.js' : 'res.web.js'; 94 | if (node) { 95 | filename = 'res.node.js'; 96 | } 97 | if (rn) { 98 | filename = 'res.rn.js'; 99 | } 100 | filename = (0, _path.join)(__dirname, filename); 101 | return new _promise2.default(function (resolve, reject) { 102 | _fs2.default.readFile(filename, { encoding: 'utf-8' }, function (error, data) { 103 | if (error) { 104 | reject(error); 105 | } else { 106 | resolve(function (core) { 107 | return data.replace('"#res.core.js#"', core); 108 | }); 109 | } 110 | }); 111 | }); 112 | } 113 | 114 | function resjs(url) { 115 | var dest = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : './res.js'; 116 | var urlPrefix = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : undefined; 117 | var node = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : undefined; 118 | var rn = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : undefined; 119 | var min = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : undefined; 120 | 121 | return _promise2.default.all([parseMeta(url), parseResjs(node, rn, min)]).then(function (_ref) { 122 | var _ref2 = (0, _slicedToArray3.default)(_ref, 2), 123 | meta = _ref2[0], 124 | generate = _ref2[1]; 125 | 126 | if (urlPrefix) { 127 | meta.urlPrefix = urlPrefix; 128 | } 129 | var code = generate(renderCore(meta)); 130 | return new _promise2.default(function (resolve, reject) { 131 | _fs2.default.writeFile(dest, code, function (error) { 132 | if (error) { 133 | reject(error); 134 | } else { 135 | resolve('OK, saved in: ' + dest); 136 | } 137 | }); 138 | }); 139 | }).then(function (message) { 140 | console.log(message); 141 | }).catch(function (error) { 142 | console.log(error); 143 | }); 144 | } 145 | 146 | module.exports = resjs; -------------------------------------------------------------------------------- /flask_restaction/resjs/dist/res.node.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _promise = require('babel-runtime/core-js/promise'); 4 | 5 | var _promise2 = _interopRequireDefault(_promise); 6 | 7 | var _superagent = require('superagent'); 8 | 9 | var _superagent2 = _interopRequireDefault(_superagent); 10 | 11 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 12 | 13 | var base = { ajax: _superagent2.default }; 14 | 15 | function init(authHeader, urlPrefix) { 16 | base.config = { authHeader: authHeader, urlPrefix: urlPrefix }; 17 | if (typeof window === 'undefined' || typeof localStorage === 'undefined') { 18 | (function () { 19 | var token = null; 20 | base.clearToken = function () { 21 | token = null; 22 | }; 23 | base.setToken = function (token) { 24 | token = token; 25 | }; 26 | base.getToken = function () { 27 | return token; 28 | }; 29 | })(); 30 | } else { 31 | base.clearToken = function () { 32 | window.localStorage.removeItem('resjs-auth-token'); 33 | }; 34 | base.setToken = function (token) { 35 | window.localStorage['resjs-auth-token'] = token; 36 | }; 37 | base.getToken = function () { 38 | return window.localStorage['resjs-auth-token']; 39 | }; 40 | } 41 | return function (url, method) { 42 | return function (data) { 43 | var request = (0, _superagent2.default)(method, base.config.urlPrefix + url); 44 | request = request.set('accept', 'application/json'); 45 | if (base.config.authHeader) { 46 | var _token = base.getToken(); 47 | if (_token) { 48 | request = request.set(base.config.authHeader, _token); 49 | } 50 | } 51 | if (method == 'PUT' || method == 'POST' || method == 'PATCH') { 52 | request = request.send(data); 53 | } else { 54 | request = request.query(data); 55 | } 56 | return new _promise2.default(function (resolve, reject) { 57 | request.end(function (error, response) { 58 | if (error) { 59 | if (error.response) { 60 | // 4xx or 5xx response 61 | if (error.response.body) { 62 | reject(error.response.body); 63 | } else { 64 | reject(error.response); 65 | } 66 | } else { 67 | // Network failures, timeouts, and other errors 68 | reject(error); 69 | } 70 | } else { 71 | if (base.config.authHeader) { 72 | var _token2 = response.header[base.config.authHeader]; 73 | if (_token2) { 74 | base.setToken(_token2); 75 | } 76 | } 77 | if (response.body) { 78 | // JSON response 79 | resolve(response.body); 80 | } else { 81 | // unparsed response 82 | resolve(response); 83 | } 84 | } 85 | }); 86 | }); 87 | }; 88 | }; 89 | } 90 | 91 | var core = "#res.core.js#"; 92 | core(base, init); 93 | 94 | module.exports = base; -------------------------------------------------------------------------------- /flask_restaction/resjs/dist/res.rn.js: -------------------------------------------------------------------------------- 1 | import { 2 | AsyncStorage 3 | } from 'react-native' 4 | 5 | const base = { 6 | async clearToken () { 7 | await AsyncStorage.removeItem('resjs-auth-token') 8 | }, 9 | async setToken (token) { 10 | await AsyncStorage.setItem('resjs-auth-token', token) 11 | }, 12 | async getToken () { 13 | return await AsyncStorage.getItem('resjs-auth-token') 14 | } 15 | } 16 | 17 | function init (authHeader, urlPrefix) { 18 | let token = null 19 | base.config = { authHeader, urlPrefix } 20 | return function (url, method) { 21 | return async (data) => { 22 | let uri = base.config.urlPrefix + url 23 | const options = { 24 | method, 25 | headers: { 26 | Accept: 'application/json', 27 | 'Content-Type': 'application/json', 28 | From: 'USRN' 29 | } 30 | } 31 | if (base.config.authHeader) { 32 | let token = await base.getToken() 33 | if (token) { 34 | options.headers[base.config.authHeader] = token 35 | } 36 | } 37 | if (method == 'PUT' || method == 'POST' || method == 'PATCH') { 38 | options.body = JSON.stringify(data) 39 | } else { 40 | uri += '?' + getQueryString(data) 41 | } 42 | return new Promise(async (resolve, reject) => { 43 | const response = await fetch(uri, options) 44 | const json = await response.json() 45 | if (response.status !== 200) { 46 | reject(json) 47 | } else { 48 | const token = response.headers.get(base.config.authHeader) 49 | if (token) { 50 | base.setToken(token) 51 | } 52 | resolve(json) 53 | } 54 | }) 55 | } 56 | } 57 | } 58 | 59 | function getQueryString (data = {}) { 60 | const qs = Object.keys(data) 61 | .map(k => encodeURIComponent(k) + '=' + encodeURIComponent(data[k])) 62 | .join('&') 63 | return qs 64 | } 65 | 66 | let core = "#res.core.js#" 67 | core(base, init) 68 | 69 | module.exports = base 70 | -------------------------------------------------------------------------------- /flask_restaction/resjs/gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp' 2 | import rename from 'gulp-rename' 3 | import babel from 'gulp-babel' 4 | import webpack from 'webpack' 5 | import webpackStream from 'webpack-stream' 6 | 7 | let webpackModule = { 8 | loaders: [{ 9 | test: /\.js$/, 10 | loader: 'babel', 11 | exclude: /node_modules/ 12 | }] 13 | } 14 | let webpackDefinePlugin = new webpack.DefinePlugin({ 15 | 'process.env': { 16 | 'NODE_ENV': JSON.stringify('production') 17 | } 18 | }) 19 | let webpackUglifyJsPlugin = new webpack.optimize.UglifyJsPlugin({ 20 | compress: { 21 | warnings: false 22 | } 23 | }) 24 | 25 | gulp.task('build:res-web', () => { 26 | return gulp.src('res.base.js') 27 | .pipe(webpackStream({ 28 | entry: './res.base.js', 29 | output: { 30 | library: 'res', 31 | libraryTarget: 'umd', 32 | filename: 'res.web.js' 33 | }, 34 | module: webpackModule, 35 | plugins: [webpackDefinePlugin] 36 | })) 37 | .pipe(gulp.dest('./dist/')) 38 | }) 39 | 40 | gulp.task('build:res-web-min', () => { 41 | return gulp.src('res.base.js') 42 | .pipe(webpackStream({ 43 | entry: './res.base.js', 44 | output: { 45 | library: 'res', 46 | libraryTarget: 'umd', 47 | filename: 'res.web.min.js' 48 | }, 49 | module: webpackModule, 50 | plugins: [webpackDefinePlugin, webpackUglifyJsPlugin] 51 | })) 52 | .pipe(gulp.dest('./dist/')) 53 | }) 54 | 55 | gulp.task('build:res-node', () => { 56 | return gulp.src('res.base.js') 57 | .pipe(babel()) 58 | .pipe(rename('res.node.js')) 59 | .pipe(gulp.dest('./dist/')) 60 | }) 61 | 62 | gulp.task('build:res-rn', () => { 63 | return gulp.src('res.rn.js') 64 | .pipe(rename('res.rn.js')) 65 | .pipe(gulp.dest('./dist/')) 66 | }) 67 | 68 | gulp.task('build:index', () => { 69 | return gulp.src('index.js') 70 | .pipe(babel()) 71 | .pipe(gulp.dest('./dist/')) 72 | }) 73 | 74 | 75 | gulp.task('build', ['build:res-web', 'build:res-web-min', 'build:res-node', 'build:res-rn', 'build:index']) 76 | gulp.task('default', ['build']) 77 | gulp.task('watch', ['build'], () => { 78 | gulp.watch('*.js', ['build']) 79 | }) 80 | -------------------------------------------------------------------------------- /flask_restaction/resjs/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import { join } from 'path' 3 | import ajax from 'superagent' 4 | 5 | function toUrl(resource, action) { 6 | //Convert resource.action to {url, method}, method is UpperCase 7 | let i = action.indexOf("_") 8 | if (i < 0) { 9 | return { 10 | url: "/" + resource, 11 | method: action.toUpperCase() 12 | } 13 | } else { 14 | return { 15 | url: `/${resource}/${action.slice(i+1)}`, 16 | method: action.slice(0, i).toUpperCase() 17 | } 18 | } 19 | } 20 | 21 | function readMeta(url) { 22 | return new Promise((resolve, reject) => { 23 | ajax('GET', url) 24 | .set('Accept', 'application/json') 25 | .end((error, response) => { 26 | if (error) { 27 | reject(error) 28 | } else { 29 | resolve(response.body) 30 | } 31 | }) 32 | }) 33 | } 34 | 35 | function parseMeta(url) { 36 | // authHeader is LowerCase 37 | return readMeta(url).then(meta => { 38 | let authHeader = meta.$auth ? meta.$auth.header.toLowerCase() : null 39 | let urlPrefix = meta.$url_prefix ? meta.$url_prefix : '' 40 | let resources = {} 41 | for (let k in meta) { 42 | if (k.slice(0, 1) != '$') { 43 | resources[k] = {} 44 | for (let action in meta[k]) { 45 | if (action.slice(0, 1) != '$') { 46 | resources[k][action] = toUrl(k, action) 47 | } 48 | } 49 | } 50 | } 51 | return { authHeader, urlPrefix, resources } 52 | }) 53 | } 54 | 55 | function renderCore(meta) { 56 | /*Generate res.code.js*/ 57 | let code = '' 58 | code += `function(root, init) {\n` 59 | code += ` var q = init('${meta.authHeader}', '${meta.urlPrefix}');\n` 60 | code += ` var r = null;\n` 61 | for (let key in meta.resources) { 62 | code += ` r = root.${key} = {};\n` 63 | for (let action in meta.resources[key]) { 64 | let item = meta.resources[key][action] 65 | code += ` r.${action} = q('${item.url}', '${item.method}');\n` 66 | } 67 | } 68 | code += `}` 69 | return code 70 | } 71 | 72 | function parseResjs(node = false, rn = false, min = false) { 73 | let filename = min ? 'res.web.min.js' : 'res.web.js' 74 | if (node) { 75 | filename = 'res.node.js' 76 | } 77 | if (rn) { 78 | filename = 'res.rn.js' 79 | } 80 | filename = join(__dirname, filename) 81 | return new Promise((resolve, reject) => { 82 | fs.readFile(filename, { encoding: 'utf-8' }, (error, data) => { 83 | if (error) { 84 | reject(error) 85 | } else { 86 | resolve(core => { 87 | return data.replace('"#res.core.js#"', core) 88 | }) 89 | } 90 | }) 91 | }) 92 | } 93 | 94 | function resjs(url, dest = './res.js', urlPrefix = undefined, node = undefined, rn = undefined, min = undefined) { 95 | return Promise.all([parseMeta(url), parseResjs(node, rn, min)]) 96 | .then(([meta, generate]) => { 97 | if (urlPrefix) { 98 | meta.urlPrefix = urlPrefix 99 | } 100 | let code = generate(renderCore(meta)) 101 | return new Promise((resolve, reject) => { 102 | fs.writeFile(dest, code, (error) => { 103 | if (error) { 104 | reject(error) 105 | } else { 106 | resolve(`OK, saved in: ${dest}`) 107 | } 108 | }) 109 | }) 110 | }).then(message => { 111 | console.log(message) 112 | }).catch(error => { 113 | console.log(error) 114 | }) 115 | } 116 | 117 | module.exports = resjs 118 | -------------------------------------------------------------------------------- /flask_restaction/resjs/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Fri May 06 2016 14:11:02 GMT+0000 (UTC) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '', 9 | 10 | 11 | // frameworks to use 12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 13 | frameworks: ['mocha', 'chai'], 14 | 15 | 16 | // list of files / patterns to load in the browser 17 | files: [ 18 | 'test/index.js', 19 | 'test/res.js', 20 | 'test/index.html' 21 | ], 22 | 23 | 24 | // list of files to exclude 25 | exclude: [], 26 | 27 | 28 | // preprocess matching files before serving them to the browser 29 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 30 | // preprocessors: { 31 | // }, 32 | 33 | 34 | // test results reporter to use 35 | // possible values: 'dots', 'progress' 36 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 37 | reporters: ['mocha'], 38 | 39 | 40 | // web server port 41 | port: 9876, 42 | 43 | 44 | // enable / disable colors in the output (reporters and logs) 45 | colors: true, 46 | 47 | 48 | // level of logging 49 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 50 | logLevel: config.LOG_INFO, 51 | 52 | 53 | // enable / disable watching file and executing tests whenever any file changes 54 | autoWatch: true, 55 | 56 | 57 | // start these browsers 58 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 59 | browsers: ['Chrome'], 60 | 61 | 62 | // Continuous Integration mode 63 | // if true, Karma captures browsers, runs the tests and exits 64 | singleRun: true, 65 | 66 | // Concurrency level 67 | // how many browser should be started simultaneous 68 | concurrency: Infinity 69 | }); 70 | }; 71 | -------------------------------------------------------------------------------- /flask_restaction/resjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "resjs", 3 | "version": "0.0.4", 4 | "description": "flask-restaction res.js generator", 5 | "main": "dist/index.js", 6 | "bin": { 7 | "resjs": "bin/resjs" 8 | }, 9 | "files": [ 10 | "dist", 11 | "bin" 12 | ], 13 | "keywords": [ 14 | "cli" 15 | ], 16 | "scripts": { 17 | "build": "gulp build", 18 | "resjs": "node bin/resjs http://127.0.0.1:5000 test/res.js --prefix http://127.0.0.1:5000", 19 | "resjs-min": "node bin/resjs http://127.0.0.1:5000 test/res.js --prefix http://127.0.0.1:5000 --min", 20 | "resjs-node": "node bin/resjs http://127.0.0.1:5000 test/res.js --prefix http://127.0.0.1:5000 --node", 21 | "test": "export CHROME_BIN=chromium && karma start --no-single-run" 22 | }, 23 | "author": "guyskk", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/guyskk/flask-restaction/issues", 27 | "email": "guyskk@qq.com" 28 | }, 29 | "devDependencies": { 30 | "babel-core": "^6.14.0", 31 | "babel-loader": "^6.2.5", 32 | "babel-plugin-transform-runtime": "^6.12.0", 33 | "babel-preset-es2015": "^6.14.0", 34 | "chai": "^3.5.0", 35 | "chai-as-promised": "^5.3.0", 36 | "gulp": "^3.9.1", 37 | "gulp-babel": "^6.1.2", 38 | "gulp-rename": "^1.2.2", 39 | "karma": "^1.2.0", 40 | "karma-chai": "^0.1.0", 41 | "karma-chai-as-promised": "^0.1.2", 42 | "karma-chrome-launcher": "^2.0.0", 43 | "karma-mocha": "^1.1.1", 44 | "karma-mocha-reporter": "^2.1.0", 45 | "mocha": "^3.0.2", 46 | "webpack": "^1.13.2", 47 | "webpack-stream": "^3.2.0" 48 | }, 49 | "dependencies": { 50 | "babel-runtime": "^6.11.6", 51 | "commander": "^2.9.0", 52 | "superagent": "^2.2.0" 53 | }, 54 | "engines": { 55 | "node": ">=4.0.0" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /flask_restaction/resjs/readme.md: -------------------------------------------------------------------------------- 1 | # res.js 2 | 3 | 用法 4 | 5 | npm install -g resjs 6 | 7 | resjs --help 8 | 9 | 10 | Usage: resjs [options] 11 | 12 | generate res.js for browser or nodejs 13 | 14 | Options: 15 | 16 | -h, --help output usage information 17 | -V, --version output the version number 18 | -d, --dest [dest] dest path to save res.js 19 | -p, --prefix [prefix] url prefix of generated res.js 20 | -n, --node [node] generate res.js for nodejs, default for browser 21 | -r, --rn [rn] generate res.js for react-native 22 | -m, --min [min] minimize generated res.js, default not minimize 23 | 24 | ReactNative 25 | 26 | resjs APIURL -p APIURL --rn 27 | 28 | 构建 29 | 30 | cd resjs 31 | npm install 32 | npm run build 33 | 34 | 准备测试 35 | 36 | // create a server for test 37 | cd server 38 | pip install -r requires.txt 39 | python index.py 40 | 41 | 测试 42 | 43 | cd resjs 44 | // 生成 res.js 45 | npm run resjs 46 | // 在linux下用chromium浏览器测试 47 | // 可以修改karma.conf.js然后在其他浏览器上测试 48 | npm run test 49 | -------------------------------------------------------------------------------- /flask_restaction/resjs/res.base.js: -------------------------------------------------------------------------------- 1 | import ajax from 'superagent' 2 | 3 | let base = { ajax } 4 | 5 | function init(authHeader, urlPrefix) { 6 | base.config = { authHeader, urlPrefix } 7 | if (typeof window === 'undefined' || typeof localStorage === 'undefined') { 8 | let token = null 9 | base.clearToken = () => { 10 | token = null 11 | } 12 | base.setToken = (token) => { 13 | token = token 14 | } 15 | base.getToken = () => { 16 | return token 17 | } 18 | } else { 19 | base.clearToken = () => { 20 | window.localStorage.removeItem('resjs-auth-token') 21 | } 22 | base.setToken = (token) => { 23 | window.localStorage['resjs-auth-token'] = token 24 | } 25 | base.getToken = () => { 26 | return window.localStorage['resjs-auth-token'] 27 | } 28 | } 29 | return function(url, method) { 30 | return function(data) { 31 | let request = ajax(method, base.config.urlPrefix + url) 32 | request = request.set('accept', 'application/json') 33 | if (base.config.authHeader) { 34 | let token = base.getToken() 35 | if (token) { 36 | request = request.set(base.config.authHeader, token) 37 | } 38 | } 39 | if (method == 'PUT' || method == 'POST' || method == 'PATCH') { 40 | request = request.send(data) 41 | } else { 42 | request = request.query(data) 43 | } 44 | return new Promise((resolve, reject) => { 45 | request.end((error, response) => { 46 | if (error) { 47 | if (error.response) { 48 | // 4xx or 5xx response 49 | if (error.response.body) { 50 | reject(error.response.body) 51 | } else { 52 | reject(error.response) 53 | } 54 | } else { 55 | // Network failures, timeouts, and other errors 56 | reject(error) 57 | } 58 | } else { 59 | if (base.config.authHeader) { 60 | let token = response.header[base.config.authHeader] 61 | if (token) { 62 | base.setToken(token) 63 | } 64 | } 65 | if (response.body) { 66 | // JSON response 67 | resolve(response.body) 68 | } else { 69 | // unparsed response 70 | resolve(response) 71 | } 72 | } 73 | }) 74 | }) 75 | } 76 | } 77 | } 78 | 79 | let core = "#res.core.js#" 80 | core(base, init) 81 | 82 | module.exports = base 83 | -------------------------------------------------------------------------------- /flask_restaction/resjs/res.proxy.js: -------------------------------------------------------------------------------- 1 | // 用ES6的Proxy实现res.js,实验性功能 2 | import { toUrl } from './index.js' 3 | import root, { init } from './res.base.js' 4 | 5 | function Res(authHeader = 'Authorization', urlPrefix = '') { 6 | function core(root, init) { 7 | let q = init(authHeader.toLowerCase(), urlPrefix) 8 | return new Proxy(root, { 9 | get: function(target1, resource) { 10 | if (resource in target1) { 11 | return target1[resource] 12 | } 13 | return new Proxy({}, { 14 | get: function(target2, action) { 15 | if (action in target2) { 16 | return target2[action] 17 | } 18 | let { url, method } = toUrl(resource, action) 19 | return q(url, method) 20 | } 21 | }) 22 | } 23 | }) 24 | } 25 | return core(root, init) 26 | } 27 | 28 | module.exports = Res 29 | -------------------------------------------------------------------------------- /flask_restaction/resjs/res.rn.js: -------------------------------------------------------------------------------- 1 | import { 2 | AsyncStorage 3 | } from 'react-native' 4 | 5 | const base = { 6 | async clearToken () { 7 | await AsyncStorage.removeItem('resjs-auth-token') 8 | }, 9 | async setToken (token) { 10 | await AsyncStorage.setItem('resjs-auth-token', token) 11 | }, 12 | async getToken () { 13 | return await AsyncStorage.getItem('resjs-auth-token') 14 | } 15 | } 16 | 17 | function init (authHeader, urlPrefix) { 18 | let token = null 19 | base.config = { authHeader, urlPrefix } 20 | return function (url, method) { 21 | return async (data) => { 22 | let uri = base.config.urlPrefix + url 23 | const options = { 24 | method, 25 | headers: { 26 | Accept: 'application/json', 27 | 'Content-Type': 'application/json', 28 | // @TODO To be or not to be, this is a question. 29 | From: 'ReactNative' 30 | } 31 | } 32 | if (base.config.authHeader) { 33 | let token = await base.getToken() 34 | if (token) { 35 | options.headers[base.config.authHeader] = token 36 | } 37 | } 38 | if (method == 'PUT' || method == 'POST' || method == 'PATCH') { 39 | options.body = JSON.stringify(data) 40 | } else { 41 | uri += '?' + getQueryString(data) 42 | } 43 | return new Promise(async (resolve, reject) => { 44 | const response = await fetch(uri, options) 45 | const json = await response.json() 46 | if (response.status !== 200) { 47 | reject(json) 48 | } else { 49 | const token = response.headers.get(base.config.authHeader) 50 | if (token) { 51 | base.setToken(token) 52 | } 53 | resolve(json) 54 | } 55 | }) 56 | } 57 | } 58 | } 59 | 60 | function getQueryString (data = {}) { 61 | const qs = Object.keys(data) 62 | .map(k => encodeURIComponent(k) + '=' + encodeURIComponent(data[k])) 63 | .join('&') 64 | return qs 65 | } 66 | 67 | let core = "#res.core.js#" 68 | core(base, init) 69 | 70 | module.exports = base 71 | -------------------------------------------------------------------------------- /flask_restaction/resjs/test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | res.js test 7 | 8 | 9 | 10 |
    11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /flask_restaction/resjs/test/index.js: -------------------------------------------------------------------------------- 1 | describe('resjs', function() { 2 | it("test.get", function() { 3 | return res.test.get({ name: "kk" }).then(function(data) { 4 | assert.deepEqual(data, { 5 | 'hello': 'kk' 6 | }) 7 | }) 8 | }) 9 | it("test.post", function() { 10 | return res.test.post({ name: "kk" }).then(function(data) { 11 | assert.deepEqual(data, { 12 | 'hello': 'kk' 13 | }) 14 | }) 15 | }) 16 | it("test.post_name", function() { 17 | return res.test.post_name({ name: "kk" }).then(function(data) { 18 | assert.deepEqual(data, { 19 | 'hello': 'kk' 20 | }) 21 | }) 22 | }) 23 | it("test.put", function() { 24 | return res.test.put({ name: "kk" }).then(function(data) { 25 | assert.deepEqual(data, { 26 | 'hello': 'kk' 27 | }) 28 | }) 29 | }) 30 | it("test.patch", function() { 31 | return res.test.patch({ name: "kk" }).then(function(data) { 32 | assert.deepEqual(data, { 33 | 'hello': 'kk' 34 | }) 35 | }) 36 | }) 37 | it("test.delete", function() { 38 | return res.test.delete({ name: "kk" }).then(function(data) { 39 | assert.deepEqual(data, { 40 | 'hello': 'kk' 41 | }) 42 | }) 43 | }) 44 | it("test.get_302", function() { 45 | return res.test.get_302().then(function(data) { 46 | assert.deepEqual(data, { 47 | 'hello': 'world' 48 | }) 49 | }) 50 | }) 51 | it("test.get_404", function(done) { 52 | res.test.get_404().catch(function(error) { 53 | assert.isNotNull(error) 54 | done() 55 | }) 56 | }) 57 | it("test.get_403", function(done) { 58 | res.test.get_403().catch(function(error) { 59 | assert.isNotNull(error) 60 | done() 61 | }) 62 | }) 63 | it("test.get_401", function(done) { 64 | res.test.get_401().catch(function(error) { 65 | assert.isNotNull(error) 66 | done() 67 | }) 68 | }) 69 | it("test.get_400", function(done) { 70 | res.test.get_400().catch(function(error) { 71 | assert.isNotNull(error) 72 | done() 73 | }) 74 | }) 75 | it("test.get_500", function(done) { 76 | res.test.get_500().catch(function(error) { 77 | assert.isNotNull(error) 78 | done() 79 | }) 80 | }) 81 | it("test.get_binary", function(done) { 82 | res.test.get_binary().then(function(data) { 83 | assert.isNotNull(data) 84 | done() 85 | }) 86 | }) 87 | 88 | it("test.post_upload", function(done) { 89 | res.test.post_upload().then(function(data) { 90 | assert.isNotNull(data) 91 | done() 92 | }) 93 | }) 94 | it("test.post_login", function(done) { 95 | res.clearToken() 96 | res.test.post_login({ name: "kk" }).then(function(data) { 97 | assert.deepEqual(data.name, "kk") 98 | }).then(function() { 99 | res.test.get_me().then(function(data) { 100 | assert.deepEqual(data.name, "kk") 101 | done() 102 | }) 103 | }) 104 | }) 105 | 106 | it("test.get_me", function(done) { 107 | res.clearToken() 108 | res.test.get_me().catch(function(error) { 109 | assert.isNotNull(error) 110 | done() 111 | }) 112 | }) 113 | }) 114 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths = tests 3 | -------------------------------------------------------------------------------- /requires-dev.txt: -------------------------------------------------------------------------------- 1 | flask-cors>=2.1.2 2 | freezegun>=0.3.7 3 | 4 | pytest>=3.0.3 5 | pytest-cov>=2.3.1 6 | flake8>=3.0.4 7 | codecov>=2.0.5 8 | 9 | sphinx>=1.4.0 10 | twine>=1.8.1 11 | -------------------------------------------------------------------------------- /requires.txt: -------------------------------------------------------------------------------- 1 | flask>=0.11.1 2 | pyjwt>=1.4.2 3 | validr>=0.12.0 4 | requests>=2.7.0 5 | simple-yaml>=0.1.0 6 | -------------------------------------------------------------------------------- /server/index.py: -------------------------------------------------------------------------------- 1 | """Test Server""" 2 | from flask import Flask, Response, request, abort, redirect, url_for, g 3 | from flask_restaction import Api, TokenAuth 4 | from flask_cors import CORS 5 | from os.path import abspath, dirname, join 6 | 7 | app = Flask(__name__) 8 | app.secret_key = "secret_key" 9 | CORS(app, expose_headers=['Authorization']) 10 | metafile = abspath(join(dirname(__file__), "meta.json")) 11 | api = Api(app, metafile=metafile) 12 | 13 | auth = TokenAuth(api) 14 | 15 | 16 | @auth.get_role 17 | def get_role(token): 18 | if token: 19 | return token["role"] 20 | return "guest" 21 | 22 | 23 | class Test: 24 | """ 25 | $shared: 26 | name: str&default="world" 27 | """ 28 | 29 | def __init__(self, api): 30 | self.api = api 31 | 32 | def get(self, name): 33 | """ 34 | $input: 35 | name@name: Your name 36 | """ 37 | return {'hello': name} 38 | 39 | def post(self, name): 40 | """ 41 | $input: 42 | name@name: Your name 43 | """ 44 | return {'hello': name} 45 | 46 | def post_name(self, name): 47 | """ 48 | $input: 49 | name@name: Your name 50 | """ 51 | return {'hello': name} 52 | 53 | def put(self, name): 54 | """ 55 | $input: 56 | name@name: Your name 57 | """ 58 | return {'hello': name} 59 | 60 | def patch(self, name): 61 | """ 62 | $input: 63 | name@name: Your name 64 | """ 65 | return {'hello': name} 66 | 67 | def delete(self, name): 68 | """ 69 | $input: 70 | name@name: Your name 71 | """ 72 | return {'hello': name} 73 | 74 | def get_404(self): 75 | abort(404) 76 | 77 | def get_403(self): 78 | abort(403) 79 | 80 | def get_401(self): 81 | abort(401) 82 | 83 | def get_400(self): 84 | abort(400) 85 | 86 | def get_302(self): 87 | return redirect(url_for('test')) 88 | 89 | def get_500(self): 90 | abort(500) 91 | 92 | def get_binary(self): 93 | def generate(): 94 | rows = '0123456789' 95 | for i in range(1, 10): 96 | yield ','.join(rows[:i]) + '\n' 97 | return Response(generate(), mimetype='text/csv') 98 | 99 | def post_upload(self): 100 | files = request.files.getlist('upload') 101 | return {'recv': len(files)} 102 | 103 | def post_login(self, name): 104 | """ 105 | $input: 106 | name@name: Your name 107 | """ 108 | g.token = token = {'role': 'admin', 'name': name} 109 | return token 110 | 111 | def get_me(self): 112 | return g.token 113 | 114 | 115 | api.add_resource(Test, api) 116 | app.route('/')(api.meta_view) 117 | 118 | if __name__ == '__main__': 119 | app.run(debug=True) 120 | -------------------------------------------------------------------------------- /server/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "$roles": { 3 | "guest": { 4 | "test": [ 5 | "get", "post", "put", "delete", "patch", 6 | "get_404", "get_403", "get_401", "get_400", "get_302", "get_500", 7 | "get_binary", "post_name", "post_upload", "post_login" 8 | ] 9 | }, 10 | "admin": { 11 | "test": [ 12 | "get", "post", "put", "delete", "patch", 13 | "get_404", "get_403", "get_401", "get_400", "get_302", "get_500", 14 | "get_binary", "post_name", "post_upload", "post_login", "get_me" 15 | ] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Flask-Restaction 3 | 4 | http://flask-restaction.readthedocs.org 5 | """ 6 | from setuptools import setup 7 | from os.path import join, dirname 8 | 9 | with open(join(dirname(__file__), 'requires.txt'), 'r') as f: 10 | install_requires = f.read().split("\n") 11 | 12 | setup( 13 | name="flask-restaction", 14 | version="0.25.3", 15 | description="A web framwork born to create RESTful API", 16 | long_description=__doc__, 17 | author="guyskk", 18 | author_email='guyskk@qq.com', 19 | url="https://github.com/guyskk/flask-restaction", 20 | license="MIT", 21 | packages=["flask_restaction"], 22 | entry_points={ 23 | 'console_scripts': ['resjs=flask_restaction.cli:main'], 24 | }, 25 | include_package_data=True, 26 | zip_safe=False, 27 | install_requires=install_requires, 28 | classifiers=[ 29 | 'Framework :: Flask', 30 | 'Environment :: Web Environment', 31 | 'Intended Audience :: Developers', 32 | 'License :: OSI Approved :: MIT License', 33 | 'Operating System :: OS Independent', 34 | 'Programming Language :: Python :: 3 :: Only', 35 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 36 | 'Topic :: Software Development :: Libraries :: Python Modules' 37 | ] 38 | ) 39 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | try: 2 | import ipdb as pdb 3 | except: 4 | import pdb 5 | __builtins__['debug'] = pdb.set_trace 6 | -------------------------------------------------------------------------------- /tests/helper.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | def resp_json(resp): 5 | return json.loads(resp.data.decode("utf-8")) 6 | -------------------------------------------------------------------------------- /tests/test_auth.py: -------------------------------------------------------------------------------- 1 | import json 2 | import jwt 3 | from flask import Flask, g 4 | from flask_restaction import Api, TokenAuth 5 | from freezegun import freeze_time 6 | from helper import resp_json 7 | 8 | AUTH_HEADER = "Authorization" 9 | 10 | 11 | def create_app(tmpdir, refresh=True, cookie=None): 12 | class Hello: 13 | 14 | def get(self): 15 | """ 16 | Get Name 17 | 18 | $output: 19 | name?str&optional: Your name 20 | """ 21 | return {"name": g.token["name"]} 22 | 23 | def post(self, name): 24 | """ 25 | Generate Token 26 | 27 | $input: 28 | name?str: Your name 29 | """ 30 | if name == "admin": 31 | role = "admin" 32 | else: 33 | role = "normal" 34 | g.token = {"role": role, "name": name} 35 | return "OK" 36 | 37 | metafile = tmpdir.join("meta.json") 38 | json.dump({ 39 | "$desc": "test", 40 | "$auth": { 41 | "refresh": refresh, 42 | "cookie": cookie 43 | }, 44 | "$roles": { 45 | "admin": { 46 | "hello": ["get", "post"] 47 | }, 48 | "guest": { 49 | "hello": ["post"] 50 | } 51 | } 52 | }, metafile.open("w")) 53 | 54 | app = Flask(__name__) 55 | app.secret_key = "secret_key" 56 | api = Api(app, metafile=metafile.strpath) 57 | 58 | auth = TokenAuth(api) 59 | 60 | @auth.get_role 61 | def get_role(token): 62 | if token: 63 | return token["role"] 64 | else: 65 | return "guest" 66 | 67 | api.add_resource(Hello) 68 | return app 69 | 70 | 71 | def test_auth(tmpdir): 72 | app = create_app(tmpdir) 73 | with app.test_client() as c: 74 | resp = c.get("/hello") 75 | assert resp.status_code == 403 76 | assert resp_json(resp)["error"] == "PermissionDeny" 77 | assert "guest" in resp_json(resp)["message"] 78 | 79 | resp = c.post("/hello", data={"name": "abc"}) 80 | assert resp.status_code == 200 81 | headers = {AUTH_HEADER: resp.headers[AUTH_HEADER]} 82 | resp = c.get("/hello", headers=headers) 83 | assert resp.status_code == 403 84 | assert resp_json(resp)["error"] == "PermissionDeny" 85 | assert "normal" in resp_json(resp)["message"] 86 | 87 | resp = c.post("/hello", data={"name": "admin"}) 88 | assert resp.status_code == 200 89 | headers = {AUTH_HEADER: resp.headers[AUTH_HEADER]} 90 | resp = c.get("/hello", headers=headers) 91 | assert resp.status_code == 200 92 | assert resp_json(resp) == {"name": "admin"} 93 | 94 | 95 | def test_no_secret_key(): 96 | app = Flask(__name__) 97 | api = Api(app) 98 | auth = TokenAuth(api) 99 | with app.test_request_context("/"): 100 | token = jwt.encode({"user_id": 1}, "") 101 | assert auth.decode_token(token) is None 102 | 103 | 104 | def test_expiration(tmpdir): 105 | app = create_app(tmpdir) 106 | with app.test_client() as c: 107 | with freeze_time("2016-09-15T15:00:00Z"): 108 | resp = c.post("/hello", data={"name": "admin"}) 109 | assert resp.status_code == 200 110 | headers = {AUTH_HEADER: resp.headers[AUTH_HEADER]} 111 | with freeze_time("2016-09-15T15:29:59Z"): 112 | resp = c.get("/hello", headers=headers) 113 | assert resp.status_code == 200 114 | # not create new token 115 | assert AUTH_HEADER not in resp.headers 116 | with freeze_time("2016-09-15T15:30:01Z"): 117 | resp = c.get("/hello", headers=headers) 118 | assert resp.status_code == 200 119 | # create new token, can reuse 60min from now 120 | assert AUTH_HEADER in resp.headers 121 | with freeze_time("2016-09-15T15:59:59Z"): 122 | resp = c.get("/hello", headers=headers) 123 | assert resp.status_code == 200 124 | # create new token, can reuse 60min from now 125 | assert AUTH_HEADER in resp.headers 126 | new_headers = {AUTH_HEADER: resp.headers[AUTH_HEADER]} 127 | with freeze_time("2016-09-15T16:00:01Z"): 128 | # token invalid 129 | resp = c.get("/hello", headers=headers) 130 | assert resp.status_code == 403 131 | # use new token 132 | resp = c.get("/hello", headers=new_headers) 133 | assert resp.status_code == 200 134 | with freeze_time("2016-09-15T16:59:58Z"): 135 | # use new token 136 | resp = c.get("/hello", headers=new_headers) 137 | assert resp.status_code == 200 138 | with freeze_time("2016-09-15T17:00:01Z"): 139 | # token invalid 140 | resp = c.get("/hello", headers=new_headers) 141 | assert resp.status_code == 403 142 | 143 | 144 | def test_disable_refresh(tmpdir): 145 | app = create_app(tmpdir, refresh=False) 146 | with app.test_client() as c: 147 | with freeze_time("2016-09-15T15:00:00Z"): 148 | resp = c.post("/hello", data={"name": "admin"}) 149 | assert resp.status_code == 200 150 | headers = {AUTH_HEADER: resp.headers[AUTH_HEADER]} 151 | with freeze_time("2016-09-15T15:30:01Z"): 152 | resp = c.get("/hello", headers=headers) 153 | assert resp.status_code == 200 154 | # not create new token 155 | assert AUTH_HEADER not in resp.headers 156 | with freeze_time("2016-09-15T15:59:59Z"): 157 | resp = c.get("/hello", headers=headers) 158 | assert resp.status_code == 200 159 | # not create new token 160 | assert AUTH_HEADER not in resp.headers 161 | with freeze_time("2016-09-15T16:00:01Z"): 162 | # token invalid 163 | resp = c.get("/hello", headers=headers) 164 | assert resp.status_code == 403 165 | 166 | 167 | def test_enable_cookie(tmpdir): 168 | cookie_name = "Authorization" 169 | app = create_app(tmpdir, cookie=cookie_name) 170 | with app.test_client() as c: 171 | with freeze_time("2016-09-15T15:00:00Z"): 172 | resp = c.post("/hello", data={"name": "admin"}) 173 | assert resp.status_code == 200 174 | assert "Set-Cookie" in resp.headers 175 | with freeze_time("2016-09-15T15:29:59Z"): 176 | resp = c.get("/hello") 177 | assert resp.status_code == 200 178 | assert "Set-Cookie" not in resp.headers 179 | with freeze_time("2016-09-15T16:00:01Z"): 180 | resp = c.get("/hello") 181 | # token invalid 182 | assert resp.status_code == 403 183 | with freeze_time("2016-09-15T15:59:59Z"): 184 | resp = c.get("/hello") 185 | assert resp.status_code == 200 186 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from unittest import mock 3 | from flask_restaction.cli import parse_meta, resjs, main 4 | 5 | 6 | def test_url_prefix(): 7 | url = "http://127.0.0.1:5000" 8 | meta = requests.get(url, headers={'Accept': 'application/json'}).json() 9 | url_prefix, __, __ = parse_meta(meta) 10 | assert url_prefix == "" 11 | 12 | 13 | def test_resjs_web(tmpdir): 14 | resjs("http://127.0.0.1:5000", tmpdir.join("res.js").strpath, 15 | prefix="/api", min=True) 16 | assert tmpdir.join("res.js").check() 17 | 18 | 19 | def test_resjs_node(tmpdir): 20 | resjs("http://127.0.0.1:5000", tmpdir.join("res.js").strpath, node=True) 21 | assert tmpdir.join("res.js").check() 22 | 23 | 24 | def test_api_meta_view(): 25 | resjs = requests.get("http://127.0.0.1:5000?f=res.js") 26 | assert resjs.headers["Content-Type"] == "application/javascript" 27 | resminjs = requests.get("http://127.0.0.1:5000?f=res.min.js") 28 | assert resminjs.headers["Content-Type"] == "application/javascript" 29 | resjs2 = requests.get("http://127.0.0.1:5000?f=res.js") 30 | assert resjs.content == resjs2.content 31 | resminjs2 = requests.get("http://127.0.0.1:5000?f=res.min.js") 32 | assert resminjs.content == resminjs2.content 33 | resp = requests.get("http://127.0.0.1:5000?f=docs.min.js") 34 | assert resp.status_code == 200 35 | resp = requests.get("http://127.0.0.1:5000?f=docs.min.css") 36 | assert resp.status_code == 200 37 | resp = requests.get("http://127.0.0.1:5000?f=unknown.js") 38 | assert resp.status_code == 404 39 | 40 | 41 | def test_cli(tmpdir): 42 | dest = tmpdir.join("res.js").strpath 43 | args = ["resjs", "http://127.0.0.1:5000", "-d", dest] 44 | with mock.patch("sys.argv", new=args): 45 | main() 46 | -------------------------------------------------------------------------------- /tests/test_res.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import sys 3 | 4 | import pytest 5 | from flask_restaction import Res 6 | from requests import HTTPError 7 | 8 | sys.path.append('./server') 9 | app = importlib.import_module("index").app 10 | 11 | 12 | @pytest.fixture(params=[ 13 | {"url_prefix": "http://127.0.0.1:5000"}, 14 | {"test_client": app.test_client} 15 | ]) 16 | def res(request): 17 | return Res(**request.param) 18 | 19 | 20 | def test_basic(res): 21 | data = {"name": "kk"} 22 | expect = {"hello": "kk"} 23 | assert res.test.get(data) == expect 24 | assert res.test.post(data) == expect 25 | assert res.test.post_name(data) == expect 26 | assert res.test.put(data) == expect 27 | assert res.test.patch(data) == expect 28 | assert res.test.delete(data) == expect 29 | 30 | 31 | def test_ajax(res): 32 | assert res.ajax("/")["test"]["get"] is not None 33 | 34 | 35 | @pytest.mark.parametrize("code", [404, 403, 401, 400, 500]) 36 | def test_error(res, code): 37 | with pytest.raises(HTTPError) as exinfo: 38 | f = getattr(res.test, "get_%d" % code) 39 | f() 40 | assert exinfo.value.response.status_code == code 41 | 42 | 43 | def test_302(res): 44 | assert res.test.get_302({"name": "kk"}) == {"hello": "world"} 45 | 46 | 47 | def test_auth(res): 48 | with pytest.raises(HTTPError): 49 | res.test.get_me() 50 | assert res.test.post_login({"name": "admin"}) 51 | assert res.test.get_me()["name"] == "admin" 52 | assert res.test.post_login({"name": "kk"}) 53 | assert res.test.get_me()["name"] == "kk" 54 | 55 | 56 | def test_api_meta_view(): 57 | with app.test_client() as c: 58 | resjs = c.get("/?f=res.js") 59 | assert resjs.status_code == 200 60 | assert resjs.headers["Content-Type"] == "application/javascript" 61 | resminjs = c.get("/?f=res.min.js") 62 | assert resminjs.status_code == 200 63 | assert resminjs.headers["Content-Type"] == "application/javascript" 64 | resjs2 = c.get("/?f=res.js") 65 | assert resjs.data == resjs2.data 66 | resminjs2 = c.get("/?f=res.min.js") 67 | assert resminjs.data == resminjs2.data 68 | resp = c.get("/?f=docs.min.js") 69 | assert resp.status_code == 200 70 | resp = c.get("/?f=docs.min.css") 71 | assert resp.status_code == 200 72 | resp = c.get("/?f=unknown.js") 73 | assert resp.status_code == 404 74 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Tox (http://tox.testrun.org/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | 6 | [tox] 7 | envlist = py33, py34, py35 8 | skip_missing_interpreters = true 9 | 10 | [testenv] 11 | commands = 12 | pytest 13 | flake8 14 | deps = -r{toxinidir}/requires-dev.txt 15 | --------------------------------------------------------------------------------