googlecloud 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 +0 -0
  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/googlecloud.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,303 @@
1
+ # Copyright (C) 2010 Google Inc.
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
+ """Schema processing for discovery based APIs
16
+
17
+ Schemas holds an APIs discovery schemas. It can return those schema as
18
+ deserialized JSON objects, or pretty print them as prototype objects that
19
+ conform to the schema.
20
+
21
+ For example, given the schema:
22
+
23
+ schema = \"\"\"{
24
+ "Foo": {
25
+ "type": "object",
26
+ "properties": {
27
+ "etag": {
28
+ "type": "string",
29
+ "description": "ETag of the collection."
30
+ },
31
+ "kind": {
32
+ "type": "string",
33
+ "description": "Type of the collection ('calendar#acl').",
34
+ "default": "calendar#acl"
35
+ },
36
+ "nextPageToken": {
37
+ "type": "string",
38
+ "description": "Token used to access the next
39
+ page of this result. Omitted if no further results are available."
40
+ }
41
+ }
42
+ }
43
+ }\"\"\"
44
+
45
+ s = Schemas(schema)
46
+ print s.prettyPrintByName('Foo')
47
+
48
+ Produces the following output:
49
+
50
+ {
51
+ "nextPageToken": "A String", # Token used to access the
52
+ # next page of this result. Omitted if no further results are available.
53
+ "kind": "A String", # Type of the collection ('calendar#acl').
54
+ "etag": "A String", # ETag of the collection.
55
+ },
56
+
57
+ The constructor takes a discovery document in which to look up named schema.
58
+ """
59
+
60
+ # TODO(user) support format, enum, minimum, maximum
61
+
62
+
63
+
64
+ import copy
65
+ from oauth2client.anyjson import simplejson
66
+
67
+
68
+ class Schemas(object):
69
+ """Schemas for an API."""
70
+
71
+ def __init__(self, discovery):
72
+ """Constructor.
73
+
74
+ Args:
75
+ discovery: object, Deserialized discovery document from which we pull
76
+ out the named schema.
77
+ """
78
+ self.schemas = discovery.get('schemas', {})
79
+
80
+ # Cache of pretty printed schemas.
81
+ self.pretty = {}
82
+
83
+ def _prettyPrintByName(self, name, seen=None, dent=0):
84
+ """Get pretty printed object prototype from the schema name.
85
+
86
+ Args:
87
+ name: string, Name of schema in the discovery document.
88
+ seen: list of string, Names of schema already seen. Used to handle
89
+ recursive definitions.
90
+
91
+ Returns:
92
+ string, A string that contains a prototype object with
93
+ comments that conforms to the given schema.
94
+ """
95
+ if seen is None:
96
+ seen = []
97
+
98
+ if name in seen:
99
+ # Do not fall into an infinite loop over recursive definitions.
100
+ return '# Object with schema name: %s' % name
101
+ seen.append(name)
102
+
103
+ if name not in self.pretty:
104
+ self.pretty[name] = _SchemaToStruct(self.schemas[name],
105
+ seen, dent).to_str(self._prettyPrintByName)
106
+
107
+ seen.pop()
108
+
109
+ return self.pretty[name]
110
+
111
+ def prettyPrintByName(self, name):
112
+ """Get pretty printed object prototype from the schema name.
113
+
114
+ Args:
115
+ name: string, Name of schema in the discovery document.
116
+
117
+ Returns:
118
+ string, A string that contains a prototype object with
119
+ comments that conforms to the given schema.
120
+ """
121
+ # Return with trailing comma and newline removed.
122
+ return self._prettyPrintByName(name, seen=[], dent=1)[:-2]
123
+
124
+ def _prettyPrintSchema(self, schema, seen=None, dent=0):
125
+ """Get pretty printed object prototype of schema.
126
+
127
+ Args:
128
+ schema: object, Parsed JSON schema.
129
+ seen: list of string, Names of schema already seen. Used to handle
130
+ recursive definitions.
131
+
132
+ Returns:
133
+ string, A string that contains a prototype object with
134
+ comments that conforms to the given schema.
135
+ """
136
+ if seen is None:
137
+ seen = []
138
+
139
+ return _SchemaToStruct(schema, seen, dent).to_str(self._prettyPrintByName)
140
+
141
+ def prettyPrintSchema(self, schema):
142
+ """Get pretty printed object prototype of schema.
143
+
144
+ Args:
145
+ schema: object, Parsed JSON schema.
146
+
147
+ Returns:
148
+ string, A string that contains a prototype object with
149
+ comments that conforms to the given schema.
150
+ """
151
+ # Return with trailing comma and newline removed.
152
+ return self._prettyPrintSchema(schema, dent=1)[:-2]
153
+
154
+ def get(self, name):
155
+ """Get deserialized JSON schema from the schema name.
156
+
157
+ Args:
158
+ name: string, Schema name.
159
+ """
160
+ return self.schemas[name]
161
+
162
+
163
+ class _SchemaToStruct(object):
164
+ """Convert schema to a prototype object."""
165
+
166
+ def __init__(self, schema, seen, dent=0):
167
+ """Constructor.
168
+
169
+ Args:
170
+ schema: object, Parsed JSON schema.
171
+ seen: list, List of names of schema already seen while parsing. Used to
172
+ handle recursive definitions.
173
+ dent: int, Initial indentation depth.
174
+ """
175
+ # The result of this parsing kept as list of strings.
176
+ self.value = []
177
+
178
+ # The final value of the parsing.
179
+ self.string = None
180
+
181
+ # The parsed JSON schema.
182
+ self.schema = schema
183
+
184
+ # Indentation level.
185
+ self.dent = dent
186
+
187
+ # Method that when called returns a prototype object for the schema with
188
+ # the given name.
189
+ self.from_cache = None
190
+
191
+ # List of names of schema already seen while parsing.
192
+ self.seen = seen
193
+
194
+ def emit(self, text):
195
+ """Add text as a line to the output.
196
+
197
+ Args:
198
+ text: string, Text to output.
199
+ """
200
+ self.value.extend([" " * self.dent, text, '\n'])
201
+
202
+ def emitBegin(self, text):
203
+ """Add text to the output, but with no line terminator.
204
+
205
+ Args:
206
+ text: string, Text to output.
207
+ """
208
+ self.value.extend([" " * self.dent, text])
209
+
210
+ def emitEnd(self, text, comment):
211
+ """Add text and comment to the output with line terminator.
212
+
213
+ Args:
214
+ text: string, Text to output.
215
+ comment: string, Python comment.
216
+ """
217
+ if comment:
218
+ divider = '\n' + ' ' * (self.dent + 2) + '# '
219
+ lines = comment.splitlines()
220
+ lines = [x.rstrip() for x in lines]
221
+ comment = divider.join(lines)
222
+ self.value.extend([text, ' # ', comment, '\n'])
223
+ else:
224
+ self.value.extend([text, '\n'])
225
+
226
+ def indent(self):
227
+ """Increase indentation level."""
228
+ self.dent += 1
229
+
230
+ def undent(self):
231
+ """Decrease indentation level."""
232
+ self.dent -= 1
233
+
234
+ def _to_str_impl(self, schema):
235
+ """Prototype object based on the schema, in Python code with comments.
236
+
237
+ Args:
238
+ schema: object, Parsed JSON schema file.
239
+
240
+ Returns:
241
+ Prototype object based on the schema, in Python code with comments.
242
+ """
243
+ stype = schema.get('type')
244
+ if stype == 'object':
245
+ self.emitEnd('{', schema.get('description', ''))
246
+ self.indent()
247
+ for pname, pschema in schema.get('properties', {}).iteritems():
248
+ self.emitBegin('"%s": ' % pname)
249
+ self._to_str_impl(pschema)
250
+ self.undent()
251
+ self.emit('},')
252
+ elif '$ref' in schema:
253
+ schemaName = schema['$ref']
254
+ description = schema.get('description', '')
255
+ s = self.from_cache(schemaName, self.seen)
256
+ parts = s.splitlines()
257
+ self.emitEnd(parts[0], description)
258
+ for line in parts[1:]:
259
+ self.emit(line.rstrip())
260
+ elif stype == 'boolean':
261
+ value = schema.get('default', 'True or False')
262
+ self.emitEnd('%s,' % str(value), schema.get('description', ''))
263
+ elif stype == 'string':
264
+ value = schema.get('default', 'A String')
265
+ self.emitEnd('"%s",' % str(value), schema.get('description', ''))
266
+ elif stype == 'integer':
267
+ value = schema.get('default', '42')
268
+ self.emitEnd('%s,' % str(value), schema.get('description', ''))
269
+ elif stype == 'number':
270
+ value = schema.get('default', '3.14')
271
+ self.emitEnd('%s,' % str(value), schema.get('description', ''))
272
+ elif stype == 'null':
273
+ self.emitEnd('None,', schema.get('description', ''))
274
+ elif stype == 'any':
275
+ self.emitEnd('"",', schema.get('description', ''))
276
+ elif stype == 'array':
277
+ self.emitEnd('[', schema.get('description'))
278
+ self.indent()
279
+ self.emitBegin('')
280
+ self._to_str_impl(schema['items'])
281
+ self.undent()
282
+ self.emit('],')
283
+ else:
284
+ self.emit('Unknown type! %s' % stype)
285
+ self.emitEnd('', '')
286
+
287
+ self.string = ''.join(self.value)
288
+ return self.string
289
+
290
+ def to_str(self, from_cache):
291
+ """Prototype object based on the schema, in Python code with comments.
292
+
293
+ Args:
294
+ from_cache: callable(name, seen), Callable that retrieves an object
295
+ prototype for a schema with the given name. Seen is a list of schema
296
+ names already seen as we recursively descend the schema definition.
297
+
298
+ Returns:
299
+ Prototype object based on the schema, in Python code with comments.
300
+ The lines of the code will all be properly indented.
301
+ """
302
+ self.from_cache = from_cache
303
+ return self._to_str_impl(self.schema)
@@ -0,0 +1,32 @@
1
+ # Copyright (C) 2010 Google Inc.
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
+ """Utility module to import a JSON module
16
+
17
+ Hides all the messy details of exactly where
18
+ we get a simplejson module from.
19
+ """
20
+
21
+
22
+
23
+
24
+ try: # pragma: no cover
25
+ # Should work for Python2.6 and higher.
26
+ import json as simplejson
27
+ except ImportError: # pragma: no cover
28
+ try:
29
+ import simplejson
30
+ except ImportError:
31
+ # Try to import from django, should work on App Engine
32
+ from django.utils import simplejson
@@ -0,0 +1,528 @@
1
+ # Copyright (C) 2010 Google Inc.
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
+ """Utilities for Google App Engine
16
+
17
+ Utilities for making it easier to use OAuth 2.0 on Google App Engine.
18
+ """
19
+
20
+
21
+
22
+ import base64
23
+ import httplib2
24
+ import logging
25
+ import pickle
26
+ import time
27
+
28
+ import clientsecrets
29
+
30
+ from anyjson import simplejson
31
+ from client import AccessTokenRefreshError
32
+ from client import AssertionCredentials
33
+ from client import Credentials
34
+ from client import Flow
35
+ from client import OAuth2WebServerFlow
36
+ from client import Storage
37
+ from google.appengine.api import memcache
38
+ from google.appengine.api import users
39
+ from google.appengine.api import app_identity
40
+ from google.appengine.ext import db
41
+ from google.appengine.ext import webapp
42
+ from google.appengine.ext.webapp.util import login_required
43
+ from google.appengine.ext.webapp.util import run_wsgi_app
44
+
45
+ OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns'
46
+
47
+
48
+ class InvalidClientSecretsError(Exception):
49
+ """The client_secrets.json file is malformed or missing required fields."""
50
+ pass
51
+
52
+
53
+ class AppAssertionCredentials(AssertionCredentials):
54
+ """Credentials object for App Engine Assertion Grants
55
+
56
+ This object will allow an App Engine application to identify itself to Google
57
+ and other OAuth 2.0 servers that can verify assertions. It can be used for
58
+ the purpose of accessing data stored under an account assigned to the App
59
+ Engine application itself.
60
+
61
+ This credential does not require a flow to instantiate because it represents
62
+ a two legged flow, and therefore has all of the required information to
63
+ generate and refresh its own access tokens.
64
+ """
65
+
66
+ def __init__(self, scope, **kwargs):
67
+ """Constructor for AppAssertionCredentials
68
+
69
+ Args:
70
+ scope: string or list of strings, scope(s) of the credentials being requested.
71
+ """
72
+ if type(scope) is list:
73
+ scope = ' '.join(scope)
74
+ self.scope = scope
75
+
76
+ super(AppAssertionCredentials, self).__init__(
77
+ None,
78
+ None,
79
+ None)
80
+
81
+ @classmethod
82
+ def from_json(cls, json):
83
+ data = simplejson.loads(json)
84
+ return AppAssertionCredentials(data['scope'])
85
+
86
+ def _refresh(self, http_request):
87
+ """Refreshes the access_token.
88
+
89
+ Since the underlying App Engine app_identity implementation does its own
90
+ caching we can skip all the storage hoops and just to a refresh using the
91
+ API.
92
+
93
+ Args:
94
+ http_request: callable, a callable that matches the method signature of
95
+ httplib2.Http.request, used to make the refresh request.
96
+
97
+ Raises:
98
+ AccessTokenRefreshError: When the refresh fails.
99
+ """
100
+ try:
101
+ (token, _) = app_identity.get_access_token(self.scope)
102
+ except app_identity.Error, e:
103
+ raise AccessTokenRefreshError(str(e))
104
+ self.access_token = token
105
+
106
+
107
+ class FlowProperty(db.Property):
108
+ """App Engine datastore Property for Flow.
109
+
110
+ Utility property that allows easy storage and retreival of an
111
+ oauth2client.Flow"""
112
+
113
+ # Tell what the user type is.
114
+ data_type = Flow
115
+
116
+ # For writing to datastore.
117
+ def get_value_for_datastore(self, model_instance):
118
+ flow = super(FlowProperty,
119
+ self).get_value_for_datastore(model_instance)
120
+ return db.Blob(pickle.dumps(flow))
121
+
122
+ # For reading from datastore.
123
+ def make_value_from_datastore(self, value):
124
+ if value is None:
125
+ return None
126
+ return pickle.loads(value)
127
+
128
+ def validate(self, value):
129
+ if value is not None and not isinstance(value, Flow):
130
+ raise db.BadValueError('Property %s must be convertible '
131
+ 'to a FlowThreeLegged instance (%s)' %
132
+ (self.name, value))
133
+ return super(FlowProperty, self).validate(value)
134
+
135
+ def empty(self, value):
136
+ return not value
137
+
138
+
139
+ class CredentialsProperty(db.Property):
140
+ """App Engine datastore Property for Credentials.
141
+
142
+ Utility property that allows easy storage and retrieval of
143
+ oath2client.Credentials
144
+ """
145
+
146
+ # Tell what the user type is.
147
+ data_type = Credentials
148
+
149
+ # For writing to datastore.
150
+ def get_value_for_datastore(self, model_instance):
151
+ logging.info("get: Got type " + str(type(model_instance)))
152
+ cred = super(CredentialsProperty,
153
+ self).get_value_for_datastore(model_instance)
154
+ if cred is None:
155
+ cred = ''
156
+ else:
157
+ cred = cred.to_json()
158
+ return db.Blob(cred)
159
+
160
+ # For reading from datastore.
161
+ def make_value_from_datastore(self, value):
162
+ logging.info("make: Got type " + str(type(value)))
163
+ if value is None:
164
+ return None
165
+ if len(value) == 0:
166
+ return None
167
+ try:
168
+ credentials = Credentials.new_from_json(value)
169
+ except ValueError:
170
+ credentials = None
171
+ return credentials
172
+
173
+ def validate(self, value):
174
+ value = super(CredentialsProperty, self).validate(value)
175
+ logging.info("validate: Got type " + str(type(value)))
176
+ if value is not None and not isinstance(value, Credentials):
177
+ raise db.BadValueError('Property %s must be convertible '
178
+ 'to a Credentials instance (%s)' %
179
+ (self.name, value))
180
+ #if value is not None and not isinstance(value, Credentials):
181
+ # return None
182
+ return value
183
+
184
+
185
+ class StorageByKeyName(Storage):
186
+ """Store and retrieve a single credential to and from
187
+ the App Engine datastore.
188
+
189
+ This Storage helper presumes the Credentials
190
+ have been stored as a CredenialsProperty
191
+ on a datastore model class, and that entities
192
+ are stored by key_name.
193
+ """
194
+
195
+ def __init__(self, model, key_name, property_name, cache=None):
196
+ """Constructor for Storage.
197
+
198
+ Args:
199
+ model: db.Model, model class
200
+ key_name: string, key name for the entity that has the credentials
201
+ property_name: string, name of the property that is a CredentialsProperty
202
+ cache: memcache, a write-through cache to put in front of the datastore
203
+ """
204
+ self._model = model
205
+ self._key_name = key_name
206
+ self._property_name = property_name
207
+ self._cache = cache
208
+
209
+ def locked_get(self):
210
+ """Retrieve Credential from datastore.
211
+
212
+ Returns:
213
+ oauth2client.Credentials
214
+ """
215
+ if self._cache:
216
+ json = self._cache.get(self._key_name)
217
+ if json:
218
+ return Credentials.new_from_json(json)
219
+
220
+ credential = None
221
+ entity = self._model.get_by_key_name(self._key_name)
222
+ if entity is not None:
223
+ credential = getattr(entity, self._property_name)
224
+ if credential and hasattr(credential, 'set_store'):
225
+ credential.set_store(self)
226
+ if self._cache:
227
+ self._cache.set(self._key_name, credential.to_json())
228
+
229
+ return credential
230
+
231
+ def locked_put(self, credentials):
232
+ """Write a Credentials to the datastore.
233
+
234
+ Args:
235
+ credentials: Credentials, the credentials to store.
236
+ """
237
+ entity = self._model.get_or_insert(self._key_name)
238
+ setattr(entity, self._property_name, credentials)
239
+ entity.put()
240
+ if self._cache:
241
+ self._cache.set(self._key_name, credentials.to_json())
242
+
243
+ def locked_delete(self):
244
+ """Delete Credential from datastore."""
245
+
246
+ if self._cache:
247
+ self._cache.delete(self._key_name)
248
+
249
+ entity = self._model.get_by_key_name(self._key_name)
250
+ if entity is not None:
251
+ entity.delete()
252
+
253
+
254
+ class CredentialsModel(db.Model):
255
+ """Storage for OAuth 2.0 Credentials
256
+
257
+ Storage of the model is keyed by the user.user_id().
258
+ """
259
+ credentials = CredentialsProperty()
260
+
261
+
262
+ class OAuth2Decorator(object):
263
+ """Utility for making OAuth 2.0 easier.
264
+
265
+ Instantiate and then use with oauth_required or oauth_aware
266
+ as decorators on webapp.RequestHandler methods.
267
+
268
+ Example:
269
+
270
+ decorator = OAuth2Decorator(
271
+ client_id='837...ent.com',
272
+ client_secret='Qh...wwI',
273
+ scope='https://www.googleapis.com/auth/plus')
274
+
275
+
276
+ class MainHandler(webapp.RequestHandler):
277
+
278
+ @decorator.oauth_required
279
+ def get(self):
280
+ http = decorator.http()
281
+ # http is authorized with the user's Credentials and can be used
282
+ # in API calls
283
+
284
+ """
285
+
286
+ def __init__(self, client_id, client_secret, scope,
287
+ auth_uri='https://accounts.google.com/o/oauth2/auth',
288
+ token_uri='https://accounts.google.com/o/oauth2/token',
289
+ user_agent=None,
290
+ message=None, **kwargs):
291
+
292
+ """Constructor for OAuth2Decorator
293
+
294
+ Args:
295
+ client_id: string, client identifier.
296
+ client_secret: string client secret.
297
+ scope: string or list of strings, scope(s) of the credentials being
298
+ requested.
299
+ auth_uri: string, URI for authorization endpoint. For convenience
300
+ defaults to Google's endpoints but any OAuth 2.0 provider can be used.
301
+ token_uri: string, URI for token endpoint. For convenience
302
+ defaults to Google's endpoints but any OAuth 2.0 provider can be used.
303
+ user_agent: string, User agent of your application, default to None.
304
+ message: Message to display if there are problems with the OAuth 2.0
305
+ configuration. The message may contain HTML and will be presented on the
306
+ web interface for any method that uses the decorator.
307
+ **kwargs: dict, Keyword arguments are be passed along as kwargs to the
308
+ OAuth2WebServerFlow constructor.
309
+ """
310
+ self.flow = OAuth2WebServerFlow(client_id, client_secret, scope, user_agent,
311
+ auth_uri, token_uri, **kwargs)
312
+ self.credentials = None
313
+ self._request_handler = None
314
+ self._message = message
315
+ self._in_error = False
316
+
317
+ def _display_error_message(self, request_handler):
318
+ request_handler.response.out.write('<html><body>')
319
+ request_handler.response.out.write(self._message)
320
+ request_handler.response.out.write('</body></html>')
321
+
322
+ def oauth_required(self, method):
323
+ """Decorator that starts the OAuth 2.0 dance.
324
+
325
+ Starts the OAuth dance for the logged in user if they haven't already
326
+ granted access for this application.
327
+
328
+ Args:
329
+ method: callable, to be decorated method of a webapp.RequestHandler
330
+ instance.
331
+ """
332
+
333
+ def check_oauth(request_handler, *args, **kwargs):
334
+ if self._in_error:
335
+ self._display_error_message(request_handler)
336
+ return
337
+
338
+ user = users.get_current_user()
339
+ # Don't use @login_decorator as this could be used in a POST request.
340
+ if not user:
341
+ request_handler.redirect(users.create_login_url(
342
+ request_handler.request.uri))
343
+ return
344
+ # Store the request URI in 'state' so we can use it later
345
+ self.flow.params['state'] = request_handler.request.url
346
+ self._request_handler = request_handler
347
+ self.credentials = StorageByKeyName(
348
+ CredentialsModel, user.user_id(), 'credentials').get()
349
+
350
+ if not self.has_credentials():
351
+ return request_handler.redirect(self.authorize_url())
352
+ try:
353
+ method(request_handler, *args, **kwargs)
354
+ except AccessTokenRefreshError:
355
+ return request_handler.redirect(self.authorize_url())
356
+
357
+ return check_oauth
358
+
359
+ def oauth_aware(self, method):
360
+ """Decorator that sets up for OAuth 2.0 dance, but doesn't do it.
361
+
362
+ Does all the setup for the OAuth dance, but doesn't initiate it.
363
+ This decorator is useful if you want to create a page that knows
364
+ whether or not the user has granted access to this application.
365
+ From within a method decorated with @oauth_aware the has_credentials()
366
+ and authorize_url() methods can be called.
367
+
368
+ Args:
369
+ method: callable, to be decorated method of a webapp.RequestHandler
370
+ instance.
371
+ """
372
+
373
+ def setup_oauth(request_handler, *args, **kwargs):
374
+ if self._in_error:
375
+ self._display_error_message(request_handler)
376
+ return
377
+
378
+ user = users.get_current_user()
379
+ # Don't use @login_decorator as this could be used in a POST request.
380
+ if not user:
381
+ request_handler.redirect(users.create_login_url(
382
+ request_handler.request.uri))
383
+ return
384
+
385
+
386
+ self.flow.params['state'] = request_handler.request.url
387
+ self._request_handler = request_handler
388
+ self.credentials = StorageByKeyName(
389
+ CredentialsModel, user.user_id(), 'credentials').get()
390
+ method(request_handler, *args, **kwargs)
391
+ return setup_oauth
392
+
393
+ def has_credentials(self):
394
+ """True if for the logged in user there are valid access Credentials.
395
+
396
+ Must only be called from with a webapp.RequestHandler subclassed method
397
+ that had been decorated with either @oauth_required or @oauth_aware.
398
+ """
399
+ return self.credentials is not None and not self.credentials.invalid
400
+
401
+ def authorize_url(self):
402
+ """Returns the URL to start the OAuth dance.
403
+
404
+ Must only be called from with a webapp.RequestHandler subclassed method
405
+ that had been decorated with either @oauth_required or @oauth_aware.
406
+ """
407
+ callback = self._request_handler.request.relative_url('/oauth2callback')
408
+ url = self.flow.step1_get_authorize_url(callback)
409
+ user = users.get_current_user()
410
+ memcache.set(user.user_id(), pickle.dumps(self.flow),
411
+ namespace=OAUTH2CLIENT_NAMESPACE)
412
+ return str(url)
413
+
414
+ def http(self):
415
+ """Returns an authorized http instance.
416
+
417
+ Must only be called from within an @oauth_required decorated method, or
418
+ from within an @oauth_aware decorated method where has_credentials()
419
+ returns True.
420
+ """
421
+ return self.credentials.authorize(httplib2.Http())
422
+
423
+
424
+ class OAuth2DecoratorFromClientSecrets(OAuth2Decorator):
425
+ """An OAuth2Decorator that builds from a clientsecrets file.
426
+
427
+ Uses a clientsecrets file as the source for all the information when
428
+ constructing an OAuth2Decorator.
429
+
430
+ Example:
431
+
432
+ decorator = OAuth2DecoratorFromClientSecrets(
433
+ os.path.join(os.path.dirname(__file__), 'client_secrets.json')
434
+ scope='https://www.googleapis.com/auth/plus')
435
+
436
+
437
+ class MainHandler(webapp.RequestHandler):
438
+
439
+ @decorator.oauth_required
440
+ def get(self):
441
+ http = decorator.http()
442
+ # http is authorized with the user's Credentials and can be used
443
+ # in API calls
444
+ """
445
+
446
+ def __init__(self, filename, scope, message=None):
447
+ """Constructor
448
+
449
+ Args:
450
+ filename: string, File name of client secrets.
451
+ scope: string or list of strings, scope(s) of the credentials being
452
+ requested.
453
+ message: string, A friendly string to display to the user if the
454
+ clientsecrets file is missing or invalid. The message may contain HTML and
455
+ will be presented on the web interface for any method that uses the
456
+ decorator.
457
+ """
458
+ try:
459
+ client_type, client_info = clientsecrets.loadfile(filename)
460
+ if client_type not in [clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
461
+ raise InvalidClientSecretsError('OAuth2Decorator doesn\'t support this OAuth 2.0 flow.')
462
+ super(OAuth2DecoratorFromClientSecrets,
463
+ self).__init__(
464
+ client_info['client_id'],
465
+ client_info['client_secret'],
466
+ scope,
467
+ client_info['auth_uri'],
468
+ client_info['token_uri'],
469
+ message)
470
+ except clientsecrets.InvalidClientSecretsError:
471
+ self._in_error = True
472
+ if message is not None:
473
+ self._message = message
474
+ else:
475
+ self._message = "Please configure your application for OAuth 2.0"
476
+
477
+
478
+ def oauth2decorator_from_clientsecrets(filename, scope, message=None):
479
+ """Creates an OAuth2Decorator populated from a clientsecrets file.
480
+
481
+ Args:
482
+ filename: string, File name of client secrets.
483
+ scope: string or list of strings, scope(s) of the credentials being
484
+ requested.
485
+ message: string, A friendly string to display to the user if the
486
+ clientsecrets file is missing or invalid. The message may contain HTML and
487
+ will be presented on the web interface for any method that uses the
488
+ decorator.
489
+
490
+ Returns: An OAuth2Decorator
491
+
492
+ """
493
+ return OAuth2DecoratorFromClientSecrets(filename, scope, message)
494
+
495
+
496
+ class OAuth2Handler(webapp.RequestHandler):
497
+ """Handler for the redirect_uri of the OAuth 2.0 dance."""
498
+
499
+ @login_required
500
+ def get(self):
501
+ error = self.request.get('error')
502
+ if error:
503
+ errormsg = self.request.get('error_description', error)
504
+ self.response.out.write(
505
+ 'The authorization request failed: %s' % errormsg)
506
+ else:
507
+ user = users.get_current_user()
508
+ flow = pickle.loads(memcache.get(user.user_id(),
509
+ namespace=OAUTH2CLIENT_NAMESPACE))
510
+ # This code should be ammended with application specific error
511
+ # handling. The following cases should be considered:
512
+ # 1. What if the flow doesn't exist in memcache? Or is corrupt?
513
+ # 2. What if the step2_exchange fails?
514
+ if flow:
515
+ credentials = flow.step2_exchange(self.request.params)
516
+ StorageByKeyName(
517
+ CredentialsModel, user.user_id(), 'credentials').put(credentials)
518
+ self.redirect(str(self.request.get('state')))
519
+ else:
520
+ # TODO Add error handling here.
521
+ pass
522
+
523
+
524
+ application = webapp.WSGIApplication([('/oauth2callback', OAuth2Handler)])
525
+
526
+
527
+ def main():
528
+ run_wsgi_app(application)