442 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Ruby
		
	
	
	
			
		
		
	
	
			442 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Ruby
		
	
	
	
# Copyright 2015 Google Inc.
 | 
						|
#
 | 
						|
# Licensed under the Apache License, Version 2.0 (the "License");
 | 
						|
# you may not use this file except in compliance with the License.
 | 
						|
# You may obtain a copy of the License at
 | 
						|
#
 | 
						|
#      http://www.apache.org/licenses/LICENSE-2.0
 | 
						|
#
 | 
						|
# Unless required by applicable law or agreed to in writing, software
 | 
						|
# distributed under the License is distributed on an "AS IS" BASIS,
 | 
						|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
						|
# See the License for the specific language governing permissions and
 | 
						|
# limitations under the License.
 | 
						|
 | 
						|
require 'addressable/uri'
 | 
						|
require 'addressable/template'
 | 
						|
require 'google/apis/version'
 | 
						|
require 'google/apis/core/api_command'
 | 
						|
require 'google/apis/core/batch'
 | 
						|
require 'google/apis/core/upload'
 | 
						|
require 'google/apis/core/download'
 | 
						|
require 'google/apis/options'
 | 
						|
require 'googleauth'
 | 
						|
require 'httpclient'
 | 
						|
 | 
						|
module Google
 | 
						|
  module Apis
 | 
						|
    module Core
 | 
						|
      # Helper class for enumerating over a result set requiring multiple fetches
 | 
						|
      class PagedResults
 | 
						|
        include Enumerable
 | 
						|
 | 
						|
        attr_reader :last_result
 | 
						|
 | 
						|
        # @param [BaseService] service
 | 
						|
        #   Current service instance
 | 
						|
        # @param [Fixnum] max
 | 
						|
        #   Maximum number of items to iterate over. Nil if no limit
 | 
						|
        # @param [Boolean] cache
 | 
						|
        #  True (default) if results should be cached so multiple iterations can be used.
 | 
						|
        # @param [Symbol] items
 | 
						|
        #   Name of the field in the result containing the items. Defaults to :items
 | 
						|
        def initialize(service, max: nil, items: :items, cache: true, response_page_token: :next_page_token, &block)
 | 
						|
          @service = service
 | 
						|
          @block = block
 | 
						|
          @max = max
 | 
						|
          @items_field = items
 | 
						|
          @response_page_token_field = response_page_token
 | 
						|
          if cache
 | 
						|
            @result_cache = Hash.new do |h, k|
 | 
						|
              h[k] = @block.call(k, @service)
 | 
						|
            end
 | 
						|
            @fetch_proc = Proc.new { |token| @result_cache[token] }
 | 
						|
          else
 | 
						|
            @fetch_proc = Proc.new { |token| @block.call(token, @service) }
 | 
						|
          end
 | 
						|
        end
 | 
						|
 | 
						|
        # Iterates over result set, fetching additional pages as needed
 | 
						|
        def each
 | 
						|
          page_token = nil
 | 
						|
          item_count = 0
 | 
						|
          loop do
 | 
						|
            @last_result = @fetch_proc.call(page_token)
 | 
						|
            items = @last_result.send(@items_field)
 | 
						|
            if items.kind_of?(Array)
 | 
						|
              for item in items
 | 
						|
                item_count = item_count + 1
 | 
						|
                break if @max && item_count > @max
 | 
						|
                yield item
 | 
						|
              end
 | 
						|
            elsif items.kind_of?(Hash)
 | 
						|
              items.each do |key, val|
 | 
						|
                item_count = item_count + 1
 | 
						|
                break if @max && item_count > @max
 | 
						|
                yield key, val
 | 
						|
              end
 | 
						|
            elsif items
 | 
						|
              # yield singular non-nil items (for genomics API)
 | 
						|
              yield items
 | 
						|
            end
 | 
						|
            break if @max && item_count >= @max
 | 
						|
            next_page_token = @last_result.send(@response_page_token_field)
 | 
						|
            break if next_page_token.nil? || next_page_token == page_token
 | 
						|
            page_token = next_page_token
 | 
						|
          end
 | 
						|
        end
 | 
						|
      end
 | 
						|
 | 
						|
      # Base service for all APIs. Not to be used directly.
 | 
						|
      #
 | 
						|
      class BaseService
 | 
						|
        include Logging
 | 
						|
 | 
						|
        # Root URL (host/port) for the API
 | 
						|
        # @return [Addressable::URI]
 | 
						|
        attr_accessor :root_url
 | 
						|
 | 
						|
        # Additional path prefix for all API methods
 | 
						|
        # @return [Addressable::URI]
 | 
						|
        attr_accessor :base_path
 | 
						|
 | 
						|
        # Alternate path prefix for media uploads
 | 
						|
        # @return [Addressable::URI]
 | 
						|
        attr_accessor :upload_path
 | 
						|
 | 
						|
        # Alternate path prefix for all batch methods
 | 
						|
        # @return [Addressable::URI]
 | 
						|
        attr_accessor :batch_path
 | 
						|
 | 
						|
        # HTTP client
 | 
						|
        # @return [HTTPClient]
 | 
						|
        attr_accessor :client
 | 
						|
 | 
						|
        # General settings
 | 
						|
        # @return [Google::Apis::ClientOptions]
 | 
						|
        attr_accessor :client_options
 | 
						|
 | 
						|
        # Default options for all requests
 | 
						|
        # @return [Google::Apis::RequestOptions]
 | 
						|
        attr_accessor :request_options
 | 
						|
 | 
						|
        # @param [String,Addressable::URI] root_url
 | 
						|
        #   Root URL for the API
 | 
						|
        # @param [String,Addressable::URI] base_path
 | 
						|
        #   Additional path prefix for all API methods
 | 
						|
        # @api private
 | 
						|
        def initialize(root_url, base_path)
 | 
						|
          self.root_url = root_url
 | 
						|
          self.base_path = base_path
 | 
						|
          self.upload_path = "upload/#{base_path}"
 | 
						|
          self.batch_path = 'batch'
 | 
						|
          self.client_options = Google::Apis::ClientOptions.default.dup
 | 
						|
          self.request_options = Google::Apis::RequestOptions.default.dup
 | 
						|
        end
 | 
						|
 | 
						|
        # @!attribute [rw] authorization
 | 
						|
        # @return [Signet::OAuth2::Client]
 | 
						|
        #  OAuth2 credentials
 | 
						|
        def authorization=(authorization)
 | 
						|
          request_options.authorization = authorization
 | 
						|
        end
 | 
						|
 | 
						|
        def authorization
 | 
						|
          request_options.authorization
 | 
						|
        end
 | 
						|
 | 
						|
        # TODO: with(options) method
 | 
						|
 | 
						|
        # Perform a batch request. Calls made within the block are sent in a single network
 | 
						|
        # request to the server.
 | 
						|
        #
 | 
						|
        # @example
 | 
						|
        #   service.batch do |s|
 | 
						|
        #     s.get_item(id1) do |res, err|
 | 
						|
        #       # process response for 1st call
 | 
						|
        #     end
 | 
						|
        #     # ...
 | 
						|
        #     s.get_item(idN) do |res, err|
 | 
						|
        #       # process response for Nth call
 | 
						|
        #     end
 | 
						|
        #   end
 | 
						|
        #
 | 
						|
        # @param [Hash, Google::Apis::RequestOptions] options
 | 
						|
        #  Request-specific options
 | 
						|
        # @yield [self]
 | 
						|
        # @return [void]
 | 
						|
        def batch(options = nil)
 | 
						|
          batch_command = BatchCommand.new(:post, Addressable::URI.parse(root_url + batch_path))
 | 
						|
          batch_command.options = request_options.merge(options)
 | 
						|
          apply_command_defaults(batch_command)
 | 
						|
          begin
 | 
						|
            start_batch(batch_command)
 | 
						|
            yield self
 | 
						|
          ensure
 | 
						|
            end_batch
 | 
						|
          end
 | 
						|
          batch_command.execute(client)
 | 
						|
        end
 | 
						|
 | 
						|
        # Perform a batch upload request. Calls made within the block are sent in a single network
 | 
						|
        # request to the server. Batch uploads are useful for uploading multiple small files. For larger
 | 
						|
        # files, use single requests which use a resumable upload protocol.
 | 
						|
        #
 | 
						|
        # @example
 | 
						|
        #   service.batch do |s|
 | 
						|
        #     s.insert_item(upload_source: 'file1.txt') do |res, err|
 | 
						|
        #       # process response for 1st call
 | 
						|
        #     end
 | 
						|
        #     # ...
 | 
						|
        #     s.insert_item(upload_source: 'fileN.txt') do |res, err|
 | 
						|
        #       # process response for Nth call
 | 
						|
        #     end
 | 
						|
        #   end
 | 
						|
        #
 | 
						|
        # @param [Hash, Google::Apis::RequestOptions] options
 | 
						|
        #  Request-specific options
 | 
						|
        # @yield [self]
 | 
						|
        # @return [void]
 | 
						|
        def batch_upload(options = nil)
 | 
						|
          batch_command = BatchUploadCommand.new(:put, Addressable::URI.parse(root_url + upload_path))
 | 
						|
          batch_command.options = request_options.merge(options)
 | 
						|
          apply_command_defaults(batch_command)
 | 
						|
          begin
 | 
						|
            start_batch(batch_command)
 | 
						|
            yield self
 | 
						|
          ensure
 | 
						|
            end_batch
 | 
						|
          end
 | 
						|
          batch_command.execute(client)
 | 
						|
        end
 | 
						|
 | 
						|
        # Get the current HTTP client
 | 
						|
        # @return [HTTPClient]
 | 
						|
        def client
 | 
						|
          @client ||= new_client
 | 
						|
        end
 | 
						|
 | 
						|
 | 
						|
        # Simple escape hatch for making API requests directly to a given
 | 
						|
        # URL. This is not intended to be used as a generic HTTP client
 | 
						|
        # and should be used only in cases where no service method exists
 | 
						|
        # (e.g. fetching an export link for a Google Drive file.)
 | 
						|
        #
 | 
						|
        # @param [Symbol] method
 | 
						|
        #   HTTP method as symbol (e.g. :get, :post, :put, ...)
 | 
						|
        # @param [String] url
 | 
						|
        #   URL to call
 | 
						|
        # @param [Hash<String,String>] params
 | 
						|
        #   Optional hash of query parameters
 | 
						|
        # @param [#read] body
 | 
						|
        #   Optional body for POST/PUT
 | 
						|
        # @param [IO, String] download_dest
 | 
						|
        #   IO stream or filename to receive content download
 | 
						|
        # @param [Google::Apis::RequestOptions] options
 | 
						|
        #   Request-specific options
 | 
						|
        #
 | 
						|
        # @yield [result, err] Result & error if block supplied
 | 
						|
        # @yieldparam result [String] HTTP response body
 | 
						|
        # @yieldparam err [StandardError] error object if request failed
 | 
						|
        #
 | 
						|
        # @return [String] HTTP response body
 | 
						|
        def http(method, url, params: nil, body: nil, download_dest: nil, options: nil, &block)
 | 
						|
          if download_dest
 | 
						|
            command = DownloadCommand.new(method, url, body: body)
 | 
						|
          else
 | 
						|
            command = HttpCommand.new(method, url, body: body)
 | 
						|
          end
 | 
						|
          command.options = request_options.merge(options)
 | 
						|
          apply_command_defaults(command)
 | 
						|
          command.query.merge(Hash(params))
 | 
						|
          execute_or_queue_command(command, &block)
 | 
						|
        end
 | 
						|
 | 
						|
        # Executes a given query with paging, automatically retrieving
 | 
						|
        # additional pages as necessary. Requires a block that returns the
 | 
						|
        # result set of a page. The current page token is supplied as an argument
 | 
						|
        # to the block.
 | 
						|
        #
 | 
						|
        # Note: The returned enumerable also contains a `last_result` field
 | 
						|
        # containing the full result of the last query executed.
 | 
						|
        #
 | 
						|
        # @param [Fixnum] max
 | 
						|
        #   Maximum number of items to iterate over. Defaults to nil -- no upper bound.
 | 
						|
        # @param [Symbol] items
 | 
						|
        #   Name of the field in the result containing the items. Defaults to :items
 | 
						|
        # @param [Boolean] cache
 | 
						|
        #  True (default) if results should be cached so multiple iterations can be used.
 | 
						|
        # @return [Enumerble]
 | 
						|
        # @yield [token, service]
 | 
						|
        #   Current page token & service instance
 | 
						|
        # @yieldparam [String] token
 | 
						|
        #   Current page token to be used in the query
 | 
						|
        # @yieldparm [service]
 | 
						|
        #   Current service instance
 | 
						|
        # @since 0.9.4
 | 
						|
        #
 | 
						|
        # @example Retrieve all files,
 | 
						|
        #   file_list = service.fetch_all { |token, s| s.list_files(page_token: token) }
 | 
						|
        #   file_list.each { |f| ... }
 | 
						|
        def fetch_all(max: nil, items: :items, cache: true, response_page_token: :next_page_token, &block)
 | 
						|
          fail "fetch_all may not be used inside a batch" if batch?
 | 
						|
          return PagedResults.new(self, max: max, items: items, cache: cache, response_page_token: response_page_token, &block)
 | 
						|
        end
 | 
						|
 | 
						|
        protected
 | 
						|
 | 
						|
        # Create a new upload command.
 | 
						|
        #
 | 
						|
        # @param [symbol] method
 | 
						|
        #   HTTP method for uploading (typically :put or :post)
 | 
						|
        # @param [String] path
 | 
						|
        #  Additional path to upload endpoint, appended to API base path
 | 
						|
        # @param [Hash, Google::Apis::RequestOptions] options
 | 
						|
        #  Request-specific options
 | 
						|
        # @return [Google::Apis::Core::UploadCommand]
 | 
						|
        def make_upload_command(method, path, options)
 | 
						|
          template = Addressable::Template.new(root_url + upload_path + path)
 | 
						|
          if batch?
 | 
						|
            command = MultipartUploadCommand.new(method, template)
 | 
						|
          else
 | 
						|
            command = ResumableUploadCommand.new(method, template)
 | 
						|
          end
 | 
						|
          command.options = request_options.merge(options)
 | 
						|
          apply_command_defaults(command)
 | 
						|
          command
 | 
						|
        end
 | 
						|
 | 
						|
        # Create a new download command.
 | 
						|
        #
 | 
						|
        # @param [symbol] method
 | 
						|
        #   HTTP method for uploading (typically :get)
 | 
						|
        # @param [String] path
 | 
						|
        #  Additional path to download endpoint, appended to API base path
 | 
						|
        # @param [Hash, Google::Apis::RequestOptions] options
 | 
						|
        #  Request-specific options
 | 
						|
        # @return [Google::Apis::Core::DownloadCommand]
 | 
						|
        def make_download_command(method, path, options)
 | 
						|
          template = Addressable::Template.new(root_url + base_path + path)
 | 
						|
          command = DownloadCommand.new(method, template)
 | 
						|
          command.options = request_options.merge(options)
 | 
						|
          command.query['alt'] = 'media'
 | 
						|
          apply_command_defaults(command)
 | 
						|
          command
 | 
						|
        end
 | 
						|
 | 
						|
        # Create a new command.
 | 
						|
        #
 | 
						|
        # @param [symbol] method
 | 
						|
        #   HTTP method (:get, :post, :delete, etc...)
 | 
						|
        # @param [String] path
 | 
						|
        #  Additional path, appended to API base path
 | 
						|
        # @param [Hash, Google::Apis::RequestOptions] options
 | 
						|
        #  Request-specific options
 | 
						|
        # @return [Google::Apis::Core::DownloadCommand]
 | 
						|
        def make_simple_command(method, path, options)
 | 
						|
          template = Addressable::Template.new(root_url + base_path + path)
 | 
						|
          command = ApiCommand.new(method, template)
 | 
						|
          command.options = request_options.merge(options)
 | 
						|
          apply_command_defaults(command)
 | 
						|
          command
 | 
						|
        end
 | 
						|
 | 
						|
        # Execute the request. If a batch is in progress, the request is added to the batch instead.
 | 
						|
        #
 | 
						|
        # @param [Google::Apis::Core::HttpCommand] command
 | 
						|
        #   Command to execute
 | 
						|
        # @return [Object] response object if command executed and no callback supplied
 | 
						|
        # @yield [result, err] Result & error if block supplied
 | 
						|
        # @raise [Google::Apis::ServerError] An error occurred on the server and the request can be retried
 | 
						|
        # @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification
 | 
						|
        # @raise [Google::Apis::AuthorizationError] Authorization is required
 | 
						|
        def execute_or_queue_command(command, &callback)
 | 
						|
          batch_command = current_batch
 | 
						|
          if batch_command
 | 
						|
            fail "Can not combine services in a batch" if Thread.current[:google_api_batch_service] != self
 | 
						|
            batch_command.add(command, &callback)
 | 
						|
            nil
 | 
						|
          else
 | 
						|
            command.execute(client, &callback)
 | 
						|
          end
 | 
						|
        end
 | 
						|
 | 
						|
        # Update commands with service-specific options. To be implemented by subclasses
 | 
						|
        # @param [Google::Apis::Core::HttpCommand] _command
 | 
						|
        def apply_command_defaults(_command)
 | 
						|
        end
 | 
						|
 | 
						|
        private
 | 
						|
 | 
						|
        # Get the current batch context
 | 
						|
        #
 | 
						|
        # @return [Google:Apis::Core::BatchRequest]
 | 
						|
        def current_batch
 | 
						|
          Thread.current[:google_api_batch]
 | 
						|
        end
 | 
						|
 | 
						|
        # Check if a batch is in progress
 | 
						|
        # @return [Boolean]
 | 
						|
        def batch?
 | 
						|
          !current_batch.nil?
 | 
						|
        end
 | 
						|
 | 
						|
        # Start a new thread-local batch context
 | 
						|
        # @param [Google::Apis::Core::BatchCommand] cmd
 | 
						|
        def start_batch(cmd)
 | 
						|
          fail "Batch already in progress" if batch?
 | 
						|
          Thread.current[:google_api_batch] = cmd
 | 
						|
          Thread.current[:google_api_batch_service] = self
 | 
						|
        end
 | 
						|
 | 
						|
        # Clear thread-local batch context
 | 
						|
        def end_batch
 | 
						|
          Thread.current[:google_api_batch] = nil
 | 
						|
          Thread.current[:google_api_batch_service] = nil
 | 
						|
        end
 | 
						|
 | 
						|
        # Create a new HTTP client
 | 
						|
        # @return [HTTPClient]
 | 
						|
        def new_client
 | 
						|
          client = ::HTTPClient.new
 | 
						|
 | 
						|
          if client_options.transparent_gzip_decompression
 | 
						|
            client.transparent_gzip_decompression = client_options.transparent_gzip_decompression
 | 
						|
          end
 | 
						|
          
 | 
						|
          client.proxy = client_options.proxy_url if client_options.proxy_url
 | 
						|
 | 
						|
          if client_options.open_timeout_sec
 | 
						|
            client.connect_timeout = client_options.open_timeout_sec
 | 
						|
          end
 | 
						|
 | 
						|
          if client_options.read_timeout_sec
 | 
						|
            client.receive_timeout = client_options.read_timeout_sec
 | 
						|
          end
 | 
						|
 | 
						|
          if client_options.send_timeout_sec
 | 
						|
            client.send_timeout = client_options.send_timeout_sec
 | 
						|
          end
 | 
						|
 | 
						|
          client.follow_redirect_count = 5
 | 
						|
          client.default_header = { 'User-Agent' => user_agent }
 | 
						|
 | 
						|
          client.debug_dev = logger if client_options.log_http_requests
 | 
						|
          client
 | 
						|
        end
 | 
						|
 | 
						|
 | 
						|
        # Build the user agent header
 | 
						|
        # @return [String]
 | 
						|
        def user_agent
 | 
						|
          sprintf('%s/%s google-api-ruby-client/%s %s (gzip)',
 | 
						|
                  client_options.application_name,
 | 
						|
                  client_options.application_version,
 | 
						|
                  Google::Apis::VERSION,
 | 
						|
                  Google::Apis::OS_VERSION)
 | 
						|
        end
 | 
						|
      end
 | 
						|
    end
 | 
						|
  end
 | 
						|
end
 |