gcloud 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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