aboutsummaryrefslogtreecommitdiffstats
path: root/opnfv_testapi/tornado_swagger
diff options
context:
space:
mode:
authorxudan <xudan16@huawei.com>2018-07-06 05:16:40 -0400
committerxudan <xudan16@huawei.com>2018-07-06 05:21:42 -0400
commitb3e40f026d655501bfa581452c447784604ecb05 (patch)
tree406f8bfc1abc1b33f98153d03abd34ef7b0e2fe9 /opnfv_testapi/tornado_swagger
parentb1b0ea32d1a296c7d055c5391261dcad6be48c63 (diff)
Move all web portal code to the new repo dovetail-webportal
This is only the first step to simply copy the file here. There still need some more work to make sure all work well. All the changes will be submitted with other patches to make it easily to review. JIRA: DOVETAIL-671 Change-Id: I64d32a9df562184166b6199e2719f298687d1a0a Signed-off-by: xudan <xudan16@huawei.com>
Diffstat (limited to 'opnfv_testapi/tornado_swagger')
-rw-r--r--opnfv_testapi/tornado_swagger/README.md273
-rw-r--r--opnfv_testapi/tornado_swagger/__init__.py8
-rw-r--r--opnfv_testapi/tornado_swagger/handlers.py38
-rw-r--r--opnfv_testapi/tornado_swagger/settings.py25
-rw-r--r--opnfv_testapi/tornado_swagger/swagger.py291
-rw-r--r--opnfv_testapi/tornado_swagger/views.py134
6 files changed, 769 insertions, 0 deletions
diff --git a/opnfv_testapi/tornado_swagger/README.md b/opnfv_testapi/tornado_swagger/README.md
new file mode 100644
index 0000000..d815f21
--- /dev/null
+++ b/opnfv_testapi/tornado_swagger/README.md
@@ -0,0 +1,273 @@
+# tornado-swagger
+
+## What is tornado-swagger?
+tornado is a wrapper for tornado which enables swagger-ui support.
+
+In essense, you just need to wrap the Api instance and add a few python decorators to
+get full swagger support.http://swagger.io/
+
+
+## How to use:
+
+
+```python
+from tornado.web import RequestHandler, HTTPError
+from tornado_swagger import swagger
+
+swagger.docs()
+
+# You may decorate your operation with @swagger.operation and use docs to inform information
+class ItemNoParamHandler(GenericApiHandler):
+ @swagger.operation(nickname='create')
+ def post(self):
+ """
+ @param body: create test results for a item.
+ @type body: L{Item}
+ @return 200: item is created.
+ @raise 400: invalid input
+ """
+
+# Operations not decorated with @swagger.operation do not get added to the swagger docs
+
+class ItemNoParamHandler(GenericApiHandler):
+ def options(self):
+ """
+ I'm not visible in the swagger docs
+ """
+ pass
+
+
+# Then you use swagger.Application instead of tornado.web.Application
+# and do other operations as usual
+
+def make_app():
+ return swagger.Application([
+ (r"/items", ItemNoParamHandler),
+ (r"/items/([^/]+)", ItemHandler),
+ (r"/items/([^/]+)/cases/([^/]+)", ItemOptionParamHandler),
+ ])
+
+# You define models like this:
+@swagger.model
+class Item:
+ """
+ @descriptin:
+ This is an example of a model class that has parameters in its constructor
+ and the fields in the swagger spec are derived from the parameters to __init__.
+ @notes:
+ In this case we would have property1, property2 as required parameters
+ and property3 as optional parameter.
+ @property property3: Item decription
+ @ptype property3: L{PropertySubclass}
+ """
+ def __init__(self, property1, property2=None):
+ self.property1 = property1
+ self.property2 = property2
+
+# Swagger json:
+ "models": {
+ "Item": {
+ "description": "A description...",
+ "id": "Item",
+ "required": [
+ "property1",
+ ],
+ "properties": [
+ "property1": {
+ "type": "string"
+ },
+ "property2": {
+ "type": "string"
+ "default": null
+ }
+ ]
+ }
+ }
+
+# If you declare an __init__ method with meaningful arguments
+# then those args could be used to deduce the swagger model fields.
+# just as shown above
+
+# if you declare an @property in docs, this property property2 will also be used
+# to deduce the swagger model fields
+class Item:
+ """
+ @property property3: Item description
+ """
+ def __init__(self, property1, property2):
+ self.property1 = property1
+ self.property2 = property2
+
+# Swagger json:
+ "models": {
+ "Item": {
+ "description": "A description...",
+ "id": "Item",
+ "required": [
+ "property1",
+ ],
+ "properties": [
+ "property1": {
+ "type": "string"
+ },
+ "property2": {
+ "type": "string"
+ }
+ "property3": {
+ "type": "string"
+ }
+ ]
+ }
+ }
+
+# if you declare an argument with @ptype, the type of this argument will be specified
+# rather than the default 'string'
+class Item:
+ """
+ @ptype property3: L{PropertySubclass}
+ """
+ def __init__(self, property1, property2, property3=None):
+ self.property1 = property1
+ self.property2 = property2
+ self.property3 = property3
+
+# Swagger json:
+ "models": {
+ "Item": {
+ "description": "A description...",
+ "id": "Item",
+ "required": [
+ "property1",
+ ],
+ "properties": [
+ "property1": {
+ "type": "string"
+ },
+ "property2": {
+ "type": "string"
+ },
+ "property3": {
+ "type": "PropertySubclass"
+ "default": null
+ }
+ ]
+ }
+ }
+
+# if you want to declare an list property, you can do it like this:
+class Item:
+ """
+ @ptype property3: L{PropertySubclass}
+ @ptype property4: C{list} of L{PropertySubclass}
+ """
+ def __init__(self, property1, property2, property3, property4=None):
+ self.property1 = property1
+ self.property2 = property2
+ self.property3 = property3
+ self.property4 = property4
+
+# Swagger json:
+ "models": {
+ "Item": {
+ "description": "A description...",
+ "id": "Item",
+ "required": [
+ "property1",
+ ],
+ "properties": [
+ "property1": {
+ "type": "string"
+ },
+ "property2": {
+ "type": "string"
+ },
+ "property3": {
+ "type": "PropertySubclass"
+ "default": null
+ },
+ "property4": {
+ "default": null,
+ "items": {
+ "type": "PropertySubclass"},
+ "type": "array"
+ }
+ }
+ ]
+ }
+ }
+
+# if it is a query:
+class ItemQueryHandler(GenericApiHandler):
+ @swagger.operation(nickname='query')
+ def get(self):
+ """
+ @param property1:
+ @type property1: L{string}
+ @in property1: query
+ @required property1: False
+
+ @param property2:
+ @type property2: L{string}
+ @in property2: query
+ @required property2: True
+ @rtype: L{Item}
+
+ @notes: GET /item?property1=1&property2=1
+ """
+
+# Swagger json:
+ "apis": [
+ {
+ "operations": [
+ {
+ "parameters": [
+ {
+ "name": "property1",
+ "dataType": "string",
+ "paramType": "query",
+ "description": ""
+ },
+ {
+ "name": "property2",
+ "dataType": "string",
+ "paramType": "query",
+ "required": true,
+ "description": ""
+ }
+ ],
+ "responseClass": "Item",
+ "notes": null,
+ "responseMessages": [],
+ "summary": null,
+ "httpMethod": "GET",
+ "nickname": "query"
+ }
+ ],
+ "path": "/item",
+ "description": null
+ },
+ ....
+ ]
+```
+
+# Running and testing
+
+Now run your tornado app
+
+```
+python main.py
+```
+
+And visit:
+
+```
+curl http://ip:port/swagger/spec
+```
+
+access to web
+```
+http://ip:port/swagger/spec.html
+```
+
+# Passing more metadata to swagger
+customized arguments used in creating the 'swagger.docs' object will be supported later
diff --git a/opnfv_testapi/tornado_swagger/__init__.py b/opnfv_testapi/tornado_swagger/__init__.py
new file mode 100644
index 0000000..363bc38
--- /dev/null
+++ b/opnfv_testapi/tornado_swagger/__init__.py
@@ -0,0 +1,8 @@
+##############################################################################
+# Copyright (c) 2016 ZTE Corporation
+# feng.xiaowei@zte.com.cn
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
diff --git a/opnfv_testapi/tornado_swagger/handlers.py b/opnfv_testapi/tornado_swagger/handlers.py
new file mode 100644
index 0000000..e39a9f6
--- /dev/null
+++ b/opnfv_testapi/tornado_swagger/handlers.py
@@ -0,0 +1,38 @@
+##############################################################################
+# Copyright (c) 2016 ZTE Corporation
+# feng.xiaowei@zte.com.cn
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+import tornado.web
+
+from opnfv_testapi.tornado_swagger import settings
+from opnfv_testapi.tornado_swagger import views
+
+
+def swagger_handlers():
+ prefix = settings.docs_settings.get('swagger_prefix', '/swagger')
+ if prefix[-1] != '/':
+ prefix += '/'
+
+ def _path(suffix):
+ return prefix + suffix
+ return [
+ tornado.web.URLSpec(
+ _path(r'spec.html$'),
+ views.SwaggerUIHandler,
+ settings.docs_settings,
+ name=settings.API_DOCS_NAME),
+ tornado.web.URLSpec(
+ _path(r'resources.json$'),
+ views.SwaggerResourcesHandler,
+ settings.docs_settings,
+ name=settings.RESOURCE_LISTING_NAME),
+ tornado.web.URLSpec(
+ _path(r'APIs$'),
+ views.SwaggerApiHandler,
+ settings.docs_settings,
+ name=settings.API_DECLARATION_NAME),
+ ]
diff --git a/opnfv_testapi/tornado_swagger/settings.py b/opnfv_testapi/tornado_swagger/settings.py
new file mode 100644
index 0000000..2842261
--- /dev/null
+++ b/opnfv_testapi/tornado_swagger/settings.py
@@ -0,0 +1,25 @@
+##############################################################################
+# Copyright (c) 2016 ZTE Corporation
+# feng.xiaowei@zte.com.cn
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+API_DOCS_NAME = 'swagger-api-docs'
+RESOURCE_LISTING_NAME = 'swagger-resource-listing'
+API_DECLARATION_NAME = 'swagger-api-declaration'
+
+docs_settings = {
+ 'base_url': '',
+ 'static_path': '',
+ 'swagger_prefix': '/swagger',
+ 'api_version': 'v1.0',
+ 'swagger_version': '1.2',
+ 'api_key': '',
+ 'enabled_methods': ['get', 'post', 'put', 'patch', 'delete'],
+ 'exclude_namespaces': [],
+}
+
+models = []
diff --git a/opnfv_testapi/tornado_swagger/swagger.py b/opnfv_testapi/tornado_swagger/swagger.py
new file mode 100644
index 0000000..83f389a
--- /dev/null
+++ b/opnfv_testapi/tornado_swagger/swagger.py
@@ -0,0 +1,291 @@
+##############################################################################
+# Copyright (c) 2016 ZTE Corporation
+# feng.xiaowei@zte.com.cn
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+from HTMLParser import HTMLParser
+from functools import wraps
+import inspect
+
+import epydoc.markup
+import tornado.web
+
+from opnfv_testapi.tornado_swagger import handlers
+from opnfv_testapi.tornado_swagger import settings
+
+
+class EpytextParser(HTMLParser):
+ a_text = False
+
+ def __init__(self, tag):
+ HTMLParser.__init__(self)
+ self.tag = tag
+ self.data = None
+
+ def handle_starttag(self, tag, attr):
+ if tag == self.tag:
+ self.a_text = True
+
+ def handle_endtag(self, tag):
+ if tag == self.tag:
+ self.a_text = False
+
+ def handle_data(self, data):
+ if self.a_text:
+ self.data = data
+
+ def get_data(self):
+ return self.data
+
+
+class DocParser(object):
+ def __init__(self):
+ self.notes = None
+ self.summary = None
+ self.responseClass = None
+ self.responseMessages = []
+ self.params = {}
+ self.properties = {}
+
+ def parse_docstring(self, text):
+ if text is None:
+ return
+
+ errors = []
+ doc = epydoc.markup.parse(text, markup='epytext', errors=errors)
+ _, fields = doc.split_fields(errors)
+
+ for field in fields:
+ tag = field.tag()
+ arg = field.arg()
+ body = field.body()
+ self._get_parser(tag)(arg=arg, body=body)
+ return doc
+
+ def _get_parser(self, tag):
+ parser = {
+ 'param': self._parse_param,
+ 'type': self._parse_type,
+ 'in': self._parse_in,
+ 'required': self._parse_required,
+ 'rtype': self._parse_rtype,
+ 'property': self._parse_property,
+ 'ptype': self._parse_ptype,
+ 'return': self._parse_return,
+ 'raise': self._parse_return,
+ 'notes': self._parse_notes,
+ 'description': self._parse_description,
+ }
+ return parser.get(tag, self._not_supported)
+
+ def _parse_param(self, **kwargs):
+ arg = kwargs.get('arg', None)
+ body = self._get_body(**kwargs)
+ self.params.setdefault(arg, {}).update({
+ 'name': arg,
+ 'description': body,
+ })
+
+ if 'paramType' not in self.params[arg]:
+ self.params[arg]['paramType'] = 'query'
+
+ def _parse_type(self, **kwargs):
+ arg = kwargs.get('arg', None)
+ body = self._get_body(**kwargs)
+ self.params.setdefault(arg, {}).update({
+ 'name': arg,
+ 'dataType': body
+ })
+
+ def _parse_in(self, **kwargs):
+ arg = kwargs.get('arg', None)
+ body = self._get_body(**kwargs)
+ self.params.setdefault(arg, {}).update({
+ 'name': arg,
+ 'paramType': body
+ })
+
+ def _parse_required(self, **kwargs):
+ arg = kwargs.get('arg', None)
+ body = self._get_body(**kwargs)
+ self.params.setdefault(arg, {}).update({
+ 'name': arg,
+ 'required': False if body in ['False', 'false'] else True
+ })
+
+ def _parse_rtype(self, **kwargs):
+ body = self._get_body(**kwargs)
+ self.responseClass = body
+
+ def _parse_property(self, **kwargs):
+ arg = kwargs.get('arg', None)
+ self.properties.setdefault(arg, {}).update({
+ 'type': 'string'
+ })
+
+ def _parse_ptype(self, **kwargs):
+ arg = kwargs.get('arg', None)
+ code = self._parse_epytext_para('code', **kwargs)
+ link = self._parse_epytext_para('link', **kwargs)
+ if code is None:
+ self.properties.setdefault(arg, {}).update({
+ 'type': link
+ })
+ elif code == 'list':
+ self.properties.setdefault(arg, {}).update({
+ 'type': 'array',
+ 'items': {'type': link}
+ })
+
+ def _parse_return(self, **kwargs):
+ arg = kwargs.get('arg', None)
+ body = self._get_body(**kwargs)
+ self.responseMessages.append({
+ 'code': arg,
+ 'message': body
+ })
+
+ def _parse_notes(self, **kwargs):
+ body = self._get_body(**kwargs)
+ self.notes = self._sanitize_doc(body)
+
+ def _parse_description(self, **kwargs):
+ body = self._get_body(**kwargs)
+ self.summary = self._sanitize_doc(body)
+
+ def _not_supported(self, **kwargs):
+ pass
+
+ @staticmethod
+ def _sanitize_doc(comment):
+ return comment.replace('\n', '<br/>') if comment else comment
+
+ @staticmethod
+ def _get_body(**kwargs):
+ body = kwargs.get('body', None)
+ return body.to_plaintext(None).strip() if body else body
+
+ @staticmethod
+ def _parse_epytext_para(tag, **kwargs):
+ def _parse_epytext(tag, body):
+ epytextParser = EpytextParser(tag)
+ epytextParser.feed(str(body))
+ data = epytextParser.get_data()
+ epytextParser.close()
+ return data
+
+ body = kwargs.get('body', None)
+ return _parse_epytext(tag, body) if body else body
+
+
+class model(DocParser):
+ def __init__(self, *args, **kwargs):
+ super(model, self).__init__()
+ self.args = args
+ self.kwargs = kwargs
+ self.required = []
+ self.cls = None
+
+ def __call__(self, *args):
+ if self.cls:
+ return self.cls
+
+ cls = args[0]
+ self._parse_model(cls)
+
+ return cls
+
+ def _parse_model(self, cls):
+ self.id = cls.__name__
+ self.cls = cls
+ if '__init__' in dir(cls):
+ self._parse_args(cls.__init__)
+ self.parse_docstring(inspect.getdoc(cls))
+ settings.models.append(self)
+
+ def _parse_args(self, func):
+ argspec = inspect.getargspec(func)
+ argspec.args.remove("self")
+ defaults = {}
+ if argspec.defaults:
+ defaults = list(zip(argspec.args[-len(argspec.defaults):],
+ argspec.defaults))
+ required_args_count = len(argspec.args) - len(defaults)
+ for arg in argspec.args[:required_args_count]:
+ self.required.append(arg)
+ self.properties.setdefault(arg, {'type': 'string'})
+ for arg, default in defaults:
+ self.properties.setdefault(arg, {
+ 'type': 'string',
+ "default": default
+ })
+
+
+class operation(DocParser):
+ def __init__(self, nickname='apis', **kwds):
+ super(operation, self).__init__()
+ self.nickname = nickname
+ self.func = None
+ self.func_args = []
+ self.kwds = kwds
+
+ def __call__(self, *args, **kwds):
+ if self.func:
+ return self.func(*args, **kwds)
+
+ func = args[0]
+ self._parse_operation(func)
+
+ @wraps(func)
+ def __wrapper__(*in_args, **in_kwds):
+ return self.func(*in_args, **in_kwds)
+
+ __wrapper__.rest_api = self
+ return __wrapper__
+
+ def _parse_operation(self, func):
+ self.func = func
+
+ self.__name__ = func.__name__
+ self._parse_args(func)
+ self.parse_docstring(inspect.getdoc(self.func))
+
+ def _parse_args(self, func):
+ argspec = inspect.getargspec(func)
+ argspec.args.remove("self")
+
+ defaults = []
+ if argspec.defaults:
+ defaults = argspec.args[-len(argspec.defaults):]
+
+ for arg in argspec.args:
+ if arg in defaults:
+ required = False
+ else:
+ required = True
+ self.params.setdefault(arg, {
+ 'name': arg,
+ 'required': required,
+ 'paramType': 'path',
+ 'dataType': 'string'
+ })
+ self.func_args = argspec.args
+
+
+def docs(**opts):
+ settings.docs_settings.update(opts)
+
+
+class Application(tornado.web.Application):
+ def __init__(self, app_handlers=None,
+ default_host="",
+ transforms=None,
+ **settings):
+ super(Application, self).__init__(
+ handlers.swagger_handlers() + app_handlers,
+ default_host,
+ transforms,
+ **settings)
diff --git a/opnfv_testapi/tornado_swagger/views.py b/opnfv_testapi/tornado_swagger/views.py
new file mode 100644
index 0000000..7939997
--- /dev/null
+++ b/opnfv_testapi/tornado_swagger/views.py
@@ -0,0 +1,134 @@
+##############################################################################
+# Copyright (c) 2016 ZTE Corporation
+# feng.xiaowei@zte.com.cn
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+import inspect
+import json
+
+import tornado.template
+import tornado.web
+
+from opnfv_testapi.tornado_swagger import settings
+
+
+def json_dumps(obj, pretty=False):
+ return json.dumps(obj,
+ sort_keys=True,
+ indent=4,
+ separators=(',', ': ')) if pretty else json.dumps(obj)
+
+
+class SwaggerUIHandler(tornado.web.RequestHandler):
+ def initialize(self, **kwargs):
+ self.static_path = kwargs.get('static_path')
+ self.base_url = kwargs.get('base_url')
+
+ def get_template_path(self):
+ return self.static_path
+
+ def get(self):
+ resource_url = self.reverse_url(settings.RESOURCE_LISTING_NAME)
+ discovery_url = self.base_url + resource_url
+ self.render('swagger/index.html', discovery_url=discovery_url)
+
+
+class SwaggerResourcesHandler(tornado.web.RequestHandler):
+ def initialize(self, **kwargs):
+ self.api_version = kwargs.get('api_version')
+ self.swagger_version = kwargs.get('swagger_version')
+ self.base_url = kwargs.get('base_url')
+ self.exclude_namespaces = kwargs.get('exclude_namespaces')
+
+ def get(self):
+ self.set_header('content-type', 'application/json')
+ resources = {
+ 'apiVersion': self.api_version,
+ 'swaggerVersion': self.swagger_version,
+ 'basePath': self.base_url,
+ 'apis': [{
+ 'path': self.reverse_url(settings.API_DECLARATION_NAME),
+ 'description': 'Restful APIs Specification'
+ }]
+ }
+
+ self.finish(json_dumps(resources, self.get_arguments('pretty')))
+
+
+class SwaggerApiHandler(tornado.web.RequestHandler):
+ def initialize(self, **kwargs):
+ self.api_version = kwargs.get('api_version')
+ self.swagger_version = kwargs.get('swagger_version')
+ self.base_url = kwargs.get('base_url')
+
+ def get(self):
+ self.set_header('content-type', 'application/json')
+ apis = self.find_api(self.application.handlers)
+ if apis is None:
+ raise tornado.web.HTTPError(404)
+
+ specs = {
+ 'apiVersion': self.api_version,
+ 'swaggerVersion': self.swagger_version,
+ 'basePath': self.base_url,
+ 'resourcePath': '/',
+ 'produces': ["application/json"],
+ 'apis': [self.__get_api_spec__(path, spec, operations)
+ for path, spec, operations in apis],
+ 'models': self.__get_models_spec(settings.models)
+ }
+ self.finish(json_dumps(specs, self.get_arguments('pretty')))
+
+ def __get_models_spec(self, models):
+ models_spec = {}
+ for model in models:
+ models_spec.setdefault(model.id, self.__get_model_spec(model))
+ return models_spec
+
+ @staticmethod
+ def __get_model_spec(model):
+ return {
+ 'description': model.summary,
+ 'id': model.id,
+ 'notes': model.notes,
+ 'properties': model.properties,
+ 'required': model.required
+ }
+
+ @staticmethod
+ def __get_api_spec__(path, spec, operations):
+ return {
+ 'path': path,
+ 'description': spec.handler_class.__doc__,
+ 'operations': [{
+ 'httpMethod': api.func.__name__.upper(),
+ 'nickname': api.nickname,
+ 'parameters': api.params.values(),
+ 'summary': api.summary,
+ 'notes': api.notes,
+ 'responseClass': api.responseClass,
+ 'responseMessages': api.responseMessages,
+ } for api in operations]
+ }
+
+ @staticmethod
+ def find_api(host_handlers):
+ def get_path(url, args):
+ return url % tuple(['{%s}' % arg for arg in args])
+
+ def get_operations(cls):
+ return [member.rest_api
+ for (_, member) in inspect.getmembers(cls)
+ if hasattr(member, 'rest_api')]
+
+ for host, handlers in host_handlers:
+ for spec in handlers:
+ for (_, mbr) in inspect.getmembers(spec.handler_class):
+ if inspect.ismethod(mbr) and hasattr(mbr, 'rest_api'):
+ path = get_path(spec._path, mbr.rest_api.func_args)
+ operations = get_operations(spec.handler_class)
+ yield path, spec, operations
+ break