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