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,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)