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('--discovery_uri_template', default=DISCOVERY_URI, 140 help='URI Template for discovery.') 141 142parser.add_argument('--discovery_uri', default='', 143 help=('URI of discovery document. If supplied then only ' 144 'this API will be documented.')) 145 146parser.add_argument('--directory_uri', default=DIRECTORY_URI, 147 help=('URI of directory document. Unused if --discovery_uri' 148 ' is supplied.')) 149 150parser.add_argument('--dest', default=BASE, 151 help='Directory name to write documents into.') 152 153 154 155def safe_version(version): 156 """Create a safe version of the verion string. 157 158 Needed so that we can distinguish between versions 159 and sub-collections in URIs. I.e. we don't want 160 adsense_v1.1 to refer to the '1' collection in the v1 161 version of the adsense api. 162 163 Args: 164 version: string, The version string. 165 Returns: 166 The string with '.' replaced with '_'. 167 """ 168 169 return version.replace('.', '_') 170 171 172def unsafe_version(version): 173 """Undoes what safe_version() does. 174 175 See safe_version() for the details. 176 177 178 Args: 179 version: string, The safe version string. 180 Returns: 181 The string with '_' replaced with '.'. 182 """ 183 184 return version.replace('_', '.') 185 186 187def method_params(doc): 188 """Document the parameters of a method. 189 190 Args: 191 doc: string, The method's docstring. 192 193 Returns: 194 The method signature as a string. 195 """ 196 doclines = doc.splitlines() 197 if 'Args:' in doclines: 198 begin = doclines.index('Args:') 199 if 'Returns:' in doclines[begin+1:]: 200 end = doclines.index('Returns:', begin) 201 args = doclines[begin+1: end] 202 else: 203 args = doclines[begin+1:] 204 205 parameters = [] 206 pname = None 207 desc = '' 208 def add_param(pname, desc): 209 if pname is None: 210 return 211 if '(required)' not in desc: 212 pname = pname + '=None' 213 parameters.append(pname) 214 for line in args: 215 m = re.search('^\s+([a-zA-Z0-9_]+): (.*)', line) 216 if m is None: 217 desc += line 218 continue 219 add_param(pname, desc) 220 pname = m.group(1) 221 desc = m.group(2) 222 add_param(pname, desc) 223 parameters = ', '.join(parameters) 224 else: 225 parameters = '' 226 return parameters 227 228 229def method(name, doc): 230 """Documents an individual method. 231 232 Args: 233 name: string, Name of the method. 234 doc: string, The methods docstring. 235 """ 236 237 params = method_params(doc) 238 return string.Template(METHOD_TEMPLATE).substitute( 239 name=name, params=params, doc=doc) 240 241 242def breadcrumbs(path, root_discovery): 243 """Create the breadcrumb trail to this page of documentation. 244 245 Args: 246 path: string, Dot separated name of the resource. 247 root_discovery: Deserialized discovery document. 248 249 Returns: 250 HTML with links to each of the parent resources of this resource. 251 """ 252 parts = path.split('.') 253 254 crumbs = [] 255 accumulated = [] 256 257 for i, p in enumerate(parts): 258 prefix = '.'.join(accumulated) 259 # The first time through prefix will be [], so we avoid adding in a 260 # superfluous '.' to prefix. 261 if prefix: 262 prefix += '.' 263 display = p 264 if i == 0: 265 display = root_discovery.get('title', display) 266 crumbs.append('<a href="%s.html">%s</a>' % (prefix + p, display)) 267 accumulated.append(p) 268 269 return ' . '.join(crumbs) 270 271 272def document_collection(resource, path, root_discovery, discovery, css=CSS): 273 """Document a single collection in an API. 274 275 Args: 276 resource: Collection or service being documented. 277 path: string, Dot separated name of the resource. 278 root_discovery: Deserialized discovery document. 279 discovery: Deserialized discovery document, but just the portion that 280 describes the resource. 281 css: string, The CSS to include in the generated file. 282 """ 283 collections = [] 284 methods = [] 285 resource_name = path.split('.')[-2] 286 html = [ 287 '<html><body>', 288 css, 289 '<h1>%s</h1>' % breadcrumbs(path[:-1], root_discovery), 290 '<h2>Instance Methods</h2>' 291 ] 292 293 # Which methods are for collections. 294 for name in dir(resource): 295 if not name.startswith('_') and callable(getattr(resource, name)): 296 if hasattr(getattr(resource, name), '__is_resource__'): 297 collections.append(name) 298 else: 299 methods.append(name) 300 301 302 # TOC 303 if collections: 304 for name in collections: 305 if not name.startswith('_') and callable(getattr(resource, name)): 306 href = path + name + '.html' 307 html.append(string.Template(COLLECTION_LINK).substitute( 308 href=href, name=name)) 309 310 if methods: 311 for name in methods: 312 if not name.startswith('_') and callable(getattr(resource, name)): 313 doc = getattr(resource, name).__doc__ 314 params = method_params(doc) 315 firstline = doc.splitlines()[0] 316 html.append(string.Template(METHOD_LINK).substitute( 317 name=name, params=params, firstline=firstline)) 318 319 if methods: 320 html.append('<h3>Method Details</h3>') 321 for name in methods: 322 dname = name.rsplit('_')[0] 323 html.append(method(name, getattr(resource, name).__doc__)) 324 325 html.append('</body></html>') 326 327 return '\n'.join(html) 328 329 330def document_collection_recursive(resource, path, root_discovery, discovery): 331 332 html = document_collection(resource, path, root_discovery, discovery) 333 334 f = open(os.path.join(FLAGS.dest, path + 'html'), 'w') 335 f.write(html.encode('utf-8')) 336 f.close() 337 338 for name in dir(resource): 339 if (not name.startswith('_') 340 and callable(getattr(resource, name)) 341 and hasattr(getattr(resource, name), '__is_resource__') 342 and discovery != {}): 343 dname = name.rsplit('_')[0] 344 collection = getattr(resource, name)() 345 document_collection_recursive(collection, path + name + '.', root_discovery, 346 discovery['resources'].get(dname, {})) 347 348def document_api(name, version): 349 """Document the given API. 350 351 Args: 352 name: string, Name of the API. 353 version: string, Version of the API. 354 """ 355 try: 356 service = build(name, version) 357 except UnknownApiNameOrVersion as e: 358 print('Warning: {} {} found but could not be built.'.format(name, version)) 359 return 360 361 http = build_http() 362 response, content = http.request( 363 uritemplate.expand( 364 FLAGS.discovery_uri_template, { 365 'api': name, 366 'apiVersion': version}) 367 ) 368 discovery = json.loads(content) 369 370 version = safe_version(version) 371 372 document_collection_recursive( 373 service, '%s_%s.' % (name, version), discovery, discovery) 374 375 376def document_api_from_discovery_document(uri): 377 """Document the given API. 378 379 Args: 380 uri: string, URI of discovery document. 381 """ 382 http = build_http() 383 response, content = http.request(FLAGS.discovery_uri) 384 discovery = json.loads(content) 385 386 service = build_from_document(discovery) 387 388 name = discovery['version'] 389 version = safe_version(discovery['version']) 390 391 document_collection_recursive( 392 service, '%s_%s.' % (name, version), discovery, discovery) 393 394 395if __name__ == '__main__': 396 FLAGS = parser.parse_args(sys.argv[1:]) 397 if FLAGS.discovery_uri: 398 document_api_from_discovery_document(FLAGS.discovery_uri) 399 else: 400 api_directory = collections.defaultdict(list) 401 http = build_http() 402 resp, content = http.request( 403 FLAGS.directory_uri, 404 headers={'X-User-IP': '0.0.0.0'}) 405 if resp.status == 200: 406 directory = json.loads(content)['items'] 407 for api in directory: 408 document_api(api['name'], api['version']) 409 api_directory[api['name']].append(api['version']) 410 411 # sort by api name and version number 412 for api in api_directory: 413 api_directory[api] = sorted(api_directory[api]) 414 api_directory = OrderedDict(sorted(api_directory.items(), key = lambda x: x[0])) 415 416 markdown = [] 417 for api, versions in api_directory.items(): 418 markdown.append('## %s' % api) 419 for version in versions: 420 markdown.append('* [%s](http://googleapis.github.io/google-api-python-client/docs/dyn/%s_%s.html)' % (version, api, version)) 421 markdown.append('\n') 422 423 with open('docs/dyn/index.md', 'w') as f: 424 f.write('\n'.join(markdown).encode('utf-8')) 425 426 else: 427 sys.exit("Failed to load the discovery document.") 428