google-apis-core 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,20 @@
1
+ # Copyright 2020 Google LLC
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
+ module Google
16
+ module Apis
17
+ module Core
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,224 @@
1
+ # Copyright 2020 Google LLC
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 'addressable/template'
17
+ require 'google/apis/core/http_command'
18
+ require 'google/apis/errors'
19
+ require 'json'
20
+ require 'retriable'
21
+
22
+ module Google
23
+ module Apis
24
+ module Core
25
+ # Command for executing most basic API request with JSON requests/responses
26
+ class ApiCommand < HttpCommand
27
+ JSON_CONTENT_TYPE = 'application/json'
28
+ FIELDS_PARAM = 'fields'
29
+ ERROR_REASON_MAPPING = {
30
+ 'rateLimitExceeded' => Google::Apis::RateLimitError,
31
+ 'userRateLimitExceeded' => Google::Apis::RateLimitError,
32
+ 'projectNotLinked' => Google::Apis::ProjectNotLinkedError
33
+ }
34
+
35
+ # JSON serializer for request objects
36
+ # @return [Google::Apis::Core::JsonRepresentation]
37
+ attr_accessor :request_representation
38
+
39
+ # Request body to serialize
40
+ # @return [Object]
41
+ attr_accessor :request_object
42
+
43
+ # JSON serializer for response objects
44
+ # @return [Google::Apis::Core::JsonRepresentation]
45
+ attr_accessor :response_representation
46
+
47
+ # Class to instantiate when de-serializing responses
48
+ # @return [Object]
49
+ attr_accessor :response_class
50
+
51
+ # Client library version.
52
+ # @return [String]
53
+ attr_accessor :client_version
54
+
55
+ # @param [symbol] method
56
+ # HTTP method
57
+ # @param [String,Addressable::URI, Addressable::Template] url
58
+ # HTTP URL or template
59
+ # @param [String, #read] body
60
+ # Request body
61
+ def initialize(method, url, body: nil, client_version: nil)
62
+ super(method, url, body: body)
63
+ self.client_version = client_version || '0.0'
64
+ end
65
+
66
+ # Serialize the request body
67
+ #
68
+ # @return [void]
69
+ def prepare!
70
+ set_api_client_header
71
+ set_user_project_header
72
+ if options&.api_format_version
73
+ header['X-Goog-Api-Format-Version'] = options.api_format_version.to_s
74
+ end
75
+ query[FIELDS_PARAM] = normalize_fields_param(query[FIELDS_PARAM]) if query.key?(FIELDS_PARAM)
76
+ if request_representation && request_object
77
+ header['Content-Type'] ||= JSON_CONTENT_TYPE
78
+ if options && options.skip_serialization
79
+ self.body = request_object
80
+ else
81
+ self.body = request_representation.new(request_object).to_json(user_options: { skip_undefined: true })
82
+ end
83
+ end
84
+ super
85
+ end
86
+
87
+ # Deserialize the response body if present
88
+ #
89
+ # @param [String] content_type
90
+ # Content type of body
91
+ # @param [String, #read] body
92
+ # Response body
93
+ # @return [Object]
94
+ # Response object
95
+ # noinspection RubyUnusedLocalVariable
96
+ def decode_response_body(content_type, body)
97
+ return super unless response_representation
98
+ return super if options && options.skip_deserialization
99
+ return super if content_type.nil?
100
+ return nil unless content_type.start_with?(JSON_CONTENT_TYPE)
101
+ body = "{}" if body.empty?
102
+ instance = response_class.new
103
+ response_representation.new(instance).from_json(body, unwrap: response_class)
104
+ instance
105
+ end
106
+
107
+ # Check the response and raise error if needed
108
+ #
109
+ # @param [Fixnum] status
110
+ # HTTP status code of response
111
+ # @param [Hash] header
112
+ # HTTP response headers
113
+ # @param [String] body
114
+ # HTTP response body
115
+ # @param [String] message
116
+ # Error message text
117
+ # @return [void]
118
+ # @raise [Google::Apis::ServerError] An error occurred on the server and the request can be retried
119
+ # @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification
120
+ # @raise [Google::Apis::AuthorizationError] Authorization is required
121
+ def check_status(status, header = nil, body = nil, message = nil)
122
+ case status
123
+ when 400, 402...500
124
+ reason, message = parse_error(body)
125
+ if reason
126
+ message = sprintf('%s: %s', reason, message)
127
+ raise ERROR_REASON_MAPPING[reason].new(
128
+ message,
129
+ status_code: status,
130
+ header: header,
131
+ body: body
132
+ ) if ERROR_REASON_MAPPING.key?(reason)
133
+ end
134
+ super(status, header, body, message)
135
+ else
136
+ super(status, header, body, message)
137
+ end
138
+ end
139
+
140
+ def allow_form_encoding?
141
+ request_representation.nil? && super
142
+ end
143
+
144
+ private
145
+
146
+ def set_api_client_header
147
+ old_xgac = header
148
+ .find_all { |k, v| k.downcase == 'x-goog-api-client' }
149
+ .map { |(a, b)| b }
150
+ .join(' ')
151
+ .split
152
+ .find_all { |s| s !~ %r{^gl-ruby/|^gdcl/} }
153
+ .join(' ')
154
+ xgac = "gl-ruby/#{RUBY_VERSION} gdcl/#{client_version}"
155
+ xgac = old_xgac.empty? ? xgac : "#{old_xgac} #{xgac}"
156
+ header.delete_if { |k, v| k.downcase == 'x-goog-api-client' }
157
+ header['X-Goog-Api-Client'] = xgac
158
+ end
159
+
160
+ def set_user_project_header
161
+ quota_project_id = options.quota_project
162
+ if !quota_project_id && options&.authorization.respond_to?(:quota_project_id)
163
+ quota_project_id = options.authorization.quota_project_id
164
+ end
165
+ header['X-Goog-User-Project'] = quota_project_id if quota_project_id
166
+ end
167
+
168
+ # Attempt to parse a JSON error message
169
+ # @param [String] body
170
+ # HTTP response body
171
+ # @return [Array<(String, String)>]
172
+ # Error reason and message
173
+ def parse_error(body)
174
+ obj = JSON.load(body)
175
+ error = obj['error']
176
+ if error['details']
177
+ return extract_v2_error_details(error)
178
+ elsif error['errors']
179
+ return extract_v1_error_details(error)
180
+ else
181
+ fail 'Can not parse error message. No "details" or "errors" detected'
182
+ end
183
+ rescue
184
+ return [nil, nil]
185
+ end
186
+
187
+ # Extracts details from a v1 error message
188
+ # @param [Hash] error
189
+ # Parsed JSON
190
+ # @return [Array<(String, String)>]
191
+ # Error reason and message
192
+ def extract_v1_error_details(error)
193
+ reason = error['errors'].first['reason']
194
+ message = error['message']
195
+ return [reason, message]
196
+ end
197
+
198
+ # Extracts details from a v2error message
199
+ # @param [Hash] error
200
+ # Parsed JSON
201
+ # @return [Array<(String, String)>]
202
+ # Error reason and message
203
+ def extract_v2_error_details(error)
204
+ reason = error['status']
205
+ message = error['message']
206
+ return [reason, message]
207
+ end
208
+
209
+ # Convert field names from ruby conventions to original names in JSON
210
+ #
211
+ # @param [String] fields
212
+ # Value of 'fields' param
213
+ # @return [String]
214
+ # Updated header value
215
+ def normalize_fields_param(fields)
216
+ # TODO: Generate map of parameter names during code gen. Small possibility that camelization fails
217
+ fields.gsub(/:/, '').gsub(/\w+/) do |str|
218
+ str.gsub(/(?:^|_)([a-z])/){ Regexp.last_match.begin(0) == 0 ? $1 : $1.upcase }
219
+ end
220
+ end
221
+ end
222
+ end
223
+ end
224
+ end
@@ -0,0 +1,459 @@
1
+ # Copyright 2020 Google LLC
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 'addressable/template'
17
+ require 'google/apis'
18
+ require 'google/apis/core/version'
19
+ require 'google/apis/core/api_command'
20
+ require 'google/apis/core/batch'
21
+ require 'google/apis/core/upload'
22
+ require 'google/apis/core/download'
23
+ require 'google/apis/options'
24
+ require 'googleauth'
25
+ require 'httpclient'
26
+
27
+ module Google
28
+ module Apis
29
+ module Core
30
+ # Helper class for enumerating over a result set requiring multiple fetches
31
+ class PagedResults
32
+ include Enumerable
33
+
34
+ attr_reader :last_result
35
+
36
+ # @param [BaseService] service
37
+ # Current service instance
38
+ # @param [Fixnum] max
39
+ # Maximum number of items to iterate over. Nil if no limit
40
+ # @param [Boolean] cache
41
+ # True (default) if results should be cached so multiple iterations can be used.
42
+ # @param [Symbol] items
43
+ # Name of the field in the result containing the items. Defaults to :items
44
+ def initialize(service, max: nil, items: :items, cache: true, response_page_token: :next_page_token, &block)
45
+ @service = service
46
+ @block = block
47
+ @max = max
48
+ @items_field = items
49
+ @response_page_token_field = response_page_token
50
+ if cache
51
+ @result_cache = Hash.new do |h, k|
52
+ h[k] = @block.call(k, @service)
53
+ end
54
+ @fetch_proc = Proc.new { |token| @result_cache[token] }
55
+ else
56
+ @fetch_proc = Proc.new { |token| @block.call(token, @service) }
57
+ end
58
+ end
59
+
60
+ # Iterates over result set, fetching additional pages as needed
61
+ def each
62
+ page_token = nil
63
+ item_count = 0
64
+ loop do
65
+ @last_result = @fetch_proc.call(page_token)
66
+ items = @last_result.send(@items_field)
67
+ if items.kind_of?(Array)
68
+ for item in items
69
+ item_count = item_count + 1
70
+ break if @max && item_count > @max
71
+ yield item
72
+ end
73
+ elsif items.kind_of?(Hash)
74
+ items.each do |key, val|
75
+ item_count = item_count + 1
76
+ break if @max && item_count > @max
77
+ yield key, val
78
+ end
79
+ elsif items
80
+ # yield singular non-nil items (for genomics API)
81
+ yield items
82
+ end
83
+ break if @max && item_count >= @max
84
+ next_page_token = @last_result.send(@response_page_token_field)
85
+ break if next_page_token.nil? || next_page_token == page_token
86
+ page_token = next_page_token
87
+ end
88
+ end
89
+ end
90
+
91
+ # Base service for all APIs. Not to be used directly.
92
+ #
93
+ class BaseService
94
+ include Logging
95
+
96
+ # Root URL (host/port) for the API
97
+ # @return [Addressable::URI]
98
+ attr_accessor :root_url
99
+
100
+ # Additional path prefix for all API methods
101
+ # @return [Addressable::URI]
102
+ attr_accessor :base_path
103
+
104
+ # Alternate path prefix for media uploads
105
+ # @return [Addressable::URI]
106
+ attr_accessor :upload_path
107
+
108
+ # Alternate path prefix for all batch methods
109
+ # @return [Addressable::URI]
110
+ attr_accessor :batch_path
111
+
112
+ # HTTP client
113
+ # @return [HTTPClient]
114
+ attr_accessor :client
115
+
116
+ # General settings
117
+ # @return [Google::Apis::ClientOptions]
118
+ attr_accessor :client_options
119
+
120
+ # Default options for all requests
121
+ # @return [Google::Apis::RequestOptions]
122
+ attr_accessor :request_options
123
+
124
+ # Client library name.
125
+ # @return [String]
126
+ attr_accessor :client_name
127
+
128
+ # Client library version.
129
+ # @return [String]
130
+ attr_accessor :client_version
131
+
132
+ # @param [String,Addressable::URI] root_url
133
+ # Root URL for the API
134
+ # @param [String,Addressable::URI] base_path
135
+ # Additional path prefix for all API methods
136
+ # @api private
137
+ def initialize(root_url, base_path, client_name: nil, client_version: nil)
138
+ self.root_url = root_url
139
+ self.base_path = base_path
140
+ self.client_name = client_name || 'google-api-ruby-client'
141
+ self.client_version = client_version || Google::Apis::Core::VERSION
142
+ self.upload_path = "upload/#{base_path}"
143
+ self.batch_path = 'batch'
144
+ self.client_options = Google::Apis::ClientOptions.default.dup
145
+ self.request_options = Google::Apis::RequestOptions.default.dup
146
+ end
147
+
148
+ # @!attribute [rw] authorization
149
+ # @return [Signet::OAuth2::Client]
150
+ # OAuth2 credentials
151
+ def authorization=(authorization)
152
+ request_options.authorization = authorization
153
+ end
154
+
155
+ def authorization
156
+ request_options.authorization
157
+ end
158
+
159
+ # TODO: with(options) method
160
+
161
+ # Perform a batch request. Calls made within the block are sent in a single network
162
+ # request to the server.
163
+ #
164
+ # @example
165
+ # service.batch do |s|
166
+ # s.get_item(id1) do |res, err|
167
+ # # process response for 1st call
168
+ # end
169
+ # # ...
170
+ # s.get_item(idN) do |res, err|
171
+ # # process response for Nth call
172
+ # end
173
+ # end
174
+ #
175
+ # @param [Hash, Google::Apis::RequestOptions] options
176
+ # Request-specific options
177
+ # @yield [self]
178
+ # @return [void]
179
+ def batch(options = nil)
180
+ batch_command = BatchCommand.new(:post, Addressable::URI.parse(root_url + batch_path))
181
+ batch_command.options = request_options.merge(options)
182
+ apply_command_defaults(batch_command)
183
+ begin
184
+ start_batch(batch_command)
185
+ yield self
186
+ ensure
187
+ end_batch
188
+ end
189
+ batch_command.execute(client)
190
+ end
191
+
192
+ # Perform a batch upload request. Calls made within the block are sent in a single network
193
+ # request to the server. Batch uploads are useful for uploading multiple small files. For larger
194
+ # files, use single requests which use a resumable upload protocol.
195
+ #
196
+ # @example
197
+ # service.batch do |s|
198
+ # s.insert_item(upload_source: 'file1.txt') do |res, err|
199
+ # # process response for 1st call
200
+ # end
201
+ # # ...
202
+ # s.insert_item(upload_source: 'fileN.txt') do |res, err|
203
+ # # process response for Nth call
204
+ # end
205
+ # end
206
+ #
207
+ # @param [Hash, Google::Apis::RequestOptions] options
208
+ # Request-specific options
209
+ # @yield [self]
210
+ # @return [void]
211
+ def batch_upload(options = nil)
212
+ batch_command = BatchUploadCommand.new(:put, Addressable::URI.parse(root_url + upload_path))
213
+ batch_command.options = request_options.merge(options)
214
+ apply_command_defaults(batch_command)
215
+ begin
216
+ start_batch(batch_command)
217
+ yield self
218
+ ensure
219
+ end_batch
220
+ end
221
+ batch_command.execute(client)
222
+ end
223
+
224
+ # Get the current HTTP client
225
+ # @return [HTTPClient]
226
+ def client
227
+ @client ||= new_client
228
+ end
229
+
230
+
231
+ # Simple escape hatch for making API requests directly to a given
232
+ # URL. This is not intended to be used as a generic HTTP client
233
+ # and should be used only in cases where no service method exists
234
+ # (e.g. fetching an export link for a Google Drive file.)
235
+ #
236
+ # @param [Symbol] method
237
+ # HTTP method as symbol (e.g. :get, :post, :put, ...)
238
+ # @param [String] url
239
+ # URL to call
240
+ # @param [Hash<String,String>] params
241
+ # Optional hash of query parameters
242
+ # @param [#read] body
243
+ # Optional body for POST/PUT
244
+ # @param [IO, String] download_dest
245
+ # IO stream or filename to receive content download
246
+ # @param [Google::Apis::RequestOptions] options
247
+ # Request-specific options
248
+ #
249
+ # @yield [result, err] Result & error if block supplied
250
+ # @yieldparam result [String] HTTP response body
251
+ # @yieldparam err [StandardError] error object if request failed
252
+ #
253
+ # @return [String] HTTP response body
254
+ def http(method, url, params: nil, body: nil, download_dest: nil, options: nil, &block)
255
+ if download_dest
256
+ command = DownloadCommand.new(method, url, body: body, client_version: client_version)
257
+ else
258
+ command = HttpCommand.new(method, url, body: body)
259
+ end
260
+ command.options = request_options.merge(options)
261
+ apply_command_defaults(command)
262
+ command.query.merge(Hash(params))
263
+ execute_or_queue_command(command, &block)
264
+ end
265
+
266
+ # Executes a given query with paging, automatically retrieving
267
+ # additional pages as necessary. Requires a block that returns the
268
+ # result set of a page. The current page token is supplied as an argument
269
+ # to the block.
270
+ #
271
+ # Note: The returned enumerable also contains a `last_result` field
272
+ # containing the full result of the last query executed.
273
+ #
274
+ # @param [Fixnum] max
275
+ # Maximum number of items to iterate over. Defaults to nil -- no upper bound.
276
+ # @param [Symbol] items
277
+ # Name of the field in the result containing the items. Defaults to :items
278
+ # @param [Boolean] cache
279
+ # True (default) if results should be cached so multiple iterations can be used.
280
+ # @return [Enumerble]
281
+ # @yield [token, service]
282
+ # Current page token & service instance
283
+ # @yieldparam [String] token
284
+ # Current page token to be used in the query
285
+ # @yieldparam [service]
286
+ # Current service instance
287
+ # @since 0.9.4
288
+ #
289
+ # @example Retrieve all files,
290
+ # file_list = service.fetch_all { |token, s| s.list_files(page_token: token) }
291
+ # file_list.each { |f| ... }
292
+ def fetch_all(max: nil, items: :items, cache: true, response_page_token: :next_page_token, &block)
293
+ fail "fetch_all may not be used inside a batch" if batch?
294
+ return PagedResults.new(self, max: max, items: items, cache: cache, response_page_token: response_page_token, &block)
295
+ end
296
+
297
+ protected
298
+
299
+ # Create a new upload command.
300
+ #
301
+ # @param [symbol] method
302
+ # HTTP method for uploading (typically :put or :post)
303
+ # @param [String] path
304
+ # Additional path to upload endpoint, appended to API base path
305
+ # @param [Hash, Google::Apis::RequestOptions] options
306
+ # Request-specific options
307
+ # @return [Google::Apis::Core::UploadCommand]
308
+ def make_upload_command(method, path, options)
309
+ template = Addressable::Template.new(root_url + upload_path + path)
310
+ if batch?
311
+ command = MultipartUploadCommand.new(method, template, client_version: client_version)
312
+ else
313
+ command = ResumableUploadCommand.new(method, template, client_version: client_version)
314
+ end
315
+ command.options = request_options.merge(options)
316
+ apply_command_defaults(command)
317
+ command
318
+ end
319
+
320
+ # Create a new download command.
321
+ #
322
+ # @param [symbol] method
323
+ # HTTP method for uploading (typically :get)
324
+ # @param [String] path
325
+ # Additional path to download endpoint, appended to API base path
326
+ # @param [Hash, Google::Apis::RequestOptions] options
327
+ # Request-specific options
328
+ # @return [Google::Apis::Core::DownloadCommand]
329
+ def make_download_command(method, path, options)
330
+ template = Addressable::Template.new(root_url + base_path + path)
331
+ command = DownloadCommand.new(method, template, client_version: client_version)
332
+ command.options = request_options.merge(options)
333
+ command.query['alt'] = 'media'
334
+ apply_command_defaults(command)
335
+ command
336
+ end
337
+
338
+ # Create a new command.
339
+ #
340
+ # @param [symbol] method
341
+ # HTTP method (:get, :post, :delete, etc...)
342
+ # @param [String] path
343
+ # Additional path, appended to API base path
344
+ # @param [Hash, Google::Apis::RequestOptions] options
345
+ # Request-specific options
346
+ # @return [Google::Apis::Core::DownloadCommand]
347
+ def make_simple_command(method, path, options)
348
+ full_path =
349
+ if path.start_with? "/"
350
+ path[1..-1]
351
+ else
352
+ base_path + path
353
+ end
354
+ template = Addressable::Template.new(root_url + full_path)
355
+ command = ApiCommand.new(method, template, client_version: client_version)
356
+ command.options = request_options.merge(options)
357
+ apply_command_defaults(command)
358
+ command
359
+ end
360
+
361
+ # Execute the request. If a batch is in progress, the request is added to the batch instead.
362
+ #
363
+ # @param [Google::Apis::Core::HttpCommand] command
364
+ # Command to execute
365
+ # @return [Object] response object if command executed and no callback supplied
366
+ # @yield [result, err] Result & error if block supplied
367
+ # @raise [Google::Apis::ServerError] An error occurred on the server and the request can be retried
368
+ # @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification
369
+ # @raise [Google::Apis::AuthorizationError] Authorization is required
370
+ def execute_or_queue_command(command, &callback)
371
+ batch_command = current_batch
372
+ if batch_command
373
+ fail "Can not combine services in a batch" if Thread.current[:google_api_batch_service] != self
374
+ batch_command.add(command, &callback)
375
+ nil
376
+ else
377
+ command.execute(client, &callback)
378
+ end
379
+ end
380
+
381
+ # Update commands with service-specific options. To be implemented by subclasses
382
+ # @param [Google::Apis::Core::HttpCommand] _command
383
+ def apply_command_defaults(_command)
384
+ end
385
+
386
+ private
387
+
388
+ # Get the current batch context
389
+ #
390
+ # @return [Google:Apis::Core::BatchRequest]
391
+ def current_batch
392
+ Thread.current[:google_api_batch]
393
+ end
394
+
395
+ # Check if a batch is in progress
396
+ # @return [Boolean]
397
+ def batch?
398
+ !current_batch.nil?
399
+ end
400
+
401
+ # Start a new thread-local batch context
402
+ # @param [Google::Apis::Core::BatchCommand] cmd
403
+ def start_batch(cmd)
404
+ fail "Batch already in progress" if batch?
405
+ Thread.current[:google_api_batch] = cmd
406
+ Thread.current[:google_api_batch_service] = self
407
+ end
408
+
409
+ # Clear thread-local batch context
410
+ def end_batch
411
+ Thread.current[:google_api_batch] = nil
412
+ Thread.current[:google_api_batch_service] = nil
413
+ end
414
+
415
+ # Create a new HTTP client
416
+ # @return [HTTPClient]
417
+ def new_client
418
+ client = ::HTTPClient.new
419
+
420
+ if client_options.transparent_gzip_decompression
421
+ client.transparent_gzip_decompression = client_options.transparent_gzip_decompression
422
+ end
423
+
424
+ client.proxy = client_options.proxy_url if client_options.proxy_url
425
+
426
+ if client_options.open_timeout_sec
427
+ client.connect_timeout = client_options.open_timeout_sec
428
+ end
429
+
430
+ if client_options.read_timeout_sec
431
+ client.receive_timeout = client_options.read_timeout_sec
432
+ end
433
+
434
+ if client_options.send_timeout_sec
435
+ client.send_timeout = client_options.send_timeout_sec
436
+ end
437
+
438
+ client.follow_redirect_count = 5
439
+ client.default_header = { 'User-Agent' => user_agent }
440
+
441
+ client.debug_dev = logger if client_options.log_http_requests
442
+ client
443
+ end
444
+
445
+
446
+ # Build the user agent header
447
+ # @return [String]
448
+ def user_agent
449
+ sprintf('%s/%s %s/%s %s (gzip)',
450
+ client_options.application_name,
451
+ client_options.application_version,
452
+ client_name,
453
+ client_version,
454
+ Google::Apis::OS_VERSION)
455
+ end
456
+ end
457
+ end
458
+ end
459
+ end