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