google-api-client 0.4.3 → 0.4.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.
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ # 0.4.4
2
+
3
+ * Added batch execution
4
+ * Added service accounts
5
+ * Can now supply authorization on a per-request basis.
6
+
1
7
  # 0.4.3
2
8
 
3
9
  * Added media upload capabilities
data/Gemfile ADDED
@@ -0,0 +1,28 @@
1
+ source :rubygems
2
+
3
+ gem 'signet', '>= 0.3.4'
4
+ gem 'addressable', '>= 2.2.3'
5
+ gem 'uuidtools', '>= 2.1.0'
6
+ gem 'autoparse', '>= 0.3.1'
7
+ gem 'faraday', '~> 0.7.0'
8
+ gem 'multi_json', '>= 1.3.0'
9
+ gem 'extlib', '>= 0.9.15'
10
+ gem 'jruby-openssl', :platforms => :jruby
11
+
12
+ group :development do
13
+ gem 'launchy'
14
+ gem 'yard'
15
+ gem 'redcarpet'
16
+ end
17
+
18
+ group :examples do
19
+ gem 'sinatra'
20
+ end
21
+
22
+ group :test, :development do
23
+ gem 'rake', '>= 0.9.0'
24
+ gem 'rspec', '~> 1.2.9'
25
+ gem 'rcov', '>= 0.9.9', :platform => :mri_18
26
+ end
27
+
28
+ gem 'idn', :platform => :mri_18
data/Gemfile.lock ADDED
@@ -0,0 +1,61 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ addressable (2.2.8)
5
+ autoparse (0.3.1)
6
+ addressable (~> 2.2.3)
7
+ extlib (>= 0.9.15)
8
+ multi_json (>= 1.0.0)
9
+ extlib (0.9.15)
10
+ faraday (0.7.6)
11
+ addressable (~> 2.2)
12
+ multipart-post (~> 1.1)
13
+ rack (~> 1.1)
14
+ idn (0.0.2)
15
+ json (1.7.3)
16
+ jwt (0.1.4)
17
+ json (>= 1.2.4)
18
+ launchy (2.1.0)
19
+ addressable (~> 2.2.6)
20
+ multi_json (1.3.6)
21
+ multipart-post (1.1.5)
22
+ rack (1.4.1)
23
+ rack-protection (1.2.0)
24
+ rack
25
+ rake (0.9.2.2)
26
+ rcov (1.0.0)
27
+ redcarpet (2.1.1)
28
+ rspec (1.2.9)
29
+ signet (0.3.4)
30
+ addressable (~> 2.2.3)
31
+ faraday (~> 0.7.0)
32
+ jwt (>= 0.1.4)
33
+ multi_json (>= 1.0.0)
34
+ sinatra (1.3.2)
35
+ rack (~> 1.3, >= 1.3.6)
36
+ rack-protection (~> 1.2)
37
+ tilt (~> 1.3, >= 1.3.3)
38
+ tilt (1.3.3)
39
+ uuidtools (2.1.2)
40
+ yard (0.8.1)
41
+
42
+ PLATFORMS
43
+ ruby
44
+
45
+ DEPENDENCIES
46
+ addressable (>= 2.2.3)
47
+ autoparse (>= 0.3.1)
48
+ extlib (>= 0.9.15)
49
+ faraday (~> 0.7.0)
50
+ idn
51
+ jruby-openssl
52
+ launchy
53
+ multi_json (>= 1.3.0)
54
+ rake (>= 0.9.0)
55
+ rcov (>= 0.9.9)
56
+ redcarpet
57
+ rspec (~> 1.2.9)
58
+ signet (>= 0.3.4)
59
+ sinatra
60
+ uuidtools (>= 2.1.0)
61
+ yard
data/README.md CHANGED
@@ -7,6 +7,9 @@
7
7
  <dt>License</dt><dd>Apache 2.0</dd>
8
8
  </dl>
9
9
 
