├── .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 | [](https://travis-ci.org/guyskk/flask-restaction) [](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 | [](https://travis-ci.org/guyskk/flask-restaction) [](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 | 
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 | 
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:/^ *\[([^\]]+)\]: *([^\s>]+)>?(?: +["(]([^\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"\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+""+type+">\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+""+type+">\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='"+text+"";return out};Renderer.prototype.image=function(href,title,text){var out='
":">";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',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 |
57 | Error
58 | No Error Info
59 |
60 |
64 |
65 | {{roleName}}
66 |
67 |
68 | {{resource}} |
69 |
70 |
71 |
72 | {{action}}
73 |
74 | {{info.desc}}
75 | |
76 |
77 |
78 |
79 |
80 | {{resourceName}}
81 |
82 |
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 |
--------------------------------------------------------------------------------