gcloud 0.0.2 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (118) hide show
  1. data.tar.gz.sig +2 -3
  2. data/CHANGELOG +4 -0
  3. data/LICENSE +674 -0
  4. data/Manifest +111 -0
  5. data/README.md +4 -3
  6. data/bin/gcutil +53 -0
  7. data/gcloud.gemspec +4 -3
  8. data/packages/gcutil-1.7.1/CHANGELOG +197 -0
  9. data/packages/gcutil-1.7.1/LICENSE +202 -0
  10. data/packages/gcutil-1.7.1/VERSION +1 -0
  11. data/packages/gcutil-1.7.1/gcutil +53 -0
  12. data/packages/gcutil-1.7.1/lib/google_api_python_client/LICENSE +23 -0
  13. data/packages/gcutil-1.7.1/lib/google_api_python_client/apiclient/__init__.py +1 -0
  14. data/packages/gcutil-1.7.1/lib/google_api_python_client/apiclient/discovery.py +743 -0
  15. data/packages/gcutil-1.7.1/lib/google_api_python_client/apiclient/errors.py +123 -0
  16. data/packages/gcutil-1.7.1/lib/google_api_python_client/apiclient/ext/__init__.py +0 -0
  17. data/packages/gcutil-1.7.1/lib/google_api_python_client/apiclient/http.py +1443 -0
  18. data/packages/gcutil-1.7.1/lib/google_api_python_client/apiclient/mimeparse.py +172 -0
  19. data/packages/gcutil-1.7.1/lib/google_api_python_client/apiclient/model.py +385 -0
  20. data/packages/gcutil-1.7.1/lib/google_api_python_client/apiclient/schema.py +303 -0
  21. data/packages/gcutil-1.7.1/lib/google_api_python_client/oauth2client/__init__.py +1 -0
  22. data/packages/gcutil-1.7.1/lib/google_api_python_client/oauth2client/anyjson.py +32 -0
  23. data/packages/gcutil-1.7.1/lib/google_api_python_client/oauth2client/appengine.py +528 -0
  24. data/packages/gcutil-1.7.1/lib/google_api_python_client/oauth2client/client.py +1139 -0
  25. data/packages/gcutil-1.7.1/lib/google_api_python_client/oauth2client/clientsecrets.py +105 -0
  26. data/packages/gcutil-1.7.1/lib/google_api_python_client/oauth2client/crypt.py +244 -0
  27. data/packages/gcutil-1.7.1/lib/google_api_python_client/oauth2client/django_orm.py +124 -0
  28. data/packages/gcutil-1.7.1/lib/google_api_python_client/oauth2client/file.py +107 -0
  29. data/packages/gcutil-1.7.1/lib/google_api_python_client/oauth2client/locked_file.py +343 -0
  30. data/packages/gcutil-1.7.1/lib/google_api_python_client/oauth2client/multistore_file.py +379 -0
  31. data/packages/gcutil-1.7.1/lib/google_api_python_client/oauth2client/tools.py +174 -0
  32. data/packages/gcutil-1.7.1/lib/google_api_python_client/uritemplate/__init__.py +147 -0
  33. data/packages/gcutil-1.7.1/lib/google_apputils/LICENSE +202 -0
  34. data/packages/gcutil-1.7.1/lib/google_apputils/google/__init__.py +3 -0
  35. data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/__init__.py +3 -0
  36. data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/app.py +356 -0
  37. data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/appcommands.py +783 -0
  38. data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/basetest.py +1260 -0
  39. data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/datelib.py +421 -0
  40. data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/debug.py +60 -0
  41. data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/file_util.py +181 -0
  42. data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/resources.py +67 -0
  43. data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/run_script_module.py +217 -0
  44. data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/setup_command.py +159 -0
  45. data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/shellutil.py +49 -0
  46. data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/stopwatch.py +204 -0
  47. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/__init__.py +0 -0
  48. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/auth_helper.py +140 -0
  49. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/auth_helper_test.py +149 -0
  50. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/auto_auth.py +130 -0
  51. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/auto_auth_test.py +75 -0
  52. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/basic_cmds.py +128 -0
  53. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/basic_cmds_test.py +111 -0
  54. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/command_base.py +1808 -0
  55. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/command_base_test.py +1651 -0
  56. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/compute/v1beta13.json +2851 -0
  57. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/compute/v1beta14.json +3361 -0
  58. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/disk_cmds.py +342 -0
  59. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/disk_cmds_test.py +474 -0
  60. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/firewall_cmds.py +344 -0
  61. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/firewall_cmds_test.py +231 -0
  62. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/flags_cache.py +274 -0
  63. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/gcutil +89 -0
  64. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/gcutil_logging.py +69 -0
  65. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/image_cmds.py +262 -0
  66. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/image_cmds_test.py +172 -0
  67. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/instance_cmds.py +1506 -0
  68. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/instance_cmds_test.py +1904 -0
  69. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/kernel_cmds.py +91 -0
  70. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/kernel_cmds_test.py +56 -0
  71. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/machine_type_cmds.py +106 -0
  72. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/machine_type_cmds_test.py +59 -0
  73. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/metadata.py +96 -0
  74. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/metadata_lib.py +357 -0
  75. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/metadata_test.py +84 -0
  76. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/mock_api.py +420 -0
  77. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/mock_metadata.py +58 -0
  78. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/move_cmds.py +824 -0
  79. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/move_cmds_test.py +307 -0
  80. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/network_cmds.py +178 -0
  81. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/network_cmds_test.py +133 -0
  82. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/operation_cmds.py +181 -0
  83. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/operation_cmds_test.py +196 -0
  84. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/path_initializer.py +38 -0
  85. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/project_cmds.py +173 -0
  86. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/project_cmds_test.py +111 -0
  87. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/scopes.py +61 -0
  88. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/scopes_test.py +50 -0
  89. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/snapshot_cmds.py +276 -0
  90. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/snapshot_cmds_test.py +260 -0
  91. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/ssh_keys.py +266 -0
  92. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/ssh_keys_test.py +128 -0
  93. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/table_formatter.py +563 -0
  94. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/thread_pool.py +188 -0
  95. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/thread_pool_test.py +88 -0
  96. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/utils.py +208 -0
  97. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/utils_test.py +193 -0
  98. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/version.py +17 -0
  99. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/version_checker.py +246 -0
  100. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/version_checker_test.py +271 -0
  101. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/zone_cmds.py +151 -0
  102. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/zone_cmds_test.py +60 -0
  103. data/packages/gcutil-1.7.1/lib/httplib2/LICENSE +21 -0
  104. data/packages/gcutil-1.7.1/lib/httplib2/httplib2/__init__.py +1630 -0
  105. data/packages/gcutil-1.7.1/lib/httplib2/httplib2/cacerts.txt +714 -0
  106. data/packages/gcutil-1.7.1/lib/httplib2/httplib2/iri2uri.py +110 -0
  107. data/packages/gcutil-1.7.1/lib/httplib2/httplib2/socks.py +438 -0
  108. data/packages/gcutil-1.7.1/lib/iso8601/LICENSE +20 -0
  109. data/packages/gcutil-1.7.1/lib/iso8601/iso8601/__init__.py +1 -0
  110. data/packages/gcutil-1.7.1/lib/iso8601/iso8601/iso8601.py +102 -0
  111. data/packages/gcutil-1.7.1/lib/iso8601/iso8601/test_iso8601.py +111 -0
  112. data/packages/gcutil-1.7.1/lib/python_gflags/AUTHORS +2 -0
  113. data/packages/gcutil-1.7.1/lib/python_gflags/LICENSE +28 -0
  114. data/packages/gcutil-1.7.1/lib/python_gflags/gflags.py +2862 -0
  115. data/packages/gcutil-1.7.1/lib/python_gflags/gflags2man.py +544 -0
  116. data/packages/gcutil-1.7.1/lib/python_gflags/gflags_validators.py +187 -0
  117. metadata +118 -5
  118. metadata.gz.sig +0 -0
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/python
2
+ #
3
+ # Copyright 2012 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
+ """Unit tests for the ssh key utility."""
18
+
19
+
20
+
21
+ import path_initializer
22
+ path_initializer.InitializeSysPath()
23
+
24
+ import base64
25
+
26
+ import gflags as flags
27
+ import unittest
28
+
29
+ from gcutil import ssh_keys
30
+
31
+ FLAGS = flags.FLAGS
32
+
33
+
34
+ class SshKeyTest(unittest.TestCase):
35
+
36
+
37
+ def testNoKeysProducesEmptyAuthorizedUserKeysString(self):
38
+ authorized_user_keys = ssh_keys.SshKeys.GetAuthorizedUserKeys(
39
+ use_compute_key=False,
40
+ authorized_ssh_keys=[])
41
+
42
+ self.assertEqual(authorized_user_keys, [])
43
+
44
+ def testGetAuthorizedUserKeysFromMetadata(self):
45
+ mock_ssh_keys = 'foo:bar\nbaz:bat'
46
+ mock_metadata = [
47
+ {'key': 'startup-script', 'value': 'echo "Hello, World!"'},
48
+ {'key': 'sshKeys', 'value': mock_ssh_keys},
49
+ {'key': 'key', 'value': 'value'}]
50
+ authorized_user_keys = ssh_keys.SshKeys.GetAuthorizedUserKeysFromMetadata(
51
+ mock_metadata)
52
+
53
+ self.assertEqual(authorized_user_keys,
54
+ [{'user': 'foo', 'key': 'bar'},
55
+ {'user': 'baz', 'key': 'bat'}])
56
+
57
+ def testGetAuthorizedUserKeysFromMetadataWithNoMetadata(self):
58
+ authorized_user_keys = (
59
+ ssh_keys.SshKeys.GetAuthorizedUserKeysFromMetadata([]))
60
+ self.assertEqual(authorized_user_keys, [])
61
+
62
+ def testGetAuthorizedUserKeysFromMetadataWithEmptySshKeys(self):
63
+ authorized_user_keys = ssh_keys.SshKeys.GetAuthorizedUserKeysFromMetadata(
64
+ [{'key': 'sshKeys', 'value': ''}])
65
+ self.assertEqual(authorized_user_keys, [])
66
+
67
+ def testSetAuthorizedUserKeysInMetadata(self):
68
+ metadata = [{'key': 'key1', 'value': 'value1'},
69
+ {'key': 'sshKeys', 'value': 'foo:bar\nbaz:bat'},
70
+ {'key': 'key3', 'value': 'value3'}]
71
+ ssh_keys.SshKeys.SetAuthorizedUserKeysInMetadata(
72
+ metadata,
73
+ [{'user': 'user1', 'key': 'key1'}, {'user': 'user2', 'key': 'key2'}])
74
+ self.assertEqual(
75
+ [{'key': 'key1', 'value': 'value1'},
76
+ {'key': 'sshKeys', 'value': 'user1:key1\nuser2:key2'},
77
+ {'key': 'key3', 'value': 'value3'}],
78
+ metadata)
79
+
80
+ def testValidateSshKey(self):
81
+ key = ('ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCm60IMJ0qIzJB+K4AEpEhyvfEL0eX'
82
+ '2WuQ2LpT51s5JE0UCIUqX7t7cYHwfFxqwx34ZGM5RqZbj2RFkeZfU7NwjAS1YMfjxkT'
83
+ '0l/pGnsCPzCZhV5++U9+AZpnM+669fQiA9pRq9JlL4JJtmz0dZHbBSlOwe2ty6lSS5G'
84
+ 'xAcZ+g553dj5NLfTTAH+HRzA9AnySOEExUIJ1Vpix+NEyyRkMQbBHcJnWAnqd+yBe5d'
85
+ 'E0ojpO6ZZzciF4waBhmMK4T8kuuXII/bTqlZKGzl3qdzBIhFaMmXDXq+3bw9hRvPb+g'
86
+ 'ChIDYiPmx0HJyqtZ7OkRbh5MyR5W/Zu8cn4sjoiBWIfxJ comment@comment.com')
87
+ self.assertEqual(key, ssh_keys.SshKeys._ValidateSshKey(key, 'filename'))
88
+
89
+ # No key - exception
90
+ self.assertRaises(ssh_keys.UserSetupError, ssh_keys.SshKeys._ValidateSshKey,
91
+ '', 'filename')
92
+
93
+ def AssertUserSetupError(key, message):
94
+ filename = '/the/file/path'
95
+ try:
96
+ ssh_keys.SshKeys._ValidateSshKey(key, filename)
97
+ except ssh_keys.UserSetupError, e:
98
+ self.assert_(key in e.msg)
99
+ self.assert_(filename in e.msg)
100
+ self.assert_(message in e.msg)
101
+ else:
102
+ self.fail('Expected an UserSetupError exception')
103
+
104
+ # Newline in the key
105
+ AssertUserSetupError('\n'.join(key.split()), 'single line')
106
+ # More than 3 parts
107
+ AssertUserSetupError(key + ' another_part', 'exactly three space separated')
108
+ # Or fewer than 3
109
+ AssertUserSetupError(
110
+ ' '.join(key.split()[:-1]),
111
+ 'exactly three space separated')
112
+ # Base-64 cannot contain '_'
113
+ AssertUserSetupError(key.replace('Q', '_'), 'is not a valid base64 encoded')
114
+ # Malformed key - not enought data
115
+ AssertUserSetupError(
116
+ 'ssh-rsa ' + base64.b64encode('\00\00\04') + ' comment',
117
+ 'The key has invalid length.')
118
+ # Malformed key - not enough data for length + type
119
+ AssertUserSetupError(
120
+ 'ssh-rsa ' + base64.b64encode('\00\00\00\07ssh-') + ' comment',
121
+ 'The key doesn\'t have a valid type.')
122
+ # Malformed key - mismatched type
123
+ AssertUserSetupError(
124
+ ' '.join(['ssh-rsb'] + key.split()[1:]),
125
+ 'The decoded key type doesn\'t match.')
126
+
127
+ if __name__ == '__main__':
128
+ unittest.main()
@@ -0,0 +1,563 @@
1
+ # Copyright 2012 Google Inc. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Table formatting library.
16
+
17
+ We define a TableFormatter interface, and create subclasses for
18
+ several different print formats, including formats intended for both
19
+ human and machine consumption:
20
+
21
+ Human Consumption
22
+ -----------------
23
+
24
+ PrettyFormatter: This prints ASCII-art bordered tables. Inspired
25
+ by the prettytable python library. Example:
26
+
27
+ +-----+---------------+
28
+ | foo | longer header |
29
+ +-----+---------------+
30
+ | a | 3 |
31
+ | ... |
32
+ | abc | 123 |
33
+ +-----+---------------+
34
+
35
+ SparsePrettyFormatter: This is a PrettyFormatter which simply
36
+ doesn't print most of the border. Example:
37
+
38
+ foo longer header
39
+ ----- ---------------
40
+ a 3
41
+ ...
42
+ abc 123
43
+
44
+ PrettyJsonFormatter: Prints JSON output in a format easily
45
+ read by a human. Example:
46
+
47
+ [
48
+ {
49
+ "foo": "a",
50
+ "longer header": 3
51
+ },
52
+ ...
53
+ {
54
+ "foo": "abc",
55
+ "longer header": 123
56
+ }
57
+ ]
58
+
59
+ Machine Consumption
60
+ -------------------
61
+
62
+ CsvFormatter: Prints output in CSV form, with minimal
63
+ quoting, '\n' separation between lines, and including
64
+ a header line. Example:
65
+
66
+ foo,longer header
67
+ a,3
68
+ ...
69
+ abc,123
70
+
71
+ JsonFormatter: Prints JSON output in the most compact
72
+ form possible. Example:
73
+
74
+ [{"foo":"a","longer header":3},...,{"foo":"abc","longer header":123}]
75
+
76
+ Additional formatters can be added by subclassing TableFormatter and
77
+ overriding the following methods:
78
+ __len__, __unicode__, AddRow, column_names, AddColumn
79
+ """
80
+
81
+
82
+
83
+ import cStringIO
84
+ import csv
85
+ import itertools
86
+ import json
87
+ import sys
88
+
89
+
90
+ class FormatterException(Exception):
91
+ pass
92
+
93
+
94
+ class TableFormatter(object):
95
+ """Interface for table formatters."""
96
+
97
+ def __init__(self, **kwds):
98
+ """Initializes the base class.
99
+
100
+ Keyword arguments:
101
+ skip_header_when_empty: If true, does not print the table's header
102
+ if there are zero rows. This argument has no effect on
103
+ PrettyJsonFormatter.
104
+ """
105
+ if self.__class__ == TableFormatter:
106
+ raise NotImplementedError(
107
+ 'Cannot instantiate abstract class TableFormatter')
108
+ self.skip_header_when_empty = kwds.get('skip_header_when_empty', False)
109
+
110
+ def __nonzero__(self):
111
+ return bool(len(self))
112
+
113
+ def __len__(self):
114
+ raise NotImplementedError('__len__ must be implemented by subclass')
115
+
116
+ def __str__(self):
117
+ return unicode(self).encode(sys.getdefaultencoding(), 'backslashreplace')
118
+
119
+ def __unicode__(self):
120
+ raise NotImplementedError('__unicode__ must be implemented by subclass')
121
+
122
+ def Print(self):
123
+ if self:
124
+ # TODO(user): Make encoding a customizable attribute on
125
+ # the TableFormatter.
126
+ encoding = sys.stdout.encoding or 'utf8'
127
+ print unicode(self).encode(encoding, 'backslashreplace')
128
+
129
+ def AddRow(self, row):
130
+ """Add a new row (an iterable) to this formatter."""
131
+ raise NotImplementedError('AddRow must be implemented by subclass')
132
+
133
+ def AddRows(self, rows):
134
+ """Add all rows to this table."""
135
+ for row in rows:
136
+ self.AddRow(row)
137
+
138
+ def AddField(self, field):
139
+ """Add a field as a new column to this formatter."""
140
+ # TODO(user): Excise this bigquery-specific method.
141
+ align = 'l' if field.get('type', []) == 'STRING' else 'r'
142
+ self.AddColumn(field['name'], align=align)
143
+
144
+ def AddFields(self, fields):
145
+ """Convenience method to add a list of fields."""
146
+ for field in fields:
147
+ self.AddField(field)
148
+
149
+ def AddDict(self, d):
150
+ """Add a dict as a row by using column names as keys."""
151
+ self.AddRow([d.get(name, '') for name in self.column_names])
152
+
153
+ @property
154
+ def column_names(self):
155
+ """Return the ordered list of column names in self."""
156
+ raise NotImplementedError('column_names must be implemented by subclass')
157
+
158
+ def AddColumn(self, column_name, align='r', **kwds):
159
+ """Add a new column to this formatter."""
160
+ raise NotImplementedError('AddColumn must be implemented by subclass')
161
+
162
+ def AddColumns(self, column_names, kwdss=None):
163
+ """Add a series of columns to this formatter."""
164
+ kwdss = kwdss or [{}] * len(column_names)
165
+ for column_name, kwds in zip(column_names, kwdss):
166
+ self.AddColumn(column_name, **kwds)
167
+
168
+
169
+ class PrettyFormatter(TableFormatter):
170
+ """Formats output as an ASCII-art table with borders."""
171
+
172
+ def __init__(self, **kwds):
173
+ """Initialize a new PrettyFormatter.
174
+
175
+ Keyword arguments:
176
+ junction_char: (default: +) Character to use for table junctions.
177
+ horizontal_char: (default: -) Character to use for horizontal lines.
178
+ vertical_char: (default: |) Character to use for vertical lines.
179
+ """
180
+ super(PrettyFormatter, self).__init__(**kwds)
181
+
182
+ self.junction_char = kwds.get('junction_char', '+')
183
+ self.horizontal_char = kwds.get('horizontal_char', '-')
184
+ self.vertical_char = kwds.get('vertical_char', '|')
185
+
186
+ self.rows = []
187
+ self.row_heights = []
188
+ self._column_names = []
189
+ self.column_widths = []
190
+ self.column_alignments = []
191
+ self.header_height = 1
192
+
193
+ def __len__(self):
194
+ return len(self.rows)
195
+
196
+ def __unicode__(self):
197
+ if self or not self.skip_header_when_empty:
198
+ lines = itertools.chain(
199
+ self.FormatHeader(), self.FormatRows(), self.FormatHrule())
200
+ else:
201
+ lines = []
202
+ return '\n'.join(lines)
203
+
204
+ @staticmethod
205
+ def CenteredPadding(interval, size, left_justify=True):
206
+ """Compute information for centering a string in a fixed space.
207
+
208
+ Given two integers interval and size, with size <= interval, this
209
+ function computes two integers left_padding and right_padding with
210
+ left_padding + right_padding + size = interval
211
+ and
212
+ |left_padding - right_padding| <= 1.
213
+
214
+ In the case that interval and size have different parity,
215
+ left_padding will be larger iff left_justify is True. (That is,
216
+ iff the string should be left justified in the "center" space.)
217
+
218
+ Args:
219
+ interval: Size of the fixed space.
220
+ size: Size of the string to center in that space.
221
+ left_justify: (optional, default: True) Whether the string
222
+ should be left-justified in the center space.
223
+
224
+ Returns:
225
+ left_padding, right_padding: The size of the left and right
226
+ margins for centering the string.
227
+
228
+ Raises:
229
+ FormatterException: If size > interval.
230
+ """
231
+ if size > interval:
232
+ raise FormatterException('Illegal state in table formatting')
233
+ same_parity = (interval % 2) == (size % 2)
234
+ padding = (interval - size) / 2
235
+ if same_parity:
236
+ return padding, padding
237
+ elif left_justify:
238
+ return padding, padding + 1
239
+ else:
240
+ return padding + 1, padding
241
+
242
+ @staticmethod
243
+ def Abbreviate(s, width):
244
+ """Abbreviate a string to at most width characters."""
245
+ suffix = '.' * min(width, 3)
246
+ return s if len(s) <= width else s[:width - len(suffix)] + suffix
247
+
248
+ @staticmethod
249
+ def FormatCell(entry, cell_width, cell_height=1, align='c', valign='t'):
250
+ """Format an entry into a list of strings for a fixed cell size.
251
+
252
+ Given a (possibly multi-line) entry and a cell height and width,
253
+ we split the entry into a list of lines and format each one into
254
+ the given width and alignment. We then pad the list with
255
+ additional blank lines of the appropriate width so that the
256
+ resulting list has exactly cell_height entries. Each entry
257
+ is also padded with one space on either side.
258
+
259
+ We abbreviate strings for width, but we require that the
260
+ number of lines in entry is at most cell_height.
261
+
262
+ Args:
263
+ entry: String to format, which may have newlines.
264
+ cell_width: Maximum width for lines in the cell.
265
+ cell_height: Number of lines in the cell.
266
+ align: Alignment to use for lines of text.
267
+ valign: Vertical alignment in the cell. One of 't',
268
+ 'c', or 'b' (top, center, and bottom, respectively).
269
+
270
+ Returns:
271
+ An iterator yielding exactly cell_height lines, each of
272
+ exact width cell_width + 2, corresponding to this cell.
273
+
274
+ Raises:
275
+ FormatterException: If there are too many lines in entry.
276
+ ValueError: If the valign is invalid.
277
+ """
278
+ entry_lines = [PrettyFormatter.Abbreviate(line, cell_width)
279
+ for line in entry.split('\n')]
280
+ if len(entry_lines) > cell_height:
281
+ raise FormatterException('Too many lines (%s) for a cell of size %s' % (
282
+ len(entry_lines), cell_height))
283
+ if valign == 't':
284
+ top_lines = []
285
+ bottom_lines = itertools.repeat(' ' * (cell_width + 2),
286
+ cell_height - len(entry_lines))
287
+ elif valign == 'c':
288
+ top_padding, bottom_padding = PrettyFormatter.CenteredPadding(
289
+ cell_height, len(entry_lines))
290
+ top_lines = itertools.repeat(' ' * (cell_width + 2), top_padding)
291
+ bottom_lines = itertools.repeat(' ' * (cell_width + 2), bottom_padding)
292
+ elif valign == 'b':
293
+ bottom_lines = []
294
+ top_lines = itertools.repeat(' ' * (cell_width + 2),
295
+ cell_height - len(entry_lines))
296
+ else:
297
+ raise ValueError('Unknown value for valign: %s' % (valign,))
298
+ content_lines = []
299
+ for line in entry_lines:
300
+ if align == 'c':
301
+ left_padding, right_padding = PrettyFormatter.CenteredPadding(
302
+ cell_width, len(line))
303
+ content_lines.append(' %s%s%s ' % (
304
+ ' ' * left_padding, line, ' ' * right_padding))
305
+ elif align in ('l', 'r'):
306
+ fmt = ' %*s ' if align == 'r' else ' %-*s '
307
+ content_lines.append(fmt % (cell_width, line))
308
+ else:
309
+ raise FormatterException('Unknown alignment: %s' % (align,))
310
+ return itertools.chain(top_lines, content_lines, bottom_lines)
311
+
312
+ def FormatRow(self, entries, row_height,
313
+ column_alignments=None, column_widths=None):
314
+ """Format a row into a list of strings.
315
+
316
+ Given a list of entries, which must be the same length as the
317
+ number of columns in this table, and a desired row height, we
318
+ generate a list of strings corresponding to the printed
319
+ representation of that row.
320
+
321
+ Args:
322
+ entries: List of entries to format.
323
+ row_height: Number of printed lines corresponding to this row.
324
+ column_alignments: (optional, default self.column_alignments)
325
+ The alignment to use for each column.
326
+ column_widths: (optional, default self.column_widths) The widths
327
+ of each column.
328
+
329
+ Returns:
330
+ An iterator over the strings in the printed representation
331
+ of this row.
332
+ """
333
+ column_alignments = column_alignments or self.column_alignments
334
+ column_widths = column_widths or self.column_widths
335
+
336
+ # pylint:disable-msg=C6402
337
+ curried_format = lambda entry, width, align: self.__class__.FormatCell(
338
+ unicode(entry), width, cell_height=row_height, align=align)
339
+ printed_rows = itertools.izip(*itertools.imap(
340
+ curried_format, entries, column_widths, column_alignments))
341
+ return (self.vertical_char.join(itertools.chain([''], cells, ['']))
342
+ for cells in printed_rows)
343
+
344
+ def HeaderLines(self):
345
+ """Return an iterator over the row(s) for the column names."""
346
+ aligns = itertools.repeat('c')
347
+ return self.FormatRow(self.column_names, self.header_height,
348
+ column_alignments=aligns)
349
+
350
+ def FormatHrule(self):
351
+ """Return a list containing an hrule for this table."""
352
+ entries = (''.join(itertools.repeat('-', width + 2))
353
+ for width in self.column_widths)
354
+ return [self.junction_char.join(itertools.chain([''], entries, ['']))]
355
+
356
+ def FormatHeader(self):
357
+ """Return an iterator over the lines for the header of this table."""
358
+ return itertools.chain(
359
+ self.FormatHrule(), self.HeaderLines(), self.FormatHrule())
360
+
361
+ def FormatRows(self):
362
+ """Return an iterator over all the rows in this table."""
363
+ return itertools.chain(*itertools.imap(
364
+ self.FormatRow, self.rows, self.row_heights))
365
+
366
+ def AddRow(self, row):
367
+ """Add a row to this table.
368
+
369
+ Args:
370
+ row: A list of length equal to the number of columns in this table.
371
+
372
+ Raises:
373
+ FormatterException: If the row length is invalid.
374
+ """
375
+ if len(row) != len(self.column_names):
376
+ raise FormatterException('Invalid row length: %s' % (len(row),))
377
+ split_rows = [unicode(entry).split('\n') for entry in row]
378
+ self.row_heights.append(max(len(lines) for lines in split_rows))
379
+ column_widths = (max(len(line) for line in entry) for entry in split_rows)
380
+ self.column_widths = [max(width, current) for width, current
381
+ in itertools.izip(column_widths, self.column_widths)]
382
+ self.rows.append(row)
383
+
384
+ def AddColumn(self, column_name, align='l', **kwds):
385
+ """Add a column to this table.
386
+
387
+ Args:
388
+ column_name: Name for the new column.
389
+ align: (optional, default: 'l') Alignment for the new column entries.
390
+
391
+ Raises:
392
+ FormatterException: If the table already has any rows, or if the
393
+ provided alignment is invalid.
394
+ """
395
+ if self:
396
+ raise FormatterException(
397
+ 'Cannot add a new column to an initialized table')
398
+ if align not in ('l', 'c', 'r'):
399
+ raise FormatterException('Invalid column alignment: %s' % (align,))
400
+ lines = column_name.split('\n')
401
+ self.column_widths.append(max(len(line) for line in lines))
402
+ self.column_alignments.append(align)
403
+ self.column_names.append(column_name)
404
+ self.header_height = max(len(lines), self.header_height)
405
+
406
+ @property
407
+ def column_names(self):
408
+ return self._column_names
409
+
410
+
411
+ class SparsePrettyFormatter(PrettyFormatter):
412
+ """Formats output as a table with a header and separator line."""
413
+
414
+ def __init__(self, **kwds):
415
+ """Initialize a new SparsePrettyFormatter."""
416
+ default_kwds = {'junction_char': ' ',
417
+ 'vertical_char': ' '}
418
+ default_kwds.update(kwds)
419
+ super(SparsePrettyFormatter, self).__init__(**default_kwds)
420
+
421
+ def __unicode__(self):
422
+ if self or not self.skip_header_when_empty:
423
+ lines = itertools.chain(self.FormatHeader(), self.FormatRows())
424
+ else:
425
+ lines = []
426
+ return '\n'.join(lines)
427
+
428
+ def FormatHeader(self):
429
+ """Return an iterator over the header lines for this table."""
430
+ return itertools.chain(self.HeaderLines(), self.FormatHrule())
431
+
432
+
433
+ class CsvFormatter(TableFormatter):
434
+ """Formats output as CSV with header lines.
435
+
436
+ The resulting CSV file includes a header line, uses Unix-style
437
+ newlines, and only quotes those entries which require it, namely
438
+ those that contain quotes, newlines, or commas.
439
+ """
440
+
441
+ def __init__(self, **kwds):
442
+ super(CsvFormatter, self).__init__(**kwds)
443
+ self._buffer = cStringIO.StringIO()
444
+ self._header = []
445
+ self._table = csv.writer(
446
+ self._buffer, quoting=csv.QUOTE_MINIMAL, lineterminator='\n')
447
+
448
+ def __nonzero__(self):
449
+ return bool(self._buffer.tell())
450
+
451
+ def __len__(self):
452
+ return len(unicode(self).splitlines())
453
+
454
+ def __unicode__(self):
455
+ if self or not self.skip_header_when_empty:
456
+ lines = [','.join(self._header), self._buffer.getvalue()]
457
+ else:
458
+ lines = []
459
+ # Note that we need to explicitly decode here to work around
460
+ # the fact that the CSV module does not work with unicode.
461
+ return '\n'.join(line.decode('utf8') for line in lines).rstrip()
462
+
463
+ @property
464
+ def column_names(self):
465
+ return self._header[:]
466
+
467
+ def AddColumn(self, column_name, **kwds):
468
+ if self:
469
+ raise FormatterException(
470
+ 'Cannot add a new column to an initialized table')
471
+ self._header.append(column_name)
472
+
473
+ def AddRow(self, row):
474
+ self._table.writerow([unicode(entry).encode('utf8', 'backslashreplace')
475
+ for entry in row])
476
+
477
+
478
+ class JsonFormatter(TableFormatter):
479
+ """Formats output in maximally compact JSON."""
480
+
481
+ def __init__(self, **kwds):
482
+ super(JsonFormatter, self).__init__(**kwds)
483
+ self._field_names = []
484
+ self._table = []
485
+
486
+ def __len__(self):
487
+ return len(self._table)
488
+
489
+ def __unicode__(self):
490
+ return json.dumps(self._table, separators=(',', ':'), ensure_ascii=False)
491
+
492
+ @property
493
+ def column_names(self):
494
+ return self._field_names[:]
495
+
496
+ def AddColumn(self, column_name, **kwds):
497
+ if self:
498
+ raise FormatterException(
499
+ 'Cannot add a new column to an initialized table')
500
+ self._field_names.append(column_name)
501
+
502
+ def AddRow(self, row):
503
+ if len(row) != len(self._field_names):
504
+ raise FormatterException('Invalid row: %s' % (row,))
505
+ self._table.append(dict(zip(self._field_names, row)))
506
+
507
+
508
+ class PrettyJsonFormatter(JsonFormatter):
509
+ """Formats output in human-legible JSON."""
510
+
511
+ def __unicode__(self):
512
+ return json.dumps(self._table, sort_keys=True, indent=2, ensure_ascii=False)
513
+
514
+
515
+ class NullFormatter(TableFormatter):
516
+ """Formatter that prints no output at all."""
517
+
518
+ def __init__(self, **kwds):
519
+ super(NullFormatter, self).__init__(**kwds)
520
+ self._column_names = []
521
+ self._rows = []
522
+
523
+ def __nonzero__(self):
524
+ return bool(self._rows)
525
+
526
+ def __len__(self):
527
+ return len(self._rows)
528
+
529
+ def __unicode__(self):
530
+ return ''
531
+
532
+ def AddRow(self, row):
533
+ self._rows.append(row)
534
+
535
+ def AddRows(self, rows):
536
+ for row in rows:
537
+ self.AddRow(row)
538
+
539
+ @property
540
+ def column_names(self):
541
+ return self._column_names[:]
542
+
543
+ def AddColumn(self, column_name, **kwds):
544
+ self._column_names.append(column_name)
545
+
546
+
547
+ def GetFormatter(table_format):
548
+ """Map a format name to a TableFormatter object."""
549
+ if table_format == 'csv':
550
+ table_formatter = CsvFormatter()
551
+ elif table_format == 'pretty':
552
+ table_formatter = PrettyFormatter()
553
+ elif table_format == 'json':
554
+ table_formatter = JsonFormatter()
555
+ elif table_format == 'prettyjson':
556
+ table_formatter = PrettyJsonFormatter()
557
+ elif table_format == 'sparse':
558
+ table_formatter = SparsePrettyFormatter()
559
+ elif table_format == 'none':
560
+ table_formatter = NullFormatter()
561
+ else:
562
+ raise FormatterException('Unknown format: %s' % table_format)
563
+ return table_formatter