google-api-client 0.4.3 → 0.4.4

Sign up to get free protection for your applications and to get access to all the features.
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