blob: 1a7a24b6d6ff18e9788d57648452b32599915df0 [file] [log] [blame]
Joe Gregorio79daca02013-03-29 16:25:52 -04001#!/usr/bin/python
Joe Gregorio20a5aa92011-04-01 17:44:25 -04002#
Joe Gregorio81d92cc2012-07-09 16:46:02 -04003# Copyright 2012 Google Inc.
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
Joe Gregorio79daca02013-03-29 16:25:52 -040036import httplib2
Joe Gregorio81d92cc2012-07-09 16:46:02 -040037import uritemplate
38
Joe Gregorio81d92cc2012-07-09 16:46:02 -040039CSS = """<style>
40
41body, h1, h2, h3, div, span, p, pre, a {
42 margin: 0;
43 padding: 0;
44 border: 0;
45 font-weight: inherit;
46 font-style: inherit;
47 font-size: 100%;
48 font-family: inherit;
49 vertical-align: baseline;
50}
51
52body {
53 font-size: 13px;
54 padding: 1em;
55}
56
57h1 {
58 font-size: 26px;
59 margin-bottom: 1em;
60}
61
62h2 {
63 font-size: 24px;
64 margin-bottom: 1em;
65}
66
67h3 {
68 font-size: 20px;
69 margin-bottom: 1em;
70 margin-top: 1em;
71}
72
73pre, code {
74 line-height: 1.5;
75 font-family: Monaco, 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Lucida Console', monospace;
76}
77
78pre {
79 margin-top: 0.5em;
80}
81
82h1, h2, h3, p {
83 font-family: Arial, sans serif;
84}
85
86h1, h2, h3 {
87 border-bottom: solid #CCC 1px;
88}
89
90.toc_element {
91 margin-top: 0.5em;
92}
93
94.firstline {
95 margin-left: 2 em;
96}
97
98.method {
99 margin-top: 1em;
100 border: solid 1px #CCC;
101 padding: 1em;
102 background: #EEE;
103}
104
105.details {
106 font-weight: bold;
107 font-size: 14px;
108}
109
110</style>
111"""
112
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400113METHOD_TEMPLATE = """<div class="method">
114 <code class="details" id="$name">$name($params)</code>
115 <pre>$doc</pre>
116</div>
117"""
118
119COLLECTION_LINK = """<p class="toc_element">
120 <code><a href="$href">$name()</a></code>
121</p>
122<p class="firstline">Returns the $name Resource.</p>
123"""
124
125METHOD_LINK = """<p class="toc_element">
126 <code><a href="#$name">$name($params)</a></code></p>
127<p class="firstline">$firstline</p>"""
128
Joe Gregoriobb964352013-03-03 20:45:29 -0500129BASE = 'docs/dyn'
130
Joe Gregorio41be8e82013-03-07 10:31:47 -0500131DIRECTORY_URI = 'https://www.googleapis.com/discovery/v1/apis?preferred=true'
Joe Gregoriobb964352013-03-03 20:45:29 -0500132
Joe Gregorio79daca02013-03-29 16:25:52 -0400133parser = argparse.ArgumentParser(description=__doc__)
Joe Gregoriobb964352013-03-03 20:45:29 -0500134
Joe Gregorio79daca02013-03-29 16:25:52 -0400135parser.add_argument('--discovery_uri_template', default=DISCOVERY_URI,
136 help='URI Template for discovery.')
Joe Gregoriobb964352013-03-03 20:45:29 -0500137
Joe Gregorio79daca02013-03-29 16:25:52 -0400138parser.add_argument('--discovery_uri', default='',
139 help=('URI of discovery document. If supplied then only '
140 'this API will be documented.'))
Joe Gregoriobb964352013-03-03 20:45:29 -0500141
Joe Gregorio79daca02013-03-29 16:25:52 -0400142parser.add_argument('--directory_uri', default=DIRECTORY_URI,
143 help=('URI of directory document. Unused if --discovery_uri'
144 ' is supplied.'))
Joe Gregoriobb964352013-03-03 20:45:29 -0500145
Joe Gregorio79daca02013-03-29 16:25:52 -0400146parser.add_argument('--dest', default=BASE,
147 help='Directory name to write documents into.')
148
Joe Gregoriobb964352013-03-03 20:45:29 -0500149
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400150
151def safe_version(version):
152 """Create a safe version of the verion string.
153
154 Needed so that we can distinguish between versions
155 and sub-collections in URIs. I.e. we don't want
156 adsense_v1.1 to refer to the '1' collection in the v1
157 version of the adsense api.
158
159 Args:
160 version: string, The version string.
161 Returns:
162 The string with '.' replaced with '_'.
163 """
164
165 return version.replace('.', '_')
166
167
168def unsafe_version(version):
169 """Undoes what safe_version() does.
170
171 See safe_version() for the details.
172
173
174 Args:
175 version: string, The safe version string.
176 Returns:
177 The string with '_' replaced with '.'.
178 """
179
180 return version.replace('_', '.')
181
182
183def method_params(doc):
184 """Document the parameters of a method.
185
186 Args:
187 doc: string, The method's docstring.
188
189 Returns:
190 The method signature as a string.
191 """
192 doclines = doc.splitlines()
193 if 'Args:' in doclines:
194 begin = doclines.index('Args:')
195 if 'Returns:' in doclines[begin+1:]:
196 end = doclines.index('Returns:', begin)
197 args = doclines[begin+1: end]
198 else:
199 args = doclines[begin+1:]
200
201 parameters = []
202 for line in args:
203 m = re.search('^\s+([a-zA-Z0-9_]+): (.*)', line)
204 if m is None:
205 continue
206 pname = m.group(1)
207 desc = m.group(2)
208 if '(required)' not in desc:
209 pname = pname + '=None'
210 parameters.append(pname)
211 parameters = ', '.join(parameters)
212 else:
213 parameters = ''
214 return parameters
215
216
217def method(name, doc):
218 """Documents an individual method.
219
220 Args:
221 name: string, Name of the method.
222 doc: string, The methods docstring.
223 """
224
225 params = method_params(doc)
Joe Gregorio79daca02013-03-29 16:25:52 -0400226 return string.Template(METHOD_TEMPLATE).substitute(
227 name=name, params=params, doc=doc)
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400228
229
230def breadcrumbs(path, root_discovery):
231 """Create the breadcrumb trail to this page of documentation.
232
233 Args:
234 path: string, Dot separated name of the resource.
235 root_discovery: Deserialized discovery document.
236
237 Returns:
238 HTML with links to each of the parent resources of this resource.
239 """
240 parts = path.split('.')
241
242 crumbs = []
243 accumulated = []
244
245 for i, p in enumerate(parts):
246 prefix = '.'.join(accumulated)
247 # The first time through prefix will be [], so we avoid adding in a
248 # superfluous '.' to prefix.
249 if prefix:
250 prefix += '.'
251 display = p
252 if i == 0:
253 display = root_discovery.get('title', display)
254 crumbs.append('<a href="%s.html">%s</a>' % (prefix + p, display))
255 accumulated.append(p)
256
257 return ' . '.join(crumbs)
258
259
260def document_collection(resource, path, root_discovery, discovery, css=CSS):
261 """Document a single collection in an API.
262
263 Args:
264 resource: Collection or service being documented.
265 path: string, Dot separated name of the resource.
266 root_discovery: Deserialized discovery document.
267 discovery: Deserialized discovery document, but just the portion that
268 describes the resource.
269 css: string, The CSS to include in the generated file.
270 """
Joe Gregorioafc45f22011-02-20 16:11:28 -0500271 collections = []
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400272 methods = []
273 resource_name = path.split('.')[-2]
274 html = [
275 '<html><body>',
276 css,
277 '<h1>%s</h1>' % breadcrumbs(path[:-1], root_discovery),
278 '<h2>Instance Methods</h2>'
279 ]
280
281 # Which methods are for collections.
Joe Gregorioafc45f22011-02-20 16:11:28 -0500282 for name in dir(resource):
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400283 if not name.startswith('_') and callable(getattr(resource, name)):
284 if hasattr(getattr(resource, name), '__is_resource__'):
285 collections.append(name)
286 else:
287 methods.append(name)
Joe Gregorioafc45f22011-02-20 16:11:28 -0500288
Joe Gregorioafc45f22011-02-20 16:11:28 -0500289
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400290 # TOC
291 if collections:
292 for name in collections:
293 if not name.startswith('_') and callable(getattr(resource, name)):
294 href = path + name + '.html'
Joe Gregorio79daca02013-03-29 16:25:52 -0400295 html.append(string.Template(COLLECTION_LINK).substitute(
296 href=href, name=name))
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400297
298 if methods:
299 for name in methods:
300 if not name.startswith('_') and callable(getattr(resource, name)):
301 doc = getattr(resource, name).__doc__
302 params = method_params(doc)
303 firstline = doc.splitlines()[0]
Joe Gregorio79daca02013-03-29 16:25:52 -0400304 html.append(string.Template(METHOD_LINK).substitute(
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400305 name=name, params=params, firstline=firstline))
306
307 if methods:
308 html.append('<h3>Method Details</h3>')
309 for name in methods:
310 dname = name.rsplit('_')[0]
311 html.append(method(name, getattr(resource, name).__doc__))
312
313 html.append('</body></html>')
314
315 return '\n'.join(html)
316
317
318def document_collection_recursive(resource, path, root_discovery, discovery):
319
320 html = document_collection(resource, path, root_discovery, discovery)
Joe Gregorioafc45f22011-02-20 16:11:28 -0500321
Joe Gregoriobb964352013-03-03 20:45:29 -0500322 f = open(os.path.join(FLAGS.dest, path + 'html'), 'w')
Joe Gregoriod67010d2012-11-05 08:57:06 -0500323 f.write(html.encode('utf-8'))
Joe Gregorioafc45f22011-02-20 16:11:28 -0500324 f.close()
325
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400326 for name in dir(resource):
327 if (not name.startswith('_')
328 and callable(getattr(resource, name))
329 and hasattr(getattr(resource, name), '__is_resource__')):
330 dname = name.rsplit('_')[0]
331 collection = getattr(resource, name)()
332 document_collection_recursive(collection, path + name + '.', root_discovery,
333 discovery['resources'].get(dname, {}))
334
Joe Gregorioafc45f22011-02-20 16:11:28 -0500335def document_api(name, version):
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400336 """Document the given API.
337
338 Args:
339 name: string, Name of the API.
340 version: string, Version of the API.
341 """
Joe Gregorioafc45f22011-02-20 16:11:28 -0500342 service = build(name, version)
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400343 response, content = http.request(
344 uritemplate.expand(
Joe Gregoriobb964352013-03-03 20:45:29 -0500345 FLAGS.discovery_uri_template, {
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400346 'api': name,
347 'apiVersion': version})
348 )
Craig Citro6ae34d72014-08-18 23:10:09 -0700349 discovery = json.loads(content)
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400350
351 version = safe_version(version)
352
353 document_collection_recursive(
354 service, '%s_%s.' % (name, version), discovery, discovery)
355
Joe Gregorioafc45f22011-02-20 16:11:28 -0500356
Joe Gregoriobb964352013-03-03 20:45:29 -0500357def document_api_from_discovery_document(uri):
358 """Document the given API.
359
360 Args:
361 uri: string, URI of discovery document.
362 """
Joe Gregorio20a5aa92011-04-01 17:44:25 -0400363 http = httplib2.Http()
Joe Gregoriobb964352013-03-03 20:45:29 -0500364 response, content = http.request(FLAGS.discovery_uri)
Craig Citro6ae34d72014-08-18 23:10:09 -0700365 discovery = json.loads(content)
Joe Gregoriobb964352013-03-03 20:45:29 -0500366
367 service = build_from_document(discovery)
368
369 name = discovery['version']
370 version = safe_version(discovery['version'])
371
372 document_collection_recursive(
373 service, '%s_%s.' % (name, version), discovery, discovery)
374
375
376if __name__ == '__main__':
Joe Gregorio79daca02013-03-29 16:25:52 -0400377 FLAGS = parser.parse_args(sys.argv[1:])
Joe Gregoriobb964352013-03-03 20:45:29 -0500378 if FLAGS.discovery_uri:
379 document_api_from_discovery_document(FLAGS.discovery_uri)
Joe Gregorio20a5aa92011-04-01 17:44:25 -0400380 else:
Joe Gregoriobb964352013-03-03 20:45:29 -0500381 http = httplib2.Http()
382 resp, content = http.request(
383 FLAGS.directory_uri,
384 headers={'X-User-IP': '0.0.0.0'})
385 if resp.status == 200:
Craig Citro6ae34d72014-08-18 23:10:09 -0700386 directory = json.loads(content)['items']
Joe Gregoriobb964352013-03-03 20:45:29 -0500387 for api in directory:
388 document_api(api['name'], api['version'])
389 else:
390 sys.exit("Failed to load the discovery document.")