• 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"""
23from __future__ import print_function
24
25__author__ = 'jcgregorio@google.com (Joe Gregorio)'
26
27from collections import OrderedDict
28import argparse
29import collections
30import json
31import os
32import re
33import string
34import sys
35
36from googleapiclient.discovery import DISCOVERY_URI
37from googleapiclient.discovery import build
38from googleapiclient.discovery import build_from_document
39from googleapiclient.discovery import UnknownApiNameOrVersion
40from googleapiclient.http import build_http
41import uritemplate
42
43CSS = """<style>
44
45body, h1, h2, h3, div, span, p, pre, a {
46  margin: 0;
47  padding: 0;
48  border: 0;
49  font-weight: inherit;
50  font-style: inherit;
51  font-size: 100%;
52  font-family: inherit;
53  vertical-align: baseline;
54}
55
56body {
57  font-size: 13px;
58  padding: 1em;
59}
60
61h1 {
62  font-size: 26px;
63  margin-bottom: 1em;
64}
65
66h2 {
67  font-size: 24px;
68  margin-bottom: 1em;
69}
70
71h3 {
72  font-size: 20px;
73  margin-bottom: 1em;
74  margin-top: 1em;
75}
76
77pre, code {
78  line-height: 1.5;
79  font-family: Monaco, 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Lucida Console', monospace;
80}
81
82pre {
83  margin-top: 0.5em;
84}
85
86h1, h2, h3, p {
87  font-family: Arial, sans serif;
88}
89
90h1, h2, h3 {
91  border-bottom: solid #CCC 1px;
92}
93
94.toc_element {
95  margin-top: 0.5em;
96}
97
98.firstline {
99  margin-left: 2 em;
100}
101
102.method  {
103  margin-top: 1em;
104  border: solid 1px #CCC;
105  padding: 1em;
106  background: #EEE;
107}
108
109.details {
110  font-weight: bold;
111  font-size: 14px;
112}
113
114</style>
115"""
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
133BASE = 'docs/dyn'
134
135DIRECTORY_URI = 'https://www.googleapis.com/discovery/v1/apis'
136
137parser = argparse.ArgumentParser(description=__doc__)
138
139parser.add_argument('--discovery_uri_template', default=DISCOVERY_URI,
140                    help='URI Template for discovery.')
141
142parser.add_argument('--discovery_uri', default='',
143                    help=('URI of discovery document. If supplied then only '
144                          'this API will be documented.'))
145
146parser.add_argument('--directory_uri', default=DIRECTORY_URI,
147                    help=('URI of directory document. Unused if --discovery_uri'
148                          ' is supplied.'))
149
150parser.add_argument('--dest', default=BASE,
151                    help='Directory name to write documents into.')
152
153
154
155def safe_version(version):
156  """Create a safe version of the verion string.
157
158  Needed so that we can distinguish between versions
159  and sub-collections in URIs. I.e. we don't want
160  adsense_v1.1 to refer to the '1' collection in the v1
161  version of the adsense api.
162
163  Args:
164    version: string, The version string.
165  Returns:
166    The string with '.' replaced with '_'.
167  """
168
169  return version.replace('.', '_')
170
171
172def unsafe_version(version):
173  """Undoes what safe_version() does.
174
175  See safe_version() for the details.
176
177
178  Args:
179    version: string, The safe version string.
180  Returns:
181    The string with '_' replaced with '.'.
182  """
183
184  return version.replace('_', '.')
185
186
187def method_params(doc):
188  """Document the parameters of a method.
189
190  Args:
191    doc: string, The method's docstring.
192
193  Returns:
194    The method signature as a string.
195  """
196  doclines = doc.splitlines()
197  if 'Args:' in doclines:
198    begin = doclines.index('Args:')
199    if 'Returns:' in doclines[begin+1:]:
200      end = doclines.index('Returns:', begin)
201      args = doclines[begin+1: end]
202    else:
203      args = doclines[begin+1:]
204
205    parameters = []
206    pname = None
207    desc = ''
208    def add_param(pname, desc):
209      if pname is None:
210        return
211      if '(required)' not in desc:
212        pname = pname + '=None'
213      parameters.append(pname)
214    for line in args:
215      m = re.search('^\s+([a-zA-Z0-9_]+): (.*)', line)
216      if m is None:
217        desc += line
218        continue
219      add_param(pname, desc)
220      pname = m.group(1)
221      desc = m.group(2)
222    add_param(pname, desc)
223    parameters = ', '.join(parameters)
224  else:
225    parameters = ''
226  return parameters
227
228
229def method(name, doc):
230  """Documents an individual method.
231
232  Args:
233    name: string, Name of the method.
234    doc: string, The methods docstring.
235  """
236
237  params = method_params(doc)
238  return string.Template(METHOD_TEMPLATE).substitute(
239      name=name, params=params, doc=doc)
240
241
242def breadcrumbs(path, root_discovery):
243  """Create the breadcrumb trail to this page of documentation.
244
245  Args:
246    path: string, Dot separated name of the resource.
247    root_discovery: Deserialized discovery document.
248
249  Returns:
250    HTML with links to each of the parent resources of this resource.
251  """
252  parts = path.split('.')
253
254  crumbs = []
255  accumulated = []
256
257  for i, p in enumerate(parts):
258    prefix = '.'.join(accumulated)
259    # The first time through prefix will be [], so we avoid adding in a
260    # superfluous '.' to prefix.
261    if prefix:
262      prefix += '.'
263    display = p
264    if i == 0:
265      display = root_discovery.get('title', display)
266    crumbs.append('<a href="%s.html">%s</a>' % (prefix + p, display))
267    accumulated.append(p)
268
269  return ' . '.join(crumbs)
270
271
272def document_collection(resource, path, root_discovery, discovery, css=CSS):
273  """Document a single collection in an API.
274
275  Args:
276    resource: Collection or service being documented.
277    path: string, Dot separated name of the resource.
278    root_discovery: Deserialized discovery document.
279    discovery: Deserialized discovery document, but just the portion that
280      describes the resource.
281    css: string, The CSS to include in the generated file.
282  """
283  collections = []
284  methods = []
285  resource_name = path.split('.')[-2]
286  html = [
287      '<html><body>',
288      css,
289      '<h1>%s</h1>' % breadcrumbs(path[:-1], root_discovery),
290      '<h2>Instance Methods</h2>'
291      ]
292
293  # Which methods are for collections.
294  for name in dir(resource):
295    if not name.startswith('_') and callable(getattr(resource, name)):
296      if hasattr(getattr(resource, name), '__is_resource__'):
297        collections.append(name)
298      else:
299        methods.append(name)
300
301
302  # TOC
303  if collections:
304    for name in collections:
305      if not name.startswith('_') and callable(getattr(resource, name)):
306        href = path + name + '.html'
307        html.append(string.Template(COLLECTION_LINK).substitute(
308            href=href, name=name))
309
310  if methods:
311    for name in methods:
312      if not name.startswith('_') and callable(getattr(resource, name)):
313        doc = getattr(resource, name).__doc__
314        params = method_params(doc)
315        firstline = doc.splitlines()[0]
316        html.append(string.Template(METHOD_LINK).substitute(
317            name=name, params=params, firstline=firstline))
318
319  if methods:
320    html.append('<h3>Method Details</h3>')
321    for name in methods:
322      dname = name.rsplit('_')[0]
323      html.append(method(name, getattr(resource, name).__doc__))
324
325  html.append('</body></html>')
326
327  return '\n'.join(html)
328
329
330def document_collection_recursive(resource, path, root_discovery, discovery):
331
332  html = document_collection(resource, path, root_discovery, discovery)
333
334  f = open(os.path.join(FLAGS.dest, path + 'html'), 'w')
335  f.write(html.encode('utf-8'))
336  f.close()
337
338  for name in dir(resource):
339    if (not name.startswith('_')
340        and callable(getattr(resource, name))
341        and hasattr(getattr(resource, name), '__is_resource__')
342        and discovery != {}):
343      dname = name.rsplit('_')[0]
344      collection = getattr(resource, name)()
345      document_collection_recursive(collection, path + name + '.', root_discovery,
346               discovery['resources'].get(dname, {}))
347
348def document_api(name, version):
349  """Document the given API.
350
351  Args:
352    name: string, Name of the API.
353    version: string, Version of the API.
354  """
355  try:
356    service = build(name, version)
357  except UnknownApiNameOrVersion as e:
358    print('Warning: {} {} found but could not be built.'.format(name, version))
359    return
360
361  http = build_http()
362  response, content = http.request(
363      uritemplate.expand(
364          FLAGS.discovery_uri_template, {
365              'api': name,
366              'apiVersion': version})
367          )
368  discovery = json.loads(content)
369
370  version = safe_version(version)
371
372  document_collection_recursive(
373      service, '%s_%s.' % (name, version), discovery, discovery)
374
375
376def document_api_from_discovery_document(uri):
377  """Document the given API.
378
379  Args:
380    uri: string, URI of discovery document.
381  """
382  http = build_http()
383  response, content = http.request(FLAGS.discovery_uri)
384  discovery = json.loads(content)
385
386  service = build_from_document(discovery)
387
388  name = discovery['version']
389  version = safe_version(discovery['version'])
390
391  document_collection_recursive(
392      service, '%s_%s.' % (name, version), discovery, discovery)
393
394
395if __name__ == '__main__':
396  FLAGS = parser.parse_args(sys.argv[1:])
397  if FLAGS.discovery_uri:
398    document_api_from_discovery_document(FLAGS.discovery_uri)
399  else:
400    api_directory = collections.defaultdict(list)
401    http = build_http()
402    resp, content = http.request(
403        FLAGS.directory_uri,
404        headers={'X-User-IP': '0.0.0.0'})
405    if resp.status == 200:
406      directory = json.loads(content)['items']
407      for api in directory:
408        document_api(api['name'], api['version'])
409        api_directory[api['name']].append(api['version'])
410
411      # sort by api name and version number
412      for api in api_directory:
413        api_directory[api] = sorted(api_directory[api])
414      api_directory = OrderedDict(sorted(api_directory.items(), key = lambda x: x[0]))
415
416      markdown = []
417      for api, versions in api_directory.items():
418          markdown.append('## %s' % api)
419          for version in versions:
420              markdown.append('* [%s](http://googleapis.github.io/google-api-python-client/docs/dyn/%s_%s.html)' % (version, api, version))
421          markdown.append('\n')
422
423      with open('docs/dyn/index.md', 'w') as f:
424        f.write('\n'.join(markdown).encode('utf-8'))
425
426    else:
427      sys.exit("Failed to load the discovery document.")
428