Last active
May 9, 2016 22:43
-
-
Save dnordberg/5661696 to your computer and use it in GitHub Desktop.
Generate Swagger documentation stubs for flask-restless.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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