#!/usr/bin/python # # Copyright 2014 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Create documentation for generate API surfaces. Command-line tool that creates documentation for all APIs listed in discovery. The documentation is generated from a combination of the discovery document and the generated API surface itself. """ __author__ = 'jcgregorio@google.com (Joe Gregorio)' import argparse import json import os import re import string import sys from googleapiclient.discovery import DISCOVERY_URI from googleapiclient.discovery import build from googleapiclient.discovery import build_from_document from googleapiclient.discovery import UnknownApiNameOrVersion from googleapiclient.http import build_http import uritemplate CSS = """ """ METHOD_TEMPLATE = """
$name($params)
$doc
""" COLLECTION_LINK = """

$name()

Returns the $name Resource.

""" METHOD_LINK = """

$name($params)

$firstline

""" BASE = 'docs/dyn' DIRECTORY_URI = 'https://www.googleapis.com/discovery/v1/apis' parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--discovery_uri_template', default=DISCOVERY_URI, help='URI Template for discovery.') parser.add_argument('--discovery_uri', default='', help=('URI of discovery document. If supplied then only ' 'this API will be documented.')) parser.add_argument('--directory_uri', default=DIRECTORY_URI, help=('URI of directory document. Unused if --discovery_uri' ' is supplied.')) parser.add_argument('--dest', default=BASE, help='Directory name to write documents into.') def safe_version(version): """Create a safe version of the verion string. Needed so that we can distinguish between versions and sub-collections in URIs. I.e. we don't want adsense_v1.1 to refer to the '1' collection in the v1 version of the adsense api. Args: version: string, The version string. Returns: The string with '.' replaced with '_'. """ return version.replace('.', '_') def unsafe_version(version): """Undoes what safe_version() does. See safe_version() for the details. Args: version: string, The safe version string. Returns: The string with '_' replaced with '.'. """ return version.replace('_', '.') def method_params(doc): """Document the parameters of a method. Args: doc: string, The method's docstring. Returns: The method signature as a string. """ doclines = doc.splitlines() if 'Args:' in doclines: begin = doclines.index('Args:') if 'Returns:' in doclines[begin+1:]: end = doclines.index('Returns:', begin) args = doclines[begin+1: end] else: args = doclines[begin+1:] parameters = [] pname = None desc = '' def add_param(pname, desc): if pname is None: return if '(required)' not in desc: pname = pname + '=None' parameters.append(pname) for line in args: m = re.search('^\s+([a-zA-Z0-9_]+): (.*)', line) if m is None: desc += line continue add_param(pname, desc) pname = m.group(1) desc = m.group(2) add_param(pname, desc) parameters = ', '.join(parameters) else: parameters = '' return parameters def method(name, doc): """Documents an individual method. Args: name: string, Name of the method. doc: string, The methods docstring. """ params = method_params(doc) return string.Template(METHOD_TEMPLATE).substitute( name=name, params=params, doc=doc) def breadcrumbs(path, root_discovery): """Create the breadcrumb trail to this page of documentation. Args: path: string, Dot separated name of the resource. root_discovery: Deserialized discovery document. Returns: HTML with links to each of the parent resources of this resource. """ parts = path.split('.') crumbs = [] accumulated = [] for i, p in enumerate(parts): prefix = '.'.join(accumulated) # The first time through prefix will be [], so we avoid adding in a # superfluous '.' to prefix. if prefix: prefix += '.' display = p if i == 0: display = root_discovery.get('title', display) crumbs.append('%s' % (prefix + p, display)) accumulated.append(p) return ' . '.join(crumbs) def document_collection(resource, path, root_discovery, discovery, css=CSS): """Document a single collection in an API. Args: resource: Collection or service being documented. path: string, Dot separated name of the resource. root_discovery: Deserialized discovery document. discovery: Deserialized discovery document, but just the portion that describes the resource. css: string, The CSS to include in the generated file. """ collections = [] methods = [] resource_name = path.split('.')[-2] html = [ '', css, '

%s

' % breadcrumbs(path[:-1], root_discovery), '

Instance Methods

' ] # Which methods are for collections. for name in dir(resource): if not name.startswith('_') and callable(getattr(resource, name)): if hasattr(getattr(resource, name), '__is_resource__'): collections.append(name) else: methods.append(name) # TOC if collections: for name in collections: if not name.startswith('_') and callable(getattr(resource, name)): href = path + name + '.html' html.append(string.Template(COLLECTION_LINK).substitute( href=href, name=name)) if methods: for name in methods: if not name.startswith('_') and callable(getattr(resource, name)): doc = getattr(resource, name).__doc__ params = method_params(doc) firstline = doc.splitlines()[0] html.append(string.Template(METHOD_LINK).substitute( name=name, params=params, firstline=firstline)) if methods: html.append('

Method Details

') for name in methods: dname = name.rsplit('_')[0] html.append(method(name, getattr(resource, name).__doc__)) html.append('') return '\n'.join(html) def document_collection_recursive(resource, path, root_discovery, discovery): html = document_collection(resource, path, root_discovery, discovery) f = open(os.path.join(FLAGS.dest, path + 'html'), 'w') f.write(html.encode('utf-8')) f.close() for name in dir(resource): if (not name.startswith('_') and callable(getattr(resource, name)) and hasattr(getattr(resource, name), '__is_resource__')): dname = name.rsplit('_')[0] collection = getattr(resource, name)() document_collection_recursive(collection, path + name + '.', root_discovery, discovery['resources'].get(dname, {})) def document_api(name, version): """Document the given API. Args: name: string, Name of the API. version: string, Version of the API. """ try: service = build(name, version) except UnknownApiNameOrVersion as e: print 'Warning: {} {} found but could not be built.'.format(name, version) return http = build_http() response, content = http.request( uritemplate.expand( FLAGS.discovery_uri_template, { 'api': name, 'apiVersion': version}) ) discovery = json.loads(content) version = safe_version(version) document_collection_recursive( service, '%s_%s.' % (name, version), discovery, discovery) def document_api_from_discovery_document(uri): """Document the given API. Args: uri: string, URI of discovery document. """ http = build_http() response, content = http.request(FLAGS.discovery_uri) discovery = json.loads(content) service = build_from_document(discovery) name = discovery['version'] version = safe_version(discovery['version']) document_collection_recursive( service, '%s_%s.' % (name, version), discovery, discovery) if __name__ == '__main__': FLAGS = parser.parse_args(sys.argv[1:]) if FLAGS.discovery_uri: document_api_from_discovery_document(FLAGS.discovery_uri) else: http = build_http() resp, content = http.request( FLAGS.directory_uri, headers={'X-User-IP': '0.0.0.0'}) if resp.status == 200: directory = json.loads(content)['items'] for api in directory: document_api(api['name'], api['version']) else: sys.exit("Failed to load the discovery document.")