blob: 49ae2745114c2de525bbefcead5322ac81e7f008 [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)"
import argparse
import collections
import json
import pathlib
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.http import build_http
import uritemplate
DISCOVERY_DOC_DIR = (
pathlib.Path(__file__).parent.resolve()
/ "googleapiclient"
/ "discovery_cache"
/ "documents"
)
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 = pathlib.Path(__file__).parent.resolve() / "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 = []
sorted_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)
else:
# required params should be put straight into sorted_parameters
# to maintain order for positional args
sorted_parameters.append(pname)
for line in args:
m = re.search(r"^\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)
sorted_parameters.extend(sorted(parameters))
sorted_parameters = ", ".join(sorted_parameters)
else:
sorted_parameters = ""
return sorted_parameters
def method(name, doc):
"""Documents an individual method.
Args:
name: string, Name of the method.
doc: string, The methods docstring.
"""
import html
params = method_params(doc)
doc = html.escape(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="{}.html">{}</a>'.format(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, doc_destination_dir
):
html = document_collection(resource, path, root_discovery, discovery)
f = open(pathlib.Path(doc_destination_dir).joinpath(path + "html"), "w")
f.write(html)
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, {}),
doc_destination_dir,
)
def document_api(name, version, uri, doc_destination_dir):
"""Document the given API.
Args:
name (str): Name of the API.
version (str): Version of the API.
uri (str): URI of the API's discovery document
doc_destination_dir (str): relative path where the reference
documentation should be saved.
"""
http = build_http()
resp, content = http.request(
uri
or uritemplate.expand(
FLAGS.discovery_uri_template, {"api": name, "apiVersion": version}
)
)
if resp.status == 200:
discovery = json.loads(content)
service = build_from_document(discovery)
doc_name = "{}.{}.json".format(name, version)
discovery_file_path = DISCOVERY_DOC_DIR / doc_name
revision = None
pathlib.Path(discovery_file_path).touch(exist_ok=True)
# Write discovery artifact to disk if revision equal or newer
with open(discovery_file_path, "r+") as f:
try:
json_data = json.load(f)
revision = json_data["revision"]
except json.JSONDecodeError:
revision = None
if revision is None or discovery["revision"] >= revision:
# Reset position to the beginning
f.seek(0)
# Write the changes to disk
json.dump(discovery, f, indent=2, sort_keys=True)
# Truncate anything left as it's not needed
f.truncate()
elif resp.status == 404:
print(
"Warning: {} {} not found. HTTP Code: {}".format(name, version, resp.status)
)
return
else:
print(
"Warning: {} {} could not be built. HTTP Code: {}".format(
name, version, resp.status
)
)
return
document_collection_recursive(
service,
"{}_{}.".format(name, safe_version(version)),
discovery,
discovery,
doc_destination_dir,
)
def document_api_from_discovery_document(discovery_url, doc_destination_dir):
"""Document the given API.
Args:
discovery_url (str): URI of discovery document.
doc_destination_dir (str): relative path where the reference
documentation should be saved.
"""
http = build_http()
response, content = http.request(discovery_url)
discovery = json.loads(content)
service = build_from_document(discovery)
name = discovery["version"]
version = safe_version(discovery["version"])
document_collection_recursive(
service,
"{}_{}.".format(name, version),
discovery,
discovery,
doc_destination_dir,
)
def generate_all_api_documents(directory_uri=DIRECTORY_URI, doc_destination_dir=BASE):
"""Retrieve discovery artifacts and fetch reference documentations
for all apis listed in the public discovery directory.
args:
directory_uri (str): uri of the public discovery directory.
doc_destination_dir (str): relative path where the reference
documentation should be saved.
"""
api_directory = collections.defaultdict(list)
http = build_http()
resp, content = http.request(directory_uri)
if resp.status == 200:
directory = json.loads(content)["items"]
for api in directory:
document_api(
api["name"],
api["version"],
api["discoveryRestUrl"],
doc_destination_dir,
)
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 = collections.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, safe_version(version))
)
markdown.append("\n")
with open(BASE / "index.md", "w") as f:
markdown = "\n".join(markdown)
f.write(markdown)
else:
sys.exit("Failed to load the discovery document.")
if __name__ == "__main__":
FLAGS = parser.parse_args(sys.argv[1:])
if FLAGS.discovery_uri:
document_api_from_discovery_document(
discovery_url=FLAGS.discovery_uri, doc_destination_dir=FLAGS.dest
)
else:
generate_all_api_documents(
directory_uri=FLAGS.directory_uri, doc_destination_dir=FLAGS.dest
)