diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..1b097e279931e088c564c80dd5de395d8411f9d2 Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore index 6d7b9c17f7d3c0a133b2328a1c52005d3e39f75d..4a23ab3b2dfdfeee476d2a6c3d0f63bed253e8c0 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,5 @@ docs/_build/ # PyBuilder target/ +/examples/industrial-ai-apiserver/test.json +/examples/industrial-ai-apiserver/test.yaml diff --git a/pecan_swagger/__init__.py b/.idea/.gitignore similarity index 100% rename from pecan_swagger/__init__.py rename to .idea/.gitignore diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000000000000000000000000000000000000..105ce2da2d6447d11dfe32bfb846c3d5b199fc99 --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000000000000000000000000000000000000..2d85f807005f0c32ffe93c3c6b32844890f9498e --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000000000000000000000000000000000000..c5b50f1e38dd7da465c1f810acaf0d148d078f1a --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/pecan-swagger.iml b/.idea/pecan-swagger.iml new file mode 100644 index 0000000000000000000000000000000000000000..c03885c89ad187df967cb71b3d05bdecb2e33b05 --- /dev/null +++ b/.idea/pecan-swagger.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/shelf/Uncommitted_changes_before_Checkout_at_2022_5_24,_14_46_[Changes]/shelved.patch b/.idea/shelf/Uncommitted_changes_before_Checkout_at_2022_5_24,_14_46_[Changes]/shelved.patch new file mode 100644 index 0000000000000000000000000000000000000000..fdc7d9e1f5a6536389d2995c74dae583c5a86b33 --- /dev/null +++ b/.idea/shelf/Uncommitted_changes_before_Checkout_at_2022_5_24,_14_46_[Changes]/shelved.patch @@ -0,0 +1,1136 @@ +Index: pecan_swagger/utils.py +=================================================================== +diff --git a/pecan_swagger/utils.py b/pecan_swagger/utils.py +deleted file mode 100644 +--- a/pecan_swagger/utils.py (revision 2159b53baa4c86d9c1b739260824b7f89f935759) ++++ /dev/null (revision 2159b53baa4c86d9c1b739260824b7f89f935759) +@@ -1,64 +0,0 @@ +-from pecan_swagger import g as g +- +- +-""" +-utility function module +- +-this module contains the utility functions to assemble the swagger +-dictionary object. they can be consumed by end-user applications to +-build up swagger objects for applications. +- +-functions: +-swagger_build -- build a full swagger dictionary +-""" +- +- +-def swagger_build(title, version): +- swag = dict() +- swag['swagger'] = '2.0' +- swag['info'] = dict(title=title, version=version) +- swag['consumes'] = [] +- swag['produces'] = [] +- swag['paths'] = {} +- for p in g.get_paths(): +- if p[0] not in swag['paths']: +- swag['paths'][p[0]] = _tuple_to_dict(p[1]) +- elif len(p[1]) > 0: +- swag['paths'][p[0]].update(_tuple_to_dict(p[1])) +- swag['definitions'] = g._definitions +- return swag +- +- +-def _tuple_to_dict(tpl): +- """Convert tuple to dictionary +- +- each tuple must have key and value. +- This function arrows taple including lists. +- +- ex.) acceptable taple +- OK: ('/', ('get',(('desc',{}),('res',{}),('params',['id','name'])))) +- NG: ('/', ('get',('desc','res',('params',['id','name'])))) +- """ +- if isinstance(tpl, list): +- d = [] +- for e in tpl: +- d.append(_tuple_to_dict(e)) +- elif isinstance(tpl, tuple): +- d = {} +- if isinstance(tpl[0], tuple): +- # tuple has some child tuple +- for e in tpl: +- d[e[0]] = _tuple_to_dict(e[1]) +- elif isinstance(tpl[0], list): +- # list member should be processed recursively +- d = _tuple_to_dict(tpl[0]) +- else: +- if len(tpl) == 2: +- # single tuple node +- d[tpl[0]] = _tuple_to_dict(tpl[1]) +- else: +- raise Exception(tpl) +- else: +- # value or dict +- d = tpl +- return d +Index: pecan_swagger/decorators.py +=================================================================== +diff --git a/pecan_swagger/decorators.py b/pecan_swagger/decorators.py +deleted file mode 100644 +--- a/pecan_swagger/decorators.py (revision 2159b53baa4c86d9c1b739260824b7f89f935759) ++++ /dev/null (revision 2159b53baa4c86d9c1b739260824b7f89f935759) +@@ -1,38 +0,0 @@ +-import pecan_swagger.g as g +- +-""" +-decorators module +- +-these decorators are meant to be used by developers who wish to markup +-their pecan applications to produce swagger. +-""" +- +- +-def path(endpoint, name, parent=None): +- """ +- path decorator +- +- this decorator should be used on pecan controllers to instruct how +- they map to routes. +- +- :param endpoint: the root uri for this controller. +- :param name: the name of this path. +- :param parent: an optional path name to indicate a parent/child +- relationship between this path and another. +- """ +- def decorator(c): +- if hasattr(c, '__swag'): +- raise Exception('{} already has swag'.format(c.__name__)) +- c.__swag = dict(endpoint=endpoint, name=name, parent=parent) +- g.add_path(c) +- return c +- return decorator +- +- +-def method(method): +- def decorator(m): +- if hasattr(m, '__swag'): +- raise Exception('{} already has swag'.format(m.__name__)) +- m.__swag = dict(method=method) +- return m +- return decorator +Index: pecan_swagger/g.py +=================================================================== +diff --git a/pecan_swagger/g.py b/pecan_swagger/g.py +deleted file mode 100644 +--- a/pecan_swagger/g.py (revision 2159b53baa4c86d9c1b739260824b7f89f935759) ++++ /dev/null (revision 2159b53baa4c86d9c1b739260824b7f89f935759) +@@ -1,487 +0,0 @@ +-import copy +-import inspect +- +-from pecan import util as p_u +-import wsme.types as wtypes +-import six +-import weakref +- +- +-""" +-global hierarchy module +- +-this module is at the heart of the swagger conversion utility. it +-contains a global "hierarchy" object which will provide a single point +-to assemble an application's swagger output. +- +-there are also several helper functions to assist in building the +-hierarchy of controllers, routes, and methods. +-""" +- +-_hierarchy = {} +- +-_http_methods = {'get': '', 'post': '', 'put': '', +- 'patch': '', 'delete': '', +- 'head': '', 'trace': ''} +- +-_all_wsme_types = wtypes.native_types + ( +- wtypes.ArrayType, wtypes.DictType, wtypes.BinaryType, +- wtypes.IntegerType, wtypes.StringType, wtypes.IPv4AddressType, +- wtypes.IPv6AddressType, wtypes.UuidType, wtypes.Enum, wtypes.UnsetType) +- +-_definitions = {} +- +- +-def add_path(c): +- """adds a named controller to the hierarchy.""" +- if _hierarchy.get(c.__swag['name']): +- raise Exception( +- 'name {} already exists in hierarchy'.format(c.__swag['name'])) +- _hierarchy[c.__swag['name']] = c +- +- +-def build_path(swaginfo): +- """return the route path for a swag metadata item.""" +- if swaginfo.get('parent') is not None: +- return path_join(build_path(get_swag(swaginfo.get('parent'))), +- swaginfo.get('endpoint')) +- return swaginfo.get('endpoint') +- +- +-def get_controller_paths(controllers, wsme_defs): +- """ +- get a list of paths with methods +- +- returns a list of tuples (controller path, methods). +- """ +- def get_methods_for_generic(name): +- methods = [] +- generic_handlers = lc.get(name).get('generic_handlers', {}) +- for method, func in generic_handlers.items(): +- if method == 'DEFAULT': +- methods.append('get') +- else: +- methods.append(method.lower()) +- # TODO drill down through decorators to get true function name +- truename = getrealname(func) +- del lc[truename] +- return methods +- +- def append_methods_for_specific(name, tpl): +- paths.append(tpl) +- del lc[name] +- +- lc = copy.deepcopy(controllers) +- paths = [] +- # TODO incorporate method decorator, removing functions marked +- if lc.get('index'): +- for method in get_methods_for_generic('index'): +- paths.append(('', (method, {}))) +- +- # for REST controller +- for method, path in _http_methods.items(): +- if lc.get(method): +- spec = wsme_defs.get(method, {}) # add wsme definitions +- append_methods_for_specific(method, (path, (method, spec))) +- +- if lc.get('get_all'): +- # add wsme definitions +- spec = wsme_defs.get('get_all') # add wsme definitions +- append_methods_for_specific('get_all', ('', ('get', spec))) +- if lc.get('get_one'): +- # add wsme definitions +- spec = wsme_defs.get('get_one') # add wsme definitions +- append_methods_for_specific('get_one', ('', ('get', spec))) +- +- if lc.get('_default'): +- append_methods_for_specific('_default', ('', ('*', {}))) +- if lc.get('_route'): +- append_methods_for_specific('_route', ('', ('*', {}))) +- if lc.get('_lookup'): +- del lc['_lookup'] +- +- if lc.get('_custom_actions'): +- for ca, ca_method in lc['_custom_actions'].items(): +- spec = wsme_defs.get(ca) # add wsme definitions +- for m in ca_method: +- paths.append((ca, (m.lower(), spec))) +- del lc['_custom_actions'] +- +- generic_controllers = [c for c in lc if lc[c].get('generic')] +- for controller in generic_controllers: +- for method in get_methods_for_generic(controller): +- paths.append((controller, (method, {}))) +- for controller in lc: +- if controller not in [path[0] for path in paths]: +- paths.append((controller, ('get', {}))) +- return paths +- +- +-def get_wsme_defs(name): +- +- def datatype_to_type_and_format(datatype): +- tf = {} +- if datatype == 'uuid' or datatype == 'ipv4address' \ +- or datatype == 'ipv6address' or datatype == 'date' \ +- or datatype == 'time': +- tf['type'] = 'string' +- tf['format'] = datatype +- elif datatype == 'datetime': +- tf['type'] = 'string' +- tf['format'] = 'date-time' +- elif datatype == 'binary' or datatype == 'bytes': +- # base64 encoded characters +- tf['type'] = 'string' +- tf['format'] = 'byte' +- elif datatype == 'array' or datatype == 'boolean' \ +- or datatype == 'integer' or datatype == 'string': +- # no format +- tf['type'] = datatype +- elif datatype == 'float': +- # number +- tf['type'] = 'number' +- tf['format'] = datatype +- elif datatype == 'unicode' or datatype == 'str' \ +- or datatype == 'text': +- # primitive, no format +- tf['type'] = 'string' +- elif datatype == 'int' or datatype == 'decimal': +- # primitive, no format +- tf['type'] = 'integer' +- elif datatype == 'bool': +- # primitive, no format +- tf['type'] = 'boolean' +- elif datatype == 'enum': +- tf['type'] = 'enum' +- elif datatype == 'unset' or datatype == 'none': +- tf['type'] = None +- elif datatype == 'dict': +- tf['type'] = 'object' +- else: +- tf['type'] = 'object' +- return tf +- +- def class_to_name_str(c): +- return (('%s' % c).replace('<', '').replace('>', '') +- .replace('class ', '').replace('\'', '') +- .split(' ', 1)[0].rsplit('.', 1)[-1]) +- +- def conv_class_name_to_type_str(cn): +- type_str = '' +- if cn.endswith('Type'): +- type_str = cn[:-4] +- elif cn == 'str': +- type_str = 'string' +- else: +- type_str = cn +- type_str = type_str.lower() +- return type_str +- +- def get_type_str(obj, src_dict=None): +- type_str = '' +- if hasattr(obj, '__name__'): +- type_str = obj.__name__ +- else: +- type_str = obj.__class__.__name__ +- type_str = conv_class_name_to_type_str(type_str) +- +- tf = datatype_to_type_and_format(type_str) +- +- if hasattr(obj, 'basetype') and \ +- (obj.__class__ not in _all_wsme_types or type_str == 'enum'): +- # UserType or Enum +- tf['type'] = get_type_str(obj.basetype) +- +- if isinstance(src_dict, dict): +- # if dict is in args, set 'type' and 'format' into the dict and +- # return +- src_dict.update({'type': tf['type']}) +- if 'format' in tf: +- src_dict.update({'format': tf['format']}) +- +- # get datatype options. ex.) min_length, minimum, .. +- for k, v in inspect.getmembers(obj): +- if ((k == 'minimum' or k == 'maxmum' +- or k == 'min_length' or k == 'max_length') +- and v is not None): +- src_dict[to_swag_key(k)] = v +- elif k == 'pattern' and v is not None: +- src_dict[to_swag_key(k)] = v.pattern +- # TODO(shu-mutou): this should be removed for production. +- # else: +- # src_dict[to_swag_key(k)] = v +- +- if type_str == 'enum': +- # EnumType use 'set' that doesn't have sequence. +- # So use 'sorted' for keeping static output. +- src_dict['enum'] = sorted([v for v in obj.values]) +- +- # return 'type' only +- return tf['type'] +- +- def to_swag_key(key): +- keys = { +- 'doc': 'description', +- 'arguments': 'parameters', +- 'return_type': 'schema', +- 'datatype': 'type', +- 'mandatory': 'required', +- 'sample': 'examples', +- 'readonly': 'readOnly', +- 'min_length': 'minLength', +- 'max_length': 'maxLength', +- } +- if key in keys: +- return keys[key] +- else: +- return key +- +- def get_wm_item_prop(item, isparams=False): +- # Add prop into 'properties' and 'required' in 'Items Object' +- # 'Items Object' can be part of 'Schema Object' or 'Items Object', +- # and can use recursively +- prop_dict = {} +- # TODO(shu-mutou): this should be removed for production. +- # prop_dict['obj'] = inspect.getmembers(item) +- +- _item = item +- if wtypes.iscomplex(item): +- _item = weakref.ref(item) +- +- for a, i in inspect.getmembers(_item): +- if a == 'datatype': +- datatype = get_type_str(i, prop_dict) +- if datatype == 'array': +- # if array, do recursively +- prop_dict['items'] = inspect_wm_schema(i.item_type) +- elif datatype == 'object': +- # if obuject, do recursively +- prop_dict['items'] = inspect_wm_schema(i) +- elif a == 'default' and i: +- prop_dict[to_swag_key(a)] = i +- elif a == 'name' and isparams: +- prop_dict[to_swag_key(a)] = i +- elif a == 'mandatory' and i: +- prop_dict[to_swag_key(a)] = i +- elif a == 'readonly' and i: +- prop_dict[to_swag_key(a)] = i +- elif a == 'doc' and i is not None: +- prop_dict[to_swag_key(a)] = i +- +- if isparams and prop_dict['type'] in ['object', 'array']: +- prop_dict['schema'] = {'items': prop_dict['items'], +- 'type': prop_dict['type']} +- del prop_dict['type'] +- del prop_dict['items'] +- return prop_dict +- +- def get_wsattr_and_wsproperty(obj): +- ws_dict = {} +- for key, value in inspect.getmembers(obj): +- if (key[0] != '_' and +- (value.__class__.__name__ == 'wsattr' +- or value.__class__.__name__ == 'wsproperty')): +- ws_dict[key] = value +- return ws_dict +- +- def inspect_wm_schema(schema_obj, isparams=False): +- schema_dict = {} +- # TODO(shu-mutou): this should be removed for production. +- # schema_dict['obj'] = get_wsattr_and_wsproperty(schema_obj) +- ws_len = len(get_wsattr_and_wsproperty(schema_obj)) +- model_name = class_to_name_str(schema_obj) +- +- for key, obj in inspect.getmembers(schema_obj): +- if (key[0] != '_' and +- (obj.__class__.__name__ == 'wsattr' +- or obj.__class__.__name__ == 'wsproperty')): +- # TODO(shu-mutou): this should be removed for production. +- # _definitions[model_name][to_swag_key(key)] = \ +- # {'obj': inspect.getmembers(obj)} +- +- if ws_len == 1: +- # single schema +- schema_dict = get_wm_item_prop(obj, isparams) +- else: +- # multi property schema +- schema_dict.update({'type': 'object'}) +- # properties +- if 'items' not in schema_dict: +- schema_dict['items'] = {'properties': {}} +- prop = {key: get_wm_item_prop(obj, isparams)} +- # required as array of string +- if 'required' in prop[key] and prop[key]['required'] \ +- and isinstance(prop[key]['required'], bool): +- if 'required' not in schema_dict: +- schema_dict['required'] = [] +- schema_dict['required'].append(key) +- del prop[key]['required'] +- schema_dict['items']['properties'].update(prop) +- +- if schema_obj not in _all_wsme_types: +- if model_name not in _definitions: +- _definitions[model_name] = schema_dict +- schema_dict = {'$ref': "#/definitions/%s" % model_name} +- +- return schema_dict +- +- def get_wm_def(o): +- wsme = {'description': ''} +- for w, d in inspect.getmembers(o): +- if w == 'arguments': +- wsme[to_swag_key(w)] = [] +- for arg in d: +- # set each 'Parameter Object', it can include +- # 'Items Object' recursively +- item_dict = get_wm_item_prop(arg, True) +- # TODO: MUST be set one of +- # 'body|query|path|header|formData' +- item_dict['in'] = 'query' +- wsme[to_swag_key(w)].append(item_dict) +- # 'body' should be set due to existing 'body' option +- # in WSME expose +- if o.body_type is not None: +- # if body is set, last arg is it. +- wsme[to_swag_key(w)][-1]['in'] = 'body' +- elif w == 'doc' and d: +- wsme[to_swag_key(w)] = d +- elif w == 'return_type': +- wsme['responses'] = {'status': {'description': ''}} +- if d: +- if d in _all_wsme_types: +- # d is set single WSME type class or implicit type +- wsme['responses']['status'][to_swag_key(w)] = ( +- datatype_to_type_and_format( +- conv_class_name_to_type_str( +- class_to_name_str(d)))) +- else: +- # d is model class +- wsme['responses']['status'][to_swag_key(w)] = ( +- inspect_wm_schema(d, False)) +- doc = inspect.getdoc(d) +- if doc is not None: +- wsme['responses']['status']['description'] = doc +- elif w == 'status_code': +- wsme['responses'][d] = wsme['responses']['status'] +- del wsme['responses']['status'] +- # TODO(shu-mutou): this should be removed for production. +- # elif w == 'body_type': +- # wsme[to_swag_key(w)] = get_type_str(d) +- # elif w == 'extra_options' or w == 'ignore_extra_args' \ +- # or w == 'name' or w == 'pass_request': +- # wsme[to_swag_key(w)] = d +- # else: +- # wsme[to_swag_key(w)] = d +- return wsme +- +- c = _hierarchy[name] +- wsme_defs = {} +- for k, v in inspect.getmembers(c): +- if p_u.iscontroller(v): +- for m, o in inspect.getmembers(v): +- if m == "_wsme_definition": +- wsme_defs[k] = get_wm_def(o) +- +- # TODO(shu-mutou): this should be removed for production. +- # output wsme info into files by each controller for dev +- # import pprint +- # with open(name + '.txt', 'w') as fout: +- # pprint.pprint(wsme_defs, stream=fout, indent=2) +- +- return wsme_defs +- +- +-def get_controllers(name): +- """ +- get all the controllers associated with a path +- +- returns a dictionary of controllers indexed by their names. +- """ +- c = _hierarchy[name] +- controllers = {k: p_u._cfg(v) +- for k, v in c.__dict__.items() if p_u.iscontroller(v)} +- cacts = {k: v for k, v in c.__dict__.items() if k == '_custom_actions'} +- if len(cacts): +- controllers.update(cacts) +- return controllers +- +- +-def get_paths(): +- """ +- return all the registered paths +- +- loops through the hierarchy and retuns a list of tuples containing the +- paths and their methods. +- +- :returns: [(path, methods), ...] +- """ +- pathlist = [] +- for name in _hierarchy: +- fullpath = build_path(get_swag(name)) +- controllers = get_controllers(name) +- wsme_defs = get_wsme_defs(name) +- paths = get_controller_paths(controllers, wsme_defs) +- for path in paths: +- ptuple = (path_join(fullpath, path[0]), path[1]) +- pathlist.append(ptuple) +- return pathlist +- +- +-def get_swag(name): +- """return the swag metadata from an named controller.""" +- return _hierarchy.get(name).__swag +- +- +-def getrealname(method): +- """attempt to get a method's real name.""" +- argspec = inspect.getargspec(method) +- args = argspec[0] +- if args and args[0] == 'self': +- return method.__name__ +- if hasattr(method, '__func__'): +- method = method.__func__ +- +- func_closure = six.get_function_closure(method) +- +- # NOTE(sileht): if the closure is None we cannot look deeper, +- # so return actual argspec, this occurs when the method +- # is static for example. +- if func_closure is None: +- return method.__name__ +- +- closure = next( +- ( +- c for c in func_closure if six.callable(c.cell_contents) +- ), +- None +- ) +- method = closure.cell_contents +- return getrealname(method) +- +- +-def methods_get(name): +- """get all the methods for a named controller.""" +- c = _hierarchy[name] +- mlist = [] +- if hasattr(c, 'index') and p_u.iscontroller(c.index): +- cfg = p_u._cfg(c.index) +- if cfg.get('generic_handlers').get('DEFAULT'): +- mlist.append('get') +- mlist += cfg.get('allowed_methods') +- for i in c.__dict__: +- ii = getattr(c, i) +- if hasattr(ii, '__swag'): +- m = ii.__swag.get('method') +- if m is not None: +- mlist.append(m) +- return mlist +- +- +-def path_join(part1, part2): +- """join two url paths.""" +- if len(part2) == 0: +- return part1 +- sep = '/' +- if part1[-1] == sep: +- sep = '' +- return part1 + sep + part2 +Index: examples/industrial-ai-apiserver/README.md +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.BaseRevisionTextPatchEP +<+># industrial-ai-apiserver\n\n数据字典 & API:\n\n## 本地调试\n\n```bash\ncd industrial-ai-apiserver\n\n# 创建 python 虚拟环境\npython -m virtualenv .venv\n\n# 进入 python 虚拟环境\n.venv\\Scripts\\activate \n\n# 安装依赖\npip3 install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt\n\n# 清理原数据库\nrm -rf /tmp/industrialai.db\n\n# 初始化数据库\npython datainit.py\n\n# 启动 web 服务\npython run.py\n# python run.py 0.0.0.0 8080\n\n# 尝试访问\ncurl http://localhost:8080/users/\n```\n\n## 测试\n\n```bash\npython setup.py test -q\n\nstestr run\n\ntox\n```\n\n## 容器镜像制作和部署\n\n```bash\ndocker build -t industrialai .\n\ndocker stop industrialai; docker rm industrialai\n\ndocker run -d --name industrialai -p 8888:8080 industrialai\n\ncurl http://localhost:8888/users/\n``` +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +diff --git a/examples/industrial-ai-apiserver/README.md b/examples/industrial-ai-apiserver/README.md +--- a/examples/industrial-ai-apiserver/README.md (revision 2159b53baa4c86d9c1b739260824b7f89f935759) ++++ b/examples/industrial-ai-apiserver/README.md (date 1651652456000) +@@ -17,7 +17,7 @@ + pip3 install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt + + # 清理原数据库 +-rm -rf /tmp/industrialai.db ++#rm -rf /tmp/industrialai.db + + # 初始化数据库 + python datainit.py +@@ -27,7 +27,7 @@ + # python run.py 0.0.0.0 8080 + + # 尝试访问 +-curl http://localhost:8080/users/ ++curl http://localhost:8080/ + ``` + + ## 测试 +@@ -40,14 +40,22 @@ + tox + ``` + +-## 容器镜像制作和部署 ++## API-doc ++ ++### 安装 api-doc ++1. 安装npm: ++ - 前往 https://nodejs.org/en/ 下载node.js(集成npm)的最新版本 ++ - 在终端输入 npm -v ,输出npm版本号 ++2. npm install apidoc –g ++3. 参考: ++ - https://segmentfault.com/a/1190000011636199 ++ - https://apidocjs.com/#getting-started ++ - https://github.com/apidoc/apidoc ++ ++### 生成接口文档 + + ```bash +-docker build -t industrialai . +- +-docker stop industrialai; docker rm industrialai ++apidoc -i industrialai/api/controllers -o api-doc ++``` + +-docker run -d --name industrialai -p 8888:8080 industrialai +- +-curl http://localhost:8888/users/ +-``` +\ No newline at end of file ++点击 api-doc/index.html 访问 +\ No newline at end of file +Index: examples/industrial-ai-apiserver/.gitignore +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.BaseRevisionTextPatchEP +<+># Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n\n# Build\nE:\\\\dhuProject\\\\pecan-swagger\\\\examples\\\\industrial-ai-apiserver\\\\industrialai.db\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nenv/\nbuild/\ndevelop-eggs/\ndist/\neggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\n*.egg-info/\n.installed.cfg\n*.egg\ncloudkitty.egg-info\n.idea/\n.python-version\n\n# Configuration file\netc/cloudkitty/cloudkitty.conf.sample\netc/cloudkitty/policy.yaml.sample\n\n# vscode\n.vscode\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.stestr/\n.coverage\n.cache\ncoverage.xml\ncover\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\n\n# Sphinx documentation\ndoc/build/\n\n# Rope\n.ropeproject/\n\n# Others\n*.sqlite\n*.swp\n*~\n*.swn\n*.swo\n*.DS_Store\n\n# tox -e docs\nAUTHORS\nChangeLog\n\nreleasenotes/build\n +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +diff --git a/examples/industrial-ai-apiserver/.gitignore b/examples/industrial-ai-apiserver/.gitignore +--- a/examples/industrial-ai-apiserver/.gitignore (revision 2159b53baa4c86d9c1b739260824b7f89f935759) ++++ b/examples/industrial-ai-apiserver/.gitignore (date 1651652456000) +@@ -2,9 +2,6 @@ + __pycache__/ + *.py[cod] + +-# Build +-E:\\dhuProject\\pecan-swagger\\examples\\industrial-ai-apiserver\\industrialai.db +- + # C extensions + *.so + +Index: examples/industrial-ai-apiserver/datainit.py +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.BaseRevisionTextPatchEP +<+>from industrialai.db.model import Base, Role, User\nfrom sqlalchemy.orm import sessionmaker\nimport os\nfrom sqlalchemy import create_engine\n\ndef init_db():\n DB_URL = os.getenv('db_url') or \\\n \"sqlite:///E:\\\\dhuProject\\\\pecan-swagger\\\\examples\\\\industrial-ai-apiserver\\\\industrialai.db?check_same_thread=False\"\n engine = create_engine(DB_URL, echo=True)\n Base.metadata.drop_all(engine)\n Base.metadata.create_all(engine)\n Session = sessionmaker(bind=engine)\n session = Session()\n\n session.add_all([\n Role(title=\"expert\"),\n Role(title=\"government\"),\n Role(title=\"enterpriseAdmin\"),\n Role(title=\"member\"),\n Role(title=\"admin\"),\n User(account=\"admin\", pwd=\"123456\", roleId=3),\n User(account=\"member\", pwd=\"123456\", roleId=4),\n User(account=\"root\", pwd=\"123456\", roleId=5)\n ])\n session.commit()\n\n\ninit_db()\n +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +diff --git a/examples/industrial-ai-apiserver/datainit.py b/examples/industrial-ai-apiserver/datainit.py +--- a/examples/industrial-ai-apiserver/datainit.py (revision 2159b53baa4c86d9c1b739260824b7f89f935759) ++++ b/examples/industrial-ai-apiserver/datainit.py (date 1651652456000) +@@ -5,7 +5,7 @@ + + def init_db(): + DB_URL = os.getenv('db_url') or \ +- "sqlite:///E:\\dhuProject\\pecan-swagger\\examples\\industrial-ai-apiserver\\industrialai.db?check_same_thread=False" ++ "sqlite:///C:\\Users\\23360\\Desktop\\dhuProject\\pecan-swagger\\examples\\industrial-ai-apiserver\\industrialai.db?check_same_thread=False" + engine = create_engine(DB_URL, echo=True) + Base.metadata.drop_all(engine) + Base.metadata.create_all(engine) +Index: examples/industrial-ai-apiserver/industrialai/db/api.py +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.BaseRevisionTextPatchEP +<+>from industrialai.db import model as db_model\nimport logging\nimport math\nimport os\nfrom pecan import abort\nfrom sqlalchemy import create_engine, func\nfrom sqlalchemy.orm import exc\nfrom sqlalchemy.orm import sessionmaker\n\nlogger = logging.getLogger(__name__)\nDomain = \"sqlalchemy\"\n\n\n_ENGINE = None\n_SESSION_MAKER = None\n\n\ndef get_engine():\n global _ENGINE\n if _ENGINE is not None:\n return _ENGINE\n DB_URL = (\n os.getenv(\"db_url\") or\n \"sqlite:///E:\\\\dhuProject\\\\pecan-swagger\\\\examples\\\\\\\n industrial-ai-apiserver\\\\industrialai.db?check_same_thread=False\"\n )\n _ENGINE = create_engine(DB_URL, echo=True)\n return _ENGINE\n\n\ndef get_session_maker(engine):\n global _SESSION_MAKER\n if _SESSION_MAKER is not None:\n return _SESSION_MAKER\n _SESSION_MAKER = sessionmaker(bind=engine)\n return _SESSION_MAKER\n\n\ndef get_session():\n engine = get_engine()\n maker = get_session_maker(engine)\n session = maker()\n return session\n\n\nclass Connection(object):\n def __init__(self):\n pass\n\n def get_user(self, user_id):\n user = None\n query = get_session().query(db_model.User).filter_by(id=user_id)\n try:\n user = query.one()\n except exc.NoResultFound:\n logger.error(\"query by enterpriseId not found ...\")\n abort(404)\n return user\n\n def list_users(self, page_num, page_size, **kwargs):\n users = []\n # query = get_session().query(db_model.User.account,\n # db_model.User.name, db_model.User.roleId,\n # db_model.User.status, db_model.User.create_at, db_model.User.source)\n query = get_session().query(db_model.User)\n for k, v in kwargs.items():\n if v:\n if k == \"name\":\n query = query.filter(\n db_model.User.name.like(\"%{keyword}%\".\n format(keyword=v))\n )\n elif k == \"account\":\n query = query.filter(\n db_model.User.account.like(\"%{keyword}%\".\n format(keyword=v))\n )\n else:\n query = query.filter_by(**{k: v})\n try:\n users = query.slice((page_num-1)*page_size, page_size).all()\n num = query.count()\n pages = math.ceil(num/page_size)\n for item in users:\n item.pwd = None\n except exc.NoResultFound:\n logger.error(\"query all user occur error ...\")\n abort(404)\n return users, pages\n\n def update_user(self, user):\n id = user.get(\"id\")\n logger.info(\"user.id: %s\" % (id))\n try:\n session = get_session()\n session.query(db_model.User).filter_by(id=id).one()\n for k, v in user.items():\n if v is not None:\n session.query(db_model.User).filter_by(id=id). \\\n update({k: v})\n session.flush()\n session.commit()\n except exc.NoResultFound:\n logger.error(\"update user occur error ...\")\n abort(404)\n\n return user\n\n def delete_user(self, user_id):\n logger.info(\"user.id: %s\" % (user_id))\n try:\n session = get_session()\n user = session.query(db_model.User).filter_by(id=user_id).first()\n session.delete(user)\n session.flush()\n session.commit()\n except exc.NoResultFound:\n logger.error(\"delete user occur error ...\")\n abort(404)\n\n def add_user(self, user):\n db_user = db_model.User(\n name=user.get(\"name\"),\n account=user.get(\"account\"),\n pwd=user.get(\"pwd\"),\n phone=user.get(\"phone\"),\n roleId=user.get(\"roleId\"),\n enterpriseId=user.get(\"enterpriseId\"),\n status=user.get(\"status\"),\n )\n try:\n session = get_session()\n if not user.get(\"groupId\") is None:\n group = (\n session.query(db_model.Group)\n .filter_by(id=user.get(\"groupId\"))\n .one()\n )\n db_user.group = group\n session.add(db_user)\n session.flush()\n session.commit()\n except exc.NoResultFound:\n logger.error(\"add user occour error ...\")\n abort(404)\n # return session.query(db_model.Group).options(\n # joinedload(db_model.Group.users)).all()\n\n def list_roles(self, page_num, page_size):\n roles = []\n query = get_session().query(db_model.Role)\n try:\n roles = query.slice((page_num-1)*page_size, page_size).all()\n num = get_session().query(func.count(db_model.Role.id)).scalar()\n pages = math.ceil(num/page_size)\n except exc.NoResultFound:\n logger.error(\"query all roles occur error ...\")\n abort(404)\n return roles, pages\n +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +diff --git a/examples/industrial-ai-apiserver/industrialai/db/api.py b/examples/industrial-ai-apiserver/industrialai/db/api.py +--- a/examples/industrial-ai-apiserver/industrialai/db/api.py (revision 2159b53baa4c86d9c1b739260824b7f89f935759) ++++ b/examples/industrial-ai-apiserver/industrialai/db/api.py (date 1651652456000) +@@ -21,8 +21,9 @@ + return _ENGINE + DB_URL = ( + os.getenv("db_url") or +- "sqlite:///E:\\dhuProject\\pecan-swagger\\examples\\\ +- industrial-ai-apiserver\\industrialai.db?check_same_thread=False" ++ "sqlite:///C:\\Users\\23360\\Desktop\\dhuProject\\pecan-swagger\\" ++ "examples\\industrial-ai-apiserver\\" ++ "industrialai.db?check_same_thread=False " + ) + _ENGINE = create_engine(DB_URL, echo=True) + return _ENGINE +Index: examples/industrial-ai-apiserver/industrialai/api/controllers/root.py +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.BaseRevisionTextPatchEP +<+>from industrialai.api.controllers.v1 import controller as v1_controller\nfrom industrialai.api.expose import expose as wsexpose\nfrom industrialai.db import api\nfrom industrialai.db.model import User\nfrom industrialai.pkg.authentication import JWT\nimport logging\nfrom pecan import abort\nfrom pecan import expose\nfrom pecan import request\nfrom wsme import types as wtypes\nlogger = logging.getLogger(__name__)\n\n\nclass logForm:\n account = wtypes.text\n pwd = wtypes.text\n\n\nclass checkForm:\n id = int\n pwd = wtypes.text\n\n\nclass registryForm:\n account = wtypes.text\n pwd = wtypes.text\n phone = wtypes.text\n mail = wtypes.text\n name = wtypes.text\n number = wtypes.text\n\n\nclass RootController(object):\n\n @expose('json')\n def index(self):\n return None\n\n @wsexpose(wtypes.text, body=logForm)\n def signin(self, logForm):\n account = logForm.account\n pwd = logForm.pwd\n user = api.get_session().query(User). \\\n filter_by(account=account, pwd=pwd).first()\n if user is None:\n abort(401)\n token = JWT.createToken(user.id, user.roleId)\n return {\n \"token\": token\n }\n\n @wsexpose(wtypes.text, body=checkForm)\n def pwdcheck(self, checkForm):\n try:\n api.get_session().query(User). \\\n filter_by(id=checkForm.id, pwd=checkForm.pwd).first()\n except Exception:\n return \"Failure\"\n return \"Success\"\n\n @expose('json')\n def myself(self):\n token = request.cookies.get(\"token\")\n message = JWT.deToken(token)\n if message is None:\n abort(401)\n else:\n uId = message.get(\"userId\")\n user = api.get_session().query(User). \\\n filter_by(id=uId).first()\n user.pwd = None\n return user\n\n v1 = v1_controller.v1Controller()\n +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +diff --git a/examples/industrial-ai-apiserver/industrialai/api/controllers/root.py b/examples/industrial-ai-apiserver/industrialai/api/controllers/root.py +--- a/examples/industrial-ai-apiserver/industrialai/api/controllers/root.py (revision 2159b53baa4c86d9c1b739260824b7f89f935759) ++++ b/examples/industrial-ai-apiserver/industrialai/api/controllers/root.py (date 1652796268000) +@@ -7,7 +7,9 @@ + from pecan import abort + from pecan import expose + from pecan import request ++from pecan_swagger import decorators as swagger + from wsme import types as wtypes ++ + logger = logging.getLogger(__name__) + + +@@ -21,6 +23,7 @@ + pwd = wtypes.text + + ++@swagger.definitions('object') + class registryForm: + account = wtypes.text + pwd = wtypes.text +@@ -28,14 +31,60 @@ + mail = wtypes.text + name = wtypes.text + number = wtypes.text ++ check = checkForm() + + ++@swagger.response('myself', '401', 'fail') ++@swagger.response('index', '401', 'fail') ++@swagger.response('test', '200', 'test description') ++@swagger.method('test', 'index', 'signin', 'pwdcheck', 'myself') ++@swagger.path("/", "Root") + class RootController(object): ++ _custom_actions = { ++ 'test': ['GET'], ++ 'signin': ['POST'], ++ 'pwdcheck': ['POST'], ++ 'myself': ['POST'] ++ } ++ ++ @expose('json') ++ def test(self): ++ print("123") ++ return "test" + + @expose('json') + def index(self): + return None + ++ """ ++ @api {post} /signin signin ++ @apiName SignIn ++ @apiGroup User ++ ++ @apiBody {String} account 账号 ++ @apiBody {String} pwd 密码 ++ ++ @apiSuccess {String} token ++ @apiSuccessExample Success-Response: ++ { ++ "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9. ++ eyJleHAiOjE2NDYzNzc2MjksInVzZXJJZCI6MSwiZ3JvdXBJZCI6MSwicm9sZUlkIjozfQ. ++ fEqaaS8c2A_i-2W9D_L76mJac2vcyoS51g6trXr34lM" ++ } ++ ++ @apiError Unauthorized ++ @apiErrorExample {json} Error-Response: ++ { ++ "faultcode": "Client", ++ "faultstring": "This server could not verify that you are ++ authorized to access the document you requested. Either ++ you supplied the wrong credentials (e.g., bad password), ++ or your browser does not understand how to supply the ++ credentials required.", ++ "debuginfo": null ++ } ++ """ ++ + @wsexpose(wtypes.text, body=logForm) + def signin(self, logForm): + account = logForm.account +@@ -49,6 +98,22 @@ + "token": token + } + ++ """ ++ @api {post} /pwdcheck check ++ @apiName PwdCheck ++ @apiGroup User ++ ++ @apiBody {Integer} id 用户id ++ @apiBody {String} pwd 密码 ++ ++ @apiSuccess {Bool} result ++ @apiSuccessExample Success-Response: ++ HTTP/1.1 OK 200 ++ { ++ "result": True True:校验通过/False:校验失败 ++ } ++ """ ++ + @wsexpose(wtypes.text, body=checkForm) + def pwdcheck(self, checkForm): + try: +@@ -58,6 +123,24 @@ + return "Failure" + return "Success" + ++ """@api {get} /myself get_myself ++ @apiName GetMyself ++ @apiGroup User ++ ++ @apiSuccess (200) {Object} user ++ @apiSuccessExample Success-Response: ++ { ++ "id": 6, 用户id ++ "number": null, 身份证号 ++ "roleId": 3, 角色 ++ "status": 1, 状态:0/1 ++ "create_at": "2022-03-09 11:24:18.856936", 创建时间 ++ "name": "test", 名称 ++ "account": "testsss1-put", 账号 ++ "phone": "12345", 手机号 ++ } ++ """ ++ + @expose('json') + def myself(self): + token = request.cookies.get("token") +Index: examples/industrial-ai-apiserver/industrialai/api/controllers/v1/user.py +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.BaseRevisionTextPatchEP +<+>from industrialai.pkg.authentication import JWT\nfrom industrialai.pkg.page import get_page\nfrom pecan import abort\nfrom pecan import expose\nfrom pecan import request\nfrom pecan.rest import RestController\n\n\nclass UserController(RestController):\n\n @expose(\"json\")\n def get(self, **kwargs):\n page_num, page_size = get_page(request.body)\n validKeys = [\"enterpriseId\", \"name\", \"account\", \"roleId\", \"status\"]\n kwargs = {k: v for k, v in kwargs.items() if k in validKeys}\n token = request.cookies.get(\"token\")\n message = JWT.deToken(token)\n if message is None:\n abort(401)\n else:\n eId = message.get(\"enterpriseId\")\n rId = message.get(\"roleId\")\n if rId != 5:\n kwargs[\"enterpriseId\"] = eId\n db_conn = request.db_conn\n users, pages = db_conn.list_users(page_num, page_size, **kwargs)\n return {\n \"data\": users,\n \"pages\": pages\n }\n\n @expose(\"json\")\n def get_one(self, user_id):\n db_conn = request.db_conn\n user = db_conn.get_user(user_id)\n user.pwd = None\n return user\n\n @expose(\"json\")\n def post(self, user):\n db_conn = request.db_conn\n db_conn.add_user(user)\n return \"Success\"\n\n @expose(\"json\")\n def put(self, user):\n token = request.cookies.get(\"token\")\n message = JWT.deToken(token)\n if message is None:\n abort(401)\n else:\n uId = message.get(\"userId\")\n rId = message.get(\"roleId\")\n if uId != int(user.get(\"id\")) and rId != 5:\n abort(401)\n elif uId == int(user.get(\"id\")):\n user[\"roleId\"] = None\n user[\"phone\"] = None\n db_conn = request.db_conn\n db_conn.update_user(user)\n return \"Success\"\n +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +diff --git a/examples/industrial-ai-apiserver/industrialai/api/controllers/v1/user.py b/examples/industrial-ai-apiserver/industrialai/api/controllers/v1/user.py +--- a/examples/industrial-ai-apiserver/industrialai/api/controllers/v1/user.py (revision 2159b53baa4c86d9c1b739260824b7f89f935759) ++++ b/examples/industrial-ai-apiserver/industrialai/api/controllers/v1/user.py (date 1652794836000) +@@ -4,9 +4,64 @@ + from pecan import expose + from pecan import request + from pecan.rest import RestController ++from pecan_swagger import decorators as swagger + + ++@swagger.response('put', '401', 'fail') ++@swagger.response('post', '401', 'fail') ++@swagger.response('get_one', '401', 'fail') ++@swagger.response('get', '401', 'fail') ++@swagger.response('get', '200', 'test description') ++@swagger.parameter('get', {'name': "status", ++ 'in': "query", ++ 'description': 'Status values that need to be considered for filter', ++ 'required': True, ++ 'type': 'array', ++ 'items': {'type': 'string', ++ 'enum': ["available", "pending", "sold"], ++ 'default': "available"}, ++ 'collectionFormat': "multi"}) ++@swagger.operationId('get', 'get users') ++@swagger.description('get', 'get users information') ++@swagger.summary('get', 'get users information') ++@swagger.tags('put', 'user') ++@swagger.tags('post', 'user') ++@swagger.tags('get', 'user') ++@swagger.tags('get_one', 'user') ++@swagger.method('get', 'get_one', 'post', 'put') ++@swagger.path("users", "User", "Root") + class UserController(RestController): ++ """@api {get} /v1/users/?name= get_all_users ++ ++ @apiName GetAllUsers ++ @apiGroup User ++ ++ @apiParam {String} name name of user 根据名称进行查询(使用like进行匹配) ++ @apiParam {Integer} status status of user 根据用户状态过滤 ++ @apiParam {String} account account of user 根据账号进行查询(使用like进行匹配) ++ @apiParam {Integer} roleId role of user 根据角色进行过滤 ++ ++ @apiSuccess (200) {Object[]} users the list of user ++ @apiSuccessExample Success-Response: ++ { ++ "data": [ ++ { ++ "id": 6, 用户id ++ "number": null, 身份证号 ++ "roleId": 3, 角色 ++ "status": 1, 状态:0/1 ++ "create_at": "2022-03-09 11:24:18.856936", 创建时间 ++ "name": "test", 名称 ++ "account": "testsss1-put", 账号 ++ "phone": "12345", 手机号 ++ "email": null, 邮箱 ++ }, ++ ], ++ "pages": 10 总页数 ++ “total": 10 总数 ++ } ++ """ ++ + + @expose("json") + def get(self, **kwargs): +@@ -29,6 +84,27 @@ + "pages": pages + } + ++ """@api {get} /v1/users/ get_one_user ++ @apiName GetOneUser ++ @apiGroup User ++ ++ @apiParam {Integer} userId id of user ++ ++ @apiSuccess (200) {Object} user ++ @apiSuccessExample Success-Response: ++ { ++ "id": 6, 用户id ++ "number": null, 身份证号 ++ "roleId": 3, 角色 ++ "status": 1, 状态:0/1 ++ "create_at": "2022-03-09 11:24:18.856936", 创建时间 ++ "name": "test", 名称 ++ "account": "testsss1-put", 账号 ++ "phone": "12345", 手机号 ++ "email": null, 邮箱 ++ } ++ """ ++ + @expose("json") + def get_one(self, user_id): + db_conn = request.db_conn +@@ -36,12 +112,78 @@ + user.pwd = None + return user + ++ """@api {post} /v1/users create_user ++ @apiName CreateUser ++ @apiGroup User ++ ++ @apiBody {Object} user ++ @apiParamExample {json} Request-Example: ++ { ++ "user": { ++ "account": "testsss1-put", ++ "phone": "12345", ++ "email": xxx@xxxx.com, ++ "name": "test", ++ "pwd": "123456", ++ "roleId": 3 ++ } ++ } ++ ++ @apiSuccess {String} result ++ @apiSuccessExample Success-Response: ++ HTTP/1.1 200 OK ++ ++ @apiError Unauthorized ++ @apiErrorExample {json} Error-Response: ++ { ++ "faultcode": "Client", ++ "faultstring": "This server could not verify that you are ++ authorized to access the document you requested. Either you ++ supplied the wrong credentials (e.g., bad password), or your ++ browser does not understand how to supply the credentials ++ required.", ++ "debuginfo": null ++ } ++ """ ++ + @expose("json") + def post(self, user): + db_conn = request.db_conn + db_conn.add_user(user) + return "Success" + ++ """@api {put} /v1/users update_user ++ @apiName UpdateUser ++ @apiGroup User ++ ++ @apiBody {Object} user ++ @apiParamExample {json} Request-Example: ++ { ++ "user": { ++ "id": 6, ++ "enterpriseId": 1, ++ "enterpriseGroupId": 1, ++ "name": "test" ++ } ++ } ++ ++ @apiSuccess {String} result ++ @apiSuccessExample Success-Response: ++ "Success" ++ ++ @apiError Unauthorized ++ @apiErrorExample {json} Error-Response: ++ { ++ "faultcode": "Client", ++ "faultstring": "This server could not verify that you are ++ authorized to access the document you requested. Either you ++ supplied the wrong credentials (e.g., bad password), or your ++ browser does not understand how to supply the credentials ++ required.", ++ "debuginfo": null ++ } ++ """ ++ + @expose("json") + def put(self, user): + token = request.cookies.get("token") +Index: examples/industrial-ai-apiserver/industrialai/api/controllers/v1/role.py +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.BaseRevisionTextPatchEP +<+>from industrialai.pkg.page import get_page\nfrom pecan import expose\nfrom pecan import request\nfrom pecan.rest import RestController\n\n\nclass RoleController(RestController):\n\n @expose('json')\n def get_all(self):\n page_num, page_size = get_page(request.body)\n db_conn = request.db_conn\n roles, num = db_conn.list_roles(page_num, page_size)\n return {\n \"data\": roles,\n \"pages\": num\n }\n +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +diff --git a/examples/industrial-ai-apiserver/industrialai/api/controllers/v1/role.py b/examples/industrial-ai-apiserver/industrialai/api/controllers/v1/role.py +--- a/examples/industrial-ai-apiserver/industrialai/api/controllers/v1/role.py (revision 2159b53baa4c86d9c1b739260824b7f89f935759) ++++ b/examples/industrial-ai-apiserver/industrialai/api/controllers/v1/role.py (date 1652794554000) +@@ -1,10 +1,44 @@ ++from industrialai.api.expose import expose as wsexpose + from industrialai.pkg.page import get_page + from pecan import expose + from pecan import request + from pecan.rest import RestController ++from pecan_swagger import decorators as swagger ++from wsme import types as wstypes ++ + ++class Roletest(wstypes.Base): ++ message = wstypes.text ++ roleId = int + ++ ++@swagger.response('get_all', '401', 'fail') ++@swagger.method('test', 'get_all') ++@swagger.path("roles", "Role", "Root") + class RoleController(RestController): ++ _custom_actions = { ++ 'test': ['POST'] ++ } ++ ++ """ ++ @api {get} /v1/roles get_all_roles ++ @apiName GetAllRoles ++ @apiGroup Role ++ ++ @apiSuccess {Object[]} roles ++ @apiSuccessExample {type} Success-Response: ++ HTTP/1.1 200 OK ++ [ ++ “data":[ 角色列表 ++ { ++ "title": "expert", 角色名称 ++ "id": 1 角色id ++ }, ++ ] ++ "pages": 12 总页数 ++ “total": 10 总数 ++ ] ++ """ + + @expose('json') + def get_all(self): +@@ -15,3 +49,11 @@ + "data": roles, + "pages": num + } ++ ++ @wsexpose(Roletest, body=Roletest) ++ def test(self, role): ++ print(role) ++ role.message = "out" ++ role.roleId = 1 ++ print(role) ++ return role +Index: .gitignore +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.BaseRevisionTextPatchEP +<+># Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n\n# Build\nexample_wsme_app.json\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nenv/\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\n*.egg-info/\n.installed.cfg\n*.egg\n\n# PyInstaller\n# Usually these files are written by a python script from a template\n# before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.coverage\n.cache\nnosetests.xml\ncoverage.xml\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +diff --git a/.gitignore b/.gitignore +--- a/.gitignore (revision 2159b53baa4c86d9c1b739260824b7f89f935759) ++++ b/.gitignore (date 1653373170493) +@@ -55,3 +55,5 @@ + + # PyBuilder + target/ ++/examples/industrial-ai-apiserver/test.json ++/examples/industrial-ai-apiserver/test.yaml +diff --git a/pecan_swagger/__init__.py b/pecan_swagger/__init__.py +deleted file mode 100644 diff --git a/.idea/shelf/Uncommitted_changes_before_Checkout_at_2022_5_24__14_46__Changes_.xml b/.idea/shelf/Uncommitted_changes_before_Checkout_at_2022_5_24__14_46__Changes_.xml new file mode 100644 index 0000000000000000000000000000000000000000..436185fddf02c9f241ad8a447831b8144e4868ea --- /dev/null +++ b/.idea/shelf/Uncommitted_changes_before_Checkout_at_2022_5_24__14_46__Changes_.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000000000000000000000000000000000000..94a25f7f4cb416c083d265558da75d457237d671 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000000000000000000000000000000000000..11edc86381186b4238eab5594de549d6814db0a0 --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1651814739960 + + + + + + + \ No newline at end of file diff --git a/API Reference.md b/API Reference.md new file mode 100644 index 0000000000000000000000000000000000000000..2169d98915a1ca931fdcde7489c908d50f23f030 --- /dev/null +++ b/API Reference.md @@ -0,0 +1,142 @@ +# Pecan Swagger API Reference + +```python +pecan_swagger.utils.swagger_build(title, version) +``` +Parameters: +- **title** (String): The title of the application. +- **version** (String): Provides the version of the application API. + +Returns: +- **Swagger Dict** + +```python +pecan_swagger.utils.add_info(s, description=None, terms_of_service=None, contact=None, license=None) +``` +Parameters: +- **s** (Swagger Dict): Swagger Dict create by swagger_build. +- **description** (String): A short description of the application. +- **terms_of_service** (String): The Terms of Service for the API. +- **contact** (Dict): The contact information for the exposed API. Key should be in "name", "url", "email" or patterned string started with "x-". Values should be string for contact. +- **license** (String): Provides the version of the application API. Key should be in "name", "url" or patterned string started with "x-". Values should be string for license. + +Returns: +- **Swagger Dict** + +```python +pecan_swagger.utils.add_external_docs(s, external_docs) +``` +Parameters: +- **s** (Swagger Dict): Swagger Dict create by swagger_build. +- **external_docs** (Dict): Allows referencing an external resource for extended documentation. Key must include "url" and should be in "description", or patterned string started with "x-". Values should be string for license. + +Returns: +- **Swagger Dict** + +```python +pecan_swagger.utils.add_host(s, host) +``` +Parameters: +- **s** (Swagger Dict): Swagger Dict create by swagger_build. +- **host** (String): The host (name or ip) serving the API. +This MUST be the host only and does not include the scheme nor sub-paths. + +Returns: +- **Swagger Dict** + +```python +pecan_swagger.utils.add_base_path(s, base_path) +``` +Parameters: +- **s** (Swagger Dict): Swagger Dict create by swagger_build. +- **base_path** (String): The base path on which the API is served, which is relative to the host. If it is not included, the API is served directly under the host. + +Returns: +- **Swagger Dict** + +```python +pecan_swagger.utils.print_json(s, output_path) +``` +Parameters: +- **s** (Swagger Dict): Swagger Dict create by swagger_build. +- **base_path** (String): Path to output the json file. + +```python +pecan_swagger.utils.print_yaml(s, output_path) +``` +Parameters: +- **s** (Swagger Dict): Swagger Dict create by swagger_build. +- **base_path** (String): Path to output the json file. + + +```python +@pecan_swagger.decorators.path(endpoint, name, parent=None) +``` +Parameters: +- **endpoint** (String): The root uri for this controller. +- **base_path** (String): The name of this path. +- **parent** (String): An optional path name to indicate a parent/child relationship between this path and another. + +```python +@pecan_swagger.decorators.response(method_name, res_code, des, schema=None) +``` +Parameters: +- **method_name** (String): The name of the method. +- **res_code** (String): The HTTP response code of this response. +- **des** (String): The description of this response. +- **schema** (String): The schema of this response. + +```python +@pecan_swagger.decorators.tags(method_name, *tags) +``` +Parameters: +- **method_name** (String): The name of the method. +- ***tags** (String): The tags of this response. + +```python +@pecan_swagger.decorators.summary(method_name, summary) +``` +Parameters: +- **method_name** (String): The name of the method. +- **summary** (String): The summary of the method. + +```python +@pecan_swagger.decorators.description(method_name, des) +``` +Parameters: +- **method_name** (String): The name of the method. +- **des** (String): The description of the method. + +```python +@pecan_swagger.decorators.operationId(method_name, oi) +``` +Parameters: +- **method_name** (String): The name of the method. +- **oi** (String): The operationId of the method. + +```python +@pecan_swagger.decorators.method(*args) +``` +Parameters: +- ***args** (String): The names of the method. + +```python +@pecan_swagger.decorators.definitions(t, *args) +``` +Parameters: +- **t** (String): The type of the definition. +- ***args** (String): The properties of the definition. + +```python +@pecan_swagger.decorators.definitions(t, *args) +``` +Parameters: +- **t** (String): The type of the definition. +- ***args** (String): The properties of the definition. + +```python +@pecan_swagger.decorators.parameter(method_name, *args) +``` +Parameters: +- **method_name** (String): The name of the method. +- ***args** (String): The parameters of the definition. \ No newline at end of file diff --git a/examples/.DS_Store b/examples/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..11b20ae6c07fee8a9188fce9c11763f834b55ea0 Binary files /dev/null and b/examples/.DS_Store differ diff --git a/examples/industrial-ai-apiserver/README.md b/examples/industrial-ai-apiserver/README.md index a9a80262b6f07825b6f7eef5d1233bff70c4f6d7..fd4789cbe0afb99fa6c88a047ea97a25b45909c9 100644 --- a/examples/industrial-ai-apiserver/README.md +++ b/examples/industrial-ai-apiserver/README.md @@ -17,7 +17,7 @@ python -m virtualenv .venv pip3 install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt # 清理原数据库 -rm -rf /tmp/industrialai.db +#rm -rf /tmp/industrialai.db # 初始化数据库 python datainit.py @@ -27,7 +27,7 @@ python run.py # python run.py 0.0.0.0 8080 # 尝试访问 -curl http://localhost:8080/users/ +curl http://localhost:8080/ ``` ## 测试 @@ -40,14 +40,22 @@ stestr run tox ``` -## 容器镜像制作和部署 +## API-doc -```bash -docker build -t industrialai . +### 安装 api-doc +1. 安装npm: + - 前往 https://nodejs.org/en/ 下载node.js(集成npm)的最新版本 + - 在终端输入 npm -v ,输出npm版本号 +2. npm install apidoc –g +3. 参考: + - https://segmentfault.com/a/1190000011636199 + - https://apidocjs.com/#getting-started + - https://github.com/apidoc/apidoc -docker stop industrialai; docker rm industrialai +### 生成接口文档 -docker run -d --name industrialai -p 8888:8080 industrialai +```bash +apidoc -i industrialai/api/controllers -o api-doc +``` -curl http://localhost:8888/users/ -``` \ No newline at end of file +点击 api-doc/index.html 访问 \ No newline at end of file diff --git a/examples/industrial-ai-apiserver/datainit.py b/examples/industrial-ai-apiserver/datainit.py index bf11856b32059fa23fbbdf232c12c1c219cbd3fd..64814c81c3aba30f492e88a71376c2f2120ab08a 100644 --- a/examples/industrial-ai-apiserver/datainit.py +++ b/examples/industrial-ai-apiserver/datainit.py @@ -5,7 +5,7 @@ from sqlalchemy import create_engine def init_db(): DB_URL = os.getenv('db_url') or \ - "sqlite:///E:\\dhuProject\\pecan-swagger\\examples\\industrial-ai-apiserver\\industrialai.db?check_same_thread=False" + "sqlite:///C:\\Users\\23360\\Desktop\\dhuProject\\pecan-swagger\\examples\\industrial-ai-apiserver\\industrialai.db?check_same_thread=False" engine = create_engine(DB_URL, echo=True) Base.metadata.drop_all(engine) Base.metadata.create_all(engine) diff --git a/examples/industrial-ai-apiserver/industrialai/api/controllers/root.py b/examples/industrial-ai-apiserver/industrialai/api/controllers/root.py index fd82b3a291eb8546c47fe2916e0dffedf284f9d2..2462069b60cc43040d64eb4e8196cf7728f7639a 100644 --- a/examples/industrial-ai-apiserver/industrialai/api/controllers/root.py +++ b/examples/industrial-ai-apiserver/industrialai/api/controllers/root.py @@ -7,7 +7,9 @@ import logging from pecan import abort from pecan import expose from pecan import request +from pecan_swagger import decorators as swagger from wsme import types as wtypes + logger = logging.getLogger(__name__) @@ -21,6 +23,7 @@ class checkForm: pwd = wtypes.text +@swagger.definitions('object') class registryForm: account = wtypes.text pwd = wtypes.text @@ -28,14 +31,60 @@ class registryForm: mail = wtypes.text name = wtypes.text number = wtypes.text + check = checkForm() +@swagger.response('myself', '401', 'fail') +@swagger.response('index', '401', 'fail') +@swagger.response('test', '200', 'test description') +@swagger.method('test', 'index', 'signin', 'pwdcheck', 'myself') +@swagger.path("/", "Root") class RootController(object): + _custom_actions = { + 'test': ['GET'], + 'signin': ['POST'], + 'pwdcheck': ['POST'], + 'myself': ['POST'] + } + + @expose('json') + def test(self): + print("123") + return "test" @expose('json') def index(self): return None + """ + @api {post} /signin signin + @apiName SignIn + @apiGroup User + + @apiBody {String} account 账号 + @apiBody {String} pwd 密码 + + @apiSuccess {String} token + @apiSuccessExample Success-Response: + { + "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9. + eyJleHAiOjE2NDYzNzc2MjksInVzZXJJZCI6MSwiZ3JvdXBJZCI6MSwicm9sZUlkIjozfQ. + fEqaaS8c2A_i-2W9D_L76mJac2vcyoS51g6trXr34lM" + } + + @apiError Unauthorized + @apiErrorExample {json} Error-Response: + { + "faultcode": "Client", + "faultstring": "This server could not verify that you are + authorized to access the document you requested. Either + you supplied the wrong credentials (e.g., bad password), + or your browser does not understand how to supply the + credentials required.", + "debuginfo": null + } + """ + @wsexpose(wtypes.text, body=logForm) def signin(self, logForm): account = logForm.account @@ -49,6 +98,22 @@ class RootController(object): "token": token } + """ + @api {post} /pwdcheck check + @apiName PwdCheck + @apiGroup User + + @apiBody {Integer} id 用户id + @apiBody {String} pwd 密码 + + @apiSuccess {Bool} result + @apiSuccessExample Success-Response: + HTTP/1.1 OK 200 + { + "result": True True:校验通过/False:校验失败 + } + """ + @wsexpose(wtypes.text, body=checkForm) def pwdcheck(self, checkForm): try: @@ -58,6 +123,24 @@ class RootController(object): return "Failure" return "Success" + """@api {get} /myself get_myself + @apiName GetMyself + @apiGroup User + + @apiSuccess (200) {Object} user + @apiSuccessExample Success-Response: + { + "id": 6, 用户id + "number": null, 身份证号 + "roleId": 3, 角色 + "status": 1, 状态:0/1 + "create_at": "2022-03-09 11:24:18.856936", 创建时间 + "name": "test", 名称 + "account": "testsss1-put", 账号 + "phone": "12345", 手机号 + } + """ + @expose('json') def myself(self): token = request.cookies.get("token") diff --git a/examples/industrial-ai-apiserver/industrialai/api/controllers/v1/role.py b/examples/industrial-ai-apiserver/industrialai/api/controllers/v1/role.py index 34bb33bba79ccdbf314933856fd96f5ee55afc03..9faace8868ab1539cc57678354c7536c35ac9068 100644 --- a/examples/industrial-ai-apiserver/industrialai/api/controllers/v1/role.py +++ b/examples/industrial-ai-apiserver/industrialai/api/controllers/v1/role.py @@ -1,10 +1,44 @@ +from industrialai.api.expose import expose as wsexpose from industrialai.pkg.page import get_page from pecan import expose from pecan import request from pecan.rest import RestController +from pecan_swagger import decorators as swagger +from wsme import types as wstypes +class Roletest(wstypes.Base): + message = wstypes.text + roleId = int + + +@swagger.response('get_all', '401', 'fail') +@swagger.method('test', 'get_all') +@swagger.path("roles", "Role", "Root") class RoleController(RestController): + _custom_actions = { + 'test': ['POST'] + } + + """ + @api {get} /v1/roles get_all_roles + @apiName GetAllRoles + @apiGroup Role + + @apiSuccess {Object[]} roles + @apiSuccessExample {type} Success-Response: + HTTP/1.1 200 OK + [ + “data":[ 角色列表 + { + "title": "expert", 角色名称 + "id": 1 角色id + }, + ] + "pages": 12 总页数 + “total": 10 总数 + ] + """ @expose('json') def get_all(self): @@ -15,3 +49,11 @@ class RoleController(RestController): "data": roles, "pages": num } + + @wsexpose(Roletest, body=Roletest) + def test(self, role): + print(role) + role.message = "out" + role.roleId = 1 + print(role) + return role diff --git a/examples/industrial-ai-apiserver/industrialai/api/controllers/v1/user.py b/examples/industrial-ai-apiserver/industrialai/api/controllers/v1/user.py index 3c9132778429c786f2883c67ed67ebc7d8061812..5a837490f9c49900e0ed379ff7865f039dcf281c 100644 --- a/examples/industrial-ai-apiserver/industrialai/api/controllers/v1/user.py +++ b/examples/industrial-ai-apiserver/industrialai/api/controllers/v1/user.py @@ -4,9 +4,64 @@ from pecan import abort from pecan import expose from pecan import request from pecan.rest import RestController +from pecan_swagger import decorators as swagger +@swagger.response('put', '401', 'fail') +@swagger.response('post', '401', 'fail') +@swagger.response('get_one', '401', 'fail') +@swagger.response('get', '401', 'fail') +@swagger.response('get', '200', 'test description') +@swagger.parameter('get', {'name': "status", + 'in': "query", + 'description': 'Status values that need to be considered for filter', + 'required': True, + 'type': 'array', + 'items': {'type': 'string', + 'enum': ["available", "pending", "sold"], + 'default': "available"}, + 'collectionFormat': "multi"}) +@swagger.operationId('get', 'get users') +@swagger.description('get', 'get users information') +@swagger.summary('get', 'get users information') +@swagger.tags('put', 'user') +@swagger.tags('post', 'user') +@swagger.tags('get', 'user') +@swagger.tags('get_one', 'user') +@swagger.method('get', 'get_one', 'post', 'put') +@swagger.path("users", "User", "Root") class UserController(RestController): + """@api {get} /v1/users/?name= get_all_users + + @apiName GetAllUsers + @apiGroup User + + @apiParam {String} name name of user 根据名称进行查询(使用like进行匹配) + @apiParam {Integer} status status of user 根据用户状态过滤 + @apiParam {String} account account of user 根据账号进行查询(使用like进行匹配) + @apiParam {Integer} roleId role of user 根据角色进行过滤 + + @apiSuccess (200) {Object[]} users the list of user + @apiSuccessExample Success-Response: + { + "data": [ + { + "id": 6, 用户id + "number": null, 身份证号 + "roleId": 3, 角色 + "status": 1, 状态:0/1 + "create_at": "2022-03-09 11:24:18.856936", 创建时间 + "name": "test", 名称 + "account": "testsss1-put", 账号 + "phone": "12345", 手机号 + "email": null, 邮箱 + }, + ], + "pages": 10 总页数 + “total": 10 总数 + } + """ + @expose("json") def get(self, **kwargs): @@ -29,6 +84,27 @@ class UserController(RestController): "pages": pages } + """@api {get} /v1/users/ get_one_user + @apiName GetOneUser + @apiGroup User + + @apiParam {Integer} userId id of user + + @apiSuccess (200) {Object} user + @apiSuccessExample Success-Response: + { + "id": 6, 用户id + "number": null, 身份证号 + "roleId": 3, 角色 + "status": 1, 状态:0/1 + "create_at": "2022-03-09 11:24:18.856936", 创建时间 + "name": "test", 名称 + "account": "testsss1-put", 账号 + "phone": "12345", 手机号 + "email": null, 邮箱 + } + """ + @expose("json") def get_one(self, user_id): db_conn = request.db_conn @@ -36,12 +112,78 @@ class UserController(RestController): user.pwd = None return user + """@api {post} /v1/users create_user + @apiName CreateUser + @apiGroup User + + @apiBody {Object} user + @apiParamExample {json} Request-Example: + { + "user": { + "account": "testsss1-put", + "phone": "12345", + "email": xxx@xxxx.com, + "name": "test", + "pwd": "123456", + "roleId": 3 + } + } + + @apiSuccess {String} result + @apiSuccessExample Success-Response: + HTTP/1.1 200 OK + + @apiError Unauthorized + @apiErrorExample {json} Error-Response: + { + "faultcode": "Client", + "faultstring": "This server could not verify that you are + authorized to access the document you requested. Either you + supplied the wrong credentials (e.g., bad password), or your + browser does not understand how to supply the credentials + required.", + "debuginfo": null + } + """ + @expose("json") def post(self, user): db_conn = request.db_conn db_conn.add_user(user) return "Success" + """@api {put} /v1/users update_user + @apiName UpdateUser + @apiGroup User + + @apiBody {Object} user + @apiParamExample {json} Request-Example: + { + "user": { + "id": 6, + "enterpriseId": 1, + "enterpriseGroupId": 1, + "name": "test" + } + } + + @apiSuccess {String} result + @apiSuccessExample Success-Response: + "Success" + + @apiError Unauthorized + @apiErrorExample {json} Error-Response: + { + "faultcode": "Client", + "faultstring": "This server could not verify that you are + authorized to access the document you requested. Either you + supplied the wrong credentials (e.g., bad password), or your + browser does not understand how to supply the credentials + required.", + "debuginfo": null + } + """ + @expose("json") def put(self, user): token = request.cookies.get("token") diff --git a/examples/industrial-ai-apiserver/industrialai/db/api.py b/examples/industrial-ai-apiserver/industrialai/db/api.py index 2d5825ddacc18587a2e3653730250c25d20b9457..3a6ecd3361894b539fc721fe5caadf31130a7fe8 100644 --- a/examples/industrial-ai-apiserver/industrialai/db/api.py +++ b/examples/industrial-ai-apiserver/industrialai/db/api.py @@ -5,7 +5,6 @@ import os from pecan import abort from sqlalchemy import create_engine, func from sqlalchemy.orm import exc -from sqlalchemy.orm import joinedload from sqlalchemy.orm import sessionmaker logger = logging.getLogger(__name__) @@ -22,7 +21,9 @@ def get_engine(): return _ENGINE DB_URL = ( os.getenv("db_url") or - "sqlite:///E:\\dhuProject\\pecan-swagger\\examples\\industrial-ai-apiserver\\industrialai.db?check_same_thread=False" + "sqlite:///C:\\Users\\23360\\Desktop\\dhuProject\\pecan-swagger\\" + "examples\\industrial-ai-apiserver\\" + "industrialai.db?check_same_thread=False " ) _ENGINE = create_engine(DB_URL, echo=True) return _ENGINE diff --git a/examples/industrial-ai-apiserver/industrialai/db/model.py b/examples/industrial-ai-apiserver/industrialai/db/model.py index 6c5c9811e5b1ac4740f628da5251515df7dfd955..3c7492cbc76778259de808a7fc694bb9d20e24a4 100644 --- a/examples/industrial-ai-apiserver/industrialai/db/model.py +++ b/examples/industrial-ai-apiserver/industrialai/db/model.py @@ -4,9 +4,7 @@ from sqlalchemy import DateTime from sqlalchemy.ext.declarative import declarative_base from sqlalchemy import ForeignKey from sqlalchemy import Integer -from sqlalchemy.orm import relationship from sqlalchemy import String -from sqlalchemy import Table Base = declarative_base() @@ -14,6 +12,7 @@ Base = declarative_base() def init_db(_ENGINE): Base.metadata.create_all(_ENGINE) + class User(Base): __tablename__ = 'user' id = Column(Integer, primary_key=True) @@ -32,4 +31,3 @@ class Role(Base): __tablename__ = 'role' id = Column(Integer, primary_key=True) title = Column(String(32)) - diff --git a/examples/industrial-ai-apiserver/myapp-doc.py b/examples/industrial-ai-apiserver/myapp-doc.py new file mode 100755 index 0000000000000000000000000000000000000000..88c51afba95579067a619fda6d3e64482db63ff5 --- /dev/null +++ b/examples/industrial-ai-apiserver/myapp-doc.py @@ -0,0 +1,24 @@ +# import pprint +import json +from pecan_swagger import utils +import industrialai.api.controllers.root + +# pp = pprint.PrettyPrinter(indent=2) +# pp.pprint(utils.swagger_build('industrialai', '1.0')) +# Create JSON file +s = utils.swagger_build('industrialai', '1.0') +description = 'This is a This is a test project' +termsOfService = 'http://swagger.io/terms/' +contact = {'email': "apiteam@swagger.io"} +license = {'name': "Apache 2.0", + 'url': "http://www.apache.org/licenses/LICENSE-2.0.html"} +s = utils.add_info(s, description, termsOfService, contact, license) +externalDocs = {'description': "Find out more about Swagger", + 'url': "http://swagger.io"} +s = utils.add_external_docs(s, externalDocs) +host = "127.0.0.1" +s = utils.add_host(s, host) +basePath = "/v1" +s = utils.add_base_path(s, basePath) +output_path = "test.json" +utils.print_json(s, output_path) diff --git a/examples/industrial-ai-apiserver/pecan_swagger/__init__.py b/examples/industrial-ai-apiserver/pecan_swagger/__init__.py new file mode 100755 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/examples/industrial-ai-apiserver/pecan_swagger/decorators.py b/examples/industrial-ai-apiserver/pecan_swagger/decorators.py new file mode 100755 index 0000000000000000000000000000000000000000..08de1584942739d632cec61f5d2c636b454e92df --- /dev/null +++ b/examples/industrial-ai-apiserver/pecan_swagger/decorators.py @@ -0,0 +1,161 @@ +import pecan_swagger.g as g + +""" +decorators module + +these decorators are meant to be used by developers who wish to markup +their pecan applications to produce swagger. +""" + + +def path(endpoint, name, parent=None): + """ + path decorator + + this decorator should be used on pecan controllers to instruct how + they map to routes. + + :param endpoint: the root uri for this controller. + :param name: the name of this path. + :param parent: an optional path name to indicate a parent/child + relationship between this path and another. + """ + + def decorator(c): + if hasattr(c, '__swag'): + raise Exception('{} already has swag'.format(c.__name__)) + c.__swag = dict(endpoint=endpoint, name=name, parent=parent) + g.add_path(c) + return c + + return decorator + + +def response(method_name, res_code, des, schema=None): + """ + 获取方法的response + 生成字典{response:res_code:{description:des}} + 并加入controller.__swag[method][method_name] + """ + + def decorator(m): + + if not m.__swag['method'][method_name].get('responses'): + m.__swag['method'][method_name]['responses'] = {} + if not m.__swag['method'][method_name]['responses'].get(res_code): + m.__swag['method'][method_name]['responses'][res_code] = {} + m.__swag['method'][method_name]['responses'][res_code]['description'] = des + if schema: + m.__swag['method'][method_name]['responses'][res_code]['schema'] = schema + return m + + return decorator + + +def tags(method_name, *tags): + """ + 获取方法的tags + 生成字典{tags:tag} + 并加入controller.__swag[method][method_name] + """ + + def decorator(m): + if not m.__swag['method'][method_name].get('tags'): + m.__swag['method'][method_name]['tags'] = [] + for i in tags: + m.__swag['method'][method_name]['tags'].append(i) + return m + + return decorator + + +def summary(method_name, summary): + """ + 获取方法的summary + 生成字典{summary:summary} + 并加入controller.__swag[method][method_name] + """ + + def decorator(m): + m.__swag['method'][method_name]['summary'] = summary + return m + + return decorator + + +def description(method_name, des): + """ + 获取方法的description + 生成字典{description:des} + 并加入controller.__swag[method][method_name] + """ + + def decorator(m): + m.__swag['method'][method_name]['description'] = des + return m + + return decorator + + +def operationId(method_name, oi): + """ + 获取方法的operationId + 生成字典{operationId:oi} + 并加入controller.__swag[method][method_name] + """ + + def decorator(m): + m.__swag['method'][method_name]['operationId'] = oi + return m + + return decorator + + +def method(*args): + """ + 获取被装饰类包含的方法 + 生成method字典 + 并加入controller.__swag + """ + + def decorator(m): + d = {} + for i in args: + if hasattr(m, i): + d[i] = {} + m.__swag['method'] = d + return m + + return decorator + + +def definitions(t, *args): + """ + 添加definitions到g.py中 + """ + + def decorator(m): + g.add_definitions(t, m) + return m + + return decorator + + +def parameter(method_name, *args): + """ + 获取方法的parameter + 生成字典{parameter:[]} + 并加入controller.__swag[method][method_name] + """ + + def decorator(m): + if not m.__swag['method'][method_name].get('parameters'): + m.__swag['method'][method_name]['parameters'] = [] + for i in args: + if i.get('schema'): + i['schema'] = {'$ref': '#/definitions/' + i['schema']} + m.__swag['method'][method_name]['parameters'].append(i) + return m + + return decorator + diff --git a/pecan_swagger/g.py b/examples/industrial-ai-apiserver/pecan_swagger/g.py old mode 100644 new mode 100755 similarity index 74% rename from pecan_swagger/g.py rename to examples/industrial-ai-apiserver/pecan_swagger/g.py index 383c2c6fda52f76e98717f62d5b99155dc7fc12f..15f610405e3b349cff971e534609cfc901d180c0 --- a/pecan_swagger/g.py +++ b/examples/industrial-ai-apiserver/pecan_swagger/g.py @@ -1,487 +1,617 @@ -import copy -import inspect - -from pecan import util as p_u -import wsme.types as wtypes -import six -import weakref - - -""" -global hierarchy module - -this module is at the heart of the swagger conversion utility. it -contains a global "hierarchy" object which will provide a single point -to assemble an application's swagger output. - -there are also several helper functions to assist in building the -hierarchy of controllers, routes, and methods. -""" - -_hierarchy = {} - -_http_methods = {'get': '', 'post': '', 'put': '', - 'patch': '', 'delete': '', - 'head': '', 'trace': ''} - -_all_wsme_types = wtypes.native_types + ( - wtypes.ArrayType, wtypes.DictType, wtypes.BinaryType, - wtypes.IntegerType, wtypes.StringType, wtypes.IPv4AddressType, - wtypes.IPv6AddressType, wtypes.UuidType, wtypes.Enum, wtypes.UnsetType) - -_definitions = {} - - -def add_path(c): - """adds a named controller to the hierarchy.""" - if _hierarchy.get(c.__swag['name']): - raise Exception( - 'name {} already exists in hierarchy'.format(c.__swag['name'])) - _hierarchy[c.__swag['name']] = c - - -def build_path(swaginfo): - """return the route path for a swag metadata item.""" - if swaginfo.get('parent') is not None: - return path_join(build_path(get_swag(swaginfo.get('parent'))), - swaginfo.get('endpoint')) - return swaginfo.get('endpoint') - - -def get_controller_paths(controllers, wsme_defs): - """ - get a list of paths with methods - - returns a list of tuples (controller path, methods). - """ - def get_methods_for_generic(name): - methods = [] - generic_handlers = lc.get(name).get('generic_handlers', {}) - for method, func in generic_handlers.items(): - if method == 'DEFAULT': - methods.append('get') - else: - methods.append(method.lower()) - # TODO drill down through decorators to get true function name - truename = getrealname(func) - del lc[truename] - return methods - - def append_methods_for_specific(name, tpl): - paths.append(tpl) - del lc[name] - - lc = copy.deepcopy(controllers) - paths = [] - # TODO incorporate method decorator, removing functions marked - if lc.get('index'): - for method in get_methods_for_generic('index'): - paths.append(('', (method, {}))) - - # for REST controller - for method, path in _http_methods.items(): - if lc.get(method): - spec = wsme_defs.get(method, {}) # add wsme definitions - append_methods_for_specific(method, (path, (method, spec))) - - if lc.get('get_all'): - # add wsme definitions - spec = wsme_defs.get('get_all') # add wsme definitions - append_methods_for_specific('get_all', ('', ('get', spec))) - if lc.get('get_one'): - # add wsme definitions - spec = wsme_defs.get('get_one') # add wsme definitions - append_methods_for_specific('get_one', ('', ('get', spec))) - - if lc.get('_default'): - append_methods_for_specific('_default', ('', ('*', {}))) - if lc.get('_route'): - append_methods_for_specific('_route', ('', ('*', {}))) - if lc.get('_lookup'): - del lc['_lookup'] - - if lc.get('_custom_actions'): - for ca, ca_method in lc['_custom_actions'].items(): - spec = wsme_defs.get(ca) # add wsme definitions - for m in ca_method: - paths.append((ca, (m.lower(), spec))) - del lc['_custom_actions'] - - generic_controllers = [c for c in lc if lc[c].get('generic')] - for controller in generic_controllers: - for method in get_methods_for_generic(controller): - paths.append((controller, (method, {}))) - for controller in lc: - if controller not in [path[0] for path in paths]: - paths.append((controller, ('get', {}))) - return paths - - -def get_wsme_defs(name): - - def datatype_to_type_and_format(datatype): - tf = {} - if datatype == 'uuid' or datatype == 'ipv4address' \ - or datatype == 'ipv6address' or datatype == 'date' \ - or datatype == 'time': - tf['type'] = 'string' - tf['format'] = datatype - elif datatype == 'datetime': - tf['type'] = 'string' - tf['format'] = 'date-time' - elif datatype == 'binary' or datatype == 'bytes': - # base64 encoded characters - tf['type'] = 'string' - tf['format'] = 'byte' - elif datatype == 'array' or datatype == 'boolean' \ - or datatype == 'integer' or datatype == 'string': - # no format - tf['type'] = datatype - elif datatype == 'float': - # number - tf['type'] = 'number' - tf['format'] = datatype - elif datatype == 'unicode' or datatype == 'str' \ - or datatype == 'text': - # primitive, no format - tf['type'] = 'string' - elif datatype == 'int' or datatype == 'decimal': - # primitive, no format - tf['type'] = 'integer' - elif datatype == 'bool': - # primitive, no format - tf['type'] = 'boolean' - elif datatype == 'enum': - tf['type'] = 'enum' - elif datatype == 'unset' or datatype == 'none': - tf['type'] = None - elif datatype == 'dict': - tf['type'] = 'object' - else: - tf['type'] = 'object' - return tf - - def class_to_name_str(c): - return (('%s' % c).replace('<', '').replace('>', '') - .replace('class ', '').replace('\'', '') - .split(' ', 1)[0].rsplit('.', 1)[-1]) - - def conv_class_name_to_type_str(cn): - type_str = '' - if cn.endswith('Type'): - type_str = cn[:-4] - elif cn == 'str': - type_str = 'string' - else: - type_str = cn - type_str = type_str.lower() - return type_str - - def get_type_str(obj, src_dict=None): - type_str = '' - if hasattr(obj, '__name__'): - type_str = obj.__name__ - else: - type_str = obj.__class__.__name__ - type_str = conv_class_name_to_type_str(type_str) - - tf = datatype_to_type_and_format(type_str) - - if hasattr(obj, 'basetype') and \ - (obj.__class__ not in _all_wsme_types or type_str == 'enum'): - # UserType or Enum - tf['type'] = get_type_str(obj.basetype) - - if isinstance(src_dict, dict): - # if dict is in args, set 'type' and 'format' into the dict and - # return - src_dict.update({'type': tf['type']}) - if 'format' in tf: - src_dict.update({'format': tf['format']}) - - # get datatype options. ex.) min_length, minimum, .. - for k, v in inspect.getmembers(obj): - if ((k == 'minimum' or k == 'maxmum' - or k == 'min_length' or k == 'max_length') - and v is not None): - src_dict[to_swag_key(k)] = v - elif k == 'pattern' and v is not None: - src_dict[to_swag_key(k)] = v.pattern - # TODO(shu-mutou): this should be removed for production. - # else: - # src_dict[to_swag_key(k)] = v - - if type_str == 'enum': - # EnumType use 'set' that doesn't have sequence. - # So use 'sorted' for keeping static output. - src_dict['enum'] = sorted([v for v in obj.values]) - - # return 'type' only - return tf['type'] - - def to_swag_key(key): - keys = { - 'doc': 'description', - 'arguments': 'parameters', - 'return_type': 'schema', - 'datatype': 'type', - 'mandatory': 'required', - 'sample': 'examples', - 'readonly': 'readOnly', - 'min_length': 'minLength', - 'max_length': 'maxLength', - } - if key in keys: - return keys[key] - else: - return key - - def get_wm_item_prop(item, isparams=False): - # Add prop into 'properties' and 'required' in 'Items Object' - # 'Items Object' can be part of 'Schema Object' or 'Items Object', - # and can use recursively - prop_dict = {} - # TODO(shu-mutou): this should be removed for production. - # prop_dict['obj'] = inspect.getmembers(item) - - _item = item - if wtypes.iscomplex(item): - _item = weakref.ref(item) - - for a, i in inspect.getmembers(_item): - if a == 'datatype': - datatype = get_type_str(i, prop_dict) - if datatype == 'array': - # if array, do recursively - prop_dict['items'] = inspect_wm_schema(i.item_type) - elif datatype == 'object': - # if obuject, do recursively - prop_dict['items'] = inspect_wm_schema(i) - elif a == 'default' and i: - prop_dict[to_swag_key(a)] = i - elif a == 'name' and isparams: - prop_dict[to_swag_key(a)] = i - elif a == 'mandatory' and i: - prop_dict[to_swag_key(a)] = i - elif a == 'readonly' and i: - prop_dict[to_swag_key(a)] = i - elif a == 'doc' and i is not None: - prop_dict[to_swag_key(a)] = i - - if isparams and prop_dict['type'] in ['object', 'array']: - prop_dict['schema'] = {'items': prop_dict['items'], - 'type': prop_dict['type']} - del prop_dict['type'] - del prop_dict['items'] - return prop_dict - - def get_wsattr_and_wsproperty(obj): - ws_dict = {} - for key, value in inspect.getmembers(obj): - if (key[0] != '_' and - (value.__class__.__name__ == 'wsattr' - or value.__class__.__name__ == 'wsproperty')): - ws_dict[key] = value - return ws_dict - - def inspect_wm_schema(schema_obj, isparams=False): - schema_dict = {} - # TODO(shu-mutou): this should be removed for production. - # schema_dict['obj'] = get_wsattr_and_wsproperty(schema_obj) - ws_len = len(get_wsattr_and_wsproperty(schema_obj)) - model_name = class_to_name_str(schema_obj) - - for key, obj in inspect.getmembers(schema_obj): - if (key[0] != '_' and - (obj.__class__.__name__ == 'wsattr' - or obj.__class__.__name__ == 'wsproperty')): - # TODO(shu-mutou): this should be removed for production. - # _definitions[model_name][to_swag_key(key)] = \ - # {'obj': inspect.getmembers(obj)} - - if ws_len == 1: - # single schema - schema_dict = get_wm_item_prop(obj, isparams) - else: - # multi property schema - schema_dict.update({'type': 'object'}) - # properties - if 'items' not in schema_dict: - schema_dict['items'] = {'properties': {}} - prop = {key: get_wm_item_prop(obj, isparams)} - # required as array of string - if 'required' in prop[key] and prop[key]['required'] \ - and isinstance(prop[key]['required'], bool): - if 'required' not in schema_dict: - schema_dict['required'] = [] - schema_dict['required'].append(key) - del prop[key]['required'] - schema_dict['items']['properties'].update(prop) - - if schema_obj not in _all_wsme_types: - if model_name not in _definitions: - _definitions[model_name] = schema_dict - schema_dict = {'$ref': "#/definitions/%s" % model_name} - - return schema_dict - - def get_wm_def(o): - wsme = {'description': ''} - for w, d in inspect.getmembers(o): - if w == 'arguments': - wsme[to_swag_key(w)] = [] - for arg in d: - # set each 'Parameter Object', it can include - # 'Items Object' recursively - item_dict = get_wm_item_prop(arg, True) - # TODO: MUST be set one of - # 'body|query|path|header|formData' - item_dict['in'] = 'query' - wsme[to_swag_key(w)].append(item_dict) - # 'body' should be set due to existing 'body' option - # in WSME expose - if o.body_type is not None: - # if body is set, last arg is it. - wsme[to_swag_key(w)][-1]['in'] = 'body' - elif w == 'doc' and d: - wsme[to_swag_key(w)] = d - elif w == 'return_type': - wsme['responses'] = {'status': {'description': ''}} - if d: - if d in _all_wsme_types: - # d is set single WSME type class or implicit type - wsme['responses']['status'][to_swag_key(w)] = ( - datatype_to_type_and_format( - conv_class_name_to_type_str( - class_to_name_str(d)))) - else: - # d is model class - wsme['responses']['status'][to_swag_key(w)] = ( - inspect_wm_schema(d, False)) - doc = inspect.getdoc(d) - if doc is not None: - wsme['responses']['status']['description'] = doc - elif w == 'status_code': - wsme['responses'][d] = wsme['responses']['status'] - del wsme['responses']['status'] - # TODO(shu-mutou): this should be removed for production. - # elif w == 'body_type': - # wsme[to_swag_key(w)] = get_type_str(d) - # elif w == 'extra_options' or w == 'ignore_extra_args' \ - # or w == 'name' or w == 'pass_request': - # wsme[to_swag_key(w)] = d - # else: - # wsme[to_swag_key(w)] = d - return wsme - - c = _hierarchy[name] - wsme_defs = {} - for k, v in inspect.getmembers(c): - if p_u.iscontroller(v): - for m, o in inspect.getmembers(v): - if m == "_wsme_definition": - wsme_defs[k] = get_wm_def(o) - - # TODO(shu-mutou): this should be removed for production. - # output wsme info into files by each controller for dev - # import pprint - # with open(name + '.txt', 'w') as fout: - # pprint.pprint(wsme_defs, stream=fout, indent=2) - - return wsme_defs - - -def get_controllers(name): - """ - get all the controllers associated with a path - - returns a dictionary of controllers indexed by their names. - """ - c = _hierarchy[name] - controllers = {k: p_u._cfg(v) - for k, v in c.__dict__.items() if p_u.iscontroller(v)} - cacts = {k: v for k, v in c.__dict__.items() if k == '_custom_actions'} - if len(cacts): - controllers.update(cacts) - return controllers - - -def get_paths(): - """ - return all the registered paths - - loops through the hierarchy and retuns a list of tuples containing the - paths and their methods. - - :returns: [(path, methods), ...] - """ - pathlist = [] - for name in _hierarchy: - fullpath = build_path(get_swag(name)) - controllers = get_controllers(name) - wsme_defs = get_wsme_defs(name) - paths = get_controller_paths(controllers, wsme_defs) - for path in paths: - ptuple = (path_join(fullpath, path[0]), path[1]) - pathlist.append(ptuple) - return pathlist - - -def get_swag(name): - """return the swag metadata from an named controller.""" - return _hierarchy.get(name).__swag - - -def getrealname(method): - """attempt to get a method's real name.""" - argspec = inspect.getargspec(method) - args = argspec[0] - if args and args[0] == 'self': - return method.__name__ - if hasattr(method, '__func__'): - method = method.__func__ - - func_closure = six.get_function_closure(method) - - # NOTE(sileht): if the closure is None we cannot look deeper, - # so return actual argspec, this occurs when the method - # is static for example. - if func_closure is None: - return method.__name__ - - closure = next( - ( - c for c in func_closure if six.callable(c.cell_contents) - ), - None - ) - method = closure.cell_contents - return getrealname(method) - - -def methods_get(name): - """get all the methods for a named controller.""" - c = _hierarchy[name] - mlist = [] - if hasattr(c, 'index') and p_u.iscontroller(c.index): - cfg = p_u._cfg(c.index) - if cfg.get('generic_handlers').get('DEFAULT'): - mlist.append('get') - mlist += cfg.get('allowed_methods') - for i in c.__dict__: - ii = getattr(c, i) - if hasattr(ii, '__swag'): - m = ii.__swag.get('method') - if m is not None: - mlist.append(m) - return mlist - - -def path_join(part1, part2): - """join two url paths.""" - if len(part2) == 0: - return part1 - sep = '/' - if part1[-1] == sep: - sep = '' - return part1 + sep + part2 +import copy +import inspect + +from pecan import util as p_u +import wsme.types as wtypes +import six +import weakref + +""" +global hierarchy module + +this module is at the heart of the swagger conversion utility. it +contains a global "hierarchy" object which will provide a single point +to assemble an application's swagger output. + +there are also several helper functions to assist in building the +hierarchy of controllers, routes, and methods. +""" + +_hierarchy = {} + +_http_methods = {'get': '', 'post': '', 'put': '', + 'patch': '', 'delete': '', + 'head': '', 'trace': ''} + +_all_wsme_types = wtypes.native_types + ( + wtypes.ArrayType, wtypes.DictType, wtypes.BinaryType, + wtypes.IntegerType, wtypes.StringType, wtypes.IPv4AddressType, + wtypes.IPv6AddressType, wtypes.UuidType, wtypes.Enum, wtypes.UnsetType) + +_definitions = {} + +all_definitions = {} + + +def add_path(c): + """adds a named controller to the hierarchy.""" + if _hierarchy.get(c.__swag['name']): + raise Exception( + 'name {} already exists in hierarchy'.format(c.__swag['name'])) + _hierarchy[c.__swag['name']] = c + + +def build_path(swaginfo): + """return the route path for a swag metadata item.""" + if swaginfo.get('parent') is not None: + return path_join(build_path(get_swag(swaginfo.get('parent'))), + swaginfo.get('endpoint')) + return swaginfo.get('endpoint') + + +def get_controller_paths(controllers, wsme_defs): + """ + get a list of paths with methods + + returns a list of tuples (controller path, methods). + """ + + def get_methods_for_generic(name): + methods = [] + generic_handlers = lc.get(name).get('generic_handlers', {}) + for method, func in generic_handlers.items(): + if method == 'DEFAULT': + methods.append('get') + else: + methods.append(method.lower()) + # TODO drill down through decorators to get true function name + truename = getrealname(func) + del lc[truename] + return methods + + def append_methods_for_specific(name, tpl): + paths.append(tpl) + del lc[name] + + lc = copy.deepcopy(controllers) + paths = [] + # TODO incorporate method decorator, removing functions marked + if lc.get('index'): + for method in get_methods_for_generic('index'): + paths.append(('', (method, {}))) + spec = wsme_defs.get('index') # add wsme definitions + append_methods_for_specific('index', ('', ('get', spec))) + + # for REST controller + for method, path in _http_methods.items(): + if lc.get(method): + spec = wsme_defs.get(method, {}) # add wsme definitions + append_methods_for_specific(method, (path, (method, spec))) + + if lc.get('get_all'): + # add wsme definitions + spec = wsme_defs.get('get_all') # add wsme definitions + append_methods_for_specific('get_all', ('get_all', ('get', spec))) + if lc.get('get_one'): + # add wsme definitions + spec = wsme_defs.get('get_one') # add wsme definitions + append_methods_for_specific('get_one', ('get_one', ('get', spec))) + + if lc.get('_default'): + append_methods_for_specific('_default', ('', ('*', {}))) + if lc.get('_route'): + append_methods_for_specific('_route', ('', ('*', {}))) + if lc.get('_lookup'): + del lc['_lookup'] + + if lc.get('_custom_actions'): + for ca, ca_method in lc['_custom_actions'].items(): + spec = wsme_defs.get(ca) # add wsme definitions + for m in ca_method: + paths.append((ca, (m.lower(), spec))) + del lc['_custom_actions'] + + generic_controllers = [c for c in lc if lc[c].get('generic')] + for controller in generic_controllers: + for method in get_methods_for_generic(controller): + paths.append((controller, (method, {}))) + for controller in lc: + if controller not in [path[0] for path in paths]: + paths.append((controller, ('get', {}))) + return paths + + +def get_wsme_defs(name): + """获得wsexpose装饰方法的definitions.""" + + def datatype_to_type_and_format(datatype): + tf = {} + if datatype == 'uuid' or datatype == 'ipv4address' \ + or datatype == 'ipv6address' or datatype == 'date' \ + or datatype == 'time': + tf['type'] = 'string' + tf['format'] = datatype + elif datatype == 'datetime': + tf['type'] = 'string' + tf['format'] = 'date-time' + elif datatype == 'binary' or datatype == 'bytes': + # base64 encoded characters + tf['type'] = 'string' + tf['format'] = 'byte' + elif datatype == 'array' or datatype == 'boolean' \ + or datatype == 'integer' or datatype == 'string': + # no format + tf['type'] = datatype + elif datatype == 'float': + # number + tf['type'] = 'number' + tf['format'] = datatype + elif datatype == 'unicode' or datatype == 'str' \ + or datatype == 'text': + # primitive, no format + tf['type'] = 'string' + elif datatype == 'int' or datatype == 'decimal': + # primitive, no format + tf['type'] = 'integer' + elif datatype == 'bool': + # primitive, no format + tf['type'] = 'boolean' + elif datatype == 'enum': + tf['type'] = 'enum' + elif datatype == 'unset' or datatype == 'none': + tf['type'] = None + elif datatype == 'dict': + tf['type'] = 'object' + else: + tf['type'] = 'object' + return tf + + def class_to_name_str(c): + return (('%s' % c).replace('<', '').replace('>', '') + .replace('class ', '').replace('\'', '') + .split(' ', 1)[0].rsplit('.', 1)[-1]) + + def conv_class_name_to_type_str(cn): + type_str = '' + if cn.endswith('Type'): + type_str = cn[:-4] + elif cn == 'str': + type_str = 'string' + else: + type_str = cn + type_str = type_str.lower() + return type_str + + def get_type_str(obj, src_dict=None): + type_str = '' + if hasattr(obj, '__name__'): + type_str = obj.__name__ + else: + type_str = obj.__class__.__name__ + type_str = conv_class_name_to_type_str(type_str) + + tf = datatype_to_type_and_format(type_str) + + if hasattr(obj, 'basetype') and \ + (obj.__class__ not in _all_wsme_types or type_str == 'enum'): + # UserType or Enum + tf['type'] = get_type_str(obj.basetype) + + if isinstance(src_dict, dict): + # if dict is in args, set 'type' and 'format' into the dict and + # return + src_dict.update({'type': tf['type']}) + if 'format' in tf: + src_dict.update({'format': tf['format']}) + + # get datatype options. ex.) min_length, minimum, .. + for k, v in inspect.getmembers(obj): + if ((k == 'minimum' or k == 'maxmum' + or k == 'min_length' or k == 'max_length') + and v is not None): + src_dict[to_swag_key(k)] = v + elif k == 'pattern' and v is not None: + src_dict[to_swag_key(k)] = v.pattern + # TODO(shu-mutou): this should be removed for production. + # else: + # src_dict[to_swag_key(k)] = v + + if type_str == 'enum': + # EnumType use 'set' that doesn't have sequence. + # So use 'sorted' for keeping static output. + src_dict['enum'] = sorted([v for v in obj.values]) + + # return 'type' only + return tf['type'] + + def to_swag_key(key): + keys = { + 'doc': 'description', + 'arguments': 'parameters', + 'return_type': 'schema', + 'datatype': 'type', + 'mandatory': 'required', + 'sample': 'examples', + 'readonly': 'readOnly', + 'min_length': 'minLength', + 'max_length': 'maxLength', + } + if key in keys: + return keys[key] + else: + return key + + def get_wm_item_prop(item, isparams=False): + # Add prop into 'properties' and 'required' in 'Items Object' + # 'Items Object' can be part of 'Schema Object' or 'Items Object', + # and can use recursively + prop_dict = {} + # TODO(shu-mutou): this should be removed for production. + # prop_dict['obj'] = inspect.getmembers(item) + + _item = item + if wtypes.iscomplex(item): + _item = weakref.ref(item) + + for a, i in inspect.getmembers(_item): + if a == 'datatype': + datatype = get_type_str(i, prop_dict) + if datatype == 'array': + # if array, do recursively + prop_dict['items'] = inspect_wm_schema(i.item_type) + elif datatype == 'object': + # if obuject, do recursively + prop_dict['items'] = inspect_wm_schema(i) + elif a == 'default' and i: + prop_dict[to_swag_key(a)] = i + elif a == 'name' and isparams: + prop_dict[to_swag_key(a)] = i + elif a == 'mandatory' and i: + prop_dict[to_swag_key(a)] = i + elif a == 'readonly' and i: + prop_dict[to_swag_key(a)] = i + elif a == 'doc' and i is not None: + prop_dict[to_swag_key(a)] = i + + if isparams and prop_dict['type'] in ['object', 'array']: + prop_dict['schema'] = {'items': prop_dict['items'], + 'type': prop_dict['type']} + if prop_dict['type'] == 'object': + prop_dict['schema'] = prop_dict['items'] + del prop_dict['type'] + del prop_dict['items'] + return prop_dict + + def get_wsattr_and_wsproperty(obj): + ws_dict = {} + for key, value in inspect.getmembers(obj): + if (key[0] != '_' and + (value.__class__.__name__ == 'wsattr' + or value.__class__.__name__ == 'wsproperty')): + ws_dict[key] = value + return ws_dict + + def inspect_wm_schema(schema_obj, isparams=False): + schema_dict = {} + # TODO(shu-mutou): this should be removed for production. + # schema_dict['obj'] = get_wsattr_and_wsproperty(schema_obj) + ws_len = len(get_wsattr_and_wsproperty(schema_obj)) + model_name = class_to_name_str(schema_obj) + + for key, obj in inspect.getmembers(schema_obj): + if (key[0] != '_' and + (obj.__class__.__name__ == 'wsattr' + or obj.__class__.__name__ == 'wsproperty')): + # TODO(shu-mutou): this should be removed for production. + # _definitions[model_name][to_swag_key(key)] = \ + # {'obj': inspect.getmembers(obj)} + + if ws_len == 1: + # single schema + schema_dict = get_wm_item_prop(obj, isparams) + else: + # multi property schema + schema_dict.update({'type': 'object'}) + # properties + if 'properties' not in schema_dict: + schema_dict['properties'] = {} + prop = {key: get_wm_item_prop(obj, isparams)} + # required as array of string + if 'required' in prop[key] and prop[key]['required'] \ + and isinstance(prop[key]['required'], bool): + if 'required' not in schema_dict: + schema_dict['required'] = [] + schema_dict['required'].append(key) + del prop[key]['required'] + schema_dict['properties'].update(prop) + + if schema_obj not in _all_wsme_types: + if model_name not in _definitions: + _definitions[model_name] = schema_dict + schema_dict = {'$ref': "#/definitions/%s" % model_name} + + return schema_dict + + def get_wm_def(o): + wsme = {'description': ''} + for w, d in inspect.getmembers(o): + if w == 'arguments': + wsme[to_swag_key(w)] = [] + for arg in d: + # set each 'Parameter Object', it can include + # 'Items Object' recursively + item_dict = get_wm_item_prop(arg, True) + # TODO: MUST be set one of + # 'body|query|path|header|formData' + item_dict['in'] = 'query' + wsme[to_swag_key(w)].append(item_dict) + # 'body' should be set due to existing 'body' option + # in WSME expose + if o.body_type is not None: + # if body is set, last arg is it. + wsme[to_swag_key(w)][-1]['in'] = 'body' + elif w == 'doc' and d: + wsme[to_swag_key(w)] = d + elif w == 'return_type': + wsme['responses'] = {'status': {'description': ''}} + if d: + if d in _all_wsme_types: + # d is set single WSME type class or implicit type + wsme['responses']['status'][to_swag_key(w)] = ( + datatype_to_type_and_format( + conv_class_name_to_type_str( + class_to_name_str(d)))) + else: + # d is model class + wsme['responses']['status'][to_swag_key(w)] = ( + inspect_wm_schema(d, False)) + doc = inspect.getdoc(d) + if doc is not None: + wsme['responses']['status']['description'] = doc + elif w == 'status_code': + wsme['responses'][d] = wsme['responses']['status'] + del wsme['responses']['status'] + # TODO(shu-mutou): this should be removed for production. + # elif w == 'body_type': + # wsme[to_swag_key(w)] = get_type_str(d) + # elif w == 'extra_options' or w == 'ignore_extra_args' \ + # or w == 'name' or w == 'pass_request': + # wsme[to_swag_key(w)] = d + # else: + # wsme[to_swag_key(w)] = d + return wsme + + c = _hierarchy[name] + wsme_defs = {} + for k, v in inspect.getmembers(c): + if p_u.iscontroller(v): + for m, o in inspect.getmembers(v): + if m == "_wsme_definition": + wsme_defs[k] = get_wm_def(o) + + # TODO(shu-mutou): this should be removed for production. + # output wsme info into files by each controller for dev + # import pprint + # with open(name + '.txt', 'w') as fout: + # pprint.pprint(wsme_defs, stream=fout, indent=2) + + return wsme_defs + + +def get_controllers(name): + """ + get all the controllers associated with a path + + returns a dictionary of controllers indexed by their names. + """ + c = _hierarchy[name] + controllers = {k: p_u._cfg(v) + for k, v in c.__dict__.items() if p_u.iscontroller(v)} + cacts = {k: v for k, v in c.__dict__.items() if k == '_custom_actions'} + if len(cacts): + controllers.update(cacts) + return controllers + + +def get_paths(): + """ + return all the registered paths + + loops through the hierarchy and retuns a list of tuples containing the + paths and their methods. + + :returns: [(path, methods), ...] + """ + pathlist = [] + for name in _hierarchy: + fullpath = build_path(get_swag(name)) + controllers = get_controllers(name) + paths = get_controller_paths(controllers, combine_method_defs(name)) + for path in paths: + ptuple = (path_join(fullpath, path[0]), path[1]) + pathlist.append(ptuple) + combine_all_definitions() + return pathlist + + +def get_swag(name): + """return the swag metadata from an named controller.""" + return _hierarchy.get(name).__swag + + +def getrealname(method): + """attempt to get a method's real name.""" + argspec = inspect.getargspec(method) + args = argspec[0] + if args and args[0] == 'self': + return method.__name__ + if hasattr(method, '__func__'): + method = method.__func__ + + func_closure = six.get_function_closure(method) + + # NOTE(sileht): if the closure is None we cannot look deeper, + # so return actual argspec, this occurs when the method + # is static for example. + if func_closure is None: + return method.__name__ + + closure = next( + ( + c for c in func_closure if six.callable(c.cell_contents) + ), + None + ) + method = closure.cell_contents + return getrealname(method) + + +def methods_get(name): + """get all the methods for a named controller.""" + c = _hierarchy[name] + mlist = [] + if hasattr(c, 'index') and p_u.iscontroller(c.index): + cfg = p_u._cfg(c.index) + if cfg.get('generic_handlers') and cfg.get('generic_handlers').get('DEFAULT'): + mlist.append('get') + if cfg.get('allowed_methods'): + mlist += cfg.get('allowed_methods') + if hasattr(c, '__swag') and c.__swag.get('method'): + for i in c.__swag['method']: + mlist.append(i) + # for i in c.__dict__: + # ii = getattr(c, i) + # if hasattr(ii, '__swag'): + # m = ii.__swag.get('method') + # if m is not None: + # mlist.append(m) + return mlist + + +def path_join(part1, part2): + """join two url paths.""" + if len(part2) == 0: + return part1 + sep = '/' + if part1[-1] == sep: + sep = '' + return part1 + sep + part2 + + +def get_def(name): + """获取所有expose装饰方法的definitions.""" + c = _hierarchy[name] + if hasattr(c, '__swag') and c.__swag.get('method'): + return c.__swag['method'] + return {} + + +def combine_method_defs(name): + """合并expose和wsexpose装饰方法的definitions.""" + + c = _hierarchy[name] + lc = copy.deepcopy(c) + w_d = get_wsme_defs(name) + e_d = get_def(name) + if w_d and e_d: + for i in w_d: + if e_d.get(i): + for j in w_d[i]: + e_d[i][j] = w_d[i][j] + else: + e_d[i] = w_d[i] + elif w_d: + return w_d + else: + return e_d + return e_d + + +def format_type(t): + """ + 判断数据类型是否为自定义的数据类型 + """ + + def datatype_to_type_and_format(datatype): + + tf = {} + if datatype == 'uuid' or datatype == 'ipv4address' \ + or datatype == 'ipv6address' or datatype == 'date' \ + or datatype == 'time': + tf['type'] = 'string' + tf['format'] = datatype + elif datatype == 'datetime': + tf['type'] = 'string' + tf['format'] = 'date-time' + elif datatype == 'binary' or datatype == 'bytes': + # base64 encoded characters + tf['type'] = 'string' + tf['format'] = 'byte' + elif datatype == 'array' or datatype == 'boolean' \ + or datatype == 'integer' or datatype == 'string': + # no format + tf['type'] = datatype + elif datatype == 'float': + # number + tf['type'] = 'number' + tf['format'] = datatype + elif datatype == 'unicode' or datatype == 'str' \ + or datatype == 'text': + # primitive, no format + tf['type'] = 'string' + elif datatype == 'int' or datatype == 'decimal': + # primitive, no format + tf['type'] = 'integer' + elif datatype == 'bool': + # primitive, no format + tf['type'] = 'boolean' + elif datatype == 'enum': + tf['type'] = 'enum' + elif datatype == 'unset' or datatype == 'none': + tf['type'] = None + elif datatype == 'dict': + tf['type'] = 'object' + else: + tf['$ref'] = '#/definitions/' + datatype + return tf + + def conv_class_name_to_type_str(cn): + type_str = '' + if cn.endswith('Type'): + type_str = cn[:-4] + elif cn == 'str': + type_str = 'string' + else: + type_str = cn + type_str = type_str + return type_str + + def class_to_name_str(c): + return (('%s' % c).replace('<', '').replace('>', '') + .replace('class ', '').replace('\'', '') + .split(' ', 1)[0].rsplit('.', 1)[-1]) + + return datatype_to_type_and_format(conv_class_name_to_type_str(class_to_name_str(t))) + + +def add_definitions(t, m): + """将定义的类添加到all_definitions.""" + + if not all_definitions.get(m.__name__): + d = {} + d['type'] = t + d['properties'] = {} + for i, j in inspect.getmembers(m): + if not i.endswith('__'): + d['properties'][i] = format_type(j) + all_definitions[m.__name__] = d + + +def combine_all_definitions(): + """合并自定义和wsexpose解析出的definitions.""" + + for i in all_definitions: + if not _definitions.get(i): + _definitions[i] = all_definitions[i] + + return _definitions diff --git a/pecan_swagger/utils.py b/examples/industrial-ai-apiserver/pecan_swagger/utils.py old mode 100644 new mode 100755 similarity index 62% rename from pecan_swagger/utils.py rename to examples/industrial-ai-apiserver/pecan_swagger/utils.py index aa3d3b6155a6322005791e833abb278cd6ed2a01..889fbf4d19bf24a2948c156be442f15db5ec7ccd --- a/pecan_swagger/utils.py +++ b/examples/industrial-ai-apiserver/pecan_swagger/utils.py @@ -1,64 +1,108 @@ -from pecan_swagger import g as g - - -""" -utility function module - -this module contains the utility functions to assemble the swagger -dictionary object. they can be consumed by end-user applications to -build up swagger objects for applications. - -functions: -swagger_build -- build a full swagger dictionary -""" - - -def swagger_build(title, version): - swag = dict() - swag['swagger'] = '2.0' - swag['info'] = dict(title=title, version=version) - swag['consumes'] = [] - swag['produces'] = [] - swag['paths'] = {} - for p in g.get_paths(): - if p[0] not in swag['paths']: - swag['paths'][p[0]] = _tuple_to_dict(p[1]) - elif len(p[1]) > 0: - swag['paths'][p[0]].update(_tuple_to_dict(p[1])) - swag['definitions'] = g._definitions - return swag - - -def _tuple_to_dict(tpl): - """Convert tuple to dictionary - - each tuple must have key and value. - This function arrows taple including lists. - - ex.) acceptable taple - OK: ('/', ('get',(('desc',{}),('res',{}),('params',['id','name'])))) - NG: ('/', ('get',('desc','res',('params',['id','name'])))) - """ - if isinstance(tpl, list): - d = [] - for e in tpl: - d.append(_tuple_to_dict(e)) - elif isinstance(tpl, tuple): - d = {} - if isinstance(tpl[0], tuple): - # tuple has some child tuple - for e in tpl: - d[e[0]] = _tuple_to_dict(e[1]) - elif isinstance(tpl[0], list): - # list member should be processed recursively - d = _tuple_to_dict(tpl[0]) - else: - if len(tpl) == 2: - # single tuple node - d[tpl[0]] = _tuple_to_dict(tpl[1]) - else: - raise Exception(tpl) - else: - # value or dict - d = tpl - return d +from pecan_swagger import g as g +import json +import yaml + +""" +utility function module + +this module contains the utility functions to assemble the swagger +dictionary object. they can be consumed by end-user applications to +build up swagger objects for applications. + +functions: +swagger_build -- build a full swagger dictionary +""" + + +def swagger_build(title, version): + swag = dict() + swag['swagger'] = '2.0' + swag['info'] = dict(title=title, version=version) + swag['consumes'] = [] + swag['produces'] = [] + swag['paths'] = {} + swag['tags'] = [] + swag['schemes'] = [] + for p in g.get_paths(): + if p[0] not in swag['paths']: + swag['paths'][p[0]] = _tuple_to_dict(p[1]) + elif len(p[1]) > 0: + swag['paths'][p[0]].update(_tuple_to_dict(p[1])) + swag['definitions'] = g._definitions + return swag + + +def print_json(s, output_path): + json_str = json.dumps(s, indent=4) + f = open(output_path, "w") + f.write(json_str) + f.close() + + +def print_yaml(s, output_path): + yaml_str = yaml.dump(s, indent=4) + f = open(output_path, "w") + f.write(yaml_str) + f.close() + + +def add_info(s, description=None, terms_of_service=None, contact=None, license=None): + if description is not None: + s['info']['description'] = description + if terms_of_service is not None: + s['info']['termsOfService'] = terms_of_service + if contact is not None: + s['info']['contact'] = contact + if license is not None: + s['info']['license'] = license + return s + + +def add_external_docs(s, external_docs): + s["externalDocs"] = external_docs + return s + + +def add_host(s, host): + s["host"] = host + return s + + +def add_base_path(s, basepath): + s["basePath"] = basepath + return s + + +def _tuple_to_dict(tpl): + """Convert tuple to dictionary + + each tuple must have key and value. + This function arrows taple including lists. + + ex.) acceptable taple + OK: ('/', ('get',(('desc',{}),('res',{}),('params',['id','name'])))) + NG: ('/', ('get',('desc','res',('params',['id','name'])))) + """ + if isinstance(tpl, list): + d = [] + for e in tpl: + d.append(_tuple_to_dict(e)) + elif isinstance(tpl, tuple): + d = {} + if isinstance(tpl[0], tuple): + # tuple has some child tuple + for e in tpl: + d[e[0]] = _tuple_to_dict(e[1]) + elif isinstance(tpl[0], list): + # list member should be processed recursively + d = _tuple_to_dict(tpl[0]) + else: + if len(tpl) == 2: + # single tuple node + d[tpl[0]] = _tuple_to_dict(tpl[1]) + else: + raise Exception(tpl) + else: + # value or dict + d = tpl + return d diff --git a/pecan_swagger/.DS_Store b/pecan_swagger/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 Binary files /dev/null and b/pecan_swagger/.DS_Store differ diff --git a/pecan_swagger/decorators.py b/pecan_swagger/decorators.py deleted file mode 100644 index 5b8ef4aefaa1919dbeca2d4e6f9715f41b14c6ed..0000000000000000000000000000000000000000 --- a/pecan_swagger/decorators.py +++ /dev/null @@ -1,38 +0,0 @@ -import pecan_swagger.g as g - -""" -decorators module - -these decorators are meant to be used by developers who wish to markup -their pecan applications to produce swagger. -""" - - -def path(endpoint, name, parent=None): - """ - path decorator - - this decorator should be used on pecan controllers to instruct how - they map to routes. - - :param endpoint: the root uri for this controller. - :param name: the name of this path. - :param parent: an optional path name to indicate a parent/child - relationship between this path and another. - """ - def decorator(c): - if hasattr(c, '__swag'): - raise Exception('{} already has swag'.format(c.__name__)) - c.__swag = dict(endpoint=endpoint, name=name, parent=parent) - g.add_path(c) - return c - return decorator - - -def method(method): - def decorator(m): - if hasattr(m, '__swag'): - raise Exception('{} already has swag'.format(m.__name__)) - m.__swag = dict(method=method) - return m - return decorator