#!/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 = """<style>
body, h1, h2, h3, div, span, p, pre, a {
margin: 0;
padding: 0;
border: 0;
font-weight: inherit;
font-style: inherit;
font-size: 100%;
font-family: inherit;
vertical-align: baseline;
}
body {
font-size: 13px;
padding: 1em;
}
h1 {
font-size: 26px;
margin-bottom: 1em;
}
h2 {
font-size: 24px;
margin-bottom: 1em;
}
h3 {
font-size: 20px;
margin-bottom: 1em;
margin-top: 1em;
}
pre, code {
line-height: 1.5;
font-family: Monaco, 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Lucida Console', monospace;
}
pre {
margin-top: 0.5em;
}
h1, h2, h3, p {
font-family: Arial, sans serif;
}
h1, h2, h3 {
border-bottom: solid #CCC 1px;
}
.toc_element {
margin-top: 0.5em;
}
.firstline {
margin-left: 2 em;
}
.method {
margin-top: 1em;
border: solid 1px #CCC;
padding: 1em;
background: #EEE;
}
.details {
font-weight: bold;
font-size: 14px;
}
</style>
"""
METHOD_TEMPLATE = """<div class="method">
<code class="details" id="$name">$name($params)</code>
<pre>$doc</pre>
</div>
"""
COLLECTION_LINK = """<p class="toc_element">
<code><a href="$href">$name()</a></code>
</p>
<p class="firstline">Returns the $name Resource.</p>
"""
METHOD_LINK = """<p class="toc_element">
<code><a href="#$name">$name($params)</a></code></p>
<p class="firstline">$firstline</p>"""
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('<a href="%s.html">%s</a>' % (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 = [
'<html><body>',
css,
'<h1>%s</h1>' % breadcrumbs(path[:-1], root_discovery),
'<h2>Instance Methods</h2>'
]
# 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('<h3>Method Details</h3>')
for name in methods:
dname = name.rsplit('_')[0]
html.append(method(name, getattr(resource, name).__doc__))
html.append('</body></html>')
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.")