blob: e3881aec4e916103ddc6a119361d76f707de2a1e [file] [log] [blame]
Joe Gregorio79daca02013-03-29 16:25:52 -04001#!/usr/bin/python
Joe Gregorio20a5aa92011-04-01 17:44:25 -04002#
Craig Citro751b7fb2014-09-23 11:20:38 -07003# Copyright 2014 Google Inc. All Rights Reserved.
Joe Gregorio20a5aa92011-04-01 17:44:25 -04004#
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
Joe Gregorio81d92cc2012-07-09 16:46:02 -040017"""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
Joe Gregorio20a5aa92011-04-01 17:44:25 -040024__author__ = 'jcgregorio@google.com (Joe Gregorio)'
25
Joe Gregorio79daca02013-03-29 16:25:52 -040026import argparse
Craig Citro6ae34d72014-08-18 23:10:09 -070027import json
Joe Gregorioafc45f22011-02-20 16:11:28 -050028import os
Joe Gregorioafc45f22011-02-20 16:11:28 -050029import re
Joe Gregorio79daca02013-03-29 16:25:52 -040030import string
Joe Gregorio20a5aa92011-04-01 17:44:25 -040031import sys
Joe Gregorioafc45f22011-02-20 16:11:28 -050032
John Asmuth864311d2014-04-24 15:46:08 -040033from googleapiclient.discovery import DISCOVERY_URI
34from googleapiclient.discovery import build
35from googleapiclient.discovery import build_from_document
Jon Wayne Parrottfd2f99c2016-02-19 16:02:04 -080036from googleapiclient.discovery import UnknownApiNameOrVersion
Igor Maravić22435292017-01-19 22:28:22 +010037from googleapiclient.http import build_http
Joe Gregorio81d92cc2012-07-09 16:46:02 -040038import uritemplate
39
Joe Gregorio81d92cc2012-07-09 16:46:02 -040040CSS = """<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
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400114METHOD_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
Joe Gregoriobb964352013-03-03 20:45:29 -0500130BASE = 'docs/dyn'
131
Sai Cheemalapatidf613972016-10-21 13:59:49 -0700132DIRECTORY_URI = 'https://www.googleapis.com/discovery/v1/apis'
Joe Gregoriobb964352013-03-03 20:45:29 -0500133
Joe Gregorio79daca02013-03-29 16:25:52 -0400134parser = argparse.ArgumentParser(description=__doc__)
Joe Gregoriobb964352013-03-03 20:45:29 -0500135
Joe Gregorio79daca02013-03-29 16:25:52 -0400136parser.add_argument('--discovery_uri_template', default=DISCOVERY_URI,
137 help='URI Template for discovery.')
Joe Gregoriobb964352013-03-03 20:45:29 -0500138
Joe Gregorio79daca02013-03-29 16:25:52 -0400139parser.add_argument('--discovery_uri', default='',
140 help=('URI of discovery document. If supplied then only '
141 'this API will be documented.'))
Joe Gregoriobb964352013-03-03 20:45:29 -0500142
Joe Gregorio79daca02013-03-29 16:25:52 -0400143parser.add_argument('--directory_uri', default=DIRECTORY_URI,
144 help=('URI of directory document. Unused if --discovery_uri'
145 ' is supplied.'))
Joe Gregoriobb964352013-03-03 20:45:29 -0500146
Joe Gregorio79daca02013-03-29 16:25:52 -0400147parser.add_argument('--dest', default=BASE,
148 help='Directory name to write documents into.')
149
Joe Gregoriobb964352013-03-03 20:45:29 -0500150
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400151
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 for line in args:
204 m = re.search('^\s+([a-zA-Z0-9_]+): (.*)', line)
205 if m is None:
206 continue
207 pname = m.group(1)
208 desc = m.group(2)
209 if '(required)' not in desc:
210 pname = pname + '=None'
211 parameters.append(pname)
212 parameters = ', '.join(parameters)
213 else:
214 parameters = ''
215 return parameters
216
217
218def method(name, doc):
219 """Documents an individual method.
220
221 Args:
222 name: string, Name of the method.
223 doc: string, The methods docstring.
224 """
225
226 params = method_params(doc)
Joe Gregorio79daca02013-03-29 16:25:52 -0400227 return string.Template(METHOD_TEMPLATE).substitute(
228 name=name, params=params, doc=doc)
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400229
230
231def breadcrumbs(path, root_discovery):
232 """Create the breadcrumb trail to this page of documentation.
233
234 Args:
235 path: string, Dot separated name of the resource.
236 root_discovery: Deserialized discovery document.
237
238 Returns:
239 HTML with links to each of the parent resources of this resource.
240 """
241 parts = path.split('.')
242
243 crumbs = []
244 accumulated = []
245
246 for i, p in enumerate(parts):
247 prefix = '.'.join(accumulated)
248 # The first time through prefix will be [], so we avoid adding in a
249 # superfluous '.' to prefix.
250 if prefix:
251 prefix += '.'
252 display = p
253 if i == 0:
254 display = root_discovery.get('title', display)
255 crumbs.append('<a href="%s.html">%s</a>' % (prefix + p, display))
256 accumulated.append(p)
257
258 return ' . '.join(crumbs)
259
260
261def document_collection(resource, path, root_discovery, discovery, css=CSS):
262 """Document a single collection in an API.
263
264 Args:
265 resource: Collection or service being documented.
266 path: string, Dot separated name of the resource.
267 root_discovery: Deserialized discovery document.
268 discovery: Deserialized discovery document, but just the portion that
269 describes the resource.
270 css: string, The CSS to include in the generated file.
271 """
Joe Gregorioafc45f22011-02-20 16:11:28 -0500272 collections = []
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400273 methods = []
274 resource_name = path.split('.')[-2]
275 html = [
276 '<html><body>',
277 css,
278 '<h1>%s</h1>' % breadcrumbs(path[:-1], root_discovery),
279 '<h2>Instance Methods</h2>'
280 ]
281
282 # Which methods are for collections.
Joe Gregorioafc45f22011-02-20 16:11:28 -0500283 for name in dir(resource):
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400284 if not name.startswith('_') and callable(getattr(resource, name)):
285 if hasattr(getattr(resource, name), '__is_resource__'):
286 collections.append(name)
287 else:
288 methods.append(name)
Joe Gregorioafc45f22011-02-20 16:11:28 -0500289
Joe Gregorioafc45f22011-02-20 16:11:28 -0500290
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400291 # TOC
292 if collections:
293 for name in collections:
294 if not name.startswith('_') and callable(getattr(resource, name)):
295 href = path + name + '.html'
Joe Gregorio79daca02013-03-29 16:25:52 -0400296 html.append(string.Template(COLLECTION_LINK).substitute(
297 href=href, name=name))
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400298
299 if methods:
300 for name in methods:
301 if not name.startswith('_') and callable(getattr(resource, name)):
302 doc = getattr(resource, name).__doc__
303 params = method_params(doc)
304 firstline = doc.splitlines()[0]
Joe Gregorio79daca02013-03-29 16:25:52 -0400305 html.append(string.Template(METHOD_LINK).substitute(
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400306 name=name, params=params, firstline=firstline))
307
308 if methods:
309 html.append('<h3>Method Details</h3>')
310 for name in methods:
311 dname = name.rsplit('_')[0]
312 html.append(method(name, getattr(resource, name).__doc__))
313
314 html.append('</body></html>')
315
316 return '\n'.join(html)
317
318
319def document_collection_recursive(resource, path, root_discovery, discovery):
320
321 html = document_collection(resource, path, root_discovery, discovery)
Joe Gregorioafc45f22011-02-20 16:11:28 -0500322
Joe Gregoriobb964352013-03-03 20:45:29 -0500323 f = open(os.path.join(FLAGS.dest, path + 'html'), 'w')
Joe Gregoriod67010d2012-11-05 08:57:06 -0500324 f.write(html.encode('utf-8'))
Joe Gregorioafc45f22011-02-20 16:11:28 -0500325 f.close()
326
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400327 for name in dir(resource):
328 if (not name.startswith('_')
329 and callable(getattr(resource, name))
330 and hasattr(getattr(resource, name), '__is_resource__')):
331 dname = name.rsplit('_')[0]
332 collection = getattr(resource, name)()
333 document_collection_recursive(collection, path + name + '.', root_discovery,
334 discovery['resources'].get(dname, {}))
335
Joe Gregorioafc45f22011-02-20 16:11:28 -0500336def document_api(name, version):
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400337 """Document the given API.
338
339 Args:
340 name: string, Name of the API.
341 version: string, Version of the API.
342 """
Jon Wayne Parrottfd2f99c2016-02-19 16:02:04 -0800343 try:
344 service = build(name, version)
345 except UnknownApiNameOrVersion as e:
346 print 'Warning: {} {} found but could not be built.'.format(name, version)
347 return
348
Igor Maravić22435292017-01-19 22:28:22 +0100349 http = build_http()
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400350 response, content = http.request(
351 uritemplate.expand(
Joe Gregoriobb964352013-03-03 20:45:29 -0500352 FLAGS.discovery_uri_template, {
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400353 'api': name,
354 'apiVersion': version})
355 )
Craig Citro6ae34d72014-08-18 23:10:09 -0700356 discovery = json.loads(content)
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400357
358 version = safe_version(version)
359
360 document_collection_recursive(
361 service, '%s_%s.' % (name, version), discovery, discovery)
362
Joe Gregorioafc45f22011-02-20 16:11:28 -0500363
Joe Gregoriobb964352013-03-03 20:45:29 -0500364def document_api_from_discovery_document(uri):
365 """Document the given API.
366
367 Args:
368 uri: string, URI of discovery document.
369 """
Igor Maravić22435292017-01-19 22:28:22 +0100370 http = build_http()
Joe Gregoriobb964352013-03-03 20:45:29 -0500371 response, content = http.request(FLAGS.discovery_uri)
Craig Citro6ae34d72014-08-18 23:10:09 -0700372 discovery = json.loads(content)
Joe Gregoriobb964352013-03-03 20:45:29 -0500373
374 service = build_from_document(discovery)
375
376 name = discovery['version']
377 version = safe_version(discovery['version'])
378
379 document_collection_recursive(
380 service, '%s_%s.' % (name, version), discovery, discovery)
381
382
383if __name__ == '__main__':
Joe Gregorio79daca02013-03-29 16:25:52 -0400384 FLAGS = parser.parse_args(sys.argv[1:])
Joe Gregoriobb964352013-03-03 20:45:29 -0500385 if FLAGS.discovery_uri:
386 document_api_from_discovery_document(FLAGS.discovery_uri)
Joe Gregorio20a5aa92011-04-01 17:44:25 -0400387 else:
Igor Maravić22435292017-01-19 22:28:22 +0100388 http = build_http()
Joe Gregoriobb964352013-03-03 20:45:29 -0500389 resp, content = http.request(
390 FLAGS.directory_uri,
391 headers={'X-User-IP': '0.0.0.0'})
392 if resp.status == 200:
Craig Citro6ae34d72014-08-18 23:10:09 -0700393 directory = json.loads(content)['items']
Joe Gregoriobb964352013-03-03 20:45:29 -0500394 for api in directory:
395 document_api(api['name'], api['version'])
396 else:
397 sys.exit("Failed to load the discovery document.")