Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
hub-client / dockerhub / api_docs / management / commands / generate_api_docs.py
Size: Mime:
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