blob: 8ee8e7a19d5b3f20ad2b51668e2717e18bf80535 [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 Gregorio81d92cc2012-07-09 16:46:02 -040026import json
Joe Gregorioafc45f22011-02-20 16:11:28 -050027import os
Joe Gregorioafc45f22011-02-20 16:11:28 -050028import re
Joe Gregorio20a5aa92011-04-01 17:44:25 -040029import sys
30import httplib2
Joe Gregorioafc45f22011-02-20 16:11:28 -050031
Joe Gregorio81d92cc2012-07-09 16:46:02 -040032from string import Template
33
Joe Gregorioafc45f22011-02-20 16:11:28 -050034from apiclient.discovery import build
Joe Gregorio81d92cc2012-07-09 16:46:02 -040035from oauth2client.anyjson import simplejson
36import uritemplate
37
Joe Gregorioafc45f22011-02-20 16:11:28 -050038
39BASE = 'docs/dyn'
40
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
115DISCOVERY_URI = ('https://www.googleapis.com/discovery/v1/apis/'
116 '{api}/{apiVersion}/rest')
117
118METHOD_TEMPLATE = """<div class="method">
119 <code class="details" id="$name">$name($params)</code>
120 <pre>$doc</pre>
121</div>
122"""
123
124COLLECTION_LINK = """<p class="toc_element">
125 <code><a href="$href">$name()</a></code>
126</p>
127<p class="firstline">Returns the $name Resource.</p>
128"""
129
130METHOD_LINK = """<p class="toc_element">
131 <code><a href="#$name">$name($params)</a></code></p>
132<p class="firstline">$firstline</p>"""
133
134
135def safe_version(version):
136 """Create a safe version of the verion string.
137
138 Needed so that we can distinguish between versions
139 and sub-collections in URIs. I.e. we don't want
140 adsense_v1.1 to refer to the '1' collection in the v1
141 version of the adsense api.
142
143 Args:
144 version: string, The version string.
145 Returns:
146 The string with '.' replaced with '_'.
147 """
148
149 return version.replace('.', '_')
150
151
152def unsafe_version(version):
153 """Undoes what safe_version() does.
154
155 See safe_version() for the details.
156
157
158 Args:
159 version: string, The safe version string.
160 Returns:
161 The string with '_' replaced with '.'.
162 """
163
164 return version.replace('_', '.')
165
166
167def method_params(doc):
168 """Document the parameters of a method.
169
170 Args:
171 doc: string, The method's docstring.
172
173 Returns:
174 The method signature as a string.
175 """
176 doclines = doc.splitlines()
177 if 'Args:' in doclines:
178 begin = doclines.index('Args:')
179 if 'Returns:' in doclines[begin+1:]:
180 end = doclines.index('Returns:', begin)
181 args = doclines[begin+1: end]
182 else:
183 args = doclines[begin+1:]
184
185 parameters = []
186 for line in args:
187 m = re.search('^\s+([a-zA-Z0-9_]+): (.*)', line)
188 if m is None:
189 continue
190 pname = m.group(1)
191 desc = m.group(2)
192 if '(required)' not in desc:
193 pname = pname + '=None'
194 parameters.append(pname)
195 parameters = ', '.join(parameters)
196 else:
197 parameters = ''
198 return parameters
199
200
201def method(name, doc):
202 """Documents an individual method.
203
204 Args:
205 name: string, Name of the method.
206 doc: string, The methods docstring.
207 """
208
209 params = method_params(doc)
210 return Template(METHOD_TEMPLATE).substitute(name=name, params=params, doc=doc)
211
212
213def breadcrumbs(path, root_discovery):
214 """Create the breadcrumb trail to this page of documentation.
215
216 Args:
217 path: string, Dot separated name of the resource.
218 root_discovery: Deserialized discovery document.
219
220 Returns:
221 HTML with links to each of the parent resources of this resource.
222 """
223 parts = path.split('.')
224
225 crumbs = []
226 accumulated = []
227
228 for i, p in enumerate(parts):
229 prefix = '.'.join(accumulated)
230 # The first time through prefix will be [], so we avoid adding in a
231 # superfluous '.' to prefix.
232 if prefix:
233 prefix += '.'
234 display = p
235 if i == 0:
236 display = root_discovery.get('title', display)
237 crumbs.append('<a href="%s.html">%s</a>' % (prefix + p, display))
238 accumulated.append(p)
239
240 return ' . '.join(crumbs)
241
242
243def document_collection(resource, path, root_discovery, discovery, css=CSS):
244 """Document a single collection in an API.
245
246 Args:
247 resource: Collection or service being documented.
248 path: string, Dot separated name of the resource.
249 root_discovery: Deserialized discovery document.
250 discovery: Deserialized discovery document, but just the portion that
251 describes the resource.
252 css: string, The CSS to include in the generated file.
253 """
Joe Gregorioafc45f22011-02-20 16:11:28 -0500254 collections = []
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400255 methods = []
256 resource_name = path.split('.')[-2]
257 html = [
258 '<html><body>',
259 css,
260 '<h1>%s</h1>' % breadcrumbs(path[:-1], root_discovery),
261 '<h2>Instance Methods</h2>'
262 ]
263
264 # Which methods are for collections.
Joe Gregorioafc45f22011-02-20 16:11:28 -0500265 for name in dir(resource):
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400266 if not name.startswith('_') and callable(getattr(resource, name)):
267 if hasattr(getattr(resource, name), '__is_resource__'):
268 collections.append(name)
269 else:
270 methods.append(name)
Joe Gregorioafc45f22011-02-20 16:11:28 -0500271
Joe Gregorioafc45f22011-02-20 16:11:28 -0500272
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400273 # TOC
274 if collections:
275 for name in collections:
276 if not name.startswith('_') and callable(getattr(resource, name)):
277 href = path + name + '.html'
278 html.append(Template(COLLECTION_LINK).substitute(href=href, name=name))
279
280 if methods:
281 for name in methods:
282 if not name.startswith('_') and callable(getattr(resource, name)):
283 doc = getattr(resource, name).__doc__
284 params = method_params(doc)
285 firstline = doc.splitlines()[0]
286 html.append(Template(METHOD_LINK).substitute(
287 name=name, params=params, firstline=firstline))
288
289 if methods:
290 html.append('<h3>Method Details</h3>')
291 for name in methods:
292 dname = name.rsplit('_')[0]
293 html.append(method(name, getattr(resource, name).__doc__))
294
295 html.append('</body></html>')
296
297 return '\n'.join(html)
298
299
300def document_collection_recursive(resource, path, root_discovery, discovery):
301
302 html = document_collection(resource, path, root_discovery, discovery)
Joe Gregorioafc45f22011-02-20 16:11:28 -0500303
304 f = open(os.path.join(BASE, path + 'html'), 'w')
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400305 f.write(html)
Joe Gregorioafc45f22011-02-20 16:11:28 -0500306 f.close()
307
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400308 for name in dir(resource):
309 if (not name.startswith('_')
310 and callable(getattr(resource, name))
311 and hasattr(getattr(resource, name), '__is_resource__')):
312 dname = name.rsplit('_')[0]
313 collection = getattr(resource, name)()
314 document_collection_recursive(collection, path + name + '.', root_discovery,
315 discovery['resources'].get(dname, {}))
316
Joe Gregorioafc45f22011-02-20 16:11:28 -0500317def document_api(name, version):
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400318 """Document the given API.
319
320 Args:
321 name: string, Name of the API.
322 version: string, Version of the API.
323 """
Joe Gregorioafc45f22011-02-20 16:11:28 -0500324 service = build(name, version)
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400325 response, content = http.request(
326 uritemplate.expand(
327 DISCOVERY_URI, {
328 'api': name,
329 'apiVersion': version})
330 )
331 discovery = json.loads(content)
332
333 version = safe_version(version)
334
335 document_collection_recursive(
336 service, '%s_%s.' % (name, version), discovery, discovery)
337
Joe Gregorioafc45f22011-02-20 16:11:28 -0500338
339if __name__ == '__main__':
Joe Gregorio20a5aa92011-04-01 17:44:25 -0400340 http = httplib2.Http()
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400341 resp, content = http.request(
342 'https://www.googleapis.com/discovery/v1/apis?preferred=true')
Joe Gregorio20a5aa92011-04-01 17:44:25 -0400343 if resp.status == 200:
344 directory = simplejson.loads(content)['items']
345 for api in directory:
346 document_api(api['name'], api['version'])
347 else:
348 sys.exit("Failed to load the discovery document.")