Switch the Python side to Swagger v2

This commit is contained in:
Joseph Schorr 2015-05-14 16:47:38 -04:00
parent 86f400fdf5
commit 0bc1c29dff
21 changed files with 217 additions and 138 deletions

View file

@ -1,5 +1,8 @@
""" API discovery information. """
import re
import logging
import sys
from flask.ext.restful import reqparse
@ -7,7 +10,7 @@ from endpoints.api import (ApiResource, resource, method_metadata, nickname, tru
parse_args, query_param)
from app import app
from auth import scopes
from collections import OrderedDict
logger = logging.getLogger(__name__)
@ -32,162 +35,195 @@ def fully_qualified_name(method_view_class):
def swagger_route_data(include_internal=False, compact=False):
apis = []
def swagger_parameter(name, description, kind='path', param_type='string', required=True,
enum=None, schema=None):
# https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#parameterObject
parameter_info = {
'name': name,
'in': kind,
'required': required
}
if not compact:
parameter_info['description'] = description or ''
if schema:
parameter_info['schema'] = {
'$ref': '#/definitions/%s' % schema
}
else:
parameter_info['type'] = param_type
if enum is not None and len(list(enum)) > 0:
parameter_info['enum'] = list(enum)
return parameter_info
paths = {}
models = {}
tags = []
tags_added = set()
for rule in app.url_map.iter_rules():
endpoint_method = app.view_functions[rule.endpoint]
if 'view_class' in dir(endpoint_method):
view_class = endpoint_method.view_class
# Verify that we have a view class for this API method.
if not 'view_class' in dir(endpoint_method):
continue
param_data_map = {}
if '__api_path_params' in dir(view_class):
param_data_map = view_class.__api_path_params
view_class = endpoint_method.view_class
operations = []
# Hide the class if it is internal.
internal = method_metadata(view_class, 'internal')
if not include_internal and internal:
continue
method_names = list(rule.methods.difference(['HEAD', 'OPTIONS']))
for method_name in method_names:
method = getattr(view_class, method_name.lower(), None)
# Build the tag.
parts = fully_qualified_name(view_class).split('.')
tag_name = parts[-2]
if not tag_name in tags_added:
tags_added.add(tag_name)
tags.append({
'name': tag_name,
'description': (sys.modules[view_class.__module__].__doc__ or '').strip()
})
parameters = []
# Build the Swagger data for the path.
swagger_path = PARAM_REGEX.sub(r'{\2}', rule.rule)
path_swagger = {
'name': fully_qualified_name(view_class),
'tag': tag_name
}
for param in rule.arguments:
parameters.append({
'paramType': 'path',
'name': param,
'dataType': 'string',
'description': param_data_map.get(param, {'description': ''})['description'],
'required': True,
})
paths[swagger_path] = path_swagger
if method is None:
logger.debug('Unable to find method for %s in class %s', method_name, view_class)
else:
req_schema_name = method_metadata(method, 'request_schema')
if req_schema_name:
parameters.append({
'paramType': 'body',
'name': 'body',
'description': 'Request body contents.',
'dataType': req_schema_name,
'required': True,
})
# Add any global path parameters.
param_data_map = view_class.__api_path_params if '__api_path_params' in dir(view_class) else {}
if param_data_map:
path_parameters_swagger = []
for path_parameter in param_data_map:
description = param_data_map[path_parameter].get('description')
path_parameters_swagger.append(swagger_parameter(path_parameter, description))
schema = view_class.schemas[req_schema_name]
models[req_schema_name] = schema
path_swagger['parameters'] = path_parameters_swagger
if '__api_query_params' in dir(method):
for param_spec in method.__api_query_params:
new_param = {
'paramType': 'query',
'name': param_spec['name'],
'description': param_spec['help'],
'dataType': TYPE_CONVERTER[param_spec['type']],
'required': param_spec['required'],
}
# Add the individual HTTP operations.
method_names = list(rule.methods.difference(['HEAD', 'OPTIONS']))
for method_name in method_names:
# https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#operation-object
method = getattr(view_class, method_name.lower(), None)
if method is None:
logger.debug('Unable to find method for %s in class %s', method_name, view_class)
continue
if len(param_spec['choices']) > 0:
new_param['enum'] = list(param_spec['choices'])
parameters.append(new_param)
new_operation = {
'method': method_name,
'nickname': method_metadata(method, 'nickname') or '(unnamed)'
}
if not compact:
response_type = 'void'
res_schema_name = method_metadata(method, 'response_schema')
if res_schema_name:
models[res_schema_name] = view_class.schemas[res_schema_name]
response_type = res_schema_name
new_operation.update({
'type': response_type,
'summary': method.__doc__.strip() if method.__doc__ else '',
'parameters': parameters,
})
scope = method_metadata(method, 'oauth2_scope')
if scope and not compact:
new_operation['authorizations'] = {
'oauth2': [
{
'scope': scope.scope
}
],
}
internal = method_metadata(method, 'internal')
if internal is not None:
new_operation['internal'] = True
if include_internal:
requires_fresh_login = method_metadata(method, 'requires_fresh_login')
if requires_fresh_login is not None:
new_operation['requires_fresh_login'] = True
if not internal or (internal and include_internal):
# Swagger requires valid nicknames on all operations.
if new_operation.get('nickname'):
operations.append(new_operation)
else:
logger.debug('Operation missing nickname: %s' % method)
swagger_path = PARAM_REGEX.sub(r'{\2}', rule.rule)
new_resource = {
'path': swagger_path,
'description': view_class.__doc__.strip() if view_class.__doc__ else "",
'operations': operations,
'name': fully_qualified_name(view_class),
operation_swagger = {
'operationId': method_metadata(method, 'nickname') or 'unnamed',
'parameters': [],
}
related_user_res = method_metadata(view_class, 'related_user_resource')
if related_user_res is not None:
new_resource['quayUserRelated'] = fully_qualified_name(related_user_res)
if not compact:
operation_swagger.update({
'description': method.__doc__.strip() if method.__doc__ else '',
'tags': [tag_name]
})
internal = method_metadata(view_class, 'internal')
# Mark the method as internal.
internal = method_metadata(method, 'internal')
if internal is not None:
new_resource['internal'] = True
operation_swagger['internal'] = True
if include_internal:
requires_fresh_login = method_metadata(method, 'requires_fresh_login')
if requires_fresh_login is not None:
operation_swagger['requires_fresh_login'] = True
related_user_res = method_metadata(view_class, 'related_user_resource')
if related_user_res is not None:
operation_swagger['quayUserRelated'] = fully_qualified_name(related_user_res)
# Add the path parameters.
if rule.arguments:
for path_parameter in rule.arguments:
description = param_data_map.get(path_parameter, {}).get('description')
operation_swagger['parameters'].append(swagger_parameter(path_parameter, description))
# Add the query parameters.
if '__api_query_params' in dir(method):
for query_parameter_info in method.__api_query_params:
name = query_parameter_info['name']
description = query_parameter_info['help']
param_type = TYPE_CONVERTER[query_parameter_info['type']]
required = query_parameter_info['required']
operation_swagger['parameters'].append(
swagger_parameter(name, description, kind='query',
param_type=param_type,
required=required,
enum=query_parameter_info['choices']))
# Add the OAuth security block.
# https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#securityRequirementObject
scope = method_metadata(method, 'oauth2_scope')
if scope and not compact:
operation_swagger['security'] = [{'oauth2_implicit': scope.scope}]
# TODO: Add the responses block.
# https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#responsesObject
response_schema_name = method_metadata(method, 'response_schema')
if response_schema_name and not compact:
models[response_schema_name] = view_class.schemas[response_schema_name]
response_type = response_schema_name
# Add the request block.
request_schema_name = method_metadata(method, 'request_schema')
if request_schema_name and not compact:
models[request_schema_name] = view_class.schemas[request_schema_name]
operation_swagger['parameters'].append(
swagger_parameter('body', 'Request body contents.', kind='body',
schema=request_schema_name))
# Add the operation to the parent path.
if not internal or (internal and include_internal):
apis.append(new_resource)
path_swagger[method_name.lower()] = operation_swagger
tags.sort(key=lambda t: t['name'])
paths = OrderedDict(sorted(paths.items(), key=lambda p: p[1]['tag']))
# If compact form was requested, simply return the APIs.
if compact:
return {'apis': apis}
return {'paths': paths}
swagger_data = {
'apiVersion': 'v1',
'swaggerVersion': '1.2',
'basePath': '%s://%s' % (PREFERRED_URL_SCHEME, SERVER_HOSTNAME),
'resourcePath': '/',
'swagger': '2.0',
'host': SERVER_HOSTNAME,
'basePath': '/',
'schemes': [
PREFERRED_URL_SCHEME
],
'info': {
'title': 'Quay.io API',
'version': 'v1',
'title': 'Quay.io Frontend',
'description': ('This API allows you to perform many of the operations required to work '
'with Quay.io repositories, users, and organizations. You can find out more '
'at <a href="https://quay.io">Quay.io</a>.'),
'termsOfServiceUrl': 'https://quay.io/tos',
'contact': 'support@quay.io',
'termsOfService': 'https://quay.io/tos',
'contact': {
'email': 'support@quay.io'
}
},
'authorizations': {
'oauth2': {
'scopes': [scope._asdict() for scope in scopes.ALL_SCOPES.values()],
'grantTypes': {
"implicit": {
"tokenName": "access_token",
"loginEndpoint": {
"url": "%s://%s/oauth/authorize" % (PREFERRED_URL_SCHEME, SERVER_HOSTNAME),
},
},
},
'securityDefinitions': {
'oauth2_implicit': {
"type": "oauth2",
"flow": "implicit",
"authorizationUrl": "%s://%s/oauth/authorize" % (PREFERRED_URL_SCHEME, SERVER_HOSTNAME),
'scopes': {scope.scope:scope.description for scope in scopes.ALL_SCOPES.values()},
},
},
'apis': apis,
'models': models,
'paths': paths,
'definitions': models,
'tags': tags
}
return swagger_data