Skip to content

Instantly share code, notes, and snippets.

@dnordberg
Last active May 9, 2016 22:43
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save dnordberg/5661696 to your computer and use it in GitHub Desktop.
Save dnordberg/5661696 to your computer and use it in GitHub Desktop.
Generate Swagger documentation stubs for flask-restless.
# Script used to help generate Swagger docs.
import re
import os
import argparse
import urlparse
import simplejson
from collections import defaultdict
from sqlalchemy.ext.declarative.api import DeclarativeMeta
SKIP_MODELS = ['BlogPost', 'BlogCategory', 'DBVersion']
SKIP_ROUTES = ['blog_post', 'blog_category', 'db_version']
api_docs_path = './docs/api/'
SWAGGER_VERSION = "1.1"
if not os.path.exists(api_docs_path):
os.makedirs(api_docs_path)
class APIDocumentationManager(object):
def __init__(self, app, tables, base_path, version, docs={}):
self.app = app
self.base_path = base_path
self.version = version
path_info = urlparse.urlsplit(base_path)
self.rel_path = path_info.path
self.models = self.get_models(tables)
self.resources = self.get_resources()
def get_models(self, mod):
models = dict([(name, cls)
for name, cls in mod.__dict__.items()
if hasattr(cls, '__table__')
and name not in SKIP_MODELS])
json_models = {}
for name, model in models.iteritems():
json_models[name] = {}
json_models[name]["id"] = name
properties = model.__table__.columns.items()
json_properties = {}
for propkey, prop in properties:
json_properties[propkey] = {"type": str(prop.type)}
json_models[name]["properties"] = json_properties
return json_models
def get_resources(self):
routes_by_resource = defaultdict(list)
for route in self.app.url_map.iter_rules():
if not route.rule.startswith(self.rel_path) \
or 'api-docs' in route.rule:
continue
url = route.rule.replace(self.rel_path, '')
parent = url.split('/')[0]
if not parent in SKIP_ROUTES:
routes_by_resource[parent].append(route)
return routes_by_resource
def update_index(self):
"""Writes a file api_docs_index.json and adds missing routes."""
api_routes = []
for resource, routes in self.resources.iteritems():
if not resource in SKIP_ROUTES:
api_routes.append({
'path': "/api-docs/{}".format(resource),
'description': 'The {} resource'.format(resource.capitalize())
})
with open('./docs/api/index.json', 'w') as api_docs_index:
simplejson.dump({"apiVersion": self.version,
"swaggerVersion": SWAGGER_VERSION,
"basePath": self.base_path,
"apis": api_routes,
},
api_docs_index,
indent=' ')
print "Updated index"
def update_route_spec(self):
for resource, routes in self.resources.iteritems():
api_routes = []
for route in routes:
operations = []
arguments = route.arguments # set
endpoint = route.endpoint # string
methods = route.methods # set
rule = route.rule # string
for method in methods:
response_class = resource.capitalize()
skip_params = False
if method == "GET":
if 'instid' in rule:
summary = 'Find {} by ID'.format(resource)
notes = 'Returns a {} based on ID'.format(resource)
else:
summary = 'Find {} instances'.format(resource)
notes = 'Returns {} instances'.format(resource)
skip_params = True
elif method == 'DELETE':
summary = 'Delete {} by ID'.format(resource)
notes = 'Returns 204 NO CONTENT'
response_class = 'void'
elif method == 'PUT' or method == 'PATCH':
summary = 'Update {} by ID'.format(resource)
notes = 'Returns the updated instance'
elif method == 'POST':
summary = 'Create {} by ID'.format(resource)
notes = 'Returns the created instance ID'
response_class = resource.capitalize()
else:
continue
parameters = []
if not skip_params:
for param in arguments:
description = ""
data_type = "string"
if param == "instid":
description = "Instance id"
parameters.append({
"name": param,
"description": description,
"paramType": "path",
"allowMultiple": False,
"dataType": data_type
})
error_response = [
{
"code": 400,
"reason": "Invalid ID supplied"
},
{
"code": 404,
"reason": "Not found"
},
{
"code":405,
"reason":"Validation exception"
}
]
operation = {
"httpMethod": method,
"summary": summary,
"notes": notes,
"responseClass": ''.join([s.capitalize() for s in response_class.split('_')]),
"nickname": endpoint.replace('.', '_'),
"parameters": parameters,
"errorResponse": error_response
}
operations.append(operation)
api_routes.append({
"path": route.rule.replace(self.rel_path, '/'),
"description": "Operations about {}".format(resource),
"operations": operations
})
route_spec = {
"swaggerVersion": SWAGGER_VERSION,
"basePath": self.base_path,
"resourcePath": '/{}'.format(resource),
"models": self.models,
"apis": api_routes,
}
with open('./docs/api/{}.json'.format(resource), 'w') as api_doc:
simplejson.dump(route_spec,
api_doc,
indent=' ')
print "Updated route spec {}".format(resource)
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--update-index', action='store_true',
help='Updates index page with API routes.')
parser.add_argument('--update-route-spec', action='store_true',
help='Updates route spec for specified route parent.')
parser.add_argument('--base-path', type=str, default=None,
help='Base API path.')
parser.add_argument('--version', type=str, default=None,
help='API version.')
parser.add_argument('--app-module', type=str, default=None,
help='App module.')
parser.add_argument('--tables-module', type=str, default=None,
help='Tables module.')
args = parser.parse_args()
parts = args.app_module.split('.')
mod = parts[:-1]
ins = parts[-1]
app_module = __import__('.'.join(mod), {}, {}, [mod[-1]])
app = getattr(app_module, ins)
tables = __import__(args.tables_module, {}, {}, [args.tables_module])
documentationmanager = APIDocumentationManager(app, tables, args.base_path, args.version)
if args.update_index:
documentationmanager.update_index()
if args.update_route_spec:
documentationmanager.update_route_spec()
if __name__ == '__main__':
main()
# Routes for Swagger json resources.
from flask import Response
RE_API_DOCS_BASE_PATH = re.compile(r'"basePath": "(.*)\/api\/')
API_SANDBOX_URL = '"basePath": "http://localhost:5000/api/'
@app.route('/api/v1/api-docs', methods=['GET'])
def api_docs_index():
return Response(RE_API_DOCS_BASE_PATH.sub(API_SANDBOX_URL,
open('./docs/api/v1/index.json').read()),
mimetype='application/json')
@app.route('/api/v1/api-docs/<resource>', methods=['GET'])
def api_docs(resource):
return Response(RE_API_DOCS_BASE_PATH.sub(API_SANDBOX_URL,
open('./docs/api/v1/{}.json'.format(resource)).read()),
mimetype='application/json')
# Generate Swagger docs.
python -m api_docs --base-path='http://localhost:5000/api/v1/' --version=0.9 --app-module='app.app' --tables-module='models' --update-route-spec
python -m api_docs --base-path='http://localhost:5000/api/v1/' --version=0.9 --app-module='app.app' --tables-module='models' --update-index
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment