Repository URL to install this package:
|
Version:
0.24.7.dev0 ▾
|
import json
import datetime
from optparse import make_option
from importlib import import_module
from django.core.management.base import BaseCommand
from django.conf import settings
from rest_framework_swagger.docgenerator import DocumentationGenerator
from rest_framework_swagger.urlparser import UrlParser
# Since this mgmt command is in hub-client,
# we need to dynamically import the ROOT_URLCONF
# for this to work in both docker_index and docker_io
url_conf = import_module(settings.ROOT_URLCONF)
SWAGGER_SETTINGS = getattr(settings, 'SWAGGER_SETTINGS')
class Command(BaseCommand):
"""
Generate v2 Swagger Documenation
Write swagger.json file
Full spec description can be found: http://swagger.io/specification/
"""
option_list = BaseCommand.option_list + (
make_option(
'-o', '--output-file',
action='store',
dest='output',
default=None,
help='File to write swagger spec to. If not specified will write to stdout.'
),
)
def handle(self, *args, **options):
verbosity = int(options.get('verbosity', 1))
document_generator = DocumentationGenerator()
if verbosity > 2:
self.stdout.write('Generating API docs...')
urlparser = UrlParser()
# list of URL's
urls = urlparser.get_apis(url_conf.urlpatterns)
# list of model definitions (ie serializers)
models = document_generator.get_models(urls)
# list of URLS and operations per url with reference to models above
apis = document_generator.generate(urls)
# JSON date handler, some objects in the api/models are python Datetime objects
# and JSON's default handler chokes on those.
date_handler = lambda obj: obj.isoformat() if isinstance(obj, datetime.datetime) else None
# generate a v2.0 swagger spec dictionary
swagger_spec = self.generate_swagger_object(apis, models)
# convert dictionary to JSON
swagger_spec = json.dumps(
swagger_spec,
sort_keys=True,
indent=4,
separators=(',', ': '),
default = date_handler
)
output = options.get('output')
if output is None:
self.stdout.write(swagger_spec)
else:
with open(output, 'w') as f:
f.write(swagger_spec)
self.stdout.write('Swagger spec written to `{}`'.format(output))
def generate_swagger_object(self, apis, models):
"""
Generate a Swagger Object, the root document for the API specification
http://swagger.io/specification/#swaggerObject
:retuns: {
'swagger': '2.0',
'info': InfoOjbect,
'host': 'hub.docker.com',
'schemes': ['https', ],
'basePath': '/v2',
'produces': ['application/json', ],
'paths': Paths Object,
'definitions': Definitions Object,
'parameters': Paremters Definitions Object,
'responses': Responses Definitions Object,
'securityDefinitions': Security Definitions Object,
'secirity': Security Requirement Object,
'tags': List of Tag Objects,
'externalDocs': External Documentation Object,
}
"""
return {
'swagger': '2.0',
'info': self.generate_info_object(),
'host': 'hub.docker.com',
'schemes': ['https', ],
'basePath': SWAGGER_SETTINGS['api_path'],
'produces': ['application/json', ],
'paths': self.generate_paths_object(apis),
'definitions': self.generate_definitions_object(models),
'parameters': {},
'responses': {},
'securityDefinitions': {},
'security': [],
'tags': [],
}
def generate_info_object(self):
"""
Info object provides metadata bout the apis
http://swagger.io/specification/#infoObject
:returns: {
'title': 'Docker Hub API',
'description': 'API for hub.docker.com',
'termsOfService': '',
'contact': Contect Object
'license': Licence Object
'version': "2.0.0",
}
"""
return {
'title': SWAGGER_SETTINGS['info']['title'],
'description': SWAGGER_SETTINGS['info'].get('description', ''),
'termsOfService': SWAGGER_SETTINGS['info'].get('termsOfServiceUrl', ''),
'contact': self.generate_contact_object(),
'license': self.generate_licence_object(),
'version': SWAGGER_SETTINGS['api_version'],
}
def generate_contact_object(self):
"""
Generate the Contact Object
http://swagger.io/specification/#contactObject
:returns: {
'name': '',
'url': '',
'email': '',
}
"""
return {
'name': 'Docker Hub',
'url': getattr(settings, 'WEB_APP_URL', ''),
'email': SWAGGER_SETTINGS['info']['contact'],
}
def generate_licence_object(self):
"""
Generate the Licence Object
http://swagger.io/specification/#licenseObject
:returns: {
'name': '',
'url': '',
}
"""
return {
'name': SWAGGER_SETTINGS['info'].get('license', ''),
'url': SWAGGER_SETTINGS['info'].get('licenseUrl', ''),
}
def generate_paths_object(self, paths):
"""
Generate a Paths Object
http://swagger.io/specification/#pathsObject
:param paths: List of API URL's
:type paths: list
example:
[
{
'path': string,
'description': string,
'operations': list # see `generate_operation_object`
}
]
:returns: {
'/{path}': Path Item Object
}
"""
document_paths = {}
for path in paths:
url = path['path']
path_items = document_paths.get(url, {})
path_items.update(self.generate_path_item(path))
document_paths[url] = path_items
return document_paths
def generate_path_item(self, path):
"""
Generate a Path Item Object
http://swagger.io/specification/#pathItemObject
:param path: dictionary of operations of a URL
:type path: dictionary
example:
{
'path': string,
'description': string,
'operations': list # see `generate_operation_object`
}
:returns: {
$ref: string,
'get': Operation Object,
'put': Operation Object,
'post': Operation Object,
'delete': Operation Object,
'options': Operation Object,
'head': Operation Object,
'patch': Operation Object,
'parameters': (Parameter Object | Reference Object]
}
"""
path_item = {}
for operation in path['operations']:
path_item[operation['method'].lower()] = self.generate_operation_object(operation)
return path_item
def generate_operation_object(self, operation):
"""
Generate an Operation Object
http://swagger.io/specification/#operationObject
:param operation: dictionary of operations for a URL
:type operation: dictionary
example:
{
'parameters': list of parameters, # see `generate_parameter_object`
'nickname': string,
'notes': 'string',
'responseMessages' list of response messages, # see `generate_responses_object`
'summary': string,
'type': string,
'method': string,
}
:returns: {
'tags': list of strings,
'summary': string,
'description': string,
'externalDocs': External Docs Object,
'operationId': string,
'consumes': list of stings,
'produces': list of strings,
'parameters': list of Paremter Objects,
'responses': Responses Object,
'schemes': list of strings,
'depreicated': boolean,
'security': Security Requirement Object,
}
"""
# skeleton of object to be returned
operation_object = {
'tags': operation.get('tags', []),
'summary': operation.get('summary', ''),
'description': operation.get('notes', ''),
'consumes': operation.get('consumes', []),
'produces': operation.get('produces', []),
# empty list of parameters, will fill in later
'parameters': [],
'responses': self.generate_responses_object(operation.get('responseMessages', '')),
'schemes': operation.get('schemes', []),
'security': [],
}
# fill in the empty list of parameters
for parameter in operation.get('parameters', []):
parameter_object = self.generate_parameter_object(parameter)
operation_object['parameters'].append(parameter_object)
return operation_object
def generate_parameter_object(self, parameter):
"""
Generate Parameter Object
http://swagger.io/specification/#parameterObject
:param parameter: dictionary representation of a parameter for an operation of a URL
:type path: dictionary
example:
{
'paramType': string,
'required': boolean,
'type': string,
'name': string,
'format': string,
}
:returns: {
'name': string,
'in': string ('query' | 'header' | 'path' | 'formData' | 'body'),
'description': string,
'required': boolean,
}
"""
# parameter types in v2 are named differently than they are in v1.2
# this is a mapping to convert the old param types to new.
param_location_maping = {
"query": "query",
"header": "header",
"path": "path",
'form': "formData",
"body": "body",
}
parameter_object = {
'name': parameter['name'],
'in': param_location_maping[parameter['paramType']],
'description': '' if parameter.get('description') is None else parameter['description'],
'required': parameter.get('required', False),
}
# If parameter is in the body, a schema attribute is required.
if parameter_object['in'] == 'body':
parameter_object['schema'] = {}
else:
parameter_object['type'] = parameter.get('type', 'string')
# if the param_type is `choice` or `enum` v2 requires it to be a string
# but we also need to include an `enum` which lists the values
# of the choices.
if parameter_object['type'] in ['choice', 'enum']:
parameter_object['type'] = 'string'
parameter_object['enum'] = parameter.get('enum', ['*'])
# if the param_type is a `query` it needs to be change to type string
elif parameter_object.get('type') == 'query':
parameter_object['type'] = 'string'
parameter_object['format'] = parameter.get('format', 'string')
return parameter_object
def generate_responses_object(self, responses):
"""
Generate Responses Object
http://swagger.io/specification/#responsesObject
:param responses: List of responses for a URL
:type responses: list
example:
[{
'responseModel': reference to response model (optional),
'message': string,
'code': integer
},]
:returns: {
'{code}': Response Object
}
"""
responses_object = {
"default": {
"description": "Unexpected error",
}
}
for response in responses:
responses_object[response['code']] = {'description': response.get('message', '')}
return responses_object
def generate_definitions_object(self, models):
"""
Generate a Definitions Object
http://swagger.io/specification/#definitionsObject
:param models: Dictionary generated from DocumentationGenerator.get_models()
:type models: dict
example param models
# TODO put in example
:returns: {
'{name}': Schema Object
}
"""
definitions_object = {}
for name, schema in models.iteritems():
definitions_object[name] = self.generate_schema_object(schema)
return definitions_object
def generate_schema_object(self, schema):
"""
Generate Schema Object
http://swagger.io/specification/#schemaObject
:param schema: Sub-Dictionary taken from self.models
:type schema: dict
example param schema:
{
'required': ['id', 'name', 'avatar_url', 'type', 'repo_list'],
'id': 'WriteGithubRepoDataSerializeyr',
'properties': OrderedDict([
('id', {'description': None, 'format': 'int64', 'required': True, 'readOnly': False, 'type': 'integer'}),
('name', {'description': None, 'required': True, 'readOnly': False, 'type': 'string'}),
('avatar_url', {'description': None, 'required': True, 'readOnly': False, 'type': 'string'}),
('type', {'enum': ['User', 'Organization'], 'description': None, 'required': True, 'readOnly': False, 'type': 'choice'}),
('repo_list', {'description': None, 'items': {'$ref': 'GithubRepoDetailsSerializer'}, 'required': True, 'readOnly': False, 'type': 'array'})
])
}
:returns: {
'type': 'object',
'properties': dictionary
'required': list of required fields, # optional
}
"""
# skeleton schema object
schema_object = {
'type': 'object',
'properties': {},
}
if schema.get('required'):
schema_object['required'] = schema['required']
for name, _ in schema['properties'].iteritems():
existing_prop = schema['properties'][name]
new_prop = {}
if existing_prop['type'] in ['boolean', 'string', 'array', 'integer', 'choice']:
prop_details = {
'type': existing_prop['type'],
}
if prop_details.get('type') == 'choice':
prop_details['type'] = 'string'
prop_details['enum'] = existing_prop.get('enum', [])
elif prop_details.get('type') == 'array':
prop_details['items'] = existing_prop['items']
else:
# generated swagger doc stores definitons at this path
# definitions from various api endpoints will also reference
# this defitiions path.
prop_details = {
'$ref': "#/definitions/{}".format(existing_prop['type'])
}
prop_details.update({
'format': existing_prop.get('format', 'string'),
'description': '' if existing_prop.get('description') is None else existing_prop['description'],
'readOnly': existing_prop.get('readOnly', False),
})
new_prop.update({name: prop_details})
schema_object['properties'].update(new_prop)
return schema_object