1#!/usr/bin/python 2# 3# Copyright 2014 Google Inc. All Rights Reserved. 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17"""Create documentation for generate API surfaces. 18 19Command-line tool that creates documentation for all APIs listed in discovery. 20The documentation is generated from a combination of the discovery document and 21the generated API surface itself. 22""" 23from __future__ import print_function 24 25__author__ = "jcgregorio@google.com (Joe Gregorio)" 26 27from collections import OrderedDict 28import argparse 29import collections 30import json 31import os 32import re 33import string 34import sys 35 36from googleapiclient.discovery import DISCOVERY_URI 37from googleapiclient.discovery import build 38from googleapiclient.discovery import build_from_document 39from googleapiclient.discovery import UnknownApiNameOrVersion 40from googleapiclient.http import build_http 41import uritemplate 42 43CSS = """<style> 44 45body, h1, h2, h3, div, span, p, pre, a { 46 margin: 0; 47 padding: 0; 48 border: 0; 49 font-weight: inherit; 50 font-style: inherit; 51 font-size: 100%; 52 font-family: inherit; 53 vertical-align: baseline; 54} 55 56body { 57 font-size: 13px; 58 padding: 1em; 59} 60 61h1 { 62 font-size: 26px; 63 margin-bottom: 1em; 64} 65 66h2 { 67 font-size: 24px; 68 margin-bottom: 1em; 69} 70 71h3 { 72 font-size: 20px; 73 margin-bottom: 1em; 74 margin-top: 1em; 75} 76 77pre, code { 78 line-height: 1.5; 79 font-family: Monaco, 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Lucida Console', monospace; 80} 81 82pre { 83 margin-top: 0.5em; 84} 85 86h1, h2, h3, p { 87 font-family: Arial, sans serif; 88} 89 90h1, h2, h3 { 91 border-bottom: solid #CCC 1px; 92} 93 94.toc_element { 95 margin-top: 0.5em; 96} 97 98.firstline { 99 margin-left: 2 em; 100} 101 102.method { 103 margin-top: 1em; 104 border: solid 1px #CCC; 105 padding: 1em; 106 background: #EEE; 107} 108 109.details { 110 font-weight: bold; 111 font-size: 14px; 112} 113 114</style> 115""" 116 117METHOD_TEMPLATE = """<div class="method"> 118 <code class="details" id="$name">$name($params)</code> 119 <pre>$doc</pre> 120</div> 121""" 122 123COLLECTION_LINK = """<p class="toc_element"> 124 <code><a href="$href">$name()</a></code> 125</p> 126<p class="firstline">Returns the $name Resource.</p> 127""" 128 129METHOD_LINK = """<p class="toc_element"> 130 <code><a href="#$name">$name($params)</a></code></p> 131<p class="firstline">$firstline</p>""" 132 133BASE = "docs/dyn" 134 135DIRECTORY_URI = "https://www.googleapis.com/discovery/v1/apis" 136 137parser = argparse.ArgumentParser(description=__doc__) 138 139parser.add_argument( 140 "--discovery_uri_template", 141 default=DISCOVERY_URI, 142 help="URI Template for discovery.", 143) 144 145parser.add_argument( 146 "--discovery_uri", 147 default="", 148 help=( 149 "URI of discovery document. If supplied then only " 150 "this API will be documented." 151 ), 152) 153 154parser.add_argument( 155 "--directory_uri", 156 default=DIRECTORY_URI, 157 help=("URI of directory document. Unused if --discovery_uri" " is supplied."), 158) 159 160parser.add_argument( 161 "--dest", default=BASE, help="Directory name to write documents into." 162) 163 164 165def safe_version(version): 166 """Create a safe version of the verion string. 167 168 Needed so that we can distinguish between versions 169 and sub-collections in URIs. I.e. we don't want 170 adsense_v1.1 to refer to the '1' collection in the v1 171 version of the adsense api. 172 173 Args: 174 version: string, The version string. 175 Returns: 176 The string with '.' replaced with '_'. 177 """ 178 179 return version.replace(".", "_") 180 181 182def unsafe_version(version): 183 """Undoes what safe_version() does. 184 185 See safe_version() for the details. 186 187 188 Args: 189 version: string, The safe version string. 190 Returns: 191 The string with '_' replaced with '.'. 192 """ 193 194 return version.replace("_", ".") 195 196 197def method_params(doc): 198 """Document the parameters of a method. 199 200 Args: 201 doc: string, The method's docstring. 202 203 Returns: 204 The method signature as a string. 205 """ 206 doclines = doc.splitlines() 207 if "Args:" in doclines: 208 begin = doclines.index("Args:") 209 if "Returns:" in doclines[begin + 1 :]: 210 end = doclines.index("Returns:", begin) 211 args = doclines[begin + 1 : end] 212 else: 213 args = doclines[begin + 1 :] 214 215 parameters = [] 216 pname = None 217 desc = "" 218 219 def add_param(pname, desc): 220 if pname is None: 221 return 222 if "(required)" not in desc: 223 pname = pname + "=None" 224 parameters.append(pname) 225 226 for line in args: 227 m = re.search("^\s+([a-zA-Z0-9_]+): (.*)", line) 228 if m is None: 229 desc += line 230 continue 231 add_param(pname, desc) 232 pname = m.group(1) 233 desc = m.group(2) 234 add_param(pname, desc) 235 parameters = ", ".join(parameters) 236 else: 237 parameters = "" 238 return parameters 239 240 241def method(name, doc): 242 """Documents an individual method. 243 244 Args: 245 name: string, Name of the method. 246 doc: string, The methods docstring. 247 """ 248 249 params = method_params(doc) 250 return string.Template(METHOD_TEMPLATE).substitute( 251 name=name, params=params, doc=doc 252 ) 253 254 255def breadcrumbs(path, root_discovery): 256 """Create the breadcrumb trail to this page of documentation. 257 258 Args: 259 path: string, Dot separated name of the resource. 260 root_discovery: Deserialized discovery document. 261 262 Returns: 263 HTML with links to each of the parent resources of this resource. 264 """ 265 parts = path.split(".") 266 267 crumbs = [] 268 accumulated = [] 269 270 for i, p in enumerate(parts): 271 prefix = ".".join(accumulated) 272 # The first time through prefix will be [], so we avoid adding in a 273 # superfluous '.' to prefix. 274 if prefix: 275 prefix += "." 276 display = p 277 if i == 0: 278 display = root_discovery.get("title", display) 279 crumbs.append('<a href="%s.html">%s</a>' % (prefix + p, display)) 280 accumulated.append(p) 281 282 return " . ".join(crumbs) 283 284 285def document_collection(resource, path, root_discovery, discovery, css=CSS): 286 """Document a single collection in an API. 287 288 Args: 289 resource: Collection or service being documented. 290 path: string, Dot separated name of the resource. 291 root_discovery: Deserialized discovery document. 292 discovery: Deserialized discovery document, but just the portion that 293 describes the resource. 294 css: string, The CSS to include in the generated file. 295 """ 296 collections = [] 297 methods = [] 298 resource_name = path.split(".")[-2] 299 html = [ 300 "<html><body>", 301 css, 302 "<h1>%s</h1>" % breadcrumbs(path[:-1], root_discovery), 303 "<h2>Instance Methods</h2>", 304 ] 305 306 # Which methods are for collections. 307 for name in dir(resource): 308 if not name.startswith("_") and callable(getattr(resource, name)): 309 if hasattr(getattr(resource, name), "__is_resource__"): 310 collections.append(name) 311 else: 312 methods.append(name) 313 314 # TOC 315 if collections: 316 for name in collections: 317 if not name.startswith("_") and callable(getattr(resource, name)): 318 href = path + name + ".html" 319 html.append( 320 string.Template(COLLECTION_LINK).substitute(href=href, name=name) 321 ) 322 323 if methods: 324 for name in methods: 325 if not name.startswith("_") and callable(getattr(resource, name)): 326 doc = getattr(resource, name).__doc__ 327 params = method_params(doc) 328 firstline = doc.splitlines()[0] 329 html.append( 330 string.Template(METHOD_LINK).substitute( 331 name=name, params=params, firstline=firstline 332 ) 333 ) 334 335 if methods: 336 html.append("<h3>Method Details</h3>") 337 for name in methods: 338 dname = name.rsplit("_")[0] 339 html.append(method(name, getattr(resource, name).__doc__)) 340 341 html.append("</body></html>") 342 343 return "\n".join(html) 344 345 346def document_collection_recursive(resource, path, root_discovery, discovery): 347 348 html = document_collection(resource, path, root_discovery, discovery) 349 350 f = open(os.path.join(FLAGS.dest, path + "html"), "w") 351 f.write(html.encode("utf-8")) 352 f.close() 353 354 for name in dir(resource): 355 if ( 356 not name.startswith("_") 357 and callable(getattr(resource, name)) 358 and hasattr(getattr(resource, name), "__is_resource__") 359 and discovery != {} 360 ): 361 dname = name.rsplit("_")[0] 362 collection = getattr(resource, name)() 363 document_collection_recursive( 364 collection, 365 path + name + ".", 366 root_discovery, 367 discovery["resources"].get(dname, {}), 368 ) 369 370 371def document_api(name, version): 372 """Document the given API. 373 374 Args: 375 name: string, Name of the API. 376 version: string, Version of the API. 377 """ 378 try: 379 service = build(name, version) 380 except UnknownApiNameOrVersion as e: 381 print("Warning: {} {} found but could not be built.".format(name, version)) 382 return 383 384 http = build_http() 385 response, content = http.request( 386 uritemplate.expand( 387 FLAGS.discovery_uri_template, {"api": name, "apiVersion": version} 388 ) 389 ) 390 discovery = json.loads(content) 391 392 version = safe_version(version) 393 394 document_collection_recursive( 395 service, "%s_%s." % (name, version), discovery, discovery 396 ) 397 398 399def document_api_from_discovery_document(uri): 400 """Document the given API. 401 402 Args: 403 uri: string, URI of discovery document. 404 """ 405 http = build_http() 406 response, content = http.request(FLAGS.discovery_uri) 407 discovery = json.loads(content) 408 409 service = build_from_document(discovery) 410 411 name = discovery["version"] 412 version = safe_version(discovery["version"]) 413 414 document_collection_recursive( 415 service, "%s_%s." % (name, version), discovery, discovery 416 ) 417 418 419if __name__ == "__main__": 420 FLAGS = parser.parse_args(sys.argv[1:]) 421 if FLAGS.discovery_uri: 422 document_api_from_discovery_document(FLAGS.discovery_uri) 423 else: 424 api_directory = collections.defaultdict(list) 425 http = build_http() 426 resp, content = http.request( 427 FLAGS.directory_uri, headers={"X-User-IP": "0.0.0.0"} 428 ) 429 if resp.status == 200: 430 directory = json.loads(content)["items"] 431 for api in directory: 432 document_api(api["name"], api["version"]) 433 api_directory[api["name"]].append(api["version"]) 434 435 # sort by api name and version number 436 for api in api_directory: 437 api_directory[api] = sorted(api_directory[api]) 438 api_directory = OrderedDict( 439 sorted(api_directory.items(), key=lambda x: x[0]) 440 ) 441 442 markdown = [] 443 for api, versions in api_directory.items(): 444 markdown.append("## %s" % api) 445 for version in versions: 446 markdown.append( 447 "* [%s](http://googleapis.github.io/google-api-python-client/docs/dyn/%s_%s.html)" 448 % (version, api, version) 449 ) 450 markdown.append("\n") 451 452 with open("docs/dyn/index.md", "w") as f: 453 f.write("\n".join(markdown).encode("utf-8")) 454 455 else: 456 sys.exit("Failed to load the discovery document.") 457