blob: ac1411ca8a0a0ec2595506a7f75bc1229e341185 [file] [log] [blame]
Joe Gregorio20a5aa92011-04-01 17:44:25 -04001#!/usr/bin/env python
2#
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 Gregorioafc45f22011-02-20 16:11:28 -050026import os
Joe Gregorioafc45f22011-02-20 16:11:28 -050027import re
Joe Gregorio20a5aa92011-04-01 17:44:25 -040028import sys
29import httplib2
Joe Gregorioafc45f22011-02-20 16:11:28 -050030
Joe Gregorio81d92cc2012-07-09 16:46:02 -040031from string import Template
32
Joe Gregorioafc45f22011-02-20 16:11:28 -050033from apiclient.discovery import build
Joe Gregoriobb964352013-03-03 20:45:29 -050034from apiclient.discovery import build_from_document
35from apiclient.discovery import DISCOVERY_URI
Joe Gregorio81d92cc2012-07-09 16:46:02 -040036from oauth2client.anyjson import simplejson
Joe Gregoriobb964352013-03-03 20:45:29 -050037import gflags
Joe Gregorio81d92cc2012-07-09 16:46:02 -040038import uritemplate
39
Joe Gregorioafc45f22011-02-20 16:11:28 -050040
Joe Gregorio81d92cc2012-07-09 16:46:02 -040041CSS = """<style>
42
43body, h1, h2, h3, div, span, p, pre, a {
44 margin: 0;
45 padding: 0;
46 border: 0;
47 font-weight: inherit;
48 font-style: inherit;
49 font-size: 100%;
50 font-family: inherit;
51 vertical-align: baseline;
52}
53
54body {
55 font-size: 13px;
56 padding: 1em;
57}
58
59h1 {
60 font-size: 26px;
61 margin-bottom: 1em;
62}
63
64h2 {
65 font-size: 24px;
66 margin-bottom: 1em;
67}
68
69h3 {
70 font-size: 20px;
71 margin-bottom: 1em;
72 margin-top: 1em;
73}
74
75pre, code {
76 line-height: 1.5;
77 font-family: Monaco, 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Lucida Console', monospace;
78}
79
80pre {
81 margin-top: 0.5em;
82}
83
84h1, h2, h3, p {
85 font-family: Arial, sans serif;
86}
87
88h1, h2, h3 {
89 border-bottom: solid #CCC 1px;
90}
91
92.toc_element {
93 margin-top: 0.5em;
94}
95
96.firstline {
97 margin-left: 2 em;
98}
99
100.method {
101 margin-top: 1em;
102 border: solid 1px #CCC;
103 padding: 1em;
104 background: #EEE;
105}
106
107.details {
108 font-weight: bold;
109 font-size: 14px;
110}
111
112</style>
113"""
114
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400115METHOD_TEMPLATE = """<div class="method">
116 <code class="details" id="$name">$name($params)</code>
117 <pre>$doc</pre>
118</div>
119"""
120
121COLLECTION_LINK = """<p class="toc_element">
122 <code><a href="$href">$name()</a></code>
123</p>
124<p class="firstline">Returns the $name Resource.</p>
125"""
126
127METHOD_LINK = """<p class="toc_element">
128 <code><a href="#$name">$name($params)</a></code></p>
129<p class="firstline">$firstline</p>"""
130
Joe Gregoriobb964352013-03-03 20:45:29 -0500131BASE = 'docs/dyn'
132
133DIRECTORY_URI = 'https://www.googleapis.com/discovery/v1/apis?preferred=true',
134
135FLAGS = gflags.FLAGS
136
137gflags.DEFINE_string('discovery_uri_template', DISCOVERY_URI,
138 'URI Template for discovery.')
139
140gflags.DEFINE_string('discovery_uri', '', 'URI of discovery document. '
141 'If supplied then only this API will be documented.')
142
143gflags.DEFINE_string('directory_uri', DIRECTORY_URI,
144 'URI of directory document. '
145 'Unused if --discovery_uri is supplied.')
146
147gflags.DEFINE_string('dest', BASE, 'Directory name to write documents into.')
148
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400149
150def safe_version(version):
151 """Create a safe version of the verion string.
152
153 Needed so that we can distinguish between versions
154 and sub-collections in URIs. I.e. we don't want
155 adsense_v1.1 to refer to the '1' collection in the v1
156 version of the adsense api.
157
158 Args:
159 version: string, The version string.
160 Returns:
161 The string with '.' replaced with '_'.
162 """
163
164 return version.replace('.', '_')
165
166
167def unsafe_version(version):
168 """Undoes what safe_version() does.
169
170 See safe_version() for the details.
171
172
173 Args:
174 version: string, The safe version string.
175 Returns:
176 The string with '_' replaced with '.'.
177 """
178
179 return version.replace('_', '.')
180
181
182def method_params(doc):
183 """Document the parameters of a method.
184
185 Args:
186 doc: string, The method's docstring.
187
188 Returns:
189 The method signature as a string.
190 """
191 doclines = doc.splitlines()
192 if 'Args:' in doclines:
193 begin = doclines.index('Args:')
194 if 'Returns:' in doclines[begin+1:]:
195 end = doclines.index('Returns:', begin)
196 args = doclines[begin+1: end]
197 else:
198 args = doclines[begin+1:]
199
200 parameters = []
201 for line in args:
202 m = re.search('^\s+([a-zA-Z0-9_]+): (.*)', line)
203 if m is None:
204 continue
205 pname = m.group(1)
206 desc = m.group(2)
207 if '(required)' not in desc:
208 pname = pname + '=None'
209 parameters.append(pname)
210 parameters = ', '.join(parameters)
211 else:
212 parameters = ''
213 return parameters
214
215
216def method(name, doc):
217 """Documents an individual method.
218
219 Args:
220 name: string, Name of the method.
221 doc: string, The methods docstring.
222 """
223
224 params = method_params(doc)
225 return Template(METHOD_TEMPLATE).substitute(name=name, params=params, doc=doc)
226
227
228def breadcrumbs(path, root_discovery):
229 """Create the breadcrumb trail to this page of documentation.
230
231 Args:
232 path: string, Dot separated name of the resource.
233 root_discovery: Deserialized discovery document.
234
235 Returns:
236 HTML with links to each of the parent resources of this resource.
237 """
238 parts = path.split('.')
239
240 crumbs = []
241 accumulated = []
242
243 for i, p in enumerate(parts):
244 prefix = '.'.join(accumulated)
245 # The first time through prefix will be [], so we avoid adding in a
246 # superfluous '.' to prefix.
247 if prefix:
248 prefix += '.'
249 display = p
250 if i == 0:
251 display = root_discovery.get('title', display)
252 crumbs.append('<a href="%s.html">%s</a>' % (prefix + p, display))
253 accumulated.append(p)
254
255 return ' . '.join(crumbs)
256
257
258def document_collection(resource, path, root_discovery, discovery, css=CSS):
259 """Document a single collection in an API.
260
261 Args:
262 resource: Collection or service being documented.
263 path: string, Dot separated name of the resource.
264 root_discovery: Deserialized discovery document.
265 discovery: Deserialized discovery document, but just the portion that
266 describes the resource.
267 css: string, The CSS to include in the generated file.
268 """
Joe Gregorioafc45f22011-02-20 16:11:28 -0500269 collections = []
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400270 methods = []
271 resource_name = path.split('.')[-2]
272 html = [
273 '<html><body>',
274 css,
275 '<h1>%s</h1>' % breadcrumbs(path[:-1], root_discovery),
276 '<h2>Instance Methods</h2>'
277 ]
278
279 # Which methods are for collections.
Joe Gregorioafc45f22011-02-20 16:11:28 -0500280 for name in dir(resource):
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400281 if not name.startswith('_') and callable(getattr(resource, name)):
282 if hasattr(getattr(resource, name), '__is_resource__'):
283 collections.append(name)
284 else:
285 methods.append(name)
Joe Gregorioafc45f22011-02-20 16:11:28 -0500286
Joe Gregorioafc45f22011-02-20 16:11:28 -0500287
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400288 # TOC
289 if collections:
290 for name in collections:
291 if not name.startswith('_') and callable(getattr(resource, name)):
292 href = path + name + '.html'
293 html.append(Template(COLLECTION_LINK).substitute(href=href, name=name))
294
295 if methods:
296 for name in methods:
297 if not name.startswith('_') and callable(getattr(resource, name)):
298 doc = getattr(resource, name).__doc__
299 params = method_params(doc)
300 firstline = doc.splitlines()[0]
301 html.append(Template(METHOD_LINK).substitute(
302 name=name, params=params, firstline=firstline))
303
304 if methods:
305 html.append('<h3>Method Details</h3>')
306 for name in methods:
307 dname = name.rsplit('_')[0]
308 html.append(method(name, getattr(resource, name).__doc__))
309
310 html.append('</body></html>')
311
312 return '\n'.join(html)
313
314
315def document_collection_recursive(resource, path, root_discovery, discovery):
316
317 html = document_collection(resource, path, root_discovery, discovery)
Joe Gregorioafc45f22011-02-20 16:11:28 -0500318
Joe Gregoriobb964352013-03-03 20:45:29 -0500319 f = open(os.path.join(FLAGS.dest, path + 'html'), 'w')
Joe Gregoriod67010d2012-11-05 08:57:06 -0500320 f.write(html.encode('utf-8'))
Joe Gregorioafc45f22011-02-20 16:11:28 -0500321 f.close()
322
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400323 for name in dir(resource):
324 if (not name.startswith('_')
325 and callable(getattr(resource, name))
326 and hasattr(getattr(resource, name), '__is_resource__')):
327 dname = name.rsplit('_')[0]
328 collection = getattr(resource, name)()
329 document_collection_recursive(collection, path + name + '.', root_discovery,
330 discovery['resources'].get(dname, {}))
331
Joe Gregorioafc45f22011-02-20 16:11:28 -0500332def document_api(name, version):
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400333 """Document the given API.
334
335 Args:
336 name: string, Name of the API.
337 version: string, Version of the API.
338 """
Joe Gregorioafc45f22011-02-20 16:11:28 -0500339 service = build(name, version)
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400340 response, content = http.request(
341 uritemplate.expand(
Joe Gregoriobb964352013-03-03 20:45:29 -0500342 FLAGS.discovery_uri_template, {
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400343 'api': name,
344 'apiVersion': version})
345 )
Joe Gregorio973b3a12012-07-10 10:35:14 -0400346 discovery = simplejson.loads(content)
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400347
348 version = safe_version(version)
349
350 document_collection_recursive(
351 service, '%s_%s.' % (name, version), discovery, discovery)
352
Joe Gregorioafc45f22011-02-20 16:11:28 -0500353
Joe Gregoriobb964352013-03-03 20:45:29 -0500354def document_api_from_discovery_document(uri):
355 """Document the given API.
356
357 Args:
358 uri: string, URI of discovery document.
359 """
Joe Gregorio20a5aa92011-04-01 17:44:25 -0400360 http = httplib2.Http()
Joe Gregoriobb964352013-03-03 20:45:29 -0500361 response, content = http.request(FLAGS.discovery_uri)
362 discovery = simplejson.loads(content)
363
364 service = build_from_document(discovery)
365
366 name = discovery['version']
367 version = safe_version(discovery['version'])
368
369 document_collection_recursive(
370 service, '%s_%s.' % (name, version), discovery, discovery)
371
372
373if __name__ == '__main__':
374 # Let the gflags module process the command-line arguments
375 try:
376 argv = FLAGS(sys.argv)
377 except gflags.FlagsError, e:
378 print '%s\\nUsage: %s ARGS\\n%s' % (e, argv[0], FLAGS)
379 sys.exit(1)
380
381 if FLAGS.discovery_uri:
382 document_api_from_discovery_document(FLAGS.discovery_uri)
Joe Gregorio20a5aa92011-04-01 17:44:25 -0400383 else:
Joe Gregoriobb964352013-03-03 20:45:29 -0500384 http = httplib2.Http()
385 resp, content = http.request(
386 FLAGS.directory_uri,
387 headers={'X-User-IP': '0.0.0.0'})
388 if resp.status == 200:
389 directory = simplejson.loads(content)['items']
390 for api in directory:
391 document_api(api['name'], api['version'])
392 else:
393 sys.exit("Failed to load the discovery document.")