#!/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 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.discovery import UnknownApiNameOrVersion from googleapiclient.http import build_http from googleapiclient.errors import HttpError import uritemplate DISCOVERY_DOC_DIR = ( pathlib.Path(__file__).parent.resolve() / "googleapiclient" / "discovery_cache" / "documents" ) CSS = """ """ METHOD_TEMPLATE = """
$name($params)
$doc
""" COLLECTION_LINK = """

$name()

Returns the $name Resource.

""" METHOD_LINK = """

$name($params)

$firstline

""" 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('{}'.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 = [ "", 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, 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) version = safe_version(version) doc_name = "{}.{}.json".format(name, version.replace("_", "")) 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, 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 = 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)