blob: e36db534d3714c4d10f0b6751649709d4a798747 [file] [log] [blame]
#!/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.
"""
from __future__ import print_function
__author__ = "jcgregorio@google.com (Joe Gregorio)"
from collections import OrderedDict
import argparse
import collections
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__")
and discovery != {}
):
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:
api_directory = collections.defaultdict(list)
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"])
api_directory[api["name"]].append(api["version"])
# sort by api name and version number
for api in api_directory:
api_directory[api] = sorted(api_directory[api])
api_directory = OrderedDict(
sorted(api_directory.items(), key=lambda x: x[0])
)
markdown = []
for api, versions in api_directory.items():
markdown.append("## %s" % api)
for version in versions:
markdown.append(
"* [%s](http://googleapis.github.io/google-api-python-client/docs/dyn/%s_%s.html)"
% (version, api, version)
)
markdown.append("\n")
with open("docs/dyn/index.md", "w") as f:
f.write("\n".join(markdown).encode("utf-8"))
else:
sys.exit("Failed to load the discovery document.")