• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/python
2#
3# Copyright 2014 Google Inc. All Rights Reserved.
4#
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
17"""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
24__author__ = 'jcgregorio@google.com (Joe Gregorio)'
25
26import argparse
27import json
28import os
29import re
30import string
31import sys
32
33from googleapiclient.discovery import DISCOVERY_URI
34from googleapiclient.discovery import build
35from googleapiclient.discovery import build_from_document
36from googleapiclient.discovery import UnknownApiNameOrVersion
37from googleapiclient.http import build_http
38import uritemplate
39
40CSS = """<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
114METHOD_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
130BASE = 'docs/dyn'
131
132DIRECTORY_URI = 'https://www.googleapis.com/discovery/v1/apis'
133
134parser = argparse.ArgumentParser(description=__doc__)
135
136parser.add_argument('--discovery_uri_template', default=DISCOVERY_URI,
137                    help='URI Template for discovery.')
138
139parser.add_argument('--discovery_uri', default='',
140                    help=('URI of discovery document. If supplied then only '
141                          'this API will be documented.'))
142
143parser.add_argument('--directory_uri', default=DIRECTORY_URI,
144                    help=('URI of directory document. Unused if --discovery_uri'
145                          ' is supplied.'))
146
147parser.add_argument('--dest', default=BASE,
148                    help='Directory name to write documents into.')
149
150
151
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 = []
203    pname = None
204    desc = ''
205    def add_param(pname, desc):
206      if pname is None:
207        return
208      if '(required)' not in desc:
209        pname = pname + '=None'
210      parameters.append(pname)
211    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)
220    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)
235  return string.Template(METHOD_TEMPLATE).substitute(
236      name=name, params=params, doc=doc)
237
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  """
280  collections = []
281  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.
291  for name in dir(resource):
292    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)
297
298
299  # TOC
300  if collections:
301    for name in collections:
302      if not name.startswith('_') and callable(getattr(resource, name)):
303        href = path + name + '.html'
304        html.append(string.Template(COLLECTION_LINK).substitute(
305            href=href, name=name))
306
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]
313        html.append(string.Template(METHOD_LINK).substitute(
314            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)
330
331  f = open(os.path.join(FLAGS.dest, path + 'html'), 'w')
332  f.write(html.encode('utf-8'))
333  f.close()
334
335  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
344def document_api(name, version):
345  """Document the given API.
346
347  Args:
348    name: string, Name of the API.
349    version: string, Version of the API.
350  """
351  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
357  http = build_http()
358  response, content = http.request(
359      uritemplate.expand(
360          FLAGS.discovery_uri_template, {
361              'api': name,
362              'apiVersion': version})
363          )
364  discovery = json.loads(content)
365
366  version = safe_version(version)
367
368  document_collection_recursive(
369      service, '%s_%s.' % (name, version), discovery, discovery)
370
371
372def document_api_from_discovery_document(uri):
373  """Document the given API.
374
375  Args:
376    uri: string, URI of discovery document.
377  """
378  http = build_http()
379  response, content = http.request(FLAGS.discovery_uri)
380  discovery = json.loads(content)
381
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__':
392  FLAGS = parser.parse_args(sys.argv[1:])
393  if FLAGS.discovery_uri:
394    document_api_from_discovery_document(FLAGS.discovery_uri)
395  else:
396    http = build_http()
397    resp, content = http.request(
398        FLAGS.directory_uri,
399        headers={'X-User-IP': '0.0.0.0'})
400    if resp.status == 200:
401      directory = json.loads(content)['items']
402      for api in directory:
403        document_api(api['name'], api['version'])
404    else:
405      sys.exit("Failed to load the discovery document.")
406