10
+ [![Build Status](https://secure.travis-ci.org/google/google-api-ruby-client.png)](http://travis-ci.org/google/google-api-ruby-client)
11
+ [![Dependency Status](https://gemnasium.com/google/google-api-ruby-client.png)](https://gemnasium.com/google/google-api-ruby-client)
12
+
10
13
  # Description
11
14
 
12
15
  The Google API Ruby Client makes it trivial to discover and access supported
data/Rakefile CHANGED
@@ -40,7 +40,7 @@ PKG_FILES = FileList[
40
40
 
41
41
  RCOV_ENABLED = (RUBY_PLATFORM != 'java' && RUBY_VERSION =~ /^1\.8/)
42
42
  if RCOV_ENABLED
43
- task :default => 'spec:verify'
43
+ task :default => 'spec:rcov'
44
44
  else
45
45
  task :default => 'spec'
46
46
  end
@@ -26,6 +26,8 @@ require 'google/api_client/discovery'
26
26
  require 'google/api_client/reference'
27
27
  require 'google/api_client/result'
28
28
  require 'google/api_client/media'
29
+ require 'google/api_client/service_account'
30
+ require 'google/api_client/batch'
29
31
 
30
32
  module Google
31
33
  # TODO(bobaman): Document all this stuff.
@@ -281,7 +283,7 @@ module Google
281
283
  "Expected String or StringIO, got #{discovery_document.class}."
282
284
  end
283
285
  @discovery_documents["#{api}:#{version}"] =
284
- MultiJson.decode(discovery_document)
286
+ MultiJson.load(discovery_document)
285
287
  end
286
288
 
287
289
  ##
@@ -297,7 +299,7 @@ module Google
297
299
  )
298
300
  response = self.transmit(:request => request)
299
301
  if response.status >= 200 && response.status < 300
300
- MultiJson.decode(response.body)
302
+ MultiJson.load(response.body)
301
303
  elsif response.status >= 400
302
304
  case response.status
303
305
  when 400...500
@@ -331,7 +333,7 @@ module Google
331
333
  )
332
334
  response = self.transmit(:request => request)
333
335
  if response.status >= 200 && response.status < 300
334
- MultiJson.decode(response.body)
336
+ MultiJson.load(response.body)
335
337
  elsif response.status >= 400
336
338
  case response.status
337
339
  when 400...500
@@ -484,7 +486,7 @@ module Google
484
486
  response = self.transmit(:request => request)
485
487
  if response.status >= 200 && response.status < 300
486
488
  @certificates.merge!(
487
- Hash[MultiJson.decode(response.body).map do |key, cert|
489
+ Hash[MultiJson.load(response.body).map do |key, cert|
488
490
  [key, OpenSSL::X509::Certificate.new(cert)]
489
491
  end]
490
492
  )
@@ -540,6 +542,7 @@ module Google
540
542
  def generate_request(options={})
541
543
  # Note: The merge method on a Hash object will coerce an API Reference
542
544
  # object into a Hash and merge with the default options.
545
+
543
546
  options={
544
547
  :version => 'v1',
545
548
  :authorization => self.authorization,
@@ -547,6 +550,7 @@ module Google
547
550
  :user_ip => self.user_ip,
548
551
  :connection => Faraday.default_connection
549
552
  }.merge(options)
553
+
550
554
  # The Reference object is going to need this to do method ID lookups.
551
555
  options[:client] = self
552
556
  # The default value for the :authenticated option depends on whether an
@@ -559,7 +563,7 @@ module Google
559
563
  reference = Google::APIClient::Reference.new(options)
560
564
  request = reference.to_request
561
565
  if options[:authenticated]
562
- request = self.generate_authenticated_request(
566
+ request = options[:authorization].generate_authenticated_request(
563
567
  :request => request,
564
568
  :connection => options[:connection]
565
569
  )
@@ -573,6 +577,7 @@ module Google
573
577
  # @param [Hash] options a customizable set of options
574
578
  #
575
579
  # @return [Faraday::Request] The signed or otherwise authenticated request.
580
+ # @deprecated No longer used internally
576
581
  def generate_authenticated_request(options={})
577
582
  return authorization.generate_authenticated_request(options)
578
583
  end
@@ -663,22 +668,34 @@ module Google
663
668
  ##
664
669
  # Executes a request, wrapping it in a Result object.
665
670
  #
666
- # @param [Google::APIClient::Method, String] api_method
667
- # The method object or the RPC name of the method being executed.
668
- # @param [Hash, Array] parameters
669
- # The parameters to send to the method.
670
- # @param [String] body The body of the request.
671
- # @param [Hash, Array] headers The HTTP headers for the request.
672
- # @option options [String] :version ("v1")
673
- # The service version. Only used if `api_method` is a `String`.
674
- # @option options [#generate_authenticated_request] :authorization
675
- # The authorization mechanism for the response. Used only if
676
- # `:authenticated` is `true`.
677
- # @option options [TrueClass, FalseClass] :authenticated (true)
678
- # `true` if the request must be signed or somehow
679
- # authenticated, `false` otherwise.
671
+ # @param [Google::APIClient::BatchRequest, Hash, Array] params
672
+ # Either a Google::APIClient::BatchRequest, a Hash, or an Array.
673
+ #
674
+ # If a Google::APIClient::BatchRequest, no other parameters are expected.
675
+ #
676
+ # If a Hash, the below parameters are handled. If an Array, the
677
+ # parameters are assumed to be in the below order:
680
678
  #
681
- # @return [Google::APIClient::Result] The result from the API.
679
+ # - (Google::APIClient::Method, String) api_method:
680
+ # The method object or the RPC name of the method being executed.
681
+ # - (Hash, Array) parameters:
682
+ # The parameters to send to the method.
683
+ # - (String) body: The body of the request.
684
+ # - (Hash, Array) headers: The HTTP headers for the request.
685
+ # - (Hash) options: A set of options for the request, of which:
686
+ # - (String) :version (default: "v1") -
687
+ # The service version. Only used if `api_method` is a `String`.
688
+ # - (#generate_authenticated_request) :authorization (default: true) -
689
+ # The authorization mechanism for the response. Used only if
690
+ # `:authenticated` is `true`.
691
+ # - (TrueClass, FalseClass) :authenticated (default: true) -
692
+ # `true` if the request must be signed or somehow
693
+ # authenticated, `false` otherwise.
694
+ #
695
+ # @return [Google::APIClient::Result] The result from the API, nil if batch.
696
+ #
697
+ # @example
698
+ # result = client.execute(batch_request)
682
699
  #
683
700
  # @example
684
701
  # result = client.execute(
@@ -688,28 +705,64 @@ module Google
688
705
  #
689
706
  # @see Google::APIClient#generate_request
690
707
  def execute(*params)
691
- # This block of code allows us to accept multiple parameter passing
692
- # styles, and maintaining some backwards compatibility.
693
- #
694
- # Note: I'm extremely tempted to deprecate this style of execute call.
695
- if params.last.respond_to?(:to_hash) && params.size == 1
696
- options = params.pop
708
+ if params.last.kind_of?(Google::APIClient::BatchRequest) &&
709
+ params.size == 1
710
+ batch = params.pop
711
+ options = batch.options
712
+ http_request = batch.to_http_request
713
+ request = nil
714
+
715
+ if @authorization
716
+ method, uri, headers, body = http_request
717
+ method = method.to_s.downcase.to_sym
718
+
719
+ faraday_request = Faraday::Request.create(method) do |req|
720
+ req.url(uri.to_s)
721
+ req.headers = Faraday::Utils::Headers.new(headers)
722
+ req.body = body
723
+ end
724
+
725
+ request = {
726
+ :request => self.generate_authenticated_request(
727
+ :request => faraday_request,
728
+ :connection => options[:connection]
729
+ ),
730
+ :connection => options[:connection]
731
+ }
732
+ else
733
+ request = {
734
+ :request => http_request,
735
+ :connection => options[:connection]
736
+ }
737
+ end
738
+
739
+ response = self.transmit(request)
740
+ batch.process_response(response)
741
+ return nil
697
742
  else
698
- options = {}
699
- end
700
- options[:api_method] = params.shift if params.size > 0
701
- options[:parameters] = params.shift if params.size > 0
702
- options[:body] = params.shift if params.size > 0
703
- options[:headers] = params.shift if params.size > 0
704
- options[:client] = self
743
+ # This block of code allows us to accept multiple parameter passing
744
+ # styles, and maintaining some backwards compatibility.
745
+ #
746
+ # Note: I'm extremely tempted to deprecate this style of execute call.
747
+ if params.last.respond_to?(:to_hash) && params.size == 1
748
+ options = params.pop
749
+ else
750
+ options = {}
751
+ end
705
752
 
706
- reference = Google::APIClient::Reference.new(options)
707
- request = self.generate_request(reference)
708
- response = self.transmit(
709
- :request => request,
710
- :connection => options[:connection]
711
- )
712
- return Google::APIClient::Result.new(reference, request, response)
753
+ options[:api_method] = params.shift if params.size > 0
754
+ options[:parameters] = params.shift if params.size > 0
755
+ options[:body] = params.shift if params.size > 0
756
+ options[:headers] = params.shift if params.size > 0
757
+ options[:client] = self
758
+ reference = Google::APIClient::Reference.new(options)
759
+ request = self.generate_request(reference)
760
+ response = self.transmit(
761
+ :request => request,
762
+ :connection => options[:connection]
763
+ )
764
+ return Google::APIClient::Result.new(reference, request, response)
765
+ end
713
766
  end
714
767
 
715
768
  ##
@@ -0,0 +1,296 @@
1
+ # Copyright 2012 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
+ require 'addressable/uri'
16
+ require 'uuidtools'
17
+
18
+ module Google
19
+ class APIClient
20
+
21
+ # Helper class to contain a response to an individual batched call.
22
+ class BatchedCallResponse
23
+ attr_reader :call_id
24
+ attr_accessor :status, :headers, :body
25
+
26
+ def initialize(call_id, status = nil, headers = nil, body = nil)
27
+ @call_id, @status, @headers, @body = call_id, status, headers, body
28
+ end
29
+ end
30
+
31
+ ##
32
+ # Wraps multiple API calls into a single over-the-wire HTTP request.
33
+ class BatchRequest
34
+
35
+ BATCH_BOUNDARY = "-----------RubyApiBatchRequest".freeze
36
+
37
+ attr_accessor :options
38
+ attr_reader :calls, :callbacks
39
+
40
+ ##
41
+ # Creates a new batch request.
42
+ #
43
+ # @param [Hash] options
44
+ # Set of options for this request, the only important one being
45
+ # :connection, which specifies an HTTP connection to use.
46
+ # @param [Proc] block
47
+ # Callback for every call's response. Won't be called if a call defined
48
+ # a callback of its own.
49
+ #
50
+ # @return [Google::APIClient::BatchRequest] The constructed object.
51
+ def initialize(options = {}, &block)
52
+ # Request options, ignoring method and parameters.
53
+ @options = options
54
+ # Batched calls to be made, indexed by call ID.
55
+ @calls = {}
56
+ # Callbacks per batched call, indexed by call ID.
57
+ @callbacks = {}
58
+ # Order for the call IDs, since Ruby 1.8 hashes are unordered.
59
+ @order = []
60
+ # Global callback to be used for every call. If a specific callback
61
+ # has been defined for a request, this won't be called.
62
+ @global_callback = block if block_given?
63
+ # The last auto generated ID.
64
+ @last_auto_id = 0
65
+ # Base ID for the batch request.
66
+ @base_id = nil
67
+ end
68
+
69
+ ##
70
+ # Add a new call to the batch request.
71
+ # Each call must have its own call ID; if not provided, one will
72
+ # automatically be generated, avoiding collisions. If duplicate call IDs
73
+ # are provided, an error will be thrown.
74
+ #
75
+ # @param [Hash, Google::APIClient::Reference] call: the call to be added.
76
+ # @param [String] call_id: the ID to be used for this call. Must be unique
77
+ # @param [Proc] block: callback for this call's response.
78
+ #
79
+ # @return [Google::APIClient::BatchRequest] The BatchRequest, for chaining
80
+ def add(call, call_id = nil, &block)
81
+ unless call.kind_of?(Google::APIClient::Reference)
82
+ call = Google::APIClient::Reference.new(call)
83
+ end
84
+ if call_id.nil?
85
+ call_id = new_id
86
+ end
87
+ if @calls.include?(call_id)
88
+ raise BatchError,
89
+ 'A call with this ID already exists: %s' % call_id
90
+ end
91
+ @calls[call_id] = call
92
+ @order << call_id
93
+ if block_given?
94
+ @callbacks[call_id] = block
95
+ elsif @global_callback
96
+ @callbacks[call_id] = @global_callback
97
+ end
98
+ return self
99
+ end
100
+
101
+ ##
102
+ # Convert this batch request into an HTTP request.
103
+ #
104
+ # @return [Array<String, String, Hash, String>]
105
+ # An array consisting of, in order: HTTP method, request path, request
106
+ # headers and request body.
107
+ def to_http_request
108
+ return ['POST', request_uri, request_headers, request_body]
109
+ end
110
+
111
+ ##
112
+ # Processes the HTTP response to the batch request, issuing callbacks.
113
+ #
114
+ # @param [Faraday::Response] response: the HTTP response.
115
+ def process_response(response)
116
+ content_type = find_header('Content-Type', response.headers)
117
+ boundary = /.*boundary=(.+)/.match(content_type)[1]
118
+ parts = response.body.split(/--#{Regexp.escape(boundary)}/)
119
+ parts = parts[1...-1]
120
+ parts.each do |part|
121
+ call_response = deserialize_call_response(part)
122
+ callback = @callbacks[call_response.call_id]
123
+ call = @calls[call_response.call_id]
124
+ result = Google::APIClient::Result.new(call, nil, call_response)
125
+ callback.call(result) if callback
126
+ end
127
+ end
128
+
129
+ private
130
+
131
+ ##
132
+ # Helper method to find a header from its name, regardless of case.
133
+ #
134
+ # @param [String] name: The name of the header to find.
135
+ # @param [Hash] headers: The hash of headers and their values.
136
+ #
137
+ # @return [String] The value of the desired header.
138
+ def find_header(name, headers)
139
+ _, header = headers.detect do |h, v|
140
+ h.downcase == name.downcase
141
+ end
142
+ return header
143
+ end
144
+
145
+ ##
146
+ # Create a new call ID. Uses an auto-incrementing, conflict-avoiding ID.
147
+ #
148
+ # @return [String] the new, unique ID.
149
+ def new_id
150
+ @last_auto_id += 1
151
+ while @calls.include?(@last_auto_id)
152
+ @last_auto_id += 1
153
+ end
154
+ return @last_auto_id.to_s
155
+ end
156
+
157
+ ##
158
+ # Convert an id to a Content-ID header value.
159
+ #
160
+ # @param [String] call_id: identifier of individual call.
161
+ #
162
+ # @return [String]
163
+ # A Content-ID header with the call_id encoded into it. A UUID is
164
+ # prepended to the value because Content-ID headers are supposed to be
165
+ # universally unique.
166
+ def id_to_header(call_id)
167
+ if @base_id.nil?
168
+ # TODO(sgomes): Use SecureRandom.uuid, drop UUIDTools when we drop 1.8
169
+ @base_id = UUIDTools::UUID.random_create.to_s
170
+ end
171
+
172
+ return '<%s+%s>' % [@base_id, Addressable::URI.encode(call_id)]
173
+ end
174
+
175
+ ##
176
+ # Convert a Content-ID header value to an id. Presumes the Content-ID
177
+ # header conforms to the format that id_to_header() returns.
178
+ #
179
+ # @param [String] header: Content-ID header value.
180
+ #
181
+ # @return [String] The extracted ID value.
182
+ def header_to_id(header)
183
+ if !header.start_with?('<') || !header.end_with?('>') ||
184
+ !header.include?('+')
185
+ raise BatchError, 'Invalid value for Content-ID: "%s"' % header
186
+ end
187
+
188
+ base, call_id = header[1...-1].split('+')
189
+ return Addressable::URI.unencode(call_id)
190
+ end
191
+
192
+ ##
193
+ # Convert a single batched call into a string.
194
+ #
195
+ # @param [Google::APIClient::Reference] call: the call to serialize.
196
+ #
197
+ # @return [String] The request as a string in application/http format.
198
+ def serialize_call(call)
199
+ http_request = call.to_request
200
+ method = http_request.method.to_s.upcase
201
+ path = http_request.path.to_s
202
+ status_line = method + " " + path + " HTTP/1.1"
203
+ serialized_call = status_line
204
+ if http_request.headers
205
+ http_request.headers.each do |header, value|
206
+ serialized_call << "\r\n%s: %s" % [header, value]
207
+ end
208
+ end
209
+ if http_request.body
210
+ serialized_call << "\r\n\r\n"
211
+ serialized_call << http_request.body
212
+ end
213
+ return serialized_call
214
+ end
215
+
216
+ ##
217
+ # Auxiliary method to split the headers from the body in an HTTP response.
218
+ #
219
+ # @param [String] response: the response to parse.
220
+ #
221
+ # @return [Array<Hash>, String] The headers and the body, separately.
222
+ def split_headers_and_body(response)
223
+ headers = {}
224
+ payload = response.lstrip
225
+ while payload
226
+ line, payload = payload.split("\n", 2)
227
+ line.sub!(/\s+\z/, '')
228
+ break if line.empty?
229
+ match = /\A([^:]+):\s*/.match(line)
230
+ if match
231
+ headers[match[1]] = match.post_match
232
+ else
233
+ raise BatchError, 'Invalid header line in response: %s' % line
234
+ end
235
+ end
236
+ return headers, payload
237
+ end
238
+
239
+ ##
240
+ # Convert a single batched response into a BatchedCallResponse object.
241
+ #
242
+ # @param [Google::APIClient::Reference] response:
243
+ # the request to deserialize.
244
+ #
245
+ # @return [BatchedCallResponse] The parsed and converted response.
246
+ def deserialize_call_response(call_response)
247
+ outer_headers, outer_body = split_headers_and_body(call_response)
248
+ status_line, payload = outer_body.split("\n", 2)
249
+ protocol, status, reason = status_line.split(' ', 3)
250
+
251
+ headers, body = split_headers_and_body(payload)
252
+ content_id = find_header('Content-ID', outer_headers)
253
+ call_id = header_to_id(content_id)
254
+ return BatchedCallResponse.new(call_id, status.to_i, headers, body)
255
+ end
256
+
257
+ ##
258
+ # Return the request headers for the BatchRequest's HTTP request.
259
+ #
260
+ # @return [Hash] The HTTP headers.
261
+ def request_headers
262
+ return {
263
+ 'Content-Type' => 'multipart/mixed; boundary=%s' % BATCH_BOUNDARY
264
+ }
265
+ end
266
+
267
+ ##
268
+ # Return the request path for the BatchRequest's HTTP request.
269
+ #
270
+ # @return [String] The request path.
271
+ def request_uri
272
+ if @calls.nil? || @calls.empty?
273
+ raise BatchError, 'Cannot make an empty batch request'
274
+ end
275
+ # All APIs have the same batch path, so just get the first one.
276
+ return @calls.first[1].api_method.api.batch_path
277
+ end
278
+
279
+ ##
280
+ # Return the request body for the BatchRequest's HTTP request.
281
+ #
282
+ # @return [String] The request body.
283
+ def request_body
284
+ body = ""
285
+ @order.each do |call_id|
286
+ body << "--" + BATCH_BOUNDARY + "\r\n"
287
+ body << "Content-Type: application/http\r\n"
288
+ body << "Content-ID: %s\r\n\r\n" % id_to_header(call_id)
289
+ body << serialize_call(@calls[call_id]) + "\r\n\r\n"
290
+ end
291
+ body << "--" + BATCH_BOUNDARY + "--"
292
+ return body
293
+ end
294
+ end
295
+ end
296
+ end