blob: 3aaaa50df88e05a2fdec9ae71028d5bf98ce31a3 [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 = []
Thomas Coffee2f245372017-03-27 10:39:26 -0700203 pname = None
204 desc = ''
205 def add_param(pname, desc):
206 if pname is None:
207 return
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400208 if '(required)' not in desc:
209 pname = pname + '=None'
210 parameters.append(pname)
Thomas Coffee2f245372017-03-27 10:39:26 -0700211 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)
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400220 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)
Joe Gregorio79daca02013-03-29 16:25:52 -0400235 return string.Template(METHOD_TEMPLATE).substitute(
236 name=name, params=params, doc=doc)
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400237
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 """
Joe Gregorioafc45f22011-02-20 16:11:28 -0500280 collections = []
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400281 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.
Joe Gregorioafc45f22011-02-20 16:11:28 -0500291 for name in dir(resource):
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400292 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)
Joe Gregorioafc45f22011-02-20 16:11:28 -0500297
Joe Gregorioafc45f22011-02-20 16:11:28 -0500298
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400299 # TOC
300 if collections:
301 for name in collections:
302 if not name.startswith('_') and callable(getattr(resource, name)):
303 href = path + name + '.html'
Joe Gregorio79daca02013-03-29 16:25:52 -0400304 html.append(string.Template(COLLECTION_LINK).substitute(
305 href=href, name=name))
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400306
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]
Joe Gregorio79daca02013-03-29 16:25:52 -0400313 html.append(string.Template(METHOD_LINK).substitute(
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400314 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)
Joe Gregorioafc45f22011-02-20 16:11:28 -0500330
Joe Gregoriobb964352013-03-03 20:45:29 -0500331 f = open(os.path.join(FLAGS.dest, path + 'html'), 'w')
Joe Gregoriod67010d2012-11-05 08:57:06 -0500332 f.write(html.encode('utf-8'))
Joe Gregorioafc45f22011-02-20 16:11:28 -0500333 f.close()
334
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400335 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
Joe Gregorioafc45f22011-02-20 16:11:28 -0500344def document_api(name, version):
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400345 """Document the given API.
346
347 Args:
348 name: string, Name of the API.
349 version: string, Version of the API.
350 """
Jon Wayne Parrottfd2f99c2016-02-19 16:02:04 -0800351 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
Igor Maravić22435292017-01-19 22:28:22 +0100357 http = build_http()
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400358 response, content = http.request(
359 uritemplate.expand(
Joe Gregoriobb964352013-03-03 20:45:29 -0500360 FLAGS.discovery_uri_template, {
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400361 'api': name,
362 'apiVersion': version})
363 )
Craig Citro6ae34d72014-08-18 23:10:09 -0700364 discovery = json.loads(content)
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400365
366 version = safe_version(version)
367
368 document_collection_recursive(
369 service, '%s_%s.' % (name, version), discovery, discovery)
370
Joe Gregorioafc45f22011-02-20 16:11:28 -0500371
Joe Gregoriobb964352013-03-03 20:45:29 -0500372def document_api_from_discovery_document(uri):
373 """Document the given API.
374
375 Args:
376 uri: string, URI of discovery document.
377 """
Igor Maravić22435292017-01-19 22:28:22 +0100378 http = build_http()
Joe Gregoriobb964352013-03-03 20:45:29 -0500379 response, content = http.request(FLAGS.discovery_uri)
Craig Citro6ae34d72014-08-18 23:10:09 -0700380 discovery = json.loads(content)
Joe Gregoriobb964352013-03-03 20:45:29 -0500381
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__':
Joe Gregorio79daca02013-03-29 16:25:52 -0400392 FLAGS = parser.parse_args(sys.argv[1:])
Joe Gregoriobb964352013-03-03 20:45:29 -0500393 if FLAGS.discovery_uri:
394 document_api_from_discovery_document(FLAGS.discovery_uri)
Joe Gregorio20a5aa92011-04-01 17:44:25 -0400395 else:
Igor Maravić22435292017-01-19 22:28:22 +0100396 http = build_http()
Joe Gregoriobb964352013-03-03 20:45:29 -0500397 resp, content = http.request(
398 FLAGS.directory_uri,
399 headers={'X-User-IP': '0.0.0.0'})
400 if resp.status == 200:
Craig Citro6ae34d72014-08-18 23:10:09 -0700401 directory = json.loads(content)['items']
Joe Gregoriobb964352013-03-03 20:45:29 -0500402 for api in directory:
403 document_api(api['name'], api['version'])
404 else:
405 sys.exit("Failed to load the discovery document.")