You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
391 lines
9.6 KiB
Python
391 lines
9.6 KiB
Python
#!/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
|
|
import httplib2
|
|
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?preferred=true'
|
|
|
|
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 = []
|
|
for line in args:
|
|
m = re.search('^\s+([a-zA-Z0-9_]+): (.*)', line)
|
|
if m is None:
|
|
continue
|
|
pname = m.group(1)
|
|
desc = m.group(2)
|
|
if '(required)' not in desc:
|
|
pname = pname + '=None'
|
|
parameters.append(pname)
|
|
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.
|
|
"""
|
|
service = build(name, version)
|
|
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 = httplib2.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 = httplib2.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.")
